diff --git a/src/servers/api/chat/claude-sessions.ts b/src/servers/api/chat/claude-sessions.ts
index cded3340..6e52cf22 100644
--- a/src/servers/api/chat/claude-sessions.ts
+++ b/src/servers/api/chat/claude-sessions.ts
@@ -178,7 +178,11 @@ function summarizeTranscript(filePath: string, id: string): TranscriptSummary |
sawFirstUserEntry = true;
bornFromClear = text.startsWith('/clear');
}
- if (text && !firstUserText && !text.startsWith('')) firstUserText = text;
+ // A resumed session can open with a background task reporting in, which is no more a title than a
+ // slash command is — see `isTaskNotification`.
+ if (text && !firstUserText && !text.startsWith('') && !isTaskNotification(text)) {
+ firstUserText = text;
+ }
}
}
}
@@ -384,8 +388,23 @@ function blockText(content: unknown): string {
return '';
}
-const userOrInterruption = (text: string): ClaudeChatMessage =>
- INTERRUPTION_MARKERS.has(text.trim()) ? { role: 'interrupted' } : { role: 'user', text };
+/**
+ * A background task reporting in. The harness delivers these to the agent by writing them as the *user's*
+ * next message — the same trick as `INTERRUPTION_MARKERS`, and the same consequence: replayed literally,
+ * the transcript shows a raw XML envelope in a bubble on your side of the conversation that you never
+ * typed. Live it never appears, because a finished task travels as its own `task:notification` event; it
+ * surfaces only when a refresh rebuilds the conversation from Claude's file.
+ *
+ * Anchored to the start of the message rather than matched anywhere in it, so quoting one inside a real
+ * message — this file's own conversation does exactly that — stays yours.
+ */
+const isTaskNotification = (text: string) => text.trimStart().startsWith('');
+
+function userMessageFrom(text: string): ClaudeChatMessage | null {
+ if (isTaskNotification(text)) return null;
+ if (INTERRUPTION_MARKERS.has(text.trim())) return { role: 'interrupted' };
+ return { role: 'user', text };
+}
export type ClaudeSessionDetail = { id: string; model: string; cwd: string; messages: ClaudeChatMessage[] };
@@ -430,13 +449,15 @@ function parseClaudeTranscript(filePath: string, sessionId: string, fallbackCwd
if (entry.type === 'user' && !entry.isMeta) {
if (typeof content === 'string') {
- if (content.trim()) messages.push(userOrInterruption(content));
+ const message = content.trim() ? userMessageFrom(content) : null;
+ if (message) messages.push(message);
continue;
}
if (Array.isArray(content)) {
for (const block of content as ContentBlock[]) {
if (block.type === 'text' && block.text?.trim()) {
- messages.push(userOrInterruption(block.text));
+ const message = userMessageFrom(block.text);
+ if (message) messages.push(message);
} else if (block.type === 'tool_result') {
const tool = toolById.get(block.tool_use_id);
if (tool) {