Compare commits
29 Commits
speech-inp
...
upstream/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50ccae8b27 | ||
|
|
f1ba699f9f | ||
|
|
0cb1c05903 | ||
|
|
b7ed232688 | ||
|
|
a442d53efa | ||
|
|
8c0a82d3a8 | ||
|
|
57efe5def3 | ||
|
|
3710df916f | ||
|
|
6f15ba2051 | ||
|
|
695c3fa954 | ||
|
|
d735b189f5 | ||
|
|
3d575f4f68 | ||
|
|
b58728dc0e | ||
|
|
672177f570 | ||
|
|
6961efde0b | ||
|
|
b3e0233f4b | ||
|
|
fcebcb0174 | ||
|
|
eaab5e2e9f | ||
|
|
b12825f923 | ||
|
|
8245f474b8 | ||
|
|
3a15b311a8 | ||
|
|
6cb6c0af32 | ||
|
|
7f631611fd | ||
|
|
9d91ecc649 | ||
|
|
87afb06d34 | ||
|
|
4402d9afb0 | ||
|
|
7c3f808d69 | ||
|
|
a59e929b12 | ||
|
|
8ff4019839 |
52
.github/workflows/pr-build.yml
vendored
Normal file
52
.github/workflows/pr-build.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: PR Build Validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pr-build-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
authorize:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
allowed: ${{ steps.auth.outputs.allowed }}
|
||||||
|
env:
|
||||||
|
ALLOWED_ACTORS: ${{ vars.ALLOWED_NON_DEV_PR_ACTORS }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||||
|
steps:
|
||||||
|
- name: Check PR authorization
|
||||||
|
id: auth
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "$BASE_REF" = "dev" ]; then
|
||||||
|
echo "allowed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
normalized=",${ALLOWED_ACTORS},"
|
||||||
|
if [[ "$normalized" == *",${ACTOR},"* ]]; then
|
||||||
|
echo "allowed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "allowed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Skipping builds for unauthorized PR targeting $BASE_REF" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: authorize
|
||||||
|
if: ${{ needs.authorize.outputs.allowed == 'true' }}
|
||||||
|
uses: ./.github/workflows/build-and-upload.yml
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
upload: false
|
||||||
|
set_versions: false
|
||||||
54
.github/workflows/restrict-non-dev-prs.yml
vendored
Normal file
54
.github/workflows/restrict-non-dev-prs.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: Restrict Non-Dev PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
restrict-non-dev-prs:
|
||||||
|
if: ${{ github.event.pull_request.base.ref != 'dev' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
ALLOWED_ACTORS: ${{ vars.ALLOWED_NON_DEV_PR_ACTORS }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||||
|
steps:
|
||||||
|
- name: Check allowed actor
|
||||||
|
id: auth
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
normalized=",${ALLOWED_ACTORS},"
|
||||||
|
if [[ "$normalized" == *",${ACTOR},"* ]]; then
|
||||||
|
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "authorized=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Comment on unauthorized PR
|
||||||
|
if: ${{ steps.auth.outputs.authorized != 'true' }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
gh pr comment "$PR_NUMBER" --body "Thanks for the contribution. PRs need to target \`dev\` branch. Please retarget this PR to the dev branch"
|
||||||
|
|
||||||
|
- name: Close unauthorized PR
|
||||||
|
if: ${{ steps.auth.outputs.authorized != 'true' }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
gh pr close "$PR_NUMBER"
|
||||||
|
|
||||||
|
- name: Fail unauthorized PR
|
||||||
|
if: ${{ steps.auth.outputs.authorized != 'true' }}
|
||||||
|
run: |
|
||||||
|
echo "Actor $ACTOR is not allowed to open PRs targeting $BASE_REF" >&2
|
||||||
|
exit 1
|
||||||
38
package-lock.json
generated
38
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
@@ -3253,9 +3253,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/api": {
|
"node_modules/@tauri-apps/api": {
|
||||||
"version": "2.9.1",
|
"version": "2.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
|
||||||
"integrity": "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==",
|
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3322,6 +3322,15 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-dialog": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-notification": {
|
"node_modules/@tauri-apps/plugin-notification": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
||||||
@@ -10235,14 +10244,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/tauri-plugin-keepawake-api": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tauri-plugin-keepawake-api/-/tauri-plugin-keepawake-api-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-XPUl66zUYiB7kCRxsTdmCoNjFM/++NWCJ4kdTo2NUOgBUa8UVYfayDWnnTzGIQbhT7qNAHs+jgKSjhqSKs/QHA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@tauri-apps/api": ">=2.0.0-beta.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/temp-dir": {
|
"node_modules/temp-dir": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
||||||
@@ -12002,7 +12003,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
@@ -12039,7 +12040,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
@@ -12080,7 +12081,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
@@ -12088,7 +12089,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
@@ -12098,6 +12099,8 @@
|
|||||||
"@suid/icons-material": "^0.9.0",
|
"@suid/icons-material": "^0.9.0",
|
||||||
"@suid/material": "^0.19.0",
|
"@suid/material": "^0.19.0",
|
||||||
"@suid/system": "^0.14.0",
|
"@suid/system": "^0.14.0",
|
||||||
|
"@tauri-apps/api": "^2.10.1",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||||
"ansi-sequence-parser": "^1.1.3",
|
"ansi-sequence-parser": "^1.1.3",
|
||||||
@@ -12110,7 +12113,6 @@
|
|||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
"tauri-plugin-keepawake-api": "^0.1.0",
|
|
||||||
"yaml": "^2.4.2"
|
"yaml": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"minServerVersion": "0.11.4",
|
"minServerVersion": "0.12.3",
|
||||||
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.2.14"
|
"@opencode-ai/plugin": "1.2.25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
2433
packages/tauri-app/Cargo.lock
generated
2433
packages/tauri-app/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const serverDevInstallCommand =
|
|||||||
"npm install --workspace @neuralnomads/codenomad --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
|
"npm install --workspace @neuralnomads/codenomad --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
|
||||||
const uiDevInstallCommand =
|
const uiDevInstallCommand =
|
||||||
"npm install --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
|
"npm install --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
|
||||||
|
const serverPrepareUiCommand = "npm run prepare-ui --workspace @neuralnomads/codenomad"
|
||||||
|
|
||||||
const envWithRootBin = {
|
const envWithRootBin = {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -91,6 +92,15 @@ function ensureUiBuild() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncServerUiBundle() {
|
||||||
|
console.log("[prebuild] syncing server public UI bundle...")
|
||||||
|
execSync(serverPrepareUiCommand, {
|
||||||
|
cwd: workspaceRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: envWithRootBin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function ensureServerDevDependencies() {
|
function ensureServerDevDependencies() {
|
||||||
if (fs.existsSync(braceExpansionPath)) {
|
if (fs.existsSync(braceExpansionPath)) {
|
||||||
return
|
return
|
||||||
@@ -246,6 +256,7 @@ function copyUiLoadingAssets() {
|
|||||||
ensureServerDependencies()
|
ensureServerDependencies()
|
||||||
ensureServerBuild()
|
ensureServerBuild()
|
||||||
ensureUiBuild()
|
ensureUiBuild()
|
||||||
|
syncServerUiBundle()
|
||||||
copyServerArtifacts()
|
copyServerArtifacts()
|
||||||
stripNodeModuleBins()
|
stripNodeModuleBins()
|
||||||
copyUiLoadingAssets()
|
copyUiLoadingAssets()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "codenomad-tauri"
|
name = "codenomad-tauri"
|
||||||
version = "0.1.0"
|
version = "0.12.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
@@ -19,9 +19,12 @@ thiserror = "1"
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
which = "4"
|
which = "4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
keepawake = "0.6"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
url = "2"
|
url = "2"
|
||||||
tauri-plugin-keepawake = "0.1.1"
|
|
||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows-sys = { version = "0.59", features = ["Win32_UI_Shell"] }
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2378,36 +2378,6 @@
|
|||||||
"const": "dialog:deny-save",
|
"const": "dialog:deny-save",
|
||||||
"markdownDescription": "Denies the save command without any pre-configured scope."
|
"markdownDescription": "Denies the save command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start`\n- `allow-stop`",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:default",
|
|
||||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start`\n- `allow-stop`"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Enables the start command without any pre-configured scope.",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:allow-start",
|
|
||||||
"markdownDescription": "Enables the start command without any pre-configured scope."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Enables the stop command without any pre-configured scope.",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:allow-stop",
|
|
||||||
"markdownDescription": "Enables the stop command without any pre-configured scope."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Denies the start command without any pre-configured scope.",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:deny-start",
|
|
||||||
"markdownDescription": "Denies the start command without any pre-configured scope."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Denies the stop command without any pre-configured scope.",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:deny-stop",
|
|
||||||
"markdownDescription": "Denies the stop command without any pre-configured scope."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -2378,36 +2378,6 @@
|
|||||||
"const": "dialog:deny-save",
|
"const": "dialog:deny-save",
|
||||||
"markdownDescription": "Denies the save command without any pre-configured scope."
|
"markdownDescription": "Denies the save command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start`\n- `allow-stop`",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:default",
|
|
||||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start`\n- `allow-stop`"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Enables the start command without any pre-configured scope.",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:allow-start",
|
|
||||||
"markdownDescription": "Enables the start command without any pre-configured scope."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Enables the stop command without any pre-configured scope.",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:allow-stop",
|
|
||||||
"markdownDescription": "Enables the stop command without any pre-configured scope."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Denies the start command without any pre-configured scope.",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:deny-start",
|
|
||||||
"markdownDescription": "Denies the start command without any pre-configured scope."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Denies the stop command without any pre-configured scope.",
|
|
||||||
"type": "string",
|
|
||||||
"const": "keepawake:deny-stop",
|
|
||||||
"markdownDescription": "Denies the stop command without any pre-configured scope."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use std::ffi::OsStr;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{BufRead, BufReader, Read, Write};
|
use std::io::{BufRead, BufReader, Read, Write};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
@@ -17,10 +19,24 @@ use std::thread;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tauri::{webview::cookie::Cookie, AppHandle, Emitter, Manager, Url};
|
use tauri::{webview::cookie::Cookie, AppHandle, Emitter, Manager, Url};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
||||||
fn log_line(message: &str) {
|
fn log_line(message: &str) {
|
||||||
println!("[tauri-cli] {message}");
|
println!("[tauri-cli] {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn configure_spawn(command: &mut Command) {
|
||||||
|
command.creation_flags(CREATE_NO_WINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn configure_spawn(_command: &mut Command) {}
|
||||||
|
|
||||||
fn workspace_root() -> Option<PathBuf> {
|
fn workspace_root() -> Option<PathBuf> {
|
||||||
std::env::current_dir().ok().and_then(|mut dir| {
|
std::env::current_dir().ok().and_then(|mut dir| {
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
@@ -36,6 +52,46 @@ const SESSION_COOKIE_NAME: &str = "codenomad_session";
|
|||||||
|
|
||||||
const CLI_STOP_GRACE_SECS: u64 = 30;
|
const CLI_STOP_GRACE_SECS: u64 = 30;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn configure_posix_process_group(command: &mut Command) {
|
||||||
|
// Ensure the CLI runs in its own process group so we can terminate wrapper
|
||||||
|
// processes (login shell/tsx) without leaving the server orphaned.
|
||||||
|
unsafe {
|
||||||
|
command.pre_exec(|| {
|
||||||
|
if libc::setpgid(0, 0) != 0 {
|
||||||
|
return Err(std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn kill_process_tree_windows(pid: u32, force: bool) -> bool {
|
||||||
|
let mut args = vec!["/PID".to_string(), pid.to_string(), "/T".to_string()];
|
||||||
|
if force {
|
||||||
|
args.push("/F".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut command = Command::new("taskkill");
|
||||||
|
command.args(&args);
|
||||||
|
configure_spawn(&mut command);
|
||||||
|
|
||||||
|
match command.output() {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the PID is already gone, treat it as success.
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase();
|
||||||
|
let combined = format!("{stdout}\n{stderr}");
|
||||||
|
combined.contains("not found") || combined.contains("no running instance")
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
fn navigate_main(app: &AppHandle, url: &str) {
|
fn navigate_main(app: &AppHandle, url: &str) {
|
||||||
if let Some(win) = app.webview_windows().get("main") {
|
if let Some(win) = app.webview_windows().get("main") {
|
||||||
let mut display = url.to_string();
|
let mut display = url.to_string();
|
||||||
@@ -348,11 +404,19 @@ impl CliProcessManager {
|
|||||||
log_line(&format!("stopping CLI pid={}", child.id()));
|
log_line(&format!("stopping CLI pid={}", child.id()));
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::kill(child.id() as i32, libc::SIGTERM);
|
let pid = child.id() as i32;
|
||||||
|
// Prefer signaling the process group to avoid orphaning children
|
||||||
|
// when the CLI was launched via a wrapper shell.
|
||||||
|
let group_res = libc::kill(-pid, libc::SIGTERM);
|
||||||
|
if group_res != 0 {
|
||||||
|
let _ = libc::kill(pid, libc::SIGTERM);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let _ = child.kill();
|
if !kill_process_tree_windows(child.id(), false) {
|
||||||
|
let _ = child.kill();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
@@ -368,11 +432,17 @@ impl CliProcessManager {
|
|||||||
));
|
));
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::kill(child.id() as i32, libc::SIGKILL);
|
let pid = child.id() as i32;
|
||||||
|
let group_res = libc::kill(-pid, libc::SIGKILL);
|
||||||
|
if group_res != 0 {
|
||||||
|
let _ = libc::kill(pid, libc::SIGKILL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let _ = child.kill();
|
if !kill_process_tree_windows(child.id(), true) {
|
||||||
|
let _ = child.kill();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -450,9 +520,12 @@ impl CliProcessManager {
|
|||||||
.env("ELECTRON_RUN_AS_NODE", "1")
|
.env("ELECTRON_RUN_AS_NODE", "1")
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped());
|
.stderr(Stdio::piped());
|
||||||
|
configure_spawn(&mut c);
|
||||||
if let Some(ref cwd) = cwd {
|
if let Some(ref cwd) = cwd {
|
||||||
c.current_dir(cwd);
|
c.current_dir(cwd);
|
||||||
}
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
configure_posix_process_group(&mut c);
|
||||||
c.spawn()?
|
c.spawn()?
|
||||||
}
|
}
|
||||||
ShellCommandType::Direct(cmd) => {
|
ShellCommandType::Direct(cmd) => {
|
||||||
@@ -462,9 +535,12 @@ impl CliProcessManager {
|
|||||||
.env("ELECTRON_RUN_AS_NODE", "1")
|
.env("ELECTRON_RUN_AS_NODE", "1")
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped());
|
.stderr(Stdio::piped());
|
||||||
|
configure_spawn(&mut c);
|
||||||
if let Some(ref cwd) = cwd {
|
if let Some(ref cwd) = cwd {
|
||||||
c.current_dir(cwd);
|
c.current_dir(cwd);
|
||||||
}
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
configure_posix_process_group(&mut c);
|
||||||
c.spawn()?
|
c.spawn()?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -537,7 +613,24 @@ impl CliProcessManager {
|
|||||||
locked.error = Some("CLI did not start in time".to_string());
|
locked.error = Some("CLI did not start in time".to_string());
|
||||||
log_line("timeout waiting for CLI readiness");
|
log_line("timeout waiting for CLI readiness");
|
||||||
if let Some(child) = child_holder_clone.lock().as_mut() {
|
if let Some(child) = child_holder_clone.lock().as_mut() {
|
||||||
let _ = child.kill();
|
#[cfg(unix)]
|
||||||
|
unsafe {
|
||||||
|
let pid = child.id() as i32;
|
||||||
|
let group_res = libc::kill(-pid, libc::SIGKILL);
|
||||||
|
if group_res != 0 {
|
||||||
|
let _ = libc::kill(pid, libc::SIGKILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
if !kill_process_tree_windows(child.id(), true) {
|
||||||
|
let _ = child.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
{
|
||||||
|
let _ = child.kill();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let _ = app_clone.emit("cli:error", json!({"message": "CLI did not start in time"}));
|
let _ = app_clone.emit("cli:error", json!({"message": "CLI did not start in time"}));
|
||||||
Self::emit_status(&app_clone, &locked);
|
Self::emit_status(&app_clone, &locked);
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
mod cli_manager;
|
mod cli_manager;
|
||||||
|
|
||||||
use cli_manager::{CliProcessManager, CliStatus};
|
use cli_manager::{CliProcessManager, CliStatus};
|
||||||
|
use keepawake::KeepAwake;
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Mutex;
|
||||||
use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder};
|
use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder};
|
||||||
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
|
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
|
||||||
use tauri::webview::Webview;
|
use tauri::webview::Webview;
|
||||||
@@ -12,11 +15,31 @@ use tauri::{AppHandle, Emitter, Manager, Runtime, Wry};
|
|||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::iter;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
||||||
|
|
||||||
static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[cfg(windows)]
|
||||||
|
const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub manager: CliProcessManager,
|
pub manager: CliProcessManager,
|
||||||
|
pub wake_lock: Mutex<Option<KeepAwake>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
struct WakeLockConfig {
|
||||||
|
display: bool,
|
||||||
|
idle: bool,
|
||||||
|
sleep: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -35,6 +58,39 @@ fn cli_restart(app: AppHandle, state: tauri::State<AppState>) -> Result<CliStatu
|
|||||||
Ok(state.manager.status())
|
Ok(state.manager.status())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn wake_lock_start(
|
||||||
|
state: tauri::State<AppState>,
|
||||||
|
config: Option<WakeLockConfig>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let config = config.unwrap_or(WakeLockConfig {
|
||||||
|
display: true,
|
||||||
|
idle: false,
|
||||||
|
sleep: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut builder = keepawake::Builder::default();
|
||||||
|
builder
|
||||||
|
.display(config.display)
|
||||||
|
.idle(config.idle)
|
||||||
|
.sleep(config.sleep)
|
||||||
|
.reason("CodeNomad active session")
|
||||||
|
.app_name("CodeNomad")
|
||||||
|
.app_reverse_domain("ai.neuralnomads.codenomad.client");
|
||||||
|
|
||||||
|
let wake_lock = builder.create().map_err(|err| err.to_string())?;
|
||||||
|
let mut state_lock = state.wake_lock.lock().map_err(|err| err.to_string())?;
|
||||||
|
*state_lock = Some(wake_lock);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn wake_lock_stop(state: tauri::State<AppState>) -> Result<(), String> {
|
||||||
|
let mut state_lock = state.wake_lock.lock().map_err(|err| err.to_string())?;
|
||||||
|
state_lock.take();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn is_dev_mode() -> bool {
|
fn is_dev_mode() -> bool {
|
||||||
cfg!(debug_assertions) || std::env::var("TAURI_DEV").is_ok()
|
cfg!(debug_assertions) || std::env::var("TAURI_DEV").is_ok()
|
||||||
}
|
}
|
||||||
@@ -101,6 +157,22 @@ fn emit_folder_drop_event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn set_windows_app_user_model_id() {
|
||||||
|
let app_id: Vec<u16> = OsStr::new(WINDOWS_APP_USER_MODEL_ID)
|
||||||
|
.encode_wide()
|
||||||
|
.chain(iter::once(0))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let result = unsafe { SetCurrentProcessExplicitAppUserModelID(app_id.as_ptr()) };
|
||||||
|
if result < 0 {
|
||||||
|
eprintln!("[tauri] failed to set AppUserModelID: {result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn set_windows_app_user_model_id() {}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let navigation_guard: TauriPlugin<Wry, ()> = PluginBuilder::new("external-link-guard")
|
let navigation_guard: TauriPlugin<Wry, ()> = PluginBuilder::new("external-link-guard")
|
||||||
.on_navigation(|webview, url| intercept_navigation(webview, url))
|
.on_navigation(|webview, url| intercept_navigation(webview, url))
|
||||||
@@ -109,13 +181,14 @@ fn main() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_keepawake::init())
|
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(navigation_guard)
|
.plugin(navigation_guard)
|
||||||
.manage(AppState {
|
.manage(AppState {
|
||||||
manager: CliProcessManager::new(),
|
manager: CliProcessManager::new(),
|
||||||
|
wake_lock: Mutex::new(None),
|
||||||
})
|
})
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
set_windows_app_user_model_id();
|
||||||
build_menu(&app.handle())?;
|
build_menu(&app.handle())?;
|
||||||
let dev_mode = is_dev_mode();
|
let dev_mode = is_dev_mode();
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
@@ -127,7 +200,12 @@ fn main() {
|
|||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![cli_get_status, cli_restart])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
cli_get_status,
|
||||||
|
cli_restart,
|
||||||
|
wake_lock_start,
|
||||||
|
wake_lock_stop
|
||||||
|
])
|
||||||
.on_menu_event(|app_handle, event| {
|
.on_menu_event(|app_handle, event| {
|
||||||
match event.id().0.as_str() {
|
match event.id().0.as_str() {
|
||||||
// File menu
|
// File menu
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "CodeNomad",
|
"productName": "CodeNomad",
|
||||||
"version": "0.1.0",
|
"version": "0.12.3",
|
||||||
"identifier": "ai.opencode.client",
|
"identifier": "ai.neuralnomads.codenomad.client",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev:bootstrap",
|
"beforeDevCommand": "npm run dev:bootstrap",
|
||||||
"beforeBuildCommand": "npm run bundle:server",
|
"beforeBuildCommand": "npm run bundle:server",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.12.2",
|
"version": "0.12.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -18,8 +18,10 @@
|
|||||||
"@suid/icons-material": "^0.9.0",
|
"@suid/icons-material": "^0.9.0",
|
||||||
"@suid/material": "^0.19.0",
|
"@suid/material": "^0.19.0",
|
||||||
"@suid/system": "^0.14.0",
|
"@suid/system": "^0.14.0",
|
||||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||||
"ansi-sequence-parser": "^1.1.3",
|
"ansi-sequence-parser": "^1.1.3",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
@@ -30,7 +32,6 @@
|
|||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
"tauri-plugin-keepawake-api": "^0.1.0",
|
|
||||||
"yaml": "^2.4.2"
|
"yaml": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import { createContext, createEffect, createMemo, createSignal, onCleanup, onMou
|
|||||||
import type { ParentComponent } from "solid-js"
|
import type { ParentComponent } from "solid-js"
|
||||||
import { useConfig } from "../../stores/preferences"
|
import { useConfig } from "../../stores/preferences"
|
||||||
import { enMessages } from "./messages/en"
|
import { enMessages } from "./messages/en"
|
||||||
import { esMessages } from "./messages/es"
|
|
||||||
import { frMessages } from "./messages/fr"
|
|
||||||
import { ruMessages } from "./messages/ru"
|
|
||||||
import { jaMessages } from "./messages/ja"
|
|
||||||
import { zhHansMessages } from "./messages/zh-Hans"
|
|
||||||
|
|
||||||
type Messages = Record<string, string>
|
type Messages = Record<string, string>
|
||||||
|
|
||||||
@@ -15,14 +10,18 @@ export type TranslateParams = Record<string, unknown>
|
|||||||
export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans"
|
export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans"
|
||||||
|
|
||||||
const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const
|
const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const
|
||||||
|
const SUPPORTED_LOCALES_BY_LOWER = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale]))
|
||||||
|
|
||||||
const messagesByLocale: Record<Locale, Messages> = {
|
const localeMessagesCache = new Map<Locale, Messages>([["en", enMessages]])
|
||||||
en: enMessages,
|
const localeMessagesPromises = new Map<Locale, Promise<Messages>>()
|
||||||
es: esMessages,
|
|
||||||
fr: frMessages,
|
const localeLoaders: Record<Locale, () => Promise<Messages>> = {
|
||||||
ru: ruMessages,
|
en: async () => enMessages,
|
||||||
ja: jaMessages,
|
es: async () => (await import("./messages/es")).esMessages,
|
||||||
"zh-Hans": zhHansMessages,
|
fr: async () => (await import("./messages/fr")).frMessages,
|
||||||
|
ru: async () => (await import("./messages/ru")).ruMessages,
|
||||||
|
ja: async () => (await import("./messages/ja")).jaMessages,
|
||||||
|
"zh-Hans": async () => (await import("./messages/zh-Hans")).zhHansMessages,
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLocaleTag(value: string): string {
|
function normalizeLocaleTag(value: string): string {
|
||||||
@@ -34,8 +33,7 @@ function matchSupportedLocale(value: string | undefined): Locale | null {
|
|||||||
|
|
||||||
const normalized = normalizeLocaleTag(value)
|
const normalized = normalizeLocaleTag(value)
|
||||||
const lower = normalized.toLowerCase()
|
const lower = normalized.toLowerCase()
|
||||||
const supportedLower = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale]))
|
const exact = SUPPORTED_LOCALES_BY_LOWER.get(lower)
|
||||||
const exact = supportedLower.get(lower)
|
|
||||||
if (exact) return exact
|
if (exact) return exact
|
||||||
|
|
||||||
const parts = lower.split("-")
|
const parts = lower.split("-")
|
||||||
@@ -43,11 +41,11 @@ function matchSupportedLocale(value: string | undefined): Locale | null {
|
|||||||
if (!base) return null
|
if (!base) return null
|
||||||
|
|
||||||
if (base === "zh") {
|
if (base === "zh") {
|
||||||
const zhHans = supportedLower.get("zh-hans")
|
const zhHans = SUPPORTED_LOCALES_BY_LOWER.get("zh-hans")
|
||||||
return zhHans ?? null
|
return zhHans ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseMatch = supportedLower.get(base)
|
const baseMatch = SUPPORTED_LOCALES_BY_LOWER.get(base)
|
||||||
return baseMatch ?? null
|
return baseMatch ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +82,54 @@ function translateFrom(messages: Messages, key: string, params?: TranslateParams
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [globalRevision, setGlobalRevision] = createSignal(0)
|
const [globalRevision, setGlobalRevision] = createSignal(0)
|
||||||
const initialGlobalLocale: Locale = detectNavigatorLocale() ?? "en"
|
let globalMessages: Messages = enMessages
|
||||||
let globalMessages: Messages = messagesByLocale[initialGlobalLocale]
|
let globalLocale: Locale = "en"
|
||||||
|
|
||||||
|
function getMessagesForLocale(locale: Locale): Messages {
|
||||||
|
return localeMessagesCache.get(locale) ?? enMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLocaleMessages(locale: Locale): Promise<Messages> {
|
||||||
|
const cached = localeMessagesCache.get(locale)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = localeMessagesPromises.get(locale)
|
||||||
|
if (pending) {
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = localeLoaders[locale]
|
||||||
|
const promise = loader()
|
||||||
|
.then((messages) => {
|
||||||
|
localeMessagesCache.set(locale, messages)
|
||||||
|
localeMessagesPromises.delete(locale)
|
||||||
|
return messages
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
localeMessagesPromises.delete(locale)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
localeMessagesPromises.set(locale, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preloadLocaleMessages(preferredLocale?: string | null): Promise<Locale> {
|
||||||
|
const resolvedLocale = matchSupportedLocale(preferredLocale ?? undefined) ?? detectNavigatorLocale() ?? "en"
|
||||||
|
try {
|
||||||
|
globalMessages = await loadLocaleMessages(resolvedLocale)
|
||||||
|
globalLocale = resolvedLocale
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
return resolvedLocale
|
||||||
|
} catch {
|
||||||
|
globalMessages = enMessages
|
||||||
|
globalLocale = "en"
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function tGlobal(key: string, params?: TranslateParams): string {
|
export function tGlobal(key: string, params?: TranslateParams): string {
|
||||||
globalRevision()
|
globalRevision()
|
||||||
@@ -101,9 +145,10 @@ const I18nContext = createContext<I18nContextValue>()
|
|||||||
|
|
||||||
export const I18nProvider: ParentComponent = (props) => {
|
export const I18nProvider: ParentComponent = (props) => {
|
||||||
const { preferences } = useConfig()
|
const { preferences } = useConfig()
|
||||||
const [detectedLocale, setDetectedLocale] = createSignal<Locale>("en")
|
const [detectedLocale, setDetectedLocale] = createSignal<Locale>(globalLocale)
|
||||||
|
const [resolvedLocale, setResolvedLocale] = createSignal<Locale>(globalLocale)
|
||||||
const previousMessages = globalMessages
|
const previousGlobalMessages = globalMessages
|
||||||
|
const previousGlobalLocale = globalLocale
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const detected = detectNavigatorLocale()
|
const detected = detectNavigatorLocale()
|
||||||
@@ -115,19 +160,44 @@ export const I18nProvider: ParentComponent = (props) => {
|
|||||||
return configured ?? detectedLocale() ?? "en"
|
return configured ?? detectedLocale() ?? "en"
|
||||||
})
|
})
|
||||||
|
|
||||||
const messages = createMemo<Messages>(() => messagesByLocale[locale()])
|
const messages = createMemo<Messages>(() => getMessagesForLocale(resolvedLocale()))
|
||||||
|
|
||||||
function t(key: string, params?: TranslateParams): string {
|
function t(key: string, params?: TranslateParams): string {
|
||||||
return translateFrom(messages(), key, params)
|
return translateFrom(messages(), key, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
globalMessages = messages()
|
const nextLocale = locale()
|
||||||
setGlobalRevision((value) => value + 1)
|
let cancelled = false
|
||||||
|
|
||||||
|
void loadLocaleMessages(nextLocale)
|
||||||
|
.then((loadedMessages) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setResolvedLocale(nextLocale)
|
||||||
|
globalLocale = nextLocale
|
||||||
|
globalMessages = loadedMessages
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setResolvedLocale("en")
|
||||||
|
globalMessages = enMessages
|
||||||
|
globalLocale = "en"
|
||||||
|
setGlobalRevision((value) => value + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
cancelled = true
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
globalMessages = previousMessages
|
globalMessages = previousGlobalMessages
|
||||||
|
globalLocale = previousGlobalLocale
|
||||||
setGlobalRevision((value) => value + 1)
|
setGlobalRevision((value) => value + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
import { runtimeEnv } from "../runtime-env"
|
import { runtimeEnv } from "../runtime-env"
|
||||||
import { getLogger } from "../logger"
|
import { getLogger } from "../logger"
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
@@ -15,9 +16,8 @@ export async function restartCli(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (runtimeEnv.host === "tauri") {
|
if (runtimeEnv.host === "tauri") {
|
||||||
const tauri = (window as typeof window & { __TAURI__?: { invoke?: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T> } }).__TAURI__
|
if (typeof window.__TAURI__?.core?.invoke === "function") {
|
||||||
if (tauri?.invoke) {
|
await invoke("cli_restart")
|
||||||
await tauri.invoke("cli_restart")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { listen } from "@tauri-apps/api/event"
|
||||||
import { getLogger } from "../logger"
|
import { getLogger } from "../logger"
|
||||||
import { runtimeEnv } from "../runtime-env"
|
import { runtimeEnv } from "../runtime-env"
|
||||||
|
|
||||||
@@ -107,13 +108,8 @@ export async function listenForNativeFolderDrops(onDrop: (paths: string[]) => vo
|
|||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventApi = window.__TAURI__?.event
|
|
||||||
if (!eventApi?.listen) {
|
|
||||||
return () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const unlisten = await eventApi.listen("desktop:folder-drop", (event) => {
|
const unlisten = await listen("desktop:folder-drop", (event) => {
|
||||||
const payload = (event.payload ?? {}) as TauriFolderDropPayload
|
const payload = (event.payload ?? {}) as TauriFolderDropPayload
|
||||||
const paths = normalizePathList(payload.paths)
|
const paths = normalizePathList(payload.paths)
|
||||||
if (paths.length > 0) {
|
if (paths.length > 0) {
|
||||||
@@ -134,15 +130,10 @@ export async function listenForNativeFolderDropState(onState: (state: NativeFold
|
|||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventApi = window.__TAURI__?.event
|
|
||||||
if (!eventApi?.listen) {
|
|
||||||
return () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [unlistenEnter, unlistenLeave] = await Promise.all([
|
const [unlistenEnter, unlistenLeave] = await Promise.all([
|
||||||
eventApi.listen("desktop:folder-drag-enter", () => onState("enter")),
|
listen("desktop:folder-drag-enter", () => onState("enter")),
|
||||||
eventApi.listen("desktop:folder-drag-leave", () => onState("leave")),
|
listen("desktop:folder-drag-leave", () => onState("leave")),
|
||||||
])
|
])
|
||||||
return () => {
|
return () => {
|
||||||
unlistenEnter()
|
unlistenEnter()
|
||||||
|
|||||||
@@ -1,43 +1,21 @@
|
|||||||
|
import { open } from "@tauri-apps/plugin-dialog"
|
||||||
import type { NativeDialogOptions } from "../native-functions"
|
import type { NativeDialogOptions } from "../native-functions"
|
||||||
import { getLogger } from "../../logger"
|
import { getLogger } from "../../logger"
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
|
|
||||||
interface TauriDialogModule {
|
|
||||||
open?: (
|
|
||||||
options: {
|
|
||||||
title?: string
|
|
||||||
defaultPath?: string
|
|
||||||
filters?: { name?: string; extensions: string[] }[]
|
|
||||||
directory?: boolean
|
|
||||||
multiple?: boolean
|
|
||||||
},
|
|
||||||
) => Promise<string | string[] | null>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TauriBridge {
|
|
||||||
dialog?: TauriDialogModule
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function openTauriNativeDialog(options: NativeDialogOptions): Promise<string | null> {
|
export async function openTauriNativeDialog(options: NativeDialogOptions): Promise<string | null> {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const tauriBridge = (window as Window & { __TAURI__?: TauriBridge }).__TAURI__
|
|
||||||
const dialogApi = tauriBridge?.dialog
|
|
||||||
if (!dialogApi?.open) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await dialogApi.open({
|
const response = await open({
|
||||||
title: options.title,
|
title: options.title,
|
||||||
defaultPath: options.defaultPath,
|
defaultPath: options.defaultPath,
|
||||||
directory: options.mode === "directory",
|
directory: options.mode === "directory",
|
||||||
multiple: false,
|
multiple: false,
|
||||||
filters: options.filters?.map((filter) => ({
|
filters: options.filters?.map((filter) => ({
|
||||||
name: filter.name,
|
name: filter.name ?? "Files",
|
||||||
extensions: filter.extensions,
|
extensions: filter.extensions,
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
import { runtimeEnv } from "../runtime-env"
|
import { runtimeEnv } from "../runtime-env"
|
||||||
import { getLogger } from "../logger"
|
import { getLogger } from "../logger"
|
||||||
|
|
||||||
@@ -60,8 +61,7 @@ function hasAnyWakeLockSupport(): boolean {
|
|||||||
if (api?.setWakeLock) return true
|
if (api?.setWakeLock) return true
|
||||||
}
|
}
|
||||||
if (runtimeEnv.host === "tauri") {
|
if (runtimeEnv.host === "tauri") {
|
||||||
// We'll attempt dynamic import; treat as potentially supported.
|
return typeof window.__TAURI__?.core?.invoke === "function"
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
return Boolean((navigator as any)?.wakeLock?.request)
|
return Boolean((navigator as any)?.wakeLock?.request)
|
||||||
}
|
}
|
||||||
@@ -84,21 +84,18 @@ async function setElectronWakeLock(enabled: boolean): Promise<boolean> {
|
|||||||
|
|
||||||
async function setTauriWakeLock(enabled: boolean): Promise<boolean> {
|
async function setTauriWakeLock(enabled: boolean): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const mod = await import("tauri-plugin-keepawake-api")
|
if (!hasAnyWakeLockSupport()) {
|
||||||
const start = (mod as any).start as ((config?: any) => Promise<void>) | undefined
|
|
||||||
const stop = (mod as any).stop as (() => Promise<void>) | undefined
|
|
||||||
if (!start || !stop) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
// Plugin config supports toggling display/idle/sleep. Use a conservative
|
// Match Electron's prevent-display-sleep behavior by keeping the display
|
||||||
// default to keep both system + display awake.
|
// awake without blocking explicit system sleep requests.
|
||||||
await start({ display: true, idle: true, sleep: true })
|
await invoke("wake_lock_start", { config: { display: true, idle: false, sleep: false } })
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
await stop()
|
await invoke("wake_lock_stop")
|
||||||
return false
|
return false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.log("[wake-lock] tauri wake lock failed", error)
|
log.log("[wake-lock] tauri wake lock failed", error)
|
||||||
@@ -137,13 +134,12 @@ export function setWakeLockDesired(nextDesired: boolean): Promise<boolean> {
|
|||||||
inFlight = (async () => {
|
inFlight = (async () => {
|
||||||
try {
|
try {
|
||||||
const ok = await applyWakeLock(target)
|
const ok = await applyWakeLock(target)
|
||||||
// Treat disable attempts as applied even if the underlying API doesn't exist.
|
applied = target ? ok : false
|
||||||
applied = target
|
|
||||||
return ok
|
return ok
|
||||||
} finally {
|
} finally {
|
||||||
inFlight = null
|
inFlight = null
|
||||||
// If desired changed while in-flight, re-apply once.
|
// If desired changed while in-flight, re-apply once.
|
||||||
if (desired !== applied) {
|
if (desired !== target) {
|
||||||
void setWakeLockDesired(desired)
|
void setWakeLockDesired(desired)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,14 @@ export interface RuntimeEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
interface TauriCoreModule {
|
||||||
|
invoke: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI?: unknown
|
electronAPI?: unknown
|
||||||
__TAURI__?: {
|
__TAURI__?: {
|
||||||
invoke?: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T>
|
core?: TauriCoreModule
|
||||||
event?: {
|
|
||||||
listen: (event: string, handler: (payload: { payload: unknown }) => void) => Promise<() => void>
|
|
||||||
}
|
|
||||||
dialog?: {
|
|
||||||
open?: (options: Record<string, unknown>) => Promise<string | string[] | null>
|
|
||||||
save?: (options: Record<string, unknown>) => Promise<string | null>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ThemeProvider } from "./lib/theme"
|
|||||||
import { ConfigProvider } from "./stores/preferences"
|
import { ConfigProvider } from "./stores/preferences"
|
||||||
import { InstanceConfigProvider } from "./stores/instance-config"
|
import { InstanceConfigProvider } from "./stores/instance-config"
|
||||||
import { runtimeEnv } from "./lib/runtime-env"
|
import { runtimeEnv } from "./lib/runtime-env"
|
||||||
import { I18nProvider } from "./lib/i18n"
|
import { I18nProvider, preloadLocaleMessages } from "./lib/i18n"
|
||||||
import { storage } from "./lib/storage"
|
import { storage } from "./lib/storage"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||||
@@ -31,15 +31,19 @@ async function bootstrap() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const uiConfig = await storage.loadConfigOwner("ui")
|
const uiConfig = await storage.loadConfigOwner("ui")
|
||||||
const theme = (uiConfig as any)?.theme ?? "system"
|
const theme = (uiConfig as any)?.theme
|
||||||
|
const locale = typeof (uiConfig as any)?.settings?.locale === "string" ? (uiConfig as any).settings.locale : undefined
|
||||||
|
|
||||||
if (theme === "system") {
|
if (theme === "light" || theme === "dark") {
|
||||||
document.documentElement.removeAttribute("data-theme")
|
|
||||||
} else {
|
|
||||||
document.documentElement.setAttribute("data-theme", theme)
|
document.documentElement.setAttribute("data-theme", theme)
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute("data-theme")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await preloadLocaleMessages(locale)
|
||||||
} catch {
|
} catch {
|
||||||
// If config fails to load, fall back to CSS defaults.
|
// If config fails to load, fall back to CSS defaults.
|
||||||
|
await preloadLocaleMessages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import { listen } from "@tauri-apps/api/event"
|
||||||
import { Show, createSignal, onCleanup, onMount } from "solid-js"
|
import { Show, createSignal, onCleanup, onMount } from "solid-js"
|
||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import iconUrl from "../../images/CodeNomad-Icon.png"
|
import iconUrl from "../../images/CodeNomad-Icon.png"
|
||||||
@@ -27,13 +29,6 @@ interface CliStatus {
|
|||||||
error?: string | null
|
error?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TauriBridge {
|
|
||||||
invoke?: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T>
|
|
||||||
event?: {
|
|
||||||
listen: (event: string, handler: (payload: { payload: unknown }) => void) => Promise<() => void>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickPhraseKey(previous?: PhraseKey) {
|
function pickPhraseKey(previous?: PhraseKey) {
|
||||||
const filtered = phraseKeys.filter((key) => key !== previous)
|
const filtered = phraseKeys.filter((key) => key !== previous)
|
||||||
const source = filtered.length > 0 ? filtered : phraseKeys
|
const source = filtered.length > 0 ? filtered : phraseKeys
|
||||||
@@ -46,17 +41,6 @@ function navigateTo(url?: string | null) {
|
|||||||
window.location.replace(url)
|
window.location.replace(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTauriBridge(): TauriBridge | null {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const bridge = (window as { __TAURI__?: TauriBridge }).__TAURI__
|
|
||||||
if (!bridge || !bridge.event || !bridge.invoke) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return bridge
|
|
||||||
}
|
|
||||||
|
|
||||||
function annotateDocument() {
|
function annotateDocument() {
|
||||||
if (typeof document === "undefined") {
|
if (typeof document === "undefined") {
|
||||||
return
|
return
|
||||||
@@ -77,25 +61,22 @@ function LoadingApp() {
|
|||||||
setPhraseKey(pickPhraseKey())
|
setPhraseKey(pickPhraseKey())
|
||||||
const unsubscribers: Array<() => void> = []
|
const unsubscribers: Array<() => void> = []
|
||||||
|
|
||||||
async function bootstrapTauri(tauriBridge: TauriBridge | null) {
|
async function bootstrapTauri() {
|
||||||
if (!tauriBridge || !tauriBridge.event || !tauriBridge.invoke) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const readyUnlisten = await tauriBridge.event.listen("cli:ready", (event) => {
|
const readyUnlisten = await listen("cli:ready", (event) => {
|
||||||
const payload = (event?.payload as CliStatus) || {}
|
const payload = (event?.payload as CliStatus) || {}
|
||||||
setError(null)
|
setError(null)
|
||||||
setStatusKey(null)
|
setStatusKey(null)
|
||||||
navigateTo(payload.url)
|
navigateTo(payload.url)
|
||||||
})
|
})
|
||||||
const errorUnlisten = await tauriBridge.event.listen("cli:error", (event) => {
|
const errorUnlisten = await listen("cli:error", (event) => {
|
||||||
const payload = (event?.payload as CliStatus) || {}
|
const payload = (event?.payload as CliStatus) || {}
|
||||||
if (payload.error) {
|
if (payload.error) {
|
||||||
setError(payload.error)
|
setError(payload.error)
|
||||||
setStatusKey("loadingScreen.status.issue")
|
setStatusKey("loadingScreen.status.issue")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const statusUnlisten = await tauriBridge.event.listen("cli:status", (event) => {
|
const statusUnlisten = await listen("cli:status", (event) => {
|
||||||
const payload = (event?.payload as CliStatus) || {}
|
const payload = (event?.payload as CliStatus) || {}
|
||||||
if (payload.state === "error" && payload.error) {
|
if (payload.state === "error" && payload.error) {
|
||||||
setError(payload.error)
|
setError(payload.error)
|
||||||
@@ -109,7 +90,7 @@ function LoadingApp() {
|
|||||||
})
|
})
|
||||||
unsubscribers.push(readyUnlisten, errorUnlisten, statusUnlisten)
|
unsubscribers.push(readyUnlisten, errorUnlisten, statusUnlisten)
|
||||||
|
|
||||||
const result = await tauriBridge.invoke<CliStatus>("cli_get_status")
|
const result = await invoke<CliStatus>("cli_get_status")
|
||||||
if (result?.state === "ready" && result.url) {
|
if (result?.state === "ready" && result.url) {
|
||||||
navigateTo(result.url)
|
navigateTo(result.url)
|
||||||
} else if (result?.state === "error" && result.error) {
|
} else if (result?.state === "error" && result.error) {
|
||||||
@@ -123,7 +104,7 @@ function LoadingApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTauriHost()) {
|
if (isTauriHost()) {
|
||||||
void bootstrapTauri(getTauriBridge())
|
void bootstrapTauri()
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getQuestionCallId, getQuestionMessageId } from "../../types/question"
|
|||||||
import type { Message, MessageInfo, ClientPart } from "../../types/message"
|
import type { Message, MessageInfo, ClientPart } from "../../types/message"
|
||||||
import type { Session } from "../../types/session"
|
import type { Session } from "../../types/session"
|
||||||
import { messageStoreBus } from "./bus"
|
import { messageStoreBus } from "./bus"
|
||||||
import type { MessageStatus, SessionRevertState } from "./types"
|
import type { MessageStatus, ReplaceMessageIdOptions, SessionRevertState } from "./types"
|
||||||
|
|
||||||
interface SessionMetadata {
|
interface SessionMetadata {
|
||||||
id: string
|
id: string
|
||||||
@@ -121,10 +121,10 @@ export function applyPartDeltaV2(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceMessageIdV2(instanceId: string, oldId: string, newId: string): void {
|
export function replaceMessageIdV2(instanceId: string, oldId: string, newId: string, options?: Omit<ReplaceMessageIdOptions, "oldId" | "newId">): void {
|
||||||
if (!oldId || !newId || oldId === newId) return
|
if (!oldId || !newId || oldId === newId) return
|
||||||
const store = messageStoreBus.getOrCreate(instanceId)
|
const store = messageStoreBus.getOrCreate(instanceId)
|
||||||
store.replaceMessageId({ oldId, newId })
|
store.replaceMessageId({ oldId, newId, ...(options ?? {}) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPermissionMessageId(permission: PermissionRequestLike): string | undefined {
|
function extractPermissionMessageId(permission: PermissionRequestLike): string | undefined {
|
||||||
|
|||||||
@@ -792,6 +792,8 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
|||||||
id: options.newId,
|
id: options.newId,
|
||||||
isEphemeral: false,
|
isEphemeral: false,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
partIds: options.clearParts ? [] : existing.partIds,
|
||||||
|
parts: options.clearParts ? {} : existing.parts,
|
||||||
}
|
}
|
||||||
|
|
||||||
setState("messages", options.newId, cloned)
|
setState("messages", options.newId, cloned)
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export interface PartUpdateInput {
|
|||||||
export interface ReplaceMessageIdOptions {
|
export interface ReplaceMessageIdOptions {
|
||||||
oldId: string
|
oldId: string
|
||||||
newId: string
|
newId: string
|
||||||
|
clearParts?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScrollCacheKey {
|
export interface ScrollCacheKey {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ async function sendMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageId = createId("msg")
|
const messageId = createId("msg")
|
||||||
const textPartId = createId("part")
|
const textPartId = createId("prt")
|
||||||
|
|
||||||
const resolvedPrompt = resolvePastedPlaceholders(prompt, attachments)
|
const resolvedPrompt = resolvePastedPlaceholders(prompt, attachments)
|
||||||
|
|
||||||
@@ -110,7 +110,6 @@ async function sendMessage(
|
|||||||
|
|
||||||
const requestParts: any[] = [
|
const requestParts: any[] = [
|
||||||
{
|
{
|
||||||
id: textPartId,
|
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: resolvedPrompt,
|
text: resolvedPrompt,
|
||||||
},
|
},
|
||||||
@@ -120,9 +119,8 @@ async function sendMessage(
|
|||||||
for (const att of attachments) {
|
for (const att of attachments) {
|
||||||
const source = att.source
|
const source = att.source
|
||||||
if (source.type === "file") {
|
if (source.type === "file") {
|
||||||
const partId = createId("part")
|
const partId = createId("prt")
|
||||||
requestParts.push({
|
requestParts.push({
|
||||||
id: partId,
|
|
||||||
type: "file" as const,
|
type: "file" as const,
|
||||||
url: att.url,
|
url: att.url,
|
||||||
mime: source.mime,
|
mime: source.mime,
|
||||||
@@ -148,9 +146,8 @@ async function sendMessage(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const partId = createId("part")
|
const partId = createId("prt")
|
||||||
requestParts.push({
|
requestParts.push({
|
||||||
id: partId,
|
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: value,
|
text: value,
|
||||||
})
|
})
|
||||||
@@ -184,7 +181,6 @@ async function sendMessage(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
messageID: messageId,
|
|
||||||
parts: requestParts,
|
parts: requestParts,
|
||||||
...(session.agent && { agent: session.agent }),
|
...(session.agent && { agent: session.agent }),
|
||||||
...(session.model.providerId &&
|
...(session.model.providerId &&
|
||||||
|
|||||||
@@ -240,19 +240,22 @@ function resolveMessageRole(info?: MessageInfo | null): MessageRole {
|
|||||||
return info?.role === "user" ? "user" : "assistant"
|
return info?.role === "user" ? "user" : "assistant"
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPendingMessageId(
|
function findPendingSyntheticMessageId(
|
||||||
store: InstanceMessageStore,
|
store: InstanceMessageStore,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
role: MessageRole,
|
role: MessageRole,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const messageIds = store.getSessionMessageIds(sessionId)
|
const messageIds = store.getSessionMessageIds(sessionId)
|
||||||
const lastId = messageIds[messageIds.length - 1]
|
for (const messageId of messageIds) {
|
||||||
if (!lastId) return undefined
|
const record = store.getMessage(messageId)
|
||||||
const record = store.getMessage(lastId)
|
if (!record) continue
|
||||||
if (!record) return undefined
|
if (record.sessionId !== sessionId) continue
|
||||||
if (record.sessionId !== sessionId) return undefined
|
if (record.role !== role) continue
|
||||||
if (record.role !== role) return undefined
|
if (record.status !== "sending") continue
|
||||||
return record.status === "sending" ? record.id : undefined
|
if (!record.isEphemeral) continue
|
||||||
|
return record.id
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | MessagePartUpdatedEvent): void {
|
function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | MessagePartUpdatedEvent): void {
|
||||||
@@ -282,9 +285,9 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
|
|
||||||
let record = store.getMessage(messageId)
|
let record = store.getMessage(messageId)
|
||||||
if (!record) {
|
if (!record) {
|
||||||
const pendingId = findPendingMessageId(store, sessionId, role)
|
const pendingId = findPendingSyntheticMessageId(store, sessionId, role)
|
||||||
if (pendingId && pendingId !== messageId) {
|
if (pendingId && pendingId !== messageId) {
|
||||||
replaceMessageIdV2(instanceId, pendingId, messageId)
|
replaceMessageIdV2(instanceId, pendingId, messageId, { clearParts: role === "user" })
|
||||||
record = store.getMessage(messageId)
|
record = store.getMessage(messageId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,9 +348,9 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
|
|
||||||
let record = store.getMessage(messageId)
|
let record = store.getMessage(messageId)
|
||||||
if (!record) {
|
if (!record) {
|
||||||
const pendingId = findPendingMessageId(store, sessionId, role)
|
const pendingId = findPendingSyntheticMessageId(store, sessionId, role)
|
||||||
if (pendingId && pendingId !== messageId) {
|
if (pendingId && pendingId !== messageId) {
|
||||||
replaceMessageIdV2(instanceId, pendingId, messageId)
|
replaceMessageIdV2(instanceId, pendingId, messageId, { clearParts: role === "user" })
|
||||||
record = store.getMessage(messageId)
|
record = store.getMessage(messageId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
packages/ui/src/types/global.d.ts
vendored
11
packages/ui/src/types/global.d.ts
vendored
@@ -47,16 +47,9 @@ declare global {
|
|||||||
webkitGetAsEntry?: () => FileSystemEntry | null
|
webkitGetAsEntry?: () => FileSystemEntry | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TauriDialogModule {
|
|
||||||
open?: (options: Record<string, unknown>) => Promise<string | string[] | null>
|
|
||||||
save?: (options: Record<string, unknown>) => Promise<string | null>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TauriBridge {
|
interface TauriBridge {
|
||||||
invoke?: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T>
|
core?: {
|
||||||
dialog?: TauriDialogModule
|
invoke: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T>
|
||||||
event?: {
|
|
||||||
listen: (event: string, handler: (payload: { payload: unknown }) => void) => Promise<() => void>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
declare module "tauri-plugin-keepawake-api" {
|
|
||||||
export interface KeepAwakeConfig {
|
|
||||||
display?: boolean
|
|
||||||
idle?: boolean
|
|
||||||
sleep?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function start(config?: KeepAwakeConfig): Promise<void>
|
|
||||||
export function stop(): Promise<void>
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user