Repository navigation
feat: open an existing Claude Code session as a thread - #30
Conversation
A Claude Code session started in a terminal could only be imported from the first-run welcome wizard, and the imported thread failed on its first follow-up because the adapter opened the session with sessionId instead of resume. Adds agentSessions.list for one project's recent Claude sessions, a sessionIds filter and returned threadIds on agentSessions.import, a picker reachable from the sidebar header and the command palette, and the adapter fix from upstream pingdotgg#23219 so an imported session resumes on its first T3 turn. Closes #29
There was a problem hiding this comment.
🟡 Changes recommended
The picker can expose prompt-derived titles without filesystem permission, exhaust server memory, and trigger unintended bulk imports on older hosts; session ownership and selection also remain unreliable.
8 open issues
Expose terminal titles without filesystem permission · New Retain full scanner outcomes and exhaust server memory · New Send selected imports to bulk-import RPC on older hosts · New Apply provider filtering before the scanner budget · New Miss ownership across Claude instances sharing a home · New Offer Open for archived or deleted owning threads · New Match selected imports by instance and session ID · New Use session titles in accessible names for Open buttons · New
What changed in this PR
Adds an opt-in way to continue terminal-started Claude Code sessions in T3 Code after onboarding, with per-session selection and first-turn resume support.
Changes:
- Adds session listing, filtered imports, returned thread IDs, and permission handling.
- Adds a shared picker accessible from the sidebar and command palette.
- Fixes imported Claude sessions to resume on their first T3 turn, with focused regression tests.
| File | Description |
|---|---|
packages/contracts/src/rpc.ts |
Registers the session-list RPC. |
packages/contracts/src/clientRpcPermissions.ts |
Guards session imports with operate permission. |
packages/contracts/src/agentSessions.ts |
Defines listing data and optional import-selection fields. |
packages/client-runtime/src/state/commandPermissions.test.ts |
Tests destination-specific import permissions. |
apps/web/src/state/agentSessions.ts |
Adds the session-list query. |
apps/web/src/routes/__root.tsx |
Mounts the shared session-picker host. |
apps/web/src/components/sidebar/SidebarThreadHeader.tsx |
Adds the existing-session button. |
apps/web/src/components/Sidebar.tsx |
Opens the picker for the contextual project. |
apps/web/src/components/OpenExistingSessionDialog.tsx |
Implements session listing, importing, and navigation. |
apps/web/src/components/CommandPalette.tsx |
Adds the project-scoped picker action. |
apps/server/src/ws.ts |
Routes listing requests to the importer service. |
apps/server/src/project/AgentSessionScanner.ts |
Supports refreshing transcript candidates. |
apps/server/src/project/AgentSessionImporter.ts |
Lists sessions, checks ownership, and filters imports. |
apps/server/src/project/AgentSessionImporter.test.ts |
Covers listing, selected imports, and repeat imports. |
apps/server/src/orchestration-v2/runtimeLayer.ts |
Provides projection access to the importer. |
apps/server/src/orchestration-v2/ProviderTurnControlService.test.ts |
Updates the projection-store test stub. |
apps/server/src/orchestration-v2/ProjectionStore.ts |
Adds native-session owner lookups. |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts |
Marks existing conversations for native resume. |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts |
Tests first-turn resume for imported sessions. |
apps/server/src/observability/RpcInstrumentation.ts |
Categorizes session listing as workspace activity. |
apps/server/src/auth/RpcAuthorization.ts |
Assigns the listing RPC’s required permission. |
Review effort: Balanced
| const outcomes = yield* scanner | ||
| .recentThreads(project.workspaceRoot, [], { refresh: true }) | ||
| .pipe(Stream.runCollect); |
There was a problem hiding this comment.
Fixed in 88d9e1a: the list maps each Importable outcome to a summary inside the stream before collecting, so no transcript's message history is retained past its parse.
| projectId: ProjectId, | ||
| expectedWorkspaceRoot: Schema.optional(TrimmedNonEmptyString), | ||
| // Without it, every importable session of the project is imported. | ||
| sessionIds: Schema.optional(Schema.Array(TrimmedNonEmptyString)), |
There was a problem hiding this comment.
Not changing this. The picker only renders Import buttons from a successful agentSessions.list result, and that method does not exist on older hosts, so the Import path is not reachable against one. Issue #29 specifies the filter as an optional field on the existing import input. A bulk import from a hand-written request against an older host is the same behavior that RPC always had.
| const threads = outcomes.flatMap((outcome) => | ||
| outcome._tag === "Importable" && outcome.thread.source === "claudeAgent" | ||
| ? [outcome.thread] |
There was a problem hiding this comment.
Fixed in 88d9e1a: recentThreads takes a sources option and the list passes ["claudeAgent"], so other providers' transcripts are left out before the budget is spent. Covered by a scanner test with 100 newer Codex rollouts ahead of one Claude session. T3-owned sessions still cost a read; skipping them would need ownership lookups inside the scanner, which I left out.
| .findNativeThreadOwners({ | ||
| driver: ProviderDriverKind.make("claudeAgent"), | ||
| providerInstanceId, | ||
| nativeIds: instanceThreads.map((thread) => thread.providerSessionId), |
There was a problem hiding this comment.
Fixed in 88d9e1a: findNativeThreadOwners now looks up owners by driver and native id across all instances, and returns the owner's instance so the import-thread check still works. The list test seeds an owner under a second Claude instance for a transcript the scanner credits to the default one.
| for (const thread of instanceThreads) { | ||
| const owner = owners.get(thread.providerSessionId); | ||
| if (owner !== undefined && owner !== importThreadId(thread)) continue; |
There was a problem hiding this comment.
Partly fixed in 88d9e1a: owners now carry deleted, and a session whose owning thread was deleted is left out of the list, since the event sink still treats the deleted thread as the owner so neither Open nor re-import would work. Archived import threads still show Open; the thread route loads archived threads.
| const threadId = ThreadId.make( | ||
| `import:${source.providerInstanceId}:${source.providerSessionId}`, | ||
| ); | ||
| if (wantedSessionIds !== null && !wantedSessionIds.has(source.providerSessionId)) return; |
There was a problem hiding this comment.
Not changing this. Claude session ids are UUIDs, and transcripts in a home shared by several instances are credited to one instance by the scanner, so the same id in two instances only happens when someone copies a transcript file between config dirs.
| </div> | ||
| {session.threadId !== undefined ? ( | ||
| <Button | ||
| type="button" |
There was a problem hiding this comment.
Fixed in 88d9e1a: the Open button now has an aria-label with the session title.
Listing terminal sessions reads transcripts outside T3, so it now needs filesystem:read like the other host-file RPCs. The list summarizes each transcript while streaming instead of holding every parsed history, and asks the scanner for Claude only so newer Codex rollouts no longer use up the shared transcript budget. Ownership is resolved across Claude instances that share a home, and a session whose owning thread was deleted is left out rather than offered as Open. The Open button names its session for screen readers.
…ting-claude-session
Thread transfer impact
This comment will update automatically after the next completed run. |


Problem
A Claude Code session started in a terminal could only be brought into T3 Code through the first-run welcome wizard, which bulk-imports per project and is unreachable once onboarding is done. Even when reached, the imported thread failed on its first follow-up with "Session ID … is already in use", because the Claude adapter opened the session with
sessionIdinstead ofresumewhen the thread had no conversation head and no prior T3 turn.Change
agentSessions.list { projectId }RPC returns recent Claude sessions for the project's workspace root (title, model, timestamps, message count), built on the scanner'srecentThreadswith a candidate refresh so sessions started after the last scan appear.threadIdis set when the session already has an imported thread. Sessions T3 started itself are left out, since they are already threads in the sidebar.agentSessions.importaccepts an optionalsessionIdsfilter and returnsthreadIds. Importing an already-imported session returns its existing thread without duplicating history. Import is added toCLIENT_GUARDED_RPC_SCOPES; the picker uses itspermissionAtomto gate the Import buttons.resume. The regression test is kept.Verification
vp run check:changedpasses apart fromAgentSessionScanner.test.ts › excludes sandboxes reached through a symlink into the worktrees dir, which fails identically onmainon this macOS checkout (temp dir under/var).AgentSessionImporter.test.ts); imported native session resumes on its first T3 turn (ClaudeAdapterV2.test.ts, fails on the unpatched adapter withresumeundefined); guarded import command (commandPermissions.test.ts)..t3: startedclaude -p --session-id …in the repo with the codeword "apricot", opened the picker from the sidebar, imported that session, sent one message. The spawned CLI for that first turn wasclaude … --model claude-haiku-4-5 … --resume=8706525a-3149-4017-aff9-2cec0624fd88 …with no--session-id, and the reply was "apricot". Reopening the picker showed the session with Open instead of Import.Before/after screenshots of the sidebar header, picker, palette entry, and the resumed thread were captured during the dev-server run and are added as a comment from the author's account.
Closes #29