feat: show description for active setting in /settings dialog - #4
Open
wenshao wants to merge 10 commits into
Open
feat: show description for active setting in /settings dialog#4wenshao wants to merge 10 commits into
wenshao wants to merge 10 commits into
Conversation
The /context command was missing the subcommand autocomplete feature that other commands like /stats have. Now users can type '/context ' and see 'detail' as a suggestion in the dropdown. - Added 'detail' subCommand to contextCommand with its own description - Subcommand delegates to main action with 'detail' arg - Added missing translation key for full description in zh.js - Updated commands.md documentation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…error output (QwenLM#3044) PR QwenLM#2943 fixed headers in buildHeaders() but the login flow in waitForLogin() still used a hardcoded incomplete header object. Reuse the shared buildHeaders() so all endpoints send consistent iLink-App-Id and iLink-App-ClientVersion headers. Also wrap channel.connect() in startSingle() with a try/catch so configuration errors print a clean message instead of dumping the yargs help text and a stack trace.
…enLM#3075) The "Compact Mode" label is more intuitive than "Verbose Mode" for users, as it directly describes the default compact view experience. This change inverts the boolean semantics (compactMode=false means show full output) and exposes the setting in the /settings dialog (showInDialog: true). - Rename ui.verboseMode → ui.compactMode with inverted default (false) - Rename VerboseModeContext → CompactModeContext (file and exports) - Rename TOGGLE_VERBOSE_MODE → TOGGLE_COMPACT_MODE in key bindings - Update all consumer components with inverted logic - Update i18n keys across 6 locales (verbose → compact) - Update VS Code settings schema - Add ui.compactMode documentation to settings.md - Fix Ctrl+O description in keyboard-shortcuts.md
… activation (QwenLM#3077) Replace vague "background tasks" with specific "prompt suggestions and speculative execution" in the --fast flag description across all i18n locales, docs, and VS Code schema. Update example model name from qwen3.5-flash to qwen3-coder-flash. Also fix completion logic to require a non-empty partial arg before suggesting --fast, preventing Tab+Enter from accidentally entering fast model mode. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-detail-missing fix(cli): add 'detail' subcommand to /context command
…#3069) Compact mode confirmation dialog uses ProceedAlways for "Allow always" option, but persistPermissionOutcome() only handled ProceedAlwaysProject and ProceedAlwaysUser, causing the permission to never be saved. Now ProceedAlways is treated as project scope (same as ProceedAlwaysProject).
…M#3086) Add "(--fast for suggestion model)" to the /model command description so users can discover the feature from the command list, since --fast completion no longer appears on empty input. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display the schema description of the currently highlighted setting below the settings list, so users can understand what each option does without needing to check external documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
wenshao
pushed a commit
that referenced
this pull request
May 21, 2026
* feat(cli): per-turn /diff with interactive dialog (QwenLM#4272) `/diff` now opens an interactive dialog in TUI mode with: - Current (working tree vs HEAD) plus one entry per past user turn - ←/→ to switch source, ↑/↓ to select a file, Enter for hunks, Esc to close - File list paginates at 8 entries, with new/deleted/untracked/binary tags Per-turn diffs are computed by FileHistoryService.getTurnDiff(promptId), which compares the snapshot at the start of that turn against the next snapshot (or the live worktree for the most recent turn). Files the snapshotter failed to capture are skipped rather than rendered against a stale predecessor. Non-interactive and ACP modes keep the existing plain-text summary so pipes, logs, and remote transports are unchanged. * refactor(core): align getTurnDiff promptId lookup with findSnapshot Two small audit-driven cleanups, no behavior change in normal sessions: - Match findSnapshot's last-occurrence-wins semantics so /rewind and /diff agree if a promptId is ever reused (defensive — promptIds are unique per submission in practice). - Drop the redundant `?? undefined` in the fast-path skip; `?.` already short-circuits to undefined, so the extra coalesce was noise. * fix(cli): head-truncate file paths in diff dialog to keep layout intact Long absolute paths (~> 90 chars) previously overflowed the dialog and wrapped, shattering the file-list and detail-view alignment. Reserve a fixed budget for the tag/stats columns, head-truncate the path with a leading ellipsis so the basename — the part users actually read — is always visible. Also drop the dead MAX_FILES_FOR_DETAILS guard from currentToFiles: fetchGitDiff already bounds perFileStats at MAX_FILES (=50), and returns an empty map when the diff exceeds MAX_FILES_FOR_DETAILS upstream, so the 500-entry counter could never fire. * fix(diff): address review comments — backup-read safety, oversized cap, sanitization, Ctrl+C routing Five review-driven fixes; details inline on the PR. Core (getTurnDiff): - Treat unreadable backup files as "unavailable" (return null for the row) instead of coercing to '' and fabricating phantom hunks. Same guard for both before and after endpoints. - Cap structuredPatch input at MAX_DIFF_SIZE_BYTES so a single multi-MB file in history can no longer balloon TUI memory when /diff opens. Oversized rows still surface in the file list with best-effort line stats and a new `oversized` flag. CLI (DiffDialog): - Distinguish over-large dirty trees (filesCount > 0 but empty perFileStats) from a clean tree; the empty state now reports the capped file count and totals instead of claiming "Working tree is clean." - Render the `oversized` flag with an explicit "(oversized — diff omitted)" tag in the file list and a corresponding detail-view note. Sanitization (#4): - Move sanitizeFilenameForDisplay from diffCommand.ts into the shared textUtils module, apply it to every path rendered in DiffDialog (file rows, detail header, empty messages, DiffRenderer filename prop, generated unified-diff envelope), and keep raw paths for map lookups via a separate UnifiedFile.displayPath field. Ctrl+C routing (QwenLM#7): - Register isDiffDialogOpen / closeDiffDialog with useDialogClose so Ctrl+C dismisses the dialog through the centralized handleExit path, matching how the background-tasks dialog is wired. Drop the dialog's internal Ctrl+C handler to avoid double-fire that would close the dialog AND escalate to the exit prompt. Tests: 2 new core regression tests (unreadable backup, oversized cap) plus the existing 35 still pass. CLI tests for diff/slashCommand/ AppContainer paths unchanged at 148/148. * fix(diff): second review round — candidate scope, binary, concurrency, semantics Addresses 14 of 19 outstanding review comments. Per-thread detail will be replied on the PR. Correctness (P0): - Restrict getTurnDiff candidates to keys(target.trackedFileBackups). Files first tracked in turn N+1 no longer get phantom-attributed to turn N. Drop the now-redundant union with state.trackedFiles for the latest-turn case (makeSnapshot guarantees state.trackedFiles ⊆ keys(latest.trackedFileBackups)). - Add `beforeBackup !== undefined` guard to the fast-path skip so a future broadening of the candidate set can't silently collapse a newly created file as "unchanged". - Add binary detection via NUL-byte sniff (`looksBinary`, mirrors git's heuristic). New `TurnFileDiff.isBinary` flag short-circuits hunk generation; the dialog renders the existing italic "binary" marker instead of feeding raw bytes to DiffRenderer. - Cap per-turn concurrent file reads at MAX_TURN_DIFF_FILES=500 so a 500-file turn won't issue 1000+ simultaneous open()s and hit the process fd ceiling. UX / stability: - Stabilize the dialog's keypress handler with `useCallback(()=>..,[])` reading state via refs, eliminating subscribe/unsubscribe churn on every render. - Disentangle `isNewFile` (snapshot-derived, "added in this turn") from `isUntracked` (git "never tracked") in perFileToUnified so untracked files no longer get mislabeled as "(new)" — they could not be recovered by /rewind, and the wrong tag implied otherwise. - Reorder FileRow tag priority around the disentangled flags; remove duplicate "(binary)" tag (the stats column already shows it italic). - Drop the early-exit `useEffect` clamps for sourceIndex / fileIndex in favor of inline `Math.min` derivations; effect-based clamping caused an extra render frame that could look like a flicker in Ink. - Inner `cancelled` checks in useTurnDiffs reduce wasted disk I/O when the dialog is closed mid-load. - Guard hunksToUnifiedDiff against empty hunk arrays (would otherwise hand DiffRenderer a header-only string). - Surface "…and N more (showing first M)" indicator for the Current source when fetchGitDiff capped perFileStats at MAX_FILES. - useDiffData JSDoc clarifies the snapshot-at-open semantics; catch branches now console.debug the underlying error instead of swallowing silently. Tests: - 3 new core regression tests: deleted-during-turn detection, binary detection, and the cross-turn attribution boundary. fileHistoryService tests now at 40/40. Pending review comments (deferred): the lazy-load suggestions remain intentionally deferred per the earlier reply chain; the MAX_DIFF_SIZE cap landed in the prior round mitigated the underlying memory risk. * fix(diff): third review round — per-file isolation, ENOENT semantics, binary tail scan Six review-driven correctness fixes; details inline on the PR. Core: - `readEndpointContent` now distinguishes ENOENT (genuine deletion) from other read failures (EACCES/EISDIR/EBUSY/decoding) on the live worktree branch. Previously every failure collapsed to `exists:false` and produced a phantom delete hunk for files whose perms changed mid-session. - `computeTurnFileDiff` is wrapped in per-file try/catch so a single `structuredPatch` crash or transient read error can no longer poison the whole turn's `Promise.all` and silently erase every row. - `looksBinary` now scans both the head AND the tail of the string. The head-only scan could be defeated by an 8KB+ text prefix in front of a binary payload; the oversized cap (1 MB) bounds the work either way. - `getTurnDiff` calls the existing `findSnapshotIndex` helper instead of inlining a duplicate reverse-scan loop, so a future change to `findSnapshot`'s tie-break rules can't silently desync /rewind and /diff. UI: - Add `hasHunks` to `UnifiedFile` and gate Enter on it. Untracked files don't appear in `git diff HEAD` output, and capped/oversized turn entries have empty hunks — pressing Enter on those previously landed the user on a dead-end "No hunks available" screen. - Drop the misleading `total > MAX_LINES_PER_FILE` heuristic from `perFileToUnified`'s `truncated` flag. `s.truncated` (from `parseGitNumstat`) is the only authoritative source — the OR was conflating "untracked file too big to count" with "tracked file with many accurately-counted lines", incorrectly flagging the latter. Tests: - 1 new core regression test: live-worktree EISDIR failure must not be reported as a deletion. fileHistoryService tests now at 41/41. * fix(diff): fourth review round — diagnostics, paths, UX feedback - Deterministic candidate cap in getTurnDiff: sort trackedFileBackups keys before slicing at MAX_TURN_DIFF_FILES; emit debugLogger.warn when truncating so the dropped count is traceable. - Log unreadable before/after endpoints in computeTurnFileDiffUnsafe instead of dropping rows silently — backup corruption, permission flips and EISDIR now leave a trace. - Return trackingPath as TurnFileDiff.filePath (already repo-relative via maybeShortenFilePath) so per-turn rows match the Current source on narrow terminals. The internal absolute path is kept only for live-worktree I/O. - useDiffData: replace bare console.debug with createDebugLogger ('DiffDialog') to match project convention. - DiffDialog: show a transient warning-coloured hint in the footer when Enter lands on a binary / oversized / no-hunks row (cleared on the next navigation key) so the keypress isn't silently consumed. - useDialogClose: swap diff-dialog and background-tasks branches to match DialogManager render order — Ctrl+C now dismisses whichever dialog the user actually sees when both flags are open. - useTurnDiffs: sanitize previewOfUserItem via escapeAnsiCtrlCodes so prompt previews on the source tabs can't reach the terminal raw (matching the chat-history defense). - Tests: expect repo-relative filePath in getTurnDiff regression cases; add `warn` to the mocked debugLogger. Refs PR QwenLM#4277 review comments 3259062434, 3259062465, 3264541365, 3259062480, 3259062498, 3264541346, 3264541351. * fix(diff): fifth review round — OOM guard, concurrency cap, type safety - readEndpointContent now stats both worktree and backup paths before readFile and returns a `{ kind: 'oversized' }` sentinel when the file exceeds MAX_DIFF_SIZE_BYTES. computeTurnFileDiffUnsafe handles the sentinel without allocating, so a 2 GB write_file blob no longer lands in the Node heap just to be rejected downstream. - useTurnDiffs now batches `getTurnDiff` calls at TURN_CONCURRENCY = 4 instead of an unbounded Promise.all across every user turn. Prevents EMFILE on long sessions (worst case ~4000 fds vs. unbounded N × 1000). - Add `filesOmitted` to `TurnDiff.stats` and plumb it through the dialog's `hiddenFileCount` so per-turn rows now also surface "…and N more" when MAX_TURN_DIFF_FILES truncates the candidate list (matches the Current source's existing behavior). - Make isRealUserTurn a type predicate (`item is HistoryItem & HistoryItemUser`) so callers in useTurnDiffs drop both `as` casts — a future regression that loosens either side will now be caught by tsc rather than silently bypassing the narrowing. - Add trailing `.catch()` to the Promise.all chains in useDiffData and useTurnDiffs so a thrown setState during unmount doesn't propagate to Node 22+'s default unhandled-rejection terminator. Both branches log via createDebugLogger and unstick `loading`. - Tighten the comment above the diff/background-tasks branch in useDialogClose: the invariant is scoped to that pair, not a full mirror of DialogManager's render priority. - Add focused unit tests for sanitizeFilenameForDisplay (C0 controls, DEL + C1, multi-byte ANSI CSI, mixed crafted paths, clean passthrough) — security-relevant function previously untested. Refs PR QwenLM#4277 review comments 3265032536, 3265032548, 3265032551, 3265032556, 3265032560, 3265032569, 3265032574. * fix(diff): sixth review round — discriminated union, TOCTOU, tests - Refactor EndpointRead into a proper discriminated union with explicit `kind: 'ok' | 'unreadable' | 'oversized'`. Removes the six manual `as EndpointReadOk / as EndpointReadOversized` casts in computeTurnFileDiffUnsafe; branch narrowing is now driven by tsc. - Close the stat()-then-readFile() TOCTOU window. Replace the separate syscalls with `open()` + `fh.stat()` + `fh.readFile()` against a single file descriptor, so a concurrent write_file appending to the same path between calls can't grow past MAX_DIFF_SIZE_BYTES and slip the OOM guard. Shared helper readPathWithSizeGuard handles both worktree and backup endpoints (worktree ENOENT → absence, backup ENOENT → unreadable to match prior semantics). - Document filesOmitted as an upper bound on candidates dropped at the cap (some may have been unchanged; we can't know without paying the read the cap was specifically meant to avoid). Surface that in the dialog's truncation indicator: turn sources now read "…and up to N more (showing first M)" while Current keeps the exact wording. - Tests: 3 new fileHistoryService cases covering the live-worktree oversized branch (single-snapshot path), mixed-size endpoints (small before + oversized after) exercising the discriminated-union narrowing, and a baseline filesOmitted === 0 regression. 7 new renderHook tests for useTurnDiffs covering disabled / missing-service short-circuits, filtering of slash/no-promptId/empty-diff turns, most-recent-first ordering, per-turn error isolation, batch progression beyond TURN_CONCURRENCY, and the in-flight concurrency cap itself. Refs PR QwenLM#4277 review comments 3267108813, 3267108827, 3267108831, 3267108839, 3267108847.
wenshao
pushed a commit
that referenced
this pull request
Jun 22, 2026
…ows, keyword trigger, notifications (QwenLM#4721) (QwenLM#5600) * feat(core): workflow() nested global — single-level saved-workflow invocation (QwenLM#4721 P-nested) Fills the P1 `workflow()` throwing stub with a real nested-workflow primitive. `workflow('<name>')` resolves a saved script from `.qwen/workflows/<name>.js` (project) or `~/.qwen/workflows/<name>.js` (user); `workflow({scriptPath})` reads an explicit path. The resolved script runs as a nested orchestration that SHARES the parent run's agent-count cap, concurrency window, token budget, and emitter — so nested phases/logs and token spend roll into the same registry entry and the global caps bound parent + nested together. Single-level nesting is enforced structurally: the orchestrator injects the `workflow` impl only at the top level, so a nested workflow's sandbox has no impl and a second-level `workflow()` call lands in the throwing else-branch. No depth counter to drift. - NEW `workflow-saved.ts`: `resolveSavedWorkflowScript(nameOrRef, config)` + `listSavedWorkflows` + `validateWorkflowName` + `WORKFLOW_NAME_PATTERN`. Shared with the upcoming CLI slash-command loader and save dialog. Project scope shadows user scope (matches FileCommandLoader precedence). - `Storage`: `getProjectWorkflowsDir` / `getUserWorkflowsDir` (scripts) + `getWorkflowRunsDir` / `getWorkflowRunSnapshotPath` / `getWorkflowRunJournalPath` (reserved for P6/P7b run artifacts). - `workflow-sandbox.ts`: `SandboxOptions.workflow` + `__b.hasWorkflow/ hostWorkflow` bridge. The vm `workflow()` global mirrors `agent()`: args sanitized vm→host via JSON round-trip, single result revived back into the vm realm (T1/T8/T14 escape defense). Else-branch throws a clear "unavailable / single-level limit" message. - `workflow-orchestrator.ts`: `WorkflowRunRequest.resolveSavedWorkflow` injection seam; `workflowImpl` built in `run()` closing over the shared countedDispatch/parallel/pipeline/budget/emitter; nested sandbox created WITHOUT a workflow impl. - `WorkflowTool`: wires `resolveSavedWorkflowScript(ref, this.config)`. Tests: workflow-saved.test.ts (20: name/scriptPath resolution, scope precedence, miss errors, name validation, listing). orchestrator P-nested block (7: resolve+return, nested args, shared agent cap, shared budget, single-level throw, no-resolver throw, resolver-reject surfaces to parent). sandbox (workflow() unavailable + injected-impl revival + scriptPath passthrough). 237 workflow tests pass, lint clean. Refs QwenLM#4721. * feat(core): workflow agent stall watchdog + retry (QwenLM#4721 P-stall) A workflow `agent()` could hang indefinitely — a looping model, a provider stalling mid-stream, or a tool that never returns would only be caught by the subagent's coarse 10-min `max_time_minutes`. P-stall adds a fine-grained per-dispatch stall watchdog: after `stallMs` (default 60s, env `QWEN_CODE_WORKFLOW_STALL_SECONDS`, per-call `agent({stallMs})`) of NO observable progress, the dispatch is aborted and retried up to 3 times before abandoning. "Progress" = any reasoning-loop event (round start/end, streamed text, token usage, tool call/result). The timer is SUSPENDED while a tool is in flight, so a legitimately long tool call (90s build, slow MCP) is never flagged — only true no-output-no-tool dead time counts toward the stall. Design (low-invasiveness): `runStallResilient` owns the per-attempt `AbortController` + `AgentEventEmitter`, chains the caller's parent signal into the per-attempt controller, and hands both into `runSingleDispatch` (the extracted single-attempt body of the former inline dispatch). A stall fires `controller.abort('stalled')` → the subagent returns CANCELLED → runSingleDispatch throws its "did not complete" terminal → the wrapper retries when `watchdog.stalled()` is set AND the parent signal is not aborted. Non-stall failures (MAX_TURNS / TIMEOUT / ERROR / schema-nudge-exhaustion) propagate immediately — a retry won't fix a deterministic outcome. Parent abort propagates without retry. Schema-mode rescue is free: if a stall fires after the subagent already captured a valid `structured_output`, runSingleDispatch returns that payload before the terminate-mode check, so the wrapper sees success. Uniform across both dispatch paths: the watchdog emitter is passed to the fast-path `AgentHeadless.create` (new arg 7) AND to `runOverridePath`, where `createSchemaEventEmitter` is refactored to `attachSchemaListeners` so the schema `structured_output` capture and the stall watchdog observe the SAME subagent emitter. - NEW `workflow-stall.ts`: `attachStallWatchdog`, `runStallResilient`, `resolveStallMs`, constants. 18 unit tests (fake-timer watchdog timing: fire / reset-on-activity / suspend-during-tool / dispose / disabled; retry loop: success / stall-3x-abandon / stall-then-recover / non-stall-no-retry / parent-abort-no-retry / abort-propagation / stallMs=0-passthrough). - `WorkflowAgentOpts.stallMs` + sandbox `KNOWN_AGENT_OPTS` allowlist. 334 workflow tests pass, lint clean. Scoped out (deliberate): throttle-backoff retry (degraded-GOAL detection + 45s sleep). It needs per-attempt duration/output-token threading into the wrapper and risks false-positives on legitimately short answers (a yes/no agent returns <50 tokens). The hang-prevention watchdog is the high-value core; throttle is a refinement that can follow. Refs QwenLM#4721. * fix(core): distinguish budget-exhausted slot drops in parallel/pipeline (QwenLM#4721 Gap-3) `settleToNullArray` (shared by `parallel()` / `pipeline()`) mapped every rejected thunk to `null` and logged each at debug level with the same "thunk rejected" message. A run that hits the token budget mid-fan-out drops its remaining slots via `WorkflowBudgetExceededError` — expected, capacity-shaped behaviour — but those drops were indistinguishable from arbitrary dispatch failures (rate limit, model outage) in the logs. Now budget-exhausted drops are counted separately (duck-typed on `err.name === 'WorkflowBudgetExceededError'`, since the cross-realm rejection's `instanceof` is unreliable) and summarized as `parallel: N slot(s) dropped — token budget exceeded`, while genuine failures keep their per-slot warning. Matches upstream's distinct budget-drop accounting. Behaviour to the script is unchanged (slots are still `null`); this is operator-facing observability only. Refs QwenLM#4721. * feat(core): same-session workflow resume via JSONL journal (QwenLM#4721 P6) `Workflow({resumeFromRunId})` re-runs a workflow and serves cached results for the longest UNCHANGED PREFIX of agent() calls — a run interrupted (crash, kill, network blip) at agent QwenLM#40 resumes by replaying the 39 journaled results instantly and only re-dispatching from the divergence point. Every run journals (so any run is resumable); the replay maps load only when resuming. Key derivation (upstream `v2` parity): each dispatch's key is `v2:sha256(prefixHash ‖ prompt ‖ canonicalOpts)`, where `prefixHash` is the PREVIOUS dispatch's key. The rolling chain is what gives "longest-unchanged-prefix" semantics — editing call #3 changes its key, which re-keys #4, #5… so the cache naturally invalidates from the edit point. `canonicalOpts` keeps only the dispatch-affecting opts (schema/model/isolation/agentType) with keys sorted, so a re-serialized schema or a label tweak doesn't bust the cache. Determinism (Date.now / Math.random throw in the sandbox) guarantees the key chain is stable across runs. Critical invariant ("first miss invalidates the suffix"): once ANY dispatch runs live during resume, `hadMiss` flips and no later dispatch trusts the cache — even a later key that happens to match. The prefix-hash chain already re-keys the suffix after a divergence, and `hadMiss` is the belt-and-suspenders guard matching upstream's `f` flag. The journal cache check runs BEFORE the budget gate and agent-count cap, so a cached result is free: no token spend, no agent-cap slot, no live dispatch. It still fires `agentDispatched` + `agentCompleted` so the registry/UI counters advance. Result-append is fire-and-forget (a journal write failure never fails the dispatch); the per-dispatch entry id is captured in a closure so concurrent dispatches can't clobber it. - NEW `workflow-journal.ts`: `WorkflowJournal` (jsonl-utils `read`/ `writeLine`), `deriveAgentKey`, `canonicalizeAgentOpts`, `buildReplay`. - `WorkflowRunRequest.journal` + `.resumeReplay`; cache logic in `countedDispatch`. - `Storage.getWorkflowRunsDir` / `getWorkflowRunJournalPath` / `getWorkflowRunSnapshotPath` (added in the P-nested commit). - `WorkflowParams.resumeFromRunId` + schema; `WorkflowTool` reuses the prior runId, loads the journal as replay, appends to the same file. Tests: workflow-journal.test.ts (12: canonicalize projection/sort/ function-strip, key determinism/prompt/opt/cosmetic/chain sensitivity, buildReplay last-write + accumulate, journal round-trip + missing-file). orchestrator P6 block (5: normal run journals started+result, resume serves cached prefix with 0 dispatches, first-miss-invalidates-suffix with a mid-script prompt edit, cache hit advances registry counters, cached dispatches bypass the agent-count cap). 351 workflow tests pass, lint clean. Refs QwenLM#4721. * fix(core): drop unused @ts-expect-error in workflow-journal test (QwenLM#4721) A function value is structurally assignable to `schema?: object`, so the `@ts-expect-error` on the function-strip test asserted a type error that never occurred — `tsc --noEmit` (which includes test files) flagged it as TS2578. Replace the directive with a comment explaining the test exercises the runtime strip of callable opt values. * feat(core): persist workflow run snapshots for cross-session /workflows history (QwenLM#4721 P7b-A2) The `WorkflowRunRegistry` is in-memory and dies with the CLI process, so `/workflows` could only ever show runs from the current session. Persist a JSON snapshot of each terminal run to `<projectDir>/workflows/<runId>.json` so the listing (and per-run detail view) survives a restart. - workflow-snapshot.ts: `toSnapshot`/`writeWorkflowSnapshot`/ `listWorkflowSnapshots` + retention prune (cap 30, oldest by mtime). perPhaseTokens is flattened to `[phaseOrNull, tokens]` pairs for JSON; a non-serializable script result degrades to a placeholder string. - WorkflowTask gains `script`/`scriptPath` (the snapshot carries the script source; also feeds the upcoming save-to-disk dialog). `script` defaults to '' in register() for legacy callers. - WorkflowTool registers the script source and, in its terminal `finally`, writes the snapshot once the registry entry has transitioned (best-effort, awaited so headless runs flush before the process exits). - /workflows merges disk snapshots into the listing (live registry entries win on a runId collision) and falls back to a snapshot in the detail view when the runId predates this process. Snapshot files (`<runId>.json`) never collide with resume journals (`<runId>/journal.jsonl`, in a subdir) — the `*.json` glob skips the dirs. * feat(cli): saved workflows as /<name> slash commands (QwenLM#4721 P7b-A1) A workflow script saved at `.qwen/workflows/<name>.js` (project) or `~/.qwen/workflows/<name>.js` (user) is now discoverable as a `/<name>` slash command that runs it — the user-facing complement to the in-script `workflow('<name>')` global (both resolve via core's `listSavedWorkflows`, project scope shadowing user). - SavedWorkflowLoader (CLI): a new ICommandLoader, wired into both loader arrays (interactive + non-interactive) just before FileCommandLoader. Each discovered workflow becomes a `{ type:'tool', toolName:'workflow', toolArgs:{ scriptPath } }` dispatch. Gated on isWorkflowsEnabled (the tool isn't registered otherwise), bare mode, and folder trust — mirroring FileCommandLoader. Trailing text is forwarded to the script's `args` global (parsed as JSON when valid, else the raw string). - WorkflowTool: `scriptPath` param (XOR with `script`, enforced in validateToolParamValues). When set, the tool reads the file at execution time (hot reload) and records the resolved absolute path on the registry entry as run provenance (feeds the P7b-A2 snapshot + the save dialog's "already saved" branch). - Activate the reserved `'workflow-command'` CommandSource; commands render under a "Workflow" source label. Re-exports listSavedWorkflows / resolveSavedWorkflowScript / validateWorkflowName / getSavedWorkflowDirs / WORKFLOW_NAME_PATTERN from core for CLI consumers. * feat(cli): save a completed run as a reusable workflow from /workflows (QwenLM#4721 P7b-A3) The `/workflows` detail view now offers `s` to save a finished run's script to `.qwen/workflows/<name>.js` (project) or `~/.qwen/workflows/<name>.js` (user) — the run becomes a `/<name>` slash command (P7b-A1) and a `workflow('<name>')` target (P-nested). - core `saveWorkflowScript(config, { name, scope, script, overwrite })`: validates the name, refuses to clobber unless `overwrite`, returns a discriminated result (saved / exists / invalid-name / empty-script) so the UI can prompt rather than throw. Reuses the saved-workflow dir + name rules. - WorkflowSaveOverlay: a self-contained overlay (single keypress handler with a minimal inline name editor — names are short kebab strings) for name entry, Tab scope toggle, overwrite confirmation, and saved/error states. The parent dialog yields all keys to it while open. - BackgroundTasksDialog: `s` opens the overlay for a terminal workflow entry that still carries its script; the hint row advertises it. The new `/<name>` command surfaces on the next session (the in-memory command list is not hot-reloaded mid-session); the file is usable immediately via `workflow('<name>')`. * feat(cli): workflow keyword trigger + footer indicator (QwenLM#4721 P7-trigger) Mentioning the word `workflow` in a prompt now softly steers that turn toward the Workflow tool, and the Footer shows a `⚙ workflow active` indicator for the steered turn. - workflow-keyword.ts: `detectWorkflowKeyword` (whitespace-tokenized, edge-punctuation-stripped — `workflow`/`workflow.`/`(workflow)` match; `workflows`, `dataflow`, `my-workflow-runner` do not) + `buildWorkflowSteeringNotice` (a soft nudge, not a forced tool call). - AppContainer.handleFinalSubmit: on a keyword hit (feature enabled, not a slash command, opt-out unset) prepend a `<system-reminder>` — the same one-shot mechanism as the worktree-restore notice — and arm the indicator; it clears when the turn returns to idle. - UIState gains `workflowKeywordActive`; Footer renders the indicator next to the worktree line; `ui.disableWorkflowKeywordTrigger` setting opts out. No `ultracode` naming — the trigger keyword is the plain word `workflow`, per the original issue. * feat: terminal-bell notification on workflow completion (QwenLM#4721 P-notif) A workflow can run for minutes as a single tool call; the user shouldn't have to watch the /workflows dialog to learn it finished. Fire a terminal-bell notification when a run reaches `completed` / `failed`. - WorkflowRunRegistry gains a `notificationCallback` slot (separate from the dialog-owned `statusChangeCallback`), fired in `complete()` / `fail()` — NOT `cancel()`, since a user-initiated cancel needs no notification. - AppContainer wires the callback to the existing `sendNotification` service (gated on the `general.terminalBell` setting), mirroring the agent attention-notification path. The call is optional so partial registry mocks in CLI tests no-op instead of throwing. * feat: workflow telemetry events (QwenLM#4721 P-telemetry) Two lightweight OpenTelemetry events (no-op unless telemetry is enabled), following the SpeculationEvent template: - `qwen-code.workflow_keyword` — the `workflow` keyword steered a turn (fired from AppContainer alongside the trigger). - `qwen-code.workflow_run` — a run reached a terminal state, with status + agents dispatched/completed + phase count + tokens + duration (fired from the WorkflowTool's terminal finally; wrapped so a logging failure can never mask the tool result). * test(core): direct unit coverage for the workflow snapshot module (QwenLM#4721 P7b-A2) Covers `toSnapshot` (perPhaseTokens Map flattening, non-JSON result placeholder, defensive array copy), the disk round-trip + newest-first sort, unparseable-file skipping, the missing-dir tolerance, and the MAX_RETAINED_SNAPSHOTS mtime prune — none of which were exercised directly before (only indirectly via the /workflows merge tests). * fix(core): use Array<T> for JournalEntry annotations in orchestrator test (QwenLM#4721) Five `import('./workflow-journal.js').JournalEntry[]` annotations tripped `@typescript-eslint/array-type` (T[] forbidden for non-simple types). Pure type-annotation change; behavior unchanged. * fix(core): correct stale WorkflowTool description + cover workflow-command label (QwenLM#4721) Self-review (doc-vs-code drift) caught the model-facing tool description still claiming "No resume and no background execution yet (scheduled for later phases)" — both now exist on this branch (P6 resume via resumeFromRunId; runs tracked in /workflows + the background-tasks view). Replace the stale clause with the real capabilities so the model isn't told a present feature is absent. Also: point the schema XOR meta-comment at the `scriptPath` description (which states it) rather than `script`'s (which doesn't), and add the missing `formatCommandSourceLabel('workflow-command') === 'Workflow'` test guarding the exhaustive CommandSource Record. * chore(cli): regenerate vscode settings schema for disableWorkflowKeywordTrigger (QwenLM#4721) Generated counterpart of the new `ui.disableWorkflowKeywordTrigger` setting added in the P7-trigger commit; the schema mirror is committed in this repo. * fix(cli): let a workflow-only session open the background-tasks dialog (QwenLM#4721) A real-scenario tmux run surfaced this: `descendFromComposer` only focuses the live-agent panel (agent-kind entries) or the Arena tab bar, and a workflow is never a live-agent-panel entry (`isLiveAgentPanelVisibleEntry` requires `kind === 'agent'`). So in a session whose only background task is a workflow, pressing ↓ from the composer focused nothing — the BackgroundTasksDialog could not be opened by keyboard at all, which made the P7b-A3 save action (and the per-run detail view) unreachable for exactly the workflow-only case they target. Add a final `descendFromComposer` branch: when there's no live-agent panel and no Arena tab bar but the background-tasks pill IS shown (`bgEntries.length > 0`), focus the pill, completing the composer → pill → dialog chain. * fix(workflows): address review round 1 — path-traversal, symlink, headless, resume, stall (QwenLM#4721, PR QwenLM#5600) Security: - Validate `resumeFromRunId` as `wf_<hex>` before it flows into the journal/ snapshot paths (path.join), closing a path-traversal write/read outside `<projectDir>/workflows`. - `resolveSavedWorkflowScript`: validate the string name (no `../` escape) and route both the name and `scriptPath` forms through a realpath boundary check that refuses anything resolving outside the saved-workflow dirs — this also defeats symlink escape. `listJsFiles` skips symlinked entries during discovery. Correctness: - Saved-workflow slash commands are `interactive` only — their `{type:'tool'}` action becomes `unsupported` in headless/ACP, so advertising those modes surfaced a command that then failed. - Skip workflow keyword steering for `?btw`/`/btw` so the system-reminder prefix no longer breaks BTW routing. - Seed the resume prefix-hash chain with `sha256(args)` so a resume with different args misses the journal and re-runs live instead of silently replaying the prior run's results. - The stall watchdog arms on the FIRST response event, not at attach time, so a reasoning model's slow time-to-first-token is not a false stall (that window is bounded by the subagent's max_time); it now detects post-first-response streaming stalls. Resource / docs: - `pruneSnapshots` also removes each pruned run's `<runId>/journal.jsonl` directory (previously leaked unboundedly). - Drop the inaccurate "save dialog offers 'already saved'" comment (no such affordance exists) from the three sites. Adds security + behavioural tests for each; declined as out of scope: nested- workflow log accumulation, a __proto__-guard test, hung-tool watchdog suspend (max_time backstops it), and the save TOCTOU. * fix(cli): let ↓ descend from the Arena tab bar into the bg-tasks pill (QwenLM#4721) Round-1's descendFromComposer pill branch (5a1fb6a) only reached the pill when no Arena tab bar was present. When both an Arena roster and a background-tasks pill (e.g. a workflow run) are shown, ↓ from the composer stops at the tab bar, whose Down handler was a no-op — stranding the pill and the run's detail/save dialog behind it. AgentTabBar's Down now descends into the pill when one is shown (bgEntries > 0), completing the chain BackgroundTasksPill already documents (Composer ↓ → AgentTabBar ↓ → Pill ↓ → Dialog); the dialog is now reachable in all four roster/pill coexistence cases. Also refresh the stale descendFromComposer comment to name the pill destination and add InputPrompt coverage for the round-1 pill branch. * fix(workflows): refuse a symlinked saved-workflow root dir (QwenLM#4721) A checked-in `.qwen/workflows -> /outside` symlink turned its external target into the trusted boundary: readWorkflowFileSecurely realpaths the root, so discovery listed, workflow('<name>') read, and the save dialog wrote external files. Round 1's per-entry symlink guard missed this because the link is the dir, not the files it exposes (which are regular). Refuse a symlinked root for all three operations via isSymlinkedRoot (lstat). Symlinked *ancestors* are still tolerated (realpath keeps a project under macOS `/tmp -> /private/tmp` working) — only the workflows dir itself must not be a link. Each vector carries a RED-before-fix security test (`.qwen/workflows -> outside`: discovery excludes, read refuses, save throws).
wenshao
added a commit
that referenced
this pull request
Jul 10, 2026
…ering (QwenLM#5666) * feat(tui): remove tool group borders and collapse completed tool results Remove round borders from ToolGroupMessage, CompactToolGroupDisplay, and InlineParallelAgentsDisplay. Completed tools now default to a single collapsed header line with dimColor styling. Executing/error/confirming tools continue to show their full result block. Part of QwenLM#4588 (Track 3: Simplify tool-call rendering). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): gate collapse on compact mode and fix innerWidth calculation - Only collapse completed tool results in compact mode, preserving full visibility in non-compact mode - Subtract 2 from innerWidth to account for ToolMessage paddingX={1} - Update snapshots to reflect removed borders Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): address review feedback on collapse and visual alignment - Gate isDim on compact mode so non-compact tools stay fully styled - Add paddingX={1} to CompactToolGroupDisplay for left-edge alignment - Delete Border Color Logic test block (borders removed) - Add compact-mode test coverage for Error/Executing/Pending/forceShowResult - Clean up stale border references in comments Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(tui): unify tool output with semantic summaries Replace the dual compact/normal mode tool output with a single unified mode. Completed tools always show a semantic overview line ("Read 3 files, edited 2 files") instead of dumping full results. - Add buildToolSummary() for category-based semantic summaries - Remove compactMode gate from shouldCollapse and isDim in ToolMessage - Make all-completed tool groups use CompactToolGroupDisplay - Remove unused useCompactMode hook calls from ToolMessage Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(tui): add buildToolSummary unit tests and fix stale comment - Add 10 dedicated unit tests for buildToolSummary covering edge cases - Fix stale comment referencing old compactMode gate logic Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): address audit findings for unified tool output - Add Canceled status to allComplete check in ToolGroupMessage - Move memory-only group rendering before showCompact to prevent them being swallowed by CompactToolGroupDisplay - Fix LLM summary duplication: absorbedCallIds now tracks completed groups in non-compact mode; HistoryItemDisplay no longer bypasses summaryAbsorbed when !compactMode - Update StandaloneSessionPicker test for new compact rendering - Fix design doc category order example and add missing rendering rules Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): address inline review findings - Add SHELL_COMMAND_NAME and @ file-reference pseudo-tools to TOOL_NAME_TO_CATEGORY mapping for correct category classification - Fix height calculation test to use Executing status so expanded path is actually exercised - Update stale comment about empty toolCalls behavior Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): remove unused compactMode import in HistoryItemDisplay Fixes CI build failure caused by TS6133 (noUnusedLocals) — the compactMode destructure became dead code after the summary gating was moved to summaryAbsorbed. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * ci: trigger re-run with updated merge ref Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(tui): design — remove global compact mode, add Ctrl+O transcript + mouse click-to-expand Design-only. Stacks on QwenLM#5661 (type-based tool partition baseline) and QwenLM#5751 (VP mouse foundation). Scope: remove residual global compactMode, add Ctrl+O transcript (alt-screen frozen snapshot) and mouse click to expand a tool's title/output in place. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(tui): remove global compact mode toggle (on top of QwenLM#5661 partition baseline) Builds on QwenLM#5661's type-based tool partition. Removes only the residual global compactMode switch, keeping the partition baseline intact: - ToolGroupMessage: showCompact = (compactMode || allComplete) → allComplete - delete CompactModeContext, mergeCompactToolGroups (isForceExpandGroup / compactToggleHasVisualEffect no longer used once the cross-group merge and the Ctrl+O toggle are gone) - MainContent: drop the compactMode-gated merge path; mergedHistory = visibleHistory - remove TOGGLE_COMPACT_MODE binding/matcher, ui.compactMode/compactInline settings, the compact-mode tip and shortcut entry, AppContainer state + provider + toggle keypress branch - KEEP CompactToolGroupDisplay + partition, ToolMessage forceShowResult / shouldCollapse, ToolConfirmationMessage's local compactMode prop, and ui.compactMode in WEB_SHELL_SETTINGS (web shell is a separate surface) typecheck + affected suites green (224 tests). Ctrl+O is a temporary no-op until the TranscriptView lands. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(tui): Ctrl+O opens a frozen alt-screen transcript full-detail view Adds the keyboard half of the Ctrl+O redesign on top of the QwenLM#5661 partition baseline: - fullDetail render path (HistoryItemDisplay → ToolGroupMessage): fullDetail composes into thinking `expanded`, and on tool groups forces showCompact=false + forceShowResult=true + uncapped height — so every block renders in full. - new TranscriptView: an AlternateScreen overlay (disabled in VP mode where Ink already owns the alt screen) rendering a frozen snapshot (history length + a pending copy) through ScrollableList with fullDetail, reusing QwenLM#5751's keyboard/wheel/scrollbar scrolling. Adaptive estimatedItemHeight for the taller full-detail rows. - AppContainer wiring mirrors ThinkingViewer: transcript guard is the FIRST handleGlobalKeypress branch (Esc/q/Ctrl+C/Ctrl+O close, everything else swallowed) so close keys beat QUIT and the vim INSERT guard; Ctrl+O opens when closed; auto-close on any blocking dialog / WaitingForConfirmation; message-queue drain and refreshStatic are suppressed while open. - Command.TOGGLE_TRANSCRIPT bound to Ctrl+O. typecheck + 8 suites (268 tests) green. Mouse click-to-expand (per-tool) follows in a later commit. Alt-screen enter/exit behavior still needs real-terminal verification across tmux/iTerm/VSCode. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): repaint normal buffer when transcript closes (no duplicate scrollback) E2E (VHS) caught the design's flagged highest-risk issue: in the legacy <Static> path, closing the alt-screen transcript leaked its full-detail rows into the main scrollback (a duplicate "完整记录 / Transcript" block appeared below the live history). Fix: when isTranscriptOpen goes true→false in non-VP mode, force one clearTerminal + Static remount, deferred a tick so the AlternateScreen's exit escape (\x1b[?1049l) flushes first and the during-transcript refreshStatic guard has already cleared. VP mode keeps its own scrollback via the React tree and is unaffected. Verified via VHS: open shows the transcript overlay; Esc restores the main view cleanly with no duplicated content. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(tui): rebase ctrl-o design doc to QwenLM#5661's type-based partition The design doc was written against an early state-based snapshot of QwenLM#5661 (showCompact = (compactMode || allComplete), whole-group collapse) and even asserted that forceExpandAll / isCollapsibleTool "don't exist". The merged QwenLM#5661 is type-based partition and those symbols are its core. Rewrite the affected sections to match the shipped baseline: - §1/§2: baseline described as type-based partition (collapse read/search/list via isCollapsibleTool, render mutation tools individually); compactMode no longer affects tool rendering. Added a revision note. - §3.1: table + bullets rewritten to forceExpandAll + collapsible/ non-collapsible split; shouldCollapseResult's isCollapsibleTool guard (Shell/Edit results always visible); mixed groups = summary line + per-tool. - §4.1: smaller delete scope (no showCompact / compactMode|| term to remove); delete mergeCompactToolGroups.ts; keep web-shell ui.compactMode passthrough. - §4.5: fullDetail = forceExpandAll=true (not showCompact=false) + per-tool forceShowResult=true + availableTerminalHeight=undefined. - §4.8/§5/§7/§8/§9/appendix: symbols/forensics corrected to the real merged implementation; tool_use_summary renders as a standalone line (no absorption). Matches the resolution already applied to the code in the preceding merge. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(tui): fix factual nits from cross-audit of the ctrl-o design doc Three independent audits confirmed the doc is now faithful to the merged QwenLM#5661 type-based partition; they surfaced three concrete fixes: - CATEGORY_ORDER: corrected to the real array order search/read/list/command/edit/write/agent/other (was listed as command/read/edit/write/search/list/agent/other). - CompactToolGroupDisplay exports: only getOverallStatus / isCollapsibleTool / buildToolSummary / CompactToolGroupDisplay are exported; ToolCategory / TOOL_NAME_TO_CATEGORY / CATEGORY_ORDER / getToolCategory are internal — relabeled accordingly. - §5.B file table: fixed a broken 4-column separator and escaped the literal `||` pipes in the AppContainer row so it renders as a clean 2-column table. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): don't let fullDetail be bypassed by compact early returns Audit (PR QwenLM#5666) point 2: ToolGroupMessage computed `forceExpandAll = fullDetail || ...` only AFTER two early returns — the pure-parallel-agent group (→ InlineParallelAgentsDisplay dense panel) and the completed memory-only group (→ "Recalled/Wrote N memories" badge). In transcript full-detail mode those groups were therefore NOT fully expanded. Guard both early returns with `!fullDetail` so transcript falls through to the per-tool ToolMessage path (forceExpandAll + per-tool forceShowResult + uncapped height). Add a regression test asserting a completed memory-only group renders each op individually (not the badge) under fullDetail. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(tui): resolve open design decisions from source evidence Settle the two outstanding decision points from the PR audit using the codebase + reference implementations (not preference): - Non-TTY (audit point 3): AlternateScreen has NO isTTY guard today (doc claimed it did — corrected). The TUI is already gated by stdin.isTTY (config.ts:1532), so non-TTY rarely mounts; the only edge is `-i`. Decision: add a process.stdout.isTTY guard to AlternateScreen, matching the repo convention (startInteractiveUI/notificationService guard isTTY before terminal escapes). Doc now marks it "to implement" + test. - Transcript / per-tool expansion state location: per claude-code (REPL-local transcript state), gemini-cli (dedicated ToolActionsContext), and this repo's own ThinkingViewer (AppContainer-local useState + minimal action via a dedicated context) — transcript open/freeze stays AppContainer-local and is NOT surfaced via UIStateContext (the implemented code already does this; only the doc was wrong). Per-tool expansion uses a dedicated ToolExpandedContext (real cross-layer producer/consumer), not the broad UIStateContext. Also document the fullDetail early-return guard (the just-landed fix): the pure-parallel-agent and memory-only early returns are skipped under fullDetail so transcript shows every tool in full. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(tui): align design doc status/scope with current PR (audit follow-up) Latest audit confirms the technical design is implementable and side-effect coverage is sufficient; it flagged status/scope inconsistencies for the doc to serve as an acceptance baseline. Fixes: 1. Status: "design review (docs-only)" → "implementation in progress; this doc is the acceptance baseline for the current PR". Added an implemented-vs-pending status table. 2. Mouse click-to-expand: added a banner marking it NOT yet implemented and stating the open scope decision (merge blocker vs VP-only follow-up). 3. QwenLM#5751 (and QwenLM#5661) dependency: corrected from "OPEN, must merge first" to "already merged into main; branch rebased on top". 4. alt-screen degradation: removed the undefined "overlay" fallback in the DefaultAppLayout row; non-TTY degrades via the AlternateScreen isTTY guard to in-buffer rendering (§4.2), no separate overlay path. 5. Fixed a broken bold marker (`\*\*`) in the AppContainer row. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(tui): scope mouse click-to-expand out as a follow-up Assessed the mouse click-to-expand effort against the real code: it's ~250–400 lines across 4–5 files (ToolExpandedContext + AppContainer wiring + a ClickableToolMessage component — can't call useMouseEvents inside the .map() — + ToolGroupMessage wiring + mouse hit-test tests). More importantly, under QwenLM#5661's type-based partition the collapsed read/search tools are aggregated into a single summary line, so there is no per-tool click target — the click granularity must be redesigned to "click the summary row → expand the whole group". Plus the known SGR-mouse vs native text-selection risk. Per the "small code → include, otherwise follow-up" rule: this is not small, so scope it OUT of the current PR. The current PR delivers Ctrl+O transcript only. Marked §1 goal #4, §4.8 (banner + draft), §9 commit 4, and the status table accordingly; the §4.8 design is kept as a draft for the follow-up PR. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(tui): isTTY guard for AlternateScreen + transcript shortcut/i18n cleanup Completes the remaining in-scope items for the Ctrl+O transcript PR: - AlternateScreen: guard the alt-screen escape writes on `process.stdout.isTTY` (skip when non-TTY: piped/redirected/CI), matching the repo convention (startInteractiveUI / notificationService). Non-TTY now degrades to in-buffer rendering. Adds AlternateScreen.test.tsx (enter/exit on TTY, skip when disabled, skip when non-TTY). - KeyboardShortcuts: add the `ctrl+o → view transcript` entry that was removed with the old compact-mode line but never replaced. - i18n (all 9 locales): drop the dead `to toggle compact mode` and the `Press Ctrl+O to toggle compact mode — …` tip strings (no longer referenced after compact-mode removal); add `to view transcript`. Touched suites green (AlternateScreen, i18n index/mustTranslateKeys, TranscriptView, Help). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(tui): mark isTTY guard + i18n cleanup as implemented in status table Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(i18n): add TranscriptView strings to all locales TranscriptView.tsx renders t('Transcript'), t('to close') and t('to scroll'), but these keys existed only in en/zh. The strict key-parity check (zh, zh-TW) failed CI on the missing zh-TW entries. Add all three keys to zh-TW (the failing strict-parity locale) and to ca/de/fr/ja/pt/ru for completeness so check-i18n is fully clean. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(ctrl-o): add before/after transcript capture evidence Add VHS-captured screenshots (main-view collapsed vs Ctrl+O transcript expanded) under docs/design/ctrl-o-detail-expand/assets/ and reference them from §3.4 of the design doc. Captured on the local branch build via the mac-autotest skill; shows read/search/list tools folding to a single summary row in the main view and each expanding in the transcript, with zh i18n strings rendering correctly. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(ctrl-o): design §4.9 — full tool detail passthrough in transcript Document the data-layer gap behind the "second-level fold" seen in the Ctrl+O transcript: read/ls/grep returnDisplay only stores a summary, and IndividualToolCallDisplay carries no full-content field, so fullDetail (which correctly clears partition/result folding and height limits) has no detail to render. Spec the chosen fix (path C): derive a contentForDisplay string from the raw llmContent at the single core success-assembly point (partToString + existing 32k retention cap), thread it through to a new IndividualToolCallDisplay.detailedDisplay, and render it in ToolMessage when fullDetail + isCollapsibleTool. Scope limited to read/search/list in the transcript; main-view summaries and shell/edit/write are unchanged. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(ctrl-o): adopt plan Y for §4.9 and address transcript-detail audit Address the audit on §4.9 (full tool detail in the Ctrl+O transcript): - Rewrite §4.9 to plan Y — reuse the complete content already persisted in functionResponse.response.output (responseParts) via a single core helper, instead of adding a contentForDisplay field threaded through serialize/ replay. Saved/replayed transcripts get full detail for free (audit #6). - Split fullDetail (data-source switch) from forceShowResult (un-fold) so main-view force cases (user-initiated/error) don't leak full detail into the main view (audit #2). - Use the exported compactStringForHistory, not the internal compactString (audit #4). - Scope by isCollapsibleTool incl. glob, not a hardcoded read/ls/grep list (audit #5). - §3.4: stop claiming the screenshot already shows full output; add a pre-§4.9 caveat and a merge-blocker row in the status table (audit #1). - Sync §5 file list, §8 tests, §9 commit 4 (merge blocker); move mouse click-expand out of the commit sequence to follow-up (audit #3). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(ctrl-o): tighten §4.9 per second audit (no 2nd truncation, nested media, plan-Y guard) - P1: detailedDisplay no longer runs compactStringForHistory — the 32k cap would make Ctrl+O a "32k bounded preview", contradicting the "full detail" promise (read_file has maxOutputChars=Infinity and can legitimately exceed 32k). Detail is now the full getToolResponseDisplayText output, bounded only by core's existing truncateToolOutput/pagination. - P2: spell out getToolResponseDisplayText's priority rule — media lives in nested functionResponse.parts (not top-level); read response.output, then walk nested parts for inlineData/fileData/text placeholders; undefined when neither output nor media so the UI falls back to the summary. - P3: add an explicit §8 plan-Y protection test (output >32k survives recording/loadSession/resume/replay; detailedDisplay derives from message.parts, not resultDisplay or API compressedHistory) and document the fall-back-to-X trigger. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ctrl-o): address PR review findings on transcript view - AppContainer: freeze a committed-history copy (not just a length) so in-place compaction can't corrupt the open transcript; memoize the stitched items list so streaming re-renders don't rebuild it - AppContainer: clear thinkingViewerData on openTranscript and guard openThinkingViewer so no stale "ghost" thinking popup resurfaces - AppContainer: read prevTranscriptOpen during render (StrictMode-safe) - AppContainer: close the transcript on Ctrl+D instead of swallowing it - TranscriptView: wrap content in a new ErrorBoundary and React.memo the component (stable items + onClose make the shallow compare effective) - CompactToolGroupDisplay: localize buildToolSummary via t() and add the per-category count phrases to all 9 locales - workspace-settings: drop the stale ui.compactMode web-shell allowlist entry - tests: TranscriptView default alt-screen + negative-id keyExtractor; HistoryItemDisplay fullDetail expansion + forwarding; ToolGroupMessage fullDetail parallel-agent bypass; MainContent.test import-first order Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ctrl-o): second review round — web-shell compactMode + anti-deadlock deps - settingsSchema: re-add ui.compactMode as a hidden (showInDialog:false) schema entry so the web shell's independent compact toggle keeps persisting via the daemon settings routes (mirrors voiceModel). The TUI compact mode stays retired — it just isn't shown in the TUI dialog. - workspace-settings: restore ui.compactMode in WEB_SHELL_SETTINGS now that the schema definition resolves again (fixes the web shell 400 / revert). - AppContainer: add isTranscriptOpen to the anti-deadlock auto-close effect deps so opening the transcript while a blocking prompt is already visible re-fires the effect and closes it (previously it could open over an invisible prompt and deadlock). - ToolGroupMessage.test: cover the fullDetail height-truncation lift (availableTerminalHeight undefined under fullDetail, numeric otherwise). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ctrl-o): regenerate vscode settings schema for re-added ui.compactMode The previous commit re-added ui.compactMode (showInDialog:false) to settingsSchema.ts but did not regenerate the generated vscode schema, which the CI "settings schema is up-to-date" gate checks. Regenerated. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore(ctrl-o): reset MCP/acp-bridge files to main (drop stale merge diff) These 6 files are unrelated to the Ctrl+O work. Reset to origin/main so the PR diff carries only transcript changes. Committed with --no-verify because the classic-CLI pre-commit prettier reflows union types differently than the repo's experimental-CLI formatter (CI's prettier step does not gate on this). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(ctrl-o): update compact-mode docs for transcript model; drop orphaned i18n key - settings.md: ui.compactMode is retired in the TUI (web-shell only); Ctrl+O now opens the full-detail transcript - tool-use-summaries.md: reframe "compact vs full mode" toggle as "main view (completed group) vs Ctrl+O full-detail transcript / force-expanded" - remove the now-orphaned 'Hide tool output and thinking…' locale key (was the old compactMode description) from all 9 locales Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(ctrl-o)!: §4.9 full tool-detail passthrough in transcript Implement plan Y: read/search/list tools now show their COMPLETE output in the Ctrl+O transcript instead of the summary count line, while the main view is unchanged. - core: add `getToolResponseDisplayText(parts)` — extracts the full `functionResponse.response.output` (skipping the non-informative "Tool execution succeeded." placeholder), emits `<media: mime>` placeholders for nested media parts, keeps nested text, returns undefined when nothing is extractable. No second truncation: the only bound is whatever core already applied (truncateToolOutput / paging). - cli: add derived (non-persisted) `IndividualToolCallDisplay.detailedDisplay`. Populated from the already-persisted response parts on both the live path (useReactToolScheduler success branch) and the resume path (resumeHistoryUtils tool_result, falling back to message.parts for older records). - cli: rendering split — ToolGroupMessage forwards `fullDetail` to ToolMessage; ToolMessage swaps the summary `resultDisplay` for `detailedDisplay` ONLY when `fullDetail && isCollapsibleTool(name) && detailedDisplay`. Kept separate from `forceShowResult` so main-view force scenarios (user-initiated / error / confirming) still render the summary, never the full output. - ACP path needs no change: ToolCallEmitter.transformPartsToToolCallContent already writes the same full output into the ACP `content[]` for its SSE clients; the TUI transcript does not flow through it, so no new protocol field is added. Tests: core helper unit tests (placeholder skip, nested media, plain-text part, empty fallback); ToolMessage data-source switch (collapsible+fullDetail uses detail, force-but-not-fullDetail keeps summary, non-collapsible keeps summary, missing-detail falls back); ToolGroupMessage prop-forwarding. BREAKING CHANGE: Ctrl+O is now a frozen full-detail transcript view, not a global compact-mode toggle. The `TOGGLE_COMPACT_MODE` command and the TUI effect of `ui.compactMode` / `ui.compactInline` are removed; the keys remain read-tolerant (ignored by the CLI) and `ui.compactMode` is still forwarded to the web shell. See docs/design/ctrl-o-detail-expand/design.md §6 for migration. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ctrl-o): address review — repaint race, suppressOnRestore parity, transcript error logging - AppContainer: fix close-repaint setTimeout being cancelled by streaming re-renders. `wasOpenPrevRender`/`isTranscriptOpen` were in the effect deps, so the next streaming render flipped them, ran cleanup, and clearTimeout'd the pending repaint — leaving stale pre-transcript content in the legacy <Static> normal buffer. Drive the effect off a close-transition counter instead, so post-close re-renders don't change deps and the scheduled repaint fires exactly once per close. - AppContainer: transcript snapshot now mirrors MainContent's `!display.suppressOnRestore` filter, so items collapsed on session resume (ui.history.collapseOnResume) are not re-exposed in the Ctrl+O view. - TranscriptView: pass `onError` to the ErrorBoundary so caught render errors in the fullDetail paths are logged to the debug channel, not just shown. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(ctrl-o): cover detailedDisplay resume derivation + message.parts fallback Add dedicated resumeHistoryUtils tests for §4.9: detailedDisplay derived from toolCallResult.responseParts, the `responseParts ?? message.parts` fallback for older records lacking responseParts, and the undefined fallback when neither source carries output. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ctrl-o): address review — plain-text detail, shared placeholder const, resume status guard, scroll hint Four review fixes on the §4.9 transcript work: - ToolMessage: when fullDetail swaps the data source to detailedDisplay (raw file content / grep hits / dir listings), force renderOutputAsMarkdown to false. The existing `if (availableHeight)` guard never fires in the transcript (height cap is lifted, availableTerminalHeight is undefined), so raw `#`/`*`/`-`/`>` characters were being Markdown-formatted. - core: export TOOL_SUCCEEDED_OUTPUT as the single source of truth for the "Tool execution succeeded." placeholder. coreToolScheduler (the producer, two sites) and getToolResponseDisplayText (the consumer) now share one constant so the filter can't silently drift if the wording changes. - resumeHistoryUtils: only derive detailedDisplay for SUCCESS tools, matching the live path (useReactToolScheduler sets it only in its 'success' branch). Previously it was populated unconditionally, so a resumed errored/cancelled collapsible tool would surface raw output in the transcript while the same tool live would not. - TranscriptView: footer hint now reads "Shift+↑↓ to scroll" — plain Up/Down do not scroll (ScrollableList listens for SCROLL_UP/DOWN bound to Shift+↑↓); the old "↑↓" hint was misleading. Tests: ToolMessage plain-text-detail assertion + new raw-markdown case; resume errored-tool no-detailedDisplay case. typecheck/lint/tests green (core scheduler 222, cli suites pass). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): guard transcript non-TTY output + clear detailedDisplay on compaction Addresses three review findings on the Ctrl+O transcript work: - Non-TTY byte leak: `useMouseEvents` enabled SGR mouse mode (?1002h ?1006h) whenever stdin supported raw mode, ignoring stdout. With stdout piped (`qwen | tee log`) the transcript's focused ScrollableList (bypassVpGate) leaked raw control bytes into the captured output. Gate the enable on `stdout.isTTY`, and likewise guard the transcript close-repaint `clearTerminal` write in AppContainer — both now mirror AlternateScreen's existing isTTY guard, so the non-TTY fallback stays byte-clean. - Compaction privacy regression: `compactOldItems` replaced old tool `resultDisplay` with the cleared placeholder but left `detailedDisplay` (the raw functionResponse text added for the full-detail transcript) intact, so reopening Ctrl+O after compaction re-surfaced the supposedly cleared read/search/list output. Clear `detailedDisplay` wherever `resultDisplay` is cleared, with a regression test. - Docs: keyboard-shortcuts.md still described Ctrl+O as "toggle compact mode"; updated to the open/close full-detail transcript behavior. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(tui): report a TTY stdout in ScrollableList mouse-scroll tests The new `stdout.isTTY` gate in `useMouseEvents` (which stops SGR mouse escapes leaking into piped output) left ink-testing-library's fake stdout — which has no `isTTY` — with the mouse pipeline disabled, so the scrollbar-drag and wheel-scroll assertions never received events. Mock ink's `useStdout` to report `isTTY: true` so the pipeline arms exactly as it does in a real terminal; all other ink exports are preserved. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): address Ctrl+O transcript review — q-guard, callback churn, tests, cleanup Resolves the qwen3.7-max /review findings: - Modifier guard on the transcript close key: bare `q` closed the transcript, but Ink reports Ctrl/Alt/Shift+Q as `{ name: 'q', … }` too (Alt arrives as `meta`), so those silently closed it. Guard `!key.ctrl && !key.meta && !key.shift` (Shift+Q is a literal `Q`). - Stable `openTranscript`: it captured `historyManager.history` and `pendingHistoryItems` as deps, both of which change identity every streaming tick, rebuilding the callback — and the whole `handleGlobalKeypress` closure that lists it — on every render during streaming. Read both via refs so the callback is referentially stable. - AppContainer transcript integration tests (the removed TOGGLE_COMPACT tests had no replacement): Ctrl+O installs TranscriptView; Esc / q / Ctrl+C / Ctrl+D close it; Ctrl+Q / Alt+Q / Shift+Q do NOT (modifier guard); arbitrary keys are swallowed and keep it open; a blocking confirmation (WaitingForConfirmation) auto-closes it (anti-deadlock). - Dead i18n string: removed the orphaned 'Press Ctrl+O to show full tool output' key from all 9 locale files (no `t()` reference remained after the compact-mode sweep). - Design doc: replaced the leaked absolute worktree path with a placeholder, and corrected the §6 keybinding-migration note — the codebase has no user-configurable keybinding override surface (`keyMatchers` always uses hardcoded defaults), so there is no persisted `toggleCompactMode` binding to migrate; the startup-detection step is not applicable until such a feature exists. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): escape ANSI in transcript detailedDisplay + gate its extraction Two findings from the qwen3.7-max /review on §4.9: - [Critical] ANSI escape injection: `detailedDisplay` carries raw, un-sanitized tool output (file contents, grep hits, directory listings). The Ctrl+O transcript rendered it straight to <Text> without escaping, so a malicious repo file with embedded terminal control sequences (e.g. `\x1b[?1049l` to drop the alt-screen, OSC 52 for clipboard poisoning) would execute when the transcript opened — and fullDetail lifts the height cap, exposing the whole file. Run it through `escapeAnsiCtrlCodes` (already used for agent names in this file) before rendering. Added a regression test asserting the raw ESC bytes don't survive. - [perf] `detailedDisplay` was extracted on every successful tool call (~25K chars from core's truncation) but is consumed only by the transcript's fullDetail render for collapsible (read/search/list) tools. Gate the extraction on `isCollapsibleTool(displayName)` so edit/write/command/agent calls no longer store a large string the renderer never reads — mirrors ToolMessage's `usingDetailedDisplay` gate (which also keys off the display name). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): gate resume-path detailedDisplay on isCollapsibleTool (match live path) The resume path (resumeHistoryUtils.ts) extracted `detailedDisplay` for every successful tool call, unlike the live path in useReactToolScheduler which gates on `isCollapsibleTool(displayName)`. Since the transcript's `usingDetailedDisplay` only consumes it for collapsible (read/search/list) tools, resuming a session with many edit/write/command/agent calls stored large (~25K char) strings the renderer never reads. Apply the same gate so live and resume stay consistent, using `toolCall.name` (the display name, set from `tool.displayName`) to match the renderer's key. Updated the existing derivation tests to use a collapsible read tool (an edit tool now correctly yields undefined) and added a regression asserting a non-collapsible tool leaves detailedDisplay undefined on resume. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): strip bare C0 control bytes from transcript detailedDisplay + memoize Follow-up to the ANSI-escape fix. `escapeAnsiCtrlCodes` delegates to ansi-regex, which only matches ESC-prefixed sequences, so bare C0 control bytes without an ESC prefix (BEL \x07, BS \x08, FF \x0c, SO \x0e, SI \x0f, CR, …) passed through to <Text> and could still corrupt the display or ring the bell from a malicious file's contents. Add a second pass that strips those bytes (keeping only TAB and LF, which structure multi-line output). Memoize the two-pass sanitization with useMemo keyed on detailedDisplay so the ~25K-char regex work doesn't re-run every render. Extended the ToolMessage regression test to assert bare C0 bytes are stripped alongside the ESC sequences. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(tui): memoize HistoryItemDisplay, add ErrorBoundary tests + TAB/LF invariant Addresses three review suggestions: - Wrap `HistoryItemDisplay` in `React.memo` so the Ctrl+O transcript (which re-renders on every scroll tick) skips re-rendering frozen-snapshot items whose props are shallowly unchanged. The transcript passes stable `item` references, so the default shallow compare is effective; harmless for the main view (items live in `<Static>` and render once). - Add ErrorBoundary.test.tsx covering the four behaviors: renders children when healthy, catches a render error into the default fallback with the message, renders a custom fallback, calls `onError` with the error + component stack, and `reset` clears the error state so the subtree recovers. - Lock the C0-strip invariant: assert TAB and LF survive in detailedDisplay (the regex intentionally skips \x09/\x0a) so a future regex change can't silently collapse multi-line/columnar output. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * refactor(tui): review cleanups — gate sanitize memo, drop dead code, add tests Addresses the latest /review suggestions: - ToolMessage: gate the `sanitizedDetailedDisplay` useMemo on `usingDetailedDisplay` so the ~25K-char escape+strip no longer runs for every collapsible tool in the main view (where the result is discarded). - TranscriptView: remove the dead `listRef` (created + passed as `ref` but never used imperatively) and the dead `onClose` prop (declared, then `void`-ed; close keys are owned entirely by AppContainer's global keypress guard). Dropped the now-unused `useRef` / `ScrollableListRef` imports and the `onClose` call-site + props. - Tests: add TranscriptView error-fallback coverage (a throwing item renders the recovery fallback, not a crash); add live-path `mapToDisplay` detailedDisplay extraction coverage (collapsible → extracted, non-collapsible → undefined); add Ctrl+O to the transcript close-keys it.each (the toggle key was the only close key untested). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(tui): remove orphaned no-op CompactModeProvider stubs This PR deleted the CompactModeContext, leaving identical no-op `CompactModeProvider` passthrough stubs (with an ignored `value` prop) in ToolGroupMessage.test.tsx, ToolMessage.test.tsx and MainContent.test.tsx, each still wrapping every render. Remove the stubs and unwrap the renders; drop the now-meaningless `compactMode` params/args from the local render helpers. Behavior-preserving (the stubs rendered children verbatim) — all three suites still pass. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): strip bidi overrides, sanitize error fallbacks, share filters Latest /review round: - [Critical] Strip Unicode bidirectional override / isolate chars (Trojan Source, CVE-2021-42572) from transcript `detailedDisplay` — a third sanitize pass after ANSI + C0 stripping, mirroring the repo's existing BIDI_CONTROL_RE. Regression test added. - Sanitize `error.message` with `escapeAnsiCtrlCodes` in both the ErrorBoundary default fallback and the TranscriptView custom fallback (defense-in-depth against control codes in a crafted error message). - Ctrl+O while the ThinkingViewer is open now swaps to the transcript (falls through to openTranscript, which clears the viewer) instead of being silently swallowed. - Extract the shared `isHistoryItemVisibleAfterRestore` predicate into types.ts and use it from both MainContent (main view) and AppContainer (transcript freeze), so the two surfaces can't diverge on which collapse-on-resume items are hidden. - Tests: use the exported `TOOL_SUCCEEDED_OUTPUT` constant instead of the hardcoded literal in generateContentResponseUtilities.test.ts. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): harden compaction guard to always clear detailedDisplay The compaction cleanup only cleared `detailedDisplay` inside the `resultDisplay != null` branch (both the group-level trigger, the group-count pass, and the per-tool clear). A tool carrying only `detailedDisplay` (no resultDisplay) would skip compaction and leave the raw transcript detail intact — a latent privacy leak if the two fields ever decouple. Widen all three checks to also match `detailedDisplay != null` so the memory/privacy safeguard is robust. Added a defensive regression test. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): sanitize mime/uri in getToolResponseDisplayText media placeholders The `<media: …>` placeholder interpolated `inlineData.mimeType` / `fileData.mimeType` / `fileData.fileUri` from tool responses verbatim. A crafted response could embed control characters or angle brackets to inject terminal codes or forge/mangle the placeholder markup. Add a `sanitizeMediaLabel` helper that strips C0/C1 control bytes and `<`/`>` before interpolation, falling back to the default label when emptied. Regression test added. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(tui): report a TTY stdout in BaseSelectionList mouse integration test The `stdout.isTTY` gate added to `useMouseEvents` (stops SGR mouse escapes leaking into piped output) left QwenLM#6011's BaseSelectionList mouse test — which renders via ink-testing-library where the hook-provided stdout reads as non-TTY — with the mouse layer disabled, so the any-event enable escape was never written. Mock ink's `useStdout` to report `isTTY: true` with a capturing write spy (matching useMouseEvents.test.tsx / ScrollableList.test .tsx), and assert the `?1003h` enable via that spy while items still render through ink's own stdout. Both cases pass. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(core): fix JSDoc placement + note ErrorBoundary fallback is un-translated Two small review nits: - getToolResponseDisplayText's JSDoc had ended up above sanitizeMediaLabel (added last commit), making it read as that helper's docs. Reorder so sanitizeMediaLabel + its own JSDoc come first and each doc sits directly above its function. - Document why the ErrorBoundary default fallback's title is intentionally a plain English string (last-resort message for callers with no `fallback`; renders mid-crash, so it avoids pulling in the i18n layer — the transcript passes its own localized fallback anyway). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): share terminal-sanitize pipeline; guard AlternateScreen writes - Extract the three-pass sanitizer (ANSI escape + bare-C0 strip + bidi strip) into `sanitizeTerminalText` in textUtils.ts as the single source of truth, and use it at all raw-text render sites: ToolMessage's `detailedDisplay`, and the TranscriptView + ErrorBoundary error-message fallbacks (previously those only escaped ANSI, missing C0/bidi — the boundary catches errors from the fullDetail path that processes raw tool output, so a crafted item shape could carry unsanitized bytes into error.message). Removes the duplicated regex consts from ToolMessage. - AlternateScreen: wrap the alt-screen escape writes (and the exit/cleanup writes) in try/catch so a synchronous stdout error (EPIPE on terminal close, EAGAIN under backpressure) can't propagate uncaught from the effect and crash the app or corrupt the terminal. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
descriptionof the currently highlighted setting below the settings list in the/settingsdialogTest plan
/settingsand verify a description line appears below the settings list for the highlighted item🤖 Generated with Claude Code