From 96fe1b86dd814c1691bacc9d54ceaca11c9aab5a Mon Sep 17 00:00:00 2001 From: VooDisss Date: Fri, 20 Feb 2026 12:33:52 +0200 Subject: [PATCH 01/59] fix(ui): prevent timeline auto-scroll when removing badges --- packages/ui/src/components/message-timeline.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/message-timeline.tsx b/packages/ui/src/components/message-timeline.tsx index c0e1d9d7..fc4ef7f3 100644 --- a/packages/ui/src/components/message-timeline.tsx +++ b/packages/ui/src/components/message-timeline.tsx @@ -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 { messageStoreBus } from "../stores/message-v2/bus" import type { ClientPart } from "../types/message" @@ -350,11 +350,9 @@ const MessageTimeline: Component = (props) => { clearCloseTimer() }) - createEffect(() => { - const activeId = props.activeMessageId - + createEffect(on(() => props.activeMessageId, (activeId) => { 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 const element = buttonRefs.get(targetSegment.id) if (!element) return @@ -366,7 +364,7 @@ const MessageTimeline: Component = (props) => { window.clearTimeout(timer) } }) - }) + })) createEffect(() => { const element = tooltipElement() From d6462ef524317ba5fbb5a0f94a6a2767dae6918a Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Sun, 22 Feb 2026 17:32:28 +0000 Subject: [PATCH 02/59] Min version 0.11.4 --- packages/cloudflare/release-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloudflare/release-config.json b/packages/cloudflare/release-config.json index 299fb24c..37efb960 100644 --- a/packages/cloudflare/release-config.json +++ b/packages/cloudflare/release-config.json @@ -1,4 +1,4 @@ { - "minServerVersion": "0.11.1", + "minServerVersion": "0.11.4", "latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest" } From d36e568ed00b31083ba709712c157c07feb8c2db Mon Sep 17 00:00:00 2001 From: VooDisss Date: Mon, 23 Feb 2026 02:30:44 +0200 Subject: [PATCH 03/59] fix: Use legacy diff algorithm for better large file performance - Set diffAlgorithm to 'legacy' for Monaco DiffEditor - Add maxComputationTime of 10s to avoid UI freeze on huge files This addresses the issue where sessions with large JSON files (50k-100k+ lines) would cause the UI to freeze. The 'legacy' algorithm is faster than 'advanced' for large files, similar to VSCode's workaround for the same issue. See: https://github.com/microsoft/vscode/issues/184037 --- .../ui/src/components/file-viewer/monaco-diff-viewer.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx b/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx index c6694f80..ace89d5f 100644 --- a/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx +++ b/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx @@ -61,6 +61,11 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) { // Keep enough gutter space so unified diffs don't overlap `+`/`-` markers. lineNumbersMinChars: 4, 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) From 90baefbb7e09d24c4f45cb2e3ff99c836f250af6 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 23 Feb 2026 08:54:57 +0000 Subject: [PATCH 04/59] fix(ci): rezip Electron macOS zips with ditto Add a codesign verify step on extracted artifacts to catch signature/resource mismatches before upload. --- .github/workflows/build-and-upload.yml | 72 ++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/.github/workflows/build-and-upload.yml b/.github/workflows/build-and-upload.yml index 8ddc9daa..17e8d705 100644 --- a/.github/workflows/build-and-upload.yml +++ b/.github/workflows/build-and-upload.yml @@ -72,6 +72,78 @@ jobs: - name: Build macOS binaries (Electron) run: npm run build:mac --workspace @neuralnomads/codenomad-electron-app + - name: Repackage Electron macOS zips (ditto) + shell: bash + run: | + set -euo pipefail + + # Prefer the workflow-provided version; fall back to package.json. + VERSION_TO_USE="${VERSION:-}" + if [ -z "$VERSION_TO_USE" ]; then + VERSION_TO_USE=$(node -p "require('./packages/electron-app/package.json').version") + fi + + release_root="packages/electron-app/release" + shopt -s nullglob globstar + + apps=("$release_root"/**/CodeNomad.app) + if [ "${#apps[@]}" -eq 0 ]; then + echo "No CodeNomad.app found under $release_root" >&2 + exit 1 + fi + + for app in "${apps[@]}"; do + bundle_dir=$(basename "$(dirname "$app")") + arch="x64" + if [[ "$bundle_dir" == *"arm64"* ]]; then + arch="arm64" + fi + + out_zip="$release_root/CodeNomad-${VERSION_TO_USE}-mac-${arch}.zip" + rm -f "$out_zip" + echo "ditto -ck: $app -> $out_zip" + ditto -ck --sequesterRsrc --keepParent "$app" "$out_zip" + done + + - name: Validate Electron macOS codesign (unzipped) + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + + tmp_dir=$(mktemp -d) + trap 'rm -rf "$tmp_dir"' EXIT + + zips=(packages/electron-app/release/CodeNomad-*-mac-*.zip) + if [ "${#zips[@]}" -eq 0 ]; then + echo "No Electron macOS zip artifacts found to validate" >&2 + exit 1 + fi + + for zip in "${zips[@]}"; do + echo "Validating codesign for: $zip" + extract_dir="$tmp_dir/$(basename "$zip" .zip)" + mkdir -p "$extract_dir" + + # Use ditto for extraction as well to preserve bundle metadata. + ditto -x -k "$zip" "$extract_dir" + + app_path="" + for candidate in "$extract_dir"/*.app "$extract_dir"/*/*.app; do + if [ -d "$candidate" ]; then + app_path="$candidate" + break + fi + done + + if [ -z "$app_path" ]; then + echo "No .app found after extracting $zip" >&2 + exit 1 + fi + + codesign --verify --deep --strict --verbose=2 "$app_path" + done + - name: Upload release assets if: ${{ inputs.upload && inputs.tag != '' }} run: | From e90aef4b3c526556a080796aac00423bab7416c2 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 23 Feb 2026 18:36:24 +0000 Subject: [PATCH 05/59] fix(ui): stack instance header under 1024px --- .../components/instance/instance-shell2.tsx | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index 43998a17..464a2acb 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -115,6 +115,7 @@ const InstanceShell2: Component = (props) => { const desktopQuery = useMediaQuery("(min-width: 1280px)") const tabletQuery = useMediaQuery("(min-width: 768px)") + const compactHeaderQuery = useMediaQuery("(max-width: 1024px)") const layoutMode = createMemo(() => { if (desktopQuery()) return "desktop" @@ -123,6 +124,7 @@ const InstanceShell2: Component = (props) => { }) const isPhoneLayout = createMemo(() => layoutMode() === "phone") + const compactHeaderLayout = createMemo(() => isPhoneLayout() || compactHeaderQuery()) const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout()) const compactPromptLayout = createMemo(() => layoutMode() !== "desktop") const leftPinningSupported = createMemo(() => layoutMode() !== "phone") @@ -596,7 +598,7 @@ const InstanceShell2: Component = (props) => {
@@ -634,8 +636,8 @@ const InstanceShell2: Component = (props) => { - -
+ +
= (props) => {
- + = (props) => { {rightAppBarButtonIcon()} - +
- + + +
} From 027d7fc97d2637935c89dcbec89b3d9b9fc43823 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 23 Feb 2026 18:39:21 +0000 Subject: [PATCH 06/59] fix(ui): load shiki languages from marked tokens --- packages/ui/src/lib/markdown.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/lib/markdown.ts b/packages/ui/src/lib/markdown.ts index 400d7476..025b14af 100644 --- a/packages/ui/src/lib/markdown.ts +++ b/packages/ui/src/lib/markdown.ts @@ -127,17 +127,23 @@ async function ensureLanguages(content: string) { if (highlightSuppressed) { 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() - let match - while ((match = codeBlockRegex.exec(content)) !== null) { - const langToken = match[1] - if (langToken && langToken.trim()) { - foundLanguages.add(langToken.trim()) - } + // Extract code-fence language tokens via `marked` so we correctly handle code blocks + // that contain backticks (e.g. JS template literals). Regex-based fence scans tend + // to miss these and prevent languages from loading. + const foundLanguages = new Set() + 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 From 15f362e8b5ce15c43fbdfba1f3566d0a51fbcc46 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 23 Feb 2026 23:55:52 +0000 Subject: [PATCH 07/59] Bump v0.11.5 --- package-lock.json | 12 ++++++------ package.json | 2 +- packages/electron-app/package.json | 2 +- packages/server/package-lock.json | 4 ++-- packages/server/package.json | 2 +- packages/tauri-app/package.json | 2 +- packages/ui/package.json | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99303498..6323316c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codenomad-workspace", - "version": "0.11.4", + "version": "0.11.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codenomad-workspace", - "version": "0.11.4", + "version": "0.11.5", "license": "MIT", "dependencies": { "7zip-bin": "^5.2.0", @@ -11985,7 +11985,7 @@ }, "packages/electron-app": { "name": "@neuralnomads/codenomad-electron-app", - "version": "0.11.4", + "version": "0.11.5", "license": "MIT", "dependencies": { "@codenomad/ui": "file:../ui", @@ -12021,7 +12021,7 @@ }, "packages/server": { "name": "@neuralnomads/codenomad", - "version": "0.11.4", + "version": "0.11.5", "license": "MIT", "dependencies": { "@fastify/cors": "^8.5.0", @@ -12062,7 +12062,7 @@ }, "packages/tauri-app": { "name": "@codenomad/tauri-app", - "version": "0.11.4", + "version": "0.11.5", "license": "MIT", "devDependencies": { "@tauri-apps/cli": "^2.9.4" @@ -12070,7 +12070,7 @@ }, "packages/ui": { "name": "@codenomad/ui", - "version": "0.11.4", + "version": "0.11.5", "license": "MIT", "dependencies": { "@git-diff-view/solid": "^0.0.8", diff --git a/package.json b/package.json index e2381e85..7bfbbb49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codenomad-workspace", - "version": "0.11.4", + "version": "0.11.5", "private": true, "description": "CodeNomad monorepo workspace", "license": "MIT", diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index b1918510..b6bf4cfa 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -1,6 +1,6 @@ { "name": "@neuralnomads/codenomad-electron-app", - "version": "0.11.4", + "version": "0.11.5", "description": "CodeNomad - AI coding assistant", "license": "MIT", "author": { diff --git a/packages/server/package-lock.json b/packages/server/package-lock.json index 1755c004..13aaa409 100644 --- a/packages/server/package-lock.json +++ b/packages/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@neuralnomads/codenomad", - "version": "0.11.4", + "version": "0.11.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@neuralnomads/codenomad", - "version": "0.11.4", + "version": "0.11.5", "dependencies": { "@fastify/cors": "^8.5.0", "@fastify/reply-from": "^9.8.0", diff --git a/packages/server/package.json b/packages/server/package.json index 02082c24..8ea98866 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@neuralnomads/codenomad", - "version": "0.11.4", + "version": "0.11.5", "description": "CodeNomad Server", "license": "MIT", "author": { diff --git a/packages/tauri-app/package.json b/packages/tauri-app/package.json index b86075f5..bac16d59 100644 --- a/packages/tauri-app/package.json +++ b/packages/tauri-app/package.json @@ -1,6 +1,6 @@ { "name": "@codenomad/tauri-app", - "version": "0.11.4", + "version": "0.11.5", "private": true, "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 50ab2cdd..ca801965 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@codenomad/ui", - "version": "0.11.4", + "version": "0.11.5", "private": true, "license": "MIT", "type": "module", From 8e5a7fc2139cf1fe63f85cc1717e8a6a38c03e9f Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 24 Feb 2026 00:09:49 +0000 Subject: [PATCH 08/59] fix(electron): make dev CLI log level configurable Use CLI_LOG_LEVEL when launching the server in desktop dev and add dev:info/dev:debug/dev:trace scripts with dev defaulting to info. --- packages/electron-app/electron/main/process-manager.ts | 4 +++- packages/electron-app/package.json | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/electron-app/electron/main/process-manager.ts b/packages/electron-app/electron/main/process-manager.ts index a9b940c0..de00d1f5 100644 --- a/packages/electron-app/electron/main/process-manager.ts +++ b/packages/electron-app/electron/main/process-manager.ts @@ -431,7 +431,9 @@ export class CliProcessManager extends EventEmitter { if (options.dev) { 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 diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index b6bf4cfa..cb687cc1 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -15,7 +15,10 @@ }, "homepage": "https://github.com/NeuralNomadsAI/CodeNomad", "scripts": { - "dev": "electron-vite dev", + "dev": "npm run dev:info", + "dev:info": "CLI_LOG_LEVEL=info electron-vite dev", + "dev:debug": "CLI_LOG_LEVEL=debug electron-vite dev", + "dev:trace": "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", "build": "electron-vite build", "typecheck": "tsc --noEmit -p tsconfig.json", From b970281fa7a0029a066abbf603b9c9b50a34c759 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 24 Feb 2026 00:19:39 +0000 Subject: [PATCH 09/59] chore(electron): use cross-env for dev log level scripts Make dev:info/dev:debug/dev:trace work on Windows by setting CLI_LOG_LEVEL via cross-env. --- packages/electron-app/package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index cb687cc1..266ae7fa 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -16,9 +16,9 @@ "homepage": "https://github.com/NeuralNomadsAI/CodeNomad", "scripts": { "dev": "npm run dev:info", - "dev:info": "CLI_LOG_LEVEL=info electron-vite dev", - "dev:debug": "CLI_LOG_LEVEL=debug electron-vite dev", - "dev:trace": "CLI_LOG_LEVEL=trace electron-vite dev", + "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", "build": "electron-vite build", "typecheck": "tsc --noEmit -p tsconfig.json", @@ -45,6 +45,7 @@ "devDependencies": { "7zip-bin": "^5.2.0", "app-builder-bin": "^4.2.0", + "cross-env": "^7.0.3", "electron": "39.0.0", "electron-builder": "^24.0.0", "electron-vite": "4.0.1", From 0368fe8248d30c7a29bf45850567b03a8562b6b3 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 24 Feb 2026 07:29:26 +0000 Subject: [PATCH 10/59] fix(ci): avoid bash globstar on macOS --- .github/workflows/build-and-upload.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-upload.yml b/.github/workflows/build-and-upload.yml index 17e8d705..c0dc0f3c 100644 --- a/.github/workflows/build-and-upload.yml +++ b/.github/workflows/build-and-upload.yml @@ -84,9 +84,12 @@ jobs: fi release_root="packages/electron-app/release" - shopt -s nullglob globstar - - apps=("$release_root"/**/CodeNomad.app) + # 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 From 8607fab5b56c7ba0ab4b3b60251a92140b50baac Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 24 Feb 2026 08:53:14 +0000 Subject: [PATCH 11/59] fix(ci): skip macOS codesign verify without identity --- .github/workflows/build-and-upload.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build-and-upload.yml b/.github/workflows/build-and-upload.yml index c0dc0f3c..03ddefd0 100644 --- a/.github/workflows/build-and-upload.yml +++ b/.github/workflows/build-and-upload.yml @@ -114,6 +114,13 @@ jobs: set -euo pipefail shopt -s nullglob + # Dev CI builds typically don't have a macOS signing identity available. + # When no identity is present, electron-builder skips signing and strict verification will fail. + if security find-identity -p codesigning -v | grep -q "0 valid identities found"; then + echo "No valid macOS codesigning identity found; skipping codesign verification" + exit 0 + fi + tmp_dir=$(mktemp -d) trap 'rm -rf "$tmp_dir"' EXIT From 5f755a7e1c1d06ec093e515d651f8dc6bdd407b9 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 24 Feb 2026 09:08:32 +0000 Subject: [PATCH 12/59] fix(ci): retry workspace version bump on macos --- .github/workflows/build-and-upload.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-upload.yml b/.github/workflows/build-and-upload.yml index 03ddefd0..f5e82e34 100644 --- a/.github/workflows/build-and-upload.yml +++ b/.github/workflows/build-and-upload.yml @@ -61,7 +61,21 @@ jobs: - name: Set workspace versions 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 run: npm ci --workspaces --include=optional From ef4c8ef425cb139b63783340a1325b9df0d621b3 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 24 Feb 2026 22:22:46 +0000 Subject: [PATCH 13/59] fix(ci): ad-hoc sign Electron macOS apps --- .github/workflows/build-and-upload.yml | 38 +++++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-upload.yml b/.github/workflows/build-and-upload.yml index f5e82e34..392db41d 100644 --- a/.github/workflows/build-and-upload.yml +++ b/.github/workflows/build-and-upload.yml @@ -86,6 +86,37 @@ jobs: - name: Build macOS binaries (Electron) 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: | @@ -128,13 +159,6 @@ jobs: set -euo pipefail shopt -s nullglob - # Dev CI builds typically don't have a macOS signing identity available. - # When no identity is present, electron-builder skips signing and strict verification will fail. - if security find-identity -p codesigning -v | grep -q "0 valid identities found"; then - echo "No valid macOS codesigning identity found; skipping codesign verification" - exit 0 - fi - tmp_dir=$(mktemp -d) trap 'rm -rf "$tmp_dir"' EXIT From 5834d2df1b5d7e1b8a20524d3d48bc03f7d889a2 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Wed, 25 Feb 2026 22:29:27 +0000 Subject: [PATCH 14/59] fix(ui): use v2 message info and show model variant --- .../instance/shell/right-panel/RightPanel.tsx | 2 +- .../instance/shell/right-panel/tabs/StatusTab.tsx | 2 +- .../instance/shell/useInstanceSessionContext.ts | 2 +- packages/ui/src/components/message-block.tsx | 8 ++++---- packages/ui/src/components/message-item.tsx | 14 +++++++++++--- packages/ui/src/components/tool-call.tsx | 2 +- .../ui/src/components/tool-call/diagnostics.ts | 2 +- .../src/components/tool-call/markdown-render.tsx | 2 +- .../ui/src/components/tool-call/question-block.tsx | 2 +- .../ui/src/components/tool-call/renderers/task.tsx | 2 +- .../ui/src/components/tool-call/renderers/todo.tsx | 2 +- packages/ui/src/components/tool-call/tool-title.ts | 2 +- packages/ui/src/components/tool-call/types.ts | 2 +- packages/ui/src/components/tool-call/utils.ts | 8 ++++---- packages/ui/src/types/message.ts | 8 +++++--- 15 files changed, 35 insertions(+), 25 deletions(-) diff --git a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx index fc0e58aa..a4168973 100644 --- a/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx @@ -7,7 +7,7 @@ import { type Accessor, type Component, } 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 IconButton from "@suid/material/IconButton" import MenuOpenIcon from "@suid/icons-material/MenuOpen" diff --git a/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx b/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx index e6839af6..b52c16d9 100644 --- a/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx @@ -1,5 +1,5 @@ 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 { Tooltip } from "@kobalte/core/tooltip" diff --git a/packages/ui/src/components/instance/shell/useInstanceSessionContext.ts b/packages/ui/src/components/instance/shell/useInstanceSessionContext.ts index faee193a..5eb98bf2 100644 --- a/packages/ui/src/components/instance/shell/useInstanceSessionContext.ts +++ b/packages/ui/src/components/instance/shell/useInstanceSessionContext.ts @@ -1,5 +1,5 @@ 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 { activeParentSessionId, diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index 0d485c94..50f9f4b9 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -23,10 +23,10 @@ const TOOL_BORDER_COLOR = "var(--message-tool-border)" type ToolCallPart = Extract -type ToolState = import("@opencode-ai/sdk").ToolState -type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning -type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted -type ToolStateError = import("@opencode-ai/sdk").ToolStateError +type ToolState = import("@opencode-ai/sdk/v2").ToolState +type ToolStateRunning = import("@opencode-ai/sdk/v2").ToolStateRunning +type ToolStateCompleted = import("@opencode-ai/sdk/v2").ToolStateCompleted +type ToolStateError = import("@opencode-ai/sdk/v2").ToolStateError function isToolStateRunning(state: ToolState | undefined): state is ToolStateRunning { return Boolean(state && state.status === "running") diff --git a/packages/ui/src/components/message-item.tsx b/packages/ui/src/components/message-item.tsx index 719d660d..f968858a 100644 --- a/packages/ui/src/components/message-item.tsx +++ b/packages/ui/src/components/message-item.tsx @@ -1,6 +1,6 @@ import { For, Show, createSignal } from "solid-js" import { Copy, ExternalLink, Split, Trash2, Undo } from "lucide-solid" -import type { MessageInfo, ClientPart } from "../types/message" +import type { MessageInfo, ClientPart, SDKAssistantMessageV2 } from "../types/message" import { partHasRenderableText } from "../types/message" import type { MessageRecord } from "../stores/message-v2/types" import MessagePart from "./message-part" @@ -258,8 +258,16 @@ export default function MessageItem(props: MessageItemProps) { if (!info || info.role !== "assistant") return "" const modelID = info.modelID || "" 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 = () => { diff --git a/packages/ui/src/components/tool-call.tsx b/packages/ui/src/components/tool-call.tsx index 5d3af5f5..f0086016 100644 --- a/packages/ui/src/components/tool-call.tsx +++ b/packages/ui/src/components/tool-call.tsx @@ -44,7 +44,7 @@ import { getLogger } from "../lib/logger" const log = getLogger("session") -type ToolState = import("@opencode-ai/sdk").ToolState +type ToolState = import("@opencode-ai/sdk/v2").ToolState const TOOL_CALL_CACHE_SCOPE = "tool-call" const TOOL_SCROLL_SENTINEL_MARGIN_PX = 48 diff --git a/packages/ui/src/components/tool-call/diagnostics.ts b/packages/ui/src/components/tool-call/diagnostics.ts index 4ec94fc3..304e92b8 100644 --- a/packages/ui/src/components/tool-call/diagnostics.ts +++ b/packages/ui/src/components/tool-call/diagnostics.ts @@ -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 { tGlobal } from "../../lib/i18n" diff --git a/packages/ui/src/components/tool-call/markdown-render.tsx b/packages/ui/src/components/tool-call/markdown-render.tsx index c94859db..17142718 100644 --- a/packages/ui/src/components/tool-call/markdown-render.tsx +++ b/packages/ui/src/components/tool-call/markdown-render.tsx @@ -1,5 +1,5 @@ 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 { Markdown } from "../markdown" import type { MarkdownRenderOptions, ToolScrollHelpers } from "./types" diff --git a/packages/ui/src/components/tool-call/question-block.tsx b/packages/ui/src/components/tool-call/question-block.tsx index 75544190..dee5fee9 100644 --- a/packages/ui/src/components/tool-call/question-block.tsx +++ b/packages/ui/src/components/tool-call/question-block.tsx @@ -1,5 +1,5 @@ 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 { useI18n } from "../../lib/i18n" diff --git a/packages/ui/src/components/tool-call/renderers/task.tsx b/packages/ui/src/components/tool-call/renderers/task.tsx index 80659458..1c420f7d 100644 --- a/packages/ui/src/components/tool-call/renderers/task.tsx +++ b/packages/ui/src/components/tool-call/renderers/task.tsx @@ -1,5 +1,5 @@ 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 { ensureMarkdownContent, getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils" import { resolveTitleForTool } from "../tool-title" diff --git a/packages/ui/src/components/tool-call/renderers/todo.tsx b/packages/ui/src/components/tool-call/renderers/todo.tsx index 689cfb90..5169c7ab 100644 --- a/packages/ui/src/components/tool-call/renderers/todo.tsx +++ b/packages/ui/src/components/tool-call/renderers/todo.tsx @@ -1,5 +1,5 @@ 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 { readToolStatePayload } from "../utils" import { useI18n, tGlobal } from "../../../lib/i18n" diff --git a/packages/ui/src/components/tool-call/tool-title.ts b/packages/ui/src/components/tool-call/tool-title.ts index 6d2526c5..7385a514 100644 --- a/packages/ui/src/components/tool-call/tool-title.ts +++ b/packages/ui/src/components/tool-call/tool-title.ts @@ -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 { getDefaultToolAction, getToolName, isToolStateCompleted, isToolStateRunning } from "./utils" import { enMessages } from "../../lib/i18n/messages/en" diff --git a/packages/ui/src/components/tool-call/types.ts b/packages/ui/src/components/tool-call/types.ts index c3578a07..e0d994de 100644 --- a/packages/ui/src/components/tool-call/types.ts +++ b/packages/ui/src/components/tool-call/types.ts @@ -1,5 +1,5 @@ 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" export type ToolCallPart = Extract diff --git a/packages/ui/src/components/tool-call/utils.ts b/packages/ui/src/components/tool-call/utils.ts index 993630b8..b0fd38fd 100644 --- a/packages/ui/src/components/tool-call/utils.ts +++ b/packages/ui/src/components/tool-call/utils.ts @@ -1,15 +1,15 @@ import { isRenderableDiffText } from "../../lib/diff-utils" 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 { getLogger } from "../../lib/logger" import { tGlobal } from "../../lib/i18n" const log = getLogger("session") -export type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning -export type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted -export type ToolStateError = import("@opencode-ai/sdk").ToolStateError +export type ToolStateRunning = import("@opencode-ai/sdk/v2").ToolStateRunning +export type ToolStateCompleted = import("@opencode-ai/sdk/v2").ToolStateCompleted +export type ToolStateError = import("@opencode-ai/sdk/v2").ToolStateError export const diffCapableTools = new Set(["edit", "patch"]) diff --git a/packages/ui/src/types/message.ts b/packages/ui/src/types/message.ts index db964d27..6bca8238 100644 --- a/packages/ui/src/types/message.ts +++ b/packages/ui/src/types/message.ts @@ -1,4 +1,4 @@ -// SDK types +// SDK v2 types import type { EventMessageUpdated as MessageUpdateEvent, EventMessageRemoved as MessageRemovedEvent, @@ -6,7 +6,8 @@ import type { EventMessagePartRemoved as MessagePartRemovedEvent, Part as SDKPart, Message as SDKMessage, -} from "@opencode-ai/sdk" + AssistantMessage as SDKAssistantMessageV2, +} from "@opencode-ai/sdk/v2" import type { PermissionRequestLike } from "./permission" @@ -17,7 +18,8 @@ export type { MessagePartUpdatedEvent, MessagePartRemovedEvent, SDKPart, - SDKMessage + SDKMessage, + SDKAssistantMessageV2, } // Server streaming event: append-only delta updates. From 362105fe7828c74d84cf07c65501e97a0fadde30 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Wed, 25 Feb 2026 22:49:14 +0000 Subject: [PATCH 15/59] feat(ui): add delete message action to stream --- packages/ui/src/components/message-block.tsx | 213 ++++++++++++++++-- packages/ui/src/components/message-item.tsx | 46 +++- .../ui/src/lib/i18n/messages/en/messaging.ts | 8 +- .../ui/src/lib/i18n/messages/es/messaging.ts | 8 +- .../ui/src/lib/i18n/messages/fr/messaging.ts | 8 +- .../ui/src/lib/i18n/messages/ja/messaging.ts | 8 +- .../ui/src/lib/i18n/messages/ru/messaging.ts | 8 +- .../lib/i18n/messages/zh-Hans/messaging.ts | 8 +- packages/ui/src/stores/session-actions.ts | 27 ++- 9 files changed, 295 insertions(+), 39 deletions(-) diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index 50f9f4b9..e065be82 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -1,5 +1,5 @@ import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js" -import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, Trash2 } from "lucide-solid" +import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, MessageSquareX, Trash2 } from "lucide-solid" import MessageItem from "./message-item" import ToolCall from "./tool-call" import type { InstanceMessageStore } from "../stores/message-v2/instance-store" @@ -13,6 +13,7 @@ import { sessions, setActiveParentSession, setActiveSession } from "../stores/se import { setActiveInstanceId } from "../stores/instances" import { showAlertDialog } from "../stores/alerts" import { deleteMessagePart } from "../stores/session-actions" +import { deleteMessage } from "../stores/session-actions" import { useI18n } from "../lib/i18n" const TOOL_ICON = "🔧" @@ -196,6 +197,7 @@ interface MessageContentItemProps { onRevert?: (messageId: string) => void onFork?: (messageId?: string) => void onContentRendered?: () => void + showDeleteMessage?: boolean } function isSupportedPartType(part: unknown): boolean { @@ -282,6 +284,7 @@ function MessageContentItem(props: MessageContentItemProps) { sessionId={props.sessionId} isQueued={isQueued()} showAgentMeta={showAgentMeta()} + showDeleteMessage={props.showDeleteMessage} onRevert={props.onRevert} onFork={props.onFork} onContentRendered={props.onContentRendered} @@ -298,11 +301,13 @@ interface ToolCallItemProps { messageId: string partId: string onContentRendered?: () => void + showDeleteMessage?: boolean } function ToolCallItem(props: ToolCallItemProps) { const { t } = useI18n() const [deleting, setDeleting] = createSignal(false) + const [deletingMessage, setDeletingMessage] = createSignal(false) const record = createMemo(() => props.store().getMessage(props.messageId)) const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId)) @@ -370,6 +375,27 @@ function ToolCallItem(props: ToolCallItemProps) { } } + const handleDeleteMessage = async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + + if (!props.showDeleteMessage) return + if (deletingMessage()) 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) + } + } + return ( {(resolvedToolPart) => ( @@ -381,7 +407,7 @@ function ToolCallItem(props: ToolCallItemProps) { {toolName() || t("messageBlock.tool.unknown")} -
+
+ + + + + +
- (
- {(item) => ( + {(item, index) => (
@@ -709,6 +750,10 @@ export default function MessageBlock(props: MessageBlockProps) { part={(item as StepDisplayItem).part} messageInfo={(item as StepDisplayItem).messageInfo} showAgentMeta + showDeleteMessage={index() === 0} + instanceId={props.instanceId} + sessionId={props.sessionId} + messageId={props.messageId} /> @@ -718,6 +763,10 @@ export default function MessageBlock(props: MessageBlockProps) { messageInfo={(item as StepDisplayItem).messageInfo} showUsage={props.showUsageMetrics()} borderColor={(item as StepDisplayItem).accentColor} + showDeleteMessage={index() === 0} + instanceId={props.instanceId} + sessionId={props.sessionId} + messageId={props.messageId} /> @@ -729,6 +778,7 @@ export default function MessageBlock(props: MessageBlockProps) { sessionId={props.sessionId} messageId={(item as CompactionDisplayItem).messageId} partId={(item as CompactionDisplayItem).partId} + showDeleteMessage={index() === 0} /> @@ -741,6 +791,7 @@ export default function MessageBlock(props: MessageBlockProps) { partId={(item as ReasoningDisplayItem).partId} showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta} defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded} + showDeleteMessage={index() === 0} /> @@ -759,6 +810,10 @@ interface StepCardProps { showAgentMeta?: boolean showUsage?: boolean borderColor?: string + showDeleteMessage?: boolean + instanceId?: string + sessionId?: string + messageId?: string } interface CompactionCardProps { @@ -769,11 +824,13 @@ interface CompactionCardProps { sessionId: string messageId: string partId: string + showDeleteMessage?: boolean } function CompactionCard(props: CompactionCardProps) { const { t } = useI18n() const [deleting, setDeleting] = createSignal(false) + const [deletingMessage, setDeletingMessage] = createSignal(false) const isAuto = () => Boolean((props.part as any)?.auto) const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel")) const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR) @@ -801,6 +858,27 @@ function CompactionCard(props: CompactionCardProps) { } } + const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage() + + const handleDeleteMessage = async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + if (!props.showDeleteMessage) return + 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) + } + } + return (
- +
+ + + + + +
diff --git a/packages/ui/src/lib/i18n/messages/en/messaging.ts b/packages/ui/src/lib/i18n/messages/en/messaging.ts index 61783e6f..1763ec71 100644 --- a/packages/ui/src/lib/i18n/messages/en/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/en/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Go to Session", "messageBlock.tool.goToSession.title": "Go to session", "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.title": "Delete this tool call output", "messageBlock.tool.deletePart.failed.title": "Delete failed", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "Copy", "messageItem.actions.copyTitle": "Copy message", "messageItem.actions.copied": "Copied!", + "messageItem.actions.deleteMessage": "Delete message", + "messageItem.actions.deletingMessage": "Deleting...", + "messageItem.actions.deleteMessageFailedTitle": "Delete failed", + "messageItem.actions.deleteMessageFailedMessage": "Failed to delete message", "messageItem.status.queued": "QUEUED", "messageItem.status.generating": "Generating...", "messageItem.status.sending": "Sending...", "messageItem.status.failedToSend": "Message failed to send", - "messagePart.actions.delete": "Delete", + "messagePart.actions.delete": "Delete Part", "messagePart.actions.deleting": "Deleting...", "messagePart.actions.deleteTitle": "Delete this item", "messagePart.actions.deleteFailedTitle": "Delete failed", diff --git a/packages/ui/src/lib/i18n/messages/es/messaging.ts b/packages/ui/src/lib/i18n/messages/es/messaging.ts index dd10f351..8391aa8e 100644 --- a/packages/ui/src/lib/i18n/messages/es/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/es/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Ir a 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.deletePart.label": "Eliminar", + "messageBlock.tool.deletePart.label": "Eliminar parte", "messageBlock.tool.deletePart.deleting": "Eliminando...", "messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta", "messageBlock.tool.deletePart.failed.title": "Error al eliminar", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "Copiar", "messageItem.actions.copyTitle": "Copiar mensaje", "messageItem.actions.copied": "¡Copiado!", + "messageItem.actions.deleteMessage": "Eliminar mensaje", + "messageItem.actions.deletingMessage": "Eliminando...", + "messageItem.actions.deleteMessageFailedTitle": "Error al eliminar", + "messageItem.actions.deleteMessageFailedMessage": "No se pudo eliminar el mensaje", "messageItem.status.queued": "EN COLA", "messageItem.status.generating": "Generando...", "messageItem.status.sending": "Enviando...", "messageItem.status.failedToSend": "No se pudo enviar el mensaje", - "messagePart.actions.delete": "Eliminar", + "messagePart.actions.delete": "Eliminar parte", "messagePart.actions.deleting": "Eliminando...", "messagePart.actions.deleteTitle": "Eliminar este elemento", "messagePart.actions.deleteFailedTitle": "Error al eliminar", diff --git a/packages/ui/src/lib/i18n/messages/fr/messaging.ts b/packages/ui/src/lib/i18n/messages/fr/messaging.ts index 6bb886ae..ca678355 100644 --- a/packages/ui/src/lib/i18n/messages/fr/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/fr/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Aller à la session", "messageBlock.tool.goToSession.title": "Aller à la session", "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.title": "Supprimer cette sortie d'outil", "messageBlock.tool.deletePart.failed.title": "Échec de suppression", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "Copier", "messageItem.actions.copyTitle": "Copier le message", "messageItem.actions.copied": "Copié !", + "messageItem.actions.deleteMessage": "Supprimer le message", + "messageItem.actions.deletingMessage": "Suppression...", + "messageItem.actions.deleteMessageFailedTitle": "Échec de suppression", + "messageItem.actions.deleteMessageFailedMessage": "Impossible de supprimer le message", "messageItem.status.queued": "EN FILE", "messageItem.status.generating": "Génération...", "messageItem.status.sending": "Envoi...", "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.deleteTitle": "Supprimer cet élément", "messagePart.actions.deleteFailedTitle": "Échec de suppression", diff --git a/packages/ui/src/lib/i18n/messages/ja/messaging.ts b/packages/ui/src/lib/i18n/messages/ja/messaging.ts index 456d9f4b..ebbeb3c0 100644 --- a/packages/ui/src/lib/i18n/messages/ja/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ja/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "セッションへ移動", "messageBlock.tool.goToSession.title": "セッションへ移動", "messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません", - "messageBlock.tool.deletePart.label": "削除", + "messageBlock.tool.deletePart.label": "パートを削除", "messageBlock.tool.deletePart.deleting": "削除中...", "messageBlock.tool.deletePart.title": "このツール出力を削除", "messageBlock.tool.deletePart.failed.title": "削除に失敗しました", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "コピー", "messageItem.actions.copyTitle": "メッセージをコピー", "messageItem.actions.copied": "コピーしました!", + "messageItem.actions.deleteMessage": "メッセージを削除", + "messageItem.actions.deletingMessage": "削除中...", + "messageItem.actions.deleteMessageFailedTitle": "削除に失敗しました", + "messageItem.actions.deleteMessageFailedMessage": "メッセージの削除に失敗しました", "messageItem.status.queued": "待機中", "messageItem.status.generating": "生成中...", "messageItem.status.sending": "送信中...", "messageItem.status.failedToSend": "メッセージの送信に失敗しました", - "messagePart.actions.delete": "削除", + "messagePart.actions.delete": "パートを削除", "messagePart.actions.deleting": "削除中...", "messagePart.actions.deleteTitle": "この項目を削除", "messagePart.actions.deleteFailedTitle": "削除に失敗しました", diff --git a/packages/ui/src/lib/i18n/messages/ru/messaging.ts b/packages/ui/src/lib/i18n/messages/ru/messaging.ts index 4021c18b..a88c665e 100644 --- a/packages/ui/src/lib/i18n/messages/ru/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ru/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Перейти к сессии", "messageBlock.tool.goToSession.title": "Перейти к сессии", "messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна", - "messageBlock.tool.deletePart.label": "Удалить", + "messageBlock.tool.deletePart.label": "Удалить часть", "messageBlock.tool.deletePart.deleting": "Удаление...", "messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента", "messageBlock.tool.deletePart.failed.title": "Ошибка удаления", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "Копировать", "messageItem.actions.copyTitle": "Копировать сообщение", "messageItem.actions.copied": "Скопировано!", + "messageItem.actions.deleteMessage": "Удалить сообщение", + "messageItem.actions.deletingMessage": "Удаление...", + "messageItem.actions.deleteMessageFailedTitle": "Ошибка удаления", + "messageItem.actions.deleteMessageFailedMessage": "Не удалось удалить сообщение", "messageItem.status.queued": "В ОЧЕРЕДИ", "messageItem.status.generating": "Генерация…", "messageItem.status.sending": "Отправка…", "messageItem.status.failedToSend": "Не удалось отправить сообщение", - "messagePart.actions.delete": "Удалить", + "messagePart.actions.delete": "Удалить часть", "messagePart.actions.deleting": "Удаление...", "messagePart.actions.deleteTitle": "Удалить этот элемент", "messagePart.actions.deleteFailedTitle": "Ошибка удаления", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts index e8a252af..8fe6f71b 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "前往会话", "messageBlock.tool.goToSession.title": "前往会话", "messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用", - "messageBlock.tool.deletePart.label": "删除", + "messageBlock.tool.deletePart.label": "删除部分", "messageBlock.tool.deletePart.deleting": "正在删除...", "messageBlock.tool.deletePart.title": "删除此工具输出", "messageBlock.tool.deletePart.failed.title": "删除失败", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "复制", "messageItem.actions.copyTitle": "复制消息", "messageItem.actions.copied": "已复制!", + "messageItem.actions.deleteMessage": "删除消息", + "messageItem.actions.deletingMessage": "正在删除...", + "messageItem.actions.deleteMessageFailedTitle": "删除失败", + "messageItem.actions.deleteMessageFailedMessage": "无法删除消息", "messageItem.status.queued": "排队中", "messageItem.status.generating": "正在生成...", "messageItem.status.sending": "正在发送...", "messageItem.status.failedToSend": "消息发送失败", - "messagePart.actions.delete": "删除", + "messagePart.actions.delete": "删除部分", "messagePart.actions.deleting": "正在删除...", "messagePart.actions.deleteTitle": "删除此项", "messagePart.actions.deleteFailedTitle": "删除失败", diff --git a/packages/ui/src/stores/session-actions.ts b/packages/ui/src/stores/session-actions.ts index 456f5456..f94f6ce7 100644 --- a/packages/ui/src/stores/session-actions.ts +++ b/packages/ui/src/stores/session-actions.ts @@ -7,7 +7,7 @@ import { providers, sessions, withSession } from "./session-state" import { getDefaultModel, isModelValid } from "./session-models" import { updateSessionInfo } from "./message-v2/session-info" 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 { requestData } from "../lib/opencode-api" @@ -439,8 +439,33 @@ async function deleteMessagePart(instanceId: string, sessionId: string, messageI updateSessionInfo(instanceId, sessionId) } +async function deleteMessage(instanceId: string, sessionId: string, messageId: string): Promise { + 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 { abortSession, + deleteMessage, deleteMessagePart, executeCustomCommand, renameSession, From 57b81f00f8686f1700273d6acff377b42656bc31 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Wed, 25 Feb 2026 22:54:49 +0000 Subject: [PATCH 16/59] chore(ui): reorder user message actions --- packages/ui/src/components/message-item.tsx | 38 ++++++++++--------- .../ui/src/lib/i18n/messages/en/messaging.ts | 2 +- .../ui/src/lib/i18n/messages/es/messaging.ts | 2 +- .../ui/src/lib/i18n/messages/fr/messaging.ts | 2 +- .../ui/src/lib/i18n/messages/ja/messaging.ts | 2 +- .../ui/src/lib/i18n/messages/ru/messaging.ts | 2 +- .../lib/i18n/messages/zh-Hans/messaging.ts | 2 +- 7 files changed, 26 insertions(+), 24 deletions(-) diff --git a/packages/ui/src/components/message-item.tsx b/packages/ui/src/components/message-item.tsx index e5d91700..8b603acc 100644 --- a/packages/ui/src/components/message-item.tsx +++ b/packages/ui/src/components/message-item.tsx @@ -316,16 +316,15 @@ export default function MessageItem(props: MessageItemProps) {
- - - + + - + + + +
) From f1bd681618508d8c8b736bc86570a843267ffad1 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Thu, 26 Feb 2026 10:25:38 +0000 Subject: [PATCH 21/59] chore(ui): remove delete-part actions and use trash for delete --- packages/ui/src/components/message-block.tsx | 202 ++---------------- packages/ui/src/components/message-item.tsx | 120 +---------- .../ui/src/components/message-timeline.tsx | 10 - packages/ui/src/types/delete-hover.ts | 1 - 4 files changed, 22 insertions(+), 311 deletions(-) diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index 4f9560f7..7b18047d 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -1,5 +1,5 @@ import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js" -import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, MessageSquareX, Trash2 } from "lucide-solid" +import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, Trash2 } from "lucide-solid" import MessageItem from "./message-item" import ToolCall from "./tool-call" import type { InstanceMessageStore } from "../stores/message-v2/instance-store" @@ -12,7 +12,6 @@ import { formatTokenTotal } from "../lib/formatters" import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions" import { setActiveInstanceId } from "../stores/instances" import { showAlertDialog } from "../stores/alerts" -import { deleteMessagePart } from "../stores/session-actions" import { deleteMessage } from "../stores/session-actions" import { useI18n } from "../lib/i18n" import type { DeleteHoverState } from "../types/delete-hover" @@ -199,7 +198,6 @@ interface MessageContentItemProps { onFork?: (messageId?: string) => void onContentRendered?: () => void showDeleteMessage?: boolean - deleteHover?: () => DeleteHoverState onDeleteHoverChange?: (state: DeleteHoverState) => void } @@ -288,7 +286,6 @@ function MessageContentItem(props: MessageContentItemProps) { isQueued={isQueued()} showAgentMeta={showAgentMeta()} showDeleteMessage={props.showDeleteMessage} - deleteHover={props.deleteHover} onDeleteHoverChange={props.onDeleteHoverChange} onRevert={props.onRevert} onFork={props.onFork} @@ -307,15 +304,12 @@ interface ToolCallItemProps { partId: string onContentRendered?: () => void showDeleteMessage?: boolean - deleteHover?: () => DeleteHoverState onDeleteHoverChange?: (state: DeleteHoverState) => void } function ToolCallItem(props: ToolCallItemProps) { const { t } = useI18n() - const [deleting, setDeleting] = createSignal(false) const [deletingMessage, setDeletingMessage] = createSignal(false) - const [hoverDeletePart, setHoverDeletePart] = createSignal(false) const record = createMemo(() => props.store().getMessage(props.messageId)) const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId)) @@ -332,14 +326,6 @@ function ToolCallItem(props: ToolCallItemProps) { const messageVersion = createMemo(() => record()?.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 state = toolState() if (!state) return "" @@ -363,26 +349,6 @@ function ToolCallItem(props: ToolCallItemProps) { navigateToTaskSession(location) } - const handleDeleteToolPart = async (event: MouseEvent) => { - event.preventDefault() - event.stopPropagation() - - if (deleteDisabled()) return - - setDeleting(true) - try { - await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId) - } catch (error) { - showAlertDialog(t("messageBlock.tool.deletePart.failed.message"), { - title: t("messageBlock.tool.deletePart.failed.title"), - detail: error instanceof Error ? error.message : String(error), - variant: "error", - }) - } finally { - setDeleting(false) - } - } - const handleDeleteMessage = async (event: MouseEvent) => { event.preventDefault() event.stopPropagation() @@ -404,18 +370,10 @@ function ToolCallItem(props: ToolCallItemProps) { } } - const isDeleteHoveredFromStore = () => { - const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState) - return hover.kind === "part" && hover.messageId === props.messageId && hover.partId === props.partId - } - return ( {(resolvedToolPart) => ( -
+
{TOOL_ICON} @@ -423,7 +381,7 @@ function ToolCallItem(props: ToolCallItemProps) { {toolName() || t("messageBlock.tool.unknown")}
-
+
- - - - -
+
+
@@ -820,9 +757,7 @@ export default function MessageBlock(props: MessageBlockProps) { instanceId={props.instanceId} sessionId={props.sessionId} messageId={(item as CompactionDisplayItem).messageId} - partId={(item as CompactionDisplayItem).partId} showDeleteMessage={index() === 0} - deleteHover={props.deleteHover} onDeleteHoverChange={props.onDeleteHoverChange} /> @@ -833,11 +768,9 @@ export default function MessageBlock(props: MessageBlockProps) { instanceId={props.instanceId} sessionId={props.sessionId} messageId={(item as ReasoningDisplayItem).messageId} - partId={(item as ReasoningDisplayItem).partId} showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta} defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded} showDeleteMessage={index() === 0} - deleteHover={props.deleteHover} onDeleteHoverChange={props.onDeleteHoverChange} /> @@ -871,17 +804,13 @@ interface CompactionCardProps { instanceId: string sessionId: string messageId: string - partId: string showDeleteMessage?: boolean - deleteHover?: () => DeleteHoverState onDeleteHoverChange?: (state: DeleteHoverState) => void } function CompactionCard(props: CompactionCardProps) { const { t } = useI18n() - const [deleting, setDeleting] = createSignal(false) const [deletingMessage, setDeletingMessage] = createSignal(false) - const [hoverDeletePart, setHoverDeletePart] = createSignal(false) const isAuto = () => Boolean((props.part as any)?.auto) const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel")) const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR) @@ -889,26 +818,6 @@ function CompactionCard(props: CompactionCardProps) { const containerClass = () => `message-compaction-card ${isAuto() ? "message-compaction-card--auto" : "message-compaction-card--manual"}` - const canDelete = () => Boolean(props.partId) && !deleting() - - const handleDelete = async (event: MouseEvent) => { - event.preventDefault() - event.stopPropagation() - if (!canDelete()) return - setDeleting(true) - try { - await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId) - } catch (error) { - showAlertDialog(t("messagePart.actions.deleteFailedMessage"), { - title: t("messagePart.actions.deleteFailedTitle"), - detail: error instanceof Error ? error.message : String(error), - variant: "error", - }) - } finally { - setDeleting(false) - } - } - const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage() const handleDeleteMessage = async (event: MouseEvent) => { @@ -930,15 +839,9 @@ function CompactionCard(props: CompactionCardProps) { } } - const isDeleteHoveredFromStore = () => { - const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState) - return hover.kind === "part" && hover.messageId === props.messageId && hover.partId === props.partId - } - return (
-
@@ -1097,7 +981,7 @@ function StepCard(props: StepCardProps) { title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")} aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")} > -