Skip to content

Replace native TreeView sidebar with webview sidebar (#673) - #683

Open
jeonghun-jj-lee wants to merge 30 commits into
amico/issue-663-research-projectsfrom
amico/issue-673-webview-sidebar
Open

Replace native TreeView sidebar with webview sidebar (#673)#683
jeonghun-jj-lee wants to merge 30 commits into
amico/issue-663-research-projectsfrom
amico/issue-673-webview-sidebar

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

Summary

Replace the native TreeDataProvider sidebar with a WebviewViewProvider that 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)

  • SidebarViewProvider with CSP-nonce HTML shell, VS Code theme tokens
  • sidebar_bridge.ts: typed SidebarMessage union, 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 treewebview, viewsWelcome removed, tree-scoped context menus removed
  • Deleted workspace_tree.ts (311 lines), trees.ts (40 lines, dead code)

Slice 2: Project tree with lazy file browsing (#675)

  • SidebarTreeService: scans workspace folders, classifies via detectProjectType(), reads research-project.toml for name/phase
  • Bridge: get-roots, get-children (lazy), open-file, fs-changed
  • Research Projects grouped before Dev Projects, lifecycle phase pills
  • .git hidden, files.exclude patterns respected, dirs-first alphabetical sort
  • FileSystemWatcher posts fs-changed → webview invalidates cache
  • Tree state persisted via setState/getState
  • Fleet placeholder section ("coming soon")

Slice 3: Context menus and file operations (#676)

  • 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
  • file-op-error response posted back on failure
  • Delete invariant: always useTrash: true — no permanent delete path
  • Rename validates target collision before calling fs.rename()

Slice 4: Session-aware project highlighting (#677)

  • setActiveProject(path | null): pushes active-project message, deduplicates
  • Webview highlights matching root node with accent border + selection background
  • Auto-expands highlighted project root on session switch
  • path: null → neutral state (no highlight)
  • Sidebar follows sessions; never drives session switching

Testing

24 tests covering:

  • Provider instantiation, HTML shell, CSP nonce, button rendering
  • Bridge message dispatch (all kinds)
  • Build pipeline (esbuild entry point, package.json registration)
  • Tree service: project classification, lazy-load, filtering, sorting
  • File operations: dispatch, error handling
  • Session awareness: highlight, dedup, switch, clear

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

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
@jeonghun-jj-lee jeonghun-jj-lee self-assigned this Aug 31, 2026
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 43b2d830-e23f-45f2-b7c8-68417c3f2f36

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant