Repository navigation
侧栏集成分支:门禁修复 + #3 健壮性安全 + #4 审查修复 + #5 性能 + #6 泄漏 - #2
Merged
Merged
Conversation
Code viewer tab ids carry the file path. On Windows the backslashes are read as CSS escapes, so the selector never matched and keyboard focus fell to <body> after cycling to, closing, or leaving the menu of such a tab; a path containing a quote made the selector throw and the mount fail. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
… literal colour check:ui-system was red on the side pane branch. tool-presentation.css had 101 selectors outside .vcp-ui-scope, an !important on the reduced-motion rule and two fixed 11px sizes; components.css (from upstream lioensky#234) fell back to a hard-coded indigo because --vcp-ui-primary is not a token. Every tool presentation selector now sits under .vcp-ui-scope (the chat messages container already carries it, and the file is alone in its layer, so the uniform specificity bump changes no cascade). The reduced-motion rule reuses the shimmer selector instead of !important, the 11px sizes use --vcp-ui-text-xs, and the pinned window button uses the accent token. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
Before, a provider that threw during mount (a corrupt restored payload, a failed lazy import, a bug) left its tab selected over a blank pane, with the error only in the console. The view now says the tab could not open, shows the error, and offers a retry; showing the tab again still retries as before, and closing the tab removes the error view. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
…wn with it initWorkspaceSidePane ran inside the main startup try block ahead of the filter manager and setupEventListeners, so any exception while building the side pane skipped the rest of startup and left the send button and the other main window bindings dead. It now runs in its own try/catch and reports the failure as a toast. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
A file:// page in the side browser runs with Electron's default file privileges, so its script could fetch other local files (SSH keys, VCPChat settings) and post them out. Like DeepSeek Harness (browser-guests.ts onBeforeRequest) and ZCode (isAllowedBrowserUrl), the side browser now only opens http, https and about pages, and its session cancels file: and other non-web subresource requests. Any page could also start downloads on its own, and each one opened the system browser. A download now reaches the system browser only right after a real input on that page, one per input, the same rule popups use. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
…sible focus ring - Arrow keys re-rendered the strip and then focused the removed button, while activateTab moved focus into the panel, so each press threw a keyboard user out of the tablist. Keyboard switches now activate without moving focus and refocus the new button by id, as Radix Tabs (used by ZCode) does. - While the new-tab page or hidden notifications was active, no tab had tabindex=0, so the strip had no Tab stop. The first tab now gets it. - Delete closes the focused closable tab. - Tabs and header buttons removed the outline with no replacement; they now get the same focus ring as the tab menu items. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
render() rebuilds the whole page, and it only put focus back on filter inputs and page tabs. Toggling a section or a batch, opening filters, picking a contributor, or any background refresh dropped focus to <body>. Interactive buttons now carry a data-focus-key that render() records and restores, the imperative equivalent of React keeping keyed nodes in ZCode and DSH. Clearing the filters lands on the search box. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
Shift+F10 on a change row opened the menu at (0,0) with focus left on the row, no arrow navigation, and Escape dropped focus. Like the Radix ContextMenu in ZCode's GitPaneChangeCard, the menu now opens beside the row when there is no pointer, takes focus, moves with the arrow keys, Home and End, and Escape, Tab or choosing an item returns focus to the row. Icon ligature text is hidden from screen readers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
The button called openPythonAttachmentInTextEditor, which only accepts .py file URLs on Windows, so it failed silently for every file. Sending the path to open-external-link instead would hand model-supplied paths (.bat, .lnk) to shell.openExternal. The button now reveals workspace files through the workspace-checked git:reveal-path and shows the path for anything else. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
…keys stop Each arrow, Home or End press wrote settings over IPC, about 30 writes a second while an arrow key was held. The width still applies on every press; the save now waits until the keys stop for 400 ms (the debounced onLayoutChange ZCode's resizable.tsx uses), and runs at once when focus leaves the handle or the pane is torn down. Pointer drags save on release as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
…focus hold under real use - Reopening while the close animation runs no longer leaves the pane collapsed with its expand button hidden; a repeated open no longer jumps back to zero width. - With no current conversation (group selected, early startup) topic tabs of other conversations stay off the strip, so "close all" can no longer delete their side chats. - Keep views (side chats) no longer take places under the live-view limit; browser tabs sleep only for the limit, not for being hidden; collapsing the pane no longer puts the current tab to sleep. - Arrow keys on the tab strip keep focus on the tab; tab ids with Windows paths or quotes are looked up without building selectors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
The 430-odd side pane, side chat, git and browser tests were not in any workflow; test:side-pane gathers them and the Chat Kernel and UI job runs it. The PTY-backed terminal handler tests need a real PowerShell and stay out until a Windows job exists. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
…en and re-show in constant time Measured in the real Electron app (headless, 6000-line JS file): opening went from 1215ms to 330ms and switching back to the tab from 752ms to ~110ms with no long task. Before, focusing the re-shown view forced a full style and layout pass over ~60k highlight nodes; now only the chunks in the viewport take part. Gutter and code are split into 200-line blocks with content-visibility:auto and an intrinsic height from the fixed 18px line height, so numbers stay aligned. highlight.js still highlights the whole text once; its output is split per line with spans that cross lines closed and reopened, so multi-line comments and template strings keep their colours. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bCbRB3E9yajQ3HbdMrjiJ
File paths shown as changed files come from the model's tool-call arguments, and the viewer read any of them through get-text-content, so a prompt-injected path such as ~/.ssh/id_rsa showed its content, one click from being inserted back into the chat. Like the workspace-bound previews in ZCode and DeepSeek Harness, a path outside every registered workspace now shows where it is and is only read after the user clicks to read it. Workspace files are read as before; windows without the workspace service keep the old behaviour. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
git status runs whatever command a repository's .git/config names in core.fsmonitor. A project unzipped with a prepared .git folder could therefore run code as soon as the Git page read its status. Every git call now passes -c core.fsmonitor=false, as DeepSeek Harness does in scripts/change-scope.ts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
… literal colour check:ui-system was red on the side pane branch. tool-presentation.css had 101 selectors outside .vcp-ui-scope, an !important on the reduced-motion rule and two fixed 11px sizes; components.css (from upstream lioensky#234) fell back to a hard-coded indigo because --vcp-ui-primary is not a token. Every tool presentation selector now sits under .vcp-ui-scope (the chat messages container already carries it, and the file is alone in its layer, so the uniform specificity bump changes no cascade). The reduced-motion rule reuses the shimmer selector instead of !important, the 11px sizes use --vcp-ui-text-xs, and the pinned window button uses the accent token. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
…pect the busy gate, the filter accepts input-method typing - Closing a side chat that has messages deletes its child topic, so it now asks first (also for close others / close all); declining keeps the tab. - A second send while a reply streams is refused, so the running reply keeps its stop button. A message button no longer overwrites or sends a typed draft, references or attachments. - The plan history filter no longer rebuilds the page while an input method composes, so Chinese input works; the search runs once composition ends. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
…ection copies - xterm's default OSC 8 handler showed a confirm and called window.open from the main window, which opened a bare window on the default session. The tab now passes a linkHandler that only opens http(s) through the side browser, as ZCode does. - Ctrl/Cmd+C with a selection copies it instead of sending ^C to the PTY shared with the AI tools; without a selection Ctrl+C still interrupts. - Ctrl/Cmd+Alt+B and Ctrl+PageUp/PageDown drive the side pane only and are no longer written to the shell as escape codes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
…fix branch claude/project-thread-20fk2r (PR #3) already finds tabs by id without selectors and keeps arrow-key focus on the strip, with its own tests. Drop the duplicate here so the two branches do not conflict. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
Keeps the change clear of the focus-restore lines the parallel fix branch edits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
PR #4 adds the same tab lookup as findByTabId(container, selector, tabId). Use the same signature, text and keyboard comments so the two branches merge without a semantic clash, and rename this branch's keyboard test so the files don't collide. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
Merged
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
This was referenced Oct 7, 2026
Merged
If the reply finished on its own while a slow interrupt request was pending, the failed interrupt deleted it as if it were an empty placeholder (side chat and main chat). Now only a still-streaming placeholder is removed. If upstream accepts the interrupt but never sends a terminal, the side chat stops locally after 3s and saves the partial as cancelled instead of staying busy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
…to claude/project-thread-20fk2r
21a0a48 moved restart onto the app confirm dialog, which is async and focuses its own button, but the click handler still called term.focus() right after restart(). Focus went back to the xterm behind the dialog, so typing reached the shared shell while the dialog was open. The handler now leaves focus alone when a confirm will be shown; the dialog returns focus to the restart button when it closes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KnTGLLVUgUjQG3f4cWBHJw
…to claude/project-thread-1zv9nc # Conflicts: # modules/ui-system/side-pane/git/git-view.js # modules/ui-system/side-pane/modelTrajectorySideProvider.js
After #14 side chats use dormancy 'keep', so the type note that said unsent input should use isBusy() instead of keep, and the isBusy examples that named a sending side chat, no longer matched the code. The side chat handle's setVisible, removed as dead code in 21a0a48, came back with #14's revert and is removed again. The architecture doc's terminal example used a deps.notify that does not exist, and the side chat owner is about 770 lines, not 560. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KnTGLLVUgUjQG3f4cWBHJw
States now go error, then loading, then empty (as ZCode GitPane does), so a load in progress or a failed read is never shown as "nothing here": - Git: no "还没有工作区 / 添加工作区" before the workspace list arrives or when it fails; the error has a retry. - Code viewer picker: a failed workspace read says so; the file list error has an in-place retry. - Trajectory: no "请先选择智能体和话题" flash while subscribing. - Tool output: a failed command list shows the error with a retry instead of "还没有命令记录"; a rejected output read lands on the error bar instead of hanging on "加载中…"; a failed reload during a running command keeps the output on screen. - Side chat: the intro stays hidden while history loads; the history retry works from the keyboard. Layout and feedback: - Git diff: an open diff keeps the previous text while an edited file is refetched instead of collapsing to one line; failed diffs are not cached and have a retry. - Plan: retry buttons switch to "正在重试…" at once; a background refresh with a filter swaps results in place; filter errors retry. - Code viewer: reloading a shown file keeps it until the new text arrives; its loading icon actually spins. Docs: dormancy notes follow #14 (side chats stay mounted). Tests for the new cases use waitFor instead of fixed sleeps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
… listener owner
…t send owner tests in test:workbench Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
…s to keep Mutation runs against #15's tests (5ff688f) found three regressions they no longer catch: - regenerate's removeMessageById(id, true): the mock stopped recording the save flag, so a renderer removal that saves again (and can write to the renderer's current topic) passed. Both delete and regenerate assert [id, false] again. - terminal toolbar listeners bypassing the view scope: `listener > 0` goes back to the exact count of 3. - the pane's active / collapsed classes after a reversed animation: they drive the layout (side-pane-shell.css) and the visibility check, so the four class asserts come back. Nothing tested that side chats stay mounted (#14 dropped the sleep tests only); setting the chat type back to 'none' passed every test. A draft-restore case now hides a side chat past hiddenMs and checks the same handle, draft and references are still there. Also drops the duplicate busy mark the #15 merge left in side-pane-dormancy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KnTGLLVUgUjQG3f4cWBHJw
…yAsahi/VCPChat into claude/project-thread-8tpfrr
The source editor rejected only a literal .git segment. On Windows the 8.3 short name GIT~1, trailing dots/spaces and the ::$INDEX_ALLOCATION stream name reach the same directory (git's is_ntfs_dotgit), and on any platform a workspace symlink pointing at .git let source:write-file overwrite .git/config (core.fsmonitor, hooks path). Segments are now checked with git's NTFS rules, and the real parent directory is checked for .git too. The Git panel uses the same segment check, and the reveal fallback compares paths with path.relative instead of a case-sensitive prefix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
…low it Opening the side terminal tab resized the shared PTY to the narrow pane even while the terminal window (and AI commands) were using it at full width, and a side view without focus drew the PTY's output at its own width. - terminal:create only presets the size when no PTY is running yet, and returns the PTY's real cols/rows. - PowerShellExecutor tells mirror views when the PTY size changes; the main process forwards it as terminal:resized (after flushing buffered output). - The side view claims the size only while it has focus (and only when it differs); without focus it draws at the PTY's size, as DSH's read-only views do, and fits to its own width again when the user clicks in. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012AP71nAxrCFEpH7st6asf9
…s it Sending in the side chat disabled its textarea until the reply finished. Focus fell to <body>, so anything typed during the reply was dropped, the next message needed another click into the box, and Esc could not stop the reply. Clicking the stop button also left focus on <body> because the button hides itself. The textarea now stays enabled the way the main chat's does; the existing busy guard still keeps Enter from starting a second round, so the next question can be written while waiting. Esc in the box stops the reply, like ZCode's Esc-to-stop, and the stop button hands focus back to the box. If a pending question is retracted while the user has written something new, it is put back in front of that draft instead of being dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019L2bZomLn4NVoxS4Rv5Bkv
…sion guards # Conflicts: # modules/ui-system/side-pane/side-pane-types.js
Move/up/cancel were only heard on the strip, so releasing outside it (or losing pointer capture when the dragged node was replaced by a render) left the drag session alive and every later press was ignored. They are now heard on the document for the length of a press. Renders that arrive during a drag (a browser tab changing its title or icon while loading) are held until the drop, so the dragged node is never swapped out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012AP71nAxrCFEpH7st6asf9
- Plan detail: a focus target from the status panel is applied once and
then cleared from the saved tab, so restart or wake no longer jumps back.
- Side chat restore keeps the saved createdAt, so lists keep their order.
- A local-only side chat draft that cannot be read now warns the user
instead of silently restoring blank.
- Width persistence logs a {success:false} settings save result.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
The restart button called session.restart() and then term.focus(). When the shell is running, restart() first opens the confirm dialog, which focuses its own button; the terminal then took focus straight back. Esc still closed the dialog through its document listener, but the keystroke also reached xterm and bash read it as a Meta prefix, so the next character typed was eaten (typing "echo still" ran "cho still"). Focus the terminal first and then restart: the dialog takes focus from the terminal and hands it back when it closes, and Enter on it confirms without a stray newline in the shell. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019L2bZomLn4NVoxS4Rv5Bkv
…llapses it At window widths of 960px or less the pane floats over the conversation and covers the send button, and the only way out was the collapse button or Ctrl+Alt+B. A click in the conversation now collapses it like a drawer, unless that click itself opened or switched a side tab (code block "副屏", ask in side chat). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012AP71nAxrCFEpH7st6asf9
…ix Windows file manager fallback
Opening the terminal tab and typing right away lost every key until the PTY attached. Input typed during the connection is now held (up to 4K chars) and sent in order once it connects; it is dropped if the connection fails, so a command meant for a terminal that never started is not replayed later. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012AP71nAxrCFEpH7st6asf9
The trajectory tab sleeps after 5 minutes hidden. Waking it reset every collapsed row and jumped the view to the bottom. It now captures its expansion overrides, commands and scroll position, and restores them when the remount still shows the same conversation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
…orkbench The .git alias and symlink regression test lives in source-service.test.js, which no CI job ran. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
…inal restart confirm keeps focus # Conflicts: # modules/ui-system/side-pane/terminalSideProvider.js # tests/side-pane-terminal-lifecycle.test.mjs
…row window overlay # Conflicts: # package.json
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
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.
Requested by Roxy · project thread
Motivation
This branch gathers every fix for the side pane PR (lioensky#231), so Codex can bring them all into
pr/side-pane-workbenchwith one merge. Each thread's PR comes in through a merge commit, and the branch is never force-pushed.Changes
:where(.vcp-ui-scope), which adds no specificity, and the pinned-window button uses the accent tokenfile:pages; downloads need a real click; git never runs the repo'score.fsmonitor; the code viewer asks before reading a file outside the workspaces; the source IPC goes through the top-level window sender guard; tabs close with Delete and keep focus on the strip; tab ids holding Windows paths are matched safelytest:side-panestepWeakMapand the turn navigator aWeakSet. In real Electron, opening and closing tabs leaves zero extra DOM nodes and listeners (before: 53–275 nodes per cycle)chat-event-graph.jsonafter the mergesWhere #3 and #4 overlap (
findByTabId, tab strip focus), the branch keeps #3's implementation with #4's signature. In the browser, busy protection for media and captures comes from #4, and download handling from #3.Testing
npm run test:side-pane,npm run test:chat-kernel,npm run check:ui-system,npm run check:chat-evidence, run at 2eb5587Proof
Leak measurements: real Electron driven over CDP, with GC forced before each sample. Data and repro scripts are in the project files under
sidebar/leaks/(see 侧栏端到端冒烟:真实 Electron 跑每类标签,计数必须回到起点 #6).Performance benchmarks: in the project files under
sidebar/benchmarks/(see 侧栏性能:Git 页长改动列表分批挂载 #5).The description follows the structure of DSH's
pull_request_template.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9