Compare commits
27 Commits
codenomad/
...
codenomad/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90baefbb7e | ||
|
|
1c138f4489 | ||
|
|
d36e568ed0 | ||
|
|
d6462ef524 | ||
|
|
a06884ebce | ||
|
|
62bd88f6a4 | ||
|
|
6479561779 | ||
|
|
635237c258 | ||
|
|
33f0aa5714 | ||
|
|
7ca6285d58 | ||
|
|
14c60fef6c | ||
|
|
336de6a19e | ||
|
|
377c8e2249 | ||
|
|
697dea21f8 | ||
|
|
34d3f803d5 | ||
|
|
f824a063a5 | ||
|
|
5fabf286e8 | ||
|
|
e8947d61b1 | ||
|
|
1ccd14eae8 | ||
|
|
b162764ccb | ||
|
|
2124e540aa | ||
|
|
b5790998b7 | ||
|
|
9800afb785 | ||
|
|
3b73d9d5b9 | ||
|
|
f7ac30afe3 | ||
|
|
ce370d5100 | ||
|
|
c639e535b5 |
95
.github/workflows/build-and-upload.yml
vendored
95
.github/workflows/build-and-upload.yml
vendored
@@ -3,6 +3,11 @@ name: Build and Upload Binaries
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Git ref (branch, tag, or SHA) to build from"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
version:
|
version:
|
||||||
description: "Version to apply to workspace packages (release builds)"
|
description: "Version to apply to workspace packages (release builds)"
|
||||||
required: false
|
required: false
|
||||||
@@ -45,6 +50,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -65,6 +72,78 @@ jobs:
|
|||||||
- name: Build macOS binaries (Electron)
|
- name: Build macOS binaries (Electron)
|
||||||
run: npm run build:mac --workspace @neuralnomads/codenomad-electron-app
|
run: npm run build:mac --workspace @neuralnomads/codenomad-electron-app
|
||||||
|
|
||||||
|
- name: Repackage Electron macOS zips (ditto)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Prefer the workflow-provided version; fall back to package.json.
|
||||||
|
VERSION_TO_USE="${VERSION:-}"
|
||||||
|
if [ -z "$VERSION_TO_USE" ]; then
|
||||||
|
VERSION_TO_USE=$(node -p "require('./packages/electron-app/package.json').version")
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_root="packages/electron-app/release"
|
||||||
|
shopt -s nullglob globstar
|
||||||
|
|
||||||
|
apps=("$release_root"/**/CodeNomad.app)
|
||||||
|
if [ "${#apps[@]}" -eq 0 ]; then
|
||||||
|
echo "No CodeNomad.app found under $release_root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for app in "${apps[@]}"; do
|
||||||
|
bundle_dir=$(basename "$(dirname "$app")")
|
||||||
|
arch="x64"
|
||||||
|
if [[ "$bundle_dir" == *"arm64"* ]]; then
|
||||||
|
arch="arm64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
out_zip="$release_root/CodeNomad-${VERSION_TO_USE}-mac-${arch}.zip"
|
||||||
|
rm -f "$out_zip"
|
||||||
|
echo "ditto -ck: $app -> $out_zip"
|
||||||
|
ditto -ck --sequesterRsrc --keepParent "$app" "$out_zip"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Validate Electron macOS codesign (unzipped)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
tmp_dir=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp_dir"' EXIT
|
||||||
|
|
||||||
|
zips=(packages/electron-app/release/CodeNomad-*-mac-*.zip)
|
||||||
|
if [ "${#zips[@]}" -eq 0 ]; then
|
||||||
|
echo "No Electron macOS zip artifacts found to validate" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for zip in "${zips[@]}"; do
|
||||||
|
echo "Validating codesign for: $zip"
|
||||||
|
extract_dir="$tmp_dir/$(basename "$zip" .zip)"
|
||||||
|
mkdir -p "$extract_dir"
|
||||||
|
|
||||||
|
# Use ditto for extraction as well to preserve bundle metadata.
|
||||||
|
ditto -x -k "$zip" "$extract_dir"
|
||||||
|
|
||||||
|
app_path=""
|
||||||
|
for candidate in "$extract_dir"/*.app "$extract_dir"/*/*.app; do
|
||||||
|
if [ -d "$candidate" ]; then
|
||||||
|
app_path="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$app_path" ]; then
|
||||||
|
echo "No .app found after extracting $zip" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$app_path"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
if: ${{ inputs.upload && inputs.tag != '' }}
|
if: ${{ inputs.upload && inputs.tag != '' }}
|
||||||
run: |
|
run: |
|
||||||
@@ -85,6 +164,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -124,6 +205,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -164,6 +247,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -237,6 +322,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -310,6 +397,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -388,6 +477,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -490,6 +581,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -587,6 +680,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
61
.github/workflows/dev-release.yml
vendored
61
.github/workflows/dev-release.yml
vendored
@@ -1,12 +1,13 @@
|
|||||||
name: Develop Pre-Release
|
name: Develop Pre-Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
schedule:
|
||||||
branches:
|
# Nightly build of dev (only if dev has new commits)
|
||||||
- dev
|
- cron: "0 1 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: read
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
@@ -15,25 +16,63 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
gate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version_suffix: ${{ steps.vars.outputs.version_suffix }}
|
run: ${{ steps.gate.outputs.run }}
|
||||||
|
dev_sha: ${{ steps.gate.outputs.dev_sha }}
|
||||||
|
version_suffix: ${{ steps.gate.outputs.version_suffix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Compute version suffix
|
- name: Decide whether to run
|
||||||
id: vars
|
id: gate
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SHA8="${GITHUB_SHA::8}"
|
|
||||||
|
api() {
|
||||||
|
curl -sS \
|
||||||
|
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
"$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
DEV_SHA=$(api "https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/heads/dev" | jq -r '.object.sha')
|
||||||
|
if [ -z "$DEV_SHA" ] || [ "$DEV_SHA" = "null" ]; then
|
||||||
|
echo "Failed to resolve dev head SHA" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
DATE=$(date -u +%Y%m%d)
|
DATE=$(date -u +%Y%m%d)
|
||||||
echo "version_suffix=-dev-${DATE}-${SHA8}" >> "$GITHUB_OUTPUT"
|
SHA8="${DEV_SHA::8}"
|
||||||
|
VERSION_SUFFIX="-dev-${DATE}-${SHA8}"
|
||||||
|
|
||||||
|
SHOULD_RUN="false"
|
||||||
|
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||||
|
SHOULD_RUN="true"
|
||||||
|
else
|
||||||
|
# Nightly: only run if dev has advanced since last successful dev-release build.
|
||||||
|
LAST_SHA=$(api "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/dev-release.yml/runs?branch=dev&status=success&per_page=1" | jq -r '.workflow_runs[0].head_sha // empty')
|
||||||
|
if [ -z "${LAST_SHA}" ]; then
|
||||||
|
SHOULD_RUN="true"
|
||||||
|
elif [ "${LAST_SHA}" != "${DEV_SHA}" ]; then
|
||||||
|
SHOULD_RUN="true"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "run=${SHOULD_RUN}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "dev_sha=${DEV_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version_suffix=${VERSION_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
prerelease:
|
prerelease:
|
||||||
needs: prepare
|
needs: gate
|
||||||
|
if: ${{ needs.gate.outputs.run == 'true' }}
|
||||||
uses: ./.github/workflows/reusable-release.yml
|
uses: ./.github/workflows/reusable-release.yml
|
||||||
with:
|
with:
|
||||||
version_suffix: ${{ needs.prepare.outputs.version_suffix }}
|
ref: ${{ needs.gate.outputs.dev_sha }}
|
||||||
|
version_suffix: ${{ needs.gate.outputs.version_suffix }}
|
||||||
npm_package_name: "@neuralnomads/codenomad-dev"
|
npm_package_name: "@neuralnomads/codenomad-dev"
|
||||||
dist_tag: latest
|
dist_tag: latest
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
6
.github/workflows/manual-npm-publish.yml
vendored
6
.github/workflows/manual-npm-publish.yml
vendored
@@ -19,6 +19,10 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
|
ref:
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
version:
|
version:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
@@ -46,6 +50,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
10
.github/workflows/release-ui.yml
vendored
10
.github/workflows/release-ui.yml
vendored
@@ -1,7 +1,13 @@
|
|||||||
name: Release UI
|
name: Release UI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call: {}
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Git ref (branch, tag, or SHA) to build from"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -18,6 +24,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
11
.github/workflows/reusable-release.yml
vendored
11
.github/workflows/reusable-release.yml
vendored
@@ -3,6 +3,11 @@ name: Reusable Release
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Git ref (branch, tag, or SHA) to build from"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
version_suffix:
|
version_suffix:
|
||||||
description: "Suffix appended to package.json version"
|
description: "Suffix appended to package.json version"
|
||||||
required: false
|
required: false
|
||||||
@@ -46,6 +51,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -84,6 +91,7 @@ jobs:
|
|||||||
needs: prepare-release
|
needs: prepare-release
|
||||||
uses: ./.github/workflows/build-and-upload.yml
|
uses: ./.github/workflows/build-and-upload.yml
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
version: ${{ needs.prepare-release.outputs.version }}
|
version: ${{ needs.prepare-release.outputs.version }}
|
||||||
tag: ${{ needs.prepare-release.outputs.tag }}
|
tag: ${{ needs.prepare-release.outputs.tag }}
|
||||||
release_name: ${{ needs.prepare-release.outputs.release_name }}
|
release_name: ${{ needs.prepare-release.outputs.release_name }}
|
||||||
@@ -95,6 +103,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
uses: ./.github/workflows/release-ui.yml
|
uses: ./.github/workflows/release-ui.yml
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
publish-server:
|
publish-server:
|
||||||
@@ -103,6 +113,7 @@ jobs:
|
|||||||
- build-and-upload
|
- build-and-upload
|
||||||
uses: ./.github/workflows/manual-npm-publish.yml
|
uses: ./.github/workflows/manual-npm-publish.yml
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
version: ${{ needs.prepare-release.outputs.version }}
|
version: ${{ needs.prepare-release.outputs.version }}
|
||||||
dist_tag: ${{ inputs.dist_tag }}
|
dist_tag: ${{ inputs.dist_tag }}
|
||||||
package_name: ${{ inputs.npm_package_name }}
|
package_name: ${{ inputs.npm_package_name }}
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
@@ -11985,7 +11985,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
@@ -12021,7 +12021,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
@@ -12062,7 +12062,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
@@ -12070,7 +12070,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"minServerVersion": "0.11.1",
|
"minServerVersion": "0.11.4",
|
||||||
"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.11.3",
|
"version": "0.11.4",
|
||||||
"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.6"
|
"@opencode-ai/plugin": "1.2.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,21 @@
|
|||||||
## Features & Capabilities
|
## Features & Capabilities
|
||||||
|
|
||||||
### 🌍 Deployment Freedom
|
### 🌍 Deployment Freedom
|
||||||
|
|
||||||
- **Remote Access**: Host CodeNomad on a powerful workstation and access it from your lightweight laptop.
|
- **Remote Access**: Host CodeNomad on a powerful workstation and access it from your lightweight laptop.
|
||||||
- **Code Anywhere**: Tunnel in via VPN or SSH to code securely from coffee shops or while traveling.
|
- **Code Anywhere**: Tunnel in via VPN or SSH to code securely from coffee shops or while traveling.
|
||||||
- **Multi-Device**: The responsive web client works on tablets and iPads, turning any screen into a dev terminal.
|
- **Multi-Device**: The responsive web client works on tablets and iPads, turning any screen into a dev terminal.
|
||||||
- **Always-On**: Run as a background service so your sessions are always ready when you connect.
|
- **Always-On**: Run as a background service so your sessions are always ready when you connect.
|
||||||
|
|
||||||
### ⚡️ Workspace Power
|
### ⚡️ Workspace Power
|
||||||
|
|
||||||
- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
|
- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
|
||||||
- **Long-Context Native**: Scroll through massive transcripts without hitches.
|
- **Long-Context Native**: Scroll through massive transcripts without hitches.
|
||||||
- **Deep Task Awareness**: Monitor background tasks and child sessions without losing your flow.
|
- **Deep Task Awareness**: Monitor background tasks and child sessions without losing your flow.
|
||||||
- **Command Palette**: A single, global palette to jump tabs, launch tools, and fire shortcuts.
|
- **Command Palette**: A single, global palette to jump tabs, launch tools, and fire shortcuts.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **OpenCode**: `opencode` must be installed and configured on your system.
|
- **OpenCode**: `opencode` must be installed and configured on your system.
|
||||||
- Node.js 18+ and npm (for running or building from source).
|
- Node.js 18+ and npm (for running or building from source).
|
||||||
- A workspace folder on disk you want to serve.
|
- A workspace folder on disk you want to serve.
|
||||||
@@ -25,6 +28,7 @@
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Run via npx (Recommended)
|
### Run via npx (Recommended)
|
||||||
|
|
||||||
You can run CodeNomad directly without installing it:
|
You can run CodeNomad directly without installing it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -43,6 +47,7 @@ On startup, CodeNomad prints two URLs:
|
|||||||
- `Remote Connection URL : ...` (used by browsers/other machines when remote access is enabled)
|
- `Remote Connection URL : ...` (used by browsers/other machines when remote access is enabled)
|
||||||
|
|
||||||
### Install Globally
|
### Install Globally
|
||||||
|
|
||||||
Or install it globally to use the `codenomad` command:
|
Or install it globally to use the `codenomad` command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -51,6 +56,7 @@ codenomad --launch
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Install Locally (per-project)
|
### Install Locally (per-project)
|
||||||
|
|
||||||
If you prefer to install CodeNomad into a project and run the local binary:
|
If you prefer to install CodeNomad into a project and run the local binary:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -61,6 +67,7 @@ npx codenomad --launch
|
|||||||
(`npx codenomad ...` will use `./node_modules/.bin/codenomad` when present.)
|
(`npx codenomad ...` will use `./node_modules/.bin/codenomad` when present.)
|
||||||
|
|
||||||
### Common Flags
|
### Common Flags
|
||||||
|
|
||||||
You can configure the server using flags or environment variables:
|
You can configure the server using flags or environment variables:
|
||||||
|
|
||||||
| Flag | Env Variable | Description |
|
| Flag | Env Variable | Description |
|
||||||
@@ -74,7 +81,7 @@ You can configure the server using flags or environment variables:
|
|||||||
| `--tls-ca <path>` | `CLI_TLS_CA` | Optional CA chain/bundle (PEM) |
|
| `--tls-ca <path>` | `CLI_TLS_CA` | Optional CA chain/bundle (PEM) |
|
||||||
| `--tlsSANs <list>` | `CLI_TLS_SANS` | Additional TLS SANs (comma-separated) |
|
| `--tlsSANs <list>` | `CLI_TLS_SANS` | Additional TLS SANs (comma-separated) |
|
||||||
| `--host <addr>` | `CLI_HOST` | Interface to bind (default 127.0.0.1) |
|
| `--host <addr>` | `CLI_HOST` | Interface to bind (default 127.0.0.1) |
|
||||||
| `--workspace-root <path>` | `CLI_WORKSPACE_ROOT` | Default root for new workspaces |
|
| `--workspace-root <path>` | `CLI_WORKSPACE_ROOT` | Restricts the root path where new workspaces can be opened. Git worktrees are created in `.codenomad/worktrees` inside the project folder. |
|
||||||
| `--unrestricted-root` | `CLI_UNRESTRICTED_ROOT` | Allow full-filesystem browsing |
|
| `--unrestricted-root` | `CLI_UNRESTRICTED_ROOT` | Allow full-filesystem browsing |
|
||||||
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
||||||
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
||||||
@@ -87,10 +94,11 @@ You can configure the server using flags or environment variables:
|
|||||||
| `--ui-dir <path>` | `CLI_UI_DIR` | Directory containing the built UI bundle |
|
| `--ui-dir <path>` | `CLI_UI_DIR` | Directory containing the built UI bundle |
|
||||||
| `--ui-dev-server <url>` | `CLI_UI_DEV_SERVER` | Proxy UI requests to a running dev server (requires `--https=false --http=true`) |
|
| `--ui-dev-server <url>` | `CLI_UI_DEV_SERVER` | Proxy UI requests to a running dev server (requires `--https=false --http=true`) |
|
||||||
| `--ui-no-update` | `CLI_UI_NO_UPDATE` | Disable remote UI updates |
|
| `--ui-no-update` | `CLI_UI_NO_UPDATE` | Disable remote UI updates |
|
||||||
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (true|false) |
|
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (`true` |
|
||||||
| `--ui-manifest-url <url>` | `CLI_UI_MANIFEST_URL` | Remote UI manifest URL |
|
| `--ui-manifest-url <url>` | `CLI_UI_MANIFEST_URL` | Remote UI manifest URL |
|
||||||
|
|
||||||
### Dev Releases (Advanced)
|
### Dev Releases (Advanced)
|
||||||
|
|
||||||
If you want the latest bleeding-edge builds (published as GitHub pre-releases), use the dev package:
|
If you want the latest bleeding-edge builds (published as GitHub pre-releases), use the dev package:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -141,12 +149,14 @@ codenomad --tlsSANs "localhost,127.0.0.1,my-hostname,192.168.1.10"
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
- Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
|
- Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
|
||||||
- `--dangerously-skip-auth` / `CODENOMAD_SKIP_AUTH=true` disables the login prompt and treats all requests as authenticated.
|
- `--dangerously-skip-auth` / `CODENOMAD_SKIP_AUTH=true` disables the login prompt and treats all requests as authenticated.
|
||||||
Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.).
|
Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.).
|
||||||
If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API.
|
If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API.
|
||||||
|
|
||||||
### Progressive Web App (PWA)
|
### Progressive Web App (PWA)
|
||||||
|
|
||||||
When running as a server CodeNomad can also be installed as a PWA from any supported browser, giving you a native app experience just like the Electron installation but executing on the remote server instead.
|
When running as a server CodeNomad can also be installed as a PWA from any supported browser, giving you a native app experience just like the Electron installation but executing on the remote server instead.
|
||||||
|
|
||||||
1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.).
|
1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.).
|
||||||
@@ -158,5 +168,6 @@ When running as a server CodeNomad can also be installed as a PWA from any suppo
|
|||||||
> If you host CodeNomad on a remote machine, use HTTPS. Self-signed certificates generally won't work unless they are explicitly trusted by the device/browser (e.g., via a custom CA).
|
> If you host CodeNomad on a remote machine, use HTTPS. Self-signed certificates generally won't work unless they are explicitly trusted by the device/browser (e.g., via a custom CA).
|
||||||
|
|
||||||
### Data Storage
|
### Data Storage
|
||||||
|
|
||||||
- **Config**: `~/.config/codenomad/config.json`
|
- **Config**: `~/.config/codenomad/config.json`
|
||||||
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
||||||
|
|||||||
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.11.3",
|
"version": "0.11.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"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.11.3",
|
"version": "0.11.4",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
.addOption(new Option("--tls-ca <path>", "TLS CA chain (PEM)").env("CLI_TLS_CA"))
|
.addOption(new Option("--tls-ca <path>", "TLS CA chain (PEM)").env("CLI_TLS_CA"))
|
||||||
.addOption(new Option("--tlsSANs <list>", "Additional TLS SANs (comma-separated)").env("CLI_TLS_SANS"))
|
.addOption(new Option("--tlsSANs <list>", "Additional TLS SANs (comma-separated)").env("CLI_TLS_SANS"))
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option("--workspace-root <path>", "Workspace root directory").env("CLI_WORKSPACE_ROOT").default(process.cwd()),
|
new Option("--workspace-root <path>", "Restricts root path where workspaces can be opened").env("CLI_WORKSPACE_ROOT").default(process.cwd()),
|
||||||
)
|
)
|
||||||
.addOption(new Option("--root <path>").env("CLI_ROOT").hideHelp(true))
|
.addOption(new Option("--root <path>").env("CLI_ROOT").hideHelp(true))
|
||||||
.addOption(new Option("--unrestricted-root", "Allow browsing the full filesystem").env("CLI_UNRESTRICTED_ROOT").default(false))
|
.addOption(new Option("--unrestricted-root", "Allow browsing the full filesystem").env("CLI_UNRESTRICTED_ROOT").default(false))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { FileSystemBrowser } from "../filesystem/browser"
|
|||||||
import { searchWorkspaceFiles, WorkspaceFileSearchOptions } from "../filesystem/search"
|
import { searchWorkspaceFiles, WorkspaceFileSearchOptions } from "../filesystem/search"
|
||||||
import { clearWorkspaceSearchCache } from "../filesystem/search-cache"
|
import { clearWorkspaceSearchCache } from "../filesystem/search-cache"
|
||||||
import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types"
|
import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types"
|
||||||
import { WorkspaceRuntime, ProcessExitInfo, probeBinaryVersion } from "./runtime"
|
import { WorkspaceRuntime, ProcessExitInfo } from "./runtime"
|
||||||
import { Logger } from "../logger"
|
import { Logger } from "../logger"
|
||||||
import { getOpencodeConfigDir } from "../opencode-config.js"
|
import { getOpencodeConfigDir } from "../opencode-config.js"
|
||||||
import {
|
import {
|
||||||
@@ -109,10 +109,6 @@ export class WorkspaceManager {
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!descriptor.binaryVersion) {
|
|
||||||
descriptor.binaryVersion = this.detectBinaryVersion(resolvedBinaryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.workspaces.set(id, descriptor)
|
this.workspaces.set(id, descriptor)
|
||||||
|
|
||||||
|
|
||||||
@@ -149,7 +145,10 @@ export class WorkspaceManager {
|
|||||||
onExit: (info) => this.handleProcessExit(info.workspaceId, info),
|
onExit: (info) => this.handleProcessExit(info.workspaceId, info),
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.waitForWorkspaceReadiness({ workspaceId: id, port, exitPromise, getLastOutput })
|
const runtimeVersion = await this.waitForWorkspaceReadiness({ workspaceId: id, port, exitPromise, getLastOutput })
|
||||||
|
if (runtimeVersion) {
|
||||||
|
descriptor.binaryVersion = runtimeVersion
|
||||||
|
}
|
||||||
|
|
||||||
descriptor.pid = pid
|
descriptor.pid = pid
|
||||||
descriptor.port = port
|
descriptor.port = port
|
||||||
@@ -278,36 +277,12 @@ export class WorkspaceManager {
|
|||||||
return candidates[0] ?? ""
|
return candidates[0] ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private detectBinaryVersion(resolvedPath: string): string | undefined {
|
|
||||||
if (!resolvedPath) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = probeBinaryVersion(resolvedPath)
|
|
||||||
if (result.valid) {
|
|
||||||
if (result.version) {
|
|
||||||
this.options.logger.debug({ binary: resolvedPath, version: result.version }, "Detected binary version")
|
|
||||||
return result.version
|
|
||||||
}
|
|
||||||
if (result.reported) {
|
|
||||||
this.options.logger.debug({ binary: resolvedPath, reported: result.reported }, "Binary reported version string")
|
|
||||||
return result.reported
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
this.options.logger.warn({ binary: resolvedPath, err: result.error }, "Failed to detect binary version")
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
private async waitForWorkspaceReadiness(params: {
|
private async waitForWorkspaceReadiness(params: {
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
port: number
|
port: number
|
||||||
exitPromise: Promise<ProcessExitInfo>
|
exitPromise: Promise<ProcessExitInfo>
|
||||||
getLastOutput: () => string
|
getLastOutput: () => string
|
||||||
}) {
|
}): Promise<string | undefined> {
|
||||||
|
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
this.waitForPortAvailability(params.port),
|
this.waitForPortAvailability(params.port),
|
||||||
@@ -321,7 +296,7 @@ export class WorkspaceManager {
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await this.waitForInstanceHealth(params)
|
const version = await this.waitForInstanceHealth(params)
|
||||||
|
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
this.delay(STARTUP_STABILITY_DELAY_MS),
|
this.delay(STARTUP_STABILITY_DELAY_MS),
|
||||||
@@ -334,6 +309,8 @@ export class WorkspaceManager {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForInstanceHealth(params: {
|
private async waitForInstanceHealth(params: {
|
||||||
@@ -341,7 +318,7 @@ export class WorkspaceManager {
|
|||||||
port: number
|
port: number
|
||||||
exitPromise: Promise<ProcessExitInfo>
|
exitPromise: Promise<ProcessExitInfo>
|
||||||
getLastOutput: () => string
|
getLastOutput: () => string
|
||||||
}) {
|
}): Promise<string | undefined> {
|
||||||
const probeResult = await Promise.race([
|
const probeResult = await Promise.race([
|
||||||
this.probeInstance(params.workspaceId, params.port),
|
this.probeInstance(params.workspaceId, params.port),
|
||||||
params.exitPromise.then((info) => {
|
params.exitPromise.then((info) => {
|
||||||
@@ -355,7 +332,7 @@ export class WorkspaceManager {
|
|||||||
])
|
])
|
||||||
|
|
||||||
if (probeResult.ok) {
|
if (probeResult.ok) {
|
||||||
return
|
return probeResult.version
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestOutput = params.getLastOutput().trim()
|
const latestOutput = params.getLastOutput().trim()
|
||||||
@@ -366,8 +343,11 @@ export class WorkspaceManager {
|
|||||||
throw new Error(`Workspace ${params.workspaceId} failed health check: ${reason}.`)
|
throw new Error(`Workspace ${params.workspaceId} failed health check: ${reason}.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async probeInstance(workspaceId: string, port: number): Promise<{ ok: boolean; reason?: string }> {
|
private async probeInstance(
|
||||||
const url = `http://127.0.0.1:${port}/project/current`
|
workspaceId: string,
|
||||||
|
port: number,
|
||||||
|
): Promise<{ ok: boolean; reason?: string; version?: string }> {
|
||||||
|
const url = `http://127.0.0.1:${port}/global/health`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headers: Record<string, string> = {}
|
const headers: Record<string, string> = {}
|
||||||
@@ -378,11 +358,22 @@ export class WorkspaceManager {
|
|||||||
|
|
||||||
const response = await fetch(url, { headers })
|
const response = await fetch(url, { headers })
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const reason = `health probe returned HTTP ${response.status}`
|
const reason = `/global/health returned HTTP ${response.status}`
|
||||||
this.options.logger.debug({ workspaceId, status: response.status }, "Health probe returned server error")
|
this.options.logger.debug({ workspaceId, status: response.status }, "Health probe returned server error")
|
||||||
return { ok: false, reason }
|
return { ok: false, reason }
|
||||||
}
|
}
|
||||||
return { ok: true }
|
|
||||||
|
const payload = (await response.json().catch(() => null)) as null | { healthy?: unknown; version?: unknown }
|
||||||
|
const healthy = payload?.healthy === true
|
||||||
|
const version = typeof payload?.version === "string" ? payload.version.trim() : undefined
|
||||||
|
|
||||||
|
if (!healthy) {
|
||||||
|
const reason = "Instance reported unhealthy"
|
||||||
|
this.options.logger.debug({ workspaceId, payload }, "Health probe returned unhealthy response")
|
||||||
|
return { ok: false, reason }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, version: version || undefined }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const reason = error instanceof Error ? error.message : String(error)
|
const reason = error instanceof Error ? error.message : String(error)
|
||||||
this.options.logger.debug({ workspaceId, err: error }, "Health probe failed")
|
this.options.logger.debug({ workspaceId, err: error }, "Health probe failed")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.11.3",
|
"version": "0.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { useTheme } from "./lib/theme"
|
|||||||
import { useCommands } from "./lib/hooks/use-commands"
|
import { useCommands } from "./lib/hooks/use-commands"
|
||||||
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
|
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
|
||||||
import { getLogger } from "./lib/logger"
|
import { getLogger } from "./lib/logger"
|
||||||
|
import { launchError, showLaunchError, clearLaunchError } from "./stores/launch-errors"
|
||||||
|
import { formatLaunchErrorMessage, isMissingBinaryMessage } from "./lib/launch-errors"
|
||||||
import { initReleaseNotifications } from "./stores/releases"
|
import { initReleaseNotifications } from "./stores/releases"
|
||||||
import { runtimeEnv } from "./lib/runtime-env"
|
import { runtimeEnv } from "./lib/runtime-env"
|
||||||
import { useI18n } from "./lib/i18n"
|
import { useI18n } from "./lib/i18n"
|
||||||
@@ -75,12 +77,6 @@ const App: Component = () => {
|
|||||||
setToolInputsVisibility,
|
setToolInputsVisibility,
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
||||||
interface LaunchErrorState {
|
|
||||||
message: string
|
|
||||||
binaryPath: string
|
|
||||||
missingBinary: boolean
|
|
||||||
}
|
|
||||||
const [launchError, setLaunchError] = createSignal<LaunchErrorState | null>(null)
|
|
||||||
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false)
|
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false)
|
||||||
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
||||||
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
|
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
|
||||||
@@ -245,35 +241,6 @@ const App: Component = () => {
|
|||||||
|
|
||||||
const launchErrorMessage = () => launchError()?.message ?? ""
|
const launchErrorMessage = () => launchError()?.message ?? ""
|
||||||
|
|
||||||
const formatLaunchErrorMessage = (error: unknown): string => {
|
|
||||||
if (!error) {
|
|
||||||
return t("app.launchError.fallbackMessage")
|
|
||||||
}
|
|
||||||
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error)
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw)
|
|
||||||
if (parsed && typeof parsed.error === "string") {
|
|
||||||
return parsed.error
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore JSON parse errors
|
|
||||||
}
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMissingBinaryMessage = (message: string): boolean => {
|
|
||||||
const normalized = message.toLowerCase()
|
|
||||||
return (
|
|
||||||
normalized.includes("opencode binary not found") ||
|
|
||||||
normalized.includes("binary not found") ||
|
|
||||||
normalized.includes("no such file or directory") ||
|
|
||||||
normalized.includes("binary is not executable") ||
|
|
||||||
normalized.includes("enoent")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearLaunchError = () => setLaunchError(null)
|
|
||||||
|
|
||||||
async function handleSelectFolder(folderPath: string, binaryPath?: string) {
|
async function handleSelectFolder(folderPath: string, binaryPath?: string) {
|
||||||
if (!folderPath) {
|
if (!folderPath) {
|
||||||
return
|
return
|
||||||
@@ -292,13 +259,9 @@ const App: Component = () => {
|
|||||||
port: instances().get(instanceId)?.port,
|
port: instances().get(instanceId)?.port,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = formatLaunchErrorMessage(error)
|
const message = formatLaunchErrorMessage(error, t("app.launchError.fallbackMessage"))
|
||||||
const missingBinary = isMissingBinaryMessage(message)
|
const missingBinary = isMissingBinaryMessage(message)
|
||||||
setLaunchError({
|
showLaunchError({ source: "create", message, binaryPath: selectedBinary, missingBinary })
|
||||||
message,
|
|
||||||
binaryPath: selectedBinary,
|
|
||||||
missingBinary,
|
|
||||||
})
|
|
||||||
log.error("Failed to create instance", error)
|
log.error("Failed to create instance", error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSelectingFolder(false)
|
setIsSelectingFolder(false)
|
||||||
|
|||||||
@@ -116,11 +116,8 @@ const AlertDialog: Component = () => {
|
|||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay class="modal-overlay" />
|
<Dialog.Overlay class="modal-overlay" />
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
<Dialog.Content
|
<Dialog.Content class="modal-surface w-full max-w-sm p-6 border border-base shadow-2xl" tabIndex={-1}>
|
||||||
class="modal-surface w-full max-w-xl md:max-w-2xl p-6 border border-base shadow-2xl max-h-[85vh] overflow-hidden flex flex-col"
|
<div class="flex items-start gap-3">
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<div class="flex items-start gap-3 min-h-0">
|
|
||||||
<div
|
<div
|
||||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border text-base font-semibold"
|
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border text-base font-semibold"
|
||||||
style={{
|
style={{
|
||||||
@@ -132,16 +129,11 @@ const AlertDialog: Component = () => {
|
|||||||
>
|
>
|
||||||
{accent.symbol}
|
{accent.symbol}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0 min-h-0">
|
<div class="flex-1 min-w-0">
|
||||||
<Dialog.Title class="text-lg font-semibold text-primary">{title}</Dialog.Title>
|
<Dialog.Title class="text-lg font-semibold text-primary">{title}</Dialog.Title>
|
||||||
<Dialog.Description class="text-sm text-secondary mt-1">
|
<Dialog.Description class="text-sm text-secondary mt-1 whitespace-pre-line break-words">
|
||||||
<div
|
{payload.message}
|
||||||
class="max-h-[60vh] overflow-auto pr-2 whitespace-pre-wrap break-words"
|
{payload.detail && <p class="mt-2 text-secondary">{payload.detail}</p>}
|
||||||
style={{ "overflow-wrap": "anywhere" }}
|
|
||||||
>
|
|
||||||
{payload.message}
|
|
||||||
{payload.detail && <div class="mt-3">{payload.detail}</div>}
|
|
||||||
</div>
|
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
|||||||
// Keep enough gutter space so unified diffs don't overlap `+`/`-` markers.
|
// Keep enough gutter space so unified diffs don't overlap `+`/`-` markers.
|
||||||
lineNumbersMinChars: 4,
|
lineNumbersMinChars: 4,
|
||||||
lineDecorationsWidth: 12,
|
lineDecorationsWidth: 12,
|
||||||
|
// Use legacy diff algorithm for better performance with large files
|
||||||
|
// See: https://github.com/microsoft/vscode/issues/184037
|
||||||
|
diffAlgorithm: "legacy",
|
||||||
|
// Limit computation time to avoid freezing on large files
|
||||||
|
maxComputationTime: 10000,
|
||||||
})
|
})
|
||||||
|
|
||||||
setReady(true)
|
setReady(true)
|
||||||
|
|||||||
@@ -625,7 +625,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
<div class="flex flex-wrap items-center justify-center gap-1">
|
<div class="flex flex-wrap items-center justify-center gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="connection-status-button command-palette-button px-2 py-0.5 text-xs"
|
class="connection-status-button command-palette-button"
|
||||||
onClick={handleCommandPaletteClick}
|
onClick={handleCommandPaletteClick}
|
||||||
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
|
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
|
||||||
style={{ flex: "0 0 auto", width: "auto" }}
|
style={{ flex: "0 0 auto", width: "auto" }}
|
||||||
@@ -721,7 +721,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
<div class="session-toolbar-center flex items-center justify-center gap-2 min-w-[160px]">
|
<div class="session-toolbar-center flex items-center justify-center gap-2 min-w-[160px]">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="connection-status-button command-palette-button px-2 py-0.5 text-xs"
|
class="connection-status-button command-palette-button"
|
||||||
onClick={handleCommandPaletteClick}
|
onClick={handleCommandPaletteClick}
|
||||||
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
|
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
|
||||||
style={{ flex: "0 0 auto", width: "auto" }}
|
style={{ flex: "0 0 auto", width: "auto" }}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { For, Show, type Accessor, type Component } from "solid-js"
|
import { For, Show, type Accessor, type Component } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk"
|
||||||
import { Accordion } from "@kobalte/core"
|
import { Accordion } from "@kobalte/core"
|
||||||
|
import { Tooltip } from "@kobalte/core/tooltip"
|
||||||
|
|
||||||
import { ChevronDown, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
import { ChevronDown, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
||||||
|
|
||||||
import type { Instance } from "../../../../../types/instance"
|
import type { Instance } from "../../../../../types/instance"
|
||||||
import type { BackgroundProcess } from "../../../../../../../server/src/api-types"
|
import type { BackgroundProcess } from "../../../../../../../server/src/api-types"
|
||||||
@@ -206,21 +207,25 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
|||||||
{
|
{
|
||||||
id: "session-changes",
|
id: "session-changes",
|
||||||
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
||||||
|
tooltipKey: "instanceShell.rightPanel.sections.sessionChanges.tooltip",
|
||||||
render: renderStatusSessionChanges,
|
render: renderStatusSessionChanges,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "plan",
|
id: "plan",
|
||||||
labelKey: "instanceShell.rightPanel.sections.plan",
|
labelKey: "instanceShell.rightPanel.sections.plan",
|
||||||
|
tooltipKey: "instanceShell.rightPanel.sections.plan.tooltip",
|
||||||
render: renderPlanSectionContent,
|
render: renderPlanSectionContent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "background-processes",
|
id: "background-processes",
|
||||||
labelKey: "instanceShell.rightPanel.sections.backgroundProcesses",
|
labelKey: "instanceShell.rightPanel.sections.backgroundProcesses",
|
||||||
|
tooltipKey: "instanceShell.rightPanel.sections.backgroundProcesses.tooltip",
|
||||||
render: renderBackgroundProcesses,
|
render: renderBackgroundProcesses,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "mcp",
|
id: "mcp",
|
||||||
labelKey: "instanceShell.rightPanel.sections.mcp",
|
labelKey: "instanceShell.rightPanel.sections.mcp",
|
||||||
|
tooltipKey: "instanceShell.rightPanel.sections.mcp.tooltip",
|
||||||
render: () => (
|
render: () => (
|
||||||
<InstanceServiceStatus
|
<InstanceServiceStatus
|
||||||
initialInstance={props.instance}
|
initialInstance={props.instance}
|
||||||
@@ -233,6 +238,7 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
|||||||
{
|
{
|
||||||
id: "lsp",
|
id: "lsp",
|
||||||
labelKey: "instanceShell.rightPanel.sections.lsp",
|
labelKey: "instanceShell.rightPanel.sections.lsp",
|
||||||
|
tooltipKey: "instanceShell.rightPanel.sections.lsp.tooltip",
|
||||||
render: () => (
|
render: () => (
|
||||||
<InstanceServiceStatus
|
<InstanceServiceStatus
|
||||||
initialInstance={props.instance}
|
initialInstance={props.instance}
|
||||||
@@ -245,6 +251,7 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
|||||||
{
|
{
|
||||||
id: "plugins",
|
id: "plugins",
|
||||||
labelKey: "instanceShell.rightPanel.sections.plugins",
|
labelKey: "instanceShell.rightPanel.sections.plugins",
|
||||||
|
tooltipKey: "instanceShell.rightPanel.sections.plugins.tooltip",
|
||||||
render: () => (
|
render: () => (
|
||||||
<InstanceServiceStatus
|
<InstanceServiceStatus
|
||||||
initialInstance={props.instance}
|
initialInstance={props.instance}
|
||||||
@@ -276,7 +283,23 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
|||||||
<Accordion.Item value={section.id} class="right-panel-accordion-item">
|
<Accordion.Item value={section.id} class="right-panel-accordion-item">
|
||||||
<Accordion.Header>
|
<Accordion.Header>
|
||||||
<Accordion.Trigger class="right-panel-accordion-trigger">
|
<Accordion.Trigger class="right-panel-accordion-trigger">
|
||||||
<span>{props.t(section.labelKey)}</span>
|
<span class="section-left">
|
||||||
|
<Tooltip openDelay={200} gutter={4} placement="top">
|
||||||
|
<Tooltip.Trigger
|
||||||
|
class="section-info-trigger"
|
||||||
|
aria-label={props.t(section.tooltipKey)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Info class="section-info-icon" />
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content class="section-info-tooltip">
|
||||||
|
{props.t(section.tooltipKey)}
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
<span class="section-label">{props.t(section.labelKey)}</span>
|
||||||
|
</span>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -351,7 +351,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const blockquote = lines.map((line) => `> ${line}`).join("\n")
|
const blockquote = lines.map((line) => `> ${line}`).join("\n")
|
||||||
if (!blockquote) return
|
if (!blockquote) return
|
||||||
|
|
||||||
insertBlockContent(`${blockquote}\n`)
|
// End the blockquote with a blank line so the user's next line
|
||||||
|
// doesn't get parsed as a lazy continuation of the quote.
|
||||||
|
insertBlockContent(`${blockquote}\n\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertCodeSelection(rawText: string) {
|
function insertCodeSelection(rawText: string) {
|
||||||
|
|||||||
@@ -287,7 +287,9 @@ export const taskRenderer: ToolRenderer = {
|
|||||||
content: promptContent()!,
|
content: promptContent()!,
|
||||||
cacheKey: "task:prompt",
|
cacheKey: "task:prompt",
|
||||||
disableScrollTracking: true,
|
disableScrollTracking: true,
|
||||||
disableHighlight: true,
|
// Always use the normal markdown render path for prompt (even while running)
|
||||||
|
// so the prompt doesn't visually change between running/completed states.
|
||||||
|
disableHighlight: false,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -96,11 +96,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
|
||||||
"instanceShell.rightPanel.actions.refresh": "Refresh",
|
"instanceShell.rightPanel.actions.refresh": "Refresh",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "The agent's roadmap for this session. Tracks tasks, subtasks, and their completion status.",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Background Shells",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "Background Shells",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Long-running processes started by the agent. You can monitor their output, stop, or terminate them.",
|
||||||
"instanceShell.rightPanel.sections.mcp": "MCP Servers",
|
"instanceShell.rightPanel.sections.mcp": "MCP Servers",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Model Context Protocol servers that extend the agent's capabilities with external tools and services.",
|
||||||
"instanceShell.rightPanel.sections.lsp": "LSP Servers",
|
"instanceShell.rightPanel.sections.lsp": "LSP Servers",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Language Server Protocol servers providing code intelligence, diagnostics, and language-specific features.",
|
||||||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins that customize the UI and server behavior, adding features beyond MCP and LSP.",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "Select a session to view changes.",
|
"instanceShell.sessionChanges.noSessionSelected": "Select a session to view changes.",
|
||||||
"instanceShell.sessionChanges.loading": "Fetching session changes...",
|
"instanceShell.sessionChanges.loading": "Fetching session changes...",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const instanceMessages = {
|
|||||||
"instanceShell.commandPalette.openAriaLabel": "Abrir paleta de comandos",
|
"instanceShell.commandPalette.openAriaLabel": "Abrir paleta de comandos",
|
||||||
"instanceShell.commandPalette.button": "Paleta de comandos",
|
"instanceShell.commandPalette.button": "Paleta de comandos",
|
||||||
|
|
||||||
"instanceShell.connection.ariaLabel": "Connection {status}",
|
"instanceShell.connection.ariaLabel": "Conexión {status}",
|
||||||
"instanceShell.connection.connected": "Conectada",
|
"instanceShell.connection.connected": "Conectada",
|
||||||
"instanceShell.connection.connecting": "Conectando...",
|
"instanceShell.connection.connecting": "Conectando...",
|
||||||
"instanceShell.connection.disconnected": "Desconectada",
|
"instanceShell.connection.disconnected": "Desconectada",
|
||||||
@@ -93,16 +93,22 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.files": "Archivos",
|
"instanceShell.rightPanel.tabs.files": "Archivos",
|
||||||
"instanceShell.rightPanel.tabs.status": "Estado",
|
"instanceShell.rightPanel.tabs.status": "Estado",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesion",
|
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesión",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "Hoja de ruta del agente para esta sesión. Realiza el seguimiento de tareas, subtareas y su estado de finalización.",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en segundo plano",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en segundo plano",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Procesos de larga duración iniciados por el agente. Puedes supervisar su salida, detenerlos o terminarlos.",
|
||||||
"instanceShell.rightPanel.sections.mcp": "Servidores MCP",
|
"instanceShell.rightPanel.sections.mcp": "Servidores MCP",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Servidores del Model Context Protocol (MCP) que amplían las capacidades del agente con herramientas y servicios externos.",
|
||||||
"instanceShell.rightPanel.sections.lsp": "Servidores LSP",
|
"instanceShell.rightPanel.sections.lsp": "Servidores LSP",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Servidores del Language Server Protocol (LSP) que proporcionan inteligencia de código, diagnósticos y funciones específicas del lenguaje.",
|
||||||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins que personalizan el comportamiento de la UI y del servidor, y añaden funciones más allá de MCP y LSP.",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "Selecciona una sesion para ver los cambios.",
|
"instanceShell.sessionChanges.noSessionSelected": "Selecciona una sesión para ver los cambios.",
|
||||||
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesion...",
|
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesión...",
|
||||||
"instanceShell.sessionChanges.empty": "Aun no hay cambios.",
|
"instanceShell.sessionChanges.empty": "Aún no hay cambios.",
|
||||||
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
|
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
|
||||||
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",
|
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",
|
||||||
|
|
||||||
|
|||||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "Statut",
|
"instanceShell.rightPanel.tabs.status": "Statut",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "Feuille de route de l'agent pour cette session. Suit les tâches et leur statut d'achèvement.",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en arrière-plan",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en arrière-plan",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Processus longs démarrés par l'agent. Vous pouvez surveiller leur sortie, les arrêter ou les terminer.",
|
||||||
"instanceShell.rightPanel.sections.mcp": "Serveurs MCP",
|
"instanceShell.rightPanel.sections.mcp": "Serveurs MCP",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Serveurs du protocole Model Context Protocol qui étendent les capacités de l'agent avec des outils externes.",
|
||||||
"instanceShell.rightPanel.sections.lsp": "Serveurs LSP",
|
"instanceShell.rightPanel.sections.lsp": "Serveurs LSP",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Serveurs du protocole Language Server Protocol fournissant l'intelligence de code et les diagnostics.",
|
||||||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins qui personnalisent le comportement de l'UI et du serveur, ajoutant des fonctionnalités au-delà de MCP et LSP.",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "Sélectionnez une session pour voir les changements.",
|
"instanceShell.sessionChanges.noSessionSelected": "Sélectionnez une session pour voir les changements.",
|
||||||
"instanceShell.sessionChanges.loading": "Récupération des changements...",
|
"instanceShell.sessionChanges.loading": "Récupération des changements...",
|
||||||
|
|||||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "ステータス",
|
"instanceShell.rightPanel.tabs.status": "ステータス",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
|
||||||
"instanceShell.rightPanel.sections.plan": "計画",
|
"instanceShell.rightPanel.sections.plan": "計画",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "このセッションにおけるエージェントのロードマップ。タスクやサブタスク、および完了状況を追跡します。",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "バックグラウンドシェル",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "バックグラウンドシェル",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "エージェントが開始した長時間実行プロセス。出力を監視し、停止または終了できます。",
|
||||||
"instanceShell.rightPanel.sections.mcp": "MCP サーバー",
|
"instanceShell.rightPanel.sections.mcp": "MCP サーバー",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Model Context Protocol (MCP) サーバー。外部ツールやサービスでエージェントの機能を拡張します。",
|
||||||
"instanceShell.rightPanel.sections.lsp": "LSP サーバー",
|
"instanceShell.rightPanel.sections.lsp": "LSP サーバー",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Language Server Protocolサーバーがコードインテリジェンス、診断、言語固有の機能を提供します。",
|
||||||
"instanceShell.rightPanel.sections.plugins": "プラグイン",
|
"instanceShell.rightPanel.sections.plugins": "プラグイン",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "UI とサーバーの動作をカスタマイズし、MCP や LSP 以外の機能も追加できるプラグイン。",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "変更を表示するにはセッションを選択してください。",
|
"instanceShell.sessionChanges.noSessionSelected": "変更を表示するにはセッションを選択してください。",
|
||||||
"instanceShell.sessionChanges.loading": "変更を取得中...",
|
"instanceShell.sessionChanges.loading": "変更を取得中...",
|
||||||
|
|||||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "Статус",
|
"instanceShell.rightPanel.tabs.status": "Статус",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
|
||||||
"instanceShell.rightPanel.sections.plan": "План",
|
"instanceShell.rightPanel.sections.plan": "План",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые Shell",
|
"instanceShell.rightPanel.sections.plan.tooltip": "Дорожная карта агента для этой сессии. Отслеживает задачи и их статус выполнения.",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые оболочки",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Долгоработающие процессы, запущенные агентом. Вы можете следить за их выводом, останавливать или завершать их.",
|
||||||
"instanceShell.rightPanel.sections.mcp": "MCP-серверы",
|
"instanceShell.rightPanel.sections.mcp": "MCP-серверы",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Серверы протокола Model Context Protocol, расширяющие возможности агента внешними инструментами.",
|
||||||
"instanceShell.rightPanel.sections.lsp": "LSP-серверы",
|
"instanceShell.rightPanel.sections.lsp": "LSP-серверы",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Серверы протокола Language Server Protocol, обеспечивающие интеллектуальную поддержку кода и диагностику.",
|
||||||
"instanceShell.rightPanel.sections.plugins": "Плагины",
|
"instanceShell.rightPanel.sections.plugins": "Плагины",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "Плагины, настраивающие поведение интерфейса и сервера, добавляющие функции поверх MCP и LSP.",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "Выберите сессию, чтобы просмотреть изменения.",
|
"instanceShell.sessionChanges.noSessionSelected": "Выберите сессию, чтобы просмотреть изменения.",
|
||||||
"instanceShell.sessionChanges.loading": "Загрузка изменений...",
|
"instanceShell.sessionChanges.loading": "Загрузка изменений...",
|
||||||
@@ -128,7 +134,7 @@ export const instanceMessages = {
|
|||||||
"versionPill.uiWithVersion": "UI {version}",
|
"versionPill.uiWithVersion": "UI {version}",
|
||||||
"versionPill.source": " ({source})",
|
"versionPill.source": " ({source})",
|
||||||
|
|
||||||
"opencodeBinarySelector.title": "OpenCode Binary",
|
"opencodeBinarySelector.title": "Бинарник OpenCode",
|
||||||
"opencodeBinarySelector.subtitle": "Выберите, какой исполняемый файл OpenCode запускать",
|
"opencodeBinarySelector.subtitle": "Выберите, какой исполняемый файл OpenCode запускать",
|
||||||
"opencodeBinarySelector.customPath.placeholder": "Введите путь к бинарнику opencode…",
|
"opencodeBinarySelector.customPath.placeholder": "Введите путь к бинарнику opencode…",
|
||||||
"opencodeBinarySelector.actions.add": "Добавить",
|
"opencodeBinarySelector.actions.add": "Добавить",
|
||||||
|
|||||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "状态",
|
"instanceShell.rightPanel.tabs.status": "状态",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
|
||||||
"instanceShell.rightPanel.sections.plan": "计划",
|
"instanceShell.rightPanel.sections.plan": "计划",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "代理的路线图。跟踪任务、子任务及其完成状态。",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "后台 Shell",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "后台 Shell",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "代理启动的后台进程。您可以监控其输出、停止或终止它们。",
|
||||||
"instanceShell.rightPanel.sections.mcp": "MCP 服务器",
|
"instanceShell.rightPanel.sections.mcp": "MCP 服务器",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "模型上下文协议服务器,使用外部工具和服务扩展代理能力。",
|
||||||
"instanceShell.rightPanel.sections.lsp": "LSP 服务器",
|
"instanceShell.rightPanel.sections.lsp": "LSP 服务器",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "语言服务器协议服务器,提供代码智能、诊断和语言特定的功能。",
|
||||||
"instanceShell.rightPanel.sections.plugins": "插件",
|
"instanceShell.rightPanel.sections.plugins": "插件",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "自定义 UI 和服务器行为的插件,添加超出 MCP 和 LSP 的功能。",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "选择会话以查看更改。",
|
"instanceShell.sessionChanges.noSessionSelected": "选择会话以查看更改。",
|
||||||
"instanceShell.sessionChanges.loading": "正在获取会话更改...",
|
"instanceShell.sessionChanges.loading": "正在获取会话更改...",
|
||||||
|
|||||||
29
packages/ui/src/lib/launch-errors.ts
Normal file
29
packages/ui/src/lib/launch-errors.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export function formatLaunchErrorMessage(error: unknown, fallbackMessage: string): string {
|
||||||
|
if (!error) {
|
||||||
|
return fallbackMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as unknown
|
||||||
|
if (parsed && typeof parsed === "object" && "error" in parsed && typeof (parsed as any).error === "string") {
|
||||||
|
return (parsed as any).error
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore JSON parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMissingBinaryMessage(message: string): boolean {
|
||||||
|
const normalized = message.toLowerCase()
|
||||||
|
return (
|
||||||
|
normalized.includes("opencode binary not found") ||
|
||||||
|
normalized.includes("binary not found") ||
|
||||||
|
normalized.includes("no such file or directory") ||
|
||||||
|
normalized.includes("binary is not executable") ||
|
||||||
|
normalized.includes("enoent")
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import { upsertPermissionV2, removePermissionV2, upsertQuestionV2, removeQuestio
|
|||||||
import { clearCacheForInstance } from "../lib/global-cache"
|
import { clearCacheForInstance } from "../lib/global-cache"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata"
|
import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata"
|
||||||
|
import { showWorkspaceLaunchError } from "./launch-errors"
|
||||||
|
|
||||||
const log = getLogger("api")
|
const log = getLogger("api")
|
||||||
|
|
||||||
@@ -372,6 +373,7 @@ function handleWorkspaceEvent(event: WorkspaceEventPayload) {
|
|||||||
break
|
break
|
||||||
case "workspace.error":
|
case "workspace.error":
|
||||||
upsertWorkspace(event.workspace)
|
upsertWorkspace(event.workspace)
|
||||||
|
showWorkspaceLaunchError(event.workspace)
|
||||||
break
|
break
|
||||||
case "workspace.stopped":
|
case "workspace.stopped":
|
||||||
releaseInstanceResources(event.workspaceId)
|
releaseInstanceResources(event.workspaceId)
|
||||||
|
|||||||
53
packages/ui/src/stores/launch-errors.ts
Normal file
53
packages/ui/src/stores/launch-errors.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { createSignal } from "solid-js"
|
||||||
|
import type { WorkspaceDescriptor } from "../../../server/src/api-types"
|
||||||
|
import { tGlobal } from "../lib/i18n"
|
||||||
|
import { formatLaunchErrorMessage, isMissingBinaryMessage } from "../lib/launch-errors"
|
||||||
|
|
||||||
|
type LaunchErrorSource = "create" | "workspace"
|
||||||
|
|
||||||
|
export interface LaunchErrorState {
|
||||||
|
source: LaunchErrorSource
|
||||||
|
message: string
|
||||||
|
binaryPath: string
|
||||||
|
missingBinary: boolean
|
||||||
|
instanceId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const [launchError, setLaunchError] = createSignal<LaunchErrorState | null>(null)
|
||||||
|
|
||||||
|
// Avoid spamming the user with the same modal on repeated events.
|
||||||
|
const lastWorkspaceErrorByInstanceId = new Map<string, string>()
|
||||||
|
|
||||||
|
export function showLaunchError(next: LaunchErrorState) {
|
||||||
|
setLaunchError(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLaunchError() {
|
||||||
|
setLaunchError(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showWorkspaceLaunchError(workspace: WorkspaceDescriptor) {
|
||||||
|
const instanceId = workspace.id
|
||||||
|
const rawMessage = workspace.error
|
||||||
|
const message = formatLaunchErrorMessage(rawMessage, tGlobal("app.launchError.fallbackMessage"))
|
||||||
|
|
||||||
|
const previous = lastWorkspaceErrorByInstanceId.get(instanceId)
|
||||||
|
if (previous && previous === message) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastWorkspaceErrorByInstanceId.set(instanceId, message)
|
||||||
|
|
||||||
|
const binaryPath = (workspace.binaryLabel || workspace.binaryId || "opencode").trim() || "opencode"
|
||||||
|
const missingBinary = isMissingBinaryMessage(message)
|
||||||
|
|
||||||
|
showLaunchError({
|
||||||
|
source: "workspace",
|
||||||
|
instanceId,
|
||||||
|
message,
|
||||||
|
binaryPath,
|
||||||
|
missingBinary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { launchError }
|
||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
} from "./instances"
|
} from "./instances"
|
||||||
import { showAlertDialog } from "./alerts"
|
import { showAlertDialog } from "./alerts"
|
||||||
import { createClientSession, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
import { createClientSession, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
||||||
import { sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
import { ensureSessionParentExpanded, sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
||||||
import { normalizeMessagePart } from "./message-v2/normalizers"
|
import { normalizeMessagePart } from "./message-v2/normalizers"
|
||||||
import { updateSessionInfo } from "./message-v2/session-info"
|
import { updateSessionInfo } from "./message-v2/session-info"
|
||||||
import { tGlobal } from "../lib/i18n"
|
import { tGlobal } from "../lib/i18n"
|
||||||
@@ -108,6 +108,8 @@ interface TuiToastEvent {
|
|||||||
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
||||||
|
|
||||||
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
|
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
|
||||||
|
let parentToExpand: string | null = null
|
||||||
|
|
||||||
withSession(instanceId, sessionId, (session) => {
|
withSession(instanceId, sessionId, (session) => {
|
||||||
const current = session.status ?? "idle"
|
const current = session.status ?? "idle"
|
||||||
if (current === status) return false
|
if (current === status) return false
|
||||||
@@ -117,7 +119,17 @@ function applySessionStatus(instanceId: string, sessionId: string, status: Sessi
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.status = status
|
session.status = status
|
||||||
|
|
||||||
|
// Auto-expand the parent thread when a child session starts working.
|
||||||
|
// Users can still collapse it; we only expand on the transition.
|
||||||
|
if (session.parentId && status === "working" && current !== "working") {
|
||||||
|
parentToExpand = session.parentId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (parentToExpand) {
|
||||||
|
ensureSessionParentExpanded(instanceId, parentToExpand)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSessionInfo(instanceId: string, sessionId: string, directory?: string): Promise<Session | null> {
|
async function fetchSessionInfo(instanceId: string, sessionId: string, directory?: string): Promise<Session | null> {
|
||||||
@@ -158,6 +170,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
|||||||
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
|
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
|
||||||
|
|
||||||
let updatedInstanceSessions: Map<string, Session> | undefined
|
let updatedInstanceSessions: Map<string, Session> | undefined
|
||||||
|
let shouldExpandParent: string | null = null
|
||||||
|
|
||||||
setSessions((prev) => {
|
setSessions((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
@@ -174,11 +187,19 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
|||||||
instanceSessions.set(sessionId, merged)
|
instanceSessions.set(sessionId, merged)
|
||||||
next.set(instanceId, instanceSessions)
|
next.set(instanceId, instanceSessions)
|
||||||
updatedInstanceSessions = instanceSessions
|
updatedInstanceSessions = instanceSessions
|
||||||
|
|
||||||
|
if (merged.parentId && merged.status === "working" && (existing?.status ?? "idle") !== "working") {
|
||||||
|
shouldExpandParent = merged.parentId
|
||||||
|
}
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)
|
syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)
|
||||||
|
|
||||||
|
if (shouldExpandParent) {
|
||||||
|
ensureSessionParentExpanded(instanceId, shouldExpandParent)
|
||||||
|
}
|
||||||
|
|
||||||
return fetched
|
return fetched
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to fetch session info", error)
|
log.error("Failed to fetch session info", error)
|
||||||
|
|||||||
@@ -347,10 +347,23 @@ function clearActiveParentSession(instanceId: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setSessionStatus(instanceId: string, sessionId: string, status: SessionStatus): void {
|
function setSessionStatus(instanceId: string, sessionId: string, status: SessionStatus): void {
|
||||||
|
let parentToExpand: string | null = null
|
||||||
|
|
||||||
withSession(instanceId, sessionId, (session) => {
|
withSession(instanceId, sessionId, (session) => {
|
||||||
if (session.status === status) return false
|
if (session.status === status) return false
|
||||||
|
const previous = session.status
|
||||||
session.status = status
|
session.status = status
|
||||||
|
|
||||||
|
// If a child session starts working, auto-expand its parent thread once.
|
||||||
|
// Users can still collapse it afterwards; we only expand on the transition.
|
||||||
|
if (session.parentId && status === "working" && previous !== "working") {
|
||||||
|
parentToExpand = session.parentId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (parentToExpand) {
|
||||||
|
ensureSessionParentExpanded(instanceId, parentToExpand)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveParentSession(instanceId: string): Session | null {
|
function getActiveParentSession(instanceId: string): Session | null {
|
||||||
|
|||||||
@@ -132,13 +132,15 @@
|
|||||||
|
|
||||||
/* Make the command palette trigger stand out in the header. */
|
/* Make the command palette trigger stand out in the header. */
|
||||||
.connection-status-button.command-palette-button {
|
.connection-status-button.command-palette-button {
|
||||||
border-color: var(--accent-primary);
|
border-radius: 0;
|
||||||
background-color: var(--surface-secondary);
|
@apply text-sm px-2 py-1 border border-base transition-colors;
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-status-button.command-palette-button:hover {
|
.connection-status-button.command-palette-button:hover {
|
||||||
border-color: var(--accent-primary);
|
|
||||||
background-color: var(--surface-hover);
|
background-color: var(--surface-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-status-button:hover {
|
.connection-status-button:hover {
|
||||||
|
|||||||
@@ -412,7 +412,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.right-panel-accordion-trigger {
|
.right-panel-accordion-trigger {
|
||||||
@apply w-full flex items-center justify-between gap-3 px-3 py-2.5 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-150;
|
@apply w-full flex items-center justify-between px-3 py-2.5 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-150;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
@@ -422,6 +422,11 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-left {
|
||||||
|
@apply flex items-center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.right-panel-accordion-chevron {
|
.right-panel-accordion-chevron {
|
||||||
@apply h-4 w-4 transition-transform duration-200;
|
@apply h-4 w-4 transition-transform duration-200;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -441,6 +446,51 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Section info tooltip */
|
||||||
|
.section-info-trigger {
|
||||||
|
@apply inline-flex items-center justify-center p-0.5 rounded transition-all duration-150;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-info-trigger:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-info-icon {
|
||||||
|
@apply w-3.5 h-3.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-info-tooltip {
|
||||||
|
@apply max-w-xs px-3 py-2 text-xs rounded-lg border shadow-lg;
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
border-color: var(--border-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
animation: tooltipShow 150ms ease-out;
|
||||||
|
transform-origin: var(--kb-tooltip-content-transform-origin);
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-info-tooltip[data-expanded] {
|
||||||
|
animation: tooltipShow 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltipShow {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Background process cards in status panel */
|
/* Background process cards in status panel */
|
||||||
.status-process-card {
|
.status-process-card {
|
||||||
@apply rounded-lg border flex flex-col gap-2 p-3 transition-all duration-150;
|
@apply rounded-lg border flex flex-col gap-2 p-3 transition-all duration-150;
|
||||||
|
|||||||
Reference in New Issue
Block a user