Merge pull request #364 from flagon-io/363-session-prompt-noise
A session's page shows what you asked, not what Claude Code injected; highlights leave out acknowledgements
6 files+72−140/6 viewed
| 5 | 5 | ||
| 6 | 6 | import { | |
| 7 | 7 | ENDED_BY_HAND, | |
| 8 | + | askedText, | |
| 9 | + | isAcknowledgement, | |
| 8 | 10 | checkHref, | |
| 9 | 11 | endReasonText, | |
| 10 | 12 | evidenceOf, | |
| ⋯ | |||
| 462 | 464 | assert.equal(href(99), null, "unknown"); | |
| 463 | 465 | assert.equal(href(50), null, "a deleted repository"); | |
| 464 | 466 | }); | |
| 467 | + | ||
| 468 | + | test("what someone asked leaves out what the harness or the editor put ahead of it", () => { | |
| 469 | + | const injected = "<browser_instruction># Claude in Chrome browser automation\n\nYou have access to browser tools.</browser_instruction>"; | |
| 470 | + | assert.equal(askedText(`${injected}Alright, what's next on our work items?`), "Alright, what's next on our work items?"); | |
| 471 | + | assert.equal( | |
| 472 | + | askedText("<system-reminder>Codebase instructions</system-reminder>\n<ide_selection>a.ts</ide_selection>\nWhy is the page crashing?"), | |
| 473 | + | "Why is the page crashing?", | |
| 474 | + | ); | |
| 475 | + | // Only the blocks: a prompt that was nothing else asks nothing. | |
| 476 | + | assert.equal(askedText(injected), ""); | |
| 477 | + | // A tag someone types is theirs. | |
| 478 | + | assert.equal(askedText("<div> renders twice, why?"), "<div> renders twice, why?"); | |
| 479 | + | assert.equal(intentTitle(`${injected}Fix the sidebar under the app bar`), "Fix the sidebar under the app bar"); | |
| 480 | + | }); | |
| 481 | + | ||
| 482 | + | test("acknowledging coordination updates is told apart from a note", () => { | |
| 483 | + | assert.ok(isAcknowledgement("Coordination updates upd_c62316db756f071acf8fa1bbff: Unrelated session ended")); | |
| 484 | + | assert.ok(!isAcknowledgement("The approvals were declined because nobody watched.")); | |
| 485 | + | }); | |
| 117 | 117 | /** A turn the harness starts itself, as a tag: `<command-name>`, `<task-notification>`, one with attributes. */ | |
| 118 | 118 | const HARNESS_TAG = /^\s*<[a-z][\w-]*(?:\s[^>]*)?>/i; | |
| 119 | 119 | ||
| 120 | − | /** What an editor adds ahead of a prompt, a whole block at a time: `<ide_opened_file>…</ide_opened_file>`. */ | |
| 121 | − | const IDE_CONTEXT = /^\s*<(ide_[a-z_]+|system-reminder)\b[^>]*>[\s\S]*?<\/\1>/i; | |
| 120 | + | /** | |
| 121 | + | * What a harness or an editor adds ahead of a prompt, a whole block at a | |
| 122 | + | * time: `<ide_opened_file>…</ide_opened_file>`, `<system-reminder>…`, | |
| 123 | + | * Claude Code's `<browser_instruction>…`. Their names have an underscore | |
| 124 | + | * or a hyphen, which a tag someone types (`<div>`) does not. | |
| 125 | + | */ | |
| 126 | + | const INJECTED = /^\s*<([a-z][a-z0-9]*[_-][a-z0-9_-]*)\b[^>]*>[\s\S]*?<\/\1\s*>/i; | |
| 127 | + | ||
| 128 | + | /** | |
| 129 | + | * What someone asked, as they typed it: the prompt without the blocks a | |
| 130 | + | * harness or an editor put ahead of it. Empty when the prompt was only | |
| 131 | + | * those blocks. | |
| 132 | + | */ | |
| 133 | + | export function askedText(prompt: string | null | undefined): string { | |
| 134 | + | let text = String(prompt ?? "").replace(/^\uFEFF/, ""); | |
| 135 | + | while (INJECTED.test(text)) text = text.replace(INJECTED, ""); | |
| 136 | + | return text.trim(); | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | /** A note `g1t session acknowledge` keeps for the coordination updates it took in: bookkeeping, not a highlight. */ | |
| 140 | + | export function isAcknowledgement(text: string): boolean { | |
| 141 | + | return /^Coordination updates? upd_/.test(text.trim()); | |
| 142 | + | } | |
| 122 | 143 | ||
| 123 | 144 | /** | |
| 124 | 145 | * What a prompt is for, as a session's title: its first line or sentence, | |
| ⋯ | |||
| 134 | 155 | // pasted text (Claude Code's <pasted_content id="…">…</pasted_content>) | |
| 135 | 156 | // and image placeholders ([Image #1], [Image: source: …]) go first. | |
| 136 | 157 | text = text.replace(/<pasted_content\b[^>]*>[\s\S]*?(?:<\/pasted_content[^>]*>|$)/gi, "\n").replace(/\[Image(?: #\d+)?(?::[^\]\n]*)?\]/g, " "); | |
| 137 | − | // Context an editor puts before what was typed (VS Code's | |
| 138 | − | // <ide_opened_file>, <ide_selection>) is not a turn the harness started. | |
| 139 | − | while (IDE_CONTEXT.test(text)) text = text.replace(IDE_CONTEXT, ""); | |
| 158 | + | // Context a harness or an editor puts before what was typed (VS Code's | |
| 159 | + | // <ide_opened_file>, Claude Code's <browser_instruction>) is not a turn | |
| 160 | + | // the harness started. | |
| 161 | + | while (INJECTED.test(text)) text = text.replace(INJECTED, ""); | |
| 140 | 162 | if (/^\s*\//.test(text) || HARNESS_TAG.test(text)) return null; | |
| 141 | 163 | text = text | |
| 142 | 164 | .replace(/```[\s\S]*?(```|$)/g, "\n") | |
| 70 | 70 | entry(5, "note", "The approvals were declined because nobody watched.", "2026-10-21T10:01:00Z"), | |
| 71 | 71 | entry(6, "note", "", "2026-10-21T10:03:00Z"), | |
| 72 | 72 | entry(8, "message", "Not a highlight.", "2026-10-21T10:03:30Z"), | |
| 73 | + | // Acknowledging coordination updates is bookkeeping. | |
| 74 | + | entry(9, "note", "Coordination updates upd_c62316db756f071acf8fa1bbff: Unrelated session ended", "2026-10-21T10:03:40Z"), | |
| 73 | 75 | ]; | |
| 74 | 76 | const where = (d: Decision) => (d.pull != null ? `/acme/web/pull/${d.pull}` : `/acme/-/work/${d.item}`); | |
| 75 | 77 | assert.deepEqual(highlights(decisions, entries, new Set(["ses_a", "ses_sub"]), where), [ |
| 8 | 8 | */ | |
| 9 | 9 | import type { Decision, LinkVia, SessionEntry, SessionLink, SessionTouch, WorkSession } from "@g1t/contracts"; | |
| 10 | 10 | ||
| 11 | + | import { isAcknowledgement } from "./coordination.ts"; | |
| 12 | + | ||
| 11 | 13 | /** | |
| 12 | 14 | * What the session came to, in its own words: what it reported, else, once | |
| 13 | 15 | * it has ended, the last thing it said. Null while it is still going and | |
| ⋯ | |||
| 78 | 80 | return to ? [{ text: decision.summary, at: decision.createdAt, cite: { kind: "decision", to } }] : []; | |
| 79 | 81 | }); | |
| 80 | 82 | const notes = entries | |
| 81 | − | .filter((entry) => entry.kind === "note" && !entry.withheld && entry.text.trim() !== "") | |
| 83 | + | // Acknowledging coordination updates is bookkeeping, not something the session did. | |
| 84 | + | .filter((entry) => entry.kind === "note" && !entry.withheld && entry.text.trim() !== "" && !isAcknowledgement(entry.text)) | |
| 82 | 85 | .map((entry): Highlight => ({ text: entry.text.trim(), at: entry.at, cite: { kind: "entry", seq: entry.seq } })); | |
| 83 | 86 | // One decision recorded twice (on the pull request and its work item) is one highlight. | |
| 84 | 87 | const seen = new Set<string>(); | |
| 25 | 25 | import { | |
| 26 | 26 | type SessionNode, | |
| 27 | 27 | TOUCH_KINDS, | |
| 28 | + | askedText, | |
| 28 | 29 | duration, | |
| 29 | 30 | endReasonText, | |
| 30 | 31 | harnessName, | |
| ⋯ | |||
| 520 | 521 | }, []); | |
| 521 | 522 | ||
| 522 | 523 | const tools = entries.filter((entry) => entry.kind === "tool_call").length; | |
| 523 | − | const prompt = entries.find((entry) => entry.kind === "prompt"); | |
| 524 | + | // What it was asked: the first prompt with words of its own, without what the harness put ahead of them. | |
| 525 | + | const prompt = entries.find((entry) => entry.kind === "prompt" && (entry.withheld || askedText(entry.text) !== "")); | |
| 524 | 526 | const firstPrompt = prompt?.text; | |
| 525 | 527 | const isSubagent = Boolean(session.parentId); | |
| 526 | 528 | const parent = isSubagent && root?.id === session.parentId ? root : null; | |
| ⋯ | |||
| 788 | 790 | ||
| 789 | 791 | {/* The story: what it was asked, how long it worked, and what it came to. */} | |
| 790 | 792 | <div className="mt-8 space-y-5"> | |
| 791 | − | {prompt && !prompt.withheld && prompt.text.trim() && <AskedBubble text={prompt.text} repo={repoAt(base)} />} | |
| 792 | − | {isSubagent && session.prompt && (!prompt || prompt.withheld) && <AskedBubble text={session.prompt} repo={repoAt(base)} />} | |
| 793 | + | {prompt && !prompt.withheld && <AskedBubble text={askedText(prompt.text)} repo={repoAt(base)} />} | |
| 794 | + | {isSubagent && session.prompt && (!prompt || prompt.withheld) && <AskedBubble text={askedText(session.prompt)} repo={repoAt(base)} />} | |
| 793 | 795 | <WorkedFor live={live} span={duration(session, now)} tools={tools} open={transcript} onToggle={() => setTranscript((open) => !open)} /> | |
| 794 | 796 | {answer ? ( | |
| 795 | 797 | <SessionAnswer text={answer} repo={repoAt(base)} /> | |
| 239 | 239 | false | |
| 240 | 240 | } | |
| 241 | 241 | ||
| 242 | − | /// Context an editor puts before what was typed, a whole block at a time. | |
| 242 | + | /// Context a harness or an editor puts before what was typed, a whole | |
| 243 | + | /// block at a time: `<ide_opened_file>`, `<system-reminder>`, Claude Code's | |
| 244 | + | /// `<browser_instruction>`. Their names have an underscore or a hyphen, | |
| 245 | + | /// which a tag someone types (`<div>`) does not. | |
| 243 | 246 | fn drop_leading_context(mut text: String) -> String { | |
| 244 | 247 | loop { | |
| 245 | 248 | let trimmed = text.trim_start(); | |
| 246 | 249 | let lower = trimmed.to_ascii_lowercase(); | |
| 247 | − | let name: String = if lower.starts_with("<ide_") || lower.starts_with("<system-reminder") { | |
| 248 | − | lower[1..].chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-').collect() | |
| 249 | − | } else { | |
| 250 | + | let Some(after) = lower.strip_prefix('<') else { return text }; | |
| 251 | + | let name: String = after.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-').collect(); | |
| 252 | + | if !name.starts_with(|c: char| c.is_ascii_alphabetic()) || !name.contains(['_', '-']) { | |
| 250 | 253 | return text; | |
| 251 | − | }; | |
| 254 | + | } | |
| 252 | 255 | let close = format!("</{name}>"); | |
| 253 | 256 | match lower.find(&close) { | |
| 254 | 257 | Some(end) => text = trimmed[end + close.len()..].to_owned(), | |
| ⋯ | |||
| 1130 | 1133 | ); | |
| 1131 | 1134 | assert_eq!(intent_title("<ide_selection>const a = 1;</ide_selection>\n<ide_opened_file>a.ts</ide_opened_file>\nrename this").as_deref(), Some("Rename this")); | |
| 1132 | 1135 | assert_eq!(intent_title("<ide_opened_file>a.ts</ide_opened_file>\n/clear"), None); | |
| 1136 | + | // What Claude Code injects ahead of the prompt is not what it is for. | |
| 1137 | + | assert_eq!( | |
| 1138 | + | intent_title("<browser_instruction># Claude in Chrome\n\nUse the tools.</browser_instruction>Fix the sidebar under the app bar").as_deref(), | |
| 1139 | + | Some("Fix the sidebar under the app bar") | |
| 1140 | + | ); | |
| 1133 | 1141 | for prompt in ["/clear", "/compact keep the plan", "/g1t:status 4", "<command-name>/clear</command-name>", "<local-command-stdout lang=\"en\">ok</local-command-stdout>", "```\ncode only\n```", "?", ""] { | |
| 1134 | 1142 | assert_eq!(intent_title(prompt), None, "{prompt}"); | |
| 1135 | 1143 | } | |
Comments
Sign in to comment.