Compare commits

...

31 Commits

Author SHA1 Message Date
Shantur Rathore
90baefbb7e fix(ci): rezip Electron macOS zips with ditto
Add a codesign verify step on extracted artifacts to catch signature/resource mismatches before upload.
2026-02-23 08:54:57 +00:00
Shantur Rathore
1c138f4489 Merge pull request #197 from VooDisss/issue-195
fix: Use legacy diff algorithm for better large file performance
2026-02-23 08:27:11 +00:00
VooDisss
d36e568ed0 fix: Use legacy diff algorithm for better large file performance
- Set diffAlgorithm to 'legacy' for Monaco DiffEditor
- Add maxComputationTime of 10s to avoid UI freeze on huge files

This addresses the issue where sessions with large JSON files (50k-100k+ lines)
would cause the UI to freeze. The 'legacy' algorithm is faster than 'advanced'
for large files, similar to VSCode's workaround for the same issue.

See: https://github.com/microsoft/vscode/issues/184037
2026-02-23 02:30:44 +02:00
Shantur Rathore
d6462ef524 Min version 0.11.4 2026-02-22 17:32:28 +00:00
Shantur Rathore
a06884ebce Bump to v0.11.4 2026-02-22 16:53:51 +00:00
Shantur Rathore
62bd88f6a4 chore(plugin): Upgrade dependency version 2026-02-22 16:48:49 +00:00
Shantur Rathore
6479561779 fix(ui): auto-expand session thread when child starts working 2026-02-22 16:47:04 +00:00
Shantur Rathore
635237c258 fix(ui): render task prompt consistently while running 2026-02-22 08:58:39 +00:00
Shantur Rathore
33f0aa5714 ci: run dev prerelease nightly
Replace dev push builds with nightly schedule that only runs when dev head advances; still runs on manual dispatch. Plumb a ref input through reusable workflows so scheduled runs build the dev commit.
2026-02-20 13:58:32 +00:00
Shantur Rathore
7ca6285d58 Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev 2026-02-20 13:49:03 +00:00
Shantur Rathore
14c60fef6c Merge pull request #192 from VooDisss/issue-144
[QOL] Add informational tooltips to Status Panel sections
2026-02-20 13:47:11 +00:00
Shantur Rathore
336de6a19e fix(i18n): polish Status panel tooltip translations 2026-02-20 13:46:43 +00:00
Shantur Rathore
377c8e2249 Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev 2026-02-20 13:31:52 +00:00
VooDisss
697dea21f8 Add informational tooltips to Status Panel sections 2026-02-20 14:09:54 +02:00
Shantur Rathore
34d3f803d5 Merge pull request #191 from kvokka/improve-docs
Clarify CLI_WORKSPACE_ROOT usage for worktrees
2026-02-20 11:15:01 +00:00
kvokka
f824a063a5 docs: clarify CLI_WORKSPACE_ROOT usage for worktrees\n\nFixes #184 2026-02-20 14:52:05 +04:00
Shantur Rathore
5fabf286e8 ui: restyle command palette button 2026-02-20 00:32:44 +00:00
Shantur Rathore
e8947d61b1 ui: emphasize command palette button 2026-02-20 00:32:39 +00:00
Shantur Rathore
1ccd14eae8 ui: use Check icon for completed status 2026-02-20 00:32:27 +00:00
Shantur Rathore
b162764ccb ui: use lucide status icons for tool calls 2026-02-20 00:32:15 +00:00
Shantur Rathore
2124e540aa Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev 2026-02-19 23:54:31 +00:00
Shantur Rathore
b5790998b7 ui: use emoji status icons for tool calls 2026-02-19 23:51:25 +00:00
codenomadbot[bot]
9800afb785 feat(ui): toggle tool call input YAML (#182)
* feat(ui): toggle tool call input yaml

* ui: rename tool input toggle and add IO headers

* ui: add input/output accordions in tool calls

* ui: refine tool IO accordion styling

* ui: remove extra padding around IO sections

* ui: remove semibold from IO headers

* feat(ui): add tool input visibility preference

* fix(ui): scope tool input toggle to current tool call

* ui: left-align tool IO header text

* fix(ui): let palette tool input visibility override per-call

* ui: default tool input visibility to collapsed

* fix(ui): expand read tool calls on error

---------

Co-authored-by: Shantur Rathore <i@shantur.com>
2026-02-19 22:08:41 +00:00
Shantur Rathore
3b73d9d5b9 fix(ui): show workspace launch errors in dialog 2026-02-19 15:40:58 +00:00
Shantur Rathore
f7ac30afe3 revert(ui): restore compact alert dialog 2026-02-19 15:40:55 +00:00
Shantur Rathore
ce370d5100 fix(server): read OpenCode version from /global/health 2026-02-19 14:21:13 +00:00
Shantur Rathore
c639e535b5 fix(ui): add blank line after inserted quotes 2026-02-19 10:40:51 +00:00
Shantur Rathore
e84adebe61 fix(server): detect OpenCode version via spawn spec 2026-02-19 07:24:14 +00:00
Shantur Rathore
d45a1ff078 Bump to v0.11.3 2026-02-18 19:59:54 +00:00
Shantur Rathore
b4121696bb fix(ui): track worktree context for question replies
Store the originating worktree slug when questions are enqueued and use
the stored worktree client when replying/rejecting from the global
permission center. This ensures question responses are sent through the
correct worktree, matching the behavior already implemented for
permissions.
2026-02-18 19:56:42 +00:00
Shantur Rathore
f75c942162 fix(ui): exclude hidden agents from pickers 2026-02-18 16:00:58 +00:00
60 changed files with 968 additions and 207 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

15
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.11.2", "version": "0.11.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.11.2", "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.2", "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.2", "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.2", "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.2", "version": "0.11.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@git-diff-view/solid": "^0.0.8", "@git-diff-view/solid": "^0.0.8",
@@ -12092,7 +12092,8 @@
"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" "tauri-plugin-keepawake-api": "^0.1.0",
"yaml": "^2.4.2"
}, },
"devDependencies": { "devDependencies": {
"@vite-pwa/assets-generator": "^1.0.2", "@vite-pwa/assets-generator": "^1.0.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.11.2", "version": "0.11.4",
"private": true, "private": true,
"description": "CodeNomad monorepo workspace", "description": "CodeNomad monorepo workspace",
"license": "MIT", "license": "MIT",

View File

@@ -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"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@neuralnomads/codenomad-electron-app", "name": "@neuralnomads/codenomad-electron-app",
"version": "0.11.2", "version": "0.11.4",
"description": "CodeNomad - AI coding assistant", "description": "CodeNomad - AI coding assistant",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -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"
} }
} }

View File

@@ -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.)

View File

@@ -1,12 +1,12 @@
{ {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.11.2", "version": "0.11.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.11.2", "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",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.11.2", "version": "0.11.4",
"description": "CodeNomad Server", "description": "CodeNomad Server",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -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))

View File

@@ -1,7 +1,6 @@
import { FastifyInstance } from "fastify" import { FastifyInstance } from "fastify"
import { z } from "zod" import { z } from "zod"
import { spawnSync } from "child_process" import { probeBinaryVersion } from "../../workspaces/runtime"
import { buildSpawnSpec } from "../../workspaces/runtime"
import type { SettingsService } from "../../settings/service" import type { SettingsService } from "../../settings/service"
import type { Logger } from "../../logger" import type { Logger } from "../../logger"
@@ -15,37 +14,8 @@ const ValidateBinarySchema = z.object({
}) })
function validateBinaryPath(binaryPath: string): { valid: boolean; version?: string; error?: string } { function validateBinaryPath(binaryPath: string): { valid: boolean; version?: string; error?: string } {
if (!binaryPath) { const result = probeBinaryVersion(binaryPath)
return { valid: false, error: "Missing binary path" } return { valid: result.valid, version: result.version, error: result.error }
}
const spec = buildSpawnSpec(binaryPath, ["--version"])
try {
const result = spawnSync(spec.command, spec.args, {
encoding: "utf8",
windowsVerbatimArguments: Boolean((spec.options as { windowsVerbatimArguments?: boolean }).windowsVerbatimArguments),
})
if (result.error) {
return { valid: false, error: result.error.message }
}
if (result.status !== 0) {
const stderr = result.stderr?.trim()
const stdout = result.stdout?.trim()
const combined = stderr || stdout
const error = combined ? `Exited with code ${result.status}: ${combined}` : `Exited with code ${result.status}`
return { valid: false, error }
}
const stdout = (result.stdout ?? "").trim()
const firstLine = stdout.split(/\r?\n/).find((line) => line.trim().length > 0)
const normalized = firstLine?.trim()
const versionMatch = normalized?.match(/([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/)
const version = versionMatch?.[1]
return { valid: true, version }
} catch (error) {
return { valid: false, error: error instanceof Error ? error.message : String(error) }
}
} }
export function registerSettingsRoutes(app: FastifyInstance, deps: RouteDeps) { export function registerSettingsRoutes(app: FastifyInstance, deps: RouteDeps) {

View File

@@ -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,42 +277,12 @@ export class WorkspaceManager {
return candidates[0] ?? "" return candidates[0] ?? ""
} }
private detectBinaryVersion(resolvedPath: string): string | undefined {
if (!resolvedPath) {
return undefined
}
try {
const result = spawnSync(resolvedPath, ["--version"], { encoding: "utf8" })
if (result.status === 0 && result.stdout) {
const line = result.stdout.split(/\r?\n/).find((entry) => entry.trim().length > 0)
if (line) {
const normalized = line.trim()
const versionMatch = normalized.match(/([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/)
if (versionMatch) {
const version = versionMatch[1]
this.options.logger.debug({ binary: resolvedPath, version }, "Detected binary version")
return version
}
this.options.logger.debug({ binary: resolvedPath, reported: normalized }, "Binary reported version string")
return normalized
}
} else if (result.error) {
this.options.logger.warn({ binary: resolvedPath, err: result.error }, "Failed to read binary version")
}
} catch (error) {
this.options.logger.warn({ binary: resolvedPath, err: 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),
@@ -327,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),
@@ -340,6 +309,8 @@ export class WorkspaceManager {
) )
}), }),
]) ])
return version
} }
private async waitForInstanceHealth(params: { private async waitForInstanceHealth(params: {
@@ -347,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) => {
@@ -361,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()
@@ -372,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> = {}
@@ -384,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")

View File

@@ -8,6 +8,8 @@ import { Logger } from "../logger"
export const WINDOWS_CMD_EXTENSIONS = new Set([".cmd", ".bat"]) export const WINDOWS_CMD_EXTENSIONS = new Set([".cmd", ".bat"])
export const WINDOWS_POWERSHELL_EXTENSIONS = new Set([".ps1"]) export const WINDOWS_POWERSHELL_EXTENSIONS = new Set([".ps1"])
const VERSION_REGEX = /([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/
export function buildSpawnSpec(binaryPath: string, args: string[]) { export function buildSpawnSpec(binaryPath: string, args: string[]) {
if (process.platform !== "win32") { if (process.platform !== "win32") {
return { command: binaryPath, args, options: {} as const } return { command: binaryPath, args, options: {} as const }
@@ -40,6 +42,61 @@ export function buildSpawnSpec(binaryPath: string, args: string[]) {
return { command: binaryPath, args, options: {} as const } return { command: binaryPath, args, options: {} as const }
} }
export function probeBinaryVersion(binaryPath: string): {
valid: boolean
version?: string
reported?: string
error?: string
} {
if (!binaryPath) {
return { valid: false, error: "Missing binary path" }
}
const spec = buildSpawnSpec(binaryPath, ["--version"])
try {
const result = spawnSync(spec.command, spec.args, {
encoding: "utf8",
windowsVerbatimArguments: Boolean(
(spec.options as { windowsVerbatimArguments?: boolean }).windowsVerbatimArguments,
),
})
if (result.error) {
return { valid: false, error: result.error.message }
}
if (result.status !== 0) {
const stderr = result.stderr?.trim()
const stdout = result.stdout?.trim()
const combined = stderr || stdout
const error = combined ? `Exited with code ${result.status}: ${combined}` : `Exited with code ${result.status}`
return { valid: false, error }
}
const stdoutLines = String(result.stdout ?? "")
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
const stderrLines = String(result.stderr ?? "")
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
// Prefer stdout; fall back to stderr (some tools report version there).
const reported = stdoutLines[0] ?? stderrLines[0]
if (!reported) {
return { valid: true }
}
const versionMatch = reported.match(VERSION_REGEX)
const version = versionMatch?.[1]
return { valid: true, version, reported }
} catch (error) {
return { valid: false, error: error instanceof Error ? error.message : String(error) }
}
}
const SENSITIVE_ENV_KEY = /(PASSWORD|TOKEN|SECRET)/i const SENSITIVE_ENV_KEY = /(PASSWORD|TOKEN|SECRET)/i
function redactEnvironment(env: Record<string, string | undefined>): Record<string, string | undefined> { function redactEnvironment(env: Record<string, string | undefined>): Record<string, string | undefined> {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@codenomad/tauri-app", "name": "@codenomad/tauri-app",
"version": "0.11.2", "version": "0.11.4",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@codenomad/ui", "name": "@codenomad/ui",
"version": "0.11.2", "version": "0.11.4",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
@@ -30,7 +30,8 @@
"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" "tauri-plugin-keepawake-api": "^0.1.0",
"yaml": "^2.4.2"
}, },
"devDependencies": { "devDependencies": {
"@vite-pwa/assets-generator": "^1.0.2", "@vite-pwa/assets-generator": "^1.0.2",

View File

@@ -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"
@@ -72,14 +74,9 @@ const App: Component = () => {
setToolOutputExpansion, setToolOutputExpansion,
setDiagnosticsExpansion, setDiagnosticsExpansion,
setThinkingBlocksExpansion, setThinkingBlocksExpansion,
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)
@@ -244,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
@@ -291,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)
@@ -402,6 +366,7 @@ const App: Component = () => {
setToolOutputExpansion, setToolOutputExpansion,
setDiagnosticsExpansion, setDiagnosticsExpansion,
setThinkingBlocksExpansion, setThinkingBlocksExpansion,
setToolInputsVisibility,
handleNewInstanceRequest, handleNewInstanceRequest,
handleCloseInstance, handleCloseInstance,
handleNewSession, handleNewSession,

View File

@@ -31,10 +31,10 @@ export default function AgentSelector(props: AgentSelectorProps) {
const availableAgents = createMemo(() => { const availableAgents = createMemo(() => {
const allAgents = instanceAgents() const allAgents = instanceAgents()
if (isChildSession()) { if (isChildSession()) {
return allAgents return allAgents.filter((agent) => !agent.hidden)
} }
const filtered = allAgents.filter((agent) => agent.mode !== "subagent") const filtered = allAgents.filter((agent) => !agent.hidden && agent.mode !== "subagent")
const currentAgent = allAgents.find((a) => a.name === props.currentAgent) const currentAgent = allAgents.find((a) => a.name === props.currentAgent)
if (currentAgent && !filtered.find((a) => a.name === props.currentAgent)) { if (currentAgent && !filtered.find((a) => a.name === props.currentAgent)) {
@@ -103,10 +103,10 @@ export default function AgentSelector(props: AgentSelectorProps) {
> >
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<Select.Value<Agent>> <Select.Value<Agent>>
{(state) => ( {() => (
<div class="selector-trigger-label selector-trigger-label--stacked"> <div class="selector-trigger-label selector-trigger-label--stacked">
<span class="selector-trigger-primary selector-trigger-primary--align-left"> <span class="selector-trigger-primary selector-trigger-primary--align-left">
{t("agentSelector.trigger.primary", { agent: state.selectedOption()?.name ?? t("agentSelector.none") })} {t("agentSelector.trigger.primary", { agent: props.currentAgent || t("agentSelector.none") })}
</span> </span>
</div> </div>
)} )}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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 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 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" }}

View File

@@ -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" : ""}`}
/> />

View File

@@ -51,7 +51,7 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
<div class="connection-status-shortcut-action"> <div class="connection-status-shortcut-action">
<button <button
type="button" type="button"
class="connection-status-button" class="connection-status-button command-palette-button"
onClick={props.onCommandPalette} onClick={props.onCommandPalette}
aria-label={t("messageListHeader.commandPalette.ariaLabel")} aria-label={t("messageListHeader.commandPalette.ariaLabel")}
> >

View File

@@ -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) {

View File

@@ -1,5 +1,6 @@
import { createSignal, Show, createEffect, createMemo, onCleanup } from "solid-js" import { createSignal, Show, createEffect, createMemo, onCleanup } from "solid-js"
import { Copy } from "lucide-solid" import { ArrowRightSquare, Check, Copy, Hourglass, Loader2, XCircle } from "lucide-solid"
import { stringify as stringifyYaml } from "yaml"
import { messageStoreBus } from "../stores/message-v2/bus" import { messageStoreBus } from "../stores/message-v2/bus"
import { useTheme } from "../lib/theme" import { useTheme } from "../lib/theme"
import { useGlobalCache } from "../lib/hooks/use-global-cache" import { useGlobalCache } from "../lib/hooks/use-global-cache"
@@ -27,7 +28,17 @@ import type {
ToolRendererContext, ToolRendererContext,
ToolScrollHelpers, ToolScrollHelpers,
} from "./tool-call/types" } from "./tool-call/types"
import { getRelativePath, getToolIcon, getToolName, isToolStateCompleted, isToolStateError, isToolStateRunning, getDefaultToolAction } from "./tool-call/utils" import {
ensureMarkdownContent,
getRelativePath,
getToolIcon,
getToolName,
isToolStateCompleted,
isToolStateError,
isToolStateRunning,
getDefaultToolAction,
readToolStatePayload,
} from "./tool-call/utils"
import { resolveTitleForTool } from "./tool-call/tool-title" import { resolveTitleForTool } from "./tool-call/tool-title"
import { getLogger } from "../lib/logger" import { getLogger } from "../lib/logger"
@@ -155,12 +166,33 @@ export default function ToolCall(props: ToolCallProps) {
const prefExpanded = toolOutputDefaultExpanded() const prefExpanded = toolOutputDefaultExpanded()
const toolName = toolCallMemo()?.tool || "" const toolName = toolCallMemo()?.tool || ""
if (toolName === "read") { if (toolName === "read") {
const state = toolState()
if (state?.status === "error") {
return true
}
return false return false
} }
return prefExpanded return prefExpanded
}) })
const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null) const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null)
const toolInputsVisibility = createMemo(() => preferences().toolInputsVisibility || "collapsed")
const [toolInputVisibilityOverride, setToolInputVisibilityOverride] = createSignal<"hidden" | "expanded" | null>(null)
const effectiveToolInputsVisibility = createMemo(() => toolInputVisibilityOverride() ?? toolInputsVisibility())
const isToolInputVisible = createMemo(() => effectiveToolInputsVisibility() !== "hidden")
const inputDefaultExpanded = createMemo(() => effectiveToolInputsVisibility() === "expanded")
const [inputSectionOverride, setInputSectionOverride] = createSignal<boolean | null>(null)
const [outputSectionOverride, setOutputSectionOverride] = createSignal<boolean | null>(null)
const inputSectionExpanded = () => {
const override = inputSectionOverride()
if (override !== null) return override
return inputDefaultExpanded()
}
const outputSectionExpanded = () => {
const override = outputSectionOverride()
if (override !== null) return override
return true
}
const isPermissionActive = createMemo(() => { const isPermissionActive = createMemo(() => {
const pending = pendingPermission() const pending = pendingPermission()
@@ -183,6 +215,35 @@ export default function ToolCall(props: ToolCallProps) {
return defaultExpandedForTool() return defaultExpandedForTool()
} }
const toolInput = createMemo(() => {
const state = toolState()
return readToolStatePayload(state).input
})
const hasToolInput = createMemo(() => {
const input = toolInput()
return input && Object.keys(input).length > 0
})
const toolInputMarkdown = createMemo(() => {
const input = toolInput()
if (!input || Object.keys(input).length === 0) return null
try {
const yamlText = stringifyYaml(input)
return ensureMarkdownContent(yamlText, "yaml", true)
} catch (error) {
log.error("Failed to convert tool call input to YAML", error)
try {
const jsonText = JSON.stringify(input, null, 2)
return ensureMarkdownContent(jsonText, "json", true)
} catch (nestedError) {
log.error("Failed to stringify tool call input", nestedError)
return null
}
}
})
const permissionDetails = createMemo(() => pendingPermission()?.permission) const permissionDetails = createMemo(() => pendingPermission()?.permission)
const questionDetails = createMemo(() => pendingQuestion()?.request) const questionDetails = createMemo(() => pendingQuestion()?.request)
@@ -515,13 +576,13 @@ export default function ToolCall(props: ToolCallProps) {
const status = toolState()?.status || "" const status = toolState()?.status || ""
switch (status) { switch (status) {
case "pending": case "pending":
return "⏸" return <Hourglass class="w-4 h-4" />
case "running": case "running":
return "⏳" return <Loader2 class="w-4 h-4 animate-spin" />
case "completed": case "completed":
return "✓" return <Check class="w-4 h-4" />
case "error": case "error":
return "✗" return <XCircle class="w-4 h-4" />
default: default:
return "" return ""
} }
@@ -548,6 +609,25 @@ export default function ToolCall(props: ToolCallProps) {
}) })
} }
createEffect(() => {
// When global preference changes, reset per-tool-call overrides so palette changes apply.
toolInputsVisibility()
setToolInputVisibilityOverride(null)
setInputSectionOverride(null)
setOutputSectionOverride(null)
})
const handleToggleInputVisibility = (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
if (!expanded()) {
toggle()
}
const currentlyVisible = isToolInputVisible()
setToolInputVisibilityOverride(currentlyVisible ? "hidden" : "expanded")
}
const renderer = createMemo(() => resolveToolRenderer(toolName())) const renderer = createMemo(() => resolveToolRenderer(toolName()))
const { renderAnsiContent } = createAnsiContentRenderer({ const { renderAnsiContent } = createAnsiContentRenderer({
@@ -789,6 +869,23 @@ export default function ToolCall(props: ToolCallProps) {
</span> </span>
</button> </button>
<Show when={hasToolInput()}>
<button
type="button"
class="tool-call-header-input"
onClick={handleToggleInputVisibility}
aria-pressed={isToolInputVisible()}
aria-label={
isToolInputVisible()
? t("toolCall.header.hideInputAriaLabel")
: t("toolCall.header.showInputAriaLabel")
}
title={isToolInputVisible() ? t("toolCall.header.hideInputTitle") : t("toolCall.header.showInputTitle")}
>
<ArrowRightSquare class="w-3.5 h-3.5" />
</button>
</Show>
<button <button
type="button" type="button"
class="tool-call-header-copy" class="tool-call-header-copy"
@@ -806,19 +903,79 @@ export default function ToolCall(props: ToolCallProps) {
{expanded() && ( {expanded() && (
<div class="tool-call-details"> <div class="tool-call-details">
{renderToolBody()} <Show
when={isToolInputVisible() && hasToolInput()}
{renderError()} fallback={
<>
{renderPermissionBlock()} {renderToolBody()}
{renderQuestionBlock()} {renderError()}
<Show when={status() === "pending" && !pendingPermission()}> <Show when={status() === "pending" && !pendingPermission()}>
<div class="tool-call-pending-message"> <div class="tool-call-pending-message">
<span class="spinner-small"></span> <span class="spinner-small"></span>
<span>{t("toolCall.pending.waitingToRun")}</span> <span>{t("toolCall.pending.waitingToRun")}</span>
</div>
</Show>
</>
}
>
<div class="tool-call-io-sections">
<div class="tool-call-io-section">
<button
type="button"
class="tool-call-io-toggle"
aria-expanded={inputSectionExpanded()}
onClick={() => setInputSectionOverride((prev) => {
const current = prev === null ? inputSectionExpanded() : prev
return !current
})}
>
<span class="tool-call-io-title">{t("toolCall.io.input")}</span>
</button>
<Show when={inputSectionExpanded()}>
<div class="tool-call-io-body">
{(() => {
const content = toolInputMarkdown()
if (!content) return null
return renderMarkdownContent({ content, cacheKey: "input" })
})()}
</div>
</Show>
</div>
<div class="tool-call-io-section">
<button
type="button"
class="tool-call-io-toggle"
aria-expanded={outputSectionExpanded()}
onClick={() => setOutputSectionOverride((prev) => {
const current = prev === null ? outputSectionExpanded() : prev
return !current
})}
>
<span class="tool-call-io-title">{t("toolCall.io.output")}</span>
</button>
<Show when={outputSectionExpanded()}>
<div class="tool-call-io-body">
{renderToolBody()}
{renderError()}
<Show when={status() === "pending" && !pendingPermission()}>
<div class="tool-call-pending-message">
<span class="spinner-small"></span>
<span>{t("toolCall.pending.waitingToRun")}</span>
</div>
</Show>
</div>
</Show>
</div>
</div> </div>
</Show> </Show>
{renderPermissionBlock()}
{renderQuestionBlock()}
</div> </div>
)} )}

View File

@@ -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>

View File

@@ -287,13 +287,14 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
if (mode() !== "mention") return if (mode() !== "mention") return
const query = props.searchQuery.toLowerCase() const query = props.searchQuery.toLowerCase()
const visibleAgents = props.agents.filter((agent) => !agent.hidden)
const filtered = query const filtered = query
? props.agents.filter( ? visibleAgents.filter(
(agent) => (agent) =>
agent.name.toLowerCase().includes(query) || agent.name.toLowerCase().includes(query) ||
(agent.description && agent.description.toLowerCase().includes(query)), (agent.description && agent.description.toLowerCase().includes(query)),
) )
: props.agents : visibleAgents
setFilteredAgents(filtered) setFilteredAgents(filtered)
}) })

View File

@@ -1,6 +1,6 @@
import { createSignal, onMount } from "solid-js" import { createSignal, onMount } from "solid-js"
import type { Accessor } from "solid-js" import type { Accessor } from "solid-js"
import type { Preferences, ExpansionPreference } from "../../stores/preferences" import type { Preferences, ExpansionPreference, ToolInputsVisibilityPreference } from "../../stores/preferences"
import { createCommandRegistry, type Command } from "../commands" import { createCommandRegistry, type Command } from "../commands"
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances" import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
import type { ClientPart, MessageInfo } from "../../types/message" import type { ClientPart, MessageInfo } from "../../types/message"
@@ -38,6 +38,7 @@ export interface UseCommandsOptions {
setToolOutputExpansion: (mode: ExpansionPreference) => void setToolOutputExpansion: (mode: ExpansionPreference) => void
setDiagnosticsExpansion: (mode: ExpansionPreference) => void setDiagnosticsExpansion: (mode: ExpansionPreference) => void
setThinkingBlocksExpansion: (mode: ExpansionPreference) => void setThinkingBlocksExpansion: (mode: ExpansionPreference) => void
setToolInputsVisibility: (mode: ToolInputsVisibilityPreference) => void
handleNewInstanceRequest: () => void handleNewInstanceRequest: () => void
handleCloseInstance: (instanceId: string) => Promise<void> handleCloseInstance: (instanceId: string) => Promise<void>
handleNewSession: (instanceId: string) => Promise<void> handleNewSession: (instanceId: string) => Promise<void>
@@ -551,6 +552,29 @@ export function useCommands(options: UseCommandsOptions) {
}, },
}) })
commandRegistry.register({
id: "tool-inputs-visibility",
label: () => {
const mode = options.preferences().toolInputsVisibility || "hidden"
const state =
mode === "expanded"
? tGlobal("commands.common.expanded")
: mode === "collapsed"
? tGlobal("commands.common.collapsed")
: tGlobal("commands.common.hidden")
return tGlobal("commands.toolInputsVisibility.label", { state })
},
description: () => tGlobal("commands.toolInputsVisibility.description"),
category: "System",
keywords: () => splitKeywords("commands.toolInputsVisibility.keywords"),
action: () => {
const mode = options.preferences().toolInputsVisibility || "hidden"
const next: ToolInputsVisibilityPreference =
mode === "hidden" ? "collapsed" : mode === "collapsed" ? "expanded" : "hidden"
options.setToolInputsVisibility(next)
},
})
commandRegistry.register({ commandRegistry.register({
id: "token-usage-visibility", id: "token-usage-visibility",
label: () => { label: () => {

View File

@@ -130,6 +130,10 @@ export const commandMessages = {
"commands.diagnosticsDefault.description": "Toggle default expansion for diagnostics output", "commands.diagnosticsDefault.description": "Toggle default expansion for diagnostics output",
"commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse", "commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse",
"commands.toolInputsVisibility.label": "Tool Inputs Visibility · {state}",
"commands.toolInputsVisibility.description": "Set default visibility for tool call input arguments",
"commands.toolInputsVisibility.keywords": "tool, inputs, arguments, visibility, hide, show, expand, collapse",
"commands.tokenUsageDisplay.label": "Token Usage Display · {state}", "commands.tokenUsageDisplay.label": "Token Usage Display · {state}",
"commands.tokenUsageDisplay.description": "Show or hide token and cost stats for assistant messages", "commands.tokenUsageDisplay.description": "Show or hide token and cost stats for assistant messages",
"commands.tokenUsageDisplay.keywords": "token, usage, cost, stats", "commands.tokenUsageDisplay.keywords": "token, usage, cost, stats",

View File

@@ -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...",

View File

@@ -5,6 +5,14 @@ export const toolCallMessages = {
"toolCall.header.copyTitle": "Copy tool call title", "toolCall.header.copyTitle": "Copy tool call title",
"toolCall.header.copyAriaLabel": "Copy tool call title", "toolCall.header.copyAriaLabel": "Copy tool call title",
"toolCall.header.showInputTitle": "Show Tool Arguments",
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
"toolCall.io.input": "Tool Input",
"toolCall.io.output": "Tool Output",
"toolCall.diff.label": "Diff", "toolCall.diff.label": "Diff",
"toolCall.diff.label.withPath": "Diff · {path}", "toolCall.diff.label.withPath": "Diff · {path}",
"toolCall.diff.viewMode.ariaLabel": "Diff view mode", "toolCall.diff.viewMode.ariaLabel": "Diff view mode",

View File

@@ -130,6 +130,10 @@ export const commandMessages = {
"commands.diagnosticsDefault.description": "Alternar la expansión por defecto de la salida de diagnósticos", "commands.diagnosticsDefault.description": "Alternar la expansión por defecto de la salida de diagnósticos",
"commands.diagnosticsDefault.keywords": "diagnósticos, expandir, colapsar", "commands.diagnosticsDefault.keywords": "diagnósticos, expandir, colapsar",
"commands.toolInputsVisibility.label": "Visibilidad de entradas de herramientas · {state}",
"commands.toolInputsVisibility.description": "Configurar la visibilidad por defecto de los argumentos de entrada de llamadas de herramienta",
"commands.toolInputsVisibility.keywords": "herramienta, entradas, argumentos, visibilidad, ocultar, mostrar, expandir, colapsar",
"commands.tokenUsageDisplay.label": "Mostrar uso de tokens · {state}", "commands.tokenUsageDisplay.label": "Mostrar uso de tokens · {state}",
"commands.tokenUsageDisplay.description": "Mostrar u ocultar estadísticas de tokens y costo en los mensajes del asistente", "commands.tokenUsageDisplay.description": "Mostrar u ocultar estadísticas de tokens y costo en los mensajes del asistente",
"commands.tokenUsageDisplay.keywords": "token, uso, costo, estadísticas", "commands.tokenUsageDisplay.keywords": "token, uso, costo, estadísticas",

View File

@@ -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",

View File

@@ -5,6 +5,14 @@ export const toolCallMessages = {
"toolCall.header.copyTitle": "Copy tool call title", "toolCall.header.copyTitle": "Copy tool call title",
"toolCall.header.copyAriaLabel": "Copy tool call title", "toolCall.header.copyAriaLabel": "Copy tool call title",
"toolCall.header.showInputTitle": "Show Tool Arguments",
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
"toolCall.io.input": "Tool Input",
"toolCall.io.output": "Tool Output",
"toolCall.diff.label": "Diff", "toolCall.diff.label": "Diff",
"toolCall.diff.label.withPath": "Diff · {path}", "toolCall.diff.label.withPath": "Diff · {path}",
"toolCall.diff.viewMode.ariaLabel": "Modo de vista de diff", "toolCall.diff.viewMode.ariaLabel": "Modo de vista de diff",

View File

@@ -130,6 +130,10 @@ export const commandMessages = {
"commands.diagnosticsDefault.description": "Choisir l'ouverture par défaut de la sortie des diagnostics", "commands.diagnosticsDefault.description": "Choisir l'ouverture par défaut de la sortie des diagnostics",
"commands.diagnosticsDefault.keywords": "diagnostics, développer, réduire", "commands.diagnosticsDefault.keywords": "diagnostics, développer, réduire",
"commands.toolInputsVisibility.label": "Visibilité des entrées d'outil · {state}",
"commands.toolInputsVisibility.description": "Définir la visibilité par défaut des arguments d'entrée des appels d'outil",
"commands.toolInputsVisibility.keywords": "outil, entrées, arguments, visibilité, masquer, afficher, développer, réduire",
"commands.tokenUsageDisplay.label": "Affichage de l'usage des tokens · {state}", "commands.tokenUsageDisplay.label": "Affichage de l'usage des tokens · {state}",
"commands.tokenUsageDisplay.description": "Afficher ou masquer les stats de tokens et de coût pour les messages de l'assistant", "commands.tokenUsageDisplay.description": "Afficher ou masquer les stats de tokens et de coût pour les messages de l'assistant",
"commands.tokenUsageDisplay.keywords": "token, usage, coût, stats", "commands.tokenUsageDisplay.keywords": "token, usage, coût, stats",

View File

@@ -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...",

View File

@@ -5,6 +5,14 @@ export const toolCallMessages = {
"toolCall.header.copyTitle": "Copy tool call title", "toolCall.header.copyTitle": "Copy tool call title",
"toolCall.header.copyAriaLabel": "Copy tool call title", "toolCall.header.copyAriaLabel": "Copy tool call title",
"toolCall.header.showInputTitle": "Show Tool Arguments",
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
"toolCall.io.input": "Tool Input",
"toolCall.io.output": "Tool Output",
"toolCall.diff.label": "Diff", "toolCall.diff.label": "Diff",
"toolCall.diff.label.withPath": "Diff · {path}", "toolCall.diff.label.withPath": "Diff · {path}",
"toolCall.diff.viewMode.ariaLabel": "Mode d'affichage du diff", "toolCall.diff.viewMode.ariaLabel": "Mode d'affichage du diff",

View File

@@ -130,6 +130,10 @@ export const commandMessages = {
"commands.diagnosticsDefault.description": "診断出力を既定で展開するか切り替え", "commands.diagnosticsDefault.description": "診断出力を既定で展開するか切り替え",
"commands.diagnosticsDefault.keywords": "診断, 展開, 折りたたみ, diagnostics, expand, collapse", "commands.diagnosticsDefault.keywords": "診断, 展開, 折りたたみ, diagnostics, expand, collapse",
"commands.toolInputsVisibility.label": "ツール入力の表示 · {state}",
"commands.toolInputsVisibility.description": "ツール呼び出しの入力引数の既定の表示状態を設定します",
"commands.toolInputsVisibility.keywords": "ツール, 入力, 引数, 表示, 非表示, 展開, 折りたたみ, tool, inputs, arguments, visibility, hide, show, expand, collapse",
"commands.tokenUsageDisplay.label": "トークン使用量表示 · {state}", "commands.tokenUsageDisplay.label": "トークン使用量表示 · {state}",
"commands.tokenUsageDisplay.description": "アシスタントメッセージのトークン/コスト統計を表示/非表示", "commands.tokenUsageDisplay.description": "アシスタントメッセージのトークン/コスト統計を表示/非表示",
"commands.tokenUsageDisplay.keywords": "トークン, 使用量, コスト, 統計, token, usage, cost, stats", "commands.tokenUsageDisplay.keywords": "トークン, 使用量, コスト, 統計, token, usage, cost, stats",

View File

@@ -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": "変更を取得中...",

View File

@@ -5,6 +5,14 @@ export const toolCallMessages = {
"toolCall.header.copyTitle": "Copy tool call title", "toolCall.header.copyTitle": "Copy tool call title",
"toolCall.header.copyAriaLabel": "Copy tool call title", "toolCall.header.copyAriaLabel": "Copy tool call title",
"toolCall.header.showInputTitle": "Show Tool Arguments",
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
"toolCall.io.input": "Tool Input",
"toolCall.io.output": "Tool Output",
"toolCall.diff.label": "Diff", "toolCall.diff.label": "Diff",
"toolCall.diff.label.withPath": "Diff · {path}", "toolCall.diff.label.withPath": "Diff · {path}",
"toolCall.diff.viewMode.ariaLabel": "diff 表示モード", "toolCall.diff.viewMode.ariaLabel": "diff 表示モード",

View File

@@ -130,6 +130,10 @@ export const commandMessages = {
"commands.diagnosticsDefault.description": "Переключить, разворачивать ли вывод диагностики по умолчанию", "commands.diagnosticsDefault.description": "Переключить, разворачивать ли вывод диагностики по умолчанию",
"commands.diagnosticsDefault.keywords": "diagnostics, развернуть, свернуть", "commands.diagnosticsDefault.keywords": "diagnostics, развернуть, свернуть",
"commands.toolInputsVisibility.label": "Видимость входных данных инструмента · {state}",
"commands.toolInputsVisibility.description": "Установить видимость аргументов входа вызовов инструментов по умолчанию",
"commands.toolInputsVisibility.keywords": "инструмент, вход, аргументы, видимость, скрыть, показать, раскрыть, свернуть, tool, inputs, arguments, visibility, hide, show, expand, collapse",
"commands.tokenUsageDisplay.label": "Отображение token-статистики · {state}", "commands.tokenUsageDisplay.label": "Отображение token-статистики · {state}",
"commands.tokenUsageDisplay.description": "Показать или скрыть статистику token и стоимости для сообщений ассистента", "commands.tokenUsageDisplay.description": "Показать или скрыть статистику token и стоимости для сообщений ассистента",
"commands.tokenUsageDisplay.keywords": "token, usage, cost, статистика", "commands.tokenUsageDisplay.keywords": "token, usage, cost, статистика",

View File

@@ -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": "Добавить",

View File

@@ -5,6 +5,14 @@ export const toolCallMessages = {
"toolCall.header.copyTitle": "Copy tool call title", "toolCall.header.copyTitle": "Copy tool call title",
"toolCall.header.copyAriaLabel": "Copy tool call title", "toolCall.header.copyAriaLabel": "Copy tool call title",
"toolCall.header.showInputTitle": "Show Tool Arguments",
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
"toolCall.io.input": "Tool Input",
"toolCall.io.output": "Tool Output",
"toolCall.diff.label": "Diff", "toolCall.diff.label": "Diff",
"toolCall.diff.label.withPath": "Diff · {path}", "toolCall.diff.label.withPath": "Diff · {path}",
"toolCall.diff.viewMode.ariaLabel": "Режим просмотра diff", "toolCall.diff.viewMode.ariaLabel": "Режим просмотра diff",

View File

@@ -130,6 +130,10 @@ export const commandMessages = {
"commands.diagnosticsDefault.description": "切换诊断输出是否默认展开", "commands.diagnosticsDefault.description": "切换诊断输出是否默认展开",
"commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse, 诊断, 展开, 折叠", "commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse, 诊断, 展开, 折叠",
"commands.toolInputsVisibility.label": "工具输入可见性 · {state}",
"commands.toolInputsVisibility.description": "设置工具调用输入参数的默认可见性",
"commands.toolInputsVisibility.keywords": "工具, 输入, 参数, 可见性, 隐藏, 显示, 展开, 折叠, tool, inputs, arguments, visibility, hide, show, expand, collapse",
"commands.tokenUsageDisplay.label": "Token 使用显示 · {state}", "commands.tokenUsageDisplay.label": "Token 使用显示 · {state}",
"commands.tokenUsageDisplay.description": "显示或隐藏助手消息的 token 和费用统计", "commands.tokenUsageDisplay.description": "显示或隐藏助手消息的 token 和费用统计",
"commands.tokenUsageDisplay.keywords": "token, usage, cost, stats, 令牌, 用量, 费用, 统计", "commands.tokenUsageDisplay.keywords": "token, usage, cost, stats, 令牌, 用量, 费用, 统计",

View File

@@ -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": "正在获取会话更改...",

View File

@@ -5,6 +5,14 @@ export const toolCallMessages = {
"toolCall.header.copyTitle": "Copy tool call title", "toolCall.header.copyTitle": "Copy tool call title",
"toolCall.header.copyAriaLabel": "Copy tool call title", "toolCall.header.copyAriaLabel": "Copy tool call title",
"toolCall.header.showInputTitle": "Show Tool Arguments",
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
"toolCall.io.input": "Tool Input",
"toolCall.io.output": "Tool Output",
"toolCall.diff.label": "Diff", "toolCall.diff.label": "Diff",
"toolCall.diff.label.withPath": "Diff · {path}", "toolCall.diff.label.withPath": "Diff · {path}",
"toolCall.diff.viewMode.ariaLabel": "Diff 视图模式", "toolCall.diff.viewMode.ariaLabel": "Diff 视图模式",

View 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")
)
}

View File

@@ -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")
@@ -52,6 +53,8 @@ const permissionSessionCounts = new Map<string, Map<string, number>>()
const permissionWorktreeSlugByInstance = new Map<string, Map<string, string>>() const permissionWorktreeSlugByInstance = new Map<string, Map<string, string>>()
const [questionQueues, setQuestionQueues] = createSignal<Map<string, QuestionRequest[]>>(new Map()) const [questionQueues, setQuestionQueues] = createSignal<Map<string, QuestionRequest[]>>(new Map())
// Track which worktree a question was enqueued under (by question request id).
const questionWorktreeSlugByInstance = new Map<string, Map<string, string>>()
const [activeQuestionId, setActiveQuestionId] = createSignal<Map<string, string | null>>(new Map()) const [activeQuestionId, setActiveQuestionId] = createSignal<Map<string, string | null>>(new Map())
const questionSessionCounts = new Map<string, Map<string, number>>() const questionSessionCounts = new Map<string, Map<string, number>>()
const questionEnqueuedAt = new Map<string, number>() const questionEnqueuedAt = new Map<string, number>()
@@ -370,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)
@@ -877,6 +881,16 @@ function addQuestionToQueue(instanceId: string, request: QuestionRequest): void
if (sessionId) { if (sessionId) {
incrementQuestionSessionPendingCount(instanceId, sessionId) incrementQuestionSessionPendingCount(instanceId, sessionId)
setSessionPendingQuestion(instanceId, sessionId, true) setSessionPendingQuestion(instanceId, sessionId, true)
// Record the worktree slug at the time the question is enqueued.
// This is used to respond in the same worktree context even from the global permission center.
const slug = getWorktreeSlugForSession(instanceId, sessionId)
let byQuestionId = questionWorktreeSlugByInstance.get(instanceId)
if (!byQuestionId) {
byQuestionId = new Map()
questionWorktreeSlugByInstance.set(instanceId, byQuestionId)
}
byQuestionId.set(request.id, slug)
} }
} }
@@ -897,6 +911,7 @@ function removeQuestionFromQueue(instanceId: string, requestId: string): void {
}) })
questionEnqueuedAt.delete(requestId) questionEnqueuedAt.delete(requestId)
questionWorktreeSlugByInstance.get(instanceId)?.delete(requestId)
recomputeActiveInterruption(instanceId) recomputeActiveInterruption(instanceId)
if (removedSessionId) { if (removedSessionId) {
@@ -909,6 +924,7 @@ function clearQuestionQueue(instanceId: string): void {
for (const request of getQuestionQueue(instanceId)) { for (const request of getQuestionQueue(instanceId)) {
questionEnqueuedAt.delete(request.id) questionEnqueuedAt.delete(request.id)
} }
questionWorktreeSlugByInstance.delete(instanceId)
setQuestionQueues((prev) => { setQuestionQueues((prev) => {
const next = new Map(prev) const next = new Map(prev)
@@ -934,7 +950,7 @@ function setActiveQuestionIdForInstance(instanceId: string, requestId: string):
async function sendQuestionReply( async function sendQuestionReply(
instanceId: string, instanceId: string,
_sessionId: string, sessionId: string,
requestId: string, requestId: string,
answers: string[][], answers: string[][],
): Promise<void> { ): Promise<void> {
@@ -944,8 +960,13 @@ async function sendQuestionReply(
} }
try { try {
const stored = questionWorktreeSlugByInstance.get(instanceId)?.get(requestId)
const fallback = sessionId ? getWorktreeSlugForSession(instanceId, sessionId) : "root"
const worktreeSlug = stored ?? fallback
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
await requestData( await requestData(
instance.client.question.reply({ client.question.reply({
requestID: requestId, requestID: requestId,
answers, answers,
}), }),
@@ -959,15 +980,20 @@ async function sendQuestionReply(
} }
} }
async function sendQuestionReject(instanceId: string, _sessionId: string, requestId: string): Promise<void> { async function sendQuestionReject(instanceId: string, sessionId: string, requestId: string): Promise<void> {
const instance = instances().get(instanceId) const instance = instances().get(instanceId)
if (!instance?.client) { if (!instance?.client) {
throw new Error("Instance not ready") throw new Error("Instance not ready")
} }
try { try {
const stored = questionWorktreeSlugByInstance.get(instanceId)?.get(requestId)
const fallback = sessionId ? getWorktreeSlugForSession(instanceId, sessionId) : "root"
const worktreeSlug = stored ?? fallback
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
await requestData( await requestData(
instance.client.question.reject({ client.question.reject({
requestID: requestId, requestID: requestId,
}), }),
"question.reject", "question.reject",

View 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 }

View File

@@ -25,6 +25,7 @@ export interface ModelPreference {
export type DiffViewMode = "split" | "unified" export type DiffViewMode = "split" | "unified"
export type ExpansionPreference = "expanded" | "collapsed" export type ExpansionPreference = "expanded" | "collapsed"
export type ToolInputsVisibilityPreference = "hidden" | "collapsed" | "expanded"
export type ListeningMode = "local" | "all" export type ListeningMode = "local" | "all"
export interface UiSettings { export interface UiSettings {
@@ -37,6 +38,7 @@ export interface UiSettings {
diffViewMode: DiffViewMode diffViewMode: DiffViewMode
toolOutputExpansion: ExpansionPreference toolOutputExpansion: ExpansionPreference
diagnosticsExpansion: ExpansionPreference diagnosticsExpansion: ExpansionPreference
toolInputsVisibility: ToolInputsVisibilityPreference
showUsageMetrics: boolean showUsageMetrics: boolean
autoCleanupBlankSessions: boolean autoCleanupBlankSessions: boolean
@@ -108,6 +110,7 @@ const defaultUiSettings: UiSettings = {
diffViewMode: "split", diffViewMode: "split",
toolOutputExpansion: "expanded", toolOutputExpansion: "expanded",
diagnosticsExpansion: "expanded", diagnosticsExpansion: "expanded",
toolInputsVisibility: "collapsed",
showUsageMetrics: true, showUsageMetrics: true,
autoCleanupBlankSessions: true, autoCleanupBlankSessions: true,
@@ -130,6 +133,10 @@ function normalizeUiSettings(input?: Partial<UiSettings> | null): UiSettings {
diffViewMode: sanitized.diffViewMode ?? defaultUiSettings.diffViewMode, diffViewMode: sanitized.diffViewMode ?? defaultUiSettings.diffViewMode,
toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultUiSettings.toolOutputExpansion, toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultUiSettings.toolOutputExpansion,
diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultUiSettings.diagnosticsExpansion, diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultUiSettings.diagnosticsExpansion,
toolInputsVisibility:
sanitized.toolInputsVisibility === "hidden" || sanitized.toolInputsVisibility === "collapsed" || sanitized.toolInputsVisibility === "expanded"
? sanitized.toolInputsVisibility
: defaultUiSettings.toolInputsVisibility,
showUsageMetrics: sanitized.showUsageMetrics ?? defaultUiSettings.showUsageMetrics, showUsageMetrics: sanitized.showUsageMetrics ?? defaultUiSettings.showUsageMetrics,
autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultUiSettings.autoCleanupBlankSessions, autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultUiSettings.autoCleanupBlankSessions,
osNotificationsEnabled: sanitized.osNotificationsEnabled ?? defaultUiSettings.osNotificationsEnabled, osNotificationsEnabled: sanitized.osNotificationsEnabled ?? defaultUiSettings.osNotificationsEnabled,
@@ -439,6 +446,11 @@ function setDiagnosticsExpansion(mode: ExpansionPreference): void {
updateUiSettings({ diagnosticsExpansion: mode }) updateUiSettings({ diagnosticsExpansion: mode })
} }
function setToolInputsVisibility(mode: ToolInputsVisibilityPreference): void {
if (preferences().toolInputsVisibility === mode) return
updateUiSettings({ toolInputsVisibility: mode })
}
function setThinkingBlocksExpansion(mode: ExpansionPreference): void { function setThinkingBlocksExpansion(mode: ExpansionPreference): void {
if (preferences().thinkingBlocksExpansion === mode) return if (preferences().thinkingBlocksExpansion === mode) return
updateUiSettings({ thinkingBlocksExpansion: mode }) updateUiSettings({ thinkingBlocksExpansion: mode })
@@ -536,6 +548,7 @@ interface ConfigContextValue {
setToolOutputExpansion: typeof setToolOutputExpansion setToolOutputExpansion: typeof setToolOutputExpansion
setDiagnosticsExpansion: typeof setDiagnosticsExpansion setDiagnosticsExpansion: typeof setDiagnosticsExpansion
setThinkingBlocksExpansion: typeof setThinkingBlocksExpansion setThinkingBlocksExpansion: typeof setThinkingBlocksExpansion
setToolInputsVisibility: typeof setToolInputsVisibility
// instance scoped // instance scoped
setAgentModelPreference: typeof setAgentModelPreference setAgentModelPreference: typeof setAgentModelPreference
@@ -579,6 +592,7 @@ const configContextValue: ConfigContextValue = {
setToolOutputExpansion, setToolOutputExpansion,
setDiagnosticsExpansion, setDiagnosticsExpansion,
setThinkingBlocksExpansion, setThinkingBlocksExpansion,
setToolInputsVisibility,
setAgentModelPreference, setAgentModelPreference,
getAgentModelPreference, getAgentModelPreference,
} }

View File

@@ -526,6 +526,7 @@ async function fetchAgents(instanceId: string): Promise<void> {
name: agent.name, name: agent.name,
description: agent.description || "", description: agent.description || "",
mode: agent.mode, mode: agent.mode,
hidden: agent.hidden,
model: agent.model?.modelID model: agent.model?.modelID
? { ? {
providerId: agent.model.providerID || "", providerId: agent.model.providerID || "",

View File

@@ -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)

View File

@@ -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 {

View File

@@ -130,6 +130,19 @@
color: var(--text-primary); color: var(--text-primary);
} }
/* Make the command palette trigger stand out in the header. */
.connection-status-button.command-palette-button {
border-radius: 0;
@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 {
background-color: var(--surface-hover);
color: var(--text-primary);
}
.connection-status-button:hover { .connection-status-button:hover {
background-color: var(--surface-hover); background-color: var(--surface-hover);
} }

View File

@@ -87,6 +87,7 @@
@apply flex items-stretch w-full; @apply flex items-stretch w-full;
background-color: transparent; background-color: transparent;
color: var(--text-primary); color: var(--text-primary);
border-bottom: 1px solid var(--tool-call-border-color);
} }
.tool-call-header:hover { .tool-call-header:hover {
@@ -127,11 +128,30 @@
cursor: pointer; cursor: pointer;
} }
.tool-call-header-input {
@apply inline-flex items-center justify-center;
background-color: transparent;
border: none;
color: var(--text-secondary);
padding: 0 0.5rem;
border-radius: 0;
cursor: pointer;
}
.tool-call-header-copy:hover { .tool-call-header-copy:hover {
background-color: transparent; background-color: transparent;
color: var(--text-primary); color: var(--text-primary);
} }
.tool-call-header-input:hover {
background-color: transparent;
color: var(--text-primary);
}
.tool-call-header-input[aria-pressed="true"] {
color: var(--text-primary);
}
.tool-call-header-status { .tool-call-header-status {
@apply inline-flex items-center justify-center; @apply inline-flex items-center justify-center;
font-size: 0.95rem; font-size: 0.95rem;
@@ -213,6 +233,63 @@
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
} }
.tool-call-io-sections {
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding: 0;
}
.tool-call-io-section {
border: 1px solid var(--tool-call-border-color);
overflow: hidden;
background-color: transparent;
border-radius: 0;
}
.tool-call-io-toggle {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.75rem;
padding: 0.5rem;
background-color: var(--surface-secondary);
border: none;
border-bottom: 1px solid var(--tool-call-border-color);
width: 100%;
text-align: left;
font-size: 0.875rem;
font-weight: normal;
color: var(--text-primary);
cursor: pointer;
}
.tool-call-io-toggle::before {
content: "▶";
font-size: 11px;
margin-right: 0.35rem;
color: var(--text-secondary);
}
.tool-call-io-toggle[aria-expanded="true"]::before {
content: "▼";
}
.tool-call-io-title {
font-weight: inherit;
color: inherit;
}
.tool-call-io-body {
background-color: var(--surface-code);
}
/* IO sections provide the outer frame; avoid double borders on markdown frames. */
.tool-call-io-body .tool-call-markdown {
border: none;
}
.tool-call-markdown { .tool-call-markdown {
background-color: var(--surface-code); background-color: var(--surface-code);
/* Keep a visible frame around the scroll viewport (not the content). */ /* Keep a visible frame around the scroll viewport (not the content). */

View File

@@ -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;

View File

@@ -68,6 +68,7 @@ export interface Agent {
name: string name: string
description: string description: string
mode: string mode: string
hidden?: boolean
model?: { model?: {
providerId: string providerId: string
modelId: string modelId: string