Skip to content

fix(sidebar): collapsible Workbench/Workspace nav sections#491

Open
lazmo88 wants to merge 2 commits intoThe-Vibe-Company:mainfrom
lazmo88:fix/collapsible-sidebar-sections
Open

fix(sidebar): collapsible Workbench/Workspace nav sections#491
lazmo88 wants to merge 2 commits intoThe-Vibe-Company:mainfrom
lazmo88:fix/collapsible-sidebar-sections

Conversation

@lazmo88
Copy link
Copy Markdown

@lazmo88 lazmo88 commented Mar 7, 2026

Summary

  • Make sidebar Workbench and Workspace nav sections collapsible accordion-style
  • Sections default to collapsed on mobile (<768px), expanded on desktop
  • Clickable section headers with chevron rotation indicator

Problem

On mobile viewports, the sidebar footer nav sections take ~300px of fixed vertical space (6 nav items × min-h-[44px] + section headers + padding), leaving the session list nearly unusable. Users have to scroll past Prompts, Integrations, Terminal, Environments, Agents, and Settings just to see their sessions.

Fix

Added collapsible toggle behavior to NAV_SECTIONS rendering, following the same pattern already used by "Scheduled Runs", "Agent Runs", and "Archived" sections in the sidebar:

  • Section header becomes a <button> with a chevron icon
  • Click toggles child nav items visibility
  • collapsedNavSections state tracks which sections are collapsed
  • Initialized from window.innerWidth — collapsed by default on mobile

Test plan

  • All 94 existing Sidebar tests passing
  • Sections expand/collapse on click
  • Chevron rotates to indicate state
  • Mobile: sections start collapsed, maximizing session list space
  • Desktop: sections start expanded, same behavior as before

Closes #490

Claude added 2 commits March 7, 2026 19:42
Repos initialized with `git init` but no commits have an "unborn" branch
where HEAD is a symbolic ref that doesn't point to any commit. This caused
session creation to fail with "Branch X does not exist" because:

1. `getRepoInfo()` used `rev-parse --abbrev-ref HEAD` which fails on unborn
   branches, falling back to "HEAD" — so the branch name check always
   triggered a checkout attempt.
2. `checkoutOrCreateBranch()` ran `git checkout <branch>` which fails on
   empty repos since no branches technically exist yet.

Fix:
- `getRepoInfo()`: Fall back to `symbolic-ref --short HEAD` which correctly
  returns the unborn branch name (e.g. "master" or "main").
- `checkoutOrCreateBranch()`: Detect empty repos via `rev-parse HEAD`. If
  already on the requested unborn branch, return success. If a different
  branch is requested, use `git checkout --orphan`.

Closes The-Vibe-Company#487
The sidebar footer nav sections (Workbench: Prompts/Integrations/Terminal,
Workspace: Environments/Agents/Settings) take ~300px of fixed vertical space
on mobile (6 items × 44px min-height + headers/padding). This leaves very
little room for the session list, making it nearly unusable.

Fix: Make section headers clickable toggles with a chevron indicator.
Sections default to collapsed on mobile (<768px) and expanded on desktop,
following the same pattern used by Scheduled Runs and Agent Runs sections.

Closes The-Vibe-Company#490
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 7, 2026

Someone is attempting to deploy a commit to the The Vibe Company Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 7, 2026

Greptile Summary

This PR bundles two independent changes: (1) a UX improvement making the Workbench/Workspace/Resources nav sections in the sidebar collapsible (collapsed by default on mobile), and (2) a correctness fix in git-utils.ts to gracefully handle newly-initialised repositories that have no commits yet.

Key changes:

  • Sidebar.tsxNAV_SECTIONS headers become <button> elements; a collapsedNavSections Set drives visibility; initial state is derived from window.innerWidth < 768.
  • git-utils.tsgetRepoInfo now falls back to symbolic-ref --short HEAD for unborn branches before returning "HEAD"; checkoutOrCreateBranch detects the no-commits case early and either returns immediately or uses git checkout --orphan.
  • git-utils.test.ts — Five new test cases covering every new branch added to the backend functions; all existing tests updated for a new rev-parse HEAD mock requirement.

Issues found:

  • The toggle button wraps an <h3> element, which is invalid HTML5 (headings are flow content, not phrasing content; <button> allows only phrasing content). Browsers may silently pop the <h3> outside the button in the parsed DOM, breaking the click handler.
  • No new tests cover the collapsible behavior (toggle, mobile default, desktop default), violating the project's frontend-testing requirement for behavior changes in web/src/components/**/*.tsx.

Confidence Score: 2/5

  • Not safe to merge until the invalid HTML nesting is fixed and collapsible-behavior tests are added.
  • The backend git-utils changes are well-tested and logically sound. However, the Sidebar change has a real HTML validity bug (<h3> inside <button>) that can cause browsers to silently restructure the DOM and break the toggle click handler, and the new interactive behavior ships with zero test coverage.
  • web/src/components/Sidebar.tsx — invalid HTML nesting and missing test coverage for new behavior

Important Files Changed

Filename Overview
web/src/components/Sidebar.tsx Adds collapsible NAV_SECTIONS with mobile-default-collapsed state. Two issues: <h3> is placed inside <button> (invalid HTML that browsers may reparse incorrectly), and no new tests cover the toggle or mobile-default behavior despite a clear behavior change.
web/server/git-utils.ts Adds empty-repo handling in getRepoInfo (symbolic-ref fallback) and checkoutOrCreateBranch (orphan branch path). Logic is sound and follows existing string-interpolation conventions throughout the file.
web/server/git-utils.test.ts Adds five new test cases covering unborn-branch detection, both fallback paths in getRepoInfo, and orphan-branch creation in checkoutOrCreateBranch. Coverage is thorough for the backend changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Sidebar mounts] --> B{window.innerWidth < 768?}
    B -- yes --> C[collapsedNavSections = all section IDs]
    B -- no --> D[collapsedNavSections = empty Set]
    C & D --> E[Render NAV_SECTIONS]
    E --> F{section.id in collapsedNavSections?}
    F -- yes / collapsed --> G[Render header button only\nchevron points right →]
    F -- no / expanded --> H[Render header button + child nav items\nchevron points down ↓]
    G --> I[User clicks header]
    H --> I
    I --> J[toggleNavSection: add/remove from Set]
    J --> F
Loading

Last reviewed commit: 12e0c11

Comment on lines +666 to +672
{NAV_SECTIONS.map((section) => {
const isCollapsed = collapsedNavSections.has(section.id);
return (
<section key={section.id} className="rounded-lg border border-cc-border/30 bg-cc-card/20 p-0.5">
<button
onClick={() => toggleNavSection(section.id)}
className="w-full flex items-center gap-1 px-2 py-0.5 cursor-pointer hover:text-cc-fg transition-colors"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<h3> inside <button> is invalid HTML

The new section-header button wraps an <h3> element as a direct child. Heading elements (h1h6) are "flow content" in the HTML5 spec, not "phrasing content", so they are not valid descendants of <button> (which requires phrasing content per spec). Browsers often silently recover by popping the heading outside the button in the parsed DOM, which would break the click handler. Screen readers also lose the button role or the heading role unexpectedly.

The original code had <h3> as a standalone sibling; make it a <span> (or plain text) inside the button to keep the semantics valid:

Suggested change
{NAV_SECTIONS.map((section) => {
const isCollapsed = collapsedNavSections.has(section.id);
return (
<section key={section.id} className="rounded-lg border border-cc-border/30 bg-cc-card/20 p-0.5">
<button
onClick={() => toggleNavSection(section.id)}
className="w-full flex items-center gap-1 px-2 py-0.5 cursor-pointer hover:text-cc-fg transition-colors"
<button
onClick={() => toggleNavSection(section.id)}
className="w-full flex items-center gap-1 px-2 py-0.5 cursor-pointer hover:text-cc-fg transition-colors"
aria-expanded={!isCollapsed}
>
<svg viewBox="0 0 16 16" fill="currentColor" className={`w-2.5 h-2.5 text-cc-muted/60 transition-transform duration-150 ${isCollapsed ? "" : "rotate-90"}`}>
<path d="M6 4l4 4-4 4" />
</svg>
<span className="text-[9px] font-semibold uppercase tracking-[0.14em] text-cc-muted/75">
{section.label}
</span>
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/Sidebar.tsx
Line: 666-672

Comment:
**`<h3>` inside `<button>` is invalid HTML**

The new section-header button wraps an `<h3>` element as a direct child. Heading elements (`h1``h6`) are "flow content" in the HTML5 spec, not "phrasing content", so they are not valid descendants of `<button>` (which requires phrasing content per spec). Browsers often silently recover by popping the heading outside the button in the parsed DOM, which would break the click handler. Screen readers also lose the button role or the heading role unexpectedly.

The original code had `<h3>` as a standalone sibling; make it a `<span>` (or plain text) inside the button to keep the semantics valid:

```suggestion
                <button
                  onClick={() => toggleNavSection(section.id)}
                  className="w-full flex items-center gap-1 px-2 py-0.5 cursor-pointer hover:text-cc-fg transition-colors"
                  aria-expanded={!isCollapsed}
                >
                  <svg viewBox="0 0 16 16" fill="currentColor" className={`w-2.5 h-2.5 text-cc-muted/60 transition-transform duration-150 ${isCollapsed ? "" : "rotate-90"}`}>
                    <path d="M6 4l4 4-4 4" />
                  </svg>
                  <span className="text-[9px] font-semibold uppercase tracking-[0.14em] text-cc-muted/75">
                    {section.label}
                  </span>
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 663 to 730
{/* Footer */}
<div className="px-2 py-1.5 pb-safe bg-cc-sidebar-footer border-t border-cc-border/30">
<nav className="flex flex-col gap-1.5" aria-label="Navigation">
{NAV_SECTIONS.map((section) => (
<section key={section.id} className="rounded-lg border border-cc-border/30 bg-cc-card/20 p-0.5">
<h3 className="px-2 py-0.5 text-[9px] font-semibold uppercase tracking-[0.14em] text-cc-muted/75">
{section.label}
</h3>
<div className="flex flex-col">
{section.itemIds.map((itemId) => {
const item = NAV_ITEMS_BY_ID.get(itemId);
if (!item) return null;
const isActive = item.activePages
? item.activePages.some((p) => route.page === p)
: route.page === item.id;
return (
<button
key={item.id}
onClick={() => {
if (item.id !== "terminal") {
useStore.getState().closeTerminal();
}
window.location.hash = item.hash;
// Close sidebar on mobile so the navigated page is visible
if (window.innerWidth < 768) {
useStore.getState().setSidebarOpen(false);
}
}}
title={item.label}
aria-current={isActive ? "page" : undefined}
className={`group flex min-h-[44px] md:min-h-[34px] w-full items-center gap-2 rounded-md px-2 py-1 md:py-0.5 text-left transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cc-primary/60 ${
isActive
? "bg-cc-active text-cc-fg"
: "text-cc-muted hover:text-cc-fg hover:bg-cc-hover"
}`}
>
<span
aria-hidden
className={`h-4 w-0.5 shrink-0 rounded-full transition-colors ${
isActive ? "bg-cc-primary" : "bg-transparent group-hover:bg-cc-border"
}`}
/>
<svg viewBox={item.viewBox} fill="currentColor" className="w-3.5 h-3.5 shrink-0">
<path d={item.iconPath} fillRule={item.fillRule} clipRule={item.clipRule} />
</svg>
<span className="min-w-0 flex-1 text-[12px] font-medium leading-tight">{item.label}</span>
</button>
);
})}
</div>
</section>
))}
{NAV_SECTIONS.map((section) => {
const isCollapsed = collapsedNavSections.has(section.id);
return (
<section key={section.id} className="rounded-lg border border-cc-border/30 bg-cc-card/20 p-0.5">
<button
onClick={() => toggleNavSection(section.id)}
className="w-full flex items-center gap-1 px-2 py-0.5 cursor-pointer hover:text-cc-fg transition-colors"
>
<svg viewBox="0 0 16 16" fill="currentColor" className={`w-2.5 h-2.5 text-cc-muted/60 transition-transform duration-150 ${isCollapsed ? "" : "rotate-90"}`}>
<path d="M6 4l4 4-4 4" />
</svg>
<h3 className="text-[9px] font-semibold uppercase tracking-[0.14em] text-cc-muted/75">
{section.label}
</h3>
</button>
{!isCollapsed && (
<div className="flex flex-col">
{section.itemIds.map((itemId) => {
const item = NAV_ITEMS_BY_ID.get(itemId);
if (!item) return null;
const isActive = item.activePages
? item.activePages.some((p) => route.page === p)
: route.page === item.id;
return (
<button
key={item.id}
onClick={() => {
if (item.id !== "terminal") {
useStore.getState().closeTerminal();
}
window.location.hash = item.hash;
// Close sidebar on mobile so the navigated page is visible
if (window.innerWidth < 768) {
useStore.getState().setSidebarOpen(false);
}
}}
title={item.label}
aria-current={isActive ? "page" : undefined}
className={`group flex min-h-[44px] md:min-h-[34px] w-full items-center gap-2 rounded-md px-2 py-1 md:py-0.5 text-left transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cc-primary/60 ${
isActive
? "bg-cc-active text-cc-fg"
: "text-cc-muted hover:text-cc-fg hover:bg-cc-hover"
}`}
>
<span
aria-hidden
className={`h-4 w-0.5 shrink-0 rounded-full transition-colors ${
isActive ? "bg-cc-primary" : "bg-transparent group-hover:bg-cc-border"
}`}
/>
<svg viewBox={item.viewBox} fill="currentColor" className="w-3.5 h-3.5 shrink-0">
<path d={item.iconPath} fillRule={item.fillRule} clipRule={item.clipRule} />
</svg>
<span className="min-w-0 flex-1 text-[12px] font-medium leading-tight">{item.label}</span>
</button>
);
})}
</div>
)}
</section>
);
})}
</nav>
<div className="mt-1.5 rounded-lg border border-cc-border/30 bg-cc-card/20 px-1.5 py-0.5">
<div className="flex items-center justify-between">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing tests for new collapsible behavior

The PR adds significant new behavior — section toggle on click, chevron rotation, and mobile-default-collapsed state derived from window.innerWidth — but Sidebar.test.tsx gains zero new test cases. The existing suite only asserts that the section labels are present (getByText("Workbench")); it does not cover:

  • Clicking a section header hides its child nav items (collapsed state)
  • Clicking again re-shows them (expanded state)
  • On a viewport width < 768 the sections initialize as collapsed

Per the project's frontend-testing rule, behavior changes in web/src/components/**/*.tsx require matching coverage in Sidebar.test.tsx. Please add tests for at least the toggle (click-to-collapse / click-to-expand) and the mobile-default path (mock window.innerWidth = 375 before render and assert items are not in the DOM).

Rule Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/Sidebar.tsx
Line: 663-730

Comment:
**Missing tests for new collapsible behavior**

The PR adds significant new behavior — section toggle on click, chevron rotation, and mobile-default-collapsed state derived from `window.innerWidth` — but `Sidebar.test.tsx` gains zero new test cases. The existing suite only asserts that the section labels are present (`getByText("Workbench")`); it does not cover:

- Clicking a section header hides its child nav items (collapsed state)
- Clicking again re-shows them (expanded state)
- On a viewport width < 768 the sections initialize as collapsed

Per the project's frontend-testing rule, behavior changes in `web/src/components/**/*.tsx` require matching coverage in `Sidebar.test.tsx`. Please add tests for at least the toggle (click-to-collapse / click-to-expand) and the mobile-default path (mock `window.innerWidth = 375` before render and assert items are not in the DOM).

**Rule Used:** CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=666e8656-df2f-4480-b76a-67f089c8461a))

How can I resolve this? If you propose a fix, please make it concise.

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.

Sidebar Workbench/Workspace sections should be collapsible on mobile

1 participant