Skip to content

feat(sessions): Batch 1 PR-2 — pinned zone + manual hide#136

Open
grimmerk wants to merge 12 commits into
mainfrom
feat/session-pins
Open

feat(sessions): Batch 1 PR-2 — pinned zone + manual hide#136
grimmerk wants to merge 12 commits into
mainfrom
feat/session-pins

Conversation

@grimmerk

@grimmerk grimmerk commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Session-finding Batch 1 PR-2 — pins & manual hide (plan: docs/session-finding-plan.md §4.4). Follows PR #132; design confirmed with the user (pinned-section model chosen over a separate favorites view — zero mode-switch for the highest-frequency path).

📌 Pins

  • Pinned zone at the top of the Sessions list: collapsible header (▾ 📌 Pinned (N)), collapsed state remembered (localStorage). Rows fully reuse the existing session row — status dots, badges, PR links, snippets all work.
  • Dual placement: a pinned session also keeps its chronological spot in the timeline, marked with a gold ★ — the zone is a shortcut, not a move.
  • Pin/unpin: hover 📌 icon on the selected row, or ⌘D.
  • Works for any displayed row, including deep-search matches beyond the loaded ~100: the zone resolves those via the new get-sessions-by-ids IPC (reads the full session cache) and lazily enriches them.
  • While searching, the zone steps aside — results are one unified list (★ still marks pinned matches).

⊘ Manual hide (C1's missing piece)

  • Hover icon or ⇧⌘D: forces the session into the minor-sessions fold regardless of its stats. Reversible from inside the expanded fold (the icon shows as active); hidden sessions remain searchable (search always shows everything).
  • Pin and hide are mutually exclusive: pinning unhides, hiding unpins (encoded in the pure transition helpers).

Store

~/.config/codev/session-marks.json — single cross-account file next to the accounts registry (JSON on purpose: tiny data, human-debuggable, no SQLite features needed):

{ "version": 1,
  "pins": { "<sessionId>": { "pinnedAt": "", "cwd": "", "accountLabel": "", "group": null } },
  "hidden": ["<sessionId>"] }
  • fs.watch (parent-dir watch, filename-filtered — the atomic temp+rename write replaces the inode, which would detach a file watcher) → debounced push to the renderer, same pattern as the status files.
  • IPC responses also return the updated marks for instant UI feedback (watch push is the backup path).
  • sessionIds are resume-stable (verified on Claude Code 2.1.207: --resume/--continue reuse the id; only --fork-session forks), so plain sessionId keying needs no migration logic. group field reserved for v2 named groups.

Tests

  • 7 new unit tests (session-marks.test.ts): normalize (garbage/defaults/dedupe), pin↔hide mutual exclusion, idempotent removals, input immutability, tmpdir file roundtrip incl. corrupt-file recovery and no temp leftovers — 52 total pass; tsc --noEmit clean.

Out of scope

🤖 On behalf of @grimmerk — generated with Claude Code


Summary by cubic

Adds session pins and manual hide with a collapsible 📌 Pinned zone. Pinned sessions live only in the zone while browsing, remain searchable, and marks sync across accounts via ~/.config/codev/session-marks.json.

  • New Features

    • Collapsible 📌 Pinned zone; collapse state remembered. New pins append to the bottom.
    • Pin/unpin via hover 📌 or Cmd+D. Hide/unhide via Shift+Cmd+D; pin and hide are mutually exclusive.
    • While searching, the zone hides; results are one list with a ★ on pinned matches.
    • Deep-search pins resolve via getSessionsByIds + lazy enrichment; unresolved pins render placeholders with '… msgs' so Pinned(N) stays stable.
    • Zone rows include Hide (unpin + fold). Hidden rows show a dim ⊘; the minor fold label adds “N hidden.”
  • Bug Fixes

    • Marks watcher closes on error, self-heals with bounded backoff, and logs retries; extracted path-based watchMarksFile; new test covers the watcher error path (53 tests total).
    • webContents.send guarded with isDestroyed(); marks listener returns an unsubscribe and cleans up; all marks IPC calls .catch errors; audit logs added for pin/unpin/hide.
    • Stale getSessionsByIds responses are dropped; collapsing the pinned zone snaps selection to the top.
    • Suppress hover re-selection during pin/hide layout shifts; selection re-anchors to the same session; Cmd+D requires an explicit selection; keyboard shortcuts keep working after clicking icons/folds.

Written for commit a7537ca. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a collapsible 📌 Pinned sessions zone with per-session ordering (★), remembered collapse state, and a minor-sessions hide fold.
    • Pin/hide uses keyboard shortcuts and row hover controls, enforces pin/hide mutual exclusivity, and keeps pinned results included during search (with placeholders for unresolved pins).
    • Persisted pin/hide state in ~/.config/codev/session-marks.json, shared across accounts, with live sync when the file changes.
  • Documentation
    • Updated the session switching documentation with the new pin/hide workflows and shortcuts.
  • Tests
    • Added unit coverage for marks normalization, pin/hide transitions, file roundtrips, and watcher error recovery.

grimmerk and others added 2 commits July 13, 2026 23:19
- Pinned zone at the top (collapsible, remembered); pin via hover
  icon or Cmd+D; pinned rows also keep their chronological spot with
  a gold star marker
- Works for deep-search matches beyond the loaded 100 (fetched by id
  via new get-sessions-by-ids IPC + lazy enrichment)
- Hide (hover icon or Shift+Cmd+D) forces a session into the minor
  fold; reversible from inside the fold; pin/hide are mutually
  exclusive (pinning unhides, hiding unpins)
- Store: ~/.config/codev/session-marks.json, fs.watch push to the
  renderer, atomic temp+rename writes; pure transitions unit-tested
  (7 new tests, 52 total)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e02c74b-caec-4c10-876d-e2bb0977a3c9

📥 Commits

Reviewing files that changed from the base of the PR and between 7e1b36c and a7537ca.

📒 Files selected for processing (6)
  • README.md
  • src/main.ts
  • src/session-marks.test.ts
  • src/session-marks.ts
  • src/session-marks.watch.test.ts
  • src/switcher-ui.tsx
 __________________________________________
< Dollars to donuts, you didn't test this. >
 ------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds persisted session pin/hide marks, Electron IPC and preload APIs, bulk session lookup, and a sessions switcher UI with pinned and minor-session zones, keyboard controls, file watching, tests, and release documentation.

Changes

Session marks

Layer / File(s) Summary
Marks storage and transitions
src/session-marks.ts, src/session-marks.test.ts
Adds normalized pin/hidden state, mutually exclusive transition helpers, atomic JSON persistence at ~/.config/codev/session-marks.json, filesystem watching, and unit tests.
Marks IPC bridge
src/claude-session-utility.ts, src/electron-api.d.ts, src/main.ts, src/preload.ts
Adds bulk session lookup and IPC/preload methods for reading, mutating, and subscribing to session marks.
Pinned sessions UI and release metadata
src/switcher-ui.tsx, README.md, CHANGELOG.md, docs/session-finding-plan.md, package.json
Adds pinned-session rendering, hidden-session folding, enrichment of unloaded pins, selection reanchoring, keyboard and row controls, documentation, status updates, and version 1.0.84.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SessionsView
  participant MainProcess
  participant MarksFile
  User->>SessionsView: pin or hide selected session
  SessionsView->>MainProcess: invoke session mark action
  MainProcess->>MarksFile: read, transform, and write marks
  MarksFile-->>MainProcess: filesystem update
  MainProcess-->>SessionsView: session-marks-updated
  SessionsView->>MainProcess: fetch missing pinned sessions by ID
  MainProcess-->>SessionsView: ClaudeSession records
  SessionsView-->>User: render pinned and minor-session zones
Loading

Possibly related PRs

  • grimmerk/codev#132: Modifies the sessions list and search behavior that this change extends with pinned and hidden sessions.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: session pinned zone and manual hide support.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/session-pins

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/switcher-ui.tsx">

<violation number="1" location="src/switcher-ui.tsx:584">
P3: Same unhandled-rejection concern: `window.electronAPI.pinSession(...).then(applyMarksResult)` is missing a `.catch()` handler, as is `toggleHide`'s calls to `unhideSession` and `hideSession`. Adding a `.catch(() => {})` on all four IPC call sites would suppress the unhandled rejection if the main process encounters an error.</violation>

<violation number="2" location="src/switcher-ui.tsx:610">
P2: The `useEffect` that subscribes to `onSessionMarksUpdated` never cleans up the listener on unmount. If the component remounts (StrictMode in dev, hot reload, or any React reconciliation that tears down and rebuilds this tree), a new subscription is added each time without removing the old one, resulting in duplicate callbacks and wasted state updates. Consider storing the unsubscription function returned by `ipcRenderer.on`/`ipcRenderer.removeListener` and calling it in the effect's cleanup function.</violation>

<violation number="3" location="src/switcher-ui.tsx:628">
P2: Rapidly pinning multiple sessions that are outside the loaded list can leave some of them missing from the pinned zone. An earlier `getSessionsByIds` response can overwrite the newer response, and the key ref then suppresses the retry. Consider discarding a response unless its request key still matches `extraPinnedKeyRef.current`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/switcher-ui.tsx Outdated
Comment thread src/switcher-ui.tsx
Comment thread src/switcher-ui.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/main.ts Outdated
Comment thread src/switcher-ui.tsx
Comment thread src/switcher-ui.tsx
- Marks listener returns an unsubscribe; effect cleans up on unmount
- Stale getSessionsByIds responses discarded (key check) so rapid
  pins outside the loaded list can't overwrite newer results
- .catch on all marks IPC call sites (no unhandled rejections)
- isDestroyed() guard before webContents.send in the marks watcher
- Pinned-zone collapse resets selectedSessionIndex (same as minors)
- Fold label stays honest when it contains manually hidden sessions
  (adds 'N hidden' to the descriptor)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@grimmerk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Pinning inserts a zone row above the selection, shifting every index
while selectedSessionIndex stayed put — the next Cmd+D then acted on
an unintended row (user-reported: phantom pins, toggles that seemed
to do nothing). After any pin/hide toggle the selection re-anchors to
the same session's timeline copy by sessionId.

Hidden rows now carry a persistent dim no-entry marker (was only
discoverable by hovering inside the fold).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@grimmerk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/switcher-ui.tsx (3)

617-628: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Move the localStorage.setItem side effect out of the state updater.

setPinnedCollapsed((prev) => {...localStorage.setItem(...)...}) performs a side effect inside a functional updater. React may invoke updaters more than once, so this write can fire redundantly.

♻️ Proposed refactor
   const togglePinnedCollapsed = () => {
-    setPinnedCollapsed((prev) => {
-      const next = !prev;
-      try {
-        localStorage.setItem('codev-pinned-collapsed', next ? '1' : '0');
-      } catch {}
-      return next;
-    });
+    setPinnedCollapsed((prev) => {
+      const next = !prev;
+      return next;
+    });
+    try {
+      localStorage.setItem('codev-pinned-collapsed', pinnedCollapsed ? '0' : '1');
+    } catch {}
     setSelectedSessionIndex(0);
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` around lines 617 - 628, Move the localStorage
persistence out of the functional updater in togglePinnedCollapsed. Compute the
next pinned-collapsed value from current state through the existing state
transition, then persist it separately while preserving the existing key, value
format, error handling, and selection reset behavior.

Source: Linters/SAST tools


589-616: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer async/await for togglePin/toggleHide, matching the file's own convention.

These use .then()/.catch() chains, whereas the equivalent one-shot IPC handlers already in this file (openAccountPickerForLaunch, lines 333-353) use async/await with try/catch. As per coding guidelines, src/**/*.{ts,tsx} should "Use async/await for asynchronous operations."

♻️ Proposed refactor
-  const togglePin = (session: any) => {
+  const togglePin = async (session: any) => {
     reanchorSelectionRef.current = session.sessionId;
-    if (sessionMarks.pins[session.sessionId]) {
-      window.electronAPI
-        .unpinSession(session.sessionId)
-        .then(applyMarksResult)
-        .catch(() => {});
-    } else {
-      window.electronAPI
-        .pinSession(session.sessionId, { cwd: session.project, accountLabel: session.accountLabel })
-        .then(applyMarksResult)
-        .catch(() => {});
-    }
+    try {
+      const r = sessionMarks.pins[session.sessionId]
+        ? await window.electronAPI.unpinSession(session.sessionId)
+        : await window.electronAPI.pinSession(session.sessionId, {
+            cwd: session.project,
+            accountLabel: session.accountLabel,
+          });
+      applyMarksResult(r);
+    } catch {}
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` around lines 589 - 616, Refactor togglePin and
toggleHide to async functions using await for the pin/unpin and hide/unhide IPC
calls, replacing their then/catch chains with try/catch handling while
preserving reanchorSelectionRef assignment and applyMarksResult invocation.

Source: Coding guidelines


671-705: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Run the project formatter over the new code — dozens of prettier/ESLint errors across all four files. Static analysis reports consistent "prettier/prettier" spacing/wrapping/trailing-comma errors throughout the new session-marks code; the root cause is the same everywhere (new code wasn't formatted), so a single prettier --write (or eslint --fix) pass resolves all of them.

  • src/switcher-ui.tsx#L671-L705: reformat this effect plus the other flagged ranges in the same file (408, 542, 577, 586, 598, 1561-1729, 1897).
  • src/electron-api.d.ts#L131-L149: reformat the new IElectronAPI session-marks type block (lines 136, 144-146).
  • src/main.ts#L2342-L2404: reformat the new IPC handler block, especially pin-session (lines 2365-2376).
  • src/preload.ts#L79-L91: reformat the new electronAPI bridge methods (lines 81-85, 90).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` around lines 671 - 705, Run the project formatter or
ESLint autofix over the new session-marks code. Reformat the effect around the
session-loading useEffect in src/switcher-ui.tsx and the other flagged ranges
there (408, 542, 577, 586, 598, 1561-1729, 1897); reformat the IElectronAPI
session-marks block in src/electron-api.d.ts (131-149), the IPC handlers in
src/main.ts (2342-2404), and the electronAPI bridge methods in src/preload.ts
(79-91).

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/session-marks.ts`:
- Around line 163-184: Add an error listener to the watcher created in
watchSessionMarks so fs.watch failures are handled without an unhandled
exception; preserve the existing change callback, debounce behavior, and cleanup
returned by the function.
- Around line 50-51: Reformat the pinnedAt expression in the session-mark
mapping and the corresponding block around the second reported location using
the project’s Prettier configuration, keeping behavior unchanged and ensuring
both sections comply with the 80-column limit.

In `@src/switcher-ui.tsx`:
- Around line 1561-1569: Update the ⇧⌘D branch in the keyboard handler around
selectedSessionIndex so toggleHide(s) is only called when the selected session
is not a __pinnedRow, matching the mouse hide-control restriction; preserve
togglePin(s) behavior for non-shift ⌘D.

---

Nitpick comments:
In `@src/switcher-ui.tsx`:
- Around line 617-628: Move the localStorage persistence out of the functional
updater in togglePinnedCollapsed. Compute the next pinned-collapsed value from
current state through the existing state transition, then persist it separately
while preserving the existing key, value format, error handling, and selection
reset behavior.
- Around line 589-616: Refactor togglePin and toggleHide to async functions
using await for the pin/unpin and hide/unhide IPC calls, replacing their
then/catch chains with try/catch handling while preserving reanchorSelectionRef
assignment and applyMarksResult invocation.
- Around line 671-705: Run the project formatter or ESLint autofix over the new
session-marks code. Reformat the effect around the session-loading useEffect in
src/switcher-ui.tsx and the other flagged ranges there (408, 542, 577, 586, 598,
1561-1729, 1897); reformat the IElectronAPI session-marks block in
src/electron-api.d.ts (131-149), the IPC handlers in src/main.ts (2342-2404),
and the electronAPI bridge methods in src/preload.ts (79-91).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 81090522-cb66-451c-9b85-8b8637cf23cb

📥 Commits

Reviewing files that changed from the base of the PR and between 821ae86 and 2726c84.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • README.md
  • docs/session-finding-plan.md
  • package.json
  • src/claude-session-utility.ts
  • src/electron-api.d.ts
  • src/main.ts
  • src/preload.ts
  • src/session-marks.test.ts
  • src/session-marks.ts
  • src/switcher-ui.tsx

Comment thread src/session-marks.ts Outdated
Comment thread src/session-marks.ts Outdated
Comment thread src/switcher-ui.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/switcher-ui.tsx Outdated
- fs.watch gets an error handler (dir gone / watcher limits would
  otherwise crash the main process)
- Re-anchor ref now armed together with the marks state update, so
  an unrelated render can't consume it before the list reshuffles
- Shift+Cmd+D respects the pinned-zone restriction like the mouse UI
- session-marks.ts prettier'd (new file)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/switcher-ui.tsx (1)

674-707: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

getSessionsByIds failure permanently blocks retry for the same pin set.

extraPinnedKeyRef.current is set to key before the fetch (line 679), so if the IPC call rejects, the .catch(() => {}) on line 706 swallows the error and the guard on line 678 prevents any retry until the missing-ID set changes. A transient main-process error would leave those pinned sessions absent from the zone with no recovery path.

Consider resetting extraPinnedKeyRef.current in the catch handler so the next render retries:

🛡️ Proposed fix
     window.electronAPI.getSessionsByIds(missing).then((result: any[]) => {
       // Drop stale responses (a newer pin set superseded this request)
       if (extraPinnedKeyRef.current !== key) return;
       const found = result || [];
       setExtraPinnedSessions(found);
       if (found.length === 0) return;
       window.electronAPI.loadSessionEnrichment(found).then((enrichment) => {
         if (enrichment.titles && Object.keys(enrichment.titles).length > 0) {
           setCustomTitles((prev: Record<string, string>) => ({ ...prev, ...enrichment.titles }));
         }
         if (enrichment.branches && Object.keys(enrichment.branches).length > 0) {
           setBranches((prev: Record<string, string>) => ({ ...prev, ...enrichment.branches }));
         }
         if (enrichment.prLinks && Object.keys(enrichment.prLinks).length > 0) {
           setPrLinks((prev) => ({ ...prev, ...enrichment.prLinks }));
         }
       });
       window.electronAPI.loadLastAssistantResponses(found).then((responses: Record<string, string>) => {
         if (responses && Object.keys(responses).length > 0) {
           setAssistantResponses((prev: Record<string, string>) => ({ ...prev, ...responses }));
         }
       });
-    }).catch(() => {});
+    }).catch(() => {
+      // Allow retry on next render — a transient IPC failure shouldn't
+      // permanently block pinned-session enrichment.
+      extraPinnedKeyRef.current = '';
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` around lines 674 - 707, Reset extraPinnedKeyRef.current
in the getSessionsByIds promise catch handler so a rejected fetch does not
permanently suppress retries for the same missing-ID set. Keep the existing
stale-response guard and successful loading behavior unchanged, while allowing
the next render of this useEffect to retry the request.
🧹 Nitpick comments (1)
src/switcher-ui.tsx (1)

590-590: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prettier formatting violations flagged by static analysis.

Lines 590, 601, and 1569–1574 have prettier/prettier errors. Line 601 also violates the coding guideline for trailing commas — the object { cwd: session.project, accountLabel: session.accountLabel } should have a trailing comma and be multi-lined per prettier.

Run npx prettier --write src/switcher-ui.tsx to auto-fix.

As per coding guidelines, src/**/*.{ts,tsx} files should use trailing commas in arrays/objects.

Also applies to: 601-601, 1569-1574

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` at line 590, Run Prettier on src/switcher-ui.tsx to
correct formatting violations at setSessionMarks and the other flagged sections,
including multiline formatting and trailing commas for the object near
session.project and session.accountLabel. Preserve the existing behavior.

Sources: Coding guidelines, Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/switcher-ui.tsx`:
- Around line 674-707: Reset extraPinnedKeyRef.current in the getSessionsByIds
promise catch handler so a rejected fetch does not permanently suppress retries
for the same missing-ID set. Keep the existing stale-response guard and
successful loading behavior unchanged, while allowing the next render of this
useEffect to retry the request.

---

Nitpick comments:
In `@src/switcher-ui.tsx`:
- Line 590: Run Prettier on src/switcher-ui.tsx to correct formatting violations
at setSessionMarks and the other flagged sections, including multiline
formatting and trailing commas for the object near session.project and
session.accountLabel. Preserve the existing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c13075b9-947d-48f2-8cad-00206652f70e

📥 Commits

Reviewing files that changed from the base of the PR and between 2726c84 and fcdbe90.

📒 Files selected for processing (2)
  • src/session-marks.ts
  • src/switcher-ui.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/session-marks.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/session-marks.ts Outdated
Two more root causes from live testing:

- Clicking any pin/hide icon or fold control moved focus off the
  search input, silently killing every Cmd+D until a tab switch
  refocused it ('shortcut randomly stops working'). All non-input
  controls now preventDefault on mousedown so focus never leaves.
- Pinned(N) counted only pins that resolved to a loaded session
  object; a pinned VS Code session (absent from history.jsonl until
  the closed-scan merges) flapped the count on every tab switch.
  Unresolvable pins now render a placeholder row built from the pin
  record, so the zone and its count stay stable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/switcher-ui.tsx Outdated
After a tab switch selection resets to -1 and Cmd+D silently fell
back to row 0 — the first pinned-zone row — so a bare Cmd+D toggled
the most recently pinned session (user-reported: Pinned(1)<->(2)
oscillation, accidental pin of the top row). Enter keeps its
open-top-result default; the mutating shortcut now no-ops without a
hovered/arrowed selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/switcher-ui.tsx (3)

1579-1595: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run Prettier on the keyboard shortcut branch.

ESLint reports prettier/prettier errors on Lines [1580]-[1587]; this should be formatted before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` around lines 1579 - 1595, Run Prettier on the keyboard
shortcut branch around the ⌘D handler in the event-handling logic, applying the
repository’s standard formatting to the affected conditional and nested
statements without changing its behavior.

Source: Linters/SAST tools


1621-1636: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Render the pinned zone when the main session list is empty.

When sessions.length === 0, the surrounding conditional skips this block—even if showPinnedZone is true and extraPinnedSessions supplied unresolved pinned rows. Users then cannot see or expand their pinned sessions.

-            {sessions.length === 0 ? (
+            {sessions.length === 0 && !showPinnedZone ? (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` around lines 1621 - 1636, Update the surrounding render
condition for the pinned-zone block containing togglePinnedCollapsed so it also
renders when sessions.length === 0 and showPinnedZone is true. Preserve the
existing behavior for non-empty session lists and ensure unresolved
extraPinnedSessions remain visible and expandable through pinnedRows.

1738-1759: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make pin/hide controls keyboard-accessible.

These clickable <span> elements are not focusable and have no button semantics or accessible labels, so keyboard and assistive-technology users cannot operate the new mouse controls. Use <button type="button"> with aria-label instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` around lines 1738 - 1759, Replace the pin and hide
clickable spans in the selected-session controls with type="button" buttons,
preserving their click and mouse-down behavior and visual styling. Add
descriptive aria-label values that reflect each control’s current action, such
as pin/unpin and hide/unhide, while keeping the existing conditional rendering
and handlers unchanged.
🧹 Nitpick comments (2)
src/switcher-ui.tsx (2)

418-420: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Use strict session types for the new pinning state.

extraPinnedSessions and the pinned-row pipeline are typed with any, which weakens the new by-ID/placeholder contract. Define a shared Session type and use it for fetched sessions, placeholders, and derived pinned rows.

As per coding guidelines, src/**/*.{ts,tsx} requires strict typing.

Also applies to: 532-590

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` around lines 418 - 420, Replace the any-based typing for
extraPinnedSessions and the pinned-row pipeline in the relevant switcher UI
logic with a shared strict Session type. Use Session consistently for
fetched-by-ID sessions, placeholder entries, state, and derived pinned rows,
preserving the existing pinning behavior and avoiding any in these paths.

Source: Coding guidelines


609-634: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use async/await for the new IPC flows.

The pin/hide helpers and marks-loading/enrichment effects use Promise chains. Convert these new asynchronous paths to async functions with try/catch for consistency and clearer error handling.

As per coding guidelines, asynchronous operations should use async/await.

Also applies to: 673-723

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/switcher-ui.tsx` around lines 609 - 634, Convert the asynchronous flows
in togglePin, toggleHide, and the marks-loading/enrichment effects around the
referenced section from Promise .then/.catch chains to async/await. Mark the
relevant helpers and effect callbacks async where appropriate, await each IPC
operation, and wrap the operations in try/catch while preserving the existing
applyMarksResult behavior and error-swallowing semantics.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/switcher-ui.tsx`:
- Around line 1579-1595: Run Prettier on the keyboard shortcut branch around the
⌘D handler in the event-handling logic, applying the repository’s standard
formatting to the affected conditional and nested statements without changing
its behavior.
- Around line 1621-1636: Update the surrounding render condition for the
pinned-zone block containing togglePinnedCollapsed so it also renders when
sessions.length === 0 and showPinnedZone is true. Preserve the existing behavior
for non-empty session lists and ensure unresolved extraPinnedSessions remain
visible and expandable through pinnedRows.
- Around line 1738-1759: Replace the pin and hide clickable spans in the
selected-session controls with type="button" buttons, preserving their click and
mouse-down behavior and visual styling. Add descriptive aria-label values that
reflect each control’s current action, such as pin/unpin and hide/unhide, while
keeping the existing conditional rendering and handlers unchanged.

---

Nitpick comments:
In `@src/switcher-ui.tsx`:
- Around line 418-420: Replace the any-based typing for extraPinnedSessions and
the pinned-row pipeline in the relevant switcher UI logic with a shared strict
Session type. Use Session consistently for fetched-by-ID sessions, placeholder
entries, state, and derived pinned rows, preserving the existing pinning
behavior and avoiding any in these paths.
- Around line 609-634: Convert the asynchronous flows in togglePin, toggleHide,
and the marks-loading/enrichment effects around the referenced section from
Promise .then/.catch chains to async/await. Mark the relevant helpers and effect
callbacks async where appropriate, await each IPC operation, and wrap the
operations in try/catch while preserving the existing applyMarksResult behavior
and error-swallowing semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c68e467e-495d-45d7-a2ab-aafbd977ff4a

📥 Commits

Reviewing files that changed from the base of the PR and between 8329a98 and 7e1b36c.

📒 Files selected for processing (1)
  • src/switcher-ui.tsx

grimmerk and others added 2 commits July 14, 2026 14:59
Root cause of three rounds of pin chaos (wrong sessions getting
pinned, counts jumping 1->2->3, a never-touched session surviving
restart): pin/hide moves rows under a STATIONARY cursor; Chromium
re-hit-tests after layout changes and fires mouseenter on whatever
row slid under the mouse, teleporting the selection and overriding
the re-anchor — so the next Cmd+D acted on an unrelated session.
The window-show code already suppresses exactly this phenomenon
with ignoreMouseEnterRef; now every marks-driven layout change gets
the same 400ms suppression (token-guarded), and the pinned zone
appends new pins at the bottom (pinnedAt asc) instead of
reshuffling existing rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The timeline duplicate of a pinned session read as noise, not
signal — pinning now MOVES the row into the zone (search still
shows everything). The zone rows gain the hide control (unpin +
fold in one step, per the pin/hide exclusivity) since the timeline
path is gone, and Shift+Cmd+D allows the same. Marks IPC handlers
log an audit line for the reported cross-restart pin-loss (not yet
reproduced with evidence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/switcher-ui.tsx
grimmerk and others added 2 commits July 14, 2026 23:52
README documents that shortcuts act on the selected (blue-border)
row and require explicit selection, Cmd+D vs Shift+Cmd+D, and that
the zone vanishing at zero pins is not a collapse; the zone header
tooltip carries the same hints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Dead marks watcher self-heals: on error it closes and the owner
  recreates with bounded backoff (was: swallowed error + stale
  cleanup ref meant deaf-until-restart)
- Placeholder pinned rows show '… msgs' instead of a misleading 0
- Zone comment updated to the zone-only model

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/main.ts Outdated
Comment thread src/main.ts
Comment thread src/session-marks.ts Outdated
Comment thread src/session-marks.ts Outdated
- watchMarksFile extracted (path-based, matching the module pattern);
  onError now receives the Error
- Retry timer tracked and cleared; watcher-death and retry-exhausted
  paths log to console
- New test drives the fs.watch error path via a mocked fs (dead
  watcher closed, error reported, no further onChange) — 53 total

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@grimmerk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Action performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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