Skip to content

Subagent support: index, search, and inline-expand transcripts#47

Open
JeanBaptisteRenard wants to merge 10 commits into
doctly:mainfrom
JeanBaptisteRenard:feat/subagent-support
Open

Subagent support: index, search, and inline-expand transcripts#47
JeanBaptisteRenard wants to merge 10 commits into
doctly:mainfrom
JeanBaptisteRenard:feat/subagent-support

Conversation

@JeanBaptisteRenard

Copy link
Copy Markdown

Summary

Switchboard already partially recognized subagent layout (in derive-project-path.js) but never indexed the transcripts, never exposed them in the JSONL viewer, and excluded them from full-text search. This PR makes subagents first-class:

  • Indexed in session_cache with parentSessionId, agentId, subagentType, description columns (migration v4, clears cache for re-population)
  • Searchable — subagent rows flow through the same FTS pipeline as top-level sessions, so the search bar now finds anything an Agent call discussed
  • Inline-viewable — clicking an Agent tool_use block in the JSONL viewer loads the matching .jsonl and renders it nested below, with a caret indicator. Identical (description, subagentType) blocks (parallel fanout) are disambiguated by occurrence ordinal.

On-disk layout discovered: <project-folder>/<parentSessionId>/subagents/agent-<agentId>.jsonl plus a sidecar .meta.json carrying { agentType, description }. The sidecar is the correlation key with the parent's tool_use.input — no need to re-parse the parent.

What's not in this PR (deliberate scope split)

A follow-up PR will add the observability layer: hierarchical sidebar rendering, live subagent-spawned/completed detection in session-transitions.js, per-subagent status badges in the grid view, and a read-only live-tail viewer for in-flight subagents. Keeping that in a separate PR so this one can be reviewed and merged on its core merit (data model + viewer).

Commits

  • feat(db): subagent columns + migration v4
  • feat(read-session-file): layout helpers (enumerateSessionFiles, subagentSessionId, resolveJsonlPath, readSubagentMeta) + 10 unit tests
  • feat(scan): both scanners (session-cache.js + workers/scan-projects.js) use enumerateSessionFiles
  • feat(viewer): clickable Agent block, recursive nested render, two new IPCs (read-subagent-jsonl, list-subagents)

Test plan

  • npm test — 10/10 pass on test/read-session-file.test.js (top-level + subagent branches, sidechain guard, legacy <uuid>/*.jsonl fallback, missing-sidecar tolerance, agentId fallback to filename)
  • Smoke test on a real ~/.claude/projects/<...> folder containing 8 top-level + 339 subagent transcripts: enumerateSessionFiles returns all 347, readSessionFile correctly hydrates the sidecar metadata when present
  • Manual UI check: re-index, open a session that called Agent, click the block, confirm inline expansion + collapse
  • Search bar: type a phrase you know was only mentioned by a subagent — confirm it appears in results

Subagent transcripts written by Claude CLI live at
<folder>/<parentSessionId>/subagents/agent-<agentId>.jsonl alongside a
.meta.json sidecar holding { agentType, description }. Surface them as
first-class rows in session_cache, keyed by sessionId 'sub:<parent>:<agentId>'.

- adds parentSessionId, agentId, subagentType, description columns
- migration v4 clears the cache so a re-index repopulates everything
- adds idx_session_cache_parent for hierarchy lookups
- new query getCachedByParent + widened cacheGetByFolder
Adds helpers for the on-disk subagent layout:
- enumerateSessionFiles(folderPath) walks top-level + <uuid>/subagents/*.jsonl
  + legacy <uuid>/*.jsonl fallback
- subagentSessionId(parent, agentId) gives the synthetic 'sub:<p>:<a>' id
- resolveJsonlPath(projectsDir, row) reconstructs the absolute path
- readSubagentMeta reads the .meta.json sidecar

readSessionFile now accepts opts.parentSessionId. When set, it requires
isSidechain on at least one entry (defensive guard), reads the sidecar for
agentType/description, falls back to filename for agentId if absent in entries.

Adds 10 unit tests covering both branches, the sidecar guard, the legacy
fallback path, and the synthetic-id format.
Both scanners (synchronous refreshFolder + worker scan-projects) now use
enumerateSessionFiles instead of a flat top-level readdir, so subagent
transcripts get cached as first-class rows with parentSessionId set.

FTS indexing inherits this for free: subagent summaries land in search_fts
the same way top-level sessions do, with type='session' and folder=<parent>.
Full-text search now finds anything an Agent call discussed.
Clicking an Agent tool_use block in the JSONL viewer now loads the matching
subagent transcript and renders it nested below the block. Re-click collapses
without refetching.

- main.js: new IPC read-subagent-jsonl(parentSessionId, agentId) and
  list-subagents(parentSessionId)
- preload.js: window.api.readSubagentJsonl / listSubagents
- jsonl-viewer.js: clickable Agent block, caret indicator, recursive nested
  render. Identical (description, subagentType) pairs are disambiguated by
  occurrence ordinal so parallel fanout calls each open their own transcript.
- style.css: subtle hover + left-border indent for nested transcripts
Runs `npm ci && npm test` on ubuntu-latest against Node 20 and 22.
Triggers on pull requests targeting main and on direct pushes to main.
Migrations that clear session_cache (v2, v3, v4) leave the cache empty on
first launch. The previous get-projects handler returned [] immediately and
fire-and-forgot the worker scan, relying on a later 'projects-changed' IPC
to trigger a reload — but app.js only reloads on that event when the user
happens to be on the Sessions tab. If they were on another tab (or
hadn't navigated to Sessions yet), the list stayed empty until they
manually switched tabs.

Make populateCacheViaWorker return a Promise that resolves when the scan
finishes. Concurrent callers share the same Promise. get-projects awaits
that Promise when the cache is empty, so the response carries the freshly
populated cache and the renderer paints immediately.
buildProjectsFromCache was projecting only the pre-PR#1 columns onto the
session object sent to the renderer. The renderer's hierarchical sidebar
(introduced in PR#2) reads parentSessionId/agentId/subagentType/description
from each session and silently flattens to top-level when they're missing —
which is what happened: all subagents appeared as siblings of their parent
instead of nested under it.

Add the four subagent columns to the projection. No DB or wire change.
readSessionFile JSON.parse'd every line of a session file in a single
outer try/catch. If a Claude CLI session was actively writing the file
while the worker scan read it, one mid-write line could throw and
invalidate the ENTIRE file's session row. With many parallel live sessions
this manifested as 'most projects show no sessions after a fresh index'.

- Per-line try/catch inside readSessionFile: skip malformed lines, keep
  parsing the rest.
- Per-file try/catch in workers/scan-projects.js: defensive belt and
  braces so one unparseable file can't abort an entire folder scan.
resolveWorktreePath was defined and exported in the file but never
actually called from deriveProjectPath. As a result every worktree under
<project>/.claude/worktrees/<name>/ appeared as a separate project group
in the sidebar instead of being grouped under its parent project.

Wire the call in both branches of deriveProjectPath (direct .jsonl path
and subdirectory path) and export resolveWorktreePath for other callers.
@abasiri

abasiri commented May 28, 2026

Copy link
Copy Markdown
Contributor

Great work @JeanBaptisteRenard , give me a bit to get through these, been a busy week. Sorry about the slowness.

@JeanBaptisteRenard

JeanBaptisteRenard commented May 28, 2026 via email

Copy link
Copy Markdown
Author

…psing at index time

46d596c wired resolveWorktreePath into deriveProjectPath, collapsing every
worktree cwd to its parent repo at index time. That fixed sidebar
fragmentation for the .worktrees/ and .claude-worktrees/ layouts, but it
erased the worktree projectPath the sidebar matches to build nested
worktree groups — killing the existing nesting/hide/new-session UI for
.claude/worktrees/ and leaving no anchor for worktree-scoped actions.

Fix the fragmentation where it lives instead:

- deriveProjectPath returns the raw cwd again (index-time collapse
  reverted); resolveWorktreePath stays exported for callers that need the
  parent <-> worktree relationship
- sidebar worktreePattern now matches all three layouts, so .worktrees/
  and .claude-worktrees/ nest under their parent just like
  .claude/worktrees/ already did
- worktrees whose parent project is absent from the payload stay
  top-level instead of vanishing (nested groups only render inside their
  parent's group)

Co-Authored-By: Claude Fable 5 <[email protected]>
@abasiri

abasiri commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Hey, I finally got a chance to go through this and test it. Thanks for putting these prs together. I feel like adding all the subagents to the side bar just causes rendering/memory issues. If the intention is purely indexing, like it would be better to index under the parent so that a search shows the parent rather than the subagent in the sidebar.

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.

2 participants