Compare commits
63 Commits
v0.11.2-de
...
v0.11.5-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
482313f662 | ||
|
|
9a4d378238 | ||
|
|
5d5fbfb5f2 | ||
|
|
d147ad49ff | ||
|
|
9b435e3621 | ||
|
|
ab9e188b02 | ||
|
|
2991de528a | ||
|
|
f1bd681618 | ||
|
|
b91dbb1a60 | ||
|
|
688b127c6d | ||
|
|
0f9c99e3bd | ||
|
|
1122070b9c | ||
|
|
57b81f00f8 | ||
|
|
362105fe78 | ||
|
|
5834d2df1b | ||
|
|
ef4c8ef425 | ||
|
|
5f755a7e1c | ||
|
|
8607fab5b5 | ||
|
|
0368fe8248 | ||
|
|
b970281fa7 | ||
|
|
8e5a7fc213 | ||
|
|
15f362e8b5 | ||
|
|
7bbd0a1787 | ||
|
|
f8aae56728 | ||
|
|
027d7fc97d | ||
|
|
e90aef4b3c | ||
|
|
e4e89008b2 | ||
|
|
90baefbb7e | ||
|
|
1c138f4489 | ||
|
|
d36e568ed0 | ||
|
|
d6462ef524 | ||
|
|
a06884ebce | ||
|
|
62bd88f6a4 | ||
|
|
6479561779 | ||
|
|
635237c258 | ||
|
|
33f0aa5714 | ||
|
|
7ca6285d58 | ||
|
|
14c60fef6c | ||
|
|
336de6a19e | ||
|
|
377c8e2249 | ||
|
|
697dea21f8 | ||
|
|
34d3f803d5 | ||
|
|
f824a063a5 | ||
|
|
96fe1b86dd | ||
|
|
5fabf286e8 | ||
|
|
e8947d61b1 | ||
|
|
1ccd14eae8 | ||
|
|
b162764ccb | ||
|
|
2124e540aa | ||
|
|
b5790998b7 | ||
|
|
9800afb785 | ||
|
|
3b73d9d5b9 | ||
|
|
f7ac30afe3 | ||
|
|
ce370d5100 | ||
|
|
c639e535b5 | ||
|
|
e84adebe61 | ||
|
|
d45a1ff078 | ||
|
|
b4121696bb | ||
|
|
f75c942162 | ||
|
|
127a1f628d | ||
|
|
859312ba3b | ||
|
|
4eaa711f01 | ||
|
|
c8ff858565 |
145
.github/workflows/build-and-upload.yml
vendored
145
.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
|
||||||
@@ -54,7 +61,21 @@ jobs:
|
|||||||
|
|
||||||
- name: Set workspace versions
|
- name: Set workspace versions
|
||||||
if: ${{ inputs.set_versions && inputs.version != '' }}
|
if: ${{ inputs.set_versions && inputs.version != '' }}
|
||||||
run: npm version ${VERSION} --workspaces --include-workspace-root --no-git-tag-version --allow-same-version
|
shell: bash
|
||||||
|
env:
|
||||||
|
NPM_CONFIG_FETCH_RETRIES: 5
|
||||||
|
NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: 20000
|
||||||
|
NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: 120000
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if npm version "${VERSION}" --workspaces --include-workspace-root --no-git-tag-version --allow-same-version; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "npm version failed (attempt $attempt/3); retrying..." >&2
|
||||||
|
sleep $((attempt * 10))
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --workspaces --include=optional
|
run: npm ci --workspaces --include=optional
|
||||||
@@ -65,6 +86,112 @@ 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: Ad-hoc sign Electron macOS app bundles (seal resources)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
release_root="packages/electron-app/release"
|
||||||
|
apps=()
|
||||||
|
while IFS= read -r -d '' app; do
|
||||||
|
apps+=("$app")
|
||||||
|
done < <(find "$release_root" -type d -name 'CodeNomad.app' -print0)
|
||||||
|
|
||||||
|
if [ "${#apps[@]}" -eq 0 ]; then
|
||||||
|
echo "No CodeNomad.app found under $release_root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GitHub macOS runners typically have no signing identity. Without any signature,
|
||||||
|
# the shipped .app can fail Gatekeeper with:
|
||||||
|
# code has no resources but signature indicates they must be present
|
||||||
|
# Ad-hoc signing seals bundle resources and makes the signature internally consistent.
|
||||||
|
if security find-identity -p codesigning -v | grep -q "0 valid identities found"; then
|
||||||
|
echo "No valid macOS codesigning identity found; applying ad-hoc signature"
|
||||||
|
for app in "${apps[@]}"; do
|
||||||
|
echo "codesign (adhoc): $app"
|
||||||
|
codesign --force --deep --sign - "$app"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$app"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "macOS codesigning identity present; skipping ad-hoc signing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
# macOS GitHub runners ship /bin/bash 3.2 which doesn't support `shopt -s globstar`.
|
||||||
|
# Use find to locate built app bundles instead of ** globs.
|
||||||
|
apps=()
|
||||||
|
while IFS= read -r -d '' app; do
|
||||||
|
apps+=("$app")
|
||||||
|
done < <(find "$release_root" -type d -name 'CodeNomad.app' -print0)
|
||||||
|
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 +212,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 +253,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 +295,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 +370,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 +445,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 +525,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 +629,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 +728,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 }}
|
||||||
|
|||||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
@@ -11985,7 +11985,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
@@ -12021,7 +12021,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
@@ -12062,7 +12062,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
@@ -12070,7 +12070,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
@@ -12092,7 +12092,8 @@
|
|||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
"tauri-plugin-keepawake-api": "^0.1.0"
|
"tauri-plugin-keepawake-api": "^0.1.0",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vite-pwa/assets-generator": "^1.0.2",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"minServerVersion": "0.11.1",
|
"minServerVersion": "0.11.4",
|
||||||
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,7 +431,9 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
|
|
||||||
if (options.dev) {
|
if (options.dev) {
|
||||||
const devServer = process.env.VITE_DEV_SERVER_URL || process.env.ELECTRON_RENDERER_URL || "http://localhost:3000"
|
const devServer = process.env.VITE_DEV_SERVER_URL || process.env.ELECTRON_RENDERER_URL || "http://localhost:3000"
|
||||||
args.push("--ui-dev-server", devServer, "--log-level", "debug")
|
const rawLogLevel = (process.env.CLI_LOG_LEVEL ?? "info").trim()
|
||||||
|
const logLevel = rawLogLevel.length > 0 ? rawLogLevel.toLowerCase() : "info"
|
||||||
|
args.push("--ui-dev-server", devServer, "--log-level", logLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -15,7 +15,10 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/NeuralNomadsAI/CodeNomad",
|
"homepage": "https://github.com/NeuralNomadsAI/CodeNomad",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "electron-vite dev",
|
"dev": "npm run dev:info",
|
||||||
|
"dev:info": "cross-env CLI_LOG_LEVEL=info electron-vite dev",
|
||||||
|
"dev:debug": "cross-env CLI_LOG_LEVEL=debug electron-vite dev",
|
||||||
|
"dev:trace": "cross-env CLI_LOG_LEVEL=trace electron-vite dev",
|
||||||
"dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts",
|
"dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
@@ -42,6 +45,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
"app-builder-bin": "^4.2.0",
|
"app-builder-bin": "^4.2.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"electron": "39.0.0",
|
"electron": "39.0.0",
|
||||||
"electron-builder": "^24.0.0",
|
"electron-builder": "^24.0.0",
|
||||||
"electron-vite": "4.0.1",
|
"electron-vite": "4.0.1",
|
||||||
|
|||||||
@@ -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.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,21 @@
|
|||||||
## Features & Capabilities
|
## Features & Capabilities
|
||||||
|
|
||||||
### 🌍 Deployment Freedom
|
### 🌍 Deployment Freedom
|
||||||
|
|
||||||
- **Remote Access**: Host CodeNomad on a powerful workstation and access it from your lightweight laptop.
|
- **Remote Access**: Host CodeNomad on a powerful workstation and access it from your lightweight laptop.
|
||||||
- **Code Anywhere**: Tunnel in via VPN or SSH to code securely from coffee shops or while traveling.
|
- **Code Anywhere**: Tunnel in via VPN or SSH to code securely from coffee shops or while traveling.
|
||||||
- **Multi-Device**: The responsive web client works on tablets and iPads, turning any screen into a dev terminal.
|
- **Multi-Device**: The responsive web client works on tablets and iPads, turning any screen into a dev terminal.
|
||||||
- **Always-On**: Run as a background service so your sessions are always ready when you connect.
|
- **Always-On**: Run as a background service so your sessions are always ready when you connect.
|
||||||
|
|
||||||
### ⚡️ Workspace Power
|
### ⚡️ Workspace Power
|
||||||
|
|
||||||
- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
|
- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
|
||||||
- **Long-Context Native**: Scroll through massive transcripts without hitches.
|
- **Long-Context Native**: Scroll through massive transcripts without hitches.
|
||||||
- **Deep Task Awareness**: Monitor background tasks and child sessions without losing your flow.
|
- **Deep Task Awareness**: Monitor background tasks and child sessions without losing your flow.
|
||||||
- **Command Palette**: A single, global palette to jump tabs, launch tools, and fire shortcuts.
|
- **Command Palette**: A single, global palette to jump tabs, launch tools, and fire shortcuts.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **OpenCode**: `opencode` must be installed and configured on your system.
|
- **OpenCode**: `opencode` must be installed and configured on your system.
|
||||||
- Node.js 18+ and npm (for running or building from source).
|
- Node.js 18+ and npm (for running or building from source).
|
||||||
- A workspace folder on disk you want to serve.
|
- A workspace folder on disk you want to serve.
|
||||||
@@ -25,6 +28,7 @@
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Run via npx (Recommended)
|
### Run via npx (Recommended)
|
||||||
|
|
||||||
You can run CodeNomad directly without installing it:
|
You can run CodeNomad directly without installing it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -43,6 +47,7 @@ On startup, CodeNomad prints two URLs:
|
|||||||
- `Remote Connection URL : ...` (used by browsers/other machines when remote access is enabled)
|
- `Remote Connection URL : ...` (used by browsers/other machines when remote access is enabled)
|
||||||
|
|
||||||
### Install Globally
|
### Install Globally
|
||||||
|
|
||||||
Or install it globally to use the `codenomad` command:
|
Or install it globally to use the `codenomad` command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -51,6 +56,7 @@ codenomad --launch
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Install Locally (per-project)
|
### Install Locally (per-project)
|
||||||
|
|
||||||
If you prefer to install CodeNomad into a project and run the local binary:
|
If you prefer to install CodeNomad into a project and run the local binary:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -61,6 +67,7 @@ npx codenomad --launch
|
|||||||
(`npx codenomad ...` will use `./node_modules/.bin/codenomad` when present.)
|
(`npx codenomad ...` will use `./node_modules/.bin/codenomad` when present.)
|
||||||
|
|
||||||
### Common Flags
|
### Common Flags
|
||||||
|
|
||||||
You can configure the server using flags or environment variables:
|
You can configure the server using flags or environment variables:
|
||||||
|
|
||||||
| Flag | Env Variable | Description |
|
| Flag | Env Variable | Description |
|
||||||
@@ -74,7 +81,7 @@ You can configure the server using flags or environment variables:
|
|||||||
| `--tls-ca <path>` | `CLI_TLS_CA` | Optional CA chain/bundle (PEM) |
|
| `--tls-ca <path>` | `CLI_TLS_CA` | Optional CA chain/bundle (PEM) |
|
||||||
| `--tlsSANs <list>` | `CLI_TLS_SANS` | Additional TLS SANs (comma-separated) |
|
| `--tlsSANs <list>` | `CLI_TLS_SANS` | Additional TLS SANs (comma-separated) |
|
||||||
| `--host <addr>` | `CLI_HOST` | Interface to bind (default 127.0.0.1) |
|
| `--host <addr>` | `CLI_HOST` | Interface to bind (default 127.0.0.1) |
|
||||||
| `--workspace-root <path>` | `CLI_WORKSPACE_ROOT` | Default root for new workspaces |
|
| `--workspace-root <path>` | `CLI_WORKSPACE_ROOT` | Restricts the root path where new workspaces can be opened. Git worktrees are created in `.codenomad/worktrees` inside the project folder. |
|
||||||
| `--unrestricted-root` | `CLI_UNRESTRICTED_ROOT` | Allow full-filesystem browsing |
|
| `--unrestricted-root` | `CLI_UNRESTRICTED_ROOT` | Allow full-filesystem browsing |
|
||||||
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
||||||
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
||||||
@@ -87,10 +94,11 @@ You can configure the server using flags or environment variables:
|
|||||||
| `--ui-dir <path>` | `CLI_UI_DIR` | Directory containing the built UI bundle |
|
| `--ui-dir <path>` | `CLI_UI_DIR` | Directory containing the built UI bundle |
|
||||||
| `--ui-dev-server <url>` | `CLI_UI_DEV_SERVER` | Proxy UI requests to a running dev server (requires `--https=false --http=true`) |
|
| `--ui-dev-server <url>` | `CLI_UI_DEV_SERVER` | Proxy UI requests to a running dev server (requires `--https=false --http=true`) |
|
||||||
| `--ui-no-update` | `CLI_UI_NO_UPDATE` | Disable remote UI updates |
|
| `--ui-no-update` | `CLI_UI_NO_UPDATE` | Disable remote UI updates |
|
||||||
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (true|false) |
|
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (`true` |
|
||||||
| `--ui-manifest-url <url>` | `CLI_UI_MANIFEST_URL` | Remote UI manifest URL |
|
| `--ui-manifest-url <url>` | `CLI_UI_MANIFEST_URL` | Remote UI manifest URL |
|
||||||
|
|
||||||
### Dev Releases (Advanced)
|
### Dev Releases (Advanced)
|
||||||
|
|
||||||
If you want the latest bleeding-edge builds (published as GitHub pre-releases), use the dev package:
|
If you want the latest bleeding-edge builds (published as GitHub pre-releases), use the dev package:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -141,12 +149,14 @@ codenomad --tlsSANs "localhost,127.0.0.1,my-hostname,192.168.1.10"
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
- Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
|
- Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
|
||||||
- `--dangerously-skip-auth` / `CODENOMAD_SKIP_AUTH=true` disables the login prompt and treats all requests as authenticated.
|
- `--dangerously-skip-auth` / `CODENOMAD_SKIP_AUTH=true` disables the login prompt and treats all requests as authenticated.
|
||||||
Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.).
|
Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.).
|
||||||
If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API.
|
If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API.
|
||||||
|
|
||||||
### Progressive Web App (PWA)
|
### Progressive Web App (PWA)
|
||||||
|
|
||||||
When running as a server CodeNomad can also be installed as a PWA from any supported browser, giving you a native app experience just like the Electron installation but executing on the remote server instead.
|
When running as a server CodeNomad can also be installed as a PWA from any supported browser, giving you a native app experience just like the Electron installation but executing on the remote server instead.
|
||||||
|
|
||||||
1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.).
|
1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.).
|
||||||
@@ -158,5 +168,6 @@ When running as a server CodeNomad can also be installed as a PWA from any suppo
|
|||||||
> If you host CodeNomad on a remote machine, use HTTPS. Self-signed certificates generally won't work unless they are explicitly trusted by the device/browser (e.g., via a custom CA).
|
> If you host CodeNomad on a remote machine, use HTTPS. Self-signed certificates generally won't work unless they are explicitly trusted by the device/browser (e.g., via a custom CA).
|
||||||
|
|
||||||
### Data Storage
|
### Data Storage
|
||||||
|
|
||||||
- **Config**: `~/.config/codenomad/config.json`
|
- **Config**: `~/.config/codenomad/config.json`
|
||||||
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
||||||
|
|||||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"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 "/"
|
||||||
|
|||||||
@@ -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.11.2",
|
"version": "0.11.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.11.2",
|
"version": "0.11.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -30,7 +30,8 @@
|
|||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
"tauri-plugin-keepawake-api": "^0.1.0"
|
"tauri-plugin-keepawake-api": "^0.1.0",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vite-pwa/assets-generator": "^1.0.2",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { useTheme } from "./lib/theme"
|
|||||||
import { useCommands } from "./lib/hooks/use-commands"
|
import { useCommands } from "./lib/hooks/use-commands"
|
||||||
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
|
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
|
||||||
import { getLogger } from "./lib/logger"
|
import { getLogger } from "./lib/logger"
|
||||||
|
import { launchError, showLaunchError, clearLaunchError } from "./stores/launch-errors"
|
||||||
|
import { formatLaunchErrorMessage, isMissingBinaryMessage } from "./lib/launch-errors"
|
||||||
import { initReleaseNotifications } from "./stores/releases"
|
import { initReleaseNotifications } from "./stores/releases"
|
||||||
import { runtimeEnv } from "./lib/runtime-env"
|
import { runtimeEnv } from "./lib/runtime-env"
|
||||||
import { useI18n } from "./lib/i18n"
|
import { useI18n } from "./lib/i18n"
|
||||||
@@ -72,14 +74,9 @@ const App: Component = () => {
|
|||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
setThinkingBlocksExpansion,
|
setThinkingBlocksExpansion,
|
||||||
|
setToolInputsVisibility,
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
||||||
interface LaunchErrorState {
|
|
||||||
message: string
|
|
||||||
binaryPath: string
|
|
||||||
missingBinary: boolean
|
|
||||||
}
|
|
||||||
const [launchError, setLaunchError] = createSignal<LaunchErrorState | null>(null)
|
|
||||||
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false)
|
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false)
|
||||||
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
||||||
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
|
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
|
||||||
@@ -244,35 +241,6 @@ const App: Component = () => {
|
|||||||
|
|
||||||
const launchErrorMessage = () => launchError()?.message ?? ""
|
const launchErrorMessage = () => launchError()?.message ?? ""
|
||||||
|
|
||||||
const formatLaunchErrorMessage = (error: unknown): string => {
|
|
||||||
if (!error) {
|
|
||||||
return t("app.launchError.fallbackMessage")
|
|
||||||
}
|
|
||||||
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error)
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw)
|
|
||||||
if (parsed && typeof parsed.error === "string") {
|
|
||||||
return parsed.error
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore JSON parse errors
|
|
||||||
}
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMissingBinaryMessage = (message: string): boolean => {
|
|
||||||
const normalized = message.toLowerCase()
|
|
||||||
return (
|
|
||||||
normalized.includes("opencode binary not found") ||
|
|
||||||
normalized.includes("binary not found") ||
|
|
||||||
normalized.includes("no such file or directory") ||
|
|
||||||
normalized.includes("binary is not executable") ||
|
|
||||||
normalized.includes("enoent")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearLaunchError = () => setLaunchError(null)
|
|
||||||
|
|
||||||
async function handleSelectFolder(folderPath: string, binaryPath?: string) {
|
async function handleSelectFolder(folderPath: string, binaryPath?: string) {
|
||||||
if (!folderPath) {
|
if (!folderPath) {
|
||||||
return
|
return
|
||||||
@@ -291,13 +259,9 @@ const App: Component = () => {
|
|||||||
port: instances().get(instanceId)?.port,
|
port: instances().get(instanceId)?.port,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = formatLaunchErrorMessage(error)
|
const message = formatLaunchErrorMessage(error, t("app.launchError.fallbackMessage"))
|
||||||
const missingBinary = isMissingBinaryMessage(message)
|
const missingBinary = isMissingBinaryMessage(message)
|
||||||
setLaunchError({
|
showLaunchError({ source: "create", message, binaryPath: selectedBinary, missingBinary })
|
||||||
message,
|
|
||||||
binaryPath: selectedBinary,
|
|
||||||
missingBinary,
|
|
||||||
})
|
|
||||||
log.error("Failed to create instance", error)
|
log.error("Failed to create instance", error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSelectingFolder(false)
|
setIsSelectingFolder(false)
|
||||||
@@ -402,6 +366,7 @@ const App: Component = () => {
|
|||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
setThinkingBlocksExpansion,
|
setThinkingBlocksExpansion,
|
||||||
|
setToolInputsVisibility,
|
||||||
handleNewInstanceRequest,
|
handleNewInstanceRequest,
|
||||||
handleCloseInstance,
|
handleCloseInstance,
|
||||||
handleNewSession,
|
handleNewSession,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
|||||||
// Keep enough gutter space so unified diffs don't overlap `+`/`-` markers.
|
// Keep enough gutter space so unified diffs don't overlap `+`/`-` markers.
|
||||||
lineNumbersMinChars: 4,
|
lineNumbersMinChars: 4,
|
||||||
lineDecorationsWidth: 12,
|
lineDecorationsWidth: 12,
|
||||||
|
// Use legacy diff algorithm for better performance with large files
|
||||||
|
// See: https://github.com/microsoft/vscode/issues/184037
|
||||||
|
diffAlgorithm: "legacy",
|
||||||
|
// Limit computation time to avoid freezing on large files
|
||||||
|
maxComputationTime: 10000,
|
||||||
})
|
})
|
||||||
|
|
||||||
setReady(true)
|
setReady(true)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const desktopQuery = useMediaQuery("(min-width: 1280px)")
|
const desktopQuery = useMediaQuery("(min-width: 1280px)")
|
||||||
|
|
||||||
const tabletQuery = useMediaQuery("(min-width: 768px)")
|
const tabletQuery = useMediaQuery("(min-width: 768px)")
|
||||||
|
const compactHeaderQuery = useMediaQuery("(max-width: 1024px)")
|
||||||
|
|
||||||
const layoutMode = createMemo<LayoutMode>(() => {
|
const layoutMode = createMemo<LayoutMode>(() => {
|
||||||
if (desktopQuery()) return "desktop"
|
if (desktopQuery()) return "desktop"
|
||||||
@@ -123,6 +124,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
||||||
|
const compactHeaderLayout = createMemo(() => isPhoneLayout() || compactHeaderQuery())
|
||||||
const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout())
|
const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout())
|
||||||
const compactPromptLayout = createMemo(() => layoutMode() !== "desktop")
|
const compactPromptLayout = createMemo(() => layoutMode() !== "desktop")
|
||||||
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||||
@@ -596,7 +598,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
||||||
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
||||||
<Show
|
<Show
|
||||||
when={!isPhoneLayout()}
|
when={!compactHeaderLayout()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="flex flex-col w-full gap-1.5">
|
<div class="flex flex-col w-full gap-1.5">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
||||||
@@ -625,7 +627,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" }}
|
||||||
@@ -634,8 +636,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
<span class="connection-status-shortcut-hint kbd-hint">
|
<span class="connection-status-shortcut-hint kbd-hint">
|
||||||
<Kbd shortcut="cmd+shift+p" />
|
<Kbd shortcut="cmd+shift+p" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 flex items-center justify-center min-w-0">
|
<div class="flex-1 flex items-center justify-center min-w-0">
|
||||||
<span
|
<span
|
||||||
@@ -646,7 +648,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!props.mobileFullscreenMode}>
|
<Show when={isPhoneLayout() && !props.mobileFullscreenMode}>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={props.onEnterMobileFullscreen}
|
onClick={props.onEnterMobileFullscreen}
|
||||||
@@ -670,16 +672,18 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
{rightAppBarButtonIcon()}
|
{rightAppBarButtonIcon()}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</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">
|
||||||
<ContextMeter
|
<Show when={!showingInfoView()}>
|
||||||
usedTokens={tokenStats().used}
|
<ContextMeter
|
||||||
availableTokens={tokenStats().avail}
|
usedTokens={tokenStats().used}
|
||||||
formatTokens={formatTokenTotal}
|
availableTokens={tokenStats().avail}
|
||||||
usedLabel={t("instanceShell.metrics.usedLabel")}
|
formatTokens={formatTokenTotal}
|
||||||
availableLabel={t("instanceShell.metrics.availableLabel")}
|
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||||
/>
|
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -721,7 +725,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" }}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type Accessor,
|
type Accessor,
|
||||||
type Component,
|
type Component,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { FileContent, FileNode, File as GitFileStatus } from "@opencode-ai/sdk/v2/client"
|
import type { FileContent, FileNode, File as GitFileStatus } from "@opencode-ai/sdk/v2/client"
|
||||||
import IconButton from "@suid/material/IconButton"
|
import IconButton from "@suid/material/IconButton"
|
||||||
import MenuOpenIcon from "@suid/icons-material/MenuOpen"
|
import MenuOpenIcon from "@suid/icons-material/MenuOpen"
|
||||||
|
|||||||
@@ -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/v2"
|
||||||
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" : ""}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { batch, createMemo, type Accessor } from "solid-js"
|
import { batch, createMemo, type Accessor } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { Session } from "../../../types/session"
|
import type { Session } from "../../../types/session"
|
||||||
import {
|
import {
|
||||||
activeParentSessionId,
|
activeParentSessionId,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Index, type Accessor } from "solid-js"
|
|||||||
import VirtualItem from "./virtual-item"
|
import VirtualItem from "./virtual-item"
|
||||||
import MessageBlock from "./message-block"
|
import MessageBlock from "./message-block"
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
export function getMessageAnchorId(messageId: string) {
|
export function getMessageAnchorId(messageId: string) {
|
||||||
return `message-anchor-${messageId}`
|
return `message-anchor-${messageId}`
|
||||||
@@ -21,8 +22,13 @@ interface MessageBlockListProps {
|
|||||||
scrollContainer: Accessor<HTMLDivElement | undefined>
|
scrollContainer: Accessor<HTMLDivElement | undefined>
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
|
deleteHover?: Accessor<DeleteHoverState>
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
|
selectedMessageIds?: Accessor<Set<string>>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
setBottomSentinel: (element: HTMLDivElement | null) => void
|
setBottomSentinel: (element: HTMLDivElement | null) => void
|
||||||
suspendMeasurements?: () => boolean
|
suspendMeasurements?: () => boolean
|
||||||
}
|
}
|
||||||
@@ -51,7 +57,12 @@ export default function MessageBlockList(props: MessageBlockListProps) {
|
|||||||
showThinking={props.showThinking}
|
showThinking={props.showThinking}
|
||||||
thinkingDefaultExpanded={props.thinkingDefaultExpanded}
|
thinkingDefaultExpanded={props.thinkingDefaultExpanded}
|
||||||
showUsageMetrics={props.showUsageMetrics}
|
showUsageMetrics={props.showUsageMetrics}
|
||||||
|
deleteHover={props.deleteHover}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
|
||||||
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, Trash2 } from "lucide-solid"
|
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, ListStart, Trash } from "lucide-solid"
|
||||||
import MessageItem from "./message-item"
|
import MessageItem from "./message-item"
|
||||||
import ToolCall from "./tool-call"
|
import ToolCall from "./tool-call"
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
@@ -12,8 +12,17 @@ import { formatTokenTotal } from "../lib/formatters"
|
|||||||
import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions"
|
import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions"
|
||||||
import { setActiveInstanceId } from "../stores/instances"
|
import { setActiveInstanceId } from "../stores/instances"
|
||||||
import { showAlertDialog } from "../stores/alerts"
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
import { deleteMessagePart } from "../stores/session-actions"
|
import { deleteMessage } from "../stores/session-actions"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
|
function DeleteUpToIcon() {
|
||||||
|
return (
|
||||||
|
<span class="relative inline-block w-3.5 h-3.5" aria-hidden="true">
|
||||||
|
<ListStart class="absolute inset-0 w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const TOOL_ICON = "🔧"
|
const TOOL_ICON = "🔧"
|
||||||
const USER_BORDER_COLOR = "var(--message-user-border)"
|
const USER_BORDER_COLOR = "var(--message-user-border)"
|
||||||
@@ -23,10 +32,10 @@ const TOOL_BORDER_COLOR = "var(--message-tool-border)"
|
|||||||
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
||||||
|
|
||||||
|
|
||||||
type ToolState = import("@opencode-ai/sdk").ToolState
|
type ToolState = import("@opencode-ai/sdk/v2").ToolState
|
||||||
type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning
|
type ToolStateRunning = import("@opencode-ai/sdk/v2").ToolStateRunning
|
||||||
type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted
|
type ToolStateCompleted = import("@opencode-ai/sdk/v2").ToolStateCompleted
|
||||||
type ToolStateError = import("@opencode-ai/sdk").ToolStateError
|
type ToolStateError = import("@opencode-ai/sdk/v2").ToolStateError
|
||||||
|
|
||||||
function isToolStateRunning(state: ToolState | undefined): state is ToolStateRunning {
|
function isToolStateRunning(state: ToolState | undefined): state is ToolStateRunning {
|
||||||
return Boolean(state && state.status === "running")
|
return Boolean(state && state.status === "running")
|
||||||
@@ -194,8 +203,13 @@ interface MessageContentItemProps {
|
|||||||
messageIndex: number
|
messageIndex: number
|
||||||
lastAssistantIndex: () => number
|
lastAssistantIndex: () => number
|
||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
|
selectedMessageIds?: () => Set<string>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSupportedPartType(part: unknown): boolean {
|
function isSupportedPartType(part: unknown): boolean {
|
||||||
@@ -282,7 +296,12 @@ function MessageContentItem(props: MessageContentItemProps) {
|
|||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
isQueued={isQueued()}
|
isQueued={isQueued()}
|
||||||
showAgentMeta={showAgentMeta()}
|
showAgentMeta={showAgentMeta()}
|
||||||
|
showDeleteMessage={props.showDeleteMessage}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
@@ -298,11 +317,19 @@ interface ToolCallItemProps {
|
|||||||
messageId: string
|
messageId: string
|
||||||
partId: string
|
partId: string
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
|
selectedMessageIds?: () => Set<string>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolCallItem(props: ToolCallItemProps) {
|
function ToolCallItem(props: ToolCallItemProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [deleting, setDeleting] = createSignal(false)
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
|
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||||
|
|
||||||
|
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
||||||
|
|
||||||
const record = createMemo(() => props.store().getMessage(props.messageId))
|
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||||
@@ -319,14 +346,6 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
const messageVersion = createMemo(() => record()?.revision ?? 0)
|
const messageVersion = createMemo(() => record()?.revision ?? 0)
|
||||||
const partVersion = createMemo(() => partEntry()?.revision ?? 0)
|
const partVersion = createMemo(() => partEntry()?.revision ?? 0)
|
||||||
|
|
||||||
const deleteDisabled = createMemo(() => {
|
|
||||||
if (deleting()) return true
|
|
||||||
// Avoid deleting while a tool is actively running to prevent confusing UI states.
|
|
||||||
if (isToolStateRunning(toolState())) return true
|
|
||||||
// Avoid deleting permission prompts from here; those are interactive.
|
|
||||||
return Boolean(toolPart()?.pendingPermission)
|
|
||||||
})
|
|
||||||
|
|
||||||
const taskSessionId = createMemo(() => {
|
const taskSessionId = createMemo(() => {
|
||||||
const state = toolState()
|
const state = toolState()
|
||||||
if (!state) return ""
|
if (!state) return ""
|
||||||
@@ -350,38 +369,72 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
navigateToTaskSession(location)
|
navigateToTaskSession(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteToolPart = async (event: MouseEvent) => {
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
if (deleteDisabled()) return
|
if (!props.showDeleteMessage) return
|
||||||
|
if (deletingMessage()) return
|
||||||
|
|
||||||
setDeleting(true)
|
setDeletingMessage(true)
|
||||||
try {
|
try {
|
||||||
await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId)
|
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showAlertDialog(t("messageBlock.tool.deletePart.failed.message"), {
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
title: t("messageBlock.tool.deletePart.failed.title"),
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
detail: error instanceof Error ? error.message : String(error),
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false)
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!props.showDeleteMessage) return
|
||||||
|
if (!props.onDeleteMessagesUpTo) return
|
||||||
|
if (deletingUpTo()) return
|
||||||
|
|
||||||
|
setDeletingUpTo(true)
|
||||||
|
try {
|
||||||
|
await props.onDeleteMessagesUpTo(props.messageId)
|
||||||
|
} finally {
|
||||||
|
setDeletingUpTo(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={toolPart()}>
|
<Show when={toolPart()}>
|
||||||
{(resolvedToolPart) => (
|
{(resolvedToolPart) => (
|
||||||
<>
|
<div class="delete-hover-scope">
|
||||||
<div class="tool-call-header-label">
|
<div class="tool-call-header-label">
|
||||||
<div class="tool-call-header-meta">
|
<div class="tool-call-header-meta">
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<input
|
||||||
|
class="message-select-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelectedForDeletion()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onChange={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||||
|
props.onToggleSelectedMessage?.(props.messageId, next)
|
||||||
|
}}
|
||||||
|
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<span class="tool-call-icon">{TOOL_ICON}</span>
|
<span class="tool-call-icon">{TOOL_ICON}</span>
|
||||||
<span>{t("messageBlock.tool.header")}</span>
|
<span>{t("messageBlock.tool.header")}</span>
|
||||||
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-0">
|
||||||
<Show when={taskSessionId()}>
|
<Show when={taskSessionId()}>
|
||||||
<button
|
<button
|
||||||
class="tool-call-header-button"
|
class="tool-call-header-button"
|
||||||
@@ -395,16 +448,33 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<button
|
<Show when={props.showDeleteMessage}>
|
||||||
class="tool-call-header-button"
|
<button
|
||||||
type="button"
|
class="tool-call-header-button"
|
||||||
disabled={deleteDisabled()}
|
type="button"
|
||||||
onClick={handleDeleteToolPart}
|
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||||
title={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
onClick={handleDeleteUpTo}
|
||||||
aria-label={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId })}
|
||||||
>
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
</button>
|
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
|
>
|
||||||
|
<DeleteUpToIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="tool-call-header-button"
|
||||||
|
type="button"
|
||||||
|
disabled={deletingMessage()}
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -418,7 +488,7 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
@@ -470,7 +540,12 @@ interface MessageBlockProps {
|
|||||||
showThinking: () => boolean
|
showThinking: () => boolean
|
||||||
thinkingDefaultExpanded: () => boolean
|
thinkingDefaultExpanded: () => boolean
|
||||||
showUsageMetrics: () => boolean
|
showUsageMetrics: () => boolean
|
||||||
|
deleteHover?: () => DeleteHoverState
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
|
selectedMessageIds?: () => Set<string>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
}
|
}
|
||||||
@@ -481,6 +556,30 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||||
const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId)
|
const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId)
|
||||||
|
|
||||||
|
const isDeleteMessageHovered = () => {
|
||||||
|
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
|
||||||
|
|
||||||
|
const selected = props.selectedMessageIds?.() ?? new Set<string>()
|
||||||
|
if (selected.has(props.messageId)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hover.kind === "message") {
|
||||||
|
return hover.messageId === props.messageId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hover.kind === "deleteUpTo") {
|
||||||
|
const ids = props.store().getSessionMessageIds(props.sessionId)
|
||||||
|
const targetIndex = ids.indexOf(hover.messageId)
|
||||||
|
if (targetIndex === -1) return false
|
||||||
|
const currentIndex = ids.indexOf(props.messageId)
|
||||||
|
if (currentIndex === -1) return false
|
||||||
|
return currentIndex >= targetIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const block = createMemo<MessageDisplayBlock | null>(() => {
|
const block = createMemo<MessageDisplayBlock | null>(() => {
|
||||||
const current = record()
|
const current = record()
|
||||||
if (!current) return null
|
if (!current) return null
|
||||||
@@ -668,9 +767,13 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
return (
|
return (
|
||||||
<Show when={block()}>
|
<Show when={block()}>
|
||||||
{(resolvedBlock) => (
|
{(resolvedBlock) => (
|
||||||
<div class="message-stream-block" data-message-id={resolvedBlock().record.id}>
|
<div
|
||||||
|
class="message-stream-block"
|
||||||
|
data-message-id={resolvedBlock().record.id}
|
||||||
|
data-delete-message-hover={isDeleteMessageHovered() ? "true" : undefined}
|
||||||
|
>
|
||||||
<For each={resolvedBlock().items}>
|
<For each={resolvedBlock().items}>
|
||||||
{(item) => (
|
{(item, index) => (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={item.type === "content"}>
|
<Match when={item.type === "content"}>
|
||||||
<MessageContentItem
|
<MessageContentItem
|
||||||
@@ -681,7 +784,12 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
startPartId={(item as ContentDisplayItem).startPartId}
|
startPartId={(item as ContentDisplayItem).startPartId}
|
||||||
messageIndex={props.messageIndex}
|
messageIndex={props.messageIndex}
|
||||||
lastAssistantIndex={props.lastAssistantIndex}
|
lastAssistantIndex={props.lastAssistantIndex}
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
@@ -697,6 +805,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
store={props.store}
|
store={props.store}
|
||||||
messageId={toolItem.messageId}
|
messageId={toolItem.messageId}
|
||||||
partId={toolItem.partId}
|
partId={toolItem.partId}
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -709,6 +822,14 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
part={(item as StepDisplayItem).part}
|
part={(item as StepDisplayItem).part}
|
||||||
messageInfo={(item as StepDisplayItem).messageInfo}
|
messageInfo={(item as StepDisplayItem).messageInfo}
|
||||||
showAgentMeta
|
showAgentMeta
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
instanceId={props.instanceId}
|
||||||
|
sessionId={props.sessionId}
|
||||||
|
messageId={props.messageId}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "step-finish"}>
|
<Match when={item.type === "step-finish"}>
|
||||||
@@ -718,6 +839,14 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageInfo={(item as StepDisplayItem).messageInfo}
|
messageInfo={(item as StepDisplayItem).messageInfo}
|
||||||
showUsage={props.showUsageMetrics()}
|
showUsage={props.showUsageMetrics()}
|
||||||
borderColor={(item as StepDisplayItem).accentColor}
|
borderColor={(item as StepDisplayItem).accentColor}
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
instanceId={props.instanceId}
|
||||||
|
sessionId={props.sessionId}
|
||||||
|
messageId={props.messageId}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "compaction"}>
|
<Match when={item.type === "compaction"}>
|
||||||
@@ -728,7 +857,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
messageId={(item as CompactionDisplayItem).messageId}
|
messageId={(item as CompactionDisplayItem).messageId}
|
||||||
partId={(item as CompactionDisplayItem).partId}
|
showDeleteMessage={index() === 0}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "reasoning"}>
|
<Match when={item.type === "reasoning"}>
|
||||||
@@ -738,9 +871,13 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
messageId={(item as ReasoningDisplayItem).messageId}
|
messageId={(item as ReasoningDisplayItem).messageId}
|
||||||
partId={(item as ReasoningDisplayItem).partId}
|
|
||||||
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
|
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
|
||||||
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
|
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
|
||||||
|
showDeleteMessage={index() === 0}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -759,6 +896,14 @@ interface StepCardProps {
|
|||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
showUsage?: boolean
|
showUsage?: boolean
|
||||||
borderColor?: string
|
borderColor?: string
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
instanceId?: string
|
||||||
|
sessionId?: string
|
||||||
|
messageId?: string
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
|
selectedMessageIds?: () => Set<string>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CompactionCardProps {
|
interface CompactionCardProps {
|
||||||
@@ -768,12 +913,18 @@ interface CompactionCardProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
partId: string
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
|
selectedMessageIds?: () => Set<string>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompactionCard(props: CompactionCardProps) {
|
function CompactionCard(props: CompactionCardProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [deleting, setDeleting] = createSignal(false)
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
|
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||||
|
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
||||||
const isAuto = () => Boolean((props.part as any)?.auto)
|
const isAuto = () => Boolean((props.part as any)?.auto)
|
||||||
const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel"))
|
const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel"))
|
||||||
const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR)
|
const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR)
|
||||||
@@ -781,44 +932,98 @@ function CompactionCard(props: CompactionCardProps) {
|
|||||||
const containerClass = () =>
|
const containerClass = () =>
|
||||||
`message-compaction-card ${isAuto() ? "message-compaction-card--auto" : "message-compaction-card--manual"}`
|
`message-compaction-card ${isAuto() ? "message-compaction-card--auto" : "message-compaction-card--manual"}`
|
||||||
|
|
||||||
const canDelete = () => Boolean(props.partId) && !deleting()
|
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||||
|
|
||||||
const handleDelete = async (event: MouseEvent) => {
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (!canDelete()) return
|
if (!props.showDeleteMessage) return
|
||||||
setDeleting(true)
|
if (!canDeleteMessage()) return
|
||||||
|
setDeletingMessage(true)
|
||||||
try {
|
try {
|
||||||
await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId)
|
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showAlertDialog(t("messagePart.actions.deleteFailedMessage"), {
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
title: t("messagePart.actions.deleteFailedTitle"),
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
detail: error instanceof Error ? error.message : String(error),
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false)
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!props.showDeleteMessage) return
|
||||||
|
if (!props.onDeleteMessagesUpTo) return
|
||||||
|
if (deletingUpTo()) return
|
||||||
|
|
||||||
|
setDeletingUpTo(true)
|
||||||
|
try {
|
||||||
|
await props.onDeleteMessagesUpTo(props.messageId)
|
||||||
|
} finally {
|
||||||
|
setDeletingUpTo(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`${containerClass()} relative`}
|
class={`delete-hover-scope ${containerClass()} relative`}
|
||||||
style={{ "border-left": `4px solid ${borderColor()}` }}
|
style={{ "border-left": `4px solid ${borderColor()}` }}
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={t("messageBlock.compaction.ariaLabel")}
|
aria-label={t("messageBlock.compaction.ariaLabel")}
|
||||||
>
|
>
|
||||||
<button
|
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||||
type="button"
|
<Show when={props.showDeleteMessage}>
|
||||||
class="tool-call-header-button absolute right-2 top-1/2 -translate-y-1/2"
|
<button
|
||||||
disabled={!canDelete()}
|
type="button"
|
||||||
onClick={handleDelete}
|
class="tool-call-header-button"
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||||
>
|
onClick={handleDeleteUpTo}
|
||||||
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId })}
|
||||||
</button>
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
|
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
|
>
|
||||||
|
<DeleteUpToIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-header-button"
|
||||||
|
disabled={!canDeleteMessage()}
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="message-compaction-row">
|
<div class="message-compaction-row">
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<input
|
||||||
|
class="message-select-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelectedForDeletion()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onChange={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||||
|
props.onToggleSelectedMessage?.(props.messageId, next)
|
||||||
|
}}
|
||||||
|
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
|
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
|
||||||
<span class="message-compaction-label">{label()}</span>
|
<span class="message-compaction-label">{label()}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -828,6 +1033,9 @@ function CompactionCard(props: CompactionCardProps) {
|
|||||||
|
|
||||||
function StepCard(props: StepCardProps) {
|
function StepCard(props: StepCardProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
|
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||||
|
const isSelectedForDeletion = () => Boolean(props.messageId && props.selectedMessageIds?.().has(props.messageId))
|
||||||
const timestamp = () => {
|
const timestamp = () => {
|
||||||
const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now()
|
const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now()
|
||||||
const date = new Date(value)
|
const date = new Date(value)
|
||||||
@@ -872,6 +1080,42 @@ function StepCard(props: StepCardProps) {
|
|||||||
|
|
||||||
const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
|
const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
|
||||||
|
|
||||||
|
const canDeleteMessage = () =>
|
||||||
|
Boolean(props.showDeleteMessage && props.instanceId && props.sessionId && props.messageId) && !deletingMessage()
|
||||||
|
|
||||||
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!canDeleteMessage()) return
|
||||||
|
setDeletingMessage(true)
|
||||||
|
try {
|
||||||
|
await deleteMessage(props.instanceId!, props.sessionId!, props.messageId!)
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!props.messageId) return
|
||||||
|
if (!props.onDeleteMessagesUpTo) return
|
||||||
|
if (deletingUpTo()) return
|
||||||
|
|
||||||
|
setDeletingUpTo(true)
|
||||||
|
try {
|
||||||
|
await props.onDeleteMessagesUpTo(props.messageId)
|
||||||
|
} finally {
|
||||||
|
setDeletingUpTo(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
|
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
|
||||||
const entries = [
|
const entries = [
|
||||||
@@ -902,17 +1146,83 @@ function StepCard(props: StepCardProps) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div class={`message-step-card message-step-finish message-step-finish-flush`} style={finishStyle()}>
|
<div class={`message-step-card message-step-finish message-step-finish-flush relative`} style={finishStyle()}>
|
||||||
|
<Show when={props.showDeleteMessage && props.messageId}>
|
||||||
|
<input
|
||||||
|
class="message-select-checkbox absolute left-2 top-1/2 -translate-y-1/2"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelectedForDeletion()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onChange={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||||
|
props.onToggleSelectedMessage?.(props.messageId!, next)
|
||||||
|
}}
|
||||||
|
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-action-button"
|
||||||
|
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||||
|
onClick={handleDeleteUpTo}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId! })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
|
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
|
>
|
||||||
|
<DeleteUpToIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-action-button"
|
||||||
|
disabled={!canDeleteMessage()}
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId! })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{renderUsageChips(usage)}
|
{renderUsageChips(usage)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`message-step-card message-step-start`}>
|
<div class={`message-step-card message-step-start relative`}>
|
||||||
<div class="message-step-heading">
|
<div class="message-step-heading">
|
||||||
<div class="message-step-title">
|
<div class="message-step-title">
|
||||||
<div class="message-step-title-left">
|
<div class="message-step-title-left">
|
||||||
|
<Show when={props.showDeleteMessage && props.messageId}>
|
||||||
|
<input
|
||||||
|
class="message-select-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelectedForDeletion()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onChange={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||||
|
props.onToggleSelectedMessage?.(props.messageId!, next)
|
||||||
|
}}
|
||||||
|
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={props.showAgentMeta && (agentIdentifier() || modelIdentifier())}>
|
<Show when={props.showAgentMeta && (agentIdentifier() || modelIdentifier())}>
|
||||||
<span class="message-step-meta-inline">
|
<span class="message-step-meta-inline">
|
||||||
<Show when={agentIdentifier()}>{(value) => <span>{t("messageBlock.step.agentLabel", { agent: value() })}</span>}</Show>
|
<Show when={agentIdentifier()}>{(value) => <span>{t("messageBlock.step.agentLabel", { agent: value() })}</span>}</Show>
|
||||||
@@ -939,15 +1249,27 @@ interface ReasoningCardProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
partId: string
|
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
defaultExpanded?: boolean
|
defaultExpanded?: boolean
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
|
selectedMessageIds?: () => Set<string>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReasoningCard(props: ReasoningCardProps) {
|
function ReasoningCard(props: ReasoningCardProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
|
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
|
||||||
const [deleting, setDeleting] = createSignal(false)
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
|
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||||
|
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
||||||
|
|
||||||
|
let headerEl: HTMLDivElement | undefined
|
||||||
|
let actionsEl: HTMLDivElement | undefined
|
||||||
|
let primaryEl: HTMLSpanElement | undefined
|
||||||
|
let metaMeasureEl: HTMLSpanElement | undefined
|
||||||
|
const [showMetaInline, setShowMetaInline] = createSignal(true)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setExpanded(Boolean(props.defaultExpanded))
|
setExpanded(Boolean(props.defaultExpanded))
|
||||||
@@ -974,6 +1296,35 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
return modelID
|
return modelID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasMeta = () => Boolean(props.showAgentMeta && (agentIdentifier() || modelIdentifier()))
|
||||||
|
|
||||||
|
const updateMetaLayout = () => {
|
||||||
|
if (!hasMeta()) return
|
||||||
|
if (!headerEl || !actionsEl || !primaryEl || !metaMeasureEl) return
|
||||||
|
|
||||||
|
const headerWidth = headerEl.getBoundingClientRect().width
|
||||||
|
const actionsWidth = actionsEl.getBoundingClientRect().width
|
||||||
|
const primaryWidth = primaryEl.getBoundingClientRect().width
|
||||||
|
const metaWidth = metaMeasureEl.getBoundingClientRect().width
|
||||||
|
|
||||||
|
const availableLeft = Math.max(0, headerWidth - actionsWidth - 12)
|
||||||
|
setShowMetaInline(primaryWidth + metaWidth + 8 <= availableLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!hasMeta() || typeof ResizeObserver === "undefined") {
|
||||||
|
setShowMetaInline(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMetaLayout()
|
||||||
|
const observer = new ResizeObserver(() => updateMetaLayout())
|
||||||
|
if (headerEl) observer.observe(headerEl)
|
||||||
|
if (actionsEl) observer.observe(actionsEl)
|
||||||
|
if (primaryEl) observer.observe(primaryEl)
|
||||||
|
onCleanup(() => observer.disconnect())
|
||||||
|
})
|
||||||
|
|
||||||
const reasoningText = () => {
|
const reasoningText = () => {
|
||||||
const part = props.part as any
|
const part = props.part as any
|
||||||
if (!part) return ""
|
if (!part) return ""
|
||||||
@@ -1014,30 +1365,45 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
const viewHideLabel = () =>
|
const viewHideLabel = () =>
|
||||||
expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")
|
expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")
|
||||||
|
|
||||||
const hasDeleteTarget = () => Boolean(props.partId)
|
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||||
const canDelete = () => hasDeleteTarget() && !deleting()
|
|
||||||
|
|
||||||
const handleDelete = async (event: MouseEvent) => {
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (!canDelete()) return
|
if (!props.showDeleteMessage) return
|
||||||
setDeleting(true)
|
if (!canDeleteMessage()) return
|
||||||
|
setDeletingMessage(true)
|
||||||
try {
|
try {
|
||||||
await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId)
|
await deleteMessage(props.instanceId, props.sessionId, props.messageId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showAlertDialog(t("messagePart.actions.deleteFailedMessage"), {
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
title: t("messagePart.actions.deleteFailedTitle"),
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
detail: error instanceof Error ? error.message : String(error),
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false)
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteUpTo = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!props.showDeleteMessage) return
|
||||||
|
if (!props.onDeleteMessagesUpTo) return
|
||||||
|
if (deletingUpTo()) return
|
||||||
|
|
||||||
|
setDeletingUpTo(true)
|
||||||
|
try {
|
||||||
|
await props.onDeleteMessagesUpTo(props.messageId)
|
||||||
|
} finally {
|
||||||
|
setDeletingUpTo(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="message-reasoning-card">
|
<div class="delete-hover-scope message-reasoning-card">
|
||||||
<div class="message-reasoning-header">
|
<div class="message-reasoning-header" ref={(el) => (headerEl = el)}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="message-reasoning-toggle"
|
class="message-reasoning-toggle"
|
||||||
@@ -1045,9 +1411,30 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
aria-expanded={expanded()}
|
aria-expanded={expanded()}
|
||||||
aria-label={expanded() ? t("messageBlock.reasoning.collapseAriaLabel") : t("messageBlock.reasoning.expandAriaLabel")}
|
aria-label={expanded() ? t("messageBlock.reasoning.collapseAriaLabel") : t("messageBlock.reasoning.expandAriaLabel")}
|
||||||
>
|
>
|
||||||
<span class="message-reasoning-label flex flex-wrap items-center gap-2">
|
<span class="message-reasoning-label">
|
||||||
<span>{t("messageBlock.reasoning.thinkingLabel")}</span>
|
<span class="message-reasoning-label-primary" ref={(el) => (primaryEl = el)}>
|
||||||
<Show when={props.showAgentMeta && (agentIdentifier() || modelIdentifier())}>
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<input
|
||||||
|
class="message-select-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelectedForDeletion()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onChange={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||||
|
props.onToggleSelectedMessage?.(props.messageId, next)
|
||||||
|
}}
|
||||||
|
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<span>{t("messageBlock.reasoning.thinkingLabel")}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Show when={hasMeta() && showMetaInline()}>
|
||||||
<span class="message-step-meta-inline">
|
<span class="message-step-meta-inline">
|
||||||
<Show when={agentIdentifier()}>
|
<Show when={agentIdentifier()}>
|
||||||
{(value) => (
|
{(value) => (
|
||||||
@@ -1061,10 +1448,28 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={hasMeta()}>
|
||||||
|
<span
|
||||||
|
ref={(el) => (metaMeasureEl = el)}
|
||||||
|
class="message-step-meta-inline message-step-meta-inline--measure"
|
||||||
|
>
|
||||||
|
<Show when={agentIdentifier()}>
|
||||||
|
{(value) => (
|
||||||
|
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.agentLabel", { agent: value() })}</span>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<Show when={modelIdentifier()}>
|
||||||
|
{(value) => (
|
||||||
|
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.modelLabel", { model: value() })}</span>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="message-reasoning-actions">
|
<div class="message-reasoning-actions" ref={(el) => (actionsEl = el)}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
@@ -1081,16 +1486,31 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Show when={hasDeleteTarget()}>
|
<Show when={props.showDeleteMessage}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
onClick={handleDelete}
|
onClick={handleDeleteUpTo}
|
||||||
disabled={!canDelete()}
|
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||||
aria-label={t("messagePart.actions.deleteTitle")}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId })}
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
|
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
>
|
>
|
||||||
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
<DeleteUpToIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-action-button"
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
disabled={!canDeleteMessage()}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
@@ -1098,6 +1518,23 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={hasMeta() && !showMetaInline()}>
|
||||||
|
<div class="message-reasoning-meta-row">
|
||||||
|
<span class="message-step-meta-inline">
|
||||||
|
<Show when={agentIdentifier()}>
|
||||||
|
{(value) => (
|
||||||
|
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.agentLabel", { agent: value() })}</span>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<Show when={modelIdentifier()}>
|
||||||
|
{(value) => (
|
||||||
|
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.modelLabel", { model: value() })}</span>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={expanded()}>
|
<Show when={expanded()}>
|
||||||
<div class="message-reasoning-expanded">
|
<div class="message-reasoning-expanded">
|
||||||
<div class="message-reasoning-body">
|
<div class="message-reasoning-body">
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
import { For, Show, createSignal } from "solid-js"
|
import { For, Show, createEffect, createSignal, onCleanup } from "solid-js"
|
||||||
import { Copy, ExternalLink, Split, Trash2, Undo } from "lucide-solid"
|
import { Portal } from "solid-js/web"
|
||||||
import type { MessageInfo, ClientPart } from "../types/message"
|
import { Copy, ListStart, Split, Trash, Undo } from "lucide-solid"
|
||||||
|
import type { MessageInfo, ClientPart, SDKAssistantMessageV2 } from "../types/message"
|
||||||
import { partHasRenderableText } from "../types/message"
|
import { partHasRenderableText } from "../types/message"
|
||||||
import type { MessageRecord } from "../stores/message-v2/types"
|
import type { MessageRecord } from "../stores/message-v2/types"
|
||||||
import MessagePart from "./message-part"
|
import MessagePart from "./message-part"
|
||||||
import { copyToClipboard } from "../lib/clipboard"
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
import { showAlertDialog } from "../stores/alerts"
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
import { deleteMessagePart } from "../stores/session-actions"
|
import { deleteMessage } from "../stores/session-actions"
|
||||||
import { isTauriHost } from "../lib/runtime-env"
|
import { isTauriHost } from "../lib/runtime-env"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
|
function DeleteUpToIcon() {
|
||||||
|
return (
|
||||||
|
<span class="relative inline-block w-3.5 h-3.5" aria-hidden="true">
|
||||||
|
<ListStart class="absolute inset-0 w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
record: MessageRecord
|
record: MessageRecord
|
||||||
@@ -18,15 +28,112 @@ interface MessageItemProps {
|
|||||||
isQueued?: boolean
|
isQueued?: boolean
|
||||||
parts: ClientPart[]
|
parts: ClientPart[]
|
||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
|
selectedMessageIds?: () => Set<string>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
|
showDeleteMessage?: boolean
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MessageItem(props: MessageItemProps) {
|
export default function MessageItem(props: MessageItemProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [copied, setCopied] = createSignal(false)
|
const [copied, setCopied] = createSignal(false)
|
||||||
const [deletingParts, setDeletingParts] = createSignal<Set<string>>(new Set())
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
|
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||||
|
|
||||||
|
type ImagePreviewState = {
|
||||||
|
url: string
|
||||||
|
name: string
|
||||||
|
anchor: HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const [imagePreview, setImagePreview] = createSignal<ImagePreviewState | null>(null)
|
||||||
|
|
||||||
|
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value))
|
||||||
|
|
||||||
|
const getImagePreviewPosition = () => {
|
||||||
|
const state = imagePreview()
|
||||||
|
if (!state) return null
|
||||||
|
|
||||||
|
const rect = state.anchor.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Outer box: 320px image + 8px padding on each side.
|
||||||
|
const padding = 8
|
||||||
|
const maxImage = 320
|
||||||
|
const gap = 8
|
||||||
|
const chrome = padding * 2
|
||||||
|
const outerWidth = maxImage + chrome
|
||||||
|
const outerHeight = maxImage + chrome
|
||||||
|
|
||||||
|
const viewportW = window.innerWidth
|
||||||
|
const viewportH = window.innerHeight
|
||||||
|
|
||||||
|
const left = clamp(rect.left, 8, Math.max(8, viewportW - outerWidth - 8))
|
||||||
|
|
||||||
|
const fitsAbove = rect.top >= outerHeight + gap + 8
|
||||||
|
const preferredTop = fitsAbove ? rect.top - outerHeight - gap : rect.bottom + gap
|
||||||
|
const top = clamp(preferredTop, 8, Math.max(8, viewportH - outerHeight - 8))
|
||||||
|
|
||||||
|
return { left, top }
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const active = imagePreview()
|
||||||
|
if (!active) return
|
||||||
|
|
||||||
|
// If the user scrolls (message stream scroll container) or resizes, the anchor moves.
|
||||||
|
// Hide the popover to avoid showing it in the wrong place.
|
||||||
|
const hide = () => setImagePreview(null)
|
||||||
|
window.addEventListener("scroll", hide, true)
|
||||||
|
window.addEventListener("resize", hide)
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener("scroll", hide, true)
|
||||||
|
window.removeEventListener("resize", hide)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.record.id))
|
||||||
|
|
||||||
|
let topRowEl: HTMLDivElement | undefined
|
||||||
|
let actionsEl: HTMLDivElement | undefined
|
||||||
|
let speakerPrimaryEl: HTMLDivElement | undefined
|
||||||
|
let metaMeasureEl: HTMLSpanElement | undefined
|
||||||
|
const [showMetaInline, setShowMetaInline] = createSignal(true)
|
||||||
|
|
||||||
|
const metaText = () => agentMeta()
|
||||||
|
|
||||||
|
const updateMetaLayout = () => {
|
||||||
|
const text = metaText()
|
||||||
|
if (!text) return
|
||||||
|
if (!topRowEl || !actionsEl || !speakerPrimaryEl || !metaMeasureEl) return
|
||||||
|
|
||||||
|
const rowWidth = topRowEl.getBoundingClientRect().width
|
||||||
|
const actionsWidth = actionsEl.getBoundingClientRect().width
|
||||||
|
const primaryWidth = speakerPrimaryEl.getBoundingClientRect().width
|
||||||
|
const metaWidth = metaMeasureEl.getBoundingClientRect().width
|
||||||
|
|
||||||
|
// Allow for the flex gap between left and actions.
|
||||||
|
const availableLeft = Math.max(0, rowWidth - actionsWidth - 12)
|
||||||
|
setShowMetaInline(primaryWidth + metaWidth + 8 <= availableLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const text = metaText()
|
||||||
|
if (!text || typeof ResizeObserver === "undefined") {
|
||||||
|
setShowMetaInline(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMetaLayout()
|
||||||
|
const observer = new ResizeObserver(() => updateMetaLayout())
|
||||||
|
if (topRowEl) observer.observe(topRowEl)
|
||||||
|
if (actionsEl) observer.observe(actionsEl)
|
||||||
|
if (speakerPrimaryEl) observer.observe(speakerPrimaryEl)
|
||||||
|
onCleanup(() => observer.disconnect())
|
||||||
|
})
|
||||||
|
|
||||||
const isUser = () => props.record.role === "user"
|
const isUser = () => props.record.role === "user"
|
||||||
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
|
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
|
||||||
@@ -123,6 +230,11 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showImagePreview = (anchor: HTMLElement, url: string, name: string) => {
|
||||||
|
if (!url) return
|
||||||
|
setImagePreview({ anchor, url, name })
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage = () => {
|
const errorMessage = () => {
|
||||||
const info = props.messageInfo
|
const info = props.messageInfo
|
||||||
if (!info || info.role !== "assistant" || !info.error) return null
|
if (!info || info.role !== "assistant" || !info.error) return null
|
||||||
@@ -190,47 +302,30 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletableTextPartId = () => {
|
const handleDeleteMessage = async () => {
|
||||||
const part = props.parts.find((candidate) => {
|
if (deletingMessage()) return
|
||||||
if (!candidate || candidate.type !== "text") return false
|
setDeletingMessage(true)
|
||||||
const id = (candidate as any).id
|
|
||||||
if (typeof id !== "string" || id.length === 0) return false
|
|
||||||
return !Boolean((candidate as any).synthetic)
|
|
||||||
})
|
|
||||||
return (part as any)?.id as string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDeletingPart = (partId?: string) => {
|
|
||||||
if (!partId) return false
|
|
||||||
return deletingParts().has(partId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setPartDeleting = (partId: string, value: boolean) => {
|
|
||||||
setDeletingParts((prev) => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
if (value) {
|
|
||||||
next.add(partId)
|
|
||||||
} else {
|
|
||||||
next.delete(partId)
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeletePart = async (partId?: string) => {
|
|
||||||
if (!partId) return
|
|
||||||
if (isDeletingPart(partId)) return
|
|
||||||
setPartDeleting(partId, true)
|
|
||||||
try {
|
try {
|
||||||
await deleteMessagePart(props.instanceId, props.sessionId, props.record.id, partId)
|
await deleteMessage(props.instanceId, props.sessionId, props.record.id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showAlertDialog(t("messagePart.actions.deleteFailedMessage"), {
|
showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), {
|
||||||
title: t("messagePart.actions.deleteFailedTitle"),
|
title: t("messageItem.actions.deleteMessageFailedTitle"),
|
||||||
detail: error instanceof Error ? error.message : String(error),
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setPartDeleting(partId, false)
|
setDeletingMessage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteUpTo = async () => {
|
||||||
|
if (!props.onDeleteMessagesUpTo) return
|
||||||
|
if (deletingUpTo()) return
|
||||||
|
setDeletingUpTo(true)
|
||||||
|
try {
|
||||||
|
await props.onDeleteMessagesUpTo(props.record.id)
|
||||||
|
} finally {
|
||||||
|
setDeletingUpTo(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,8 +353,16 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
if (!info || info.role !== "assistant") return ""
|
if (!info || info.role !== "assistant") return ""
|
||||||
const modelID = info.modelID || ""
|
const modelID = info.modelID || ""
|
||||||
const providerID = info.providerID || ""
|
const providerID = info.providerID || ""
|
||||||
if (modelID && providerID) return `${providerID}/${modelID}`
|
|
||||||
return modelID
|
const base = modelID && providerID ? `${providerID}/${modelID}` : modelID
|
||||||
|
if (!base) return ""
|
||||||
|
|
||||||
|
const variant = (info as SDKAssistantMessageV2).variant
|
||||||
|
if (typeof variant === "string" && variant.trim().length > 0) {
|
||||||
|
return `${base} (${variant.trim()})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentMeta = () => {
|
const agentMeta = () => {
|
||||||
@@ -280,26 +383,58 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
return (
|
return (
|
||||||
<div class={containerClass()}>
|
<div class={containerClass()}>
|
||||||
<header class={`message-item-header ${isUser() ? "pb-0.5" : "pb-0"}`}>
|
<header class={`message-item-header ${isUser() ? "pb-0.5" : "pb-0"}`}>
|
||||||
<div class="message-item-header-row message-item-header-row--top">
|
<div class="message-item-header-row message-item-header-row--top" ref={(el) => (topRowEl = el)}>
|
||||||
<div class="message-speaker">
|
<div class="message-header-left">
|
||||||
<span class="message-speaker-label" data-role={isUser() ? "user" : "assistant"}>
|
<div class="message-speaker-primary" ref={(el) => (speakerPrimaryEl = el)}>
|
||||||
{speakerLabel()}
|
<Show when={props.showDeleteMessage}>
|
||||||
</span>
|
<input
|
||||||
|
class="message-select-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelectedForDeletion()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onChange={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||||
|
props.onToggleSelectedMessage?.(props.record.id, next)
|
||||||
|
}}
|
||||||
|
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<span class="message-speaker-label" data-role={isUser() ? "user" : "assistant"}>
|
||||||
|
{speakerLabel()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={metaText() && showMetaInline()}>
|
||||||
|
<span class="message-agent-meta-inline">{metaText()}</span>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={metaText()}>
|
||||||
|
<span
|
||||||
|
ref={(el) => (metaMeasureEl = el)}
|
||||||
|
class="message-agent-meta-inline message-agent-meta-inline--measure"
|
||||||
|
>
|
||||||
|
{metaText()}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message-item-actions">
|
<div class="message-item-actions" ref={(el) => (actionsEl = el)}>
|
||||||
<Show when={isUser()}>
|
<Show when={isUser()}>
|
||||||
<div class="message-action-group">
|
<div class="message-action-group">
|
||||||
<Show when={props.onRevert}>
|
<button
|
||||||
<button
|
class="message-action-button"
|
||||||
class="message-action-button"
|
onClick={handleCopy}
|
||||||
onClick={handleRevert}
|
title={copyLabel()}
|
||||||
title={t("messageItem.actions.revert")}
|
aria-label={copyLabel()}
|
||||||
aria-label={t("messageItem.actions.revert")}
|
>
|
||||||
>
|
<Copy class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
<Undo class="w-3.5 h-3.5" aria-hidden="true" />
|
</button>
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<Show when={props.onFork}>
|
<Show when={props.onFork}>
|
||||||
<button
|
<button
|
||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
@@ -310,14 +445,43 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
<Split class="w-3.5 h-3.5" aria-hidden="true" />
|
<Split class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
<button
|
|
||||||
class="message-action-button"
|
<Show when={props.onRevert}>
|
||||||
onClick={handleCopy}
|
<button
|
||||||
title={copyLabel()}
|
class="message-action-button"
|
||||||
aria-label={copyLabel()}
|
onClick={handleRevert}
|
||||||
>
|
title={t("messageItem.actions.revertTitle")}
|
||||||
<Copy class="w-3.5 h-3.5" aria-hidden="true" />
|
aria-label={t("messageItem.actions.revertTitle")}
|
||||||
</button>
|
>
|
||||||
|
<Undo class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
|
<button
|
||||||
|
class="message-action-button"
|
||||||
|
onClick={() => void handleDeleteUpTo()}
|
||||||
|
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.record.id })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
|
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
|
>
|
||||||
|
<DeleteUpToIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="message-action-button"
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
disabled={deletingMessage()}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!isUser()}>
|
<Show when={!isUser()}>
|
||||||
@@ -331,18 +495,30 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
<Copy class="w-3.5 h-3.5" aria-hidden="true" />
|
<Copy class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Show when={deletableTextPartId()}>
|
<Show when={props.showDeleteMessage}>
|
||||||
{(partId) => (
|
<button
|
||||||
<button
|
class="message-action-button"
|
||||||
class="message-action-button"
|
onClick={() => void handleDeleteUpTo()}
|
||||||
onClick={() => void handleDeletePart(partId())}
|
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
|
||||||
disabled={isDeletingPart(partId())}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.record.id })}
|
||||||
title={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
aria-label={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
title={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
>
|
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
|
||||||
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
>
|
||||||
</button>
|
<DeleteUpToIcon />
|
||||||
)}
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="message-action-button"
|
||||||
|
onClick={handleDeleteMessage}
|
||||||
|
disabled={deletingMessage()}
|
||||||
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
|
||||||
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
|
>
|
||||||
|
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -350,12 +526,10 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={agentMeta()}>
|
<Show when={metaText() && !showMetaInline()}>
|
||||||
{(meta) => (
|
<div class="message-item-header-row message-item-header-row--meta">
|
||||||
<div class="message-item-header-row message-item-header-row--bottom">
|
<span class="message-agent-meta-block">{metaText()}</span>
|
||||||
<span class="message-agent-meta">{meta()}</span>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
@@ -378,16 +552,20 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<For each={messageParts()}>
|
<For each={messageParts()}>
|
||||||
{(part) => (
|
{(part) => {
|
||||||
<MessagePart
|
return (
|
||||||
part={part}
|
<div class="message-part-shell">
|
||||||
messageType={props.record.role}
|
<MessagePart
|
||||||
instanceId={props.instanceId}
|
part={part}
|
||||||
sessionId={props.sessionId}
|
messageType={props.record.role}
|
||||||
primaryUserTextPartId={primaryUserTextPartId()}
|
instanceId={props.instanceId}
|
||||||
onRendered={props.onContentRendered}
|
sessionId={props.sessionId}
|
||||||
/>
|
primaryUserTextPartId={primaryUserTextPartId()}
|
||||||
)}
|
onRendered={props.onContentRendered}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<Show when={fileAttachments().length > 0}>
|
<Show when={fileAttachments().length > 0}>
|
||||||
@@ -397,7 +575,16 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
const name = getAttachmentName(attachment)
|
const name = getAttachmentName(attachment)
|
||||||
const isImage = isImageAttachment(attachment)
|
const isImage = isImageAttachment(attachment)
|
||||||
return (
|
return (
|
||||||
<div class={`attachment-chip ${isImage ? "attachment-chip-image" : ""}`} title={name}>
|
<div
|
||||||
|
class={`attachment-chip ${isImage ? "attachment-chip-image" : ""}`}
|
||||||
|
title={name}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!isImage) return
|
||||||
|
const el = e.currentTarget as HTMLElement
|
||||||
|
showImagePreview(el, attachment.url || "", name)
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => setImagePreview(null)}
|
||||||
|
>
|
||||||
<Show when={isImage} fallback={
|
<Show when={isImage} fallback={
|
||||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path
|
<path
|
||||||
@@ -425,24 +612,6 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => void handleDeletePart(attachment.id)}
|
|
||||||
class="attachment-remove"
|
|
||||||
disabled={isDeletingPart(attachment.id)}
|
|
||||||
aria-label={t("messagePart.actions.deleteTitle")}
|
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
|
||||||
>
|
|
||||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<Show when={isImage}>
|
|
||||||
<div class="attachment-chip-preview">
|
|
||||||
<img src={attachment.url} alt={name} />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -450,6 +619,31 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={imagePreview()}>
|
||||||
|
{(stateAccessor) => {
|
||||||
|
const state = stateAccessor()
|
||||||
|
const pos = () => getImagePreviewPosition()
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Show when={pos()}>
|
||||||
|
{(posAccessor) => {
|
||||||
|
const coords = posAccessor()
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="attachment-image-popover"
|
||||||
|
style={{ left: `${coords.left}px`, top: `${coords.top}px` }}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<img src={state.url} alt={state.name} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={props.record.status === "sending"}>
|
<Show when={props.record.status === "sending"}>
|
||||||
<div class="message-sending">
|
<div class="message-sending">
|
||||||
<span class="generating-spinner">●</span> {t("messageItem.status.sending")}
|
<span class="generating-spinner">●</span> {t("messageItem.status.sending")}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
|
|||||||
<div class="connection-status-shortcut-action">
|
<div class="connection-status-shortcut-action">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="connection-status-button"
|
class="connection-status-button command-palette-button"
|
||||||
onClick={props.onCommandPalette}
|
onClick={props.onCommandPalette}
|
||||||
aria-label={t("messageListHeader.commandPalette.ariaLabel")}
|
aria-label={t("messageListHeader.commandPalette.ariaLabel")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import type { Component } from "solid-js"
|
import type { Component } from "solid-js"
|
||||||
import MessageBlock from "./message-block"
|
import MessageBlock from "./message-block"
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
interface MessagePreviewProps {
|
interface MessagePreviewProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
store: () => InstanceMessageStore
|
store: () => InstanceMessageStore
|
||||||
|
deleteHover?: () => DeleteHoverState
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
|
selectedMessageIds?: () => Set<string>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessagePreview: Component<MessagePreviewProps> = (props) => {
|
const MessagePreview: Component<MessagePreviewProps> = (props) => {
|
||||||
@@ -24,6 +30,11 @@ const MessagePreview: Component<MessagePreviewProps> = (props) => {
|
|||||||
showThinking={() => false}
|
showThinking={() => false}
|
||||||
thinkingDefaultExpanded={() => false}
|
thinkingDefaultExpanded={() => false}
|
||||||
showUsageMetrics={() => false}
|
showUsageMetrics={() => false}
|
||||||
|
deleteHover={props.deleteHover}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
|
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Show, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
|
import { Show, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
|
||||||
|
import { CheckSquare, Trash, X } from "lucide-solid"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
import MessageBlockList, { getMessageAnchorId } from "./message-block-list"
|
import MessageBlockList, { getMessageAnchorId } from "./message-block-list"
|
||||||
import MessageTimeline, { buildTimelineSegments, type TimelineSegment } from "./message-timeline"
|
import MessageTimeline, { buildTimelineSegments, type TimelineSegment } from "./message-timeline"
|
||||||
@@ -9,7 +10,10 @@ import { useScrollCache } from "../lib/hooks/use-scroll-cache"
|
|||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
import { copyToClipboard } from "../lib/clipboard"
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
import { showToastNotification } from "../lib/notifications"
|
import { showToastNotification } from "../lib/notifications"
|
||||||
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
|
import { deleteMessage } from "../stores/session-actions"
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
const SCROLL_SCOPE = "session"
|
const SCROLL_SCOPE = "session"
|
||||||
const SCROLL_SENTINEL_MARGIN_PX = 48
|
const SCROLL_SENTINEL_MARGIN_PX = 48
|
||||||
@@ -23,6 +27,7 @@ export interface MessageSectionProps {
|
|||||||
sessionId: string
|
sessionId: string
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
registerScrollToBottom?: (fn: () => void) => void
|
registerScrollToBottom?: (fn: () => void) => void
|
||||||
showSidebarToggle?: boolean
|
showSidebarToggle?: boolean
|
||||||
@@ -145,6 +150,63 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [activeMessageId, setActiveMessageId] = createSignal<string | null>(null)
|
const [activeMessageId, setActiveMessageId] = createSignal<string | null>(null)
|
||||||
|
|
||||||
|
const [deleteHover, setDeleteHover] = createSignal<DeleteHoverState>({ kind: "none" })
|
||||||
|
|
||||||
|
const [selectedForDeletion, setSelectedForDeletion] = createSignal<Set<string>>(new Set<string>())
|
||||||
|
const isDeleteMode = createMemo(() => selectedForDeletion().size > 0)
|
||||||
|
const selectedDeleteCount = createMemo(() => selectedForDeletion().size)
|
||||||
|
|
||||||
|
const isMessageSelectedForDeletion = (messageId: string) => selectedForDeletion().has(messageId)
|
||||||
|
|
||||||
|
const setMessageSelectedForDeletion = (messageId: string, selected: boolean) => {
|
||||||
|
if (!messageId) return
|
||||||
|
setSelectedForDeletion((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (selected) {
|
||||||
|
next.add(messageId)
|
||||||
|
} else {
|
||||||
|
next.delete(messageId)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearDeleteMode = () => {
|
||||||
|
setSelectedForDeletion(new Set<string>())
|
||||||
|
setDeleteHover({ kind: "none" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAllForDeletion = () => {
|
||||||
|
setSelectedForDeletion(new Set<string>(messageIds()))
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSelectedMessages = async () => {
|
||||||
|
const selected = selectedForDeletion()
|
||||||
|
if (selected.size === 0) return
|
||||||
|
|
||||||
|
const idsInSessionOrder = messageIds()
|
||||||
|
const toDelete: string[] = []
|
||||||
|
for (let idx = idsInSessionOrder.length - 1; idx >= 0; idx -= 1) {
|
||||||
|
const id = idsInSessionOrder[idx]
|
||||||
|
if (selected.has(id)) {
|
||||||
|
toDelete.push(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const messageId of toDelete) {
|
||||||
|
await deleteMessage(props.instanceId, props.sessionId, messageId)
|
||||||
|
}
|
||||||
|
clearDeleteMode()
|
||||||
|
} catch (error) {
|
||||||
|
showAlertDialog(t("messageSection.bulkDelete.failedMessage"), {
|
||||||
|
title: t("messageSection.bulkDelete.failedTitle"),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const changeToken = createMemo(() => String(sessionRevision()))
|
const changeToken = createMemo(() => String(sessionRevision()))
|
||||||
const isActive = createMemo(() => props.isActive !== false)
|
const isActive = createMemo(() => props.isActive !== false)
|
||||||
@@ -167,6 +229,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
const [autoScroll, setAutoScroll] = createSignal(true)
|
const [autoScroll, setAutoScroll] = createSignal(true)
|
||||||
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
||||||
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
||||||
|
const scrollButtonsCount = createMemo(() => (showScrollTopButton() ? 1 : 0) + (showScrollBottomButton() ? 1 : 0))
|
||||||
const [topSentinelVisible, setTopSentinelVisible] = createSignal(true)
|
const [topSentinelVisible, setTopSentinelVisible] = createSignal(true)
|
||||||
const [bottomSentinelVisible, setBottomSentinelVisible] = createSignal(true)
|
const [bottomSentinelVisible, setBottomSentinelVisible] = createSignal(true)
|
||||||
const [quoteSelection, setQuoteSelection] = createSignal<{ text: string; top: number; left: number } | null>(null)
|
const [quoteSelection, setQuoteSelection] = createSignal<{ text: string; top: number; left: number } | null>(null)
|
||||||
@@ -851,7 +914,10 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="message-stream-container">
|
<div class="message-stream-container">
|
||||||
<div class={`message-layout${hasTimelineSegments() ? " message-layout--with-timeline" : ""}`}>
|
<div
|
||||||
|
class={`message-layout${hasTimelineSegments() ? " message-layout--with-timeline" : ""}`}
|
||||||
|
data-scroll-buttons={scrollButtonsCount()}
|
||||||
|
>
|
||||||
<div class="message-stream-shell" ref={setShellElement}>
|
<div class="message-stream-shell" ref={setShellElement}>
|
||||||
<div class="message-stream" ref={setContainerRef} onScroll={handleScroll} onMouseUp={handleStreamMouseUp}>
|
<div class="message-stream" ref={setContainerRef} onScroll={handleScroll} onMouseUp={handleStreamMouseUp}>
|
||||||
<div ref={setTopSentinel} aria-hidden="true" style={{ height: "1px" }} />
|
<div ref={setTopSentinel} aria-hidden="true" style={{ height: "1px" }} />
|
||||||
@@ -897,8 +963,13 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
scrollContainer={scrollElement}
|
scrollContainer={scrollElement}
|
||||||
loading={props.loading}
|
loading={props.loading}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={handleContentRendered}
|
onContentRendered={handleContentRendered}
|
||||||
|
deleteHover={deleteHover}
|
||||||
|
onDeleteHoverChange={setDeleteHover}
|
||||||
|
selectedMessageIds={selectedForDeletion}
|
||||||
|
onToggleSelectedMessage={setMessageSelectedForDeletion}
|
||||||
setBottomSentinel={setBottomSentinel}
|
setBottomSentinel={setBottomSentinel}
|
||||||
suspendMeasurements={() => !isActive()}
|
suspendMeasurements={() => !isActive()}
|
||||||
/>
|
/>
|
||||||
@@ -957,9 +1028,56 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
showToolSegments={showTimelineToolsPreference()}
|
showToolSegments={showTimelineToolsPreference()}
|
||||||
|
deleteHover={deleteHover}
|
||||||
|
onDeleteHoverChange={setDeleteHover}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
|
selectedMessageIds={selectedForDeletion}
|
||||||
|
onToggleSelectedMessage={setMessageSelectedForDeletion}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={isDeleteMode()}>
|
||||||
|
<div
|
||||||
|
class="message-delete-mode-toolbar"
|
||||||
|
role="toolbar"
|
||||||
|
aria-label={t("messageSection.bulkDelete.toolbarAriaLabel", { count: selectedDeleteCount() })}
|
||||||
|
>
|
||||||
|
<span class="message-delete-mode-count" aria-hidden="true">
|
||||||
|
{selectedDeleteCount()}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-delete-mode-button"
|
||||||
|
onClick={() => void deleteSelectedMessages()}
|
||||||
|
title={t("messageSection.bulkDelete.deleteSelectedTitle")}
|
||||||
|
aria-label={t("messageSection.bulkDelete.deleteSelectedTitle")}
|
||||||
|
>
|
||||||
|
<Trash class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-delete-mode-button"
|
||||||
|
onClick={selectAllForDeletion}
|
||||||
|
title={t("messageSection.bulkDelete.selectAllTitle")}
|
||||||
|
aria-label={t("messageSection.bulkDelete.selectAllTitle")}
|
||||||
|
>
|
||||||
|
<CheckSquare class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="message-delete-mode-button"
|
||||||
|
onClick={clearDeleteMode}
|
||||||
|
title={t("messageSection.bulkDelete.cancelTitle")}
|
||||||
|
aria-label={t("messageSection.bulkDelete.cancelTitle")}
|
||||||
|
>
|
||||||
|
<X class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show, createEffect, createMemo, createSignal, onCleanup, type Component } from "solid-js"
|
import { For, Show, createEffect, createMemo, createSignal, onCleanup, on, untrack, type Component } from "solid-js"
|
||||||
import MessagePreview from "./message-preview"
|
import MessagePreview from "./message-preview"
|
||||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||||
import type { ClientPart } from "../types/message"
|
import type { ClientPart } from "../types/message"
|
||||||
@@ -7,6 +7,7 @@ import { buildRecordDisplayData } from "../stores/message-v2/record-display-cach
|
|||||||
import { getToolIcon } from "./tool-call/utils"
|
import { getToolIcon } from "./tool-call/utils"
|
||||||
import { User as UserIcon, Bot as BotIcon, FoldVertical, ShieldAlert } from "lucide-solid"
|
import { User as UserIcon, Bot as BotIcon, FoldVertical, ShieldAlert } from "lucide-solid"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction"
|
export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction"
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ export interface TimelineSegment {
|
|||||||
shortLabel?: string
|
shortLabel?: string
|
||||||
variant?: "auto" | "manual"
|
variant?: "auto" | "manual"
|
||||||
toolPartIds?: string[]
|
toolPartIds?: string[]
|
||||||
|
partIds?: string[]
|
||||||
|
partId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageTimelineProps {
|
interface MessageTimelineProps {
|
||||||
@@ -28,6 +31,11 @@ interface MessageTimelineProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
showToolSegments?: boolean
|
showToolSegments?: boolean
|
||||||
|
deleteHover?: () => DeleteHoverState
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
|
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||||
|
selectedMessageIds?: () => Set<string>
|
||||||
|
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TOOLTIP_LENGTH = 220
|
const MAX_TOOLTIP_LENGTH = 220
|
||||||
@@ -38,10 +46,7 @@ interface PendingSegment {
|
|||||||
type: TimelineSegmentType
|
type: TimelineSegmentType
|
||||||
texts: string[]
|
texts: string[]
|
||||||
reasoningTexts: string[]
|
reasoningTexts: string[]
|
||||||
toolTitles: string[]
|
partIds: string[]
|
||||||
toolTypeLabels: string[]
|
|
||||||
toolIcons: string[]
|
|
||||||
toolPartIds: string[]
|
|
||||||
hasPrimaryText: boolean
|
hasPrimaryText: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,17 +176,12 @@ export function buildTimelineSegments(
|
|||||||
pending = null
|
pending = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const isToolSegment = pending.type === "tool"
|
const label = segmentLabel(pending.type)
|
||||||
const label = isToolSegment
|
const shortLabel = undefined
|
||||||
? pending.toolTypeLabels[0] || segmentLabel("tool")
|
const tooltip = formatTextsTooltip(
|
||||||
: segmentLabel(pending.type)
|
[...pending.texts, ...pending.reasoningTexts],
|
||||||
const shortLabel = isToolSegment ? pending.toolIcons[0] || getToolIcon("tool") : undefined
|
pending.type === "user" ? t("messageTimeline.tooltip.userFallback") : t("messageTimeline.tooltip.assistantFallback"),
|
||||||
const tooltip = isToolSegment
|
)
|
||||||
? formatToolTooltip(pending.toolTitles, t)
|
|
||||||
: formatTextsTooltip(
|
|
||||||
[...pending.texts, ...pending.reasoningTexts],
|
|
||||||
pending.type === "user" ? t("messageTimeline.tooltip.userFallback") : t("messageTimeline.tooltip.assistantFallback"),
|
|
||||||
)
|
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
id: `${record.id}:${segmentIndex}`,
|
id: `${record.id}:${segmentIndex}`,
|
||||||
@@ -190,7 +190,7 @@ export function buildTimelineSegments(
|
|||||||
label,
|
label,
|
||||||
tooltip,
|
tooltip,
|
||||||
shortLabel,
|
shortLabel,
|
||||||
toolPartIds: isToolSegment ? pending.toolPartIds : undefined,
|
partIds: pending.partIds,
|
||||||
})
|
})
|
||||||
segmentIndex += 1
|
segmentIndex += 1
|
||||||
pending = null
|
pending = null
|
||||||
@@ -199,7 +199,13 @@ export function buildTimelineSegments(
|
|||||||
const ensureSegment = (type: TimelineSegmentType): PendingSegment => {
|
const ensureSegment = (type: TimelineSegmentType): PendingSegment => {
|
||||||
if (!pending || pending.type !== type) {
|
if (!pending || pending.type !== type) {
|
||||||
flushPending()
|
flushPending()
|
||||||
pending = { type, texts: [], reasoningTexts: [], toolTitles: [], toolTypeLabels: [], toolIcons: [], toolPartIds: [], hasPrimaryText: type !== "assistant" }
|
pending = {
|
||||||
|
type,
|
||||||
|
texts: [],
|
||||||
|
reasoningTexts: [],
|
||||||
|
partIds: [],
|
||||||
|
hasPrimaryText: type !== "assistant",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return pending!
|
return pending!
|
||||||
}
|
}
|
||||||
@@ -211,14 +217,20 @@ export function buildTimelineSegments(
|
|||||||
if (!part || typeof part !== "object") continue
|
if (!part || typeof part !== "object") continue
|
||||||
|
|
||||||
if (part.type === "tool") {
|
if (part.type === "tool") {
|
||||||
const target = ensureSegment("tool")
|
flushPending()
|
||||||
const toolPart = part as ToolCallPart
|
const toolPart = part as ToolCallPart
|
||||||
target.toolTitles.push(getToolTitle(toolPart, t))
|
const partId = typeof toolPart.id === "string" ? toolPart.id : ""
|
||||||
target.toolTypeLabels.push(getToolTypeLabel(toolPart, t))
|
const title = getToolTitle(toolPart, t)
|
||||||
target.toolIcons.push(getToolIcon(typeof toolPart.tool === "string" ? toolPart.tool : "tool"))
|
result.push({
|
||||||
if (typeof toolPart.id === "string" && toolPart.id.length > 0) {
|
id: `${record.id}:${segmentIndex}`,
|
||||||
target.toolPartIds.push(toolPart.id)
|
messageId: record.id,
|
||||||
}
|
type: "tool",
|
||||||
|
label: getToolTypeLabel(toolPart, t) || segmentLabel("tool"),
|
||||||
|
tooltip: formatToolTooltip([title], t),
|
||||||
|
shortLabel: getToolIcon(typeof toolPart.tool === "string" ? toolPart.tool : "tool"),
|
||||||
|
toolPartIds: partId ? [partId] : undefined,
|
||||||
|
})
|
||||||
|
segmentIndex += 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +240,9 @@ export function buildTimelineSegments(
|
|||||||
const target = ensureSegment(defaultContentType)
|
const target = ensureSegment(defaultContentType)
|
||||||
if (target) {
|
if (target) {
|
||||||
target.reasoningTexts.push(text)
|
target.reasoningTexts.push(text)
|
||||||
|
if (typeof (part as any).id === "string" && (part as any).id.length > 0) {
|
||||||
|
target.partIds.push((part as any).id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -235,6 +250,7 @@ export function buildTimelineSegments(
|
|||||||
if (part.type === "compaction") {
|
if (part.type === "compaction") {
|
||||||
flushPending()
|
flushPending()
|
||||||
const isAuto = Boolean((part as any)?.auto)
|
const isAuto = Boolean((part as any)?.auto)
|
||||||
|
const partId = typeof (part as any)?.id === "string" ? ((part as any).id as string) : ""
|
||||||
result.push({
|
result.push({
|
||||||
id: `${record.id}:${segmentIndex}`,
|
id: `${record.id}:${segmentIndex}`,
|
||||||
messageId: record.id,
|
messageId: record.id,
|
||||||
@@ -242,6 +258,7 @@ export function buildTimelineSegments(
|
|||||||
label: segmentLabel("compaction"),
|
label: segmentLabel("compaction"),
|
||||||
tooltip: isAuto ? t("messageTimeline.tooltip.compaction.auto") : t("messageTimeline.tooltip.compaction.manual"),
|
tooltip: isAuto ? t("messageTimeline.tooltip.compaction.auto") : t("messageTimeline.tooltip.compaction.manual"),
|
||||||
variant: isAuto ? "auto" : "manual",
|
variant: isAuto ? "auto" : "manual",
|
||||||
|
partId,
|
||||||
})
|
})
|
||||||
segmentIndex += 1
|
segmentIndex += 1
|
||||||
continue
|
continue
|
||||||
@@ -257,6 +274,9 @@ export function buildTimelineSegments(
|
|||||||
if (target) {
|
if (target) {
|
||||||
target.texts.push(text)
|
target.texts.push(text)
|
||||||
target.hasPrimaryText = true
|
target.hasPrimaryText = true
|
||||||
|
if (typeof (part as any).id === "string" && (part as any).id.length > 0) {
|
||||||
|
target.partIds.push((part as any).id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +298,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
let hoverTimer: number | null = null
|
let hoverTimer: number | null = null
|
||||||
let closeTimer: number | null = null
|
let closeTimer: number | null = null
|
||||||
const showTools = () => props.showToolSegments ?? true
|
const showTools = () => props.showToolSegments ?? true
|
||||||
|
const deleteHover = () => props.deleteHover?.() ?? { kind: "none" as const }
|
||||||
|
|
||||||
const registerButtonRef = (segmentId: string, element: HTMLButtonElement | null) => {
|
const registerButtonRef = (segmentId: string, element: HTMLButtonElement | null) => {
|
||||||
if (element) {
|
if (element) {
|
||||||
@@ -350,11 +371,9 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
clearCloseTimer()
|
clearCloseTimer()
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(on(() => props.activeMessageId, (activeId) => {
|
||||||
const activeId = props.activeMessageId
|
|
||||||
|
|
||||||
if (!activeId) return
|
if (!activeId) return
|
||||||
const targetSegment = props.segments.find((segment) => segment.messageId === activeId)
|
const targetSegment = untrack(() => props.segments).find((segment) => segment.messageId === activeId)
|
||||||
if (!targetSegment) return
|
if (!targetSegment) return
|
||||||
const element = buttonRefs.get(targetSegment.id)
|
const element = buttonRefs.get(targetSegment.id)
|
||||||
if (!element) return
|
if (!element) return
|
||||||
@@ -366,7 +385,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
window.clearTimeout(timer)
|
window.clearTimeout(timer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const element = tooltipElement()
|
const element = tooltipElement()
|
||||||
@@ -398,6 +417,28 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
onCleanup(() => buttonRefs.delete(segment.id))
|
onCleanup(() => buttonRefs.delete(segment.id))
|
||||||
const isActive = () => props.activeMessageId === segment.messageId
|
const isActive = () => props.activeMessageId === segment.messageId
|
||||||
|
|
||||||
|
const isDeleteHovered = () => {
|
||||||
|
const hover = deleteHover() as DeleteHoverState
|
||||||
|
const selected = props.selectedMessageIds?.() ?? new Set<string>()
|
||||||
|
if (selected.has(segment.messageId)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (hover.kind === "message") {
|
||||||
|
return hover.messageId === segment.messageId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hover.kind === "deleteUpTo") {
|
||||||
|
const ids = store().getSessionMessageIds(props.sessionId)
|
||||||
|
const targetIndex = ids.indexOf(hover.messageId)
|
||||||
|
if (targetIndex === -1) return false
|
||||||
|
const segmentIndex = ids.indexOf(segment.messageId)
|
||||||
|
if (segmentIndex === -1) return false
|
||||||
|
return segmentIndex >= targetIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const hasActivePermission = () => {
|
const hasActivePermission = () => {
|
||||||
if (segment.type !== "tool") return false
|
if (segment.type !== "tool") return false
|
||||||
const partIds = segment.toolPartIds ?? []
|
const partIds = segment.toolPartIds ?? []
|
||||||
@@ -409,7 +450,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHidden = () => segment.type === "tool" && !(showTools() || isActive() || hasActivePermission())
|
const isHidden = () => segment.type === "tool" && !(showTools() || isActive() || hasActivePermission() || isDeleteHovered())
|
||||||
|
|
||||||
const shortLabelContent = () => {
|
const shortLabelContent = () => {
|
||||||
if (segment.type === "tool") {
|
if (segment.type === "tool") {
|
||||||
@@ -429,15 +470,17 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={(el) => registerButtonRef(segment.id, el)}
|
ref={(el) => registerButtonRef(segment.id, el)}
|
||||||
type="button"
|
type="button"
|
||||||
data-variant={segment.variant}
|
data-variant={segment.variant}
|
||||||
class={`message-timeline-segment message-timeline-${segment.type} ${hasActivePermission() ? "message-timeline-segment-permission" : ""} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
|
class={`message-timeline-segment message-timeline-${segment.type} ${hasActivePermission() ? "message-timeline-segment-permission" : ""} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
|
||||||
|
|
||||||
aria-current={isActive() ? "true" : undefined}
|
data-delete-hover={isDeleteHovered() ? "true" : undefined}
|
||||||
aria-hidden={isHidden() ? "true" : undefined}
|
|
||||||
onClick={() => props.onSegmentClick?.(segment)}
|
aria-current={isActive() ? "true" : undefined}
|
||||||
onMouseEnter={(event) => handleMouseEnter(segment, event)}
|
aria-hidden={isHidden() ? "true" : undefined}
|
||||||
|
onClick={() => props.onSegmentClick?.(segment)}
|
||||||
|
onMouseEnter={(event) => handleMouseEnter(segment, event)}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<span class="message-timeline-label message-timeline-label-full">{segment.label}</span>
|
<span class="message-timeline-label message-timeline-label-full">{segment.label}</span>
|
||||||
@@ -462,6 +505,10 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
store={store}
|
store={store}
|
||||||
|
deleteHover={props.deleteHover}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
|
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||||
|
selectedMessageIds={props.selectedMessageIds}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -351,7 +351,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const blockquote = lines.map((line) => `> ${line}`).join("\n")
|
const blockquote = lines.map((line) => `> ${line}`).join("\n")
|
||||||
if (!blockquote) return
|
if (!blockquote) return
|
||||||
|
|
||||||
insertBlockContent(`${blockquote}\n`)
|
// End the blockquote with a blank line so the user's next line
|
||||||
|
// doesn't get parsed as a lazy continuation of the quote.
|
||||||
|
insertBlockContent(`${blockquote}\n\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertCodeSelection(rawText: string) {
|
function insertCodeSelection(rawText: string) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getAttachments, removeAttachment } from "../../stores/attachments"
|
|||||||
import { instances } from "../../stores/instances"
|
import { instances } from "../../stores/instances"
|
||||||
import { loadMessages, sendMessage, forkSession, renameSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
|
import { loadMessages, sendMessage, forkSession, renameSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
|
||||||
import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-status"
|
import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-status"
|
||||||
|
import { deleteMessage } from "../../stores/session-actions"
|
||||||
import { showAlertDialog } from "../../stores/alerts"
|
import { showAlertDialog } from "../../stores/alerts"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
import { requestData } from "../../lib/opencode-api"
|
import { requestData } from "../../lib/opencode-api"
|
||||||
@@ -225,6 +226,35 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteMessagesUpTo(messageId: string) {
|
||||||
|
const ids = messageStore().getSessionMessageIds(props.sessionId)
|
||||||
|
const index = ids.indexOf(messageId)
|
||||||
|
if (index === -1) return
|
||||||
|
|
||||||
|
const restoredText = getUserMessageText(messageId)
|
||||||
|
const toDelete = ids.slice(index)
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let idx = toDelete.length - 1; idx >= 0; idx -= 1) {
|
||||||
|
await deleteMessage(props.instanceId, props.sessionId, toDelete[idx])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to delete messages up to", error)
|
||||||
|
showAlertDialog(t("sessionView.alerts.deleteUpToFailed.message"), {
|
||||||
|
title: t("sessionView.alerts.deleteUpToFailed.title"),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
if (restoredText) {
|
||||||
|
if (promptInputApi) {
|
||||||
|
promptInputApi.setPromptText(restoredText, { focus: true })
|
||||||
|
} else {
|
||||||
|
pendingPromptText = restoredText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFork(messageId?: string) {
|
async function handleFork(messageId?: string) {
|
||||||
if (!messageId) {
|
if (!messageId) {
|
||||||
log.warn("Fork requires a user message id")
|
log.warn("Fork requires a user message id")
|
||||||
@@ -283,10 +313,11 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
<MessageSection
|
<MessageSection
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={activeSession.id}
|
sessionId={activeSession.id}
|
||||||
loading={messagesLoading()}
|
loading={messagesLoading()}
|
||||||
onRevert={handleRevert}
|
onRevert={handleRevert}
|
||||||
onFork={handleFork}
|
onDeleteMessagesUpTo={handleDeleteMessagesUpTo}
|
||||||
isActive={props.isActive}
|
onFork={handleFork}
|
||||||
|
isActive={props.isActive}
|
||||||
registerScrollToBottom={(fn) => {
|
registerScrollToBottom={(fn) => {
|
||||||
scrollToBottomHandle = fn
|
scrollToBottomHandle = fn
|
||||||
if (props.isActive) {
|
if (props.isActive) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import { getRelativePath, isToolStateCompleted, isToolStateError, isToolStateRunning } from "./utils"
|
import { getRelativePath, isToolStateCompleted, isToolStateError, isToolStateRunning } from "./utils"
|
||||||
import { tGlobal } from "../../lib/i18n"
|
import { tGlobal } from "../../lib/i18n"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Accessor, JSXElement } from "solid-js"
|
import type { Accessor, JSXElement } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { TextPart } from "../../types/message"
|
import type { TextPart } from "../../types/message"
|
||||||
import { Markdown } from "../markdown"
|
import { Markdown } from "../markdown"
|
||||||
import type { MarkdownRenderOptions, ToolScrollHelpers } from "./types"
|
import type { MarkdownRenderOptions, ToolScrollHelpers } from "./types"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createMemo, Show, For, createEffect, type Accessor } from "solid-js"
|
import { createMemo, Show, For, createEffect, type Accessor } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { useI18n } from "../../lib/i18n"
|
import { useI18n } from "../../lib/i18n"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { For, Show, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
import { For, Show, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { ToolRenderer } from "../types"
|
import type { ToolRenderer } from "../types"
|
||||||
import { ensureMarkdownContent, getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils"
|
import { ensureMarkdownContent, getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils"
|
||||||
import { resolveTitleForTool } from "../tool-title"
|
import { resolveTitleForTool } from "../tool-title"
|
||||||
@@ -178,28 +178,116 @@ export const taskRenderer: ToolRenderer = {
|
|||||||
void loadMessages(instanceId, id)
|
void loadMessages(instanceId, id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const childToolKeys = createMemo(() => {
|
const [childToolKeys, setChildToolKeys] = createSignal<string[]>([])
|
||||||
const id = childSessionId()
|
|
||||||
if (!id) return [] as string[]
|
|
||||||
if (!childSessionLoaded()) return [] as string[]
|
|
||||||
|
|
||||||
// React to session changes, but do the scan untracked to avoid
|
let indexedSessionId = ""
|
||||||
// subscribing to every message/part node in the store.
|
let indexedMessageCount = 0
|
||||||
|
let indexedMessageTail = ""
|
||||||
|
const indexedPartCounts = new Map<string, number>()
|
||||||
|
|
||||||
|
function resetChildToolIndex(nextSessionId: string) {
|
||||||
|
indexedSessionId = nextSessionId
|
||||||
|
indexedMessageCount = 0
|
||||||
|
indexedMessageTail = ""
|
||||||
|
indexedPartCounts.clear()
|
||||||
|
setChildToolKeys([])
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanMessageToolParts(messageId: string, startIndex: number) {
|
||||||
|
const record = store.getMessage(messageId)
|
||||||
|
if (!record) return [] as string[]
|
||||||
|
|
||||||
|
const partIds = record.partIds
|
||||||
|
const keys: string[] = []
|
||||||
|
for (let idx = startIndex; idx < partIds.length; idx += 1) {
|
||||||
|
const partId = partIds[idx]
|
||||||
|
const entry = record.parts?.[partId]
|
||||||
|
const data = entry?.data
|
||||||
|
if (!data || (data as any).type !== "tool") continue
|
||||||
|
keys.push(`${messageId}::${partId}`)
|
||||||
|
}
|
||||||
|
indexedPartCounts.set(messageId, partIds.length)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
function fullRescanChildTools(sessionId: string, messageIds: string[]) {
|
||||||
|
indexedSessionId = sessionId
|
||||||
|
indexedMessageCount = messageIds.length
|
||||||
|
indexedMessageTail = messageIds[messageIds.length - 1] ?? ""
|
||||||
|
indexedPartCounts.clear()
|
||||||
|
|
||||||
|
const nextKeys: string[] = []
|
||||||
|
for (const messageId of messageIds) {
|
||||||
|
nextKeys.push(...scanMessageToolParts(messageId, 0))
|
||||||
|
}
|
||||||
|
setChildToolKeys(nextKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const id = childSessionId()
|
||||||
|
const loaded = childSessionLoaded()
|
||||||
|
|
||||||
|
if (!id || !loaded) {
|
||||||
|
if (indexedSessionId) {
|
||||||
|
resetChildToolIndex("")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use the session revision as the reactive change point, but avoid
|
||||||
|
// rescanning the entire session on every update.
|
||||||
store.getSessionRevision(id)
|
store.getSessionRevision(id)
|
||||||
return untrack(() => {
|
|
||||||
|
untrack(() => {
|
||||||
const messageIds = store.getSessionMessageIds(id)
|
const messageIds = store.getSessionMessageIds(id)
|
||||||
const keys: string[] = []
|
|
||||||
for (const messageId of messageIds) {
|
if (!indexedSessionId || indexedSessionId !== id) {
|
||||||
const record = store.getMessage(messageId)
|
fullRescanChildTools(id, messageIds)
|
||||||
if (!record) continue
|
return
|
||||||
for (const partId of record.partIds) {
|
}
|
||||||
const entry = record.parts?.[partId]
|
|
||||||
const data = entry?.data
|
// Detect structural changes (reorder/shrink) and fall back to a full rescan.
|
||||||
if (!data || (data as any).type !== "tool") continue
|
if (messageIds.length < indexedMessageCount) {
|
||||||
keys.push(`${messageId}::${partId}`)
|
fullRescanChildTools(id, messageIds)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (indexedMessageCount > 0) {
|
||||||
|
const expectedTailIndex = indexedMessageCount - 1
|
||||||
|
if (expectedTailIndex >= 0 && messageIds[expectedTailIndex] !== indexedMessageTail) {
|
||||||
|
fullRescanChildTools(id, messageIds)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keys
|
|
||||||
|
const appendedKeys: string[] = []
|
||||||
|
|
||||||
|
// Scan any new messages appended since last index.
|
||||||
|
for (let idx = indexedMessageCount; idx < messageIds.length; idx += 1) {
|
||||||
|
const messageId = messageIds[idx]
|
||||||
|
appendedKeys.push(...scanMessageToolParts(messageId, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan a small window of recent messages for newly appended parts.
|
||||||
|
// Deltas typically affect the most recent tool call, so this avoids
|
||||||
|
// iterating every message on every revision.
|
||||||
|
const existingCount = Math.min(indexedMessageCount, messageIds.length)
|
||||||
|
const windowStart = Math.max(0, existingCount - 3)
|
||||||
|
for (let idx = windowStart; idx < existingCount; idx += 1) {
|
||||||
|
const messageId = messageIds[idx]
|
||||||
|
const previousPartCount = indexedPartCounts.get(messageId) ?? 0
|
||||||
|
const record = store.getMessage(messageId)
|
||||||
|
const nextPartCount = record?.partIds.length ?? 0
|
||||||
|
if (nextPartCount > previousPartCount) {
|
||||||
|
appendedKeys.push(...scanMessageToolParts(messageId, previousPartCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indexedMessageCount = messageIds.length
|
||||||
|
indexedMessageTail = messageIds[messageIds.length - 1] ?? ""
|
||||||
|
|
||||||
|
if (appendedKeys.length > 0) {
|
||||||
|
setChildToolKeys((prev) => [...prev, ...appendedKeys])
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const promptContent = createMemo(() => {
|
const promptContent = createMemo(() => {
|
||||||
@@ -287,7 +375,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>
|
||||||
@@ -352,7 +442,7 @@ export const taskRenderer: ToolRenderer = {
|
|||||||
scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined
|
scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="tool-call-task-summary">
|
<div class="tool-call-task-summary">
|
||||||
<For each={childToolKeys()}>
|
<For each={childToolKeys()}>
|
||||||
{(key) => (
|
{(key) => (
|
||||||
<Show when={renderToolCall}>
|
<Show when={renderToolCall}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { For, Show } from "solid-js"
|
import { For, Show } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { ToolRenderer } from "../types"
|
import type { ToolRenderer } from "../types"
|
||||||
import { readToolStatePayload } from "../utils"
|
import { readToolStatePayload } from "../utils"
|
||||||
import { useI18n, tGlobal } from "../../../lib/i18n"
|
import { useI18n, tGlobal } from "../../../lib/i18n"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { ToolRendererContext, ToolRenderer, ToolCallPart } from "./types"
|
import type { ToolRendererContext, ToolRenderer, ToolCallPart } from "./types"
|
||||||
import { getDefaultToolAction, getToolName, isToolStateCompleted, isToolStateRunning } from "./utils"
|
import { getDefaultToolAction, getToolName, isToolStateCompleted, isToolStateRunning } from "./utils"
|
||||||
import { enMessages } from "../../lib/i18n/messages/en"
|
import { enMessages } from "../../lib/i18n/messages/en"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Accessor, JSXElement } from "solid-js"
|
import type { Accessor, JSXElement } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { ClientPart } from "../../types/message"
|
import type { ClientPart } from "../../types/message"
|
||||||
|
|
||||||
export type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
export type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { isRenderableDiffText } from "../../lib/diff-utils"
|
import { isRenderableDiffText } from "../../lib/diff-utils"
|
||||||
import { getLanguageFromPath } from "../../lib/markdown"
|
import { getLanguageFromPath } from "../../lib/markdown"
|
||||||
import type { ToolState } from "@opencode-ai/sdk"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import type { DiffPayload } from "./types"
|
import type { DiffPayload } from "./types"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
import { tGlobal } from "../../lib/i18n"
|
import { tGlobal } from "../../lib/i18n"
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
|
|
||||||
export type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning
|
export type ToolStateRunning = import("@opencode-ai/sdk/v2").ToolStateRunning
|
||||||
export type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted
|
export type ToolStateCompleted = import("@opencode-ai/sdk/v2").ToolStateCompleted
|
||||||
export type ToolStateError = import("@opencode-ai/sdk").ToolStateError
|
export type ToolStateError = import("@opencode-ai/sdk/v2").ToolStateError
|
||||||
|
|
||||||
export const diffCapableTools = new Set(["edit", "patch"])
|
export const diffCapableTools = new Set(["edit", "patch"])
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -96,11 +96,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
|
||||||
"instanceShell.rightPanel.actions.refresh": "Refresh",
|
"instanceShell.rightPanel.actions.refresh": "Refresh",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "The agent's roadmap for this session. Tracks tasks, subtasks, and their completion status.",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Background Shells",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "Background Shells",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Long-running processes started by the agent. You can monitor their output, stop, or terminate them.",
|
||||||
"instanceShell.rightPanel.sections.mcp": "MCP Servers",
|
"instanceShell.rightPanel.sections.mcp": "MCP Servers",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Model Context Protocol servers that extend the agent's capabilities with external tools and services.",
|
||||||
"instanceShell.rightPanel.sections.lsp": "LSP Servers",
|
"instanceShell.rightPanel.sections.lsp": "LSP Servers",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Language Server Protocol servers providing code intelligence, diagnostics, and language-specific features.",
|
||||||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins that customize the UI and server behavior, adding features beyond MCP and LSP.",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "Select a session to view changes.",
|
"instanceShell.sessionChanges.noSessionSelected": "Select a session to view changes.",
|
||||||
"instanceShell.sessionChanges.loading": "Fetching session changes...",
|
"instanceShell.sessionChanges.loading": "Fetching session changes...",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Go to Session",
|
"messageBlock.tool.goToSession.label": "Go to Session",
|
||||||
"messageBlock.tool.goToSession.title": "Go to session",
|
"messageBlock.tool.goToSession.title": "Go to session",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "Session not available yet",
|
"messageBlock.tool.goToSession.unavailableTitle": "Session not available yet",
|
||||||
"messageBlock.tool.deletePart.label": "Delete",
|
"messageBlock.tool.deletePart.label": "Delete Part",
|
||||||
"messageBlock.tool.deletePart.deleting": "Deleting...",
|
"messageBlock.tool.deletePart.deleting": "Deleting...",
|
||||||
"messageBlock.tool.deletePart.title": "Delete this tool call output",
|
"messageBlock.tool.deletePart.title": "Delete this tool call output",
|
||||||
"messageBlock.tool.deletePart.failed.title": "Delete failed",
|
"messageBlock.tool.deletePart.failed.title": "Delete failed",
|
||||||
@@ -71,17 +71,31 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "You",
|
"messageItem.speaker.you": "You",
|
||||||
"messageItem.speaker.assistant": "Assistant",
|
"messageItem.speaker.assistant": "Assistant",
|
||||||
"messageItem.actions.revert": "Revert",
|
"messageItem.actions.revert": "Revert",
|
||||||
"messageItem.actions.revertTitle": "Revert to this message",
|
"messageItem.actions.revertTitle": "Undo changes up to here (deletes messages)",
|
||||||
"messageItem.actions.fork": "Fork",
|
"messageItem.actions.fork": "Fork",
|
||||||
"messageItem.actions.forkTitle": "Fork from this message",
|
"messageItem.actions.forkTitle": "Fork from this message",
|
||||||
"messageItem.actions.copy": "Copy",
|
"messageItem.actions.copy": "Copy",
|
||||||
"messageItem.actions.copyTitle": "Copy message",
|
"messageItem.actions.copyTitle": "Copy message",
|
||||||
"messageItem.actions.copied": "Copied!",
|
"messageItem.actions.copied": "Copied!",
|
||||||
|
"messageItem.actions.deleteMessage": "Delete message (doesn't undo changes)",
|
||||||
|
"messageItem.actions.deleteMessagesUpTo": "Delete messages up to here (doesn't undo changes)",
|
||||||
|
"messageItem.actions.deletingMessage": "Deleting...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "Delete failed",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "Failed to delete message",
|
||||||
|
|
||||||
|
"messageItem.selection.checkboxAriaLabel": "Select message for deletion",
|
||||||
|
|
||||||
|
"messageSection.bulkDelete.toolbarAriaLabel": "Selected messages ({count})",
|
||||||
|
"messageSection.bulkDelete.deleteSelectedTitle": "Delete selected messages",
|
||||||
|
"messageSection.bulkDelete.selectAllTitle": "Select all messages",
|
||||||
|
"messageSection.bulkDelete.cancelTitle": "Cancel selection",
|
||||||
|
"messageSection.bulkDelete.failedTitle": "Delete failed",
|
||||||
|
"messageSection.bulkDelete.failedMessage": "Failed to delete selected messages",
|
||||||
"messageItem.status.queued": "QUEUED",
|
"messageItem.status.queued": "QUEUED",
|
||||||
"messageItem.status.generating": "Generating...",
|
"messageItem.status.generating": "Generating...",
|
||||||
"messageItem.status.sending": "Sending...",
|
"messageItem.status.sending": "Sending...",
|
||||||
"messageItem.status.failedToSend": "Message failed to send",
|
"messageItem.status.failedToSend": "Message failed to send",
|
||||||
"messagePart.actions.delete": "Delete",
|
"messagePart.actions.delete": "Delete Part",
|
||||||
"messagePart.actions.deleting": "Deleting...",
|
"messagePart.actions.deleting": "Deleting...",
|
||||||
"messagePart.actions.deleteTitle": "Delete this item",
|
"messagePart.actions.deleteTitle": "Delete this item",
|
||||||
"messagePart.actions.deleteFailedTitle": "Delete failed",
|
"messagePart.actions.deleteFailedTitle": "Delete failed",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const sessionMessages = {
|
|||||||
"sessionView.alerts.abortFailed.title": "Stop failed",
|
"sessionView.alerts.abortFailed.title": "Stop failed",
|
||||||
"sessionView.alerts.revertFailed.message": "Failed to revert to message",
|
"sessionView.alerts.revertFailed.message": "Failed to revert to message",
|
||||||
"sessionView.alerts.revertFailed.title": "Revert failed",
|
"sessionView.alerts.revertFailed.title": "Revert failed",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.message": "Failed to delete messages",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.title": "Delete failed",
|
||||||
"sessionView.alerts.forkFailed.message": "Failed to fork session",
|
"sessionView.alerts.forkFailed.message": "Failed to fork session",
|
||||||
"sessionView.alerts.forkFailed.title": "Fork failed",
|
"sessionView.alerts.forkFailed.title": "Fork failed",
|
||||||
"sessionView.attachments.expandPastedTextAriaLabel": "Expand pasted text",
|
"sessionView.attachments.expandPastedTextAriaLabel": "Expand pasted text",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const instanceMessages = {
|
|||||||
"instanceShell.commandPalette.openAriaLabel": "Abrir paleta de comandos",
|
"instanceShell.commandPalette.openAriaLabel": "Abrir paleta de comandos",
|
||||||
"instanceShell.commandPalette.button": "Paleta de comandos",
|
"instanceShell.commandPalette.button": "Paleta de comandos",
|
||||||
|
|
||||||
"instanceShell.connection.ariaLabel": "Connection {status}",
|
"instanceShell.connection.ariaLabel": "Conexión {status}",
|
||||||
"instanceShell.connection.connected": "Conectada",
|
"instanceShell.connection.connected": "Conectada",
|
||||||
"instanceShell.connection.connecting": "Conectando...",
|
"instanceShell.connection.connecting": "Conectando...",
|
||||||
"instanceShell.connection.disconnected": "Desconectada",
|
"instanceShell.connection.disconnected": "Desconectada",
|
||||||
@@ -93,16 +93,22 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.files": "Archivos",
|
"instanceShell.rightPanel.tabs.files": "Archivos",
|
||||||
"instanceShell.rightPanel.tabs.status": "Estado",
|
"instanceShell.rightPanel.tabs.status": "Estado",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesion",
|
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesión",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "Hoja de ruta del agente para esta sesión. Realiza el seguimiento de tareas, subtareas y su estado de finalización.",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en segundo plano",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en segundo plano",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Procesos de larga duración iniciados por el agente. Puedes supervisar su salida, detenerlos o terminarlos.",
|
||||||
"instanceShell.rightPanel.sections.mcp": "Servidores MCP",
|
"instanceShell.rightPanel.sections.mcp": "Servidores MCP",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Servidores del Model Context Protocol (MCP) que amplían las capacidades del agente con herramientas y servicios externos.",
|
||||||
"instanceShell.rightPanel.sections.lsp": "Servidores LSP",
|
"instanceShell.rightPanel.sections.lsp": "Servidores LSP",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Servidores del Language Server Protocol (LSP) que proporcionan inteligencia de código, diagnósticos y funciones específicas del lenguaje.",
|
||||||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins que personalizan el comportamiento de la UI y del servidor, y añaden funciones más allá de MCP y LSP.",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "Selecciona una sesion para ver los cambios.",
|
"instanceShell.sessionChanges.noSessionSelected": "Selecciona una sesión para ver los cambios.",
|
||||||
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesion...",
|
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesión...",
|
||||||
"instanceShell.sessionChanges.empty": "Aun no hay cambios.",
|
"instanceShell.sessionChanges.empty": "Aún no hay cambios.",
|
||||||
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
|
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
|
||||||
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",
|
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Ir a sesión",
|
"messageBlock.tool.goToSession.label": "Ir a sesión",
|
||||||
"messageBlock.tool.goToSession.title": "Ir a la sesión",
|
"messageBlock.tool.goToSession.title": "Ir a la sesión",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "La sesión aún no está disponible",
|
"messageBlock.tool.goToSession.unavailableTitle": "La sesión aún no está disponible",
|
||||||
"messageBlock.tool.deletePart.label": "Eliminar",
|
"messageBlock.tool.deletePart.label": "Eliminar parte",
|
||||||
"messageBlock.tool.deletePart.deleting": "Eliminando...",
|
"messageBlock.tool.deletePart.deleting": "Eliminando...",
|
||||||
"messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta",
|
"messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta",
|
||||||
"messageBlock.tool.deletePart.failed.title": "Error al eliminar",
|
"messageBlock.tool.deletePart.failed.title": "Error al eliminar",
|
||||||
@@ -71,17 +71,31 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "Tú",
|
"messageItem.speaker.you": "Tú",
|
||||||
"messageItem.speaker.assistant": "Asistente",
|
"messageItem.speaker.assistant": "Asistente",
|
||||||
"messageItem.actions.revert": "Revertir",
|
"messageItem.actions.revert": "Revertir",
|
||||||
"messageItem.actions.revertTitle": "Revertir a este mensaje",
|
"messageItem.actions.revertTitle": "Deshacer cambios hasta aqui (elimina mensajes)",
|
||||||
"messageItem.actions.fork": "Fork",
|
"messageItem.actions.fork": "Fork",
|
||||||
"messageItem.actions.forkTitle": "Fork desde este mensaje",
|
"messageItem.actions.forkTitle": "Fork desde este mensaje",
|
||||||
"messageItem.actions.copy": "Copiar",
|
"messageItem.actions.copy": "Copiar",
|
||||||
"messageItem.actions.copyTitle": "Copiar mensaje",
|
"messageItem.actions.copyTitle": "Copiar mensaje",
|
||||||
"messageItem.actions.copied": "¡Copiado!",
|
"messageItem.actions.copied": "¡Copiado!",
|
||||||
|
"messageItem.actions.deleteMessage": "Eliminar mensaje (no deshace cambios)",
|
||||||
|
"messageItem.actions.deleteMessagesUpTo": "Eliminar mensajes hasta aqui (no deshace cambios)",
|
||||||
|
"messageItem.actions.deletingMessage": "Eliminando...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "Error al eliminar",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "No se pudo eliminar el mensaje",
|
||||||
|
|
||||||
|
"messageItem.selection.checkboxAriaLabel": "Seleccionar mensaje para eliminar",
|
||||||
|
|
||||||
|
"messageSection.bulkDelete.toolbarAriaLabel": "Mensajes seleccionados ({count})",
|
||||||
|
"messageSection.bulkDelete.deleteSelectedTitle": "Eliminar mensajes seleccionados",
|
||||||
|
"messageSection.bulkDelete.selectAllTitle": "Seleccionar todos los mensajes",
|
||||||
|
"messageSection.bulkDelete.cancelTitle": "Cancelar selección",
|
||||||
|
"messageSection.bulkDelete.failedTitle": "Error al eliminar",
|
||||||
|
"messageSection.bulkDelete.failedMessage": "No se pudieron eliminar los mensajes seleccionados",
|
||||||
"messageItem.status.queued": "EN COLA",
|
"messageItem.status.queued": "EN COLA",
|
||||||
"messageItem.status.generating": "Generando...",
|
"messageItem.status.generating": "Generando...",
|
||||||
"messageItem.status.sending": "Enviando...",
|
"messageItem.status.sending": "Enviando...",
|
||||||
"messageItem.status.failedToSend": "No se pudo enviar el mensaje",
|
"messageItem.status.failedToSend": "No se pudo enviar el mensaje",
|
||||||
"messagePart.actions.delete": "Eliminar",
|
"messagePart.actions.delete": "Eliminar parte",
|
||||||
"messagePart.actions.deleting": "Eliminando...",
|
"messagePart.actions.deleting": "Eliminando...",
|
||||||
"messagePart.actions.deleteTitle": "Eliminar este elemento",
|
"messagePart.actions.deleteTitle": "Eliminar este elemento",
|
||||||
"messagePart.actions.deleteFailedTitle": "Error al eliminar",
|
"messagePart.actions.deleteFailedTitle": "Error al eliminar",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const sessionMessages = {
|
|||||||
"sessionView.alerts.abortFailed.title": "No se pudo detener",
|
"sessionView.alerts.abortFailed.title": "No se pudo detener",
|
||||||
"sessionView.alerts.revertFailed.message": "No se pudo revertir al mensaje",
|
"sessionView.alerts.revertFailed.message": "No se pudo revertir al mensaje",
|
||||||
"sessionView.alerts.revertFailed.title": "No se pudo revertir",
|
"sessionView.alerts.revertFailed.title": "No se pudo revertir",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.message": "No se pudieron eliminar los mensajes",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.title": "Error al eliminar",
|
||||||
"sessionView.alerts.forkFailed.message": "No se pudo hacer fork de la sesión",
|
"sessionView.alerts.forkFailed.message": "No se pudo hacer fork de la sesión",
|
||||||
"sessionView.alerts.forkFailed.title": "No se pudo hacer fork",
|
"sessionView.alerts.forkFailed.title": "No se pudo hacer fork",
|
||||||
"sessionView.attachments.expandPastedTextAriaLabel": "Expandir texto pegado",
|
"sessionView.attachments.expandPastedTextAriaLabel": "Expandir texto pegado",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "Statut",
|
"instanceShell.rightPanel.tabs.status": "Statut",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "Feuille de route de l'agent pour cette session. Suit les tâches et leur statut d'achèvement.",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en arrière-plan",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en arrière-plan",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Processus longs démarrés par l'agent. Vous pouvez surveiller leur sortie, les arrêter ou les terminer.",
|
||||||
"instanceShell.rightPanel.sections.mcp": "Serveurs MCP",
|
"instanceShell.rightPanel.sections.mcp": "Serveurs MCP",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Serveurs du protocole Model Context Protocol qui étendent les capacités de l'agent avec des outils externes.",
|
||||||
"instanceShell.rightPanel.sections.lsp": "Serveurs LSP",
|
"instanceShell.rightPanel.sections.lsp": "Serveurs LSP",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Serveurs du protocole Language Server Protocol fournissant l'intelligence de code et les diagnostics.",
|
||||||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins qui personnalisent le comportement de l'UI et du serveur, ajoutant des fonctionnalités au-delà de MCP et LSP.",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "Sélectionnez une session pour voir les changements.",
|
"instanceShell.sessionChanges.noSessionSelected": "Sélectionnez une session pour voir les changements.",
|
||||||
"instanceShell.sessionChanges.loading": "Récupération des changements...",
|
"instanceShell.sessionChanges.loading": "Récupération des changements...",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Aller à la session",
|
"messageBlock.tool.goToSession.label": "Aller à la session",
|
||||||
"messageBlock.tool.goToSession.title": "Aller à la session",
|
"messageBlock.tool.goToSession.title": "Aller à la session",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "Session pas encore disponible",
|
"messageBlock.tool.goToSession.unavailableTitle": "Session pas encore disponible",
|
||||||
"messageBlock.tool.deletePart.label": "Supprimer",
|
"messageBlock.tool.deletePart.label": "Supprimer la partie",
|
||||||
"messageBlock.tool.deletePart.deleting": "Suppression...",
|
"messageBlock.tool.deletePart.deleting": "Suppression...",
|
||||||
"messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil",
|
"messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil",
|
||||||
"messageBlock.tool.deletePart.failed.title": "Échec de suppression",
|
"messageBlock.tool.deletePart.failed.title": "Échec de suppression",
|
||||||
@@ -71,17 +71,31 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "Vous",
|
"messageItem.speaker.you": "Vous",
|
||||||
"messageItem.speaker.assistant": "Assistant",
|
"messageItem.speaker.assistant": "Assistant",
|
||||||
"messageItem.actions.revert": "Revenir",
|
"messageItem.actions.revert": "Revenir",
|
||||||
"messageItem.actions.revertTitle": "Revenir à ce message",
|
"messageItem.actions.revertTitle": "Annuler les changements jusqu'ici (supprime les messages)",
|
||||||
"messageItem.actions.fork": "Fork",
|
"messageItem.actions.fork": "Fork",
|
||||||
"messageItem.actions.forkTitle": "Fork depuis ce message",
|
"messageItem.actions.forkTitle": "Fork depuis ce message",
|
||||||
"messageItem.actions.copy": "Copier",
|
"messageItem.actions.copy": "Copier",
|
||||||
"messageItem.actions.copyTitle": "Copier le message",
|
"messageItem.actions.copyTitle": "Copier le message",
|
||||||
"messageItem.actions.copied": "Copié !",
|
"messageItem.actions.copied": "Copié !",
|
||||||
|
"messageItem.actions.deleteMessage": "Supprimer le message (sans annuler les changements)",
|
||||||
|
"messageItem.actions.deleteMessagesUpTo": "Supprimer les messages jusqu'ici (sans annuler les changements)",
|
||||||
|
"messageItem.actions.deletingMessage": "Suppression...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "Échec de suppression",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "Impossible de supprimer le message",
|
||||||
|
|
||||||
|
"messageItem.selection.checkboxAriaLabel": "Sélectionner le message pour suppression",
|
||||||
|
|
||||||
|
"messageSection.bulkDelete.toolbarAriaLabel": "Messages sélectionnés ({count})",
|
||||||
|
"messageSection.bulkDelete.deleteSelectedTitle": "Supprimer les messages sélectionnés",
|
||||||
|
"messageSection.bulkDelete.selectAllTitle": "Tout sélectionner",
|
||||||
|
"messageSection.bulkDelete.cancelTitle": "Annuler la sélection",
|
||||||
|
"messageSection.bulkDelete.failedTitle": "Échec de suppression",
|
||||||
|
"messageSection.bulkDelete.failedMessage": "Impossible de supprimer les messages sélectionnés",
|
||||||
"messageItem.status.queued": "EN FILE",
|
"messageItem.status.queued": "EN FILE",
|
||||||
"messageItem.status.generating": "Génération...",
|
"messageItem.status.generating": "Génération...",
|
||||||
"messageItem.status.sending": "Envoi...",
|
"messageItem.status.sending": "Envoi...",
|
||||||
"messageItem.status.failedToSend": "Échec de l'envoi du message",
|
"messageItem.status.failedToSend": "Échec de l'envoi du message",
|
||||||
"messagePart.actions.delete": "Supprimer",
|
"messagePart.actions.delete": "Supprimer la partie",
|
||||||
"messagePart.actions.deleting": "Suppression...",
|
"messagePart.actions.deleting": "Suppression...",
|
||||||
"messagePart.actions.deleteTitle": "Supprimer cet élément",
|
"messagePart.actions.deleteTitle": "Supprimer cet élément",
|
||||||
"messagePart.actions.deleteFailedTitle": "Échec de suppression",
|
"messagePart.actions.deleteFailedTitle": "Échec de suppression",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const sessionMessages = {
|
|||||||
"sessionView.alerts.abortFailed.title": "Échec de l'arrêt",
|
"sessionView.alerts.abortFailed.title": "Échec de l'arrêt",
|
||||||
"sessionView.alerts.revertFailed.message": "Impossible de revenir au message",
|
"sessionView.alerts.revertFailed.message": "Impossible de revenir au message",
|
||||||
"sessionView.alerts.revertFailed.title": "Échec du retour",
|
"sessionView.alerts.revertFailed.title": "Échec du retour",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.message": "Impossible de supprimer les messages",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.title": "Échec de suppression",
|
||||||
"sessionView.alerts.forkFailed.message": "Impossible de forker la session",
|
"sessionView.alerts.forkFailed.message": "Impossible de forker la session",
|
||||||
"sessionView.alerts.forkFailed.title": "Échec du fork",
|
"sessionView.alerts.forkFailed.title": "Échec du fork",
|
||||||
"sessionView.attachments.expandPastedTextAriaLabel": "Développer le texte collé",
|
"sessionView.attachments.expandPastedTextAriaLabel": "Développer le texte collé",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "ステータス",
|
"instanceShell.rightPanel.tabs.status": "ステータス",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
|
||||||
"instanceShell.rightPanel.sections.plan": "計画",
|
"instanceShell.rightPanel.sections.plan": "計画",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "このセッションにおけるエージェントのロードマップ。タスクやサブタスク、および完了状況を追跡します。",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "バックグラウンドシェル",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "バックグラウンドシェル",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "エージェントが開始した長時間実行プロセス。出力を監視し、停止または終了できます。",
|
||||||
"instanceShell.rightPanel.sections.mcp": "MCP サーバー",
|
"instanceShell.rightPanel.sections.mcp": "MCP サーバー",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Model Context Protocol (MCP) サーバー。外部ツールやサービスでエージェントの機能を拡張します。",
|
||||||
"instanceShell.rightPanel.sections.lsp": "LSP サーバー",
|
"instanceShell.rightPanel.sections.lsp": "LSP サーバー",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Language Server Protocolサーバーがコードインテリジェンス、診断、言語固有の機能を提供します。",
|
||||||
"instanceShell.rightPanel.sections.plugins": "プラグイン",
|
"instanceShell.rightPanel.sections.plugins": "プラグイン",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "UI とサーバーの動作をカスタマイズし、MCP や LSP 以外の機能も追加できるプラグイン。",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "変更を表示するにはセッションを選択してください。",
|
"instanceShell.sessionChanges.noSessionSelected": "変更を表示するにはセッションを選択してください。",
|
||||||
"instanceShell.sessionChanges.loading": "変更を取得中...",
|
"instanceShell.sessionChanges.loading": "変更を取得中...",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "セッションへ移動",
|
"messageBlock.tool.goToSession.label": "セッションへ移動",
|
||||||
"messageBlock.tool.goToSession.title": "セッションへ移動",
|
"messageBlock.tool.goToSession.title": "セッションへ移動",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません",
|
"messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません",
|
||||||
"messageBlock.tool.deletePart.label": "削除",
|
"messageBlock.tool.deletePart.label": "パートを削除",
|
||||||
"messageBlock.tool.deletePart.deleting": "削除中...",
|
"messageBlock.tool.deletePart.deleting": "削除中...",
|
||||||
"messageBlock.tool.deletePart.title": "このツール出力を削除",
|
"messageBlock.tool.deletePart.title": "このツール出力を削除",
|
||||||
"messageBlock.tool.deletePart.failed.title": "削除に失敗しました",
|
"messageBlock.tool.deletePart.failed.title": "削除に失敗しました",
|
||||||
@@ -71,17 +71,31 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "あなた",
|
"messageItem.speaker.you": "あなた",
|
||||||
"messageItem.speaker.assistant": "アシスタント",
|
"messageItem.speaker.assistant": "アシスタント",
|
||||||
"messageItem.actions.revert": "戻す",
|
"messageItem.actions.revert": "戻す",
|
||||||
"messageItem.actions.revertTitle": "このメッセージまで戻す",
|
"messageItem.actions.revertTitle": "ここまでの変更を元に戻す(メッセージを削除)",
|
||||||
"messageItem.actions.fork": "フォーク",
|
"messageItem.actions.fork": "フォーク",
|
||||||
"messageItem.actions.forkTitle": "このメッセージからフォーク",
|
"messageItem.actions.forkTitle": "このメッセージからフォーク",
|
||||||
"messageItem.actions.copy": "コピー",
|
"messageItem.actions.copy": "コピー",
|
||||||
"messageItem.actions.copyTitle": "メッセージをコピー",
|
"messageItem.actions.copyTitle": "メッセージをコピー",
|
||||||
"messageItem.actions.copied": "コピーしました!",
|
"messageItem.actions.copied": "コピーしました!",
|
||||||
|
"messageItem.actions.deleteMessage": "メッセージを削除(変更は元に戻さない)",
|
||||||
|
"messageItem.actions.deleteMessagesUpTo": "ここまでのメッセージを削除(変更は元に戻さない)",
|
||||||
|
"messageItem.actions.deletingMessage": "削除中...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "削除に失敗しました",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "メッセージの削除に失敗しました",
|
||||||
|
|
||||||
|
"messageItem.selection.checkboxAriaLabel": "削除するメッセージを選択",
|
||||||
|
|
||||||
|
"messageSection.bulkDelete.toolbarAriaLabel": "選択したメッセージ({count})",
|
||||||
|
"messageSection.bulkDelete.deleteSelectedTitle": "選択したメッセージを削除",
|
||||||
|
"messageSection.bulkDelete.selectAllTitle": "すべて選択",
|
||||||
|
"messageSection.bulkDelete.cancelTitle": "選択をキャンセル",
|
||||||
|
"messageSection.bulkDelete.failedTitle": "削除に失敗しました",
|
||||||
|
"messageSection.bulkDelete.failedMessage": "選択したメッセージの削除に失敗しました",
|
||||||
"messageItem.status.queued": "待機中",
|
"messageItem.status.queued": "待機中",
|
||||||
"messageItem.status.generating": "生成中...",
|
"messageItem.status.generating": "生成中...",
|
||||||
"messageItem.status.sending": "送信中...",
|
"messageItem.status.sending": "送信中...",
|
||||||
"messageItem.status.failedToSend": "メッセージの送信に失敗しました",
|
"messageItem.status.failedToSend": "メッセージの送信に失敗しました",
|
||||||
"messagePart.actions.delete": "削除",
|
"messagePart.actions.delete": "パートを削除",
|
||||||
"messagePart.actions.deleting": "削除中...",
|
"messagePart.actions.deleting": "削除中...",
|
||||||
"messagePart.actions.deleteTitle": "この項目を削除",
|
"messagePart.actions.deleteTitle": "この項目を削除",
|
||||||
"messagePart.actions.deleteFailedTitle": "削除に失敗しました",
|
"messagePart.actions.deleteFailedTitle": "削除に失敗しました",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const sessionMessages = {
|
|||||||
"sessionView.alerts.abortFailed.title": "停止に失敗",
|
"sessionView.alerts.abortFailed.title": "停止に失敗",
|
||||||
"sessionView.alerts.revertFailed.message": "メッセージへ戻せませんでした",
|
"sessionView.alerts.revertFailed.message": "メッセージへ戻せませんでした",
|
||||||
"sessionView.alerts.revertFailed.title": "復元に失敗",
|
"sessionView.alerts.revertFailed.title": "復元に失敗",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.message": "メッセージの削除に失敗しました",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.title": "削除に失敗しました",
|
||||||
"sessionView.alerts.forkFailed.message": "セッションのフォークに失敗しました",
|
"sessionView.alerts.forkFailed.message": "セッションのフォークに失敗しました",
|
||||||
"sessionView.alerts.forkFailed.title": "フォークに失敗",
|
"sessionView.alerts.forkFailed.title": "フォークに失敗",
|
||||||
"sessionView.attachments.expandPastedTextAriaLabel": "貼り付けたテキストを展開",
|
"sessionView.attachments.expandPastedTextAriaLabel": "貼り付けたテキストを展開",
|
||||||
|
|||||||
@@ -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, статистика",
|
||||||
|
|||||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "Статус",
|
"instanceShell.rightPanel.tabs.status": "Статус",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
|
||||||
"instanceShell.rightPanel.sections.plan": "План",
|
"instanceShell.rightPanel.sections.plan": "План",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые Shell",
|
"instanceShell.rightPanel.sections.plan.tooltip": "Дорожная карта агента для этой сессии. Отслеживает задачи и их статус выполнения.",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые оболочки",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Долгоработающие процессы, запущенные агентом. Вы можете следить за их выводом, останавливать или завершать их.",
|
||||||
"instanceShell.rightPanel.sections.mcp": "MCP-серверы",
|
"instanceShell.rightPanel.sections.mcp": "MCP-серверы",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "Серверы протокола Model Context Protocol, расширяющие возможности агента внешними инструментами.",
|
||||||
"instanceShell.rightPanel.sections.lsp": "LSP-серверы",
|
"instanceShell.rightPanel.sections.lsp": "LSP-серверы",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "Серверы протокола Language Server Protocol, обеспечивающие интеллектуальную поддержку кода и диагностику.",
|
||||||
"instanceShell.rightPanel.sections.plugins": "Плагины",
|
"instanceShell.rightPanel.sections.plugins": "Плагины",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "Плагины, настраивающие поведение интерфейса и сервера, добавляющие функции поверх MCP и LSP.",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "Выберите сессию, чтобы просмотреть изменения.",
|
"instanceShell.sessionChanges.noSessionSelected": "Выберите сессию, чтобы просмотреть изменения.",
|
||||||
"instanceShell.sessionChanges.loading": "Загрузка изменений...",
|
"instanceShell.sessionChanges.loading": "Загрузка изменений...",
|
||||||
@@ -128,7 +134,7 @@ export const instanceMessages = {
|
|||||||
"versionPill.uiWithVersion": "UI {version}",
|
"versionPill.uiWithVersion": "UI {version}",
|
||||||
"versionPill.source": " ({source})",
|
"versionPill.source": " ({source})",
|
||||||
|
|
||||||
"opencodeBinarySelector.title": "OpenCode Binary",
|
"opencodeBinarySelector.title": "Бинарник OpenCode",
|
||||||
"opencodeBinarySelector.subtitle": "Выберите, какой исполняемый файл OpenCode запускать",
|
"opencodeBinarySelector.subtitle": "Выберите, какой исполняемый файл OpenCode запускать",
|
||||||
"opencodeBinarySelector.customPath.placeholder": "Введите путь к бинарнику opencode…",
|
"opencodeBinarySelector.customPath.placeholder": "Введите путь к бинарнику opencode…",
|
||||||
"opencodeBinarySelector.actions.add": "Добавить",
|
"opencodeBinarySelector.actions.add": "Добавить",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "Перейти к сессии",
|
"messageBlock.tool.goToSession.label": "Перейти к сессии",
|
||||||
"messageBlock.tool.goToSession.title": "Перейти к сессии",
|
"messageBlock.tool.goToSession.title": "Перейти к сессии",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна",
|
"messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна",
|
||||||
"messageBlock.tool.deletePart.label": "Удалить",
|
"messageBlock.tool.deletePart.label": "Удалить часть",
|
||||||
"messageBlock.tool.deletePart.deleting": "Удаление...",
|
"messageBlock.tool.deletePart.deleting": "Удаление...",
|
||||||
"messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента",
|
"messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента",
|
||||||
"messageBlock.tool.deletePart.failed.title": "Ошибка удаления",
|
"messageBlock.tool.deletePart.failed.title": "Ошибка удаления",
|
||||||
@@ -71,17 +71,31 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "Вы",
|
"messageItem.speaker.you": "Вы",
|
||||||
"messageItem.speaker.assistant": "Ассистент",
|
"messageItem.speaker.assistant": "Ассистент",
|
||||||
"messageItem.actions.revert": "Откатить",
|
"messageItem.actions.revert": "Откатить",
|
||||||
"messageItem.actions.revertTitle": "Откатиться к этому сообщению",
|
"messageItem.actions.revertTitle": "Отменить изменения до этого места (удалит сообщения)",
|
||||||
"messageItem.actions.fork": "Форк",
|
"messageItem.actions.fork": "Форк",
|
||||||
"messageItem.actions.forkTitle": "Форкнуть от этого сообщения",
|
"messageItem.actions.forkTitle": "Форкнуть от этого сообщения",
|
||||||
"messageItem.actions.copy": "Копировать",
|
"messageItem.actions.copy": "Копировать",
|
||||||
"messageItem.actions.copyTitle": "Копировать сообщение",
|
"messageItem.actions.copyTitle": "Копировать сообщение",
|
||||||
"messageItem.actions.copied": "Скопировано!",
|
"messageItem.actions.copied": "Скопировано!",
|
||||||
|
"messageItem.actions.deleteMessage": "Удалить сообщение (без отката изменений)",
|
||||||
|
"messageItem.actions.deleteMessagesUpTo": "Удалить сообщения до этого места (без отката изменений)",
|
||||||
|
"messageItem.actions.deletingMessage": "Удаление...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "Ошибка удаления",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "Не удалось удалить сообщение",
|
||||||
|
|
||||||
|
"messageItem.selection.checkboxAriaLabel": "Выбрать сообщение для удаления",
|
||||||
|
|
||||||
|
"messageSection.bulkDelete.toolbarAriaLabel": "Выбранные сообщения ({count})",
|
||||||
|
"messageSection.bulkDelete.deleteSelectedTitle": "Удалить выбранные сообщения",
|
||||||
|
"messageSection.bulkDelete.selectAllTitle": "Выбрать все сообщения",
|
||||||
|
"messageSection.bulkDelete.cancelTitle": "Отменить выбор",
|
||||||
|
"messageSection.bulkDelete.failedTitle": "Ошибка удаления",
|
||||||
|
"messageSection.bulkDelete.failedMessage": "Не удалось удалить выбранные сообщения",
|
||||||
"messageItem.status.queued": "В ОЧЕРЕДИ",
|
"messageItem.status.queued": "В ОЧЕРЕДИ",
|
||||||
"messageItem.status.generating": "Генерация…",
|
"messageItem.status.generating": "Генерация…",
|
||||||
"messageItem.status.sending": "Отправка…",
|
"messageItem.status.sending": "Отправка…",
|
||||||
"messageItem.status.failedToSend": "Не удалось отправить сообщение",
|
"messageItem.status.failedToSend": "Не удалось отправить сообщение",
|
||||||
"messagePart.actions.delete": "Удалить",
|
"messagePart.actions.delete": "Удалить часть",
|
||||||
"messagePart.actions.deleting": "Удаление...",
|
"messagePart.actions.deleting": "Удаление...",
|
||||||
"messagePart.actions.deleteTitle": "Удалить этот элемент",
|
"messagePart.actions.deleteTitle": "Удалить этот элемент",
|
||||||
"messagePart.actions.deleteFailedTitle": "Ошибка удаления",
|
"messagePart.actions.deleteFailedTitle": "Ошибка удаления",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const sessionMessages = {
|
|||||||
"sessionView.alerts.abortFailed.title": "Не удалось остановить",
|
"sessionView.alerts.abortFailed.title": "Не удалось остановить",
|
||||||
"sessionView.alerts.revertFailed.message": "Не удалось откатиться к сообщению",
|
"sessionView.alerts.revertFailed.message": "Не удалось откатиться к сообщению",
|
||||||
"sessionView.alerts.revertFailed.title": "Не удалось откатиться",
|
"sessionView.alerts.revertFailed.title": "Не удалось откатиться",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.message": "Не удалось удалить сообщения",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.title": "Ошибка удаления",
|
||||||
"sessionView.alerts.forkFailed.message": "Не удалось форкнуть сессию",
|
"sessionView.alerts.forkFailed.message": "Не удалось форкнуть сессию",
|
||||||
"sessionView.alerts.forkFailed.title": "Не удалось форкнуть",
|
"sessionView.alerts.forkFailed.title": "Не удалось форкнуть",
|
||||||
"sessionView.attachments.expandPastedTextAriaLabel": "Развернуть вставленный текст",
|
"sessionView.attachments.expandPastedTextAriaLabel": "Развернуть вставленный текст",
|
||||||
|
|||||||
@@ -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, 令牌, 用量, 费用, 统计",
|
||||||
|
|||||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "状态",
|
"instanceShell.rightPanel.tabs.status": "状态",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
||||||
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
|
||||||
"instanceShell.rightPanel.sections.plan": "计划",
|
"instanceShell.rightPanel.sections.plan": "计划",
|
||||||
|
"instanceShell.rightPanel.sections.plan.tooltip": "代理的路线图。跟踪任务、子任务及其完成状态。",
|
||||||
"instanceShell.rightPanel.sections.backgroundProcesses": "后台 Shell",
|
"instanceShell.rightPanel.sections.backgroundProcesses": "后台 Shell",
|
||||||
|
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "代理启动的后台进程。您可以监控其输出、停止或终止它们。",
|
||||||
"instanceShell.rightPanel.sections.mcp": "MCP 服务器",
|
"instanceShell.rightPanel.sections.mcp": "MCP 服务器",
|
||||||
|
"instanceShell.rightPanel.sections.mcp.tooltip": "模型上下文协议服务器,使用外部工具和服务扩展代理能力。",
|
||||||
"instanceShell.rightPanel.sections.lsp": "LSP 服务器",
|
"instanceShell.rightPanel.sections.lsp": "LSP 服务器",
|
||||||
|
"instanceShell.rightPanel.sections.lsp.tooltip": "语言服务器协议服务器,提供代码智能、诊断和语言特定的功能。",
|
||||||
"instanceShell.rightPanel.sections.plugins": "插件",
|
"instanceShell.rightPanel.sections.plugins": "插件",
|
||||||
|
"instanceShell.rightPanel.sections.plugins.tooltip": "自定义 UI 和服务器行为的插件,添加超出 MCP 和 LSP 的功能。",
|
||||||
|
|
||||||
"instanceShell.sessionChanges.noSessionSelected": "选择会话以查看更改。",
|
"instanceShell.sessionChanges.noSessionSelected": "选择会话以查看更改。",
|
||||||
"instanceShell.sessionChanges.loading": "正在获取会话更改...",
|
"instanceShell.sessionChanges.loading": "正在获取会话更改...",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const messagingMessages = {
|
|||||||
"messageBlock.tool.goToSession.label": "前往会话",
|
"messageBlock.tool.goToSession.label": "前往会话",
|
||||||
"messageBlock.tool.goToSession.title": "前往会话",
|
"messageBlock.tool.goToSession.title": "前往会话",
|
||||||
"messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用",
|
"messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用",
|
||||||
"messageBlock.tool.deletePart.label": "删除",
|
"messageBlock.tool.deletePart.label": "删除部分",
|
||||||
"messageBlock.tool.deletePart.deleting": "正在删除...",
|
"messageBlock.tool.deletePart.deleting": "正在删除...",
|
||||||
"messageBlock.tool.deletePart.title": "删除此工具输出",
|
"messageBlock.tool.deletePart.title": "删除此工具输出",
|
||||||
"messageBlock.tool.deletePart.failed.title": "删除失败",
|
"messageBlock.tool.deletePart.failed.title": "删除失败",
|
||||||
@@ -71,17 +71,31 @@ export const messagingMessages = {
|
|||||||
"messageItem.speaker.you": "你",
|
"messageItem.speaker.you": "你",
|
||||||
"messageItem.speaker.assistant": "助手",
|
"messageItem.speaker.assistant": "助手",
|
||||||
"messageItem.actions.revert": "回退",
|
"messageItem.actions.revert": "回退",
|
||||||
"messageItem.actions.revertTitle": "回退到这条消息",
|
"messageItem.actions.revertTitle": "撤销到此处的更改(会删除消息)",
|
||||||
"messageItem.actions.fork": "分叉",
|
"messageItem.actions.fork": "分叉",
|
||||||
"messageItem.actions.forkTitle": "从这条消息分叉",
|
"messageItem.actions.forkTitle": "从这条消息分叉",
|
||||||
"messageItem.actions.copy": "复制",
|
"messageItem.actions.copy": "复制",
|
||||||
"messageItem.actions.copyTitle": "复制消息",
|
"messageItem.actions.copyTitle": "复制消息",
|
||||||
"messageItem.actions.copied": "已复制!",
|
"messageItem.actions.copied": "已复制!",
|
||||||
|
"messageItem.actions.deleteMessage": "删除消息(不会撤销更改)",
|
||||||
|
"messageItem.actions.deleteMessagesUpTo": "删除到此处的消息(不会撤销更改)",
|
||||||
|
"messageItem.actions.deletingMessage": "正在删除...",
|
||||||
|
"messageItem.actions.deleteMessageFailedTitle": "删除失败",
|
||||||
|
"messageItem.actions.deleteMessageFailedMessage": "无法删除消息",
|
||||||
|
|
||||||
|
"messageItem.selection.checkboxAriaLabel": "选择要删除的消息",
|
||||||
|
|
||||||
|
"messageSection.bulkDelete.toolbarAriaLabel": "已选择的消息({count})",
|
||||||
|
"messageSection.bulkDelete.deleteSelectedTitle": "删除已选择的消息",
|
||||||
|
"messageSection.bulkDelete.selectAllTitle": "全选消息",
|
||||||
|
"messageSection.bulkDelete.cancelTitle": "取消选择",
|
||||||
|
"messageSection.bulkDelete.failedTitle": "删除失败",
|
||||||
|
"messageSection.bulkDelete.failedMessage": "无法删除已选择的消息",
|
||||||
"messageItem.status.queued": "排队中",
|
"messageItem.status.queued": "排队中",
|
||||||
"messageItem.status.generating": "正在生成...",
|
"messageItem.status.generating": "正在生成...",
|
||||||
"messageItem.status.sending": "正在发送...",
|
"messageItem.status.sending": "正在发送...",
|
||||||
"messageItem.status.failedToSend": "消息发送失败",
|
"messageItem.status.failedToSend": "消息发送失败",
|
||||||
"messagePart.actions.delete": "删除",
|
"messagePart.actions.delete": "删除部分",
|
||||||
"messagePart.actions.deleting": "正在删除...",
|
"messagePart.actions.deleting": "正在删除...",
|
||||||
"messagePart.actions.deleteTitle": "删除此项",
|
"messagePart.actions.deleteTitle": "删除此项",
|
||||||
"messagePart.actions.deleteFailedTitle": "删除失败",
|
"messagePart.actions.deleteFailedTitle": "删除失败",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const sessionMessages = {
|
|||||||
"sessionView.alerts.abortFailed.title": "停止失败",
|
"sessionView.alerts.abortFailed.title": "停止失败",
|
||||||
"sessionView.alerts.revertFailed.message": "回退到消息失败",
|
"sessionView.alerts.revertFailed.message": "回退到消息失败",
|
||||||
"sessionView.alerts.revertFailed.title": "回退失败",
|
"sessionView.alerts.revertFailed.title": "回退失败",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.message": "无法删除消息",
|
||||||
|
"sessionView.alerts.deleteUpToFailed.title": "删除失败",
|
||||||
"sessionView.alerts.forkFailed.message": "分叉会话失败",
|
"sessionView.alerts.forkFailed.message": "分叉会话失败",
|
||||||
"sessionView.alerts.forkFailed.title": "分叉失败",
|
"sessionView.alerts.forkFailed.title": "分叉失败",
|
||||||
"sessionView.attachments.expandPastedTextAriaLabel": "展开粘贴的文本",
|
"sessionView.attachments.expandPastedTextAriaLabel": "展开粘贴的文本",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -127,17 +127,23 @@ async function ensureLanguages(content: string) {
|
|||||||
if (highlightSuppressed) {
|
if (highlightSuppressed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Parse code fences to extract language tokens
|
|
||||||
// Updated regex to capture optional language tokens and handle trailing annotations
|
|
||||||
const codeBlockRegex = /```[ \t]*([A-Za-z0-9_.+#-]+)?[^`]*?```/g
|
|
||||||
const foundLanguages = new Set<string>()
|
|
||||||
let match
|
|
||||||
|
|
||||||
while ((match = codeBlockRegex.exec(content)) !== null) {
|
// Extract code-fence language tokens via `marked` so we correctly handle code blocks
|
||||||
const langToken = match[1]
|
// that contain backticks (e.g. JS template literals). Regex-based fence scans tend
|
||||||
if (langToken && langToken.trim()) {
|
// to miss these and prevent languages from loading.
|
||||||
foundLanguages.add(langToken.trim())
|
const foundLanguages = new Set<string>()
|
||||||
}
|
try {
|
||||||
|
const tokens = marked.lexer(content) as any
|
||||||
|
marked.walkTokens(tokens, (token: any) => {
|
||||||
|
if (token?.type !== "code") return
|
||||||
|
const langToken = typeof token.lang === "string" ? token.lang : ""
|
||||||
|
if (langToken.trim()) {
|
||||||
|
foundLanguages.add(langToken.trim())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// If tokenization fails for any reason, skip language preloading.
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue language loading tasks
|
// Queue language loading tasks
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -117,6 +117,7 @@ export function applyPartDeltaV2(
|
|||||||
partId: input.partId,
|
partId: input.partId,
|
||||||
field: input.field,
|
field: input.field,
|
||||||
delta: input.delta,
|
delta: input.delta,
|
||||||
|
bumpSessionRevision: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,14 @@ export interface InstanceMessageStore {
|
|||||||
hydrateMessages: (sessionId: string, inputs: MessageUpsertInput[], infos?: Iterable<MessageInfo>) => void
|
hydrateMessages: (sessionId: string, inputs: MessageUpsertInput[], infos?: Iterable<MessageInfo>) => void
|
||||||
upsertMessage: (input: MessageUpsertInput) => void
|
upsertMessage: (input: MessageUpsertInput) => void
|
||||||
applyPartUpdate: (input: PartUpdateInput) => void
|
applyPartUpdate: (input: PartUpdateInput) => void
|
||||||
applyPartDelta: (input: { messageId: string; partId: string; field: string; delta: string; bumpRevision?: boolean }) => void
|
applyPartDelta: (input: {
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
|
field: string
|
||||||
|
delta: string
|
||||||
|
bumpRevision?: boolean
|
||||||
|
bumpSessionRevision: boolean
|
||||||
|
}) => void
|
||||||
removeMessage: (messageId: string) => void
|
removeMessage: (messageId: string) => void
|
||||||
removeMessagePart: (messageId: string, partId: string) => void
|
removeMessagePart: (messageId: string, partId: string) => void
|
||||||
bufferPendingPart: (entry: PendingPartEntry) => void
|
bufferPendingPart: (entry: PendingPartEntry) => void
|
||||||
@@ -598,7 +605,14 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
|||||||
bumpSessionRevision(message.sessionId)
|
bumpSessionRevision(message.sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPartDelta(input: { messageId: string; partId: string; field: string; delta: string; bumpRevision?: boolean }) {
|
function applyPartDelta(input: {
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
|
field: string
|
||||||
|
delta: string
|
||||||
|
bumpRevision?: boolean
|
||||||
|
bumpSessionRevision?: boolean
|
||||||
|
}) {
|
||||||
if (!input?.messageId || !input.partId || !input.field || typeof input.delta !== "string") {
|
if (!input?.messageId || !input.partId || !input.field || typeof input.delta !== "string") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -632,7 +646,7 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (applied) {
|
if (applied && (input.bumpSessionRevision ?? true)) {
|
||||||
bumpSessionRevision(message.sessionId)
|
bumpSessionRevision(message.sessionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1165,4 +1179,3 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
|||||||
clearInstance,
|
clearInstance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface ModelPreference {
|
|||||||
|
|
||||||
export type DiffViewMode = "split" | "unified"
|
export type DiffViewMode = "split" | "unified"
|
||||||
export type ExpansionPreference = "expanded" | "collapsed"
|
export type ExpansionPreference = "expanded" | "collapsed"
|
||||||
|
export type ToolInputsVisibilityPreference = "hidden" | "collapsed" | "expanded"
|
||||||
export type ListeningMode = "local" | "all"
|
export type ListeningMode = "local" | "all"
|
||||||
|
|
||||||
export interface UiSettings {
|
export interface UiSettings {
|
||||||
@@ -37,6 +38,7 @@ export interface UiSettings {
|
|||||||
diffViewMode: DiffViewMode
|
diffViewMode: DiffViewMode
|
||||||
toolOutputExpansion: ExpansionPreference
|
toolOutputExpansion: ExpansionPreference
|
||||||
diagnosticsExpansion: ExpansionPreference
|
diagnosticsExpansion: ExpansionPreference
|
||||||
|
toolInputsVisibility: ToolInputsVisibilityPreference
|
||||||
showUsageMetrics: boolean
|
showUsageMetrics: boolean
|
||||||
autoCleanupBlankSessions: boolean
|
autoCleanupBlankSessions: boolean
|
||||||
|
|
||||||
@@ -108,6 +110,7 @@ const defaultUiSettings: UiSettings = {
|
|||||||
diffViewMode: "split",
|
diffViewMode: "split",
|
||||||
toolOutputExpansion: "expanded",
|
toolOutputExpansion: "expanded",
|
||||||
diagnosticsExpansion: "expanded",
|
diagnosticsExpansion: "expanded",
|
||||||
|
toolInputsVisibility: "collapsed",
|
||||||
showUsageMetrics: true,
|
showUsageMetrics: true,
|
||||||
autoCleanupBlankSessions: true,
|
autoCleanupBlankSessions: true,
|
||||||
|
|
||||||
@@ -130,6 +133,10 @@ function normalizeUiSettings(input?: Partial<UiSettings> | null): UiSettings {
|
|||||||
diffViewMode: sanitized.diffViewMode ?? defaultUiSettings.diffViewMode,
|
diffViewMode: sanitized.diffViewMode ?? defaultUiSettings.diffViewMode,
|
||||||
toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultUiSettings.toolOutputExpansion,
|
toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultUiSettings.toolOutputExpansion,
|
||||||
diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultUiSettings.diagnosticsExpansion,
|
diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultUiSettings.diagnosticsExpansion,
|
||||||
|
toolInputsVisibility:
|
||||||
|
sanitized.toolInputsVisibility === "hidden" || sanitized.toolInputsVisibility === "collapsed" || sanitized.toolInputsVisibility === "expanded"
|
||||||
|
? sanitized.toolInputsVisibility
|
||||||
|
: defaultUiSettings.toolInputsVisibility,
|
||||||
showUsageMetrics: sanitized.showUsageMetrics ?? defaultUiSettings.showUsageMetrics,
|
showUsageMetrics: sanitized.showUsageMetrics ?? defaultUiSettings.showUsageMetrics,
|
||||||
autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultUiSettings.autoCleanupBlankSessions,
|
autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultUiSettings.autoCleanupBlankSessions,
|
||||||
osNotificationsEnabled: sanitized.osNotificationsEnabled ?? defaultUiSettings.osNotificationsEnabled,
|
osNotificationsEnabled: sanitized.osNotificationsEnabled ?? defaultUiSettings.osNotificationsEnabled,
|
||||||
@@ -439,6 +446,11 @@ function setDiagnosticsExpansion(mode: ExpansionPreference): void {
|
|||||||
updateUiSettings({ diagnosticsExpansion: mode })
|
updateUiSettings({ diagnosticsExpansion: mode })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setToolInputsVisibility(mode: ToolInputsVisibilityPreference): void {
|
||||||
|
if (preferences().toolInputsVisibility === mode) return
|
||||||
|
updateUiSettings({ toolInputsVisibility: mode })
|
||||||
|
}
|
||||||
|
|
||||||
function setThinkingBlocksExpansion(mode: ExpansionPreference): void {
|
function setThinkingBlocksExpansion(mode: ExpansionPreference): void {
|
||||||
if (preferences().thinkingBlocksExpansion === mode) return
|
if (preferences().thinkingBlocksExpansion === mode) return
|
||||||
updateUiSettings({ thinkingBlocksExpansion: mode })
|
updateUiSettings({ thinkingBlocksExpansion: mode })
|
||||||
@@ -536,6 +548,7 @@ interface ConfigContextValue {
|
|||||||
setToolOutputExpansion: typeof setToolOutputExpansion
|
setToolOutputExpansion: typeof setToolOutputExpansion
|
||||||
setDiagnosticsExpansion: typeof setDiagnosticsExpansion
|
setDiagnosticsExpansion: typeof setDiagnosticsExpansion
|
||||||
setThinkingBlocksExpansion: typeof setThinkingBlocksExpansion
|
setThinkingBlocksExpansion: typeof setThinkingBlocksExpansion
|
||||||
|
setToolInputsVisibility: typeof setToolInputsVisibility
|
||||||
|
|
||||||
// instance scoped
|
// instance scoped
|
||||||
setAgentModelPreference: typeof setAgentModelPreference
|
setAgentModelPreference: typeof setAgentModelPreference
|
||||||
@@ -579,6 +592,7 @@ const configContextValue: ConfigContextValue = {
|
|||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
setThinkingBlocksExpansion,
|
setThinkingBlocksExpansion,
|
||||||
|
setToolInputsVisibility,
|
||||||
setAgentModelPreference,
|
setAgentModelPreference,
|
||||||
getAgentModelPreference,
|
getAgentModelPreference,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { providers, sessions, withSession } from "./session-state"
|
|||||||
import { getDefaultModel, isModelValid } from "./session-models"
|
import { getDefaultModel, isModelValid } from "./session-models"
|
||||||
import { updateSessionInfo } from "./message-v2/session-info"
|
import { updateSessionInfo } from "./message-v2/session-info"
|
||||||
import { messageStoreBus } from "./message-v2/bus"
|
import { messageStoreBus } from "./message-v2/bus"
|
||||||
import { removeMessagePartV2 } from "./message-v2/bridge"
|
import { removeMessagePartV2, removeMessageV2 } from "./message-v2/bridge"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { requestData } from "../lib/opencode-api"
|
import { requestData } from "../lib/opencode-api"
|
||||||
|
|
||||||
@@ -439,8 +439,33 @@ async function deleteMessagePart(instanceId: string, sessionId: string, messageI
|
|||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteMessage(instanceId: string, sessionId: string, messageId: string): Promise<void> {
|
||||||
|
if (!instanceId || !sessionId || !messageId) return
|
||||||
|
const instance = instances().get(instanceId)
|
||||||
|
if (!instance || !instance.client) {
|
||||||
|
throw new Error("Instance not ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
|
||||||
|
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
||||||
|
|
||||||
|
// The SDK generator does not currently expose a typed method for deleting a message,
|
||||||
|
// but the API is available at DELETE /session/:sessionID/message/:messageID.
|
||||||
|
await requestData(
|
||||||
|
(client as any).client.delete({
|
||||||
|
url: `/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`,
|
||||||
|
}),
|
||||||
|
"session.message.delete",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Optimistic removal; SSE will also broadcast a message-removed event.
|
||||||
|
removeMessageV2(instanceId, messageId)
|
||||||
|
updateSessionInfo(instanceId, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
abortSession,
|
abortSession,
|
||||||
|
deleteMessage,
|
||||||
deleteMessagePart,
|
deleteMessagePart,
|
||||||
executeCustomCommand,
|
executeCustomCommand,
|
||||||
renameSession,
|
renameSession,
|
||||||
|
|||||||
@@ -526,6 +526,7 @@ async function fetchAgents(instanceId: string): Promise<void> {
|
|||||||
name: agent.name,
|
name: agent.name,
|
||||||
description: agent.description || "",
|
description: agent.description || "",
|
||||||
mode: agent.mode,
|
mode: agent.mode,
|
||||||
|
hidden: agent.hidden,
|
||||||
model: agent.model?.modelID
|
model: agent.model?.modelID
|
||||||
? {
|
? {
|
||||||
providerId: agent.model.providerID || "",
|
providerId: agent.model.providerID || "",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user