Skip to content

feat: open an existing Claude Code session as a thread - #30

Merged
brandon-nash merged 3 commits into
mainfrom
sessions/29-open-existing-claude-session
Nov 2, 2026
Merged

brandon-nash merged 3 commits into
mainfrom
sessions/29-open-existing-claude-session

Conversation

@brandon-nash

@brandon-nash brandon-nash commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

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 sessionId instead of resume when the thread had no conversation head and no prior T3 turn.

Change

  • List. New agentSessions.list { projectId } RPC returns recent Claude sessions for the project's workspace root (title, model, timestamps, message count), built on the scanner's recentThreads with a candidate refresh so sessions started after the last scan appear. threadId is set when the session already has an imported thread. Sessions T3 started itself are left out, since they are already threads in the sidebar.
  • Import one. agentSessions.import accepts an optional sessionIds filter and returns threadIds. Importing an already-imported session returns its existing thread without duplicating history. Import is added to CLIENT_GUARDED_RPC_SCOPES; the picker uses its permissionAtom to gate the Import buttons.
  • Resume on the first turn. Borrowed from fix(server): imported Claude sessions resume on their first turn pingdotgg/t3code#23219: when the adapter loads a conversation whose transcript already has messages, it records the native session as opened so the first T3 turn sends resume. The regression test is kept.
  • Picker. "Open existing session" beside "New thread" in the sidebar header and an "Open existing session in " action in the command palette. Rows show title, relative time, message count, and model, with Import or Open per row; Import navigates to the new thread.
  • Mobile is untouched; contract additions are optional fields only.

Verification

  • vp run check:changed passes apart from AgentSessionScanner.test.ts › excludes sandboxes reached through a symlink into the worktrees dir, which fails identically on main on this macOS checkout (temp dir under /var).
  • Focused tests: list filter and ownership marking, single-session import and re-import without duplicates (AgentSessionImporter.test.ts); imported native session resumes on its first T3 turn (ClaudeAdapterV2.test.ts, fails on the unpatched adapter with resume undefined); guarded import command (commandPermissions.test.ts).
  • Real run against the dev server with an isolated .t3: started claude -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 was
    claude … --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

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

Copilot AI 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.

🟡 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
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

Comment on lines +208 to +210
const outcomes = yield* scanner
.recentThreads(project.workspaceRoot, [], { refresh: true })
.pipe(Stream.runCollect);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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)),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment on lines +211 to +213
const threads = outcomes.flatMap((outcome) =>
outcome._tag === "Importable" && outcome.thread.source === "claudeAgent"
? [outcome.thread]

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment on lines +220 to +223
.findNativeThreadOwners({
driver: ProviderDriverKind.make("claudeAgent"),
providerInstanceId,
nativeIds: instanceThreads.map((thread) => thread.providerSessionId),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment on lines +230 to +232
for (const thread of instanceThreads) {
const owner = owners.get(thread.providerSessionId);
if (owner !== undefined && owner !== importThreadId(thread)) continue;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL labels Oct 30, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for fefc039.

This comment will update automatically after the next completed run.

@brandon-nash
brandon-nash merged commit 639b2e7 into main Nov 2, 2026
9 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Open an existing Claude Code session as a thread (per-session import)

2 participants