Compare commits
49 Commits
v0.10.3-de
...
codenomad/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90baefbb7e | ||
|
|
1c138f4489 | ||
|
|
d36e568ed0 | ||
|
|
d6462ef524 | ||
|
|
a06884ebce | ||
|
|
62bd88f6a4 | ||
|
|
6479561779 | ||
|
|
635237c258 | ||
|
|
33f0aa5714 | ||
|
|
7ca6285d58 | ||
|
|
14c60fef6c | ||
|
|
336de6a19e | ||
|
|
377c8e2249 | ||
|
|
697dea21f8 | ||
|
|
34d3f803d5 | ||
|
|
f824a063a5 | ||
|
|
5fabf286e8 | ||
|
|
e8947d61b1 | ||
|
|
1ccd14eae8 | ||
|
|
b162764ccb | ||
|
|
2124e540aa | ||
|
|
b5790998b7 | ||
|
|
9800afb785 | ||
|
|
3b73d9d5b9 | ||
|
|
f7ac30afe3 | ||
|
|
ce370d5100 | ||
|
|
c639e535b5 | ||
|
|
e84adebe61 | ||
|
|
d45a1ff078 | ||
|
|
b4121696bb | ||
|
|
f75c942162 | ||
|
|
127a1f628d | ||
|
|
859312ba3b | ||
|
|
4eaa711f01 | ||
|
|
c8ff858565 | ||
|
|
6de6ef5a4a | ||
|
|
4dee154490 | ||
|
|
ef388adc4f | ||
|
|
e8cfad1266 | ||
|
|
3f82dd21fe | ||
|
|
dc13d9a7d0 | ||
|
|
29557fba6d | ||
|
|
dea5079713 | ||
|
|
ddc58a2c3c | ||
|
|
eafd4d83af | ||
|
|
1a0734c6b1 | ||
|
|
e16c5752ed | ||
|
|
eb6701185b | ||
|
|
d948ad8e35 |
95
.github/workflows/build-and-upload.yml
vendored
95
.github/workflows/build-and-upload.yml
vendored
@@ -3,6 +3,11 @@ name: Build and Upload Binaries
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Git ref (branch, tag, or SHA) to build from"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
version:
|
version:
|
||||||
description: "Version to apply to workspace packages (release builds)"
|
description: "Version to apply to workspace packages (release builds)"
|
||||||
required: false
|
required: false
|
||||||
@@ -45,6 +50,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -65,6 +72,78 @@ jobs:
|
|||||||
- name: Build macOS binaries (Electron)
|
- name: Build macOS binaries (Electron)
|
||||||
run: npm run build:mac --workspace @neuralnomads/codenomad-electron-app
|
run: npm run build:mac --workspace @neuralnomads/codenomad-electron-app
|
||||||
|
|
||||||
|
- name: Repackage Electron macOS zips (ditto)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Prefer the workflow-provided version; fall back to package.json.
|
||||||
|
VERSION_TO_USE="${VERSION:-}"
|
||||||
|
if [ -z "$VERSION_TO_USE" ]; then
|
||||||
|
VERSION_TO_USE=$(node -p "require('./packages/electron-app/package.json').version")
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_root="packages/electron-app/release"
|
||||||
|
shopt -s nullglob globstar
|
||||||
|
|
||||||
|
apps=("$release_root"/**/CodeNomad.app)
|
||||||
|
if [ "${#apps[@]}" -eq 0 ]; then
|
||||||
|
echo "No CodeNomad.app found under $release_root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for app in "${apps[@]}"; do
|
||||||
|
bundle_dir=$(basename "$(dirname "$app")")
|
||||||
|
arch="x64"
|
||||||
|
if [[ "$bundle_dir" == *"arm64"* ]]; then
|
||||||
|
arch="arm64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
out_zip="$release_root/CodeNomad-${VERSION_TO_USE}-mac-${arch}.zip"
|
||||||
|
rm -f "$out_zip"
|
||||||
|
echo "ditto -ck: $app -> $out_zip"
|
||||||
|
ditto -ck --sequesterRsrc --keepParent "$app" "$out_zip"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Validate Electron macOS codesign (unzipped)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
tmp_dir=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp_dir"' EXIT
|
||||||
|
|
||||||
|
zips=(packages/electron-app/release/CodeNomad-*-mac-*.zip)
|
||||||
|
if [ "${#zips[@]}" -eq 0 ]; then
|
||||||
|
echo "No Electron macOS zip artifacts found to validate" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for zip in "${zips[@]}"; do
|
||||||
|
echo "Validating codesign for: $zip"
|
||||||
|
extract_dir="$tmp_dir/$(basename "$zip" .zip)"
|
||||||
|
mkdir -p "$extract_dir"
|
||||||
|
|
||||||
|
# Use ditto for extraction as well to preserve bundle metadata.
|
||||||
|
ditto -x -k "$zip" "$extract_dir"
|
||||||
|
|
||||||
|
app_path=""
|
||||||
|
for candidate in "$extract_dir"/*.app "$extract_dir"/*/*.app; do
|
||||||
|
if [ -d "$candidate" ]; then
|
||||||
|
app_path="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$app_path" ]; then
|
||||||
|
echo "No .app found after extracting $zip" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$app_path"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
if: ${{ inputs.upload && inputs.tag != '' }}
|
if: ${{ inputs.upload && inputs.tag != '' }}
|
||||||
run: |
|
run: |
|
||||||
@@ -85,6 +164,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -124,6 +205,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -164,6 +247,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -237,6 +322,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -310,6 +397,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -388,6 +477,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -490,6 +581,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -587,6 +680,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
61
.github/workflows/dev-release.yml
vendored
61
.github/workflows/dev-release.yml
vendored
@@ -1,12 +1,13 @@
|
|||||||
name: Develop Pre-Release
|
name: Develop Pre-Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
schedule:
|
||||||
branches:
|
# Nightly build of dev (only if dev has new commits)
|
||||||
- dev
|
- cron: "0 1 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: read
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
@@ -15,25 +16,63 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
gate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version_suffix: ${{ steps.vars.outputs.version_suffix }}
|
run: ${{ steps.gate.outputs.run }}
|
||||||
|
dev_sha: ${{ steps.gate.outputs.dev_sha }}
|
||||||
|
version_suffix: ${{ steps.gate.outputs.version_suffix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Compute version suffix
|
- name: Decide whether to run
|
||||||
id: vars
|
id: gate
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SHA8="${GITHUB_SHA::8}"
|
|
||||||
|
api() {
|
||||||
|
curl -sS \
|
||||||
|
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
"$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
DEV_SHA=$(api "https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/heads/dev" | jq -r '.object.sha')
|
||||||
|
if [ -z "$DEV_SHA" ] || [ "$DEV_SHA" = "null" ]; then
|
||||||
|
echo "Failed to resolve dev head SHA" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
DATE=$(date -u +%Y%m%d)
|
DATE=$(date -u +%Y%m%d)
|
||||||
echo "version_suffix=-dev-${DATE}-${SHA8}" >> "$GITHUB_OUTPUT"
|
SHA8="${DEV_SHA::8}"
|
||||||
|
VERSION_SUFFIX="-dev-${DATE}-${SHA8}"
|
||||||
|
|
||||||
|
SHOULD_RUN="false"
|
||||||
|
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||||
|
SHOULD_RUN="true"
|
||||||
|
else
|
||||||
|
# Nightly: only run if dev has advanced since last successful dev-release build.
|
||||||
|
LAST_SHA=$(api "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/dev-release.yml/runs?branch=dev&status=success&per_page=1" | jq -r '.workflow_runs[0].head_sha // empty')
|
||||||
|
if [ -z "${LAST_SHA}" ]; then
|
||||||
|
SHOULD_RUN="true"
|
||||||
|
elif [ "${LAST_SHA}" != "${DEV_SHA}" ]; then
|
||||||
|
SHOULD_RUN="true"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "run=${SHOULD_RUN}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "dev_sha=${DEV_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version_suffix=${VERSION_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
prerelease:
|
prerelease:
|
||||||
needs: prepare
|
needs: gate
|
||||||
|
if: ${{ needs.gate.outputs.run == 'true' }}
|
||||||
uses: ./.github/workflows/reusable-release.yml
|
uses: ./.github/workflows/reusable-release.yml
|
||||||
with:
|
with:
|
||||||
version_suffix: ${{ needs.prepare.outputs.version_suffix }}
|
ref: ${{ needs.gate.outputs.dev_sha }}
|
||||||
|
version_suffix: ${{ needs.gate.outputs.version_suffix }}
|
||||||
npm_package_name: "@neuralnomads/codenomad-dev"
|
npm_package_name: "@neuralnomads/codenomad-dev"
|
||||||
dist_tag: latest
|
dist_tag: latest
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
6
.github/workflows/manual-npm-publish.yml
vendored
6
.github/workflows/manual-npm-publish.yml
vendored
@@ -19,6 +19,10 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
|
ref:
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
version:
|
version:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
@@ -46,6 +50,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
10
.github/workflows/release-ui.yml
vendored
10
.github/workflows/release-ui.yml
vendored
@@ -1,7 +1,13 @@
|
|||||||
name: Release UI
|
name: Release UI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call: {}
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Git ref (branch, tag, or SHA) to build from"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -18,6 +24,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
11
.github/workflows/reusable-release.yml
vendored
11
.github/workflows/reusable-release.yml
vendored
@@ -3,6 +3,11 @@ name: Reusable Release
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Git ref (branch, tag, or SHA) to build from"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
version_suffix:
|
version_suffix:
|
||||||
description: "Suffix appended to package.json version"
|
description: "Suffix appended to package.json version"
|
||||||
required: false
|
required: false
|
||||||
@@ -46,6 +51,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -84,6 +91,7 @@ jobs:
|
|||||||
needs: prepare-release
|
needs: prepare-release
|
||||||
uses: ./.github/workflows/build-and-upload.yml
|
uses: ./.github/workflows/build-and-upload.yml
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
version: ${{ needs.prepare-release.outputs.version }}
|
version: ${{ needs.prepare-release.outputs.version }}
|
||||||
tag: ${{ needs.prepare-release.outputs.tag }}
|
tag: ${{ needs.prepare-release.outputs.tag }}
|
||||||
release_name: ${{ needs.prepare-release.outputs.release_name }}
|
release_name: ${{ needs.prepare-release.outputs.release_name }}
|
||||||
@@ -95,6 +103,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
uses: ./.github/workflows/release-ui.yml
|
uses: ./.github/workflows/release-ui.yml
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
publish-server:
|
publish-server:
|
||||||
@@ -103,6 +113,7 @@ jobs:
|
|||||||
- build-and-upload
|
- build-and-upload
|
||||||
uses: ./.github/workflows/manual-npm-publish.yml
|
uses: ./.github/workflows/manual-npm-publish.yml
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
version: ${{ needs.prepare-release.outputs.version }}
|
version: ${{ needs.prepare-release.outputs.version }}
|
||||||
dist_tag: ${{ inputs.dist_tag }}
|
dist_tag: ${{ inputs.dist_tag }}
|
||||||
package_name: ${{ inputs.npm_package_name }}
|
package_name: ${{ inputs.npm_package_name }}
|
||||||
|
|||||||
@@ -123,3 +123,6 @@ To build the Desktop App from source:
|
|||||||
1. Clone the repo.
|
1. Clone the repo.
|
||||||
2. Run `npm install` (requires pnpm or npm 7+ for workspaces).
|
2. Run `npm install` (requires pnpm or npm 7+ for workspaces).
|
||||||
3. Run `npm run build --workspace @neuralnomads/codenomad-electron-app`.
|
3. Run `npm run build --workspace @neuralnomads/codenomad-electron-app`.
|
||||||
|
|
||||||
|
[](https://star-history.com/#NeuralNomadsAI/CodeNomad&Date)
|
||||||
|
|
||||||
|
|||||||
23
package-lock.json
generated
23
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
@@ -2809,9 +2809,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@opencode-ai/sdk": {
|
"node_modules/@opencode-ai/sdk": {
|
||||||
"version": "1.1.11",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.2.6.tgz",
|
||||||
"integrity": "sha512-vqdNDz8Q+4bygmDdQem6oxhU31ci4JVdoND4ZJNeCs9x6OIU6MM3ybgemGpzNkgtJDlfb4xCdrPaZZ6Sr3V1IQ==",
|
"integrity": "sha512-dWMF8Aku4h7fh8sw5tQ2FtbqRLbIFT8FcsukpxTird49ax7oUXP+gzqxM/VdxHjfksQvzLBjLZyMdDStc5g7xA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@pinojs/redact": {
|
"node_modules/@pinojs/redact": {
|
||||||
@@ -11985,7 +11985,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
@@ -12021,7 +12021,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
@@ -12062,7 +12062,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
@@ -12070,12 +12070,12 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@opencode-ai/sdk": "1.1.11",
|
"@opencode-ai/sdk": "1.2.6",
|
||||||
"@solidjs/router": "^0.13.0",
|
"@solidjs/router": "^0.13.0",
|
||||||
"@suid/icons-material": "^0.9.0",
|
"@suid/icons-material": "^0.9.0",
|
||||||
"@suid/material": "^0.19.0",
|
"@suid/material": "^0.19.0",
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"minServerVersion": "0.10.3",
|
"minServerVersion": "0.11.4",
|
||||||
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.2.4"
|
"@opencode-ai/plugin": "1.2.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,21 @@
|
|||||||
## Features & Capabilities
|
## Features & Capabilities
|
||||||
|
|
||||||
### 🌍 Deployment Freedom
|
### 🌍 Deployment Freedom
|
||||||
|
|
||||||
- **Remote Access**: Host CodeNomad on a powerful workstation and access it from your lightweight laptop.
|
- **Remote Access**: Host CodeNomad on a powerful workstation and access it from your lightweight laptop.
|
||||||
- **Code Anywhere**: Tunnel in via VPN or SSH to code securely from coffee shops or while traveling.
|
- **Code Anywhere**: Tunnel in via VPN or SSH to code securely from coffee shops or while traveling.
|
||||||
- **Multi-Device**: The responsive web client works on tablets and iPads, turning any screen into a dev terminal.
|
- **Multi-Device**: The responsive web client works on tablets and iPads, turning any screen into a dev terminal.
|
||||||
- **Always-On**: Run as a background service so your sessions are always ready when you connect.
|
- **Always-On**: Run as a background service so your sessions are always ready when you connect.
|
||||||
|
|
||||||
### ⚡️ Workspace Power
|
### ⚡️ Workspace Power
|
||||||
|
|
||||||
- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
|
- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
|
||||||
- **Long-Context Native**: Scroll through massive transcripts without hitches.
|
- **Long-Context Native**: Scroll through massive transcripts without hitches.
|
||||||
- **Deep Task Awareness**: Monitor background tasks and child sessions without losing your flow.
|
- **Deep Task Awareness**: Monitor background tasks and child sessions without losing your flow.
|
||||||
- **Command Palette**: A single, global palette to jump tabs, launch tools, and fire shortcuts.
|
- **Command Palette**: A single, global palette to jump tabs, launch tools, and fire shortcuts.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **OpenCode**: `opencode` must be installed and configured on your system.
|
- **OpenCode**: `opencode` must be installed and configured on your system.
|
||||||
- Node.js 18+ and npm (for running or building from source).
|
- Node.js 18+ and npm (for running or building from source).
|
||||||
- A workspace folder on disk you want to serve.
|
- A workspace folder on disk you want to serve.
|
||||||
@@ -25,6 +28,7 @@
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Run via npx (Recommended)
|
### Run via npx (Recommended)
|
||||||
|
|
||||||
You can run CodeNomad directly without installing it:
|
You can run CodeNomad directly without installing it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -43,6 +47,7 @@ On startup, CodeNomad prints two URLs:
|
|||||||
- `Remote Connection URL : ...` (used by browsers/other machines when remote access is enabled)
|
- `Remote Connection URL : ...` (used by browsers/other machines when remote access is enabled)
|
||||||
|
|
||||||
### Install Globally
|
### Install Globally
|
||||||
|
|
||||||
Or install it globally to use the `codenomad` command:
|
Or install it globally to use the `codenomad` command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -51,6 +56,7 @@ codenomad --launch
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Install Locally (per-project)
|
### Install Locally (per-project)
|
||||||
|
|
||||||
If you prefer to install CodeNomad into a project and run the local binary:
|
If you prefer to install CodeNomad into a project and run the local binary:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -61,6 +67,7 @@ npx codenomad --launch
|
|||||||
(`npx codenomad ...` will use `./node_modules/.bin/codenomad` when present.)
|
(`npx codenomad ...` will use `./node_modules/.bin/codenomad` when present.)
|
||||||
|
|
||||||
### Common Flags
|
### Common Flags
|
||||||
|
|
||||||
You can configure the server using flags or environment variables:
|
You can configure the server using flags or environment variables:
|
||||||
|
|
||||||
| Flag | Env Variable | Description |
|
| Flag | Env Variable | Description |
|
||||||
@@ -74,7 +81,7 @@ You can configure the server using flags or environment variables:
|
|||||||
| `--tls-ca <path>` | `CLI_TLS_CA` | Optional CA chain/bundle (PEM) |
|
| `--tls-ca <path>` | `CLI_TLS_CA` | Optional CA chain/bundle (PEM) |
|
||||||
| `--tlsSANs <list>` | `CLI_TLS_SANS` | Additional TLS SANs (comma-separated) |
|
| `--tlsSANs <list>` | `CLI_TLS_SANS` | Additional TLS SANs (comma-separated) |
|
||||||
| `--host <addr>` | `CLI_HOST` | Interface to bind (default 127.0.0.1) |
|
| `--host <addr>` | `CLI_HOST` | Interface to bind (default 127.0.0.1) |
|
||||||
| `--workspace-root <path>` | `CLI_WORKSPACE_ROOT` | Default root for new workspaces |
|
| `--workspace-root <path>` | `CLI_WORKSPACE_ROOT` | Restricts the root path where new workspaces can be opened. Git worktrees are created in `.codenomad/worktrees` inside the project folder. |
|
||||||
| `--unrestricted-root` | `CLI_UNRESTRICTED_ROOT` | Allow full-filesystem browsing |
|
| `--unrestricted-root` | `CLI_UNRESTRICTED_ROOT` | Allow full-filesystem browsing |
|
||||||
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
||||||
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
||||||
@@ -87,10 +94,11 @@ You can configure the server using flags or environment variables:
|
|||||||
| `--ui-dir <path>` | `CLI_UI_DIR` | Directory containing the built UI bundle |
|
| `--ui-dir <path>` | `CLI_UI_DIR` | Directory containing the built UI bundle |
|
||||||
| `--ui-dev-server <url>` | `CLI_UI_DEV_SERVER` | Proxy UI requests to a running dev server (requires `--https=false --http=true`) |
|
| `--ui-dev-server <url>` | `CLI_UI_DEV_SERVER` | Proxy UI requests to a running dev server (requires `--https=false --http=true`) |
|
||||||
| `--ui-no-update` | `CLI_UI_NO_UPDATE` | Disable remote UI updates |
|
| `--ui-no-update` | `CLI_UI_NO_UPDATE` | Disable remote UI updates |
|
||||||
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (true|false) |
|
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (`true` |
|
||||||
| `--ui-manifest-url <url>` | `CLI_UI_MANIFEST_URL` | Remote UI manifest URL |
|
| `--ui-manifest-url <url>` | `CLI_UI_MANIFEST_URL` | Remote UI manifest URL |
|
||||||
|
|
||||||
### Dev Releases (Advanced)
|
### Dev Releases (Advanced)
|
||||||
|
|
||||||
If you want the latest bleeding-edge builds (published as GitHub pre-releases), use the dev package:
|
If you want the latest bleeding-edge builds (published as GitHub pre-releases), use the dev package:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -141,12 +149,14 @@ codenomad --tlsSANs "localhost,127.0.0.1,my-hostname,192.168.1.10"
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
- Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
|
- Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
|
||||||
- `--dangerously-skip-auth` / `CODENOMAD_SKIP_AUTH=true` disables the login prompt and treats all requests as authenticated.
|
- `--dangerously-skip-auth` / `CODENOMAD_SKIP_AUTH=true` disables the login prompt and treats all requests as authenticated.
|
||||||
Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.).
|
Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.).
|
||||||
If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API.
|
If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API.
|
||||||
|
|
||||||
### Progressive Web App (PWA)
|
### Progressive Web App (PWA)
|
||||||
|
|
||||||
When running as a server CodeNomad can also be installed as a PWA from any supported browser, giving you a native app experience just like the Electron installation but executing on the remote server instead.
|
When running as a server CodeNomad can also be installed as a PWA from any supported browser, giving you a native app experience just like the Electron installation but executing on the remote server instead.
|
||||||
|
|
||||||
1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.).
|
1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.).
|
||||||
@@ -158,5 +168,6 @@ When running as a server CodeNomad can also be installed as a PWA from any suppo
|
|||||||
> If you host CodeNomad on a remote machine, use HTTPS. Self-signed certificates generally won't work unless they are explicitly trusted by the device/browser (e.g., via a custom CA).
|
> If you host CodeNomad on a remote machine, use HTTPS. Self-signed certificates generally won't work unless they are explicitly trusted by the device/browser (e.g., via a custom CA).
|
||||||
|
|
||||||
### Data Storage
|
### Data Storage
|
||||||
|
|
||||||
- **Config**: `~/.config/codenomad/config.json`
|
- **Config**: `~/.config/codenomad/config.json`
|
||||||
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
||||||
|
|||||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
.addOption(new Option("--tls-ca <path>", "TLS CA chain (PEM)").env("CLI_TLS_CA"))
|
.addOption(new Option("--tls-ca <path>", "TLS CA chain (PEM)").env("CLI_TLS_CA"))
|
||||||
.addOption(new Option("--tlsSANs <list>", "Additional TLS SANs (comma-separated)").env("CLI_TLS_SANS"))
|
.addOption(new Option("--tlsSANs <list>", "Additional TLS SANs (comma-separated)").env("CLI_TLS_SANS"))
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option("--workspace-root <path>", "Workspace root directory").env("CLI_WORKSPACE_ROOT").default(process.cwd()),
|
new Option("--workspace-root <path>", "Restricts root path where workspaces can be opened").env("CLI_WORKSPACE_ROOT").default(process.cwd()),
|
||||||
)
|
)
|
||||||
.addOption(new Option("--root <path>").env("CLI_ROOT").hideHelp(true))
|
.addOption(new Option("--root <path>").env("CLI_ROOT").hideHelp(true))
|
||||||
.addOption(new Option("--unrestricted-root", "Allow browsing the full filesystem").env("CLI_UNRESTRICTED_ROOT").default(false))
|
.addOption(new Option("--unrestricted-root", "Allow browsing the full filesystem").env("CLI_UNRESTRICTED_ROOT").default(false))
|
||||||
|
|||||||
@@ -367,6 +367,21 @@ function registerInstanceProxyRoutes(app: FastifyInstance, deps: InstanceProxyDe
|
|||||||
|
|
||||||
const INSTANCE_PROXY_HOST = "127.0.0.1"
|
const INSTANCE_PROXY_HOST = "127.0.0.1"
|
||||||
|
|
||||||
|
// Special-case OpenCode directory override.
|
||||||
|
//
|
||||||
|
// UI clients may need to scope certain requests to an arbitrary directory that is not
|
||||||
|
// part of the Git worktree list. Since the OpenCode SDK does not reliably support
|
||||||
|
// injecting per-request headers, we encode an override into the *path* and strip it
|
||||||
|
// before proxying to the instance.
|
||||||
|
//
|
||||||
|
// Example proxied request path:
|
||||||
|
// /workspaces/:id/worktrees/:slug/instance/__dir/<base64url>/session/create
|
||||||
|
//
|
||||||
|
// The server will decode <base64url> -> absolute directory, validate it, then set
|
||||||
|
// x-opencode-directory accordingly and forward the request to /session/create.
|
||||||
|
const OPENCODE_DIR_OVERRIDE_PREFIX = "__dir/"
|
||||||
|
const OPENCODE_DIR_OVERRIDE_MAX_LEN = 4096
|
||||||
|
|
||||||
async function proxyWorkspaceRequest(args: {
|
async function proxyWorkspaceRequest(args: {
|
||||||
request: FastifyRequest
|
request: FastifyRequest
|
||||||
reply: FastifyReply
|
reply: FastifyReply
|
||||||
@@ -457,19 +472,43 @@ async function proxyWorkspaceRequest(args: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const directory = await resolveWorktreeDirectory({
|
let extracted: { overrideDirectory: string | null; forwardedSuffix: string | undefined }
|
||||||
workspaceId,
|
try {
|
||||||
workspacePath: workspace.path,
|
extracted = extractOpencodeDirectoryOverride(args.pathSuffix)
|
||||||
worktreeSlug,
|
} catch (error) {
|
||||||
logger,
|
const message = error instanceof Error ? error.message : "Invalid directory override"
|
||||||
})
|
reply.code(400).send({ error: message })
|
||||||
|
|
||||||
if (!directory) {
|
|
||||||
reply.code(404).send({ error: "Worktree not found" })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let directory: string | null = null
|
||||||
|
let forwardedSuffix = extracted.forwardedSuffix
|
||||||
|
|
||||||
const normalizedSuffix = normalizeInstanceSuffix(args.pathSuffix)
|
if (extracted.overrideDirectory) {
|
||||||
|
try {
|
||||||
|
directory = validateAndNormalizeOverrideDirectory({
|
||||||
|
overrideDirectory: extracted.overrideDirectory,
|
||||||
|
workspaceRoot: workspace.path,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Invalid directory override"
|
||||||
|
reply.code(400).send({ error: message })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
directory = await resolveWorktreeDirectory({
|
||||||
|
workspaceId,
|
||||||
|
workspacePath: workspace.path,
|
||||||
|
worktreeSlug,
|
||||||
|
logger,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!directory) {
|
||||||
|
reply.code(404).send({ error: "Worktree not found" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSuffix = normalizeInstanceSuffix(forwardedSuffix)
|
||||||
const queryIndex = (request.raw.url ?? "").indexOf("?")
|
const queryIndex = (request.raw.url ?? "").indexOf("?")
|
||||||
const search = queryIndex >= 0 ? (request.raw.url ?? "").slice(queryIndex) : ""
|
const search = queryIndex >= 0 ? (request.raw.url ?? "").slice(queryIndex) : ""
|
||||||
const targetUrl = `http://${INSTANCE_PROXY_HOST}:${port}${normalizedSuffix}${search}`
|
const targetUrl = `http://${INSTANCE_PROXY_HOST}:${port}${normalizedSuffix}${search}`
|
||||||
@@ -533,6 +572,89 @@ async function proxyWorkspaceRequest(args: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractOpencodeDirectoryOverride(pathSuffix: string | undefined): {
|
||||||
|
overrideDirectory: string | null
|
||||||
|
forwardedSuffix: string | undefined
|
||||||
|
} {
|
||||||
|
if (!pathSuffix) {
|
||||||
|
return { overrideDirectory: null, forwardedSuffix: pathSuffix }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fastify wildcard param does not include a leading slash.
|
||||||
|
const trimmed = pathSuffix.replace(/^\/+/, "")
|
||||||
|
if (!trimmed.startsWith(OPENCODE_DIR_OVERRIDE_PREFIX)) {
|
||||||
|
return { overrideDirectory: null, forwardedSuffix: pathSuffix }
|
||||||
|
}
|
||||||
|
|
||||||
|
const rest = trimmed.slice(OPENCODE_DIR_OVERRIDE_PREFIX.length)
|
||||||
|
const slashIndex = rest.indexOf("/")
|
||||||
|
const encoded = (slashIndex >= 0 ? rest.slice(0, slashIndex) : rest).trim()
|
||||||
|
const remaining = slashIndex >= 0 ? rest.slice(slashIndex + 1) : ""
|
||||||
|
|
||||||
|
if (!encoded) {
|
||||||
|
throw new Error("Missing directory override")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoded.length > OPENCODE_DIR_OVERRIDE_MAX_LEN) {
|
||||||
|
throw new Error("Directory override too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
let overrideDirectory = ""
|
||||||
|
try {
|
||||||
|
overrideDirectory = decodeBase64Url(encoded)
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid directory override")
|
||||||
|
}
|
||||||
|
const forwardedSuffix = remaining
|
||||||
|
return { overrideDirectory, forwardedSuffix }
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeBase64Url(input: string): string {
|
||||||
|
// base64url -> base64
|
||||||
|
const normalized = input.replace(/-/g, "+").replace(/_/g, "/")
|
||||||
|
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4))
|
||||||
|
const base64 = `${normalized}${padding}`
|
||||||
|
return Buffer.from(base64, "base64").toString("utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAndNormalizeOverrideDirectory(params: { overrideDirectory: string; workspaceRoot: string }): string {
|
||||||
|
const raw = params.overrideDirectory.trim()
|
||||||
|
if (!raw) {
|
||||||
|
throw new Error("Override directory is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.isAbsolute(raw)) {
|
||||||
|
throw new Error("Override directory must be an absolute path")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(raw)) {
|
||||||
|
throw new Error(`Override directory does not exist: ${raw}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(raw)
|
||||||
|
if (!stats.isDirectory()) {
|
||||||
|
throw new Error(`Override path is not a directory: ${raw}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedOverride = fs.realpathSync(raw)
|
||||||
|
const normalizedRoot = fs.realpathSync(params.workspaceRoot)
|
||||||
|
|
||||||
|
if (!isSubpath(normalizedOverride, normalizedRoot)) {
|
||||||
|
throw new Error("Override directory must be within the workspace root")
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSubpath(candidate: string, root: string): boolean {
|
||||||
|
const rel = path.relative(root, candidate)
|
||||||
|
if (rel === "") return true
|
||||||
|
if (rel === "..") return false
|
||||||
|
if (rel.startsWith(`..${path.sep}`)) return false
|
||||||
|
if (path.isAbsolute(rel)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeInstanceSuffix(pathSuffix: string | undefined) {
|
function normalizeInstanceSuffix(pathSuffix: string | undefined) {
|
||||||
if (!pathSuffix || pathSuffix === "/") {
|
if (!pathSuffix || pathSuffix === "/") {
|
||||||
return "/"
|
return "/"
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
showError(message || `Login failed (${res.status})`)
|
showError(message || `Login failed (${res.status})`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.location.href = "/"
|
// Replace history entry so Back doesn't return to /login.
|
||||||
|
window.location.replace("/")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(e && e.message ? e.message : String(e))
|
showError(e && e.message ? e.message : String(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,19 @@ function getTokenHtml(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
|
export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||||
app.get("/login", async (_request, reply) => {
|
app.get("/login", async (request, reply) => {
|
||||||
|
// If already authenticated, don't show the login page.
|
||||||
|
const session = deps.authManager.getSessionFromRequest(request)
|
||||||
|
if (session) {
|
||||||
|
reply.redirect("/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid caching the login page (helps with bfcache/back behavior).
|
||||||
|
reply.header("Cache-Control", "no-store")
|
||||||
|
reply.header("Pragma", "no-cache")
|
||||||
|
reply.header("Expires", "0")
|
||||||
|
|
||||||
const status = deps.authManager.getStatus()
|
const status = deps.authManager.getStatus()
|
||||||
reply.type("text/html").send(getLoginHtml(status.username))
|
reply.type("text/html").send(getLoginHtml(status.username))
|
||||||
})
|
})
|
||||||
@@ -67,6 +79,11 @@ export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid caching the token bootstrap page.
|
||||||
|
reply.header("Cache-Control", "no-store")
|
||||||
|
reply.header("Pragma", "no-cache")
|
||||||
|
reply.header("Expires", "0")
|
||||||
|
|
||||||
reply.type("text/html").send(getTokenHtml())
|
reply.type("text/html").send(getTokenHtml())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.10.3",
|
"version": "0.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@opencode-ai/sdk": "1.1.11",
|
"@opencode-ai/sdk": "1.2.6",
|
||||||
"@solidjs/router": "^0.13.0",
|
"@solidjs/router": "^0.13.0",
|
||||||
"@suid/icons-material": "^0.9.0",
|
"@suid/icons-material": "^0.9.0",
|
||||||
"@suid/material": "^0.19.0",
|
"@suid/material": "^0.19.0",
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Component, For, Show, createMemo, createEffect, createSignal, onMount, onCleanup } from "solid-js"
|
import { Component, For, Show, createMemo, createEffect, createSignal, onMount, onCleanup } from "solid-js"
|
||||||
import { Dialog } from "@kobalte/core/dialog"
|
import { Dialog } from "@kobalte/core/dialog"
|
||||||
import { Toaster } from "solid-toast"
|
import { Toaster } from "solid-toast"
|
||||||
|
import useMediaQuery from "@suid/material/useMediaQuery"
|
||||||
|
import { Minimize2 } from "lucide-solid"
|
||||||
import AlertDialog from "./components/alert-dialog"
|
import AlertDialog from "./components/alert-dialog"
|
||||||
import FolderSelectionView from "./components/folder-selection-view"
|
import FolderSelectionView from "./components/folder-selection-view"
|
||||||
import { showConfirmDialog } from "./stores/alerts"
|
import { showConfirmDialog } from "./stores/alerts"
|
||||||
@@ -16,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"
|
||||||
@@ -70,18 +74,53 @@ 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)
|
||||||
|
|
||||||
|
const phoneQuery = useMediaQuery("(max-width: 767px)")
|
||||||
|
const isPhoneLayout = createMemo(() => phoneQuery())
|
||||||
|
|
||||||
|
// In-memory only: hides chrome on phone; may also request browser fullscreen.
|
||||||
|
const [mobileFullscreenMode, setMobileFullscreenMode] = createSignal(false)
|
||||||
|
const [browserFullscreenActive, setBrowserFullscreenActive] = createSignal(false)
|
||||||
|
|
||||||
|
const fullscreenSupported = () => {
|
||||||
|
if (typeof document === "undefined") return false
|
||||||
|
const el = document.documentElement as any
|
||||||
|
return Boolean(document.fullscreenEnabled) && typeof el?.requestFullscreen === "function"
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncBrowserFullscreenState = () => {
|
||||||
|
if (typeof document === "undefined") return
|
||||||
|
setBrowserFullscreenActive(Boolean(document.fullscreenElement))
|
||||||
|
}
|
||||||
|
|
||||||
|
const enterMobileFullscreen = async () => {
|
||||||
|
if (!isPhoneLayout()) return
|
||||||
|
setMobileFullscreenMode(true)
|
||||||
|
if (!fullscreenSupported()) return
|
||||||
|
try {
|
||||||
|
await document.documentElement.requestFullscreen()
|
||||||
|
} catch {
|
||||||
|
// Ignore: immersive mode still works without browser fullscreen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitMobileFullscreen = async () => {
|
||||||
|
if (typeof document !== "undefined" && document.fullscreenElement && typeof document.exitFullscreen === "function") {
|
||||||
|
try {
|
||||||
|
await document.exitFullscreen()
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMobileFullscreenMode(false)
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
const shouldShow =
|
const shouldShow =
|
||||||
@@ -95,6 +134,56 @@ const App: Component = () => {
|
|||||||
setInstanceTabBarHeight(element?.offsetHeight ?? 0)
|
setInstanceTabBarHeight(element?.offsetHeight ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (typeof document === "undefined") return
|
||||||
|
syncBrowserFullscreenState()
|
||||||
|
document.addEventListener("fullscreenchange", syncBrowserFullscreenState)
|
||||||
|
onCleanup(() => document.removeEventListener("fullscreenchange", syncBrowserFullscreenState))
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
const vv = window.visualViewport
|
||||||
|
if (!vv) return
|
||||||
|
|
||||||
|
const updateKeyboardOffset = () => {
|
||||||
|
// visualViewport shrinks when the OSK is visible. Use the delta as a bottom inset.
|
||||||
|
const inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop)
|
||||||
|
document.documentElement.style.setProperty("--keyboard-offset", `${Math.floor(inset)}px`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule = () => requestAnimationFrame(updateKeyboardOffset)
|
||||||
|
schedule()
|
||||||
|
vv.addEventListener("resize", schedule)
|
||||||
|
vv.addEventListener("scroll", schedule)
|
||||||
|
window.addEventListener("orientationchange", schedule)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
vv.removeEventListener("resize", schedule)
|
||||||
|
vv.removeEventListener("scroll", schedule)
|
||||||
|
window.removeEventListener("orientationchange", schedule)
|
||||||
|
document.documentElement.style.removeProperty("--keyboard-offset")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// If the user exits browser fullscreen via browser UI, restore chrome.
|
||||||
|
let lastBrowserFullscreen = false
|
||||||
|
createEffect(() => {
|
||||||
|
const active = browserFullscreenActive()
|
||||||
|
const mode = mobileFullscreenMode()
|
||||||
|
if (mode && lastBrowserFullscreen && !active) {
|
||||||
|
setMobileFullscreenMode(false)
|
||||||
|
}
|
||||||
|
lastBrowserFullscreen = active
|
||||||
|
})
|
||||||
|
|
||||||
|
// If we leave phone layout (rotation / resize), restore chrome.
|
||||||
|
createEffect(() => {
|
||||||
|
if (!isPhoneLayout() && mobileFullscreenMode()) {
|
||||||
|
void exitMobileFullscreen()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error))
|
void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error))
|
||||||
})
|
})
|
||||||
@@ -152,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
|
||||||
@@ -199,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)
|
||||||
@@ -310,6 +366,7 @@ const App: Component = () => {
|
|||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
setThinkingBlocksExpansion,
|
setThinkingBlocksExpansion,
|
||||||
|
setToolInputsVisibility,
|
||||||
handleNewInstanceRequest,
|
handleNewInstanceRequest,
|
||||||
handleCloseInstance,
|
handleCloseInstance,
|
||||||
handleNewSession,
|
handleNewSession,
|
||||||
@@ -405,19 +462,34 @@ const App: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<div class="h-screen w-screen flex flex-col">
|
<div class="h-screen w-screen flex flex-col" style={{ height: "100dvh", "padding-bottom": "var(--keyboard-offset, 0px)" }}>
|
||||||
|
<Show when={isPhoneLayout() && mobileFullscreenMode()}>
|
||||||
|
<div class="mobile-fullscreen-exit-wrapper">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-scroll-button mobile-fullscreen-exit-button"
|
||||||
|
onClick={() => void exitMobileFullscreen()}
|
||||||
|
aria-label={t("instanceShell.fullscreen.exit")}
|
||||||
|
title={t("instanceShell.fullscreen.exit")}
|
||||||
|
>
|
||||||
|
<Minimize2 class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
when={!hasInstances()}
|
when={!hasInstances()}
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<InstanceTabs
|
<Show when={!isPhoneLayout() || !mobileFullscreenMode()}>
|
||||||
instances={instances()}
|
<InstanceTabs
|
||||||
activeInstanceId={activeInstanceId()}
|
instances={instances()}
|
||||||
onSelect={setActiveInstanceId}
|
activeInstanceId={activeInstanceId()}
|
||||||
onClose={handleCloseInstance}
|
onSelect={setActiveInstanceId}
|
||||||
onNew={handleNewInstanceRequest}
|
onClose={handleCloseInstance}
|
||||||
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
onNew={handleNewInstanceRequest}
|
||||||
/>
|
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<For each={Array.from(instances().values())}>
|
<For each={Array.from(instances().values())}>
|
||||||
{(instance) => {
|
{(instance) => {
|
||||||
@@ -435,7 +507,10 @@ const App: Component = () => {
|
|||||||
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
|
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
|
||||||
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
|
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
|
||||||
onExecuteCommand={executeCommand}
|
onExecuteCommand={executeCommand}
|
||||||
tabBarOffset={instanceTabBarHeight()}
|
tabBarOffset={isPhoneLayout() && mobileFullscreenMode() ? 0 : instanceTabBarHeight()}
|
||||||
|
mobileFullscreenMode={isPhoneLayout() && mobileFullscreenMode()}
|
||||||
|
onEnterMobileFullscreen={() => void enterMobileFullscreen()}
|
||||||
|
onExitMobileFullscreen={() => void exitMobileFullscreen()}
|
||||||
/>
|
/>
|
||||||
</InstanceMetadataProvider>
|
</InstanceMetadataProvider>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -115,28 +115,28 @@ 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 class="modal-surface w-full max-w-sm p-6 border border-base shadow-2xl" tabIndex={-1}>
|
<Dialog.Content class="modal-surface w-full max-w-sm p-6 border border-base shadow-2xl" tabIndex={-1}>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<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={{
|
||||||
"background-color": accent.badgeBg,
|
"background-color": accent.badgeBg,
|
||||||
"border-color": accent.badgeBorder,
|
"border-color": accent.badgeBorder,
|
||||||
color: accent.badgeText,
|
color: accent.badgeText,
|
||||||
}}
|
}}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
{accent.symbol}
|
{accent.symbol}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-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 whitespace-pre-line break-words">
|
<Dialog.Description class="text-sm text-secondary mt-1 whitespace-pre-line break-words">
|
||||||
{payload.message}
|
{payload.message}
|
||||||
{payload.detail && <p class="mt-2 text-secondary">{payload.detail}</p>}
|
{payload.detail && <p class="mt-2 text-secondary">{payload.detail}</p>}
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={isPrompt}>
|
<Show when={isPrompt}>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
@@ -185,14 +185,14 @@ const AlertDialog: Component = () => {
|
|||||||
{confirmLabel}
|
{confirmLabel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AlertDialog
|
export default AlertDialog
|
||||||
|
|||||||
123
packages/ui/src/components/context-meter.tsx
Normal file
123
packages/ui/src/components/context-meter.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { Component } from "solid-js"
|
||||||
|
|
||||||
|
interface ContextMeterProps {
|
||||||
|
usedTokens: number
|
||||||
|
availableTokens: number | null
|
||||||
|
formatTokens: (value: number) => string
|
||||||
|
usedLabel: string
|
||||||
|
availableLabel: string
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const LABEL_CLASS = "uppercase text-[10px] tracking-wide text-muted"
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.min(Math.max(value, min), max)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFillColor(percent: number): string {
|
||||||
|
if (percent >= 0.8) return "var(--status-error)"
|
||||||
|
if (percent >= 0.6) return "var(--status-warning)"
|
||||||
|
return "var(--status-success)"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextMeter: Component<ContextMeterProps> = (props) => {
|
||||||
|
const hasAvailable = () => typeof props.availableTokens === "number" && props.availableTokens > 0
|
||||||
|
const used = () => (typeof props.usedTokens === "number" && props.usedTokens > 0 ? props.usedTokens : 0)
|
||||||
|
const available = () => (hasAvailable() ? (props.availableTokens as number) : null)
|
||||||
|
|
||||||
|
const percent = () => {
|
||||||
|
const usedValue = used()
|
||||||
|
const availableValue = available()
|
||||||
|
if (availableValue === null || availableValue <= 0) return null
|
||||||
|
|
||||||
|
// Heuristic: if available >= used, treat it like a capacity/limit.
|
||||||
|
// Otherwise treat it like remaining tokens.
|
||||||
|
const ratio = availableValue >= usedValue ? usedValue / availableValue : usedValue / (usedValue + availableValue)
|
||||||
|
return clamp(ratio, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillColor = () => {
|
||||||
|
const value = percent()
|
||||||
|
if (value === null) return "var(--border-base)"
|
||||||
|
return resolveFillColor(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentLabel = () => {
|
||||||
|
const value = percent()
|
||||||
|
if (value === null) return "--"
|
||||||
|
return `${Math.round(value * 100)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerClass =
|
||||||
|
`inline-flex items-center gap-2 rounded-full border border-base px-2 py-0.5 text-xs text-primary ${props.class ?? ""}`
|
||||||
|
|
||||||
|
function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {
|
||||||
|
const rad = (angleDeg * Math.PI) / 180
|
||||||
|
return {
|
||||||
|
x: cx + r * Math.cos(rad),
|
||||||
|
y: cy + r * Math.sin(rad),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeSectorPath(cx: number, cy: number, r: number, startAngle: number, endAngle: number) {
|
||||||
|
const start = polarToCartesian(cx, cy, r, startAngle)
|
||||||
|
const end = polarToCartesian(cx, cy, r, endAngle)
|
||||||
|
const delta = ((endAngle - startAngle) % 360 + 360) % 360
|
||||||
|
const largeArc = delta > 180 ? 1 : 0
|
||||||
|
|
||||||
|
return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y} Z`
|
||||||
|
}
|
||||||
|
|
||||||
|
const circle = () => {
|
||||||
|
const value = percent()
|
||||||
|
const size = 22
|
||||||
|
const r = 9
|
||||||
|
const cx = 11
|
||||||
|
const cy = 11
|
||||||
|
const progress = value === null ? 0 : value
|
||||||
|
const startAngle = -90
|
||||||
|
const endAngle = startAngle + progress * 360
|
||||||
|
const isFull = progress >= 0.999
|
||||||
|
const hasFill = progress > 0.001
|
||||||
|
|
||||||
|
const sectorPath = hasFill && !isFull ? describeSectorPath(cx, cy, r, startAngle, endAngle) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 22 22"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ flex: "0 0 auto" }}
|
||||||
|
>
|
||||||
|
<circle cx={String(cx)} cy={String(cy)} r={String(r)} fill="var(--surface-secondary)" />
|
||||||
|
<circle cx={String(cx)} cy={String(cy)} r={String(r)} fill="none" stroke="var(--border-base)" stroke-width="1" />
|
||||||
|
{isFull ? (
|
||||||
|
<circle cx={String(cx)} cy={String(cy)} r={String(r)} fill={fillColor()} opacity="0.95" />
|
||||||
|
) : sectorPath ? (
|
||||||
|
<path d={sectorPath} fill={fillColor()} opacity="0.95" />
|
||||||
|
) : null}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipText = () => `Context Used: ${percentLabel()}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="inline-flex items-center gap-2" title={tooltipText()}>
|
||||||
|
{circle()}
|
||||||
|
<div class={containerClass}>
|
||||||
|
<span class={LABEL_CLASS}>{props.usedLabel}</span>
|
||||||
|
<span class="font-semibold text-primary tabular-nums">{props.formatTokens(used())}</span>
|
||||||
|
<span class="text-muted">/</span>
|
||||||
|
<span class={LABEL_CLASS}>{props.availableLabel}</span>
|
||||||
|
<span class="font-semibold text-primary tabular-nums">
|
||||||
|
{available() !== null ? props.formatTokens(available() as number) : "--"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContextMeter
|
||||||
@@ -12,6 +12,7 @@ interface MonacoDiffViewerProps {
|
|||||||
after: string
|
after: string
|
||||||
viewMode?: "split" | "unified"
|
viewMode?: "split" | "unified"
|
||||||
contextMode?: "expanded" | "collapsed"
|
contextMode?: "expanded" | "collapsed"
|
||||||
|
wordWrap?: "on" | "off"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||||
@@ -54,12 +55,17 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
|||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
renderWhitespace: "selection",
|
renderWhitespace: "selection",
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
wordWrap: "off",
|
wordWrap: props.wordWrap === "on" ? "on" : "off",
|
||||||
glyphMargin: false,
|
glyphMargin: false,
|
||||||
folding: false,
|
folding: false,
|
||||||
// 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)
|
||||||
@@ -81,6 +87,7 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
|||||||
if (!ready() || !monaco || !diffEditor) return
|
if (!ready() || !monaco || !diffEditor) return
|
||||||
const viewMode = props.viewMode === "unified" ? "unified" : "split"
|
const viewMode = props.viewMode === "unified" ? "unified" : "split"
|
||||||
const contextMode = props.contextMode === "collapsed" ? "collapsed" : "expanded"
|
const contextMode = props.contextMode === "collapsed" ? "collapsed" : "expanded"
|
||||||
|
const wordWrap = props.wordWrap === "on" ? "on" : "off"
|
||||||
|
|
||||||
diffEditor.updateOptions({
|
diffEditor.updateOptions({
|
||||||
renderSideBySide: viewMode === "split",
|
renderSideBySide: viewMode === "split",
|
||||||
@@ -89,7 +96,20 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
|||||||
contextMode === "collapsed"
|
contextMode === "collapsed"
|
||||||
? { enabled: true }
|
? { enabled: true }
|
||||||
: { enabled: false },
|
: { enabled: false },
|
||||||
|
wordWrap,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
diffEditor.getOriginalEditor?.()?.updateOptions?.({ wordWrap })
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
diffEditor.getModifiedEditor?.()?.updateOptions?.({ wordWrap })
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, createMemo } from "solid-js"
|
import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, createMemo } from "solid-js"
|
||||||
import { instances, getInstanceLogs, isInstanceLogStreaming, setInstanceLogStreaming } from "../stores/instances"
|
import { getInstanceLogs, instances, isInstanceLogStreaming, setInstanceLogStreaming } from "../stores/instances"
|
||||||
import { ChevronDown } from "lucide-solid"
|
import { ChevronDown } from "lucide-solid"
|
||||||
import InstanceInfo from "./instance-info"
|
import InstanceInfo from "./instance-info"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
@@ -86,8 +86,8 @@ const InfoView: Component<InfoViewProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div class="log-container">
|
<div class="log-container">
|
||||||
<div class="flex-1 flex flex-col lg:flex-row gap-4 p-4 overflow-hidden">
|
<div class="flex-1 flex flex-col lg:flex-row gap-4 p-4 overflow-hidden">
|
||||||
<div class="lg:w-80 flex-shrink-0 overflow-y-auto">
|
<div class="lg:w-80 flex-shrink-0 min-h-0 overflow-y-auto max-h-[40vh] lg:max-h-none">
|
||||||
<Show when={instance()}>{(inst) => <InstanceInfo instance={inst()} />}</Show>
|
<Show when={instance()}>{(inst) => <InstanceInfo instance={inst()} showDisposeButton />}</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel flex-1 flex flex-col min-h-0 overflow-hidden">
|
<div class="panel flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { Component, For, Show, createMemo } from "solid-js"
|
import { Component, For, Show, createMemo, createSignal } from "solid-js"
|
||||||
import type { Instance } from "../types/instance"
|
import type { Instance } from "../types/instance"
|
||||||
import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context"
|
import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context"
|
||||||
import InstanceServiceStatus from "./instance-service-status"
|
import InstanceServiceStatus from "./instance-service-status"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import { showConfirmDialog } from "../stores/alerts"
|
||||||
|
import { disposeInstance } from "../stores/instances"
|
||||||
|
import { showToastNotification } from "../lib/notifications"
|
||||||
|
import { getLogger } from "../lib/logger"
|
||||||
|
|
||||||
interface InstanceInfoProps {
|
interface InstanceInfoProps {
|
||||||
instance: Instance
|
instance: Instance
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
|
showDisposeButton?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const log = getLogger("actions")
|
||||||
|
|
||||||
const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const metadataContext = useOptionalInstanceMetadataContext()
|
const metadataContext = useOptionalInstanceMetadataContext()
|
||||||
@@ -16,6 +23,8 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
const instanceAccessor = metadataContext?.instance ?? (() => props.instance)
|
const instanceAccessor = metadataContext?.instance ?? (() => props.instance)
|
||||||
const metadataAccessor = metadataContext?.metadata ?? (() => props.instance.metadata)
|
const metadataAccessor = metadataContext?.metadata ?? (() => props.instance.metadata)
|
||||||
|
|
||||||
|
const [isDisposing, setIsDisposing] = createSignal(false)
|
||||||
|
|
||||||
const currentInstance = () => instanceAccessor()
|
const currentInstance = () => instanceAccessor()
|
||||||
const metadata = () => metadataAccessor()
|
const metadata = () => metadataAccessor()
|
||||||
const binaryVersion = () => currentInstance().binaryVersion || metadata()?.version
|
const binaryVersion = () => currentInstance().binaryVersion || metadata()?.version
|
||||||
@@ -25,6 +34,46 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
return env ? Object.entries(env) : []
|
return env ? Object.entries(env) : []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disposeEnabled = createMemo(() => Boolean(currentInstance()?.client) && !isDisposing())
|
||||||
|
|
||||||
|
const handleDisposeInstance = async () => {
|
||||||
|
if (!disposeEnabled()) return
|
||||||
|
|
||||||
|
const confirmed = await showConfirmDialog(t("infoView.dispose.confirm.message"), {
|
||||||
|
title: t("infoView.dispose.confirm.title"),
|
||||||
|
variant: "warning",
|
||||||
|
confirmLabel: t("infoView.dispose.confirm.confirmLabel"),
|
||||||
|
cancelLabel: t("infoView.dispose.confirm.cancelLabel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
setIsDisposing(true)
|
||||||
|
try {
|
||||||
|
const ok = await disposeInstance(currentInstance().id)
|
||||||
|
if (ok) {
|
||||||
|
showToastNotification({
|
||||||
|
message: t("infoView.dispose.toast.success"),
|
||||||
|
variant: "success",
|
||||||
|
duration: 8000,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showToastNotification({
|
||||||
|
message: t("infoView.dispose.toast.error"),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to dispose instance", error)
|
||||||
|
showToastNotification({
|
||||||
|
message: t("infoView.dispose.toast.error"),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsDisposing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -156,6 +205,19 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.showDisposeButton}>
|
||||||
|
<div class="pt-3 border-t border-base">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button-danger button-small w-full"
|
||||||
|
onClick={handleDisposeInstance}
|
||||||
|
disabled={!disposeEnabled()}
|
||||||
|
>
|
||||||
|
{isDisposing() ? t("infoView.dispose.actions.disposing") : t("infoView.dispose.actions.dispose")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import PermissionNotificationBanner from "../permission-notification-banner"
|
|||||||
import PermissionApprovalModal from "../permission-approval-modal"
|
import PermissionApprovalModal from "../permission-approval-modal"
|
||||||
import SessionView from "../session/session-view"
|
import SessionView from "../session/session-view"
|
||||||
import { formatTokenTotal } from "../../lib/formatters"
|
import { formatTokenTotal } from "../../lib/formatters"
|
||||||
|
import ContextMeter from "../context-meter"
|
||||||
import { sseManager } from "../../lib/sse-manager"
|
import { sseManager } from "../../lib/sse-manager"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
import { serverApi } from "../../lib/api-client"
|
import { serverApi } from "../../lib/api-client"
|
||||||
@@ -41,7 +42,7 @@ import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests"
|
|||||||
import RightPanel from "./shell/right-panel/RightPanel"
|
import RightPanel from "./shell/right-panel/RightPanel"
|
||||||
import { useDrawerChrome } from "./shell/useDrawerChrome"
|
import { useDrawerChrome } from "./shell/useDrawerChrome"
|
||||||
import { getSessionStatus } from "../../stores/session-status"
|
import { getSessionStatus } from "../../stores/session-status"
|
||||||
import { ShieldAlert } from "lucide-solid"
|
import { Maximize2, ShieldAlert } from "lucide-solid"
|
||||||
|
|
||||||
import type { LayoutMode } from "./shell/types"
|
import type { LayoutMode } from "./shell/types"
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +70,11 @@ interface InstanceShellProps {
|
|||||||
handleSidebarModelChange: (sessionId: string, model: { providerId: string; modelId: string }) => Promise<void>
|
handleSidebarModelChange: (sessionId: string, model: { providerId: string; modelId: string }) => Promise<void>
|
||||||
onExecuteCommand: (command: Command) => void
|
onExecuteCommand: (command: Command) => void
|
||||||
tabBarOffset: number
|
tabBarOffset: number
|
||||||
|
|
||||||
|
// In-memory only: mobile immersive/fullscreen mode.
|
||||||
|
mobileFullscreenMode: boolean
|
||||||
|
onEnterMobileFullscreen: () => void
|
||||||
|
onExitMobileFullscreen: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||||
@@ -117,6 +123,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
||||||
|
const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout())
|
||||||
|
const compactPromptLayout = createMemo(() => layoutMode() !== "desktop")
|
||||||
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||||
const rightPinningSupported = createMemo(() => layoutMode() !== "phone")
|
const rightPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||||
|
|
||||||
@@ -349,16 +357,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
measureDrawerHost,
|
measureDrawerHost,
|
||||||
})
|
})
|
||||||
|
|
||||||
const formattedUsedTokens = () => formatTokenTotal(tokenStats().used)
|
|
||||||
|
|
||||||
|
|
||||||
const formattedAvailableTokens = () => {
|
|
||||||
const avail = tokenStats().avail
|
|
||||||
if (typeof avail === "number") {
|
|
||||||
return formatTokenTotal(avail)
|
|
||||||
}
|
|
||||||
return "--"
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderLeftPanel = () => {
|
const renderLeftPanel = () => {
|
||||||
if (leftPinned()) {
|
if (leftPinned()) {
|
||||||
@@ -594,13 +592,14 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
{renderLeftPanel()}
|
{renderLeftPanel()}
|
||||||
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0, minHeight: 0, overflowX: "hidden" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0, minHeight: 0, overflowX: "hidden" }}>
|
||||||
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
<Show when={!mobileFullscreen()}>
|
||||||
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
||||||
<Show
|
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
||||||
when={!isPhoneLayout()}
|
<Show
|
||||||
fallback={
|
when={!isPhoneLayout()}
|
||||||
<div class="flex flex-col w-full gap-1.5">
|
fallback={
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
<div class="flex flex-col w-full gap-1.5">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
||||||
<Show when={leftDrawerState() === "floating-closed"}>
|
<Show when={leftDrawerState() === "floating-closed"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
ref={setLeftToggleButtonEl}
|
ref={setLeftToggleButtonEl}
|
||||||
@@ -626,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" }}
|
||||||
@@ -647,6 +646,18 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={!props.mobileFullscreenMode}>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
onClick={props.onEnterMobileFullscreen}
|
||||||
|
aria-label={t("instanceShell.fullscreen.enter")}
|
||||||
|
title={t("instanceShell.fullscreen.enter")}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Maximize2 class="w-5 h-5" aria-hidden="true" />
|
||||||
|
</IconButton>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={rightDrawerState() === "floating-closed"}>
|
<Show when={rightDrawerState() === "floating-closed"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
ref={setRightToggleButtonEl}
|
ref={setRightToggleButtonEl}
|
||||||
@@ -661,20 +672,15 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
<ContextMeter
|
||||||
<span class="uppercase text-[10px] tracking-wide text-muted">
|
usedTokens={tokenStats().used}
|
||||||
{t("instanceShell.metrics.usedLabel")}
|
availableTokens={tokenStats().avail}
|
||||||
</span>
|
formatTokens={formatTokenTotal}
|
||||||
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||||
|
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
|
||||||
<span class="uppercase text-[10px] tracking-wide text-muted">
|
|
||||||
{t("instanceShell.metrics.availableLabel")}
|
|
||||||
</span>
|
|
||||||
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -693,18 +699,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!showingInfoView()}>
|
<Show when={!showingInfoView()}>
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
<ContextMeter
|
||||||
<span class="uppercase text-[10px] tracking-wide text-muted">
|
usedTokens={tokenStats().used}
|
||||||
{t("instanceShell.metrics.usedLabel")}
|
availableTokens={tokenStats().avail}
|
||||||
</span>
|
formatTokens={formatTokenTotal}
|
||||||
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
|
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||||
</div>
|
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||||
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
|
/>
|
||||||
<span class="uppercase text-[10px] tracking-wide text-muted">
|
|
||||||
{t("instanceShell.metrics.availableLabel")}
|
|
||||||
</span>
|
|
||||||
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center session-header-hints">
|
<div class="ml-auto flex items-center session-header-hints">
|
||||||
@@ -720,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" }}
|
||||||
@@ -769,9 +770,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component="main"
|
component="main"
|
||||||
@@ -808,6 +810,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
instanceId={props.instance.id}
|
instanceId={props.instance.id}
|
||||||
instanceFolder={props.instance.folder}
|
instanceFolder={props.instance.folder}
|
||||||
escapeInDebounce={props.escapeInDebounce}
|
escapeInDebounce={props.escapeInDebounce}
|
||||||
|
isPhoneLayout={isPhoneLayout()}
|
||||||
|
compactPromptLayout={compactPromptLayout()}
|
||||||
showSidebarToggle={showEmbeddedSidebarToggle()}
|
showSidebarToggle={showEmbeddedSidebarToggle()}
|
||||||
onSidebarToggle={() => setLeftOpen(true)}
|
onSidebarToggle={() => setLeftOpen(true)}
|
||||||
forceCompactStatusLayout={showEmbeddedSidebarToggle()}
|
forceCompactStatusLayout={showEmbeddedSidebarToggle()}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type { Instance } from "../../../../types/instance"
|
|||||||
import type { BackgroundProcess } from "../../../../../../server/src/api-types"
|
import type { BackgroundProcess } from "../../../../../../server/src/api-types"
|
||||||
import type { Session } from "../../../../types/session"
|
import type { Session } from "../../../../types/session"
|
||||||
import type { DrawerViewState } from "../types"
|
import type { DrawerViewState } from "../types"
|
||||||
import type { DiffContextMode, DiffViewMode, RightPanelTab } from "./types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode, RightPanelTab } from "./types"
|
||||||
|
|
||||||
import ChangesTab from "./tabs/ChangesTab"
|
import ChangesTab from "./tabs/ChangesTab"
|
||||||
import FilesTab from "./tabs/FilesTab"
|
import FilesTab from "./tabs/FilesTab"
|
||||||
@@ -32,6 +32,7 @@ import { useGlobalPointerDrag } from "../useGlobalPointerDrag"
|
|||||||
import {
|
import {
|
||||||
RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY,
|
RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY,
|
||||||
RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY,
|
RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY,
|
||||||
|
RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY,
|
||||||
RIGHT_PANEL_CHANGES_LIST_OPEN_NONPHONE_KEY,
|
RIGHT_PANEL_CHANGES_LIST_OPEN_NONPHONE_KEY,
|
||||||
RIGHT_PANEL_CHANGES_LIST_OPEN_PHONE_KEY,
|
RIGHT_PANEL_CHANGES_LIST_OPEN_PHONE_KEY,
|
||||||
RIGHT_PANEL_CHANGES_SPLIT_WIDTH_KEY,
|
RIGHT_PANEL_CHANGES_SPLIT_WIDTH_KEY,
|
||||||
@@ -102,6 +103,9 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
const [diffContextMode, setDiffContextMode] = createSignal<DiffContextMode>(
|
const [diffContextMode, setDiffContextMode] = createSignal<DiffContextMode>(
|
||||||
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, ["expanded", "collapsed"] as const) ?? "collapsed",
|
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, ["expanded", "collapsed"] as const) ?? "collapsed",
|
||||||
)
|
)
|
||||||
|
const [diffWordWrapMode, setDiffWordWrapMode] = createSignal<DiffWordWrapMode>(
|
||||||
|
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY, ["on", "off"] as const) ?? "on",
|
||||||
|
)
|
||||||
|
|
||||||
const [changesSplitWidth, setChangesSplitWidth] = createSignal(320)
|
const [changesSplitWidth, setChangesSplitWidth] = createSignal(320)
|
||||||
const [filesSplitWidth, setFilesSplitWidth] = createSignal(320)
|
const [filesSplitWidth, setFilesSplitWidth] = createSignal(320)
|
||||||
@@ -195,6 +199,11 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, diffContextMode())
|
window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, diffContextMode())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY, diffWordWrapMode())
|
||||||
|
})
|
||||||
|
|
||||||
const clampSplitWidth = (value: number) => {
|
const clampSplitWidth = (value: number) => {
|
||||||
const min = 200
|
const min = 200
|
||||||
const maxByDrawer = Math.max(min, Math.floor(props.rightDrawerWidth() * 0.65))
|
const maxByDrawer = Math.max(min, Math.floor(props.rightDrawerWidth() * 0.65))
|
||||||
@@ -738,8 +747,10 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
onSelectFile={handleSelectChangesFile}
|
onSelectFile={handleSelectChangesFile}
|
||||||
diffViewMode={diffViewMode}
|
diffViewMode={diffViewMode}
|
||||||
diffContextMode={diffContextMode}
|
diffContextMode={diffContextMode}
|
||||||
|
diffWordWrapMode={diffWordWrapMode}
|
||||||
onViewModeChange={setDiffViewMode}
|
onViewModeChange={setDiffViewMode}
|
||||||
onContextModeChange={setDiffContextMode}
|
onContextModeChange={setDiffContextMode}
|
||||||
|
onWordWrapModeChange={setDiffWordWrapMode}
|
||||||
listOpen={changesListOpen}
|
listOpen={changesListOpen}
|
||||||
onToggleList={toggleChangesList}
|
onToggleList={toggleChangesList}
|
||||||
splitWidth={changesSplitWidth}
|
splitWidth={changesSplitWidth}
|
||||||
@@ -765,8 +776,10 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
scopeKey={gitScopeKey}
|
scopeKey={gitScopeKey}
|
||||||
diffViewMode={diffViewMode}
|
diffViewMode={diffViewMode}
|
||||||
diffContextMode={diffContextMode}
|
diffContextMode={diffContextMode}
|
||||||
|
diffWordWrapMode={diffWordWrapMode}
|
||||||
onViewModeChange={setDiffViewMode}
|
onViewModeChange={setDiffViewMode}
|
||||||
onContextModeChange={setDiffContextMode}
|
onContextModeChange={setDiffContextMode}
|
||||||
|
onWordWrapModeChange={setDiffWordWrapMode}
|
||||||
onOpenFile={(path) => void openGitFile(path)}
|
onOpenFile={(path) => void openGitFile(path)}
|
||||||
onRefresh={() => void refreshGitStatus()}
|
onRefresh={() => void refreshGitStatus()}
|
||||||
listOpen={gitChangesListOpen}
|
listOpen={gitChangesListOpen}
|
||||||
|
|||||||
@@ -1,50 +1,61 @@
|
|||||||
import type { Component } from "solid-js"
|
import type { Component } from "solid-js"
|
||||||
|
|
||||||
import type { DiffContextMode, DiffViewMode } from "../types"
|
import { AlignJustify, FoldVertical, Split, UnfoldVertical, WrapText } from "lucide-solid"
|
||||||
|
|
||||||
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
||||||
|
|
||||||
interface DiffToolbarProps {
|
interface DiffToolbarProps {
|
||||||
viewMode: DiffViewMode
|
viewMode: DiffViewMode
|
||||||
contextMode: DiffContextMode
|
contextMode: DiffContextMode
|
||||||
|
wordWrapMode: DiffWordWrapMode
|
||||||
onViewModeChange: (mode: DiffViewMode) => void
|
onViewModeChange: (mode: DiffViewMode) => void
|
||||||
onContextModeChange: (mode: DiffContextMode) => void
|
onContextModeChange: (mode: DiffContextMode) => void
|
||||||
|
onWordWrapModeChange: (mode: DiffWordWrapMode) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiffToolbar: Component<DiffToolbarProps> = (props) => {
|
const DiffToolbar: Component<DiffToolbarProps> = (props) => {
|
||||||
|
const nextViewMode = (): DiffViewMode => (props.viewMode === "split" ? "unified" : "split")
|
||||||
|
const nextContextMode = (): DiffContextMode => (props.contextMode === "collapsed" ? "expanded" : "collapsed")
|
||||||
|
const nextWordWrapMode = (): DiffWordWrapMode => (props.wordWrapMode === "on" ? "off" : "on")
|
||||||
|
|
||||||
|
const viewModeTitle = () => (nextViewMode() === "split" ? "Switch to split view" : "Switch to unified view")
|
||||||
|
const contextModeTitle = () =>
|
||||||
|
nextContextMode() === "collapsed" ? "Hide unchanged regions" : "Show full file"
|
||||||
|
const wordWrapTitle = () => (nextWordWrapMode() === "on" ? "Enable word wrap" : "Disable word wrap")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="file-viewer-toolbar">
|
<div class="file-viewer-toolbar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`file-viewer-toolbar-button${props.viewMode === "split" ? " active" : ""}`}
|
class="file-viewer-toolbar-icon-button"
|
||||||
aria-pressed={props.viewMode === "split"}
|
onClick={() => props.onViewModeChange(nextViewMode())}
|
||||||
onClick={() => props.onViewModeChange("split")}
|
aria-label={viewModeTitle()}
|
||||||
|
title={viewModeTitle()}
|
||||||
>
|
>
|
||||||
Split
|
{nextViewMode() === "split" ? <Split class="h-4 w-4" aria-hidden="true" /> : <AlignJustify class="h-4 w-4" aria-hidden="true" />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`file-viewer-toolbar-button${props.viewMode === "unified" ? " active" : ""}`}
|
class="file-viewer-toolbar-icon-button"
|
||||||
aria-pressed={props.viewMode === "unified"}
|
onClick={() => props.onContextModeChange(nextContextMode())}
|
||||||
onClick={() => props.onViewModeChange("unified")}
|
aria-label={contextModeTitle()}
|
||||||
|
title={contextModeTitle()}
|
||||||
>
|
>
|
||||||
Unified
|
{nextContextMode() === "collapsed" ? (
|
||||||
|
<FoldVertical class="h-4 w-4" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<UnfoldVertical class="h-4 w-4" aria-hidden="true" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`file-viewer-toolbar-button${props.contextMode === "collapsed" ? " active" : ""}`}
|
class={`file-viewer-toolbar-icon-button${props.wordWrapMode === "on" ? " active" : ""}`}
|
||||||
aria-pressed={props.contextMode === "collapsed"}
|
onClick={() => props.onWordWrapModeChange(nextWordWrapMode())}
|
||||||
onClick={() => props.onContextModeChange("collapsed")}
|
aria-label={wordWrapTitle()}
|
||||||
title="Hide unchanged regions"
|
title={wordWrapTitle()}
|
||||||
>
|
>
|
||||||
Collapsed
|
<WrapText class="h-4 w-4" aria-hidden="true" />
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`file-viewer-toolbar-button${props.contextMode === "expanded" ? " active" : ""}`}
|
|
||||||
aria-pressed={props.contextMode === "expanded"}
|
|
||||||
onClick={() => props.onContextModeChange("expanded")}
|
|
||||||
title="Show full file"
|
|
||||||
>
|
|
||||||
Expanded
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
|
|||||||
|
|
||||||
import DiffToolbar from "../components/DiffToolbar"
|
import DiffToolbar from "../components/DiffToolbar"
|
||||||
import SplitFilePanel from "../components/SplitFilePanel"
|
import SplitFilePanel from "../components/SplitFilePanel"
|
||||||
import type { DiffContextMode, DiffViewMode } from "../types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
||||||
|
|
||||||
interface ChangesTabProps {
|
interface ChangesTabProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
@@ -18,8 +18,10 @@ interface ChangesTabProps {
|
|||||||
|
|
||||||
diffViewMode: Accessor<DiffViewMode>
|
diffViewMode: Accessor<DiffViewMode>
|
||||||
diffContextMode: Accessor<DiffContextMode>
|
diffContextMode: Accessor<DiffContextMode>
|
||||||
|
diffWordWrapMode: Accessor<DiffWordWrapMode>
|
||||||
onViewModeChange: (mode: DiffViewMode) => void
|
onViewModeChange: (mode: DiffViewMode) => void
|
||||||
onContextModeChange: (mode: DiffContextMode) => void
|
onContextModeChange: (mode: DiffContextMode) => void
|
||||||
|
onWordWrapModeChange: (mode: DiffWordWrapMode) => void
|
||||||
|
|
||||||
listOpen: Accessor<boolean>
|
listOpen: Accessor<boolean>
|
||||||
onToggleList: () => void
|
onToggleList: () => void
|
||||||
@@ -77,14 +79,6 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
|
|
||||||
const renderViewer = () => (
|
const renderViewer = () => (
|
||||||
<div class="file-viewer-panel flex-1">
|
<div class="file-viewer-panel flex-1">
|
||||||
<div class="file-viewer-header">
|
|
||||||
<DiffToolbar
|
|
||||||
viewMode={props.diffViewMode()}
|
|
||||||
contextMode={props.diffContextMode()}
|
|
||||||
onViewModeChange={props.onViewModeChange}
|
|
||||||
onContextModeChange={props.onContextModeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="file-viewer-content file-viewer-content--monaco">
|
<div class="file-viewer-content file-viewer-content--monaco">
|
||||||
<Show
|
<Show
|
||||||
when={selectedFileData && hasSession && Array.isArray(diffs) && diffs.length > 0 ? selectedFileData : null}
|
when={selectedFileData && hasSession && Array.isArray(diffs) && diffs.length > 0 ? selectedFileData : null}
|
||||||
@@ -102,6 +96,7 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
after={String((file() as any).after || "")}
|
after={String((file() as any).after || "")}
|
||||||
viewMode={props.diffViewMode()}
|
viewMode={props.diffViewMode()}
|
||||||
contextMode={props.diffContextMode()}
|
contextMode={props.diffContextMode()}
|
||||||
|
wordWrap={props.diffWordWrapMode()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -182,6 +177,17 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
<span class="files-tab-stat-value">-{totals.deletions}</span>
|
<span class="files-tab-stat-value">-{totals.deletions}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ "margin-left": "auto" }}>
|
||||||
|
<DiffToolbar
|
||||||
|
viewMode={props.diffViewMode()}
|
||||||
|
contextMode={props.diffContextMode()}
|
||||||
|
wordWrapMode={props.diffWordWrapMode()}
|
||||||
|
onViewModeChange={props.onViewModeChange}
|
||||||
|
onContextModeChange={props.onContextModeChange}
|
||||||
|
onWordWrapModeChange={props.onWordWrapModeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
|
|||||||
|
|
||||||
import DiffToolbar from "../components/DiffToolbar"
|
import DiffToolbar from "../components/DiffToolbar"
|
||||||
import SplitFilePanel from "../components/SplitFilePanel"
|
import SplitFilePanel from "../components/SplitFilePanel"
|
||||||
import type { DiffContextMode, DiffViewMode } from "../types"
|
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode } from "../types"
|
||||||
|
|
||||||
interface GitChangesTabProps {
|
interface GitChangesTabProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
@@ -29,8 +29,10 @@ interface GitChangesTabProps {
|
|||||||
|
|
||||||
diffViewMode: Accessor<DiffViewMode>
|
diffViewMode: Accessor<DiffViewMode>
|
||||||
diffContextMode: Accessor<DiffContextMode>
|
diffContextMode: Accessor<DiffContextMode>
|
||||||
|
diffWordWrapMode: Accessor<DiffWordWrapMode>
|
||||||
onViewModeChange: (mode: DiffViewMode) => void
|
onViewModeChange: (mode: DiffViewMode) => void
|
||||||
onContextModeChange: (mode: DiffContextMode) => void
|
onContextModeChange: (mode: DiffContextMode) => void
|
||||||
|
onWordWrapModeChange: (mode: DiffWordWrapMode) => void
|
||||||
|
|
||||||
onOpenFile: (path: string) => void
|
onOpenFile: (path: string) => void
|
||||||
onRefresh: () => void
|
onRefresh: () => void
|
||||||
@@ -80,14 +82,6 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
|
|
||||||
const renderViewer = () => (
|
const renderViewer = () => (
|
||||||
<div class="file-viewer-panel flex-1">
|
<div class="file-viewer-panel flex-1">
|
||||||
<div class="file-viewer-header">
|
|
||||||
<DiffToolbar
|
|
||||||
viewMode={props.diffViewMode()}
|
|
||||||
contextMode={props.diffContextMode()}
|
|
||||||
onViewModeChange={props.onViewModeChange}
|
|
||||||
onContextModeChange={props.onContextModeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="file-viewer-content file-viewer-content--monaco">
|
<div class="file-viewer-content file-viewer-content--monaco">
|
||||||
<Show
|
<Show
|
||||||
when={props.selectedLoading()}
|
when={props.selectedLoading()}
|
||||||
@@ -122,6 +116,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
after={String((file() as any).after || "")}
|
after={String((file() as any).after || "")}
|
||||||
viewMode={props.diffViewMode()}
|
viewMode={props.diffViewMode()}
|
||||||
contextMode={props.diffContextMode()}
|
contextMode={props.diffContextMode()}
|
||||||
|
wordWrap={props.diffWordWrapMode()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -237,6 +232,15 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<RefreshCw class={`h-4 w-4${props.statusLoading() ? " animate-spin" : ""}`} />
|
<RefreshCw class={`h-4 w-4${props.statusLoading() ? " animate-spin" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<DiffToolbar
|
||||||
|
viewMode={props.diffViewMode()}
|
||||||
|
contextMode={props.diffContextMode()}
|
||||||
|
wordWrapMode={props.diffWordWrapMode()}
|
||||||
|
onViewModeChange={props.onViewModeChange}
|
||||||
|
onContextModeChange={props.onContextModeChange}
|
||||||
|
onWordWrapModeChange={props.onWordWrapModeChange}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
||||||
|
|||||||
@@ -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" : ""}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ export type RightPanelTab = "changes" | "git-changes" | "files" | "status"
|
|||||||
export type DiffViewMode = "split" | "unified"
|
export type DiffViewMode = "split" | "unified"
|
||||||
|
|
||||||
export type DiffContextMode = "expanded" | "collapsed"
|
export type DiffContextMode = "expanded" | "collapsed"
|
||||||
|
|
||||||
|
export type DiffWordWrapMode = "on" | "off"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const RIGHT_PANEL_GIT_CHANGES_LIST_OPEN_NONPHONE_KEY = "opencode-session-
|
|||||||
export const RIGHT_PANEL_GIT_CHANGES_LIST_OPEN_PHONE_KEY = "opencode-session-right-panel-git-changes-list-open-phone-v1"
|
export const RIGHT_PANEL_GIT_CHANGES_LIST_OPEN_PHONE_KEY = "opencode-session-right-panel-git-changes-list-open-phone-v1"
|
||||||
export const RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY = "opencode-session-right-panel-changes-diff-view-mode-v1"
|
export const RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY = "opencode-session-right-panel-changes-diff-view-mode-v1"
|
||||||
export const RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY = "opencode-session-right-panel-changes-diff-context-mode-v1"
|
export const RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY = "opencode-session-right-panel-changes-diff-context-mode-v1"
|
||||||
|
export const RIGHT_PANEL_CHANGES_DIFF_WORD_WRAP_KEY = "opencode-session-right-panel-changes-diff-word-wrap-v1"
|
||||||
|
|
||||||
export const clampWidth = (value: number) =>
|
export const clampWidth = (value: number) =>
|
||||||
Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
|
Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
|
import ContextMeter from "./context-meter"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
|
||||||
const METRIC_CHIP_CLASS = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary"
|
|
||||||
const METRIC_LABEL_CLASS = "uppercase text-[10px] tracking-wide text-muted"
|
|
||||||
|
|
||||||
interface MessageListHeaderProps {
|
interface MessageListHeaderProps {
|
||||||
usedTokens: number
|
usedTokens: number
|
||||||
|
|
||||||
@@ -21,7 +19,6 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const hasAvailableTokens = () => typeof props.availableTokens === "number"
|
const hasAvailableTokens = () => typeof props.availableTokens === "number"
|
||||||
const availableDisplay = () => (hasAvailableTokens() ? props.formatTokens(props.availableTokens as number) : "--")
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={props.forceCompactStatusLayout ? "connection-status connection-status--compact" : "connection-status"}>
|
<div class={props.forceCompactStatusLayout ? "connection-status connection-status--compact" : "connection-status"}>
|
||||||
@@ -40,14 +37,13 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
|
|||||||
|
|
||||||
<div class="connection-status-text connection-status-info">
|
<div class="connection-status-text connection-status-info">
|
||||||
<div class="connection-status-usage">
|
<div class="connection-status-usage">
|
||||||
<div class={METRIC_CHIP_CLASS}>
|
<ContextMeter
|
||||||
<span class={METRIC_LABEL_CLASS}>{t("messageListHeader.metrics.usedLabel")}</span>
|
usedTokens={props.usedTokens}
|
||||||
<span class="font-semibold text-primary">{props.formatTokens(props.usedTokens)}</span>
|
availableTokens={hasAvailableTokens() ? (props.availableTokens as number) : null}
|
||||||
</div>
|
formatTokens={props.formatTokens}
|
||||||
<div class={METRIC_CHIP_CLASS}>
|
usedLabel={t("messageListHeader.metrics.usedLabel")}
|
||||||
<span class={METRIC_LABEL_CLASS}>{t("messageListHeader.metrics.availableLabel")}</span>
|
availableLabel={t("messageListHeader.metrics.availableLabel")}
|
||||||
<span class="font-semibold text-primary">{hasAvailableTokens() ? availableDisplay() : "--"}</span>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -55,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")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import ToolCall from "./tool-call"
|
|||||||
import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state"
|
import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state"
|
||||||
import { Markdown } from "./markdown"
|
import { Markdown } from "./markdown"
|
||||||
import { useTheme } from "../lib/theme"
|
import { useTheme } from "../lib/theme"
|
||||||
import { useConfig } from "../stores/preferences"
|
|
||||||
import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/message"
|
import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/message"
|
||||||
|
|
||||||
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
||||||
@@ -17,16 +16,18 @@ interface MessagePartProps {
|
|||||||
// Other synthetic text parts (tool traces, read outputs, etc.) should be hidden.
|
// Other synthetic text parts (tool traces, read outputs, etc.) should be hidden.
|
||||||
primaryUserTextPartId?: string | null
|
primaryUserTextPartId?: string | null
|
||||||
onRendered?: () => void
|
onRendered?: () => void
|
||||||
}
|
}
|
||||||
export default function MessagePart(props: MessagePartProps) {
|
|
||||||
|
export default function MessagePart(props: MessagePartProps) {
|
||||||
|
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const { preferences } = useConfig()
|
|
||||||
const partType = () => props.part?.type || ""
|
const partType = () => props.part?.type || ""
|
||||||
const reasoningId = () => `reasoning-${props.part?.id || ""}`
|
const reasoningId = () => `reasoning-${props.part?.id || ""}`
|
||||||
const isReasoningExpanded = () => isItemExpanded(reasoningId())
|
const isReasoningExpanded = () => isItemExpanded(reasoningId())
|
||||||
const isAssistantMessage = () => props.messageType === "assistant"
|
const isAssistantMessage = () => props.messageType === "assistant"
|
||||||
const textContainerClass = () => (isAssistantMessage() ? "message-text message-text-assistant" : "message-text")
|
const textContainerClass = () => (isAssistantMessage() ? "message-text message-text-assistant" : "message-text")
|
||||||
|
const markdownContainerClass = () => "message-text message-text-assistant"
|
||||||
|
const textContainerRole = () => props.messageType || "assistant"
|
||||||
|
|
||||||
const shouldHideTextPart = () => {
|
const shouldHideTextPart = () => {
|
||||||
const part = props.part
|
const part = props.part
|
||||||
@@ -57,6 +58,11 @@ interface MessagePartProps {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canRenderMarkdown = () => {
|
||||||
|
const id = (props.part as unknown as { id?: unknown })?.id
|
||||||
|
return typeof id === "string" && id.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
function reasoningSegmentHasText(segment: unknown): boolean {
|
function reasoningSegmentHasText(segment: unknown): boolean {
|
||||||
if (typeof segment === "string") {
|
if (typeof segment === "string") {
|
||||||
return segment.trim().length > 0
|
return segment.trim().length > 0
|
||||||
@@ -91,20 +97,28 @@ interface MessagePartProps {
|
|||||||
|
|
||||||
const createTextPartForMarkdown = (): TextPart => {
|
const createTextPartForMarkdown = (): TextPart => {
|
||||||
const part = props.part
|
const part = props.part
|
||||||
if ((part.type === "text" || part.type === "reasoning") && typeof part.text === "string") {
|
if (part.type === "text" && typeof part.text === "string") {
|
||||||
|
// Pass through the original part so `renderCache` updates persist.
|
||||||
|
return part as unknown as TextPart
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.type === "reasoning" && typeof (part as any).text === "string") {
|
||||||
|
// Reasoning parts render as markdown in some views; normalize to TextPart.
|
||||||
return {
|
return {
|
||||||
id: part.id,
|
id: part.id,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: part.text,
|
text: (part as any).text,
|
||||||
synthetic: part.type === "text" ? part.synthetic : false,
|
synthetic: false,
|
||||||
version: (part as { version?: number }).version
|
version: (part as { version?: number }).version,
|
||||||
|
renderCache: (part as any).renderCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: part.id,
|
id: part.id,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "",
|
text: "",
|
||||||
synthetic: false
|
synthetic: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,22 +131,18 @@ interface MessagePartProps {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={partType() === "text"}>
|
<Match when={partType() === "text"}>
|
||||||
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
||||||
<div class={textContainerClass()}>
|
<div class={canRenderMarkdown() ? markdownContainerClass() : textContainerClass()} data-role={textContainerRole()}>
|
||||||
<Show
|
<Show when={canRenderMarkdown()} fallback={<span class="text-primary">{plainTextContent()}</span>}>
|
||||||
when={isAssistantMessage()}
|
<Markdown
|
||||||
fallback={<span class="text-primary">{plainTextContent()}</span>}
|
part={createTextPartForMarkdown()}
|
||||||
>
|
instanceId={props.instanceId}
|
||||||
<Markdown
|
sessionId={props.sessionId}
|
||||||
part={createTextPartForMarkdown()}
|
isDark={isDark()}
|
||||||
instanceId={props.instanceId}
|
size={isAssistantMessage() ? "tight" : "base"}
|
||||||
sessionId={props.sessionId}
|
onRendered={props.onRendered}
|
||||||
isDark={isDark()}
|
/>
|
||||||
size={isAssistantMessage() ? "tight" : "base"}
|
</Show>
|
||||||
onRendered={props.onRendered}
|
</div>
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
|
|||||||
@@ -176,15 +176,26 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
onMount(() => {
|
const isCoarsePointer = () => {
|
||||||
|
if (typeof window === "undefined") return false
|
||||||
|
return Boolean(window.matchMedia?.("(pointer: coarse)")?.matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
// Scope global "type-to-focus" behavior to the active, visible prompt only.
|
||||||
|
if (typeof document === "undefined") return
|
||||||
|
if (isCoarsePointer()) return
|
||||||
|
if (props.isActive === false) return
|
||||||
|
if (props.disabled) return
|
||||||
|
|
||||||
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
||||||
const activeElement = document.activeElement as HTMLElement
|
const activeElement = document.activeElement as HTMLElement | null
|
||||||
|
|
||||||
const isInputElement =
|
const isInputElement =
|
||||||
activeElement?.tagName === "INPUT" ||
|
activeElement?.tagName === "INPUT" ||
|
||||||
activeElement?.tagName === "TEXTAREA" ||
|
activeElement?.tagName === "TEXTAREA" ||
|
||||||
activeElement?.tagName === "SELECT" ||
|
activeElement?.tagName === "SELECT" ||
|
||||||
activeElement?.isContentEditable
|
Boolean(activeElement?.isContentEditable)
|
||||||
|
|
||||||
if (isInputElement) return
|
if (isInputElement) return
|
||||||
|
|
||||||
@@ -192,16 +203,25 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
if (isModifierKey) return
|
if (isModifierKey) return
|
||||||
|
|
||||||
const isSpecialKey =
|
const isSpecialKey =
|
||||||
e.key === "Tab" || e.key === "Enter" || e.key.startsWith("Arrow") || e.key === "Backspace" || e.key === "Delete"
|
e.key === "Tab" ||
|
||||||
|
e.key === "Enter" ||
|
||||||
|
e.key.startsWith("Arrow") ||
|
||||||
|
e.key === "Backspace" ||
|
||||||
|
e.key === "Delete"
|
||||||
if (isSpecialKey) return
|
if (isSpecialKey) return
|
||||||
|
|
||||||
if (e.key.length === 1 && textareaRef && !props.disabled) {
|
const textarea = textareaRef
|
||||||
textareaRef.focus()
|
if (!textarea || textarea.disabled) return
|
||||||
|
|
||||||
|
// In session cache mode inactive panes are display:none; avoid stealing focus.
|
||||||
|
if (textarea.offsetParent === null) return
|
||||||
|
|
||||||
|
if (e.key.length === 1) {
|
||||||
|
textarea.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", handleGlobalKeyDown)
|
document.addEventListener("keydown", handleGlobalKeyDown)
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
document.removeEventListener("keydown", handleGlobalKeyDown)
|
document.removeEventListener("keydown", handleGlobalKeyDown)
|
||||||
})
|
})
|
||||||
@@ -331,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) {
|
||||||
@@ -435,7 +457,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
rows={expandState() === "expanded" ? 15 : 4}
|
rows={expandState() === "expanded" ? (props.compactLayout ? 10 : 15) : 3}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export interface PromptInputProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
instanceFolder: string
|
instanceFolder: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
|
|
||||||
|
// Used to scope global "type-to-focus" behavior.
|
||||||
|
isActive?: boolean
|
||||||
|
|
||||||
|
// Phone/tablet layouts should keep the expanded prompt more compact.
|
||||||
|
compactLayout?: boolean
|
||||||
onSend: (prompt: string, attachments: Attachment[]) => Promise<void>
|
onSend: (prompt: string, attachments: Attachment[]) => Promise<void>
|
||||||
onRunShell?: (command: string) => Promise<void>
|
onRunShell?: (command: string) => Promise<void>
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
const [meta, setMeta] = createSignal<ServerMeta | null>(null)
|
const [meta, setMeta] = createSignal<ServerMeta | null>(null)
|
||||||
const [authStatus, setAuthStatus] = createSignal<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean } | null>(null)
|
const [authStatus, setAuthStatus] = createSignal<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean } | null>(null)
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
|
const [applyingListeningMode, setApplyingListeningMode] = createSignal(false)
|
||||||
const [qrCodes, setQrCodes] = createSignal<Record<string, string>>({})
|
const [qrCodes, setQrCodes] = createSignal<Record<string, string>>({})
|
||||||
const [expandedUrl, setExpandedUrl] = createSignal<string | null>(null)
|
const [expandedUrl, setExpandedUrl] = createSignal<string | null>(null)
|
||||||
const [error, setError] = createSignal<string | null>(null)
|
const [error, setError] = createSignal<string | null>(null)
|
||||||
@@ -88,6 +89,10 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (applyingListeningMode()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const confirmed = await showConfirmDialog(t("remoteAccess.listeningMode.restartConfirm.message"), {
|
const confirmed = await showConfirmDialog(t("remoteAccess.listeningMode.restartConfirm.message"), {
|
||||||
title: allow ? t("remoteAccess.listeningMode.restartConfirm.title.all") : t("remoteAccess.listeningMode.restartConfirm.title.local"),
|
title: allow ? t("remoteAccess.listeningMode.restartConfirm.title.all") : t("remoteAccess.listeningMode.restartConfirm.title.local"),
|
||||||
variant: "warning",
|
variant: "warning",
|
||||||
@@ -100,12 +105,21 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setListeningMode(targetMode)
|
setApplyingListeningMode(true)
|
||||||
const restarted = await restartCli()
|
setError(null)
|
||||||
if (!restarted) {
|
try {
|
||||||
setError(t("remoteAccess.restart.errorManual"))
|
// Important: await the config patch before restart so Electron reads the updated mode from disk.
|
||||||
} else {
|
await setListeningMode(targetMode)
|
||||||
setMeta((prev) => (prev ? { ...prev, listeningMode: targetMode } : prev))
|
const restarted = await restartCli()
|
||||||
|
if (!restarted) {
|
||||||
|
setError(t("remoteAccess.restart.errorManual"))
|
||||||
|
} else {
|
||||||
|
setMeta((prev) => (prev ? { ...prev, listeningMode: targetMode } : prev))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
|
} finally {
|
||||||
|
setApplyingListeningMode(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
void refreshMeta()
|
void refreshMeta()
|
||||||
@@ -196,6 +210,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
onChange={(nextChecked) => {
|
onChange={(nextChecked) => {
|
||||||
void handleAllowConnectionsChange(nextChecked)
|
void handleAllowConnectionsChange(nextChecked)
|
||||||
}}
|
}}
|
||||||
|
disabled={loading() || applyingListeningMode()}
|
||||||
>
|
>
|
||||||
<Switch.Input />
|
<Switch.Input />
|
||||||
<Switch.Control class="remote-toggle-switch" data-checked={allowExternalConnections()}>
|
<Switch.Control class="remote-toggle-switch" data-checked={allowExternalConnections()}>
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ interface SessionViewProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
instanceFolder: string
|
instanceFolder: string
|
||||||
escapeInDebounce: boolean
|
escapeInDebounce: boolean
|
||||||
|
isPhoneLayout?: boolean
|
||||||
|
compactPromptLayout?: boolean
|
||||||
showSidebarToggle?: boolean
|
showSidebarToggle?: boolean
|
||||||
onSidebarToggle?: () => void
|
onSidebarToggle?: () => void
|
||||||
forceCompactStatusLayout?: boolean
|
forceCompactStatusLayout?: boolean
|
||||||
@@ -76,6 +78,9 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
(isActive) => {
|
(isActive) => {
|
||||||
if (!isActive) return
|
if (!isActive) return
|
||||||
|
|
||||||
|
// On phones, focusing the prompt on session switch is disruptive (it raises the OSK).
|
||||||
|
if (props.isPhoneLayout) return
|
||||||
|
|
||||||
// Don't steal focus from other inputs (command palette, dialogs, selectors, etc.)
|
// Don't steal focus from other inputs (command palette, dialogs, selectors, etc.)
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
const activeEl = document.activeElement as HTMLElement | null
|
const activeEl = document.activeElement as HTMLElement | null
|
||||||
@@ -314,17 +319,19 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<PromptInput
|
<PromptInput
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
instanceFolder={props.instanceFolder}
|
instanceFolder={props.instanceFolder}
|
||||||
sessionId={activeSession.id}
|
sessionId={activeSession.id}
|
||||||
onSend={handleSendMessage}
|
isActive={props.isActive}
|
||||||
onRunShell={handleRunShell}
|
compactLayout={props.compactPromptLayout}
|
||||||
escapeInDebounce={props.escapeInDebounce}
|
onSend={handleSendMessage}
|
||||||
isSessionBusy={sessionBusy()}
|
onRunShell={handleRunShell}
|
||||||
disabled={sessionNeedsInput()}
|
escapeInDebounce={props.escapeInDebounce}
|
||||||
onAbortSession={handleAbortSession}
|
isSessionBusy={sessionBusy()}
|
||||||
registerPromptInputApi={registerPromptInputApi}
|
disabled={sessionNeedsInput()}
|
||||||
/>
|
onAbortSession={handleAbortSession}
|
||||||
|
registerPromptInputApi={registerPromptInputApi}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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: () => {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "Open right drawer",
|
"instanceShell.rightDrawer.toggle.open": "Open right drawer",
|
||||||
"instanceShell.rightDrawer.toggle.close": "Close right drawer",
|
"instanceShell.rightDrawer.toggle.close": "Close right drawer",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "Full screen",
|
||||||
|
"instanceShell.fullscreen.exit": "Exit full screen",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "Used",
|
"instanceShell.metrics.usedLabel": "Used",
|
||||||
"instanceShell.metrics.availableLabel": "Avail",
|
"instanceShell.metrics.availableLabel": "Avail",
|
||||||
|
|
||||||
@@ -93,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...",
|
||||||
|
|||||||
@@ -15,4 +15,13 @@ export const logMessages = {
|
|||||||
"infoView.logs.paused.description": "Enable streaming to watch your OpenCode server activity.",
|
"infoView.logs.paused.description": "Enable streaming to watch your OpenCode server activity.",
|
||||||
"infoView.logs.empty.waiting": "Waiting for server output...",
|
"infoView.logs.empty.waiting": "Waiting for server output...",
|
||||||
"infoView.logs.scrollToBottom": "Scroll to bottom",
|
"infoView.logs.scrollToBottom": "Scroll to bottom",
|
||||||
|
|
||||||
|
"infoView.dispose.actions.dispose": "Dispose instance",
|
||||||
|
"infoView.dispose.actions.disposing": "Disposing...",
|
||||||
|
"infoView.dispose.confirm.title": "Dispose instance?",
|
||||||
|
"infoView.dispose.confirm.message": "This clears cached per-project state for this directory and reloads the instance.",
|
||||||
|
"infoView.dispose.confirm.confirmLabel": "Dispose",
|
||||||
|
"infoView.dispose.confirm.cancelLabel": "Cancel",
|
||||||
|
"infoView.dispose.toast.success": "Instance disposed. Reloading...",
|
||||||
|
"infoView.dispose.toast.error": "Failed to dispose instance.",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -39,13 +39,16 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "Abrir panel derecho",
|
"instanceShell.rightDrawer.toggle.open": "Abrir panel derecho",
|
||||||
"instanceShell.rightDrawer.toggle.close": "Cerrar panel derecho",
|
"instanceShell.rightDrawer.toggle.close": "Cerrar panel derecho",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "Pantalla completa",
|
||||||
|
"instanceShell.fullscreen.exit": "Salir de pantalla completa",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "Usado",
|
"instanceShell.metrics.usedLabel": "Usado",
|
||||||
"instanceShell.metrics.availableLabel": "Disp.",
|
"instanceShell.metrics.availableLabel": "Disp.",
|
||||||
|
|
||||||
"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",
|
||||||
@@ -90,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",
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,13 @@ export const logMessages = {
|
|||||||
"infoView.logs.paused.description": "Activa el streaming para ver la actividad de tu servidor de OpenCode.",
|
"infoView.logs.paused.description": "Activa el streaming para ver la actividad de tu servidor de OpenCode.",
|
||||||
"infoView.logs.empty.waiting": "Esperando la salida del servidor...",
|
"infoView.logs.empty.waiting": "Esperando la salida del servidor...",
|
||||||
"infoView.logs.scrollToBottom": "Desplazarse al final",
|
"infoView.logs.scrollToBottom": "Desplazarse al final",
|
||||||
|
|
||||||
|
"infoView.dispose.actions.dispose": "Desechar instancia",
|
||||||
|
"infoView.dispose.actions.disposing": "Desechando...",
|
||||||
|
"infoView.dispose.confirm.title": "¿Desechar instancia?",
|
||||||
|
"infoView.dispose.confirm.message": "Esto borra el estado en caché por proyecto para este directorio y recarga la instancia.",
|
||||||
|
"infoView.dispose.confirm.confirmLabel": "Desechar",
|
||||||
|
"infoView.dispose.confirm.cancelLabel": "Cancelar",
|
||||||
|
"infoView.dispose.toast.success": "Instancia desechada. Recargando...",
|
||||||
|
"infoView.dispose.toast.error": "No se pudo desechar la instancia.",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "Ouvrir le tiroir droit",
|
"instanceShell.rightDrawer.toggle.open": "Ouvrir le tiroir droit",
|
||||||
"instanceShell.rightDrawer.toggle.close": "Fermer le tiroir droit",
|
"instanceShell.rightDrawer.toggle.close": "Fermer le tiroir droit",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "Plein écran",
|
||||||
|
"instanceShell.fullscreen.exit": "Quitter le plein écran",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "Utilisé",
|
"instanceShell.metrics.usedLabel": "Utilisé",
|
||||||
"instanceShell.metrics.availableLabel": "Dispo",
|
"instanceShell.metrics.availableLabel": "Dispo",
|
||||||
|
|
||||||
@@ -91,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...",
|
||||||
|
|||||||
@@ -15,4 +15,13 @@ export const logMessages = {
|
|||||||
"infoView.logs.paused.description": "Activez le streaming pour suivre l'activité de votre serveur OpenCode.",
|
"infoView.logs.paused.description": "Activez le streaming pour suivre l'activité de votre serveur OpenCode.",
|
||||||
"infoView.logs.empty.waiting": "En attente de la sortie du serveur...",
|
"infoView.logs.empty.waiting": "En attente de la sortie du serveur...",
|
||||||
"infoView.logs.scrollToBottom": "Aller en bas",
|
"infoView.logs.scrollToBottom": "Aller en bas",
|
||||||
|
|
||||||
|
"infoView.dispose.actions.dispose": "Réinitialiser l'instance",
|
||||||
|
"infoView.dispose.actions.disposing": "Réinitialisation...",
|
||||||
|
"infoView.dispose.confirm.title": "Réinitialiser l'instance ?",
|
||||||
|
"infoView.dispose.confirm.message": "Cela efface l'état en cache pour ce répertoire et recharge l'instance.",
|
||||||
|
"infoView.dispose.confirm.confirmLabel": "Réinitialiser",
|
||||||
|
"infoView.dispose.confirm.cancelLabel": "Annuler",
|
||||||
|
"infoView.dispose.toast.success": "Instance réinitialisée. Rechargement...",
|
||||||
|
"infoView.dispose.toast.error": "Impossible de réinitialiser l'instance.",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "右ドロワーを開く",
|
"instanceShell.rightDrawer.toggle.open": "右ドロワーを開く",
|
||||||
"instanceShell.rightDrawer.toggle.close": "右ドロワーを閉じる",
|
"instanceShell.rightDrawer.toggle.close": "右ドロワーを閉じる",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "全画面",
|
||||||
|
"instanceShell.fullscreen.exit": "全画面を終了",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "使用",
|
"instanceShell.metrics.usedLabel": "使用",
|
||||||
"instanceShell.metrics.availableLabel": "残り",
|
"instanceShell.metrics.availableLabel": "残り",
|
||||||
|
|
||||||
@@ -91,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": "変更を取得中...",
|
||||||
|
|||||||
@@ -15,4 +15,13 @@ export const logMessages = {
|
|||||||
"infoView.logs.paused.description": "ストリーミングを有効にして OpenCode サーバーの動作を監視します。",
|
"infoView.logs.paused.description": "ストリーミングを有効にして OpenCode サーバーの動作を監視します。",
|
||||||
"infoView.logs.empty.waiting": "サーバー出力を待機中...",
|
"infoView.logs.empty.waiting": "サーバー出力を待機中...",
|
||||||
"infoView.logs.scrollToBottom": "最下部へスクロール",
|
"infoView.logs.scrollToBottom": "最下部へスクロール",
|
||||||
|
|
||||||
|
"infoView.dispose.actions.dispose": "インスタンスを破棄",
|
||||||
|
"infoView.dispose.actions.disposing": "破棄しています...",
|
||||||
|
"infoView.dispose.confirm.title": "インスタンスを破棄しますか?",
|
||||||
|
"infoView.dispose.confirm.message": "このディレクトリのプロジェクト状態キャッシュをクリアし、インスタンスを再読み込みします。",
|
||||||
|
"infoView.dispose.confirm.confirmLabel": "破棄",
|
||||||
|
"infoView.dispose.confirm.cancelLabel": "キャンセル",
|
||||||
|
"infoView.dispose.toast.success": "インスタンスを破棄しました。再読み込み中...",
|
||||||
|
"infoView.dispose.toast.error": "インスタンスの破棄に失敗しました。",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -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 表示モード",
|
||||||
|
|||||||
@@ -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, статистика",
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "Открыть правую панель",
|
"instanceShell.rightDrawer.toggle.open": "Открыть правую панель",
|
||||||
"instanceShell.rightDrawer.toggle.close": "Закрыть правую панель",
|
"instanceShell.rightDrawer.toggle.close": "Закрыть правую панель",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "Полный экран",
|
||||||
|
"instanceShell.fullscreen.exit": "Выйти из полного экрана",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "Использовано",
|
"instanceShell.metrics.usedLabel": "Использовано",
|
||||||
"instanceShell.metrics.availableLabel": "Доступно",
|
"instanceShell.metrics.availableLabel": "Доступно",
|
||||||
|
|
||||||
@@ -91,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": "Загрузка изменений...",
|
||||||
@@ -125,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": "Добавить",
|
||||||
|
|||||||
@@ -15,4 +15,13 @@ export const logMessages = {
|
|||||||
"infoView.logs.paused.description": "Включите стриминг, чтобы наблюдать за активностью сервера OpenCode.",
|
"infoView.logs.paused.description": "Включите стриминг, чтобы наблюдать за активностью сервера OpenCode.",
|
||||||
"infoView.logs.empty.waiting": "Ожидание вывода сервера…",
|
"infoView.logs.empty.waiting": "Ожидание вывода сервера…",
|
||||||
"infoView.logs.scrollToBottom": "Прокрутить вниз",
|
"infoView.logs.scrollToBottom": "Прокрутить вниз",
|
||||||
|
|
||||||
|
"infoView.dispose.actions.dispose": "Сбросить инстанс",
|
||||||
|
"infoView.dispose.actions.disposing": "Сброс...",
|
||||||
|
"infoView.dispose.confirm.title": "Сбросить инстанс?",
|
||||||
|
"infoView.dispose.confirm.message": "Это очистит кэш состояния проекта для этого каталога и перезагрузит инстанс.",
|
||||||
|
"infoView.dispose.confirm.confirmLabel": "Сбросить",
|
||||||
|
"infoView.dispose.confirm.cancelLabel": "Отмена",
|
||||||
|
"infoView.dispose.toast.success": "Инстанс сброшен. Перезагрузка...",
|
||||||
|
"infoView.dispose.toast.error": "Не удалось сбросить инстанс.",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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, 令牌, 用量, 费用, 统计",
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightDrawer.toggle.open": "打开右侧抽屉",
|
"instanceShell.rightDrawer.toggle.open": "打开右侧抽屉",
|
||||||
"instanceShell.rightDrawer.toggle.close": "关闭右侧抽屉",
|
"instanceShell.rightDrawer.toggle.close": "关闭右侧抽屉",
|
||||||
|
|
||||||
|
"instanceShell.fullscreen.enter": "全屏",
|
||||||
|
"instanceShell.fullscreen.exit": "退出全屏",
|
||||||
|
|
||||||
"instanceShell.metrics.usedLabel": "已用",
|
"instanceShell.metrics.usedLabel": "已用",
|
||||||
"instanceShell.metrics.availableLabel": "可用",
|
"instanceShell.metrics.availableLabel": "可用",
|
||||||
|
|
||||||
@@ -91,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": "正在获取会话更改...",
|
||||||
|
|||||||
@@ -15,4 +15,13 @@ export const logMessages = {
|
|||||||
"infoView.logs.paused.description": "启用流式输出以查看 OpenCode 服务器活动。",
|
"infoView.logs.paused.description": "启用流式输出以查看 OpenCode 服务器活动。",
|
||||||
"infoView.logs.empty.waiting": "正在等待服务器输出...",
|
"infoView.logs.empty.waiting": "正在等待服务器输出...",
|
||||||
"infoView.logs.scrollToBottom": "滚动到底部",
|
"infoView.logs.scrollToBottom": "滚动到底部",
|
||||||
|
|
||||||
|
"infoView.dispose.actions.dispose": "释放实例",
|
||||||
|
"infoView.dispose.actions.disposing": "正在释放...",
|
||||||
|
"infoView.dispose.confirm.title": "要释放实例吗?",
|
||||||
|
"infoView.dispose.confirm.message": "这将清除此目录的项目缓存状态,并重新加载实例。",
|
||||||
|
"infoView.dispose.confirm.confirmLabel": "释放",
|
||||||
|
"infoView.dispose.confirm.cancelLabel": "取消",
|
||||||
|
"infoView.dispose.toast.success": "实例已释放。正在重新加载...",
|
||||||
|
"infoView.dispose.toast.error": "释放实例失败。",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -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 视图模式",
|
||||||
|
|||||||
29
packages/ui/src/lib/launch-errors.ts
Normal file
29
packages/ui/src/lib/launch-errors.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export function formatLaunchErrorMessage(error: unknown, fallbackMessage: string): string {
|
||||||
|
if (!error) {
|
||||||
|
return fallbackMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as unknown
|
||||||
|
if (parsed && typeof parsed === "object" && "error" in parsed && typeof (parsed as any).error === "string") {
|
||||||
|
return (parsed as any).error
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore JSON parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMissingBinaryMessage(message: string): boolean {
|
||||||
|
const normalized = message.toLowerCase()
|
||||||
|
return (
|
||||||
|
normalized.includes("opencode binary not found") ||
|
||||||
|
normalized.includes("binary not found") ||
|
||||||
|
normalized.includes("no such file or directory") ||
|
||||||
|
normalized.includes("binary is not executable") ||
|
||||||
|
normalized.includes("enoent")
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,12 +4,12 @@ import { CODENOMAD_API_BASE } from "./api-client"
|
|||||||
class SDKManager {
|
class SDKManager {
|
||||||
private clients = new Map<string, OpencodeClient>()
|
private clients = new Map<string, OpencodeClient>()
|
||||||
|
|
||||||
private key(instanceId: string, worktreeSlug: string): string {
|
private key(instanceId: string, proxyPath: string): string {
|
||||||
return `${instanceId}:${worktreeSlug || "root"}`
|
return `${instanceId}:${normalizeProxyPath(proxyPath)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
createClient(instanceId: string, proxyPath: string, worktreeSlug = "root"): OpencodeClient {
|
createClient(instanceId: string, proxyPath: string, _worktreeSlug = "root"): OpencodeClient {
|
||||||
const key = this.key(instanceId, worktreeSlug)
|
const key = this.key(instanceId, proxyPath)
|
||||||
const existing = this.clients.get(key)
|
const existing = this.clients.get(key)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing
|
return existing
|
||||||
@@ -23,12 +23,12 @@ class SDKManager {
|
|||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
getClient(instanceId: string, worktreeSlug = "root"): OpencodeClient | null {
|
getClient(instanceId: string, proxyPath: string): OpencodeClient | null {
|
||||||
return this.clients.get(this.key(instanceId, worktreeSlug)) ?? null
|
return this.clients.get(this.key(instanceId, proxyPath)) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyClient(instanceId: string, worktreeSlug = "root"): void {
|
destroyClient(instanceId: string, proxyPath: string): void {
|
||||||
this.clients.delete(this.key(instanceId, worktreeSlug))
|
this.clients.delete(this.key(instanceId, proxyPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyClientsForInstance(instanceId: string): void {
|
destroyClientsForInstance(instanceId: string): void {
|
||||||
@@ -46,7 +46,7 @@ class SDKManager {
|
|||||||
|
|
||||||
export type { OpencodeClient }
|
export type { OpencodeClient }
|
||||||
|
|
||||||
function buildInstanceBaseUrl(proxyPath: string): string {
|
export function buildInstanceBaseUrl(proxyPath: string): string {
|
||||||
const normalized = normalizeProxyPath(proxyPath)
|
const normalized = normalizeProxyPath(proxyPath)
|
||||||
const base = stripTrailingSlashes(CODENOMAD_API_BASE)
|
const base = stripTrailingSlashes(CODENOMAD_API_BASE)
|
||||||
return `${base}${normalized}/`
|
return `${base}${normalized}/`
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ interface BackgroundProcessRemovedEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ServerInstanceDisposedEvent {
|
||||||
|
type: "server.instance.disposed"
|
||||||
|
properties: {
|
||||||
|
directory: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type SSEEvent =
|
type SSEEvent =
|
||||||
| MessageUpdateEvent
|
| MessageUpdateEvent
|
||||||
| MessageRemovedEvent
|
| MessageRemovedEvent
|
||||||
@@ -74,6 +81,7 @@ type SSEEvent =
|
|||||||
| TuiToastEvent
|
| TuiToastEvent
|
||||||
| BackgroundProcessUpdatedEvent
|
| BackgroundProcessUpdatedEvent
|
||||||
| BackgroundProcessRemovedEvent
|
| BackgroundProcessRemovedEvent
|
||||||
|
| ServerInstanceDisposedEvent
|
||||||
| { type: string; properties?: Record<string, unknown> }
|
| { type: string; properties?: Record<string, unknown> }
|
||||||
|
|
||||||
type ConnectionStatus = InstanceStreamStatus
|
type ConnectionStatus = InstanceStreamStatus
|
||||||
@@ -173,6 +181,9 @@ class SSEManager {
|
|||||||
case "background.process.removed":
|
case "background.process.removed":
|
||||||
this.onBackgroundProcessRemoved?.(instanceId, event as BackgroundProcessRemovedEvent)
|
this.onBackgroundProcessRemoved?.(instanceId, event as BackgroundProcessRemovedEvent)
|
||||||
break
|
break
|
||||||
|
case "server.instance.disposed":
|
||||||
|
this.onInstanceDisposed?.(instanceId, event as ServerInstanceDisposedEvent)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
log.warn("Unknown SSE event type", { type: event.type })
|
log.warn("Unknown SSE event type", { type: event.type })
|
||||||
}
|
}
|
||||||
@@ -205,6 +216,7 @@ class SSEManager {
|
|||||||
onLspUpdated?: (instanceId: string, event: EventLspUpdated) => void
|
onLspUpdated?: (instanceId: string, event: EventLspUpdated) => void
|
||||||
onBackgroundProcessUpdated?: (instanceId: string, event: BackgroundProcessUpdatedEvent) => void
|
onBackgroundProcessUpdated?: (instanceId: string, event: BackgroundProcessUpdatedEvent) => void
|
||||||
onBackgroundProcessRemoved?: (instanceId: string, event: BackgroundProcessRemovedEvent) => void
|
onBackgroundProcessRemoved?: (instanceId: string, event: BackgroundProcessRemovedEvent) => void
|
||||||
|
onInstanceDisposed?: (instanceId: string, event: ServerInstanceDisposedEvent) => void
|
||||||
onConnectionLost?: (instanceId: string, reason: string) => void | Promise<void>
|
onConnectionLost?: (instanceId: string, reason: string) => void | Promise<void>
|
||||||
|
|
||||||
getStatus(instanceId: string): ConnectionStatus | null {
|
getStatus(instanceId: string): ConnectionStatus | null {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getPermissionCreatedAt, getPermissionSessionId } from "../types/permiss
|
|||||||
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { getQuestionSessionId } from "../types/question"
|
import { getQuestionSessionId } from "../types/question"
|
||||||
import { requestData } from "../lib/opencode-api"
|
import { requestData } from "../lib/opencode-api"
|
||||||
import { sdkManager } from "../lib/sdk-manager"
|
import { buildInstanceBaseUrl, sdkManager } from "../lib/sdk-manager"
|
||||||
import { sseManager } from "../lib/sse-manager"
|
import { sseManager } from "../lib/sse-manager"
|
||||||
import { serverApi } from "../lib/api-client"
|
import { serverApi } from "../lib/api-client"
|
||||||
import { serverEvents } from "../lib/server-events"
|
import { serverEvents } from "../lib/server-events"
|
||||||
@@ -18,7 +18,14 @@ import {
|
|||||||
fetchProviders,
|
fetchProviders,
|
||||||
clearInstanceDraftPrompts,
|
clearInstanceDraftPrompts,
|
||||||
} from "./sessions"
|
} from "./sessions"
|
||||||
import { ensureWorktreesLoaded, ensureWorktreeMapLoaded, getOrCreateWorktreeClient, getWorktreeSlugForSession } from "./worktrees"
|
import {
|
||||||
|
ensureWorktreesLoaded,
|
||||||
|
ensureWorktreeMapLoaded,
|
||||||
|
getOrCreateWorktreeClient,
|
||||||
|
getWorktreeSlugForSession,
|
||||||
|
reloadWorktreeMap,
|
||||||
|
reloadWorktrees,
|
||||||
|
} from "./worktrees"
|
||||||
import { fetchCommands, clearCommands } from "./commands"
|
import { fetchCommands, clearCommands } from "./commands"
|
||||||
import { serverSettings } from "./preferences"
|
import { serverSettings } from "./preferences"
|
||||||
import { setSessionPendingPermission, setSessionPendingQuestion } from "./session-state"
|
import { setSessionPendingPermission, setSessionPendingQuestion } from "./session-state"
|
||||||
@@ -28,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")
|
||||||
|
|
||||||
@@ -45,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>()
|
||||||
@@ -76,6 +86,9 @@ const [disconnectedInstance, setDisconnectedInstance] = createSignal<Disconnecte
|
|||||||
|
|
||||||
const MAX_LOG_ENTRIES = 1000
|
const MAX_LOG_ENTRIES = 1000
|
||||||
|
|
||||||
|
const pendingDisposeRequests = new Map<string, Promise<boolean>>()
|
||||||
|
const pendingRehydrations = new Map<string, Promise<void>>()
|
||||||
|
|
||||||
function workspaceDescriptorToInstance(descriptor: WorkspaceDescriptor): Instance {
|
function workspaceDescriptorToInstance(descriptor: WorkspaceDescriptor): Instance {
|
||||||
const existing = instances().get(descriptor.id)
|
const existing = instances().get(descriptor.id)
|
||||||
return {
|
return {
|
||||||
@@ -228,10 +241,15 @@ async function syncPendingQuestions(instanceId: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hydrateInstanceData(instanceId: string) {
|
async function hydrateInstanceData(instanceId: string, options?: { force?: boolean }) {
|
||||||
try {
|
try {
|
||||||
await ensureWorktreesLoaded(instanceId)
|
if (options?.force) {
|
||||||
await ensureWorktreeMapLoaded(instanceId)
|
await reloadWorktrees(instanceId)
|
||||||
|
await reloadWorktreeMap(instanceId)
|
||||||
|
} else {
|
||||||
|
await ensureWorktreesLoaded(instanceId)
|
||||||
|
await ensureWorktreeMapLoaded(instanceId)
|
||||||
|
}
|
||||||
await fetchSessions(instanceId)
|
await fetchSessions(instanceId)
|
||||||
await fetchAgents(instanceId)
|
await fetchAgents(instanceId)
|
||||||
await fetchProviders(instanceId)
|
await fetchProviders(instanceId)
|
||||||
@@ -246,6 +264,91 @@ async function hydrateInstanceData(instanceId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function postInstanceDispose(instanceId: string): Promise<boolean> {
|
||||||
|
const instance = instances().get(instanceId)
|
||||||
|
if (!instance?.proxyPath) {
|
||||||
|
throw new Error("Instance not ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = buildInstanceBaseUrl(instance.proxyPath)
|
||||||
|
const url = new URL("instance/dispose", baseUrl)
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await response.text().catch(() => "")
|
||||||
|
throw new Error(message || `Dispose request failed with ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get("content-type") ?? ""
|
||||||
|
if (contentType.includes("application/json")) {
|
||||||
|
const data = await response.json().catch(() => undefined)
|
||||||
|
if (typeof data === "boolean") return data
|
||||||
|
if (data && typeof data === "object" && "data" in (data as any)) {
|
||||||
|
return Boolean((data as any).data)
|
||||||
|
}
|
||||||
|
return Boolean(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text().catch(() => "")
|
||||||
|
if (text.trim() === "true") return true
|
||||||
|
if (text.trim() === "false") return false
|
||||||
|
return Boolean(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rehydrateInstance(instanceId: string, options?: { reason?: string }): Promise<void> {
|
||||||
|
if (pendingRehydrations.has(instanceId)) {
|
||||||
|
return pendingRehydrations.get(instanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const instance = instances().get(instanceId)
|
||||||
|
if (!instance?.client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Rehydrating instance", { instanceId, reason: options?.reason })
|
||||||
|
clearCacheForInstance(instanceId)
|
||||||
|
clearCommands(instanceId)
|
||||||
|
clearInstanceMetadata(instanceId)
|
||||||
|
clearInstanceDraftPrompts(instanceId)
|
||||||
|
clearPermissionQueue(instanceId)
|
||||||
|
clearQuestionQueue(instanceId)
|
||||||
|
|
||||||
|
await hydrateInstanceData(instanceId, { force: true })
|
||||||
|
})().finally(() => {
|
||||||
|
pendingRehydrations.delete(instanceId)
|
||||||
|
})
|
||||||
|
|
||||||
|
pendingRehydrations.set(instanceId, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disposeInstance(instanceId: string): Promise<boolean> {
|
||||||
|
if (pendingDisposeRequests.has(instanceId)) {
|
||||||
|
return pendingDisposeRequests.get(instanceId)!
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const ok = await postInstanceDispose(instanceId)
|
||||||
|
if (ok) {
|
||||||
|
await rehydrateInstance(instanceId, { reason: "disposed" })
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
})().finally(() => {
|
||||||
|
pendingDisposeRequests.delete(instanceId)
|
||||||
|
})
|
||||||
|
|
||||||
|
pendingDisposeRequests.set(instanceId, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
void (async function initializeWorkspaces() {
|
void (async function initializeWorkspaces() {
|
||||||
try {
|
try {
|
||||||
const workspaces = await serverApi.fetchWorkspaces()
|
const workspaces = await serverApi.fetchWorkspaces()
|
||||||
@@ -270,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)
|
||||||
@@ -777,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,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) {
|
||||||
@@ -809,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)
|
||||||
@@ -834,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> {
|
||||||
@@ -844,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,
|
||||||
}),
|
}),
|
||||||
@@ -859,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",
|
||||||
@@ -939,6 +1065,30 @@ sseManager.onLspUpdated = async (instanceId) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sseManager.onInstanceDisposed = (sourceInstanceId, event) => {
|
||||||
|
const directory = event?.properties?.directory
|
||||||
|
if (!directory) {
|
||||||
|
void rehydrateInstance(sourceInstanceId, { reason: "disposed" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingInstanceIds: string[] = []
|
||||||
|
for (const instance of instances().values()) {
|
||||||
|
if (instance.folder === directory) {
|
||||||
|
matchingInstanceIds.push(instance.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingInstanceIds.length === 0) {
|
||||||
|
void rehydrateInstance(sourceInstanceId, { reason: "disposed" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const instanceId of matchingInstanceIds) {
|
||||||
|
void rehydrateInstance(instanceId, { reason: "disposed" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function acknowledgeDisconnectedInstance(): Promise<void> {
|
async function acknowledgeDisconnectedInstance(): Promise<void> {
|
||||||
const pending = disconnectedInstance()
|
const pending = disconnectedInstance()
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
@@ -995,4 +1145,5 @@ export {
|
|||||||
disconnectedInstance,
|
disconnectedInstance,
|
||||||
acknowledgeDisconnectedInstance,
|
acknowledgeDisconnectedInstance,
|
||||||
fetchLspStatus,
|
fetchLspStatus,
|
||||||
|
disposeInstance,
|
||||||
}
|
}
|
||||||
|
|||||||
53
packages/ui/src/stores/launch-errors.ts
Normal file
53
packages/ui/src/stores/launch-errors.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { createSignal } from "solid-js"
|
||||||
|
import type { WorkspaceDescriptor } from "../../../server/src/api-types"
|
||||||
|
import { tGlobal } from "../lib/i18n"
|
||||||
|
import { formatLaunchErrorMessage, isMissingBinaryMessage } from "../lib/launch-errors"
|
||||||
|
|
||||||
|
type LaunchErrorSource = "create" | "workspace"
|
||||||
|
|
||||||
|
export interface LaunchErrorState {
|
||||||
|
source: LaunchErrorSource
|
||||||
|
message: string
|
||||||
|
binaryPath: string
|
||||||
|
missingBinary: boolean
|
||||||
|
instanceId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const [launchError, setLaunchError] = createSignal<LaunchErrorState | null>(null)
|
||||||
|
|
||||||
|
// Avoid spamming the user with the same modal on repeated events.
|
||||||
|
const lastWorkspaceErrorByInstanceId = new Map<string, string>()
|
||||||
|
|
||||||
|
export function showLaunchError(next: LaunchErrorState) {
|
||||||
|
setLaunchError(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLaunchError() {
|
||||||
|
setLaunchError(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showWorkspaceLaunchError(workspace: WorkspaceDescriptor) {
|
||||||
|
const instanceId = workspace.id
|
||||||
|
const rawMessage = workspace.error
|
||||||
|
const message = formatLaunchErrorMessage(rawMessage, tGlobal("app.launchError.fallbackMessage"))
|
||||||
|
|
||||||
|
const previous = lastWorkspaceErrorByInstanceId.get(instanceId)
|
||||||
|
if (previous && previous === message) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastWorkspaceErrorByInstanceId.set(instanceId, message)
|
||||||
|
|
||||||
|
const binaryPath = (workspace.binaryLabel || workspace.binaryId || "opencode").trim() || "opencode"
|
||||||
|
const missingBinary = isMissingBinaryMessage(message)
|
||||||
|
|
||||||
|
showLaunchError({
|
||||||
|
source: "workspace",
|
||||||
|
instanceId,
|
||||||
|
message,
|
||||||
|
binaryPath,
|
||||||
|
missingBinary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { launchError }
|
||||||
@@ -63,9 +63,14 @@ export function updateSessionInfo(instanceId: string, sessionId: string): void {
|
|||||||
resolveSelectedModel(instanceProviders, latestProviderId, latestModelId)
|
resolveSelectedModel(instanceProviders, latestProviderId, latestModelId)
|
||||||
|
|
||||||
let modelOutputLimit = DEFAULT_MODEL_OUTPUT_LIMIT
|
let modelOutputLimit = DEFAULT_MODEL_OUTPUT_LIMIT
|
||||||
|
let modelInputLimit: number | null = null
|
||||||
|
|
||||||
if (selectedModel) {
|
if (selectedModel) {
|
||||||
contextWindow = selectedModel.limit?.context ?? 0
|
contextWindow = selectedModel.limit?.context ?? 0
|
||||||
|
const inputLimit = selectedModel.limit?.input
|
||||||
|
if (typeof inputLimit === "number" && inputLimit > 0) {
|
||||||
|
modelInputLimit = inputLimit
|
||||||
|
}
|
||||||
const outputLimit = selectedModel.limit?.output
|
const outputLimit = selectedModel.limit?.output
|
||||||
if (typeof outputLimit === "number" && outputLimit > 0) {
|
if (typeof outputLimit === "number" && outputLimit > 0) {
|
||||||
modelOutputLimit = Math.min(outputLimit, DEFAULT_MODEL_OUTPUT_LIMIT)
|
modelOutputLimit = Math.min(outputLimit, DEFAULT_MODEL_OUTPUT_LIMIT)
|
||||||
@@ -107,7 +112,13 @@ export function updateSessionInfo(instanceId: string, sessionId: string): void {
|
|||||||
|
|
||||||
const outputBudget = Math.min(modelOutputLimit, DEFAULT_MODEL_OUTPUT_LIMIT)
|
const outputBudget = Math.min(modelOutputLimit, DEFAULT_MODEL_OUTPUT_LIMIT)
|
||||||
|
|
||||||
if (!contextAvailableFromPrevious) {
|
if (modelInputLimit !== null) {
|
||||||
|
// Prefer explicit input limits when provided by the API.
|
||||||
|
// This is used by the UI "Avail" chip.
|
||||||
|
contextAvailableTokens = modelInputLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contextAvailableFromPrevious && contextAvailableTokens === null) {
|
||||||
if (contextWindow > 0) {
|
if (contextWindow > 0) {
|
||||||
if (latestHasContextUsage && actualUsageTokens > 0) {
|
if (latestHasContextUsage && actualUsageTokens > 0) {
|
||||||
contextAvailableTokens = Math.max(contextWindow - (actualUsageTokens + outputBudget), 0)
|
contextAvailableTokens = Math.max(contextWindow - (actualUsageTokens + outputBudget), 0)
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -304,10 +311,10 @@ function setThemePreference(preference: ThemePreference): void {
|
|||||||
void patchConfigOwner("ui", { theme: preference }).catch((error) => log.error("Failed to set theme", error))
|
void patchConfigOwner("ui", { theme: preference }).catch((error) => log.error("Failed to set theme", error))
|
||||||
}
|
}
|
||||||
|
|
||||||
function setListeningMode(mode: ListeningMode): void {
|
async function setListeningMode(mode: ListeningMode): Promise<void> {
|
||||||
if (serverSettings().listeningMode === mode) return
|
if (serverSettings().listeningMode === mode) return
|
||||||
void patchConfigOwner("server", { listeningMode: mode }).catch((error) => log.error("Failed to set listening mode", error))
|
await patchConfigOwner("server", { listeningMode: mode })
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEnvironmentVariables(envVars: Record<string, string>): void {
|
function updateEnvironmentVariables(envVars: Record<string, string>): void {
|
||||||
void patchConfigOwner("server", { environmentVariables: envVars }).catch((error) =>
|
void patchConfigOwner("server", { environmentVariables: envVars }).catch((error) =>
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,12 +291,13 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
|||||||
const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId)
|
const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId)
|
||||||
const initialModel = initialProvider?.models.find((m) => m.id === session.model.modelId)
|
const initialModel = initialProvider?.models.find((m) => m.id === session.model.modelId)
|
||||||
const initialContextWindow = initialModel?.limit?.context ?? 0
|
const initialContextWindow = initialModel?.limit?.context ?? 0
|
||||||
|
const initialInputLimit = initialModel?.limit?.input ?? 0
|
||||||
const initialSubscriptionModel = initialModel?.cost?.input === 0 && initialModel?.cost?.output === 0
|
const initialSubscriptionModel = initialModel?.cost?.input === 0 && initialModel?.cost?.output === 0
|
||||||
const initialOutputLimit =
|
const initialOutputLimit =
|
||||||
initialModel?.limit?.output && initialModel.limit.output > 0
|
initialModel?.limit?.output && initialModel.limit.output > 0
|
||||||
? initialModel.limit.output
|
? initialModel.limit.output
|
||||||
: DEFAULT_MODEL_OUTPUT_LIMIT
|
: DEFAULT_MODEL_OUTPUT_LIMIT
|
||||||
const initialContextAvailable = initialContextWindow > 0 ? initialContextWindow : null
|
const initialContextAvailable = initialInputLimit > 0 ? initialInputLimit : initialContextWindow > 0 ? initialContextWindow : null
|
||||||
|
|
||||||
setSessionInfoByInstance((prev) => {
|
setSessionInfoByInstance((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
@@ -398,10 +399,11 @@ async function forkSession(
|
|||||||
const forkProvider = instanceProviders.find((p) => p.id === forkedSession.model.providerId)
|
const forkProvider = instanceProviders.find((p) => p.id === forkedSession.model.providerId)
|
||||||
const forkModel = forkProvider?.models.find((m) => m.id === forkedSession.model.modelId)
|
const forkModel = forkProvider?.models.find((m) => m.id === forkedSession.model.modelId)
|
||||||
const forkContextWindow = forkModel?.limit?.context ?? 0
|
const forkContextWindow = forkModel?.limit?.context ?? 0
|
||||||
|
const forkInputLimit = forkModel?.limit?.input ?? 0
|
||||||
const forkSubscriptionModel = forkModel?.cost?.input === 0 && forkModel?.cost?.output === 0
|
const forkSubscriptionModel = forkModel?.cost?.input === 0 && forkModel?.cost?.output === 0
|
||||||
const forkOutputLimit =
|
const forkOutputLimit =
|
||||||
forkModel?.limit?.output && forkModel.limit.output > 0 ? forkModel.limit.output : DEFAULT_MODEL_OUTPUT_LIMIT
|
forkModel?.limit?.output && forkModel.limit.output > 0 ? forkModel.limit.output : DEFAULT_MODEL_OUTPUT_LIMIT
|
||||||
const forkContextAvailable = forkContextWindow > 0 ? forkContextWindow : null
|
const forkContextAvailable = forkInputLimit > 0 ? forkInputLimit : forkContextWindow > 0 ? forkContextWindow : null
|
||||||
|
|
||||||
setSessionInfoByInstance((prev) => {
|
setSessionInfoByInstance((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
@@ -524,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 || "",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
} from "./instances"
|
} from "./instances"
|
||||||
import { showAlertDialog } from "./alerts"
|
import { showAlertDialog } from "./alerts"
|
||||||
import { createClientSession, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
import { createClientSession, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
||||||
import { sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
import { ensureSessionParentExpanded, sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
||||||
import { normalizeMessagePart } from "./message-v2/normalizers"
|
import { normalizeMessagePart } from "./message-v2/normalizers"
|
||||||
import { updateSessionInfo } from "./message-v2/session-info"
|
import { updateSessionInfo } from "./message-v2/session-info"
|
||||||
import { tGlobal } from "../lib/i18n"
|
import { tGlobal } from "../lib/i18n"
|
||||||
@@ -108,6 +108,8 @@ interface TuiToastEvent {
|
|||||||
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
||||||
|
|
||||||
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
|
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
|
||||||
|
let parentToExpand: string | null = null
|
||||||
|
|
||||||
withSession(instanceId, sessionId, (session) => {
|
withSession(instanceId, sessionId, (session) => {
|
||||||
const current = session.status ?? "idle"
|
const current = session.status ?? "idle"
|
||||||
if (current === status) return false
|
if (current === status) return false
|
||||||
@@ -117,7 +119,17 @@ function applySessionStatus(instanceId: string, sessionId: string, status: Sessi
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.status = status
|
session.status = status
|
||||||
|
|
||||||
|
// Auto-expand the parent thread when a child session starts working.
|
||||||
|
// Users can still collapse it; we only expand on the transition.
|
||||||
|
if (session.parentId && status === "working" && current !== "working") {
|
||||||
|
parentToExpand = session.parentId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (parentToExpand) {
|
||||||
|
ensureSessionParentExpanded(instanceId, parentToExpand)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSessionInfo(instanceId: string, sessionId: string, directory?: string): Promise<Session | null> {
|
async function fetchSessionInfo(instanceId: string, sessionId: string, directory?: string): Promise<Session | null> {
|
||||||
@@ -158,6 +170,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
|||||||
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
|
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
|
||||||
|
|
||||||
let updatedInstanceSessions: Map<string, Session> | undefined
|
let updatedInstanceSessions: Map<string, Session> | undefined
|
||||||
|
let shouldExpandParent: string | null = null
|
||||||
|
|
||||||
setSessions((prev) => {
|
setSessions((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
@@ -174,11 +187,19 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
|||||||
instanceSessions.set(sessionId, merged)
|
instanceSessions.set(sessionId, merged)
|
||||||
next.set(instanceId, instanceSessions)
|
next.set(instanceId, instanceSessions)
|
||||||
updatedInstanceSessions = instanceSessions
|
updatedInstanceSessions = instanceSessions
|
||||||
|
|
||||||
|
if (merged.parentId && merged.status === "working" && (existing?.status ?? "idle") !== "working") {
|
||||||
|
shouldExpandParent = merged.parentId
|
||||||
|
}
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)
|
syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)
|
||||||
|
|
||||||
|
if (shouldExpandParent) {
|
||||||
|
ensureSessionParentExpanded(instanceId, shouldExpandParent)
|
||||||
|
}
|
||||||
|
|
||||||
return fetched
|
return fetched
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to fetch session info", error)
|
log.error("Failed to fetch session info", error)
|
||||||
|
|||||||
@@ -347,10 +347,23 @@ function clearActiveParentSession(instanceId: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setSessionStatus(instanceId: string, sessionId: string, status: SessionStatus): void {
|
function setSessionStatus(instanceId: string, sessionId: string, status: SessionStatus): void {
|
||||||
|
let parentToExpand: string | null = null
|
||||||
|
|
||||||
withSession(instanceId, sessionId, (session) => {
|
withSession(instanceId, sessionId, (session) => {
|
||||||
if (session.status === status) return false
|
if (session.status === status) return false
|
||||||
|
const previous = session.status
|
||||||
session.status = status
|
session.status = status
|
||||||
|
|
||||||
|
// If a child session starts working, auto-expand its parent thread once.
|
||||||
|
// Users can still collapse it afterwards; we only expand on the transition.
|
||||||
|
if (session.parentId && status === "working" && previous !== "working") {
|
||||||
|
parentToExpand = session.parentId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (parentToExpand) {
|
||||||
|
ensureSessionParentExpanded(instanceId, parentToExpand)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveParentSession(instanceId: string): Session | null {
|
function getActiveParentSession(instanceId: string): Session | null {
|
||||||
|
|||||||
@@ -329,12 +329,38 @@ function buildWorktreeProxyPath(instanceId: string, slug: string): string {
|
|||||||
return `/workspaces/${encodeURIComponent(instanceId)}/worktrees/${encodeURIComponent(normalizedSlug)}/instance`
|
return `/workspaces/${encodeURIComponent(instanceId)}/worktrees/${encodeURIComponent(normalizedSlug)}/instance`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeBase64UrlUtf8(input: string): string {
|
||||||
|
const bytes = new TextEncoder().encode(input)
|
||||||
|
// Convert bytes -> base64 (btoa expects a binary string)
|
||||||
|
let binary = ""
|
||||||
|
const chunkSize = 0x8000
|
||||||
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||||
|
const chunk = bytes.subarray(i, i + chunkSize)
|
||||||
|
binary += String.fromCharCode(...chunk)
|
||||||
|
}
|
||||||
|
const base64 = btoa(binary)
|
||||||
|
// base64 -> base64url (strip padding)
|
||||||
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorktreeProxyPathWithDirectoryOverride(instanceId: string, slug: string, directory: string): string {
|
||||||
|
const base = buildWorktreeProxyPath(instanceId, slug)
|
||||||
|
const encoded = encodeBase64UrlUtf8(directory)
|
||||||
|
return `${base}/__dir/${encoded}`
|
||||||
|
}
|
||||||
|
|
||||||
function getOrCreateWorktreeClient(instanceId: string, slug: string): OpencodeClient {
|
function getOrCreateWorktreeClient(instanceId: string, slug: string): OpencodeClient {
|
||||||
const normalized = normalizeWorktreeSlug(instanceId, slug || "root")
|
const normalized = normalizeWorktreeSlug(instanceId, slug || "root")
|
||||||
const proxyPath = buildWorktreeProxyPath(instanceId, normalized)
|
const proxyPath = buildWorktreeProxyPath(instanceId, normalized)
|
||||||
return sdkManager.createClient(instanceId, proxyPath, normalized)
|
return sdkManager.createClient(instanceId, proxyPath, normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOrCreateWorktreeClientWithDirectoryOverride(instanceId: string, slug: string, directory: string): OpencodeClient {
|
||||||
|
const normalized = normalizeWorktreeSlug(instanceId, slug || "root")
|
||||||
|
const proxyPath = buildWorktreeProxyPathWithDirectoryOverride(instanceId, normalized, directory)
|
||||||
|
return sdkManager.createClient(instanceId, proxyPath, normalized)
|
||||||
|
}
|
||||||
|
|
||||||
function getRootClient(instanceId: string): OpencodeClient {
|
function getRootClient(instanceId: string): OpencodeClient {
|
||||||
return getOrCreateWorktreeClient(instanceId, "root")
|
return getOrCreateWorktreeClient(instanceId, "root")
|
||||||
}
|
}
|
||||||
@@ -359,7 +385,9 @@ export {
|
|||||||
removeParentSessionMapping,
|
removeParentSessionMapping,
|
||||||
getWorktreeSlugForDirectory,
|
getWorktreeSlugForDirectory,
|
||||||
buildWorktreeProxyPath,
|
buildWorktreeProxyPath,
|
||||||
|
buildWorktreeProxyPathWithDirectoryOverride,
|
||||||
getOrCreateWorktreeClient,
|
getOrCreateWorktreeClient,
|
||||||
|
getOrCreateWorktreeClientWithDirectoryOverride,
|
||||||
getRootClient,
|
getRootClient,
|
||||||
createWorktree,
|
createWorktree,
|
||||||
deleteWorktree,
|
deleteWorktree,
|
||||||
|
|||||||
@@ -54,3 +54,28 @@ button.button-tertiary:hover:not(:disabled) {
|
|||||||
button.button-tertiary:focus-visible {
|
button.button-tertiary:focus-visible {
|
||||||
box-shadow: 0 0 0 2px var(--focus-ring-offset), 0 0 0 4px var(--focus-ring-color);
|
box-shadow: 0 0 0 2px var(--focus-ring-offset), 0 0 0 4px var(--focus-ring-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-danger,
|
||||||
|
button.button-danger {
|
||||||
|
@apply px-6 py-3 text-base rounded-lg;
|
||||||
|
background-color: var(--button-danger-bg);
|
||||||
|
color: var(--button-danger-text);
|
||||||
|
border-color: var(--button-danger-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-danger:hover:not(:disabled),
|
||||||
|
button.button-danger:hover:not(:disabled) {
|
||||||
|
background-color: var(--button-danger-hover-bg);
|
||||||
|
border-color: var(--button-danger-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-danger:focus-visible,
|
||||||
|
button.button-danger:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px var(--focus-ring-offset), 0 0 0 4px var(--focus-ring-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smaller sizing variant for destructive actions in tight spaces. */
|
||||||
|
.button-danger.button-small,
|
||||||
|
button.button-danger.button-small {
|
||||||
|
@apply px-4 py-2 text-sm;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
line-height: var(--line-height-normal);
|
line-height: var(--line-height-normal);
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
/* Message containers may use `whitespace-pre-wrap` for plain text.
|
||||||
|
Markdown should always match assistant rendering (normal whitespace). */
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body p,
|
.markdown-body p,
|
||||||
@@ -28,7 +31,7 @@
|
|||||||
.markdown-body h5,
|
.markdown-body h5,
|
||||||
.markdown-body h6 {
|
.markdown-body h6 {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: inherit;
|
color: var(--markdown-heading-color, inherit);
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
margin-top: 0.9em;
|
margin-top: 0.9em;
|
||||||
@@ -71,7 +74,7 @@
|
|||||||
|
|
||||||
.markdown-body strong {
|
.markdown-body strong {
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
color: var(--message-assistant-border);
|
color: var(--markdown-accent, var(--message-assistant-border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body em {
|
.markdown-body em {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
/* Message item base styles */
|
/* Message item base styles */
|
||||||
.message-item-base {
|
.message-item-base {
|
||||||
@apply flex flex-col gap-2 p-3 w-full;
|
@apply flex flex-col gap-2 p-3 w-full;
|
||||||
|
|
||||||
|
/* Markdown rendering uses these to theme emphasis + headings per message role. */
|
||||||
|
--markdown-accent: var(--message-user-border);
|
||||||
|
--markdown-heading-color: var(--message-user-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item-header {
|
.message-item-header {
|
||||||
@@ -71,6 +75,9 @@
|
|||||||
padding: 0.6rem 0.65rem;
|
padding: 0.6rem 0.65rem;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
--markdown-accent: var(--message-assistant-border);
|
||||||
|
--markdown-heading-color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item-base:not(.assistant-message) {
|
.message-item-base:not(.assistant-message) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,7 +295,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input {
|
.prompt-input {
|
||||||
padding-bottom: 1.5rem;
|
/* Prevent iOS Safari input zoom + keep input compact. */
|
||||||
|
font-size: 16px;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1279px) {
|
||||||
|
:root {
|
||||||
|
--prompt-input-compact-height: 104px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input-wrapper {
|
||||||
|
min-height: var(--prompt-input-compact-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input-field-container {
|
||||||
|
min-height: var(--prompt-input-compact-height);
|
||||||
|
height: var(--prompt-input-compact-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input-field {
|
||||||
|
height: var(--prompt-input-compact-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input-field-container.is-expanded,
|
||||||
|
.prompt-input-field.is-expanded {
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,9 +333,9 @@
|
|||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.prompt-input {
|
.prompt-input {
|
||||||
min-height: 64px;
|
min-height: 0;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
padding-bottom: 2.25rem;
|
padding-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input-wrapper {
|
.prompt-input-wrapper {
|
||||||
|
|||||||
@@ -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). */
|
||||||
|
|||||||
@@ -282,7 +282,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-toolbar {
|
.file-viewer-toolbar {
|
||||||
@apply ml-auto flex items-center gap-1;
|
@apply flex items-center gap-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-toolbar-button {
|
.file-viewer-toolbar-button {
|
||||||
@@ -291,6 +291,22 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-viewer-toolbar-icon-button {
|
||||||
|
@apply inline-flex items-center justify-center shrink-0 w-7 h-7 border border-base transition-colors;
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-viewer-toolbar-icon-button:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-viewer-toolbar-icon-button.active {
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.file-viewer-toolbar-button:hover {
|
.file-viewer-toolbar-button:hover {
|
||||||
background-color: var(--surface-hover);
|
background-color: var(--surface-hover);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -396,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;
|
||||||
}
|
}
|
||||||
@@ -406,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);
|
||||||
@@ -425,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;
|
||||||
|
|||||||
@@ -125,6 +125,18 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-fullscreen-exit-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(env(safe-area-inset-top, 0px) + 12px);
|
||||||
|
right: calc(env(safe-area-inset-right, 0px) + 12px);
|
||||||
|
z-index: 1250;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-fullscreen-exit-button {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.session-resize-handle {
|
.session-resize-handle {
|
||||||
@apply absolute top-0 w-1 h-full cursor-col-resize bg-transparent transition-colors;
|
@apply absolute top-0 w-1 h-full cursor-col-resize bg-transparent transition-colors;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
button.button-primary,
|
button.button-primary,
|
||||||
.button-secondary,
|
.button-secondary,
|
||||||
button.button-secondary,
|
button.button-secondary,
|
||||||
|
.button-danger,
|
||||||
|
button.button-danger,
|
||||||
.button-tertiary,
|
.button-tertiary,
|
||||||
button.button-tertiary) {
|
button.button-tertiary) {
|
||||||
@apply inline-flex items-center justify-center gap-2 font-medium transition-colors rounded-md;
|
@apply inline-flex items-center justify-center gap-2 font-medium transition-colors rounded-md;
|
||||||
@@ -74,6 +76,8 @@
|
|||||||
button.button-primary,
|
button.button-primary,
|
||||||
.button-secondary,
|
.button-secondary,
|
||||||
button.button-secondary,
|
button.button-secondary,
|
||||||
|
.button-danger,
|
||||||
|
button.button-danger,
|
||||||
.button-tertiary,
|
.button-tertiary,
|
||||||
button.button-tertiary):focus-visible {
|
button.button-tertiary):focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -84,6 +88,8 @@
|
|||||||
button.button-primary,
|
button.button-primary,
|
||||||
.button-secondary,
|
.button-secondary,
|
||||||
button.button-secondary,
|
button.button-secondary,
|
||||||
|
.button-danger,
|
||||||
|
button.button-danger,
|
||||||
.button-tertiary,
|
.button-tertiary,
|
||||||
button.button-tertiary):disabled {
|
button.button-tertiary):disabled {
|
||||||
@apply cursor-not-allowed opacity-50;
|
@apply cursor-not-allowed opacity-50;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -90,6 +91,7 @@ export interface Model {
|
|||||||
variantKeys?: string[]
|
variantKeys?: string[]
|
||||||
limit?: {
|
limit?: {
|
||||||
context?: number
|
context?: number
|
||||||
|
input?: number
|
||||||
output?: number
|
output?: number
|
||||||
}
|
}
|
||||||
cost?: {
|
cost?: {
|
||||||
|
|||||||
BIN
temp/opencode-ai-sdk-1.2.6.tgz
Normal file
BIN
temp/opencode-ai-sdk-1.2.6.tgz
Normal file
Binary file not shown.
51
temp/package/package.json
Normal file
51
temp/package/package.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"name": "@opencode-ai/sdk",
|
||||||
|
"version": "1.2.6",
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsgo --noEmit",
|
||||||
|
"build": "./script/build.ts"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
},
|
||||||
|
"./client": {
|
||||||
|
"import": "./dist/client.js",
|
||||||
|
"types": "./dist/client.d.ts"
|
||||||
|
},
|
||||||
|
"./server": {
|
||||||
|
"import": "./dist/server.js",
|
||||||
|
"types": "./dist/server.d.ts"
|
||||||
|
},
|
||||||
|
"./v2": {
|
||||||
|
"import": "./dist/v2/index.js",
|
||||||
|
"types": "./dist/v2/index.d.ts"
|
||||||
|
},
|
||||||
|
"./v2/client": {
|
||||||
|
"import": "./dist/v2/client.js",
|
||||||
|
"types": "./dist/v2/client.d.ts"
|
||||||
|
},
|
||||||
|
"./v2/server": {
|
||||||
|
"import": "./dist/v2/server.js",
|
||||||
|
"types": "./dist/v2/server.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
|
"@tsconfig/node22": "22.0.2",
|
||||||
|
"@types/node": "22.13.9",
|
||||||
|
"typescript": "5.8.2",
|
||||||
|
"@typescript/native-preview": "7.0.0-dev.20251207.1"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"publishConfig": {
|
||||||
|
"directory": "dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user