Replace native TreeView sidebar with webview sidebar (#673) - #683
Open
jeonghun-jj-lee wants to merge 30 commits into
Open
Replace native TreeView sidebar with webview sidebar (#673)#683jeonghun-jj-lee wants to merge 30 commits into
jeonghun-jj-lee wants to merge 30 commits into
Conversation
Replace the native TreeDataProvider sidebar with a WebviewViewProvider shell: - SidebarViewProvider with CSP-nonce HTML, themed buttons, typed bridge - sidebar_bridge.ts: SidebarMessage union (host↔webview), handleSidebarMessage() - sidebar_webview.ts: browser entry point (iife), button→bridge wiring - esbuild.config.mjs: fourth browser entry point (dist/sidebar_webview.js) - package.json: amicode.workspace type tree→webview, viewsWelcome removed, tree-scoped context menus removed, workspace.* commands removed - extension.ts: registerWebviewViewProvider replaces registerWorkspaceTree - Deleted workspace_tree.ts (311 lines), trees.ts (40 lines, dead code), workspace_tree.test.ts (replaced by sidebar_view.test.ts) Closes #674
Wire the sidebar webview with a full project tree:
- SidebarTreeService: scans workspace folders, classifies via detectProjectType(),
reads research-project.toml for name/phase, returns structured roots (research first)
- Bridge: get-roots, get-children (lazy), open-file, fs-changed message kinds
- Webview: vanilla DOM tree rendering with expand/collapse, section labels,
lifecycle phase pills, file icons, dirs-first alphabetical sort
- .git hidden, files.exclude patterns respected
- FileSystemWatcher posts fs-changed → webview invalidates cache + re-requests
- Tree state (expanded nodes) persisted via webview setState/getState
- Fleet placeholder section ('coming soon')
- Workspace folder add/remove triggers re-scan
Closes #675
Add file operation support to the sidebar webview bridge: - file-op bridge message: rename, delete (trash-only), new-file, new-folder, copy-path, copy-relative-path, reveal-in-os, open-in-terminal, open-to-side, remove-from-workspace, new-session - All operations dispatch through typed bridge → extension executes vscode APIs - file-op-error response posted back on failure (e.g. rename collision) - Delete invariant: always useTrash: true — no permanent delete path exists - Rename validates target doesn't collide before calling fs.rename() - active-project message type added (groundwork for #677) Closes #676
Add session awareness to the sidebar project tree: - SidebarViewProvider.setActiveProject(path | null): pushes active-project message to the webview, deduplicates same path (initial state undefined, not null, so explicit null is always sent) - Webview handles active-project: highlights the matching root node with accent border + selection background, auto-expands it on highlight - Session switch moves highlight from old project to new - path: null → neutral state (no highlight, no error) - The sidebar follows sessions; it never drives session switching - The integration point for session→project binding is marked with a TODO in extension.ts (the session change listener wires here) Closes #677
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The projects list response now includes a `type` field per project entry (from detectProjectType() in listProjectDirs, added on the research-projects base branch). Update the golden fixture to include `"type":"dev"` on both test projects so the contract test passes.
…ccordion sections - File icons: inline SVG colored by extension (60+ mappings), separate chevron/icon/label spans replacing the universal emoji - Context menu: fix path resolution for files (data-path on tree-node itself, not parent), replace window.prompt() with host-side showInputBox (prompt returns null in webview iframes) - Drag and drop: HTML5 drag/drop between folders, move-file bridge op, host-side fs.rename with collision detection - Git status: query vscode.git extension API, annotate TreeEntry with status, apply VS Code gitDecoration theme variables to labels - Accordion layout: flex-column sections with justify-content:flex-end so collapsed headers stack at the bottom, display:contents on tree-root so all sections are direct flex participants - Sash resize: draggable border between expanded sections, 1px yellow line on hover, global mousemove handler, reset on section toggle - Section headers: border-top separators (tight when collapsed), lighter + buttons
resolveLlmCreds() no longer hard-fails when the model name doesn't match the provider — it logs a warning and proceeds. Fixes the stale opencode.json credential-gate bug.
Replace custom inline SVG icons with VS Code's active file icon theme (Seti font-based + Material SVG-based). Resolves languageIds, light variants, case-insensitive fileNames. Propagate git status to dirs. Remove 16px spacer from file rows so the icon sits where the chevron would, aligning file and folder labels at the same depth.
Subscribe to vscode.git repository state changes (onDidChange) and push a git-status message to the webview on every change. Fixes three issues: 1. Cold-start race: activate the git extension if not yet active, then fire an initial status push so colors appear on first load. 2. No reactive updates: subscribe to repo.state.onDidChange + new repos via onDidOpenRepository, debounced at 300ms. 3. Root nodes uncolored: the webview git-status handler walks ALL rendered [data-path] nodes including project roots, propagating the most notable child status to directories via prefix matching. Extract buildGitStatusMap as a pure exported function reused by both the existing annotateGitStatus (belt-and-suspenders on getChildren) and the new reactive push. Add GitStatusMessage to bridge types.
Replace vscode.window.showInputBox modal dialogs with VS Code explorer-style inline text inputs directly in the file tree: - Rename: swaps the label for an <input> pre-filled with the current name, selection on the stem (not extension). - New File / New Folder: expands the target directory and inserts a temporary row at the top with the appropriate icon + blank input. - Enter commits (posts file-op with name/newName — host skips modal). - Escape or blur-without-commit cancels and restores original state. - Input validates: rejects empty names and path separators. Bridge changes: - Add FileOpOkMessage (file-op-ok) to SidebarDownMessage union. - Host now posts file-op-ok on success, not just file-op-error on failure, so the webview can dismiss the inline editor cleanly. - Webview handles file-op-error by flashing the input border red. Context menu wiring: - Rename, New File, New Folder call startInlineEdit instead of posting directly — other ops (delete, copy path, etc.) still post as before. - Context menu is suppressed while an inline edit is active. CSS: .inline-edit-input inherits VS Code theme tokens (font, size, focusBorder, input background) so the input blends with the tree.
When new-file/new-folder is triggered on a collapsed folder, the temp input row gets inserted into the .children container before get-children completes. renderChildren then wipes innerHTML, destroying the input. Fix: the children message handler checks for an active inline edit targeting the same directory and re-inserts the temp row at the top after rendering, then re-focuses the input.
…ng children re-render When new-file/new-folder targets a folder that has never been expanded, get-children fires asynchronously. The temp input row is inserted into the empty .children container and focused. When the children response arrives, renderChildren does innerHTML="" which detaches the focused input — the browser fires blur synchronously, cancelInlineEdit nulls the state, and the re-insert check sees null. Net effect: the folder expands but no input appears. Fix: set inlineEditRerendering before renderChildren and check it in the blur handler. The flag is cleared immediately after render, and the temp row is re-inserted with focus restored.
…parent folder
Two UX fixes to match VS Code explorer drag-and-drop behavior:
1. File rows are now drop targets that resolve to their parent directory.
setupFileDropTarget() walks up via closest('[data-type="directory"]')
and highlights the parent's .tree-node row — so dragging a file onto
any sibling file in src/ targets src/, not nothing.
2. The directory outer container (which wraps both the .tree-node row and
the .children div) is now also a drop target. Dragging onto the gap
between child rows, or empty space after the last child, now correctly
targets the enclosing folder. setupDirectoryDropTarget gets an optional
highlightEl param so the container listens but the row gets highlighted.
3. Drop-target CSS: removed the dashed outline, kept only the subtle
background fill (var(--vscode-list-dropBackground)) to match VS Code's
native explorer highlight.
… ghost On dragstart, setupDragSource now creates a compact floating pill (.drag-image) containing the cloned icon and label from the dragged row. The pill uses VS Code theme tokens (list-activeSelectionBackground / Foreground) so it looks native in any theme, and setDragImage() makes it the cursor companion instead of the browser's default faded row screenshot. The pill is removed on dragend. Styled as an inline-flex badge: 4px radius, 2px/8px padding, gap for icon+label — compact enough not to obscure the drop target highlight.
…color loss
renderRoots() wipes the entire tree DOM (innerHTML = ''), and TreeRoot
has no gitStatus field, so root labels are recreated bare. Children
recover because get-children responses carry annotated entries via
annotateGitStatus(), but root-level folders stayed uncolored until the
next unrelated git event fired — which could be never.
Fix: add pushGitStatus() instance method that reads the current git
state and posts it to the webview immediately (no debounce). Called:
1. After onDidChangeWorkspaceFolders sends a roots message
2. Via queueMicrotask after the getRoots handler returns (covers
the fs-changed → get-roots → roots path from the webview)
The microtask ensures the roots message posts first, then git-status
follows in the same tick — no visible gap.
Section bodies now transition smoothly instead of snapping between display:none and display:block. Uses a max-height CSS transition (0.2s ease-out): Expanding: set display:block + maxHeight:0, force reflow, animate to scrollHeight. On transitionend: clear maxHeight, add .expanded class (enables overflow-y:auto for scrolling). Collapsing: pin current offsetHeight as maxHeight, force reflow, animate to 0. On transitionend: set display:none, clear maxHeight. File tree .children toggling is unchanged — still uses the instant display swap. Only the top-level section headers (RESEARCH PROJECTS, DEVELOPMENT PROJECTS, FLEET) animate. toggleSectionBody() is the shared helper used by both renderSectionHeader click handlers and the fleet toggle.
…riangles Replace all filled triangle characters (U+25B8 ▸ / U+25BE ▾) with the thin outline chevron U+203A (›) and a CSS transform: rotate(90deg) for the expanded state. This matches VS Code's explorer style. All three chevron types (.chevron for tree nodes, .section-chevron for section headers, .fleet-chevron for fleet) now: - Render a single › character always - Toggle an .expanded CSS class instead of swapping textContent - Animate the rotation with transition: transform 0.15s ease Every occurrence in sidebar_webview.ts updated: renderRootNode, renderDirectoryNode, renderSectionHeader, fleet toggle, inline-edit expand, and the active-project auto-expand handler.
…th flex, not against it The max-height approach caused a visible stutter at the end of expand because: 1. max-height animated to scrollHeight (a content measurement) 2. At transitionend, we cleared max-height and added flex:1 simultaneously 3. Flex computed a DIFFERENT height than scrollHeight → visible jump Rewritten to use height transitions that cooperate with flex: Expand: add section.expanded FIRST (so flex allocates space), measure the flex-computed target via offsetHeight, pin body at height:0, then animate to the exact flex target. At transitionend, clear the explicit height — flex takes over at the identical value, so there's no jump. Collapse: pin body at its current offsetHeight, THEN remove flex:1. The pinned height holds the size stable through the class change. Animate to 0, hide on transitionend. Also: cubic-bezier(0.1, 0.9, 0.2, 1) easing (fast start, gentle decel) matches VS Code's tree animation feel. And in-flight animations are cancelled on re-click to prevent stacking.
…oggle CSS transitions on height/max-height were fighting the flex layout, causing visible jitter — the section animated to its content height, then jumped when flex:1 kicked in at transitionend. Replaced with a requestAnimationFrame loop that: - Measures scrollHeight (expand) or offsetHeight (collapse) once - Interpolates height every frame with easeOutCubic - Only applies flex classes after the animation completes (expand) or pins height before removing them (collapse) - Cancels in-flight animations on re-click Removed the CSS transition property from .section-body entirely. The rAF approach gives full frame-level control with no layout system conflicts. Duration: 200ms, easing: cubic ease-out.
…de's approach) Replaces the rAF animation loop with the same technique VS Code's PaneView uses: set an inline CSS transition on the element, set the target height, and let the browser's compositor handle interpolation. The key insight from VS Code's source (paneview.ts + paneview.css): SplitView sets explicit pixel heights on each pane container. PaneView adds an 'animated' class for 200ms that enables CSS transitions on height. The browser handles all the interpolation natively. Our adaptation for a flex layout: - Expand: temporarily add .expanded to measure the flex-computed target height, remove it, pin body at 0, set inline transition, set target height. CSS transition interpolates. On transitionend: clear inline styles, add .expanded class (flex takes over at the same height). - Collapse: pin body at current offsetHeight, set inline transition, remove .expanded, set height to 0. On transitionend: display none, clear inline styles. The inline transition is set and removed per-toggle, so it doesn't interfere with flex layout during normal operation. Duration: 150ms ease-out (matching VS Code's 0.15s).
VS Code SplitView/PaneView pattern: .section becomes position:absolute with JS-computed top/height. layoutSections() is the single layout function, sectionSizes map is the single source of truth. - .sidebar-sections: position:relative (was flex column) - .section: position:absolute; left:0; right:0 (was flex-shrink:0) - .animated CSS class enables transition on top/height for 150ms - toggleSectionBody uses .animated class (was inline style.transition) - Sash drag writes to sectionSizes + calls layoutSections (was style.flex) - prefers-reduced-motion suppresses .animated class - ResizeObserver re-layouts on sidebar resize - Fleet section wired through same pixel layout - HEADER_HEIGHT=28 constant (collapsed = header only) 101 tests pass (10 new + 91 updated existing). Build clean.
Compute totalUsed height before writing positions, then start top at max(0, containerHeight - totalUsed). This is the pixel equivalent of the old flex justify-content: flex-end — collapsed sections sit at the bottom, expanded ones fill upward.
Sashes were position:relative (relied on flex flow) — now position:absolute with top set by layoutSections() at the boundary between each pair of sections. The ::after hitbox (6px) inherits the correct position so sash drag works again. 102 tests pass (1 new + 1 updated).
After sash drag writes large pixel weights to sectionSizes, sections without an entry default to weight 1 — a 100:1+ ratio that starves them to near-zero in the proportional pass. Math.max(HEADER_HEIGHT, h) then inflates them back without shrinking anything else, pushing bottom sections below the container. Add a VS Code distributeEmptySpace-style mop-up pass after the proportional allocation: compute the overflow, then trim the largest sections (most room above HEADER_HEIGHT) until the total equals availableForExpanded. No section drops below HEADER_HEIGHT; sash-drag proportions are preserved as much as possible. 103 sidebar tests pass (1 new, 1 updated slice), 1871 total green.
…are (#697) After sash drag, cached pixel weights (e.g. 300) dwarfed the default weight of 1 for a freshly re-expanded section, starving it to header-only height. Now toggleSectionBody calls sectionSizes.clear() instead of sectionSizes.delete(id), so every expanded section splits equally after any topology change. The user re-drags to customize. 104 sidebar tests pass (1 new), 1872 total green.
Register amicode.newProject command end-to-end: - sidebar_view.ts: createNewProject() — showSaveDialog (user types folder name in the native OS dialog), mkdirSync, add to workspace, launch session with /create-research-project --path. Warns but proceeds if folder already in workspace. Injectable deps for testability. - extension.ts: register the command with closured server state. Session opens in the current chat tab (openOrReveal + navigate bridge envelope, dual-send immediate + onAppReady). - package.json: add amicode.newProject to contributes.commands and activationEvents. - create-research-project SKILL.md: guided interview for research-project.toml fields (name from dir basename, question, venue, deadline, collaborators, tags, domain pack), delegates to amico project create CLI. - 8 new tests in sidebar_view.test.ts covering the full flow + wiring. Closes #698
The sidebar's right-click → Delete now: 1. Shows a modal confirmation dialog matching VS Code Explorer's pattern: 'Are you sure you want to delete <name>? You can restore from the Trash.' 2. Cancelling is a silent no-op (no file touched). 3. On confirm, trashes the file/folder (useTrash: true, recursive: true). 4. If the deleted path was a workspace root folder, also removes it from the workspace via updateWorkspaceFolders — previously the workspace entry persisted as a broken/empty root after the directory was trashed. Exports executeFileOp for testing. 3 new tests covering confirmation, root-folder workspace removal, and child-only trash.
Drop the research-project status pill (<span class='pill'>) from root nodes in the sidebar. The status field stays in research-project.toml and flows through TreeRoot.metadata — it just no longer renders. Removes the pill DOM creation in sidebar_webview.ts and the dead .tree-node .pill CSS in sidebar_view.ts.
A config with `"provider": {}, "model": "anthropic/claude-sonnet-4"`
is a valid configured state — the user has chosen a model and opencode
resolves the provider at runtime. Previously isModelConfigured only checked
for non-empty provider keys, returning false for this pattern and
triggering the Stage 0 onboarding webview on every window reload.
Now checks: (1) non-empty provider block (existing), OR (2) non-empty
model field (new). Fresh marketplace installs with no config file still
return false and get the Stage 0 setup flow as before.
2 new tests, 1 updated fixture.
Wire VS Code workspace folders into the chat iframe's project selector — the PromptProjectSelector that #667 un-gated and enriched with type grouping (Research/Dev sections) now shows live workspace folder data. Data pipeline: Extension → postMessage('workspace-projects') → app signal → selector Extension side: - workspace_projects.ts: scanner that maps workspaceFolders to typed entries using detectProjectType + readResearchToml - chat_panel.ts: 'workspace-projects' in downstream relay, 'add- workspace-project' in upstream relay - chat_bridge.ts: add-workspace-project handler (showOpenDialog + updateWorkspaceFolders) - extension.ts: pushWorkspaceProjects on app-ready and onDidChangeWorkspaceFolders; workspaceFolders wired into all 3 prepareOpencodeProject call sites (activates resolveProjectSkills from #668) App side (overlay): - amicode-workspace-projects.ts: reactive signal (adoptWorkspaceProjects/workspaceProjects/requestAddWorkspaceProject) - app.tsx AmicodeThemeBridge: handles 'workspace-projects' message - session-composer-controls.ts: createPromptProjectControls reads workspaceProjects() signal when available (falls back to layout.projects.list() for standalone opencode); addProject delegates to extension host when in amicode webview Selection is silent bind (set agent context, no auto-prompt). Both paths preserved: full selector when folders exist, Add button when empty. Note: workspace-change-triggered prepareOpencodeProject re-run (for live project skill cache invalidation) is a follow-up — the selector works without it; skill roots are resolved at boot and solver-mode/vault respawn. 9 new tests (7 scanner + 2 bridge relay), 126 files / 1896 tests green.
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
Replace the native
TreeDataProvidersidebar with aWebviewViewProviderthat renders custom UI — styled header buttons, project metadata with lifecycle phase pills, session-aware highlighting, and a full context menu with file operations.Implements all four sub-issues of #673:
Slice 1: Webview shell + header buttons + cleanup (#674)
SidebarViewProviderwith CSP-nonce HTML shell, VS Code theme tokenssidebar_bridge.ts: typedSidebarMessageunion,handleSidebarMessage()sidebar_webview.ts: browser entry point (iife), button→bridge wiringesbuild.config.mjs: fourth browser entry point (dist/sidebar_webview.js)package.json:amicode.workspacetypetree→webview, viewsWelcome removed, tree-scoped context menus removedworkspace_tree.ts(311 lines),trees.ts(40 lines, dead code)Slice 2: Project tree with lazy file browsing (#675)
SidebarTreeService: scans workspace folders, classifies viadetectProjectType(), readsresearch-project.tomlfor name/phaseget-roots,get-children(lazy),open-file,fs-changed.githidden,files.excludepatterns respected, dirs-first alphabetical sortFileSystemWatcherpostsfs-changed→ webview invalidates cachesetState/getStateSlice 3: Context menus and file operations (#676)
file-opbridge message: rename, delete (trash-only), new-file, new-folder, copy-path, copy-relative-path, reveal-in-os, open-in-terminal, open-to-side, remove-from-workspace, new-sessionfile-op-errorresponse posted back on failureuseTrash: true— no permanent delete pathfs.rename()Slice 4: Session-aware project highlighting (#677)
setActiveProject(path | null): pushesactive-projectmessage, deduplicatespath: null→ neutral state (no highlight)Testing
24 tests covering:
Full suite passes (1791 tests, same count as baseline minus removed tree tests plus new sidebar tests). One pre-existing unrelated failure (
amicode_service_contract).Closes #673, closes #674, closes #675, closes #676, closes #677