feat(agent): replay imported Claude Code CLI transcripts#2876
Conversation
Teach the Claude adapter to replay an imported CLI transcript into a fresh session: emit user prompts and top-level assistant text/thinking (no client history to dedupe against), recover typed slash-command invocations, and mark replayed user chunks so the load path can promote them into user bubbles. Also extracts encodeCwdToProjectKey from the JSONL path helper and adds the shared IMPORTED_USER_PROMPT_META_KEY marker plus the importedClaudeSession task-creation field. Part 1/3 of splitting #2873 (import Claude Code sessions). Generated-By: PostHog Code Task-Id: 6c93b6e8-27b6-45c8-8135-73a09076ea93
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
|
Reviews (1): Last reviewed commit: "feat(agent): replay imported Claude Code..." | Re-trigger Greptile |
| it("surfaces a typed slash command, not its raw markers", async () => { | ||
| const { context, updates } = createImportReplayContext(); | ||
| await handleUserAssistantMessage( | ||
| userMessage( | ||
| "<command-message>review</command-message>\n<command-name>/review</command-name>\n<command-args>#2198 - findings first</command-args>", | ||
| ), | ||
| context, | ||
| ); | ||
| expect(userChunkTexts(updates)).toEqual(["/review #2198 - findings first"]); | ||
| }); | ||
|
|
||
| it("surfaces a no-arg slash command as just the command name", async () => { | ||
| const { context, updates } = createImportReplayContext(); | ||
| await handleUserAssistantMessage( | ||
| userMessage( | ||
| "<command-message>compact</command-message>\n<command-name>/compact</command-name>\n<command-args></command-args>", | ||
| ), | ||
| context, | ||
| ); | ||
| expect(userChunkTexts(updates)).toEqual(["/compact"]); | ||
| }); |
There was a problem hiding this comment.
Slash-command tests could be parameterised
The two tests below exercise the same code path (extractSlashCommandInvocation) with different inputs and expected outputs, which is exactly the pattern the repo's simplicity rules ask to consolidate with it.each. Calling createImportReplayContext() and handleUserAssistantMessage identically in both bodies is duplication that a parameterised table would remove — e.g. it.each([[rawInput, expectedOutput], ...])("renders slash command %s as %s", ...). The same applies to "emits plain-text user prompts" and "marks imported user prompts", which both call handleUserAssistantMessage(userMessage("my earlier prompt"), context) and could be a single test asserting both the emitted text and the _meta flag.
Context Used: Do not attempt to comment on incorrect alphabetica... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| contentToProcess = | ||
| extractSlashCommandInvocation(content) ?? stripMarkerTags(content); |
There was a problem hiding this comment.
stripMarkerTags can return an empty string, producing a hollow user_message_chunk
If a user message consists entirely of marker tags (e.g. a bare <command-args>...</command-args> with no surrounding prose), extractSlashCommandInvocation returns null and stripMarkerTags returns "". That empty string is then forwarded to toAcpNotifications, which may emit a user_message_chunk notification with empty content; the notification is then stamped with IMPORTED_USER_PROMPT_META_KEY and recorded in notificationHistory. The analogous path in shouldSkipUserAssistantMessage already guards against this via stripLocalCommandMetadata, which returns null when nothing renderable remains. Applying the same null guard here (?? null with an early return) would keep the two paths consistent.

Problem
Changes
How did you test this?
Automatic notifications