Compare commits

..

16 Commits

Author SHA1 Message Date
Shantur Rathore
0e5695a903 ui: emphasize command palette button 2026-02-20 00:24:24 +00:00
Shantur Rathore
77103b7292 ui: use Check icon for completed status 2026-02-20 00:14:02 +00:00
Shantur Rathore
b14a144ddc ui: use lucide status icons for tool calls 2026-02-20 00:08:07 +00:00
Shantur Rathore
8ac67311d8 ui: use emoji status icons for tool calls 2026-02-19 23:51:33 +00:00
Shantur Rathore
0c97db393c fix(ui): expand read tool calls on error 2026-02-19 21:16:14 +00:00
Shantur Rathore
614c300d2f ui: default tool input visibility to collapsed 2026-02-19 21:12:39 +00:00
Shantur Rathore
e6ca4bd43d fix(ui): let palette tool input visibility override per-call 2026-02-19 20:46:46 +00:00
Shantur Rathore
84f81cf829 ui: left-align tool IO header text 2026-02-19 20:44:22 +00:00
Shantur Rathore
3760ba2d7f fix(ui): scope tool input toggle to current tool call 2026-02-19 20:42:23 +00:00
Shantur Rathore
09e7a3f8da feat(ui): add tool input visibility preference 2026-02-19 20:37:48 +00:00
Shantur Rathore
c55d56c94b ui: remove semibold from IO headers 2026-02-19 18:44:57 +00:00
Shantur Rathore
cc53123bcd ui: remove extra padding around IO sections 2026-02-19 18:44:29 +00:00
Shantur Rathore
d64027d43d ui: refine tool IO accordion styling 2026-02-19 18:43:06 +00:00
Shantur Rathore
6b7162f50f ui: add input/output accordions in tool calls 2026-02-19 18:37:46 +00:00
Shantur Rathore
5fd985f0c2 ui: rename tool input toggle and add IO headers 2026-02-19 18:31:41 +00:00
Shantur Rathore
2a438b2bb3 feat(ui): toggle tool call input yaml 2026-02-19 18:09:16 +00:00
35 changed files with 140 additions and 417 deletions

View File

@@ -3,11 +3,6 @@ name: Build and Upload Binaries
on:
workflow_call:
inputs:
ref:
description: "Git ref (branch, tag, or SHA) to build from"
required: false
default: ""
type: string
version:
description: "Version to apply to workspace packages (release builds)"
required: false
@@ -50,8 +45,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -92,8 +85,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -133,8 +124,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -175,8 +164,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -250,8 +237,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -325,8 +310,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -405,8 +388,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -509,8 +490,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
@@ -608,8 +587,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -1,13 +1,12 @@
name: Develop Pre-Release
on:
schedule:
# Nightly build of dev (only if dev has new commits)
- cron: "0 1 * * *"
push:
branches:
- dev
workflow_dispatch:
permissions:
actions: read
id-token: write
contents: write
@@ -16,63 +15,25 @@ concurrency:
cancel-in-progress: true
jobs:
gate:
prepare:
runs-on: ubuntu-latest
outputs:
run: ${{ steps.gate.outputs.run }}
dev_sha: ${{ steps.gate.outputs.dev_sha }}
version_suffix: ${{ steps.gate.outputs.version_suffix }}
version_suffix: ${{ steps.vars.outputs.version_suffix }}
steps:
- name: Decide whether to run
id: gate
- name: Compute version suffix
id: vars
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
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
SHA8="${GITHUB_SHA::8}"
DATE=$(date -u +%Y%m%d)
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"
echo "version_suffix=-dev-${DATE}-${SHA8}" >> "$GITHUB_OUTPUT"
prerelease:
needs: gate
if: ${{ needs.gate.outputs.run == 'true' }}
needs: prepare
uses: ./.github/workflows/reusable-release.yml
with:
ref: ${{ needs.gate.outputs.dev_sha }}
version_suffix: ${{ needs.gate.outputs.version_suffix }}
version_suffix: ${{ needs.prepare.outputs.version_suffix }}
npm_package_name: "@neuralnomads/codenomad-dev"
dist_tag: latest
prerelease: true

View File

@@ -19,10 +19,6 @@ on:
type: string
workflow_call:
inputs:
ref:
required: false
default: ""
type: string
version:
required: true
type: string
@@ -50,8 +46,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -1,13 +1,7 @@
name: Release UI
on:
workflow_call:
inputs:
ref:
description: "Git ref (branch, tag, or SHA) to build from"
required: false
default: ""
type: string
workflow_call: {}
workflow_dispatch: {}
permissions:
@@ -24,8 +18,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -3,11 +3,6 @@ name: Reusable Release
on:
workflow_call:
inputs:
ref:
description: "Git ref (branch, tag, or SHA) to build from"
required: false
default: ""
type: string
version_suffix:
description: "Suffix appended to package.json version"
required: false
@@ -51,8 +46,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node
uses: actions/setup-node@v4
@@ -91,7 +84,6 @@ jobs:
needs: prepare-release
uses: ./.github/workflows/build-and-upload.yml
with:
ref: ${{ inputs.ref || github.ref }}
version: ${{ needs.prepare-release.outputs.version }}
tag: ${{ needs.prepare-release.outputs.tag }}
release_name: ${{ needs.prepare-release.outputs.release_name }}
@@ -103,8 +95,6 @@ jobs:
permissions:
contents: read
uses: ./.github/workflows/release-ui.yml
with:
ref: ${{ inputs.ref || github.ref }}
secrets: inherit
publish-server:
@@ -113,7 +103,6 @@ jobs:
- build-and-upload
uses: ./.github/workflows/manual-npm-publish.yml
with:
ref: ${{ inputs.ref || github.ref }}
version: ${{ needs.prepare-release.outputs.version }}
dist_tag: ${{ inputs.dist_tag }}
package_name: ${{ inputs.npm_package_name }}

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "codenomad-workspace",
"version": "0.11.4",
"version": "0.11.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codenomad-workspace",
"version": "0.11.4",
"version": "0.11.3",
"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.3",
"license": "MIT",
"dependencies": {
"@codenomad/ui": "file:../ui",
@@ -12021,7 +12021,7 @@
},
"packages/server": {
"name": "@neuralnomads/codenomad",
"version": "0.11.4",
"version": "0.11.3",
"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.3",
"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.3",
"license": "MIT",
"dependencies": {
"@git-diff-view/solid": "^0.0.8",

View File

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

View File

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

View File

@@ -4,6 +4,6 @@
"private": true,
"license": "MIT",
"dependencies": {
"@opencode-ai/plugin": "1.2.10"
"@opencode-ai/plugin": "1.2.6"
}
}

View File

@@ -5,21 +5,18 @@
## Features & Capabilities
### 🌍 Deployment Freedom
- **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.
- **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.
### ⚡️ Workspace Power
- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
- **Long-Context Native**: Scroll through massive transcripts without hitches.
- **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.
## Prerequisites
- **OpenCode**: `opencode` must be installed and configured on your system.
- Node.js 18+ and npm (for running or building from source).
- A workspace folder on disk you want to serve.
@@ -28,7 +25,6 @@
## Usage
### Run via npx (Recommended)
You can run CodeNomad directly without installing it:
```sh
@@ -47,7 +43,6 @@ On startup, CodeNomad prints two URLs:
- `Remote Connection URL : ...` (used by browsers/other machines when remote access is enabled)
### Install Globally
Or install it globally to use the `codenomad` command:
```sh
@@ -56,7 +51,6 @@ codenomad --launch
```
### Install Locally (per-project)
If you prefer to install CodeNomad into a project and run the local binary:
```sh
@@ -67,7 +61,6 @@ npx codenomad --launch
(`npx codenomad ...` will use `./node_modules/.bin/codenomad` when present.)
### Common Flags
You can configure the server using flags or environment variables:
| Flag | Env Variable | Description |
@@ -81,7 +74,7 @@ You can configure the server using flags or environment variables:
| `--tls-ca <path>` | `CLI_TLS_CA` | Optional CA chain/bundle (PEM) |
| `--tlsSANs <list>` | `CLI_TLS_SANS` | Additional TLS SANs (comma-separated) |
| `--host <addr>` | `CLI_HOST` | Interface to bind (default 127.0.0.1) |
| `--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. |
| `--workspace-root <path>` | `CLI_WORKSPACE_ROOT` | Default root for new workspaces |
| `--unrestricted-root` | `CLI_UNRESTRICTED_ROOT` | Allow full-filesystem browsing |
| `--config <path>` | `CLI_CONFIG` | Config file location |
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
@@ -94,11 +87,10 @@ You can configure the server using flags or environment variables:
| `--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-no-update` | `CLI_UI_NO_UPDATE` | Disable remote UI updates |
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (`true` |
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (true|false) |
| `--ui-manifest-url <url>` | `CLI_UI_MANIFEST_URL` | Remote UI manifest URL |
### Dev Releases (Advanced)
If you want the latest bleeding-edge builds (published as GitHub pre-releases), use the dev package:
```sh
@@ -149,14 +141,12 @@ codenomad --tlsSANs "localhost,127.0.0.1,my-hostname,192.168.1.10"
```
### Authentication
- 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.
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.
### 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.
1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.).
@@ -168,6 +158,5 @@ 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).
### Data Storage
- **Config**: `~/.config/codenomad/config.json`
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)

View File

@@ -1,12 +1,12 @@
{
"name": "@neuralnomads/codenomad",
"version": "0.11.4",
"version": "0.11.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@neuralnomads/codenomad",
"version": "0.11.4",
"version": "0.11.3",
"dependencies": {
"@fastify/cors": "^8.5.0",
"@fastify/reply-from": "^9.8.0",

View File

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

View File

@@ -78,7 +78,7 @@ function parseCliOptions(argv: string[]): CliOptions {
.addOption(new Option("--tls-ca <path>", "TLS CA chain (PEM)").env("CLI_TLS_CA"))
.addOption(new Option("--tlsSANs <list>", "Additional TLS SANs (comma-separated)").env("CLI_TLS_SANS"))
.addOption(
new Option("--workspace-root <path>", "Restricts root path where workspaces can be opened").env("CLI_WORKSPACE_ROOT").default(process.cwd()),
new Option("--workspace-root <path>", "Workspace root directory").env("CLI_WORKSPACE_ROOT").default(process.cwd()),
)
.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))

View File

@@ -8,7 +8,7 @@ import { FileSystemBrowser } from "../filesystem/browser"
import { searchWorkspaceFiles, WorkspaceFileSearchOptions } from "../filesystem/search"
import { clearWorkspaceSearchCache } from "../filesystem/search-cache"
import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types"
import { WorkspaceRuntime, ProcessExitInfo } from "./runtime"
import { WorkspaceRuntime, ProcessExitInfo, probeBinaryVersion } from "./runtime"
import { Logger } from "../logger"
import { getOpencodeConfigDir } from "../opencode-config.js"
import {
@@ -109,6 +109,10 @@ export class WorkspaceManager {
updatedAt: new Date().toISOString(),
}
if (!descriptor.binaryVersion) {
descriptor.binaryVersion = this.detectBinaryVersion(resolvedBinaryPath)
}
this.workspaces.set(id, descriptor)
@@ -145,10 +149,7 @@ export class WorkspaceManager {
onExit: (info) => this.handleProcessExit(info.workspaceId, info),
})
const runtimeVersion = await this.waitForWorkspaceReadiness({ workspaceId: id, port, exitPromise, getLastOutput })
if (runtimeVersion) {
descriptor.binaryVersion = runtimeVersion
}
await this.waitForWorkspaceReadiness({ workspaceId: id, port, exitPromise, getLastOutput })
descriptor.pid = pid
descriptor.port = port
@@ -277,12 +278,36 @@ export class WorkspaceManager {
return candidates[0] ?? ""
}
private detectBinaryVersion(resolvedPath: string): string | undefined {
if (!resolvedPath) {
return undefined
}
const result = probeBinaryVersion(resolvedPath)
if (result.valid) {
if (result.version) {
this.options.logger.debug({ binary: resolvedPath, version: result.version }, "Detected binary version")
return result.version
}
if (result.reported) {
this.options.logger.debug({ binary: resolvedPath, reported: result.reported }, "Binary reported version string")
return result.reported
}
return undefined
}
if (result.error) {
this.options.logger.warn({ binary: resolvedPath, err: result.error }, "Failed to detect binary version")
}
return undefined
}
private async waitForWorkspaceReadiness(params: {
workspaceId: string
port: number
exitPromise: Promise<ProcessExitInfo>
getLastOutput: () => string
}): Promise<string | undefined> {
}) {
await Promise.race([
this.waitForPortAvailability(params.port),
@@ -296,7 +321,7 @@ export class WorkspaceManager {
}),
])
const version = await this.waitForInstanceHealth(params)
await this.waitForInstanceHealth(params)
await Promise.race([
this.delay(STARTUP_STABILITY_DELAY_MS),
@@ -309,8 +334,6 @@ export class WorkspaceManager {
)
}),
])
return version
}
private async waitForInstanceHealth(params: {
@@ -318,7 +341,7 @@ export class WorkspaceManager {
port: number
exitPromise: Promise<ProcessExitInfo>
getLastOutput: () => string
}): Promise<string | undefined> {
}) {
const probeResult = await Promise.race([
this.probeInstance(params.workspaceId, params.port),
params.exitPromise.then((info) => {
@@ -332,7 +355,7 @@ export class WorkspaceManager {
])
if (probeResult.ok) {
return probeResult.version
return
}
const latestOutput = params.getLastOutput().trim()
@@ -343,11 +366,8 @@ export class WorkspaceManager {
throw new Error(`Workspace ${params.workspaceId} failed health check: ${reason}.`)
}
private async probeInstance(
workspaceId: string,
port: number,
): Promise<{ ok: boolean; reason?: string; version?: string }> {
const url = `http://127.0.0.1:${port}/global/health`
private async probeInstance(workspaceId: string, port: number): Promise<{ ok: boolean; reason?: string }> {
const url = `http://127.0.0.1:${port}/project/current`
try {
const headers: Record<string, string> = {}
@@ -358,22 +378,11 @@ export class WorkspaceManager {
const response = await fetch(url, { headers })
if (!response.ok) {
const reason = `/global/health returned HTTP ${response.status}`
const reason = `health probe returned HTTP ${response.status}`
this.options.logger.debug({ workspaceId, status: response.status }, "Health probe returned server error")
return { ok: false, reason }
}
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 }
return { ok: true }
} catch (error) {
const reason = error instanceof Error ? error.message : String(error)
this.options.logger.debug({ workspaceId, err: error }, "Health probe failed")

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@codenomad/ui",
"version": "0.11.4",
"version": "0.11.3",
"private": true,
"license": "MIT",
"type": "module",

View File

@@ -18,8 +18,6 @@ import { useTheme } from "./lib/theme"
import { useCommands } from "./lib/hooks/use-commands"
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
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 { runtimeEnv } from "./lib/runtime-env"
import { useI18n } from "./lib/i18n"
@@ -77,6 +75,12 @@ const App: Component = () => {
setToolInputsVisibility,
} = useConfig()
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 [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
@@ -241,6 +245,35 @@ const App: Component = () => {
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) {
if (!folderPath) {
return
@@ -259,9 +292,13 @@ const App: Component = () => {
port: instances().get(instanceId)?.port,
})
} catch (error) {
const message = formatLaunchErrorMessage(error, t("app.launchError.fallbackMessage"))
const message = formatLaunchErrorMessage(error)
const missingBinary = isMissingBinaryMessage(message)
showLaunchError({ source: "create", message, binaryPath: selectedBinary, missingBinary })
setLaunchError({
message,
binaryPath: selectedBinary,
missingBinary,
})
log.error("Failed to create instance", error)
} finally {
setIsSelectingFolder(false)

View File

@@ -116,8 +116,11 @@ const AlertDialog: Component = () => {
<Dialog.Portal>
<Dialog.Overlay class="modal-overlay" />
<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}>
<div class="flex items-start gap-3">
<Dialog.Content
class="modal-surface w-full max-w-xl md:max-w-2xl p-6 border border-base shadow-2xl max-h-[85vh] overflow-hidden flex flex-col"
tabIndex={-1}
>
<div class="flex items-start gap-3 min-h-0">
<div
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border text-base font-semibold"
style={{
@@ -129,11 +132,16 @@ const AlertDialog: Component = () => {
>
{accent.symbol}
</div>
<div class="flex-1 min-w-0">
<div class="flex-1 min-w-0 min-h-0">
<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">
{payload.message}
{payload.detail && <p class="mt-2 text-secondary">{payload.detail}</p>}
<Dialog.Description class="text-sm text-secondary mt-1">
<div
class="max-h-[60vh] overflow-auto pr-2 whitespace-pre-wrap break-words"
style={{ "overflow-wrap": "anywhere" }}
>
{payload.message}
{payload.detail && <div class="mt-3">{payload.detail}</div>}
</div>
</Dialog.Description>
</div>
</div>

View File

@@ -625,7 +625,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<div class="flex flex-wrap items-center justify-center gap-1">
<button
type="button"
class="connection-status-button command-palette-button"
class="connection-status-button command-palette-button px-2 py-0.5 text-xs"
onClick={handleCommandPaletteClick}
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
style={{ flex: "0 0 auto", width: "auto" }}
@@ -721,7 +721,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<div class="session-toolbar-center flex items-center justify-center gap-2 min-w-[160px]">
<button
type="button"
class="connection-status-button command-palette-button"
class="connection-status-button command-palette-button px-2 py-0.5 text-xs"
onClick={handleCommandPaletteClick}
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
style={{ flex: "0 0 auto", width: "auto" }}

View File

@@ -1,9 +1,8 @@
import { For, Show, type Accessor, type Component } from "solid-js"
import type { ToolState } from "@opencode-ai/sdk"
import { Accordion } from "@kobalte/core"
import { Tooltip } from "@kobalte/core/tooltip"
import { ChevronDown, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
import { ChevronDown, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
import type { Instance } from "../../../../../types/instance"
import type { BackgroundProcess } from "../../../../../../../server/src/api-types"
@@ -207,25 +206,21 @@ const StatusTab: Component<StatusTabProps> = (props) => {
{
id: "session-changes",
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
tooltipKey: "instanceShell.rightPanel.sections.sessionChanges.tooltip",
render: renderStatusSessionChanges,
},
{
id: "plan",
labelKey: "instanceShell.rightPanel.sections.plan",
tooltipKey: "instanceShell.rightPanel.sections.plan.tooltip",
render: renderPlanSectionContent,
},
{
id: "background-processes",
labelKey: "instanceShell.rightPanel.sections.backgroundProcesses",
tooltipKey: "instanceShell.rightPanel.sections.backgroundProcesses.tooltip",
render: renderBackgroundProcesses,
},
{
id: "mcp",
labelKey: "instanceShell.rightPanel.sections.mcp",
tooltipKey: "instanceShell.rightPanel.sections.mcp.tooltip",
render: () => (
<InstanceServiceStatus
initialInstance={props.instance}
@@ -238,7 +233,6 @@ const StatusTab: Component<StatusTabProps> = (props) => {
{
id: "lsp",
labelKey: "instanceShell.rightPanel.sections.lsp",
tooltipKey: "instanceShell.rightPanel.sections.lsp.tooltip",
render: () => (
<InstanceServiceStatus
initialInstance={props.instance}
@@ -251,7 +245,6 @@ const StatusTab: Component<StatusTabProps> = (props) => {
{
id: "plugins",
labelKey: "instanceShell.rightPanel.sections.plugins",
tooltipKey: "instanceShell.rightPanel.sections.plugins.tooltip",
render: () => (
<InstanceServiceStatus
initialInstance={props.instance}
@@ -283,23 +276,7 @@ const StatusTab: Component<StatusTabProps> = (props) => {
<Accordion.Item value={section.id} class="right-panel-accordion-item">
<Accordion.Header>
<Accordion.Trigger class="right-panel-accordion-trigger">
<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>
<span>{props.t(section.labelKey)}</span>
<ChevronDown
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
/>

View File

@@ -351,9 +351,7 @@ export default function PromptInput(props: PromptInputProps) {
const blockquote = lines.map((line) => `> ${line}`).join("\n")
if (!blockquote) return
// 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`)
insertBlockContent(`${blockquote}\n`)
}
function insertCodeSelection(rawText: string) {

View File

@@ -287,9 +287,7 @@ export const taskRenderer: ToolRenderer = {
content: promptContent()!,
cacheKey: "task:prompt",
disableScrollTracking: 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,
disableHighlight: true,
})}
</div>
</section>

View File

@@ -96,17 +96,11 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
"instanceShell.rightPanel.actions.refresh": "Refresh",
"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.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.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.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.tooltip": "Language Server Protocol servers providing code intelligence, diagnostics, and language-specific features.",
"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.loading": "Fetching session changes...",

View File

@@ -48,7 +48,7 @@ export const instanceMessages = {
"instanceShell.commandPalette.openAriaLabel": "Abrir paleta de comandos",
"instanceShell.commandPalette.button": "Paleta de comandos",
"instanceShell.connection.ariaLabel": "Conexión {status}",
"instanceShell.connection.ariaLabel": "Connection {status}",
"instanceShell.connection.connected": "Conectada",
"instanceShell.connection.connecting": "Conectando...",
"instanceShell.connection.disconnected": "Desconectada",
@@ -93,22 +93,16 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.files": "Archivos",
"instanceShell.rightPanel.tabs.status": "Estado",
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
"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.sessionChanges": "Cambios de sesion",
"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.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.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.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.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 sesión para ver los cambios.",
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesión...",
"instanceShell.sessionChanges.empty": "Aún no hay cambios.",
"instanceShell.sessionChanges.noSessionSelected": "Selecciona una sesion para ver los cambios.",
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesion...",
"instanceShell.sessionChanges.empty": "Aun no hay cambios.",
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",

View File

@@ -94,17 +94,11 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "Statut",
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
"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.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.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.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.tooltip": "Serveurs du protocole Language Server Protocol fournissant l'intelligence de code et les diagnostics.",
"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.loading": "Récupération des changements...",

View File

@@ -94,17 +94,11 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "ステータス",
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
"instanceShell.rightPanel.sections.plan": "計画",
"instanceShell.rightPanel.sections.plan.tooltip": "このセッションにおけるエージェントのロードマップ。タスクやサブタスク、および完了状況を追跡します。",
"instanceShell.rightPanel.sections.backgroundProcesses": "バックグラウンドシェル",
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "エージェントが開始した長時間実行プロセス。出力を監視し、停止または終了できます。",
"instanceShell.rightPanel.sections.mcp": "MCP サーバー",
"instanceShell.rightPanel.sections.mcp.tooltip": "Model Context Protocol (MCP) サーバー。外部ツールやサービスでエージェントの機能を拡張します。",
"instanceShell.rightPanel.sections.lsp": "LSP サーバー",
"instanceShell.rightPanel.sections.lsp.tooltip": "Language Server Protocolサーバーがコードインテリジェンス、診断、言語固有の機能を提供します。",
"instanceShell.rightPanel.sections.plugins": "プラグイン",
"instanceShell.rightPanel.sections.plugins.tooltip": "UI とサーバーの動作をカスタマイズし、MCP や LSP 以外の機能も追加できるプラグイン。",
"instanceShell.sessionChanges.noSessionSelected": "変更を表示するにはセッションを選択してください。",
"instanceShell.sessionChanges.loading": "変更を取得中...",

View File

@@ -94,17 +94,11 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "Статус",
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
"instanceShell.rightPanel.sections.plan": "План",
"instanceShell.rightPanel.sections.plan.tooltip": "Дорожная карта агента для этой сессии. Отслеживает задачи и их статус выполнения.",
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые оболочки",
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Долгоработающие процессы, запущенные агентом. Вы можете следить за их выводом, останавливать или завершать их.",
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые Shell",
"instanceShell.rightPanel.sections.mcp": "MCP-серверы",
"instanceShell.rightPanel.sections.mcp.tooltip": "Серверы протокола Model Context Protocol, расширяющие возможности агента внешними инструментами.",
"instanceShell.rightPanel.sections.lsp": "LSP-серверы",
"instanceShell.rightPanel.sections.lsp.tooltip": "Серверы протокола Language Server Protocol, обеспечивающие интеллектуальную поддержку кода и диагностику.",
"instanceShell.rightPanel.sections.plugins": "Плагины",
"instanceShell.rightPanel.sections.plugins.tooltip": "Плагины, настраивающие поведение интерфейса и сервера, добавляющие функции поверх MCP и LSP.",
"instanceShell.sessionChanges.noSessionSelected": "Выберите сессию, чтобы просмотреть изменения.",
"instanceShell.sessionChanges.loading": "Загрузка изменений...",
@@ -134,7 +128,7 @@ export const instanceMessages = {
"versionPill.uiWithVersion": "UI {version}",
"versionPill.source": " ({source})",
"opencodeBinarySelector.title": "Бинарник OpenCode",
"opencodeBinarySelector.title": "OpenCode Binary",
"opencodeBinarySelector.subtitle": "Выберите, какой исполняемый файл OpenCode запускать",
"opencodeBinarySelector.customPath.placeholder": "Введите путь к бинарнику opencode…",
"opencodeBinarySelector.actions.add": "Добавить",

View File

@@ -94,17 +94,11 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "状态",
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
"instanceShell.rightPanel.sections.plan": "计划",
"instanceShell.rightPanel.sections.plan.tooltip": "代理的路线图。跟踪任务、子任务及其完成状态。",
"instanceShell.rightPanel.sections.backgroundProcesses": "后台 Shell",
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "代理启动的后台进程。您可以监控其输出、停止或终止它们。",
"instanceShell.rightPanel.sections.mcp": "MCP 服务器",
"instanceShell.rightPanel.sections.mcp.tooltip": "模型上下文协议服务器,使用外部工具和服务扩展代理能力。",
"instanceShell.rightPanel.sections.lsp": "LSP 服务器",
"instanceShell.rightPanel.sections.lsp.tooltip": "语言服务器协议服务器,提供代码智能、诊断和语言特定的功能。",
"instanceShell.rightPanel.sections.plugins": "插件",
"instanceShell.rightPanel.sections.plugins.tooltip": "自定义 UI 和服务器行为的插件,添加超出 MCP 和 LSP 的功能。",
"instanceShell.sessionChanges.noSessionSelected": "选择会话以查看更改。",
"instanceShell.sessionChanges.loading": "正在获取会话更改...",

View File

@@ -1,29 +0,0 @@
export function formatLaunchErrorMessage(error: unknown, fallbackMessage: string): string {
if (!error) {
return fallbackMessage
}
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error)
try {
const parsed = JSON.parse(raw) as unknown
if (parsed && typeof parsed === "object" && "error" in parsed && typeof (parsed as any).error === "string") {
return (parsed as any).error
}
} catch {
// ignore JSON parse errors
}
return raw
}
export function isMissingBinaryMessage(message: string): boolean {
const normalized = message.toLowerCase()
return (
normalized.includes("opencode binary not found") ||
normalized.includes("binary not found") ||
normalized.includes("no such file or directory") ||
normalized.includes("binary is not executable") ||
normalized.includes("enoent")
)
}

View File

@@ -35,7 +35,6 @@ import { upsertPermissionV2, removePermissionV2, upsertQuestionV2, removeQuestio
import { clearCacheForInstance } from "../lib/global-cache"
import { getLogger } from "../lib/logger"
import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata"
import { showWorkspaceLaunchError } from "./launch-errors"
const log = getLogger("api")
@@ -373,7 +372,6 @@ function handleWorkspaceEvent(event: WorkspaceEventPayload) {
break
case "workspace.error":
upsertWorkspace(event.workspace)
showWorkspaceLaunchError(event.workspace)
break
case "workspace.stopped":
releaseInstanceResources(event.workspaceId)

View File

@@ -1,53 +0,0 @@
import { createSignal } from "solid-js"
import type { WorkspaceDescriptor } from "../../../server/src/api-types"
import { tGlobal } from "../lib/i18n"
import { formatLaunchErrorMessage, isMissingBinaryMessage } from "../lib/launch-errors"
type LaunchErrorSource = "create" | "workspace"
export interface LaunchErrorState {
source: LaunchErrorSource
message: string
binaryPath: string
missingBinary: boolean
instanceId?: string
}
const [launchError, setLaunchError] = createSignal<LaunchErrorState | null>(null)
// Avoid spamming the user with the same modal on repeated events.
const lastWorkspaceErrorByInstanceId = new Map<string, string>()
export function showLaunchError(next: LaunchErrorState) {
setLaunchError(next)
}
export function clearLaunchError() {
setLaunchError(null)
}
export function showWorkspaceLaunchError(workspace: WorkspaceDescriptor) {
const instanceId = workspace.id
const rawMessage = workspace.error
const message = formatLaunchErrorMessage(rawMessage, tGlobal("app.launchError.fallbackMessage"))
const previous = lastWorkspaceErrorByInstanceId.get(instanceId)
if (previous && previous === message) {
return
}
lastWorkspaceErrorByInstanceId.set(instanceId, message)
const binaryPath = (workspace.binaryLabel || workspace.binaryId || "opencode").trim() || "opencode"
const missingBinary = isMissingBinaryMessage(message)
showLaunchError({
source: "workspace",
instanceId,
message,
binaryPath,
missingBinary,
})
}
export { launchError }

View File

@@ -40,7 +40,7 @@ import {
} from "./instances"
import { showAlertDialog } from "./alerts"
import { createClientSession, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
import { ensureSessionParentExpanded, sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
import { sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
import { normalizeMessagePart } from "./message-v2/normalizers"
import { updateSessionInfo } from "./message-v2/session-info"
import { tGlobal } from "../lib/i18n"
@@ -108,8 +108,6 @@ interface TuiToastEvent {
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
let parentToExpand: string | null = null
withSession(instanceId, sessionId, (session) => {
const current = session.status ?? "idle"
if (current === status) return false
@@ -119,17 +117,7 @@ function applySessionStatus(instanceId: string, sessionId: string, status: Sessi
}
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> {
@@ -170,7 +158,6 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
let updatedInstanceSessions: Map<string, Session> | undefined
let shouldExpandParent: string | null = null
setSessions((prev) => {
const next = new Map(prev)
@@ -187,19 +174,11 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
instanceSessions.set(sessionId, merged)
next.set(instanceId, instanceSessions)
updatedInstanceSessions = instanceSessions
if (merged.parentId && merged.status === "working" && (existing?.status ?? "idle") !== "working") {
shouldExpandParent = merged.parentId
}
return next
})
syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)
if (shouldExpandParent) {
ensureSessionParentExpanded(instanceId, shouldExpandParent)
}
return fetched
} catch (error) {
log.error("Failed to fetch session info", error)

View File

@@ -347,23 +347,10 @@ function clearActiveParentSession(instanceId: string): void {
}
function setSessionStatus(instanceId: string, sessionId: string, status: SessionStatus): void {
let parentToExpand: string | null = null
withSession(instanceId, sessionId, (session) => {
if (session.status === status) return false
const previous = session.status
session.status = status
// If a child session starts working, auto-expand its parent thread once.
// Users can still collapse it afterwards; we only expand on the transition.
if (session.parentId && status === "working" && previous !== "working") {
parentToExpand = session.parentId
}
})
if (parentToExpand) {
ensureSessionParentExpanded(instanceId, parentToExpand)
}
}
function getActiveParentSession(instanceId: string): Session | null {

View File

@@ -132,15 +132,13 @@
/* Make the command palette trigger stand out in the header. */
.connection-status-button.command-palette-button {
border-radius: 0;
@apply text-sm px-2 py-1 border border-base transition-colors;
background-color: var(--surface-base);
color: var(--text-primary);
border-color: var(--accent-primary);
background-color: var(--surface-secondary);
}
.connection-status-button.command-palette-button:hover {
border-color: var(--accent-primary);
background-color: var(--surface-hover);
color: var(--text-primary);
}
.connection-status-button:hover {

View File

@@ -412,7 +412,7 @@
}
.right-panel-accordion-trigger {
@apply w-full flex items-center justify-between px-3 py-2.5 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-150;
@apply w-full flex items-center justify-between gap-3 px-3 py-2.5 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-150;
color: var(--text-secondary);
background-color: transparent;
}
@@ -422,11 +422,6 @@
color: var(--text-primary);
}
.section-left {
@apply flex items-center;
flex-shrink: 0;
}
.right-panel-accordion-chevron {
@apply h-4 w-4 transition-transform duration-200;
color: var(--text-muted);
@@ -446,51 +441,6 @@
min-height: 0;
}
/* Section info tooltip */
.section-info-trigger {
@apply inline-flex items-center justify-center p-0.5 rounded transition-all duration-150;
color: var(--text-muted);
flex-shrink: 0;
}
.section-info-trigger:hover {
color: var(--text-primary);
background-color: var(--surface-hover);
}
.section-label {
margin-left: 2px;
}
.section-info-icon {
@apply w-3.5 h-3.5;
}
.section-info-tooltip {
@apply max-w-xs px-3 py-2 text-xs rounded-lg border shadow-lg;
background-color: var(--surface-base);
border-color: var(--border-base);
color: var(--text-primary);
animation: tooltipShow 150ms ease-out;
transform-origin: var(--kb-tooltip-content-transform-origin);
z-index: 9999;
}
.section-info-tooltip[data-expanded] {
animation: tooltipShow 150ms ease-out;
}
@keyframes tooltipShow {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Background process cards in status panel */
.status-process-card {
@apply rounded-lg border flex flex-col gap-2 p-3 transition-all duration-150;