Import local chat history into agent host session as editable turns#324858
Draft
vijayupadya wants to merge 2 commits into
Draft
Import local chat history into agent host session as editable turns#324858vijayupadya wants to merge 2 commits into
vijayupadya wants to merge 2 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR enables “Continue in… → Copilot CLI” to migrate an existing local chat into a new agent-host (Copilot CLI) session as real, editable turns. It does so by translating the local IChatModel into agent-host Turn[], seeding a synthesized Copilot SDK events.jsonl, and wiring a short-lived store to bridge the untitled→real session-resource lifecycle.
Changes:
- Add
IChatModel → Turn[]translation for markdown, reasoning, inline references, and tool/sub-agent invocations. - Add a resource-keyed, consume-once import handoff store and thread it through session creation/rebind paths.
- Implement Copilot SDK event-log synthesis (
events.jsonl) + resume-based import, with unit/integration tests covering round-trip and editability.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/chat/test/browser/agentSessions/importLocalConversationToAgentSession.test.ts | New unit tests for IChatModel → Turn[] translation behavior. |
| src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostImportConversationStore.test.ts | New unit tests for consume-once import stash + rename handoff. |
| src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts | Stash imported conversation before opening to support eager backend creation. |
| src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts | Register the import-conversation store as a singleton service. |
| src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/importLocalConversationToAgentSession.ts | New translation layer from local chat model to agent-host turns/parts. |
| src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostUntitledProvisionalSessionService.ts | Rebind path consumes imported conversation and seeds backend session creation. |
| src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListController.ts | Rename imported-conversation stash from untitled resource to real resource on graduation. |
| src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts | Session creation path consumes imported conversation and passes it into backend create. |
| src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostImportConversationStore.ts | New consume-once, rename-capable handoff store keyed by session resource. |
| src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts | For local → Copilot CLI continuation, generate turns + carry model id instead of transcript attachment. |
| src/vs/platform/agentHost/test/node/protocol/copilotImportSession.integrationTest.ts | New (opt-in) real SDK integration tests proving seeded history is editable (truncate). |
| src/vs/platform/agentHost/test/node/buildSessionEvents.test.ts | New unit tests for Turn[] ↔ events.jsonl round-trip with mapSessionEvents. |
| src/vs/platform/agentHost/node/copilot/diskSessionFsProvider.ts | New filesystem provider for session-state seeding in tests/tools. |
| src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts | Deduplicate BYOK model selections to avoid runtime config rejection. |
| src/vs/platform/agentHost/node/copilot/copilotAgent.ts | Implement _importConversation: write events.jsonl, persist metadata, resume session. |
| src/vs/platform/agentHost/node/copilot/buildSessionEvents.ts | New event-log synthesis from turns, including tool and sub-agent events. |
| src/vs/platform/agentHost/node/agentService.ts | Assign fresh UUID turn ids for imports; seed protocol turns + set imported-session title. |
| src/vs/platform/agentHost/common/agentService.ts | Extend create-session config with importConversation option and semantics. |
Review details
- Files reviewed: 18/18 changed files
- Comments generated: 4
- Review effort level: Low
Comment on lines
+200
to
+204
| if (part.kind === ResponsePartKind.Markdown) { | ||
| markdown += part.content; | ||
| } else if (part.kind === ResponsePartKind.Reasoning) { | ||
| reasoning += part.content; | ||
| } else if (part.kind === ResponsePartKind.ToolCall && part.toolCall.status === ToolCallStatus.Completed) { |
Comment on lines
+38
to
+41
| // Normalize separators and strip any leading root (`/`, `\`, or a drive | ||
| // like `C:`) so the path is always resolved *within* the base directory. | ||
| const normalized = sessionFsPath.replace(/\\/g, '/').replace(/^[a-zA-Z]:/, '').replace(/^\/+/, ''); | ||
| return join(this._baseDir, normalized); |
| Sidebar = 'sidebar' | ||
| } | ||
|
|
||
| import { IAgentHostImportConversationStore, type IAgentHostImportConversation } from '../agentSessions/agentHost/agentHostImportConversationStore.js'; |
Comment on lines
+27
to
+41
| /** | ||
| * Directory entry shape returned by {@link SessionFsProvider.readdirWithTypes}. | ||
| * Mirrors the SDK's `SessionFsReaddirWithTypesEntry`, which is not re-exported | ||
| * from the package root. | ||
| */ | ||
| type SessionFsReaddirWithTypesEntry = { name: string; type: 'file' | 'directory' }; | ||
| import { FileAccess } from '../../../../../base/common/network.js'; | ||
| import { delimiter, dirname, join } from '../../../../../base/common/path.js'; | ||
| import { URI } from '../../../../../base/common/uri.js'; | ||
| import { generateUuid } from '../../../../../base/common/uuid.js'; | ||
| import { rgDiskPath } from '../../../../../base/node/ripgrep.js'; | ||
| import { MessageKind, ResponsePartKind, TurnState, type ResponsePart, type Turn } from '../../../common/state/sessionState.js'; | ||
| import { buildSessionEventLogFromTurns } from '../../../node/copilot/buildSessionEvents.js'; | ||
| import { DiskSessionFsProvider } from '../../../node/copilot/diskSessionFsProvider.js'; | ||
| import { resolveGitHubToken } from './realSdkTestHelpers.js'; |
# Conflicts: # src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostUntitledProvisionalSessionService.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When continuing a local chat into Copilot CLI (Continue in… → Copilot CLI), the prior
conversation is now imported into the new agent-host session as real, editable turns — edit,
fork, and truncate all work — instead of a read-only pasted transcript.
This is done by translating the local
IChatModelinto the Copilot SDK's nativeevents.jsonlformat and resuming the session, so the CLI reconstitutes the history as genuine backend turns.
How it works
The conversation is translated once, seeded on disk, and resumed:
High-level architecture
Trigger → graduation → seed (sequence)
The session goes through heavy lifecycle churn: an
untitled-<uuid>provisional session ispre-warmed, then graduates to a real backend session. The import is keyed by resource
and renamed across the graduation so it lands on the session that actually survives.
Why resource-keyed rename
tryRebindcreates a fresh backend session for the real resource and disposes theuntitled one. If the import were consumed at the pre-warm (
getOrCreate) it would be seeded intothe discarded session. Keeping the stash resource-keyed and renaming it at graduation
(untitled → real) ensures
tryRebindconsumes it and seeds the surviving session.Fidelity
subagent.startedeventevents.jsonl)Not included: file-edit interactive diffs and full sub-agent transcripts