@ai-matrx/coding-sessions 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/engine/index.cjs +54 -10
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +1 -1
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +54 -10
- package/dist/engine/index.js.map +1 -1
- package/dist/{index-29t7n472.d.cts → index-Bbe11HoB.d.cts} +30 -3
- package/dist/{index-29t7n472.d.ts → index-Bbe11HoB.d.ts} +30 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +34 -5
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +15 -1
- package/dist/react/index.d.ts +15 -1
- package/dist/react/index.js +34 -5
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +44 -9
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +44 -9
- package/dist/server/index.js.map +1 -1
- package/dist/sessions/index.cjs +26 -1
- package/dist/sessions/index.cjs.map +1 -1
- package/dist/sessions/index.d.cts +13 -5
- package/dist/sessions/index.d.ts +13 -5
- package/dist/sessions/index.js +26 -1
- package/dist/sessions/index.js.map +1 -1
- package/dist/{titles-eCnA5sY7.d.cts → titles-DjxUb0ji.d.cts} +8 -1
- package/dist/{titles-eCnA5sY7.d.ts → titles-DjxUb0ji.d.ts} +8 -1
- package/dist/ui/assets/index-BqzTnRFe.css +2 -0
- package/dist/ui/assets/{index-SuRr0-D2.js → index-U98AY8r-.js} +17 -17
- package/dist/ui/index.html +2 -2
- package/package.json +3 -3
- package/dist/ui/assets/index-BqpVx_My.css +0 -2
package/dist/sessions/index.cjs
CHANGED
|
@@ -21,12 +21,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var sessions_exports = {};
|
|
22
22
|
__export(sessions_exports, {
|
|
23
23
|
BRIDGE_PLACEHOLDER: () => BRIDGE_PLACEHOLDER,
|
|
24
|
+
DISPLAY_MAX: () => DISPLAY_MAX,
|
|
24
25
|
TITLE_MAX: () => TITLE_MAX,
|
|
25
26
|
TITLE_PRECEDENCE: () => TITLE_PRECEDENCE,
|
|
26
27
|
UNTITLED: () => UNTITLED,
|
|
28
|
+
appTitlesOf: () => appTitlesOf,
|
|
27
29
|
claudeTranscriptTitleSources: () => claudeTranscriptTitleSources,
|
|
28
30
|
cleanTitle: () => cleanTitle,
|
|
29
31
|
createTitleHintsStore: () => createTitleHintsStore,
|
|
32
|
+
displayTitle: () => displayTitle,
|
|
30
33
|
isBridgePlaceholder: () => isBridgePlaceholder,
|
|
31
34
|
resolveSessionTitle: () => resolveSessionTitle,
|
|
32
35
|
titleHintKey: () => titleHintKey
|
|
@@ -270,6 +273,12 @@ function cleanTitle(value) {
|
|
|
270
273
|
if (!line) return null;
|
|
271
274
|
return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}\u2026` : line;
|
|
272
275
|
}
|
|
276
|
+
var DISPLAY_MAX = 100;
|
|
277
|
+
var LEADING_NOISE = /^(?:\s*(?:\d{1,3}[.)]|[-*•]|#{1,6}|>)\s+|\s*["“'‘`]+)+/;
|
|
278
|
+
function displayTitle(title, max = DISPLAY_MAX) {
|
|
279
|
+
const stripped = title.replace(LEADING_NOISE, "").replace(/["”'’`]+$/, "").trim() || title.trim();
|
|
280
|
+
return stripped.length > max ? `${stripped.slice(0, max - 1).trimEnd()}\u2026` : stripped;
|
|
281
|
+
}
|
|
273
282
|
function resolveSessionTitle(sources) {
|
|
274
283
|
for (const source of TITLE_PRECEDENCE) {
|
|
275
284
|
const raw = sources[source];
|
|
@@ -319,17 +328,33 @@ function titleHintKey(provider, sessionId) {
|
|
|
319
328
|
}
|
|
320
329
|
function createTitleHintsStore() {
|
|
321
330
|
let hints = null;
|
|
331
|
+
let problem = null;
|
|
322
332
|
const listeners = /* @__PURE__ */ new Set();
|
|
333
|
+
const tell = () => {
|
|
334
|
+
for (const l of listeners) l();
|
|
335
|
+
};
|
|
323
336
|
return {
|
|
324
337
|
current: () => hints,
|
|
338
|
+
problem: () => problem,
|
|
325
339
|
subscribe(listener) {
|
|
326
340
|
listeners.add(listener);
|
|
327
341
|
return () => listeners.delete(listener);
|
|
328
342
|
},
|
|
329
343
|
set(next) {
|
|
330
344
|
hints = next;
|
|
331
|
-
|
|
345
|
+
problem = null;
|
|
346
|
+
tell();
|
|
347
|
+
},
|
|
348
|
+
setProblem(next) {
|
|
349
|
+
if (next === problem) return;
|
|
350
|
+
problem = next;
|
|
351
|
+
tell();
|
|
332
352
|
}
|
|
333
353
|
};
|
|
334
354
|
}
|
|
355
|
+
function appTitlesOf(hints) {
|
|
356
|
+
const out = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const [key, hint] of hints) out.set(key, hint.title);
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
335
360
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sessions/index.ts","../../src/normalize/agent-block.ts","../../src/normalize/claude.ts","../../src/sessions/titles.ts","../../src/sessions/hints.ts"],"sourcesContent":["/**\n * `@ai-matrx/coding-sessions/sessions` — the shared core of \"what is this coding chat and what is it\n * called\": the one title rule, the title-hints port, and the read-only file access every reader\n * of the coding apps' own files takes (Matrx 2's core passes its `fs`/`exec`-backed one). Pure.\n */\n\nexport {\n BRIDGE_PLACEHOLDER,\n TITLE_MAX,\n TITLE_PRECEDENCE,\n UNTITLED,\n claudeTranscriptTitleSources,\n cleanTitle,\n isBridgePlaceholder,\n resolveSessionTitle,\n type ResolvedTitle,\n type TitleSource,\n type TitleSources,\n} from \"./titles\";\nexport { createTitleHintsStore, titleHintKey, type HintProvider, type TitleHint, type TitleHints } from \"./hints\";\n\n/** One folder entry, as a reader of the apps' files sees it. */\nexport interface SourceEntry {\n readonly name: string;\n readonly kind: \"file\" | \"dir\" | \"other\";\n readonly size: number;\n readonly mtimeMs: number;\n}\n\n/** Read-only access to the apps' files (Matrx 2: files through fs, SQLite through exec). */\nexport interface SourceIo {\n join(...parts: string[]): string;\n list(dir: string): Promise<SourceEntry[] | null>;\n stat(path: string): Promise<SourceEntry | null>;\n /** A whole file as text (null when absent). */\n readText(path: string): Promise<string | null>;\n readBytes(path: string, offset: number, length: number): Promise<Uint8Array>;\n /** Rows of one read-only query (`immutable` when no writer can be running). Throws a typed error. */\n sqlite(db: string, sql: string, immutable: boolean): Promise<Array<Record<string, unknown>>>;\n}\n","/**\n * The delimited AI Matrx agent block a coding session puts in front of a\n * person's message when a CLI must be TOLD about the agent (a Codex takeover,\n * an agent switch, after a compaction). Pure: the transcript strips it so the\n * screen shows what the person wrote, not how the CLI was told.\n */\n\n/** The marker every agent text we hand a CLI starts with. */\nexport const AGENT_BLOCK_MARKER = \"<ai_matrx_agent \";\n\n/**\n * Remove a leading agent block from a user message, so the transcript shows\n * what the PERSON wrote (the block is how the CLI was told, not their words).\n */\nexport function stripAgentBlock(text: string): string {\n const removed = text.match(/^<ai_matrx_agent_removed>[\\s\\S]*?<\\/ai_matrx_agent_removed>\\s*/);\n if (removed) return text.slice(removed[0].length);\n if (!text.startsWith(AGENT_BLOCK_MARKER)) return text;\n const end = text.indexOf(\"</ai_matrx_agent>\");\n if (end < 0) return text;\n let rest = text.slice(end + \"</ai_matrx_agent>\".length);\n // The Codex skills list rides after the block.\n const skills = rest.match(/^\\s*<skills_instructions>[\\s\\S]*?<\\/skills_instructions>/);\n if (skills) rest = rest.slice(skills[0].length);\n return rest.replace(/^\\s+/, \"\");\n}\n","/**\n * Claude Code → `TranscriptItem`.\n *\n * ONE normalizer reads both of Claude's shapes, because they are the same\n * shape: `claude --print --output-format stream-json` lines (live) and the\n * entries of the user's own session file `~/.claude/projects/<cwd>/<id>.jsonl`\n * (history) both carry `{ type: \"assistant\" | \"user\", message: { content } }`.\n * The live stream adds `system`/`stream_event`/`result`; the file adds its own\n * bookkeeping entries (titles, attachments, snapshots), which are skipped.\n *\n * Block identity: an assistant message arrives as several lines sharing one\n * `message.id`, one content block per line, in block order — the same order\n * the partial-message `content_block_start.index` counts. So a text block's id\n * is `<message id>:<index>` on both paths, and a streamed draft is replaced by\n * its final text rather than duplicated.\n */\n\nimport type { RunUsage, TranscriptItem } from \"../types\";\nimport { stripAgentBlock } from \"./agent-block\";\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction num(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/** User-role text Claude Code writes that the person never typed. */\nconst SYNTHETIC_USER_PREFIXES = [\n \"<command-name>\",\n \"<command-message>\",\n \"<command-args>\",\n \"<local-command-stdout>\",\n \"<local-command-stderr>\",\n \"<local-command-caveat>\",\n \"<system-reminder>\",\n \"<bash-input>\",\n \"<bash-stdout>\",\n \"<bash-stderr>\",\n \"<user-memory-input>\",\n \"<task-notification>\",\n \"Caveat: The messages below were generated by the user while running local commands\",\n \"[Request interrupted by user\",\n];\n\nexport function isSyntheticClaudeUserText(text: string): boolean {\n const trimmed = text.trimStart();\n return SYNTHETIC_USER_PREFIXES.some((prefix) => trimmed.startsWith(prefix));\n}\n\nfunction toolResultText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((block) => {\n if (!isObject(block)) return \"\";\n if (block.type === \"text\") return str(block.text) ?? \"\";\n if (block.type === \"image\") return \"[image]\";\n return \"\";\n })\n .filter(Boolean)\n .join(\"\\n\");\n }\n return content === undefined || content === null ? \"\" : JSON.stringify(content);\n}\n\nexport interface ClaudeNormalizer {\n /** Feed one parsed JSON line; returns the items it produced (often none). */\n push(entry: unknown): TranscriptItem[];\n /** The Claude session id, once a line has carried it. */\n readonly sessionId: string | null;\n}\n\nexport function createClaudeNormalizer(): ClaudeNormalizer {\n let sessionId: string | null = null;\n let streamingMessageId: string | null = null;\n const blockCounters = new Map<string, number>();\n let userCounter = 0;\n let noticeCounter = 0;\n\n function nextBlockIndex(messageId: string): number {\n const index = blockCounters.get(messageId) ?? 0;\n blockCounters.set(messageId, index + 1);\n return index;\n }\n\n function pushAssistant(entry: Json): TranscriptItem[] {\n const message = isObject(entry.message) ? entry.message : null;\n if (!message) return [];\n const messageId = str(message.id) ?? str(entry.uuid) ?? `assistant-${blockCounters.size}`;\n const content = Array.isArray(message.content) ? message.content : [];\n const items: TranscriptItem[] = [];\n for (const block of content) {\n if (!isObject(block)) continue;\n const index = nextBlockIndex(messageId);\n const id = `${messageId}:${index}`;\n if (block.type === \"text\") {\n const text = str(block.text) ?? \"\";\n if (text.length > 0) items.push({ kind: \"text\", id, text });\n } else if (block.type === \"thinking\") {\n // Claude Code writes thinking blocks with an EMPTY text and only a\n // signature unless thinking display is on; an empty aside is noise.\n const text = str(block.thinking) ?? \"\";\n if (text.trim().length > 0) items.push({ kind: \"reasoning\", id, text });\n } else if (block.type === \"tool_use\" || block.type === \"server_tool_use\") {\n items.push({\n kind: \"tool-call\",\n id: str(block.id) ?? id,\n name: str(block.name) ?? \"tool\",\n input: block.input ?? null,\n });\n }\n }\n return items;\n }\n\n function pushUser(entry: Json): TranscriptItem[] {\n if (entry.isMeta === true || entry.isCompactSummary === true) return [];\n const message = isObject(entry.message) ? entry.message : null;\n if (!message) return [];\n const at = str(entry.timestamp);\n const content = message.content;\n if (typeof content === \"string\") {\n if (isSyntheticClaudeUserText(content) || content.trim().length === 0) return [];\n userCounter += 1;\n // The agent block is how the CLI was told, not the person's words.\n return [{ kind: \"user\", id: str(entry.uuid) ?? `user-${userCounter}`, text: stripAgentBlock(content), at }];\n }\n if (!Array.isArray(content)) return [];\n const items: TranscriptItem[] = [];\n const typed: string[] = [];\n for (const block of content) {\n if (!isObject(block)) continue;\n if (block.type === \"tool_result\") {\n items.push({\n kind: \"tool-result\",\n id: str(block.tool_use_id) ?? \"unknown\",\n output: toolResultText(block.content),\n isError: block.is_error === true,\n });\n } else if (block.type === \"text\") {\n const text = str(block.text) ?? \"\";\n if (text.trim().length > 0 && !isSyntheticClaudeUserText(text)) typed.push(text);\n }\n }\n if (typed.length > 0) {\n userCounter += 1;\n items.unshift({\n kind: \"user\",\n id: str(entry.uuid) ?? `user-${userCounter}`,\n text: stripAgentBlock(typed.join(\"\\n\\n\")),\n at,\n });\n }\n return items;\n }\n\n function pushStreamEvent(entry: Json): TranscriptItem[] {\n const event = isObject(entry.event) ? entry.event : null;\n if (!event) return [];\n if (event.type === \"message_start\" && isObject(event.message)) {\n streamingMessageId = str(event.message.id);\n return [];\n }\n if (event.type === \"content_block_delta\" && streamingMessageId !== null) {\n const delta = isObject(event.delta) ? event.delta : null;\n if (delta?.type === \"text_delta\") {\n const text = str(delta.text) ?? \"\";\n if (text.length === 0) return [];\n return [{ kind: \"text-delta\", id: `${streamingMessageId}:${num(event.index)}`, text }];\n }\n }\n return [];\n }\n\n function pushResult(entry: Json): TranscriptItem[] {\n const items: TranscriptItem[] = [];\n const usage = isObject(entry.usage) ? entry.usage : null;\n if (usage) {\n const runUsage: RunUsage = {\n inputTokens: num(usage.input_tokens) + num(usage.cache_creation_input_tokens),\n outputTokens: num(usage.output_tokens),\n cachedInputTokens: num(usage.cache_read_input_tokens),\n };\n const cost = typeof entry.total_cost_usd === \"number\" ? entry.total_cost_usd : null;\n items.push({ kind: \"usage\", usage: runUsage, costUsd: cost });\n }\n if (entry.is_error === true || (typeof entry.subtype === \"string\" && entry.subtype !== \"success\")) {\n const text =\n str(entry.result) ??\n (Array.isArray(entry.errors) ? entry.errors.map(String).join(\"\\n\") : null) ??\n `Claude Code stopped with \"${String(entry.subtype)}\".`;\n noticeCounter += 1;\n items.push({ kind: \"error\", id: `claude-result-${noticeCounter}`, text });\n }\n return items;\n }\n\n return {\n get sessionId() {\n return sessionId;\n },\n push(raw: unknown): TranscriptItem[] {\n if (!isObject(raw)) return [];\n const lineSession = str(raw.session_id) ?? str(raw.sessionId);\n if (lineSession && sessionId === null) sessionId = lineSession;\n if (raw.isSidechain === true) return [];\n switch (raw.type) {\n case \"assistant\":\n return pushAssistant(raw);\n case \"user\":\n return pushUser(raw);\n case \"stream_event\":\n return pushStreamEvent(raw);\n case \"result\":\n return pushResult(raw);\n case \"system\":\n if (raw.subtype === \"compact_boundary\") {\n noticeCounter += 1;\n return [\n {\n kind: \"notice\",\n id: `claude-compact-${noticeCounter}`,\n level: \"info\",\n text: \"Claude Code summarized the earlier part of this conversation to make room.\",\n },\n ];\n }\n return [];\n default:\n return [];\n }\n },\n };\n}\n","/**\n * THE ONE TITLE RULE for a coding chat, shared by every reader (Matrx 2's Coding Sessions and\n * Code › Chats, the standalone host, search). Pure: no file access, browser-safe.\n *\n * What a chat is called is what the person sees in the coding app, so the app's own record leads;\n * the CLI's transcript entries fill in for chats the app never listed. The person's rename inside\n * Matrx sits on top and is applied ONCE, by whoever draws the row (never baked into the engine's\n * own `title`). Plan: common-docs/projects/matrx-2/apps-chats.md §12.\n */\n\nimport { createClaudeNormalizer } from \"../normalize/claude\";\n\n/** Every title is cut to this (the app tables' rule); the screen truncates further visually. */\nexport const TITLE_MAX = 300;\n/** What a row shows when no source has a title. */\nexport const UNTITLED = \"(untitled)\";\n\n/**\n * The name the AI Matrx coding-session bridge used to stamp on every mirrored Claude Code prompt\n * (`sessionTitle` from its UserPromptSubmit hook; stopped at the source 2026-10-06, still inside\n * old transcripts as `custom-title` / `agent-name` entries). Never a chat's title.\n */\nexport const BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;\n\n/** Where a title came from, strongest first. */\nexport const TITLE_PRECEDENCE = [\n /** The person's rename inside Matrx (Coding Sessions' preference). */\n \"override\",\n /** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */\n \"app\",\n /** The title the app last showed, remembered by the claude-accounts engine after the record went away. */\n \"app-remembered\",\n /** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */\n \"custom-title\",\n \"ai-title\",\n \"agent-name\",\n \"summary\",\n /** The person's first typed message (Codex: the thread's title / first user message). */\n \"first-prompt\",\n \"last-prompt\",\n] as const;\n\nexport type TitleSource = (typeof TITLE_PRECEDENCE)[number];\n\n/** Whatever each source says; absent, null and blank all mean \"no title here\". */\nexport type TitleSources = Partial<Record<TitleSource, string | null | undefined>>;\n\nexport interface ResolvedTitle {\n title: string;\n source: TitleSource;\n}\n\n/** Transcript-only sources the bridge placeholder can poison. */\nconst STAMPABLE: ReadonlySet<TitleSource> = new Set([\"custom-title\", \"ai-title\", \"agent-name\"]);\n\nexport function isBridgePlaceholder(value: unknown): boolean {\n return typeof value === \"string\" && BRIDGE_PLACEHOLDER.test(value.trim());\n}\n\n/** The first non-empty line, trimmed, cut to `TITLE_MAX` with \"…\"; null when there is none. */\nexport function cleanTitle(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const line = value\n .split(/\\r?\\n/)\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n if (!line) return null;\n return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}…` : line;\n}\n\n/** The title to show and where it came from: the first source in `TITLE_PRECEDENCE` that has one. */\nexport function resolveSessionTitle(sources: TitleSources): ResolvedTitle | null {\n for (const source of TITLE_PRECEDENCE) {\n const raw = sources[source];\n if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;\n const title = cleanTitle(raw);\n if (title) return { title, source };\n }\n return null;\n}\n\n/**\n * Claude Code's own title entries from transcript lines (already parsed JSON), plus the person's\n * first typed message from `head` (the same normalizer the transcript screen uses). Later entries\n * win within a kind; a bridge placeholder never replaces a real one.\n */\nexport function claudeTranscriptTitleSources(head: Iterable<unknown>, tail: Iterable<unknown> = []): TitleSources {\n const out: TitleSources = {};\n const take = (source: TitleSource, value: unknown) => {\n if (typeof value !== \"string\" || !value.trim()) return;\n if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;\n out[source] = value;\n };\n const normalizer = createClaudeNormalizer();\n let firstPrompt: string | null = null;\n const scan = (entries: Iterable<unknown>, findPrompt: boolean) => {\n for (const entry of entries) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const e = entry as Record<string, unknown>;\n if (e.type === \"custom-title\") take(\"custom-title\", e.customTitle);\n else if (e.type === \"ai-title\") take(\"ai-title\", e.aiTitle);\n else if (e.type === \"agent-name\") take(\"agent-name\", e.agentName);\n else if (e.type === \"summary\") take(\"summary\", e.summary);\n else if (e.type === \"last-prompt\") take(\"last-prompt\", e.lastPrompt);\n else if (findPrompt && firstPrompt === null) {\n for (const item of normalizer.push(entry)) {\n if (item.kind === \"user\" && item.text.trim()) {\n firstPrompt = item.text;\n break;\n }\n }\n }\n }\n };\n scan(head, true);\n scan(tail, false);\n if (firstPrompt !== null) out[\"first-prompt\"] = firstPrompt;\n return out;\n}\n","/**\n * TITLE HINTS — the port through which the owner of the coding apps' index (Matrx 2's core, which\n * reads Claude's records, Codex's database and the rest through its own `fs`/`exec` owners) tells\n * this package what each chat is called and whether the app hides it. The package never walks the\n * apps' records itself: it reads transcripts (titles by the transcript rules, content search,\n * activity) and overlays these hints as they arrive. Pure: browser-safe types and helpers.\n */\n\nimport type { TitleSource } from \"./titles\";\n\nexport type HintProvider = \"claude\" | \"codex\";\n\nexport interface TitleHint {\n /** The app's title (pre-override: the person's rename is applied by whoever draws the row). */\n title: string;\n source: TitleSource;\n /** The app hides it: archived in the app (shown behind a toggle). */\n archived?: boolean | undefined;\n /** A scheduled or automated run (shown behind a toggle). */\n scheduled?: boolean | undefined;\n /** Listed by the app but its conversation file is gone: why (it is shown, never dropped). */\n unavailable?: string | null | undefined;\n /** Last activity the app records (ms), for chats only the app knows. */\n lastActiveMs?: number | null | undefined;\n}\n\nexport interface TitleHints {\n /** The hints now, keyed `titleHintKey()`; null while the owner's index is still loading. Never waits. */\n current(): ReadonlyMap<string, TitleHint> | null;\n /** Called after the hints change (a rename in the app, a new chat, the first load). */\n subscribe(listener: () => void): () => void;\n}\n\nexport function titleHintKey(provider: HintProvider, sessionId: string): string {\n return `${provider}:${sessionId}`;\n}\n\n/** A hints port the host fills by hand (tests, or an owner that pushes whole snapshots). */\nexport function createTitleHintsStore(): TitleHints & { set(hints: ReadonlyMap<string, TitleHint> | null): void } {\n let hints: ReadonlyMap<string, TitleHint> | null = null;\n const listeners = new Set<() => void>();\n return {\n current: () => hints,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n set(next) {\n hints = next;\n for (const l of listeners) l();\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/sessions/index.ts","../../src/normalize/agent-block.ts","../../src/normalize/claude.ts","../../src/sessions/titles.ts","../../src/sessions/hints.ts"],"sourcesContent":["/**\n * `@ai-matrx/coding-sessions/sessions` — the shared core of \"what is this coding chat and what is it\n * called\": the one title rule, the title-hints port, and the read-only file access every reader\n * of the coding apps' own files takes (Matrx 2's core passes its `fs`/`exec`-backed one). Pure.\n */\n\nexport {\n BRIDGE_PLACEHOLDER,\n DISPLAY_MAX,\n TITLE_MAX,\n TITLE_PRECEDENCE,\n UNTITLED,\n claudeTranscriptTitleSources,\n cleanTitle,\n displayTitle,\n isBridgePlaceholder,\n resolveSessionTitle,\n type ResolvedTitle,\n type TitleSource,\n type TitleSources,\n} from \"./titles\";\nexport { appTitlesOf, createTitleHintsStore, titleHintKey, type HintProvider, type TitleHint, type TitleHints } from \"./hints\";\n\n/** One folder entry, as a reader of the apps' files sees it. */\nexport interface SourceEntry {\n readonly name: string;\n readonly kind: \"file\" | \"dir\" | \"other\";\n readonly size: number;\n readonly mtimeMs: number;\n}\n\n/** Read-only access to the apps' files (Matrx 2: files through fs, SQLite through exec). */\nexport interface SourceIo {\n join(...parts: string[]): string;\n list(dir: string): Promise<SourceEntry[] | null>;\n stat(path: string): Promise<SourceEntry | null>;\n /** A whole file as text (null when absent). */\n readText(path: string): Promise<string | null>;\n readBytes(path: string, offset: number, length: number): Promise<Uint8Array>;\n /** Rows of one read-only query (`immutable` when no writer can be running). Throws a typed error. */\n sqlite(db: string, sql: string, immutable: boolean): Promise<Array<Record<string, unknown>>>;\n}\n","/**\n * The delimited AI Matrx agent block a coding session puts in front of a\n * person's message when a CLI must be TOLD about the agent (a Codex takeover,\n * an agent switch, after a compaction). Pure: the transcript strips it so the\n * screen shows what the person wrote, not how the CLI was told.\n */\n\n/** The marker every agent text we hand a CLI starts with. */\nexport const AGENT_BLOCK_MARKER = \"<ai_matrx_agent \";\n\n/**\n * Remove a leading agent block from a user message, so the transcript shows\n * what the PERSON wrote (the block is how the CLI was told, not their words).\n */\nexport function stripAgentBlock(text: string): string {\n const removed = text.match(/^<ai_matrx_agent_removed>[\\s\\S]*?<\\/ai_matrx_agent_removed>\\s*/);\n if (removed) return text.slice(removed[0].length);\n if (!text.startsWith(AGENT_BLOCK_MARKER)) return text;\n const end = text.indexOf(\"</ai_matrx_agent>\");\n if (end < 0) return text;\n let rest = text.slice(end + \"</ai_matrx_agent>\".length);\n // The Codex skills list rides after the block.\n const skills = rest.match(/^\\s*<skills_instructions>[\\s\\S]*?<\\/skills_instructions>/);\n if (skills) rest = rest.slice(skills[0].length);\n return rest.replace(/^\\s+/, \"\");\n}\n","/**\n * Claude Code → `TranscriptItem`.\n *\n * ONE normalizer reads both of Claude's shapes, because they are the same\n * shape: `claude --print --output-format stream-json` lines (live) and the\n * entries of the user's own session file `~/.claude/projects/<cwd>/<id>.jsonl`\n * (history) both carry `{ type: \"assistant\" | \"user\", message: { content } }`.\n * The live stream adds `system`/`stream_event`/`result`; the file adds its own\n * bookkeeping entries (titles, attachments, snapshots), which are skipped.\n *\n * Block identity: an assistant message arrives as several lines sharing one\n * `message.id`, one content block per line, in block order — the same order\n * the partial-message `content_block_start.index` counts. So a text block's id\n * is `<message id>:<index>` on both paths, and a streamed draft is replaced by\n * its final text rather than duplicated.\n */\n\nimport type { RunUsage, TranscriptItem } from \"../types\";\nimport { stripAgentBlock } from \"./agent-block\";\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction num(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/** User-role text Claude Code writes that the person never typed. */\nconst SYNTHETIC_USER_PREFIXES = [\n \"<command-name>\",\n \"<command-message>\",\n \"<command-args>\",\n \"<local-command-stdout>\",\n \"<local-command-stderr>\",\n \"<local-command-caveat>\",\n \"<system-reminder>\",\n \"<bash-input>\",\n \"<bash-stdout>\",\n \"<bash-stderr>\",\n \"<user-memory-input>\",\n \"<task-notification>\",\n \"Caveat: The messages below were generated by the user while running local commands\",\n \"[Request interrupted by user\",\n];\n\nexport function isSyntheticClaudeUserText(text: string): boolean {\n const trimmed = text.trimStart();\n return SYNTHETIC_USER_PREFIXES.some((prefix) => trimmed.startsWith(prefix));\n}\n\nfunction toolResultText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((block) => {\n if (!isObject(block)) return \"\";\n if (block.type === \"text\") return str(block.text) ?? \"\";\n if (block.type === \"image\") return \"[image]\";\n return \"\";\n })\n .filter(Boolean)\n .join(\"\\n\");\n }\n return content === undefined || content === null ? \"\" : JSON.stringify(content);\n}\n\nexport interface ClaudeNormalizer {\n /** Feed one parsed JSON line; returns the items it produced (often none). */\n push(entry: unknown): TranscriptItem[];\n /** The Claude session id, once a line has carried it. */\n readonly sessionId: string | null;\n}\n\nexport function createClaudeNormalizer(): ClaudeNormalizer {\n let sessionId: string | null = null;\n let streamingMessageId: string | null = null;\n const blockCounters = new Map<string, number>();\n let userCounter = 0;\n let noticeCounter = 0;\n\n function nextBlockIndex(messageId: string): number {\n const index = blockCounters.get(messageId) ?? 0;\n blockCounters.set(messageId, index + 1);\n return index;\n }\n\n function pushAssistant(entry: Json): TranscriptItem[] {\n const message = isObject(entry.message) ? entry.message : null;\n if (!message) return [];\n const messageId = str(message.id) ?? str(entry.uuid) ?? `assistant-${blockCounters.size}`;\n const content = Array.isArray(message.content) ? message.content : [];\n const items: TranscriptItem[] = [];\n for (const block of content) {\n if (!isObject(block)) continue;\n const index = nextBlockIndex(messageId);\n const id = `${messageId}:${index}`;\n if (block.type === \"text\") {\n const text = str(block.text) ?? \"\";\n if (text.length > 0) items.push({ kind: \"text\", id, text });\n } else if (block.type === \"thinking\") {\n // Claude Code writes thinking blocks with an EMPTY text and only a\n // signature unless thinking display is on; an empty aside is noise.\n const text = str(block.thinking) ?? \"\";\n if (text.trim().length > 0) items.push({ kind: \"reasoning\", id, text });\n } else if (block.type === \"tool_use\" || block.type === \"server_tool_use\") {\n items.push({\n kind: \"tool-call\",\n id: str(block.id) ?? id,\n name: str(block.name) ?? \"tool\",\n input: block.input ?? null,\n });\n }\n }\n return items;\n }\n\n function pushUser(entry: Json): TranscriptItem[] {\n if (entry.isMeta === true || entry.isCompactSummary === true) return [];\n const message = isObject(entry.message) ? entry.message : null;\n if (!message) return [];\n const at = str(entry.timestamp);\n const content = message.content;\n if (typeof content === \"string\") {\n if (isSyntheticClaudeUserText(content) || content.trim().length === 0) return [];\n userCounter += 1;\n // The agent block is how the CLI was told, not the person's words.\n return [{ kind: \"user\", id: str(entry.uuid) ?? `user-${userCounter}`, text: stripAgentBlock(content), at }];\n }\n if (!Array.isArray(content)) return [];\n const items: TranscriptItem[] = [];\n const typed: string[] = [];\n for (const block of content) {\n if (!isObject(block)) continue;\n if (block.type === \"tool_result\") {\n items.push({\n kind: \"tool-result\",\n id: str(block.tool_use_id) ?? \"unknown\",\n output: toolResultText(block.content),\n isError: block.is_error === true,\n });\n } else if (block.type === \"text\") {\n const text = str(block.text) ?? \"\";\n if (text.trim().length > 0 && !isSyntheticClaudeUserText(text)) typed.push(text);\n }\n }\n if (typed.length > 0) {\n userCounter += 1;\n items.unshift({\n kind: \"user\",\n id: str(entry.uuid) ?? `user-${userCounter}`,\n text: stripAgentBlock(typed.join(\"\\n\\n\")),\n at,\n });\n }\n return items;\n }\n\n function pushStreamEvent(entry: Json): TranscriptItem[] {\n const event = isObject(entry.event) ? entry.event : null;\n if (!event) return [];\n if (event.type === \"message_start\" && isObject(event.message)) {\n streamingMessageId = str(event.message.id);\n return [];\n }\n if (event.type === \"content_block_delta\" && streamingMessageId !== null) {\n const delta = isObject(event.delta) ? event.delta : null;\n if (delta?.type === \"text_delta\") {\n const text = str(delta.text) ?? \"\";\n if (text.length === 0) return [];\n return [{ kind: \"text-delta\", id: `${streamingMessageId}:${num(event.index)}`, text }];\n }\n }\n return [];\n }\n\n function pushResult(entry: Json): TranscriptItem[] {\n const items: TranscriptItem[] = [];\n const usage = isObject(entry.usage) ? entry.usage : null;\n if (usage) {\n const runUsage: RunUsage = {\n inputTokens: num(usage.input_tokens) + num(usage.cache_creation_input_tokens),\n outputTokens: num(usage.output_tokens),\n cachedInputTokens: num(usage.cache_read_input_tokens),\n };\n const cost = typeof entry.total_cost_usd === \"number\" ? entry.total_cost_usd : null;\n items.push({ kind: \"usage\", usage: runUsage, costUsd: cost });\n }\n if (entry.is_error === true || (typeof entry.subtype === \"string\" && entry.subtype !== \"success\")) {\n const text =\n str(entry.result) ??\n (Array.isArray(entry.errors) ? entry.errors.map(String).join(\"\\n\") : null) ??\n `Claude Code stopped with \"${String(entry.subtype)}\".`;\n noticeCounter += 1;\n items.push({ kind: \"error\", id: `claude-result-${noticeCounter}`, text });\n }\n return items;\n }\n\n return {\n get sessionId() {\n return sessionId;\n },\n push(raw: unknown): TranscriptItem[] {\n if (!isObject(raw)) return [];\n const lineSession = str(raw.session_id) ?? str(raw.sessionId);\n if (lineSession && sessionId === null) sessionId = lineSession;\n if (raw.isSidechain === true) return [];\n switch (raw.type) {\n case \"assistant\":\n return pushAssistant(raw);\n case \"user\":\n return pushUser(raw);\n case \"stream_event\":\n return pushStreamEvent(raw);\n case \"result\":\n return pushResult(raw);\n case \"system\":\n if (raw.subtype === \"compact_boundary\") {\n noticeCounter += 1;\n return [\n {\n kind: \"notice\",\n id: `claude-compact-${noticeCounter}`,\n level: \"info\",\n text: \"Claude Code summarized the earlier part of this conversation to make room.\",\n },\n ];\n }\n return [];\n default:\n return [];\n }\n },\n };\n}\n","/**\n * THE ONE TITLE RULE for a coding chat, shared by every reader (Matrx 2's Coding Sessions and\n * Code › Chats, the standalone host, search). Pure: no file access, browser-safe.\n *\n * What a chat is called is what the person sees in the coding app, so the app's own record leads;\n * the CLI's transcript entries fill in for chats the app never listed. The person's rename inside\n * Matrx sits on top and is applied ONCE, by whoever draws the row (never baked into the engine's\n * own `title`). Plan: common-docs/projects/matrx-2/apps-chats.md §12.\n */\n\nimport { createClaudeNormalizer } from \"../normalize/claude\";\n\n/** Every title is cut to this (the app tables' rule); the screen truncates further visually. */\nexport const TITLE_MAX = 300;\n/** What a row shows when no source has a title. */\nexport const UNTITLED = \"(untitled)\";\n\n/**\n * The name the AI Matrx coding-session bridge used to stamp on every mirrored Claude Code prompt\n * (`sessionTitle` from its UserPromptSubmit hook; stopped at the source 2026-10-06, still inside\n * old transcripts as `custom-title` / `agent-name` entries). Never a chat's title.\n */\nexport const BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;\n\n/** Where a title came from, strongest first. */\nexport const TITLE_PRECEDENCE = [\n /** The person's rename inside Matrx (Coding Sessions' preference). */\n \"override\",\n /** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */\n \"app\",\n /** The title the app last showed, remembered by the claude-accounts engine after the record went away. */\n \"app-remembered\",\n /** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */\n \"custom-title\",\n \"ai-title\",\n \"agent-name\",\n \"summary\",\n /** The person's first typed message (Codex: the thread's title / first user message). */\n \"first-prompt\",\n \"last-prompt\",\n] as const;\n\nexport type TitleSource = (typeof TITLE_PRECEDENCE)[number];\n\n/** Whatever each source says; absent, null and blank all mean \"no title here\". */\nexport type TitleSources = Partial<Record<TitleSource, string | null | undefined>>;\n\nexport interface ResolvedTitle {\n title: string;\n source: TitleSource;\n}\n\n/** Transcript-only sources the bridge placeholder can poison. */\nconst STAMPABLE: ReadonlySet<TitleSource> = new Set([\"custom-title\", \"ai-title\", \"agent-name\"]);\n\nexport function isBridgePlaceholder(value: unknown): boolean {\n return typeof value === \"string\" && BRIDGE_PLACEHOLDER.test(value.trim());\n}\n\n/** The first non-empty line, trimmed, cut to `TITLE_MAX` with \"…\"; null when there is none. */\nexport function cleanTitle(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const line = value\n .split(/\\r?\\n/)\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n if (!line) return null;\n return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}…` : line;\n}\n\n/** The longest title a list shows in full (the stored title stays exact; the full text goes on hover). */\nexport const DISPLAY_MAX = 100;\n\n/** A list marker, heading mark or opening quote a pasted first line starts with (\"1) \", \"- \", \"# \", \"> \", \"“\"). */\nconst LEADING_NOISE = /^(?:\\s*(?:\\d{1,3}[.)]|[-*•]|#{1,6}|>)\\s+|\\s*[\"“'‘`]+)+/;\n\n/**\n * How a list draws a title: leading list markers and quotes dropped, cut to `max` with \"…\". Only for\n * display — the stored title (and search, and the hover text) stays exactly `title`.\n */\nexport function displayTitle(title: string, max: number = DISPLAY_MAX): string {\n const stripped = title.replace(LEADING_NOISE, \"\").replace(/[\"”'’`]+$/, \"\").trim() || title.trim();\n return stripped.length > max ? `${stripped.slice(0, max - 1).trimEnd()}…` : stripped;\n}\n\n/** The title to show and where it came from: the first source in `TITLE_PRECEDENCE` that has one. */\nexport function resolveSessionTitle(sources: TitleSources): ResolvedTitle | null {\n for (const source of TITLE_PRECEDENCE) {\n const raw = sources[source];\n if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;\n const title = cleanTitle(raw);\n if (title) return { title, source };\n }\n return null;\n}\n\n/**\n * Claude Code's own title entries from transcript lines (already parsed JSON), plus the person's\n * first typed message from `head` (the same normalizer the transcript screen uses). Later entries\n * win within a kind; a bridge placeholder never replaces a real one.\n */\nexport function claudeTranscriptTitleSources(head: Iterable<unknown>, tail: Iterable<unknown> = []): TitleSources {\n const out: TitleSources = {};\n const take = (source: TitleSource, value: unknown) => {\n if (typeof value !== \"string\" || !value.trim()) return;\n if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;\n out[source] = value;\n };\n const normalizer = createClaudeNormalizer();\n let firstPrompt: string | null = null;\n const scan = (entries: Iterable<unknown>, findPrompt: boolean) => {\n for (const entry of entries) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const e = entry as Record<string, unknown>;\n if (e.type === \"custom-title\") take(\"custom-title\", e.customTitle);\n else if (e.type === \"ai-title\") take(\"ai-title\", e.aiTitle);\n else if (e.type === \"agent-name\") take(\"agent-name\", e.agentName);\n else if (e.type === \"summary\") take(\"summary\", e.summary);\n else if (e.type === \"last-prompt\") take(\"last-prompt\", e.lastPrompt);\n else if (findPrompt && firstPrompt === null) {\n for (const item of normalizer.push(entry)) {\n if (item.kind === \"user\" && item.text.trim()) {\n firstPrompt = item.text;\n break;\n }\n }\n }\n }\n };\n scan(head, true);\n scan(tail, false);\n if (firstPrompt !== null) out[\"first-prompt\"] = firstPrompt;\n return out;\n}\n","/**\n * TITLE HINTS — the port through which the owner of the coding apps' index (Matrx 2's core, which\n * reads Claude's records, Codex's database and the rest through its own `fs`/`exec` owners) tells\n * this package what each chat is called and whether the app hides it. The package never walks the\n * apps' records itself: it reads transcripts (titles by the transcript rules, content search,\n * activity) and overlays these hints as they arrive. Pure: browser-safe types and helpers.\n */\n\nimport type { TitleSource } from \"./titles\";\n\nexport type HintProvider = \"claude\" | \"codex\";\n\nexport interface TitleHint {\n /** The app's title (pre-override: the person's rename is applied by whoever draws the row). */\n title: string;\n /** Where the owner's title came from; \"unknown\" when the owner did not say (shown as such, never guessed). */\n source: TitleSource | \"unknown\";\n /** The app hides it: archived in the app (shown behind a toggle). */\n archived?: boolean | undefined;\n /** A scheduled or automated run (shown behind a toggle). */\n scheduled?: boolean | undefined;\n /** Listed by the app but its conversation file is gone: why (it is shown, never dropped). */\n unavailable?: string | null | undefined;\n /** Last activity the app records (ms), for chats only the app knows. */\n lastActiveMs?: number | null | undefined;\n}\n\nexport interface TitleHints {\n /** The hints now, keyed `titleHintKey()`; null while the owner's index is still loading. Never waits. */\n current(): ReadonlyMap<string, TitleHint> | null;\n /** Called after the hints change (a rename in the app, a new chat, the first load, a failure). */\n subscribe(listener: () => void): () => void;\n /** Why the apps' titles could not be had right now (shown with a Retry), or null. */\n problem?(): string | null;\n /** Ask the owner again now (the Retry). */\n retry?(): void;\n}\n\nexport function titleHintKey(provider: HintProvider, sessionId: string): string {\n return `${provider}:${sessionId}`;\n}\n\n/** A hints port the host fills by hand (tests, or an owner that pushes whole snapshots). */\nexport function createTitleHintsStore(): TitleHints & {\n set(hints: ReadonlyMap<string, TitleHint> | null): void;\n setProblem(problem: string | null): void;\n} {\n let hints: ReadonlyMap<string, TitleHint> | null = null;\n let problem: string | null = null;\n const listeners = new Set<() => void>();\n const tell = () => {\n for (const l of listeners) l();\n };\n return {\n current: () => hints,\n problem: () => problem,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n set(next) {\n hints = next;\n problem = null;\n tell();\n },\n setProblem(next) {\n if (next === problem) return;\n problem = next;\n tell();\n },\n };\n}\n\n/** The app titles a hints map carries, keyed `provider:id` (for title search and Codex names). */\nexport function appTitlesOf(hints: ReadonlyMap<string, TitleHint>): Map<string, string> {\n const out = new Map<string, string>();\n for (const [key, hint] of hints) out.set(key, hint.title);\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as TitleSource } from '../titles-
|
|
2
|
-
export { B as BRIDGE_PLACEHOLDER, R as ResolvedTitle, a as TITLE_MAX, b as TITLE_PRECEDENCE, c as TitleSources, U as UNTITLED, d as claudeTranscriptTitleSources, e as cleanTitle, i as isBridgePlaceholder, r as resolveSessionTitle } from '../titles-
|
|
1
|
+
import { T as TitleSource } from '../titles-DjxUb0ji.cjs';
|
|
2
|
+
export { B as BRIDGE_PLACEHOLDER, D as DISPLAY_MAX, R as ResolvedTitle, a as TITLE_MAX, b as TITLE_PRECEDENCE, c as TitleSources, U as UNTITLED, d as claudeTranscriptTitleSources, e as cleanTitle, f as displayTitle, i as isBridgePlaceholder, r as resolveSessionTitle } from '../titles-DjxUb0ji.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* TITLE HINTS — the port through which the owner of the coding apps' index (Matrx 2's core, which
|
|
@@ -13,7 +13,8 @@ type HintProvider = "claude" | "codex";
|
|
|
13
13
|
interface TitleHint {
|
|
14
14
|
/** The app's title (pre-override: the person's rename is applied by whoever draws the row). */
|
|
15
15
|
title: string;
|
|
16
|
-
|
|
16
|
+
/** Where the owner's title came from; "unknown" when the owner did not say (shown as such, never guessed). */
|
|
17
|
+
source: TitleSource | "unknown";
|
|
17
18
|
/** The app hides it: archived in the app (shown behind a toggle). */
|
|
18
19
|
archived?: boolean | undefined;
|
|
19
20
|
/** A scheduled or automated run (shown behind a toggle). */
|
|
@@ -26,14 +27,21 @@ interface TitleHint {
|
|
|
26
27
|
interface TitleHints {
|
|
27
28
|
/** The hints now, keyed `titleHintKey()`; null while the owner's index is still loading. Never waits. */
|
|
28
29
|
current(): ReadonlyMap<string, TitleHint> | null;
|
|
29
|
-
/** Called after the hints change (a rename in the app, a new chat, the first load). */
|
|
30
|
+
/** Called after the hints change (a rename in the app, a new chat, the first load, a failure). */
|
|
30
31
|
subscribe(listener: () => void): () => void;
|
|
32
|
+
/** Why the apps' titles could not be had right now (shown with a Retry), or null. */
|
|
33
|
+
problem?(): string | null;
|
|
34
|
+
/** Ask the owner again now (the Retry). */
|
|
35
|
+
retry?(): void;
|
|
31
36
|
}
|
|
32
37
|
declare function titleHintKey(provider: HintProvider, sessionId: string): string;
|
|
33
38
|
/** A hints port the host fills by hand (tests, or an owner that pushes whole snapshots). */
|
|
34
39
|
declare function createTitleHintsStore(): TitleHints & {
|
|
35
40
|
set(hints: ReadonlyMap<string, TitleHint> | null): void;
|
|
41
|
+
setProblem(problem: string | null): void;
|
|
36
42
|
};
|
|
43
|
+
/** The app titles a hints map carries, keyed `provider:id` (for title search and Codex names). */
|
|
44
|
+
declare function appTitlesOf(hints: ReadonlyMap<string, TitleHint>): Map<string, string>;
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
47
|
* `@ai-matrx/coding-sessions/sessions` — the shared core of "what is this coding chat and what is it
|
|
@@ -60,4 +68,4 @@ interface SourceIo {
|
|
|
60
68
|
sqlite(db: string, sql: string, immutable: boolean): Promise<Array<Record<string, unknown>>>;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
export { type HintProvider, type SourceEntry, type SourceIo, type TitleHint, type TitleHints, TitleSource, createTitleHintsStore, titleHintKey };
|
|
71
|
+
export { type HintProvider, type SourceEntry, type SourceIo, type TitleHint, type TitleHints, TitleSource, appTitlesOf, createTitleHintsStore, titleHintKey };
|
package/dist/sessions/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as TitleSource } from '../titles-
|
|
2
|
-
export { B as BRIDGE_PLACEHOLDER, R as ResolvedTitle, a as TITLE_MAX, b as TITLE_PRECEDENCE, c as TitleSources, U as UNTITLED, d as claudeTranscriptTitleSources, e as cleanTitle, i as isBridgePlaceholder, r as resolveSessionTitle } from '../titles-
|
|
1
|
+
import { T as TitleSource } from '../titles-DjxUb0ji.js';
|
|
2
|
+
export { B as BRIDGE_PLACEHOLDER, D as DISPLAY_MAX, R as ResolvedTitle, a as TITLE_MAX, b as TITLE_PRECEDENCE, c as TitleSources, U as UNTITLED, d as claudeTranscriptTitleSources, e as cleanTitle, f as displayTitle, i as isBridgePlaceholder, r as resolveSessionTitle } from '../titles-DjxUb0ji.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* TITLE HINTS — the port through which the owner of the coding apps' index (Matrx 2's core, which
|
|
@@ -13,7 +13,8 @@ type HintProvider = "claude" | "codex";
|
|
|
13
13
|
interface TitleHint {
|
|
14
14
|
/** The app's title (pre-override: the person's rename is applied by whoever draws the row). */
|
|
15
15
|
title: string;
|
|
16
|
-
|
|
16
|
+
/** Where the owner's title came from; "unknown" when the owner did not say (shown as such, never guessed). */
|
|
17
|
+
source: TitleSource | "unknown";
|
|
17
18
|
/** The app hides it: archived in the app (shown behind a toggle). */
|
|
18
19
|
archived?: boolean | undefined;
|
|
19
20
|
/** A scheduled or automated run (shown behind a toggle). */
|
|
@@ -26,14 +27,21 @@ interface TitleHint {
|
|
|
26
27
|
interface TitleHints {
|
|
27
28
|
/** The hints now, keyed `titleHintKey()`; null while the owner's index is still loading. Never waits. */
|
|
28
29
|
current(): ReadonlyMap<string, TitleHint> | null;
|
|
29
|
-
/** Called after the hints change (a rename in the app, a new chat, the first load). */
|
|
30
|
+
/** Called after the hints change (a rename in the app, a new chat, the first load, a failure). */
|
|
30
31
|
subscribe(listener: () => void): () => void;
|
|
32
|
+
/** Why the apps' titles could not be had right now (shown with a Retry), or null. */
|
|
33
|
+
problem?(): string | null;
|
|
34
|
+
/** Ask the owner again now (the Retry). */
|
|
35
|
+
retry?(): void;
|
|
31
36
|
}
|
|
32
37
|
declare function titleHintKey(provider: HintProvider, sessionId: string): string;
|
|
33
38
|
/** A hints port the host fills by hand (tests, or an owner that pushes whole snapshots). */
|
|
34
39
|
declare function createTitleHintsStore(): TitleHints & {
|
|
35
40
|
set(hints: ReadonlyMap<string, TitleHint> | null): void;
|
|
41
|
+
setProblem(problem: string | null): void;
|
|
36
42
|
};
|
|
43
|
+
/** The app titles a hints map carries, keyed `provider:id` (for title search and Codex names). */
|
|
44
|
+
declare function appTitlesOf(hints: ReadonlyMap<string, TitleHint>): Map<string, string>;
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
47
|
* `@ai-matrx/coding-sessions/sessions` — the shared core of "what is this coding chat and what is it
|
|
@@ -60,4 +68,4 @@ interface SourceIo {
|
|
|
60
68
|
sqlite(db: string, sql: string, immutable: boolean): Promise<Array<Record<string, unknown>>>;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
export { type HintProvider, type SourceEntry, type SourceIo, type TitleHint, type TitleHints, TitleSource, createTitleHintsStore, titleHintKey };
|
|
71
|
+
export { type HintProvider, type SourceEntry, type SourceIo, type TitleHint, type TitleHints, TitleSource, appTitlesOf, createTitleHintsStore, titleHintKey };
|
package/dist/sessions/index.js
CHANGED
|
@@ -235,6 +235,12 @@ function cleanTitle(value) {
|
|
|
235
235
|
if (!line) return null;
|
|
236
236
|
return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}\u2026` : line;
|
|
237
237
|
}
|
|
238
|
+
var DISPLAY_MAX = 100;
|
|
239
|
+
var LEADING_NOISE = /^(?:\s*(?:\d{1,3}[.)]|[-*•]|#{1,6}|>)\s+|\s*["“'‘`]+)+/;
|
|
240
|
+
function displayTitle(title, max = DISPLAY_MAX) {
|
|
241
|
+
const stripped = title.replace(LEADING_NOISE, "").replace(/["”'’`]+$/, "").trim() || title.trim();
|
|
242
|
+
return stripped.length > max ? `${stripped.slice(0, max - 1).trimEnd()}\u2026` : stripped;
|
|
243
|
+
}
|
|
238
244
|
function resolveSessionTitle(sources) {
|
|
239
245
|
for (const source of TITLE_PRECEDENCE) {
|
|
240
246
|
const raw = sources[source];
|
|
@@ -284,27 +290,46 @@ function titleHintKey(provider, sessionId) {
|
|
|
284
290
|
}
|
|
285
291
|
function createTitleHintsStore() {
|
|
286
292
|
let hints = null;
|
|
293
|
+
let problem = null;
|
|
287
294
|
const listeners = /* @__PURE__ */ new Set();
|
|
295
|
+
const tell = () => {
|
|
296
|
+
for (const l of listeners) l();
|
|
297
|
+
};
|
|
288
298
|
return {
|
|
289
299
|
current: () => hints,
|
|
300
|
+
problem: () => problem,
|
|
290
301
|
subscribe(listener) {
|
|
291
302
|
listeners.add(listener);
|
|
292
303
|
return () => listeners.delete(listener);
|
|
293
304
|
},
|
|
294
305
|
set(next) {
|
|
295
306
|
hints = next;
|
|
296
|
-
|
|
307
|
+
problem = null;
|
|
308
|
+
tell();
|
|
309
|
+
},
|
|
310
|
+
setProblem(next) {
|
|
311
|
+
if (next === problem) return;
|
|
312
|
+
problem = next;
|
|
313
|
+
tell();
|
|
297
314
|
}
|
|
298
315
|
};
|
|
299
316
|
}
|
|
317
|
+
function appTitlesOf(hints) {
|
|
318
|
+
const out = /* @__PURE__ */ new Map();
|
|
319
|
+
for (const [key, hint] of hints) out.set(key, hint.title);
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
300
322
|
export {
|
|
301
323
|
BRIDGE_PLACEHOLDER,
|
|
324
|
+
DISPLAY_MAX,
|
|
302
325
|
TITLE_MAX,
|
|
303
326
|
TITLE_PRECEDENCE,
|
|
304
327
|
UNTITLED,
|
|
328
|
+
appTitlesOf,
|
|
305
329
|
claudeTranscriptTitleSources,
|
|
306
330
|
cleanTitle,
|
|
307
331
|
createTitleHintsStore,
|
|
332
|
+
displayTitle,
|
|
308
333
|
isBridgePlaceholder,
|
|
309
334
|
resolveSessionTitle,
|
|
310
335
|
titleHintKey
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/normalize/agent-block.ts","../../src/normalize/claude.ts","../../src/sessions/titles.ts","../../src/sessions/hints.ts"],"sourcesContent":["/**\n * The delimited AI Matrx agent block a coding session puts in front of a\n * person's message when a CLI must be TOLD about the agent (a Codex takeover,\n * an agent switch, after a compaction). Pure: the transcript strips it so the\n * screen shows what the person wrote, not how the CLI was told.\n */\n\n/** The marker every agent text we hand a CLI starts with. */\nexport const AGENT_BLOCK_MARKER = \"<ai_matrx_agent \";\n\n/**\n * Remove a leading agent block from a user message, so the transcript shows\n * what the PERSON wrote (the block is how the CLI was told, not their words).\n */\nexport function stripAgentBlock(text: string): string {\n const removed = text.match(/^<ai_matrx_agent_removed>[\\s\\S]*?<\\/ai_matrx_agent_removed>\\s*/);\n if (removed) return text.slice(removed[0].length);\n if (!text.startsWith(AGENT_BLOCK_MARKER)) return text;\n const end = text.indexOf(\"</ai_matrx_agent>\");\n if (end < 0) return text;\n let rest = text.slice(end + \"</ai_matrx_agent>\".length);\n // The Codex skills list rides after the block.\n const skills = rest.match(/^\\s*<skills_instructions>[\\s\\S]*?<\\/skills_instructions>/);\n if (skills) rest = rest.slice(skills[0].length);\n return rest.replace(/^\\s+/, \"\");\n}\n","/**\n * Claude Code → `TranscriptItem`.\n *\n * ONE normalizer reads both of Claude's shapes, because they are the same\n * shape: `claude --print --output-format stream-json` lines (live) and the\n * entries of the user's own session file `~/.claude/projects/<cwd>/<id>.jsonl`\n * (history) both carry `{ type: \"assistant\" | \"user\", message: { content } }`.\n * The live stream adds `system`/`stream_event`/`result`; the file adds its own\n * bookkeeping entries (titles, attachments, snapshots), which are skipped.\n *\n * Block identity: an assistant message arrives as several lines sharing one\n * `message.id`, one content block per line, in block order — the same order\n * the partial-message `content_block_start.index` counts. So a text block's id\n * is `<message id>:<index>` on both paths, and a streamed draft is replaced by\n * its final text rather than duplicated.\n */\n\nimport type { RunUsage, TranscriptItem } from \"../types\";\nimport { stripAgentBlock } from \"./agent-block\";\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction num(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/** User-role text Claude Code writes that the person never typed. */\nconst SYNTHETIC_USER_PREFIXES = [\n \"<command-name>\",\n \"<command-message>\",\n \"<command-args>\",\n \"<local-command-stdout>\",\n \"<local-command-stderr>\",\n \"<local-command-caveat>\",\n \"<system-reminder>\",\n \"<bash-input>\",\n \"<bash-stdout>\",\n \"<bash-stderr>\",\n \"<user-memory-input>\",\n \"<task-notification>\",\n \"Caveat: The messages below were generated by the user while running local commands\",\n \"[Request interrupted by user\",\n];\n\nexport function isSyntheticClaudeUserText(text: string): boolean {\n const trimmed = text.trimStart();\n return SYNTHETIC_USER_PREFIXES.some((prefix) => trimmed.startsWith(prefix));\n}\n\nfunction toolResultText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((block) => {\n if (!isObject(block)) return \"\";\n if (block.type === \"text\") return str(block.text) ?? \"\";\n if (block.type === \"image\") return \"[image]\";\n return \"\";\n })\n .filter(Boolean)\n .join(\"\\n\");\n }\n return content === undefined || content === null ? \"\" : JSON.stringify(content);\n}\n\nexport interface ClaudeNormalizer {\n /** Feed one parsed JSON line; returns the items it produced (often none). */\n push(entry: unknown): TranscriptItem[];\n /** The Claude session id, once a line has carried it. */\n readonly sessionId: string | null;\n}\n\nexport function createClaudeNormalizer(): ClaudeNormalizer {\n let sessionId: string | null = null;\n let streamingMessageId: string | null = null;\n const blockCounters = new Map<string, number>();\n let userCounter = 0;\n let noticeCounter = 0;\n\n function nextBlockIndex(messageId: string): number {\n const index = blockCounters.get(messageId) ?? 0;\n blockCounters.set(messageId, index + 1);\n return index;\n }\n\n function pushAssistant(entry: Json): TranscriptItem[] {\n const message = isObject(entry.message) ? entry.message : null;\n if (!message) return [];\n const messageId = str(message.id) ?? str(entry.uuid) ?? `assistant-${blockCounters.size}`;\n const content = Array.isArray(message.content) ? message.content : [];\n const items: TranscriptItem[] = [];\n for (const block of content) {\n if (!isObject(block)) continue;\n const index = nextBlockIndex(messageId);\n const id = `${messageId}:${index}`;\n if (block.type === \"text\") {\n const text = str(block.text) ?? \"\";\n if (text.length > 0) items.push({ kind: \"text\", id, text });\n } else if (block.type === \"thinking\") {\n // Claude Code writes thinking blocks with an EMPTY text and only a\n // signature unless thinking display is on; an empty aside is noise.\n const text = str(block.thinking) ?? \"\";\n if (text.trim().length > 0) items.push({ kind: \"reasoning\", id, text });\n } else if (block.type === \"tool_use\" || block.type === \"server_tool_use\") {\n items.push({\n kind: \"tool-call\",\n id: str(block.id) ?? id,\n name: str(block.name) ?? \"tool\",\n input: block.input ?? null,\n });\n }\n }\n return items;\n }\n\n function pushUser(entry: Json): TranscriptItem[] {\n if (entry.isMeta === true || entry.isCompactSummary === true) return [];\n const message = isObject(entry.message) ? entry.message : null;\n if (!message) return [];\n const at = str(entry.timestamp);\n const content = message.content;\n if (typeof content === \"string\") {\n if (isSyntheticClaudeUserText(content) || content.trim().length === 0) return [];\n userCounter += 1;\n // The agent block is how the CLI was told, not the person's words.\n return [{ kind: \"user\", id: str(entry.uuid) ?? `user-${userCounter}`, text: stripAgentBlock(content), at }];\n }\n if (!Array.isArray(content)) return [];\n const items: TranscriptItem[] = [];\n const typed: string[] = [];\n for (const block of content) {\n if (!isObject(block)) continue;\n if (block.type === \"tool_result\") {\n items.push({\n kind: \"tool-result\",\n id: str(block.tool_use_id) ?? \"unknown\",\n output: toolResultText(block.content),\n isError: block.is_error === true,\n });\n } else if (block.type === \"text\") {\n const text = str(block.text) ?? \"\";\n if (text.trim().length > 0 && !isSyntheticClaudeUserText(text)) typed.push(text);\n }\n }\n if (typed.length > 0) {\n userCounter += 1;\n items.unshift({\n kind: \"user\",\n id: str(entry.uuid) ?? `user-${userCounter}`,\n text: stripAgentBlock(typed.join(\"\\n\\n\")),\n at,\n });\n }\n return items;\n }\n\n function pushStreamEvent(entry: Json): TranscriptItem[] {\n const event = isObject(entry.event) ? entry.event : null;\n if (!event) return [];\n if (event.type === \"message_start\" && isObject(event.message)) {\n streamingMessageId = str(event.message.id);\n return [];\n }\n if (event.type === \"content_block_delta\" && streamingMessageId !== null) {\n const delta = isObject(event.delta) ? event.delta : null;\n if (delta?.type === \"text_delta\") {\n const text = str(delta.text) ?? \"\";\n if (text.length === 0) return [];\n return [{ kind: \"text-delta\", id: `${streamingMessageId}:${num(event.index)}`, text }];\n }\n }\n return [];\n }\n\n function pushResult(entry: Json): TranscriptItem[] {\n const items: TranscriptItem[] = [];\n const usage = isObject(entry.usage) ? entry.usage : null;\n if (usage) {\n const runUsage: RunUsage = {\n inputTokens: num(usage.input_tokens) + num(usage.cache_creation_input_tokens),\n outputTokens: num(usage.output_tokens),\n cachedInputTokens: num(usage.cache_read_input_tokens),\n };\n const cost = typeof entry.total_cost_usd === \"number\" ? entry.total_cost_usd : null;\n items.push({ kind: \"usage\", usage: runUsage, costUsd: cost });\n }\n if (entry.is_error === true || (typeof entry.subtype === \"string\" && entry.subtype !== \"success\")) {\n const text =\n str(entry.result) ??\n (Array.isArray(entry.errors) ? entry.errors.map(String).join(\"\\n\") : null) ??\n `Claude Code stopped with \"${String(entry.subtype)}\".`;\n noticeCounter += 1;\n items.push({ kind: \"error\", id: `claude-result-${noticeCounter}`, text });\n }\n return items;\n }\n\n return {\n get sessionId() {\n return sessionId;\n },\n push(raw: unknown): TranscriptItem[] {\n if (!isObject(raw)) return [];\n const lineSession = str(raw.session_id) ?? str(raw.sessionId);\n if (lineSession && sessionId === null) sessionId = lineSession;\n if (raw.isSidechain === true) return [];\n switch (raw.type) {\n case \"assistant\":\n return pushAssistant(raw);\n case \"user\":\n return pushUser(raw);\n case \"stream_event\":\n return pushStreamEvent(raw);\n case \"result\":\n return pushResult(raw);\n case \"system\":\n if (raw.subtype === \"compact_boundary\") {\n noticeCounter += 1;\n return [\n {\n kind: \"notice\",\n id: `claude-compact-${noticeCounter}`,\n level: \"info\",\n text: \"Claude Code summarized the earlier part of this conversation to make room.\",\n },\n ];\n }\n return [];\n default:\n return [];\n }\n },\n };\n}\n","/**\n * THE ONE TITLE RULE for a coding chat, shared by every reader (Matrx 2's Coding Sessions and\n * Code › Chats, the standalone host, search). Pure: no file access, browser-safe.\n *\n * What a chat is called is what the person sees in the coding app, so the app's own record leads;\n * the CLI's transcript entries fill in for chats the app never listed. The person's rename inside\n * Matrx sits on top and is applied ONCE, by whoever draws the row (never baked into the engine's\n * own `title`). Plan: common-docs/projects/matrx-2/apps-chats.md §12.\n */\n\nimport { createClaudeNormalizer } from \"../normalize/claude\";\n\n/** Every title is cut to this (the app tables' rule); the screen truncates further visually. */\nexport const TITLE_MAX = 300;\n/** What a row shows when no source has a title. */\nexport const UNTITLED = \"(untitled)\";\n\n/**\n * The name the AI Matrx coding-session bridge used to stamp on every mirrored Claude Code prompt\n * (`sessionTitle` from its UserPromptSubmit hook; stopped at the source 2026-10-06, still inside\n * old transcripts as `custom-title` / `agent-name` entries). Never a chat's title.\n */\nexport const BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;\n\n/** Where a title came from, strongest first. */\nexport const TITLE_PRECEDENCE = [\n /** The person's rename inside Matrx (Coding Sessions' preference). */\n \"override\",\n /** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */\n \"app\",\n /** The title the app last showed, remembered by the claude-accounts engine after the record went away. */\n \"app-remembered\",\n /** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */\n \"custom-title\",\n \"ai-title\",\n \"agent-name\",\n \"summary\",\n /** The person's first typed message (Codex: the thread's title / first user message). */\n \"first-prompt\",\n \"last-prompt\",\n] as const;\n\nexport type TitleSource = (typeof TITLE_PRECEDENCE)[number];\n\n/** Whatever each source says; absent, null and blank all mean \"no title here\". */\nexport type TitleSources = Partial<Record<TitleSource, string | null | undefined>>;\n\nexport interface ResolvedTitle {\n title: string;\n source: TitleSource;\n}\n\n/** Transcript-only sources the bridge placeholder can poison. */\nconst STAMPABLE: ReadonlySet<TitleSource> = new Set([\"custom-title\", \"ai-title\", \"agent-name\"]);\n\nexport function isBridgePlaceholder(value: unknown): boolean {\n return typeof value === \"string\" && BRIDGE_PLACEHOLDER.test(value.trim());\n}\n\n/** The first non-empty line, trimmed, cut to `TITLE_MAX` with \"…\"; null when there is none. */\nexport function cleanTitle(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const line = value\n .split(/\\r?\\n/)\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n if (!line) return null;\n return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}…` : line;\n}\n\n/** The title to show and where it came from: the first source in `TITLE_PRECEDENCE` that has one. */\nexport function resolveSessionTitle(sources: TitleSources): ResolvedTitle | null {\n for (const source of TITLE_PRECEDENCE) {\n const raw = sources[source];\n if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;\n const title = cleanTitle(raw);\n if (title) return { title, source };\n }\n return null;\n}\n\n/**\n * Claude Code's own title entries from transcript lines (already parsed JSON), plus the person's\n * first typed message from `head` (the same normalizer the transcript screen uses). Later entries\n * win within a kind; a bridge placeholder never replaces a real one.\n */\nexport function claudeTranscriptTitleSources(head: Iterable<unknown>, tail: Iterable<unknown> = []): TitleSources {\n const out: TitleSources = {};\n const take = (source: TitleSource, value: unknown) => {\n if (typeof value !== \"string\" || !value.trim()) return;\n if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;\n out[source] = value;\n };\n const normalizer = createClaudeNormalizer();\n let firstPrompt: string | null = null;\n const scan = (entries: Iterable<unknown>, findPrompt: boolean) => {\n for (const entry of entries) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const e = entry as Record<string, unknown>;\n if (e.type === \"custom-title\") take(\"custom-title\", e.customTitle);\n else if (e.type === \"ai-title\") take(\"ai-title\", e.aiTitle);\n else if (e.type === \"agent-name\") take(\"agent-name\", e.agentName);\n else if (e.type === \"summary\") take(\"summary\", e.summary);\n else if (e.type === \"last-prompt\") take(\"last-prompt\", e.lastPrompt);\n else if (findPrompt && firstPrompt === null) {\n for (const item of normalizer.push(entry)) {\n if (item.kind === \"user\" && item.text.trim()) {\n firstPrompt = item.text;\n break;\n }\n }\n }\n }\n };\n scan(head, true);\n scan(tail, false);\n if (firstPrompt !== null) out[\"first-prompt\"] = firstPrompt;\n return out;\n}\n","/**\n * TITLE HINTS — the port through which the owner of the coding apps' index (Matrx 2's core, which\n * reads Claude's records, Codex's database and the rest through its own `fs`/`exec` owners) tells\n * this package what each chat is called and whether the app hides it. The package never walks the\n * apps' records itself: it reads transcripts (titles by the transcript rules, content search,\n * activity) and overlays these hints as they arrive. Pure: browser-safe types and helpers.\n */\n\nimport type { TitleSource } from \"./titles\";\n\nexport type HintProvider = \"claude\" | \"codex\";\n\nexport interface TitleHint {\n /** The app's title (pre-override: the person's rename is applied by whoever draws the row). */\n title: string;\n source: TitleSource;\n /** The app hides it: archived in the app (shown behind a toggle). */\n archived?: boolean | undefined;\n /** A scheduled or automated run (shown behind a toggle). */\n scheduled?: boolean | undefined;\n /** Listed by the app but its conversation file is gone: why (it is shown, never dropped). */\n unavailable?: string | null | undefined;\n /** Last activity the app records (ms), for chats only the app knows. */\n lastActiveMs?: number | null | undefined;\n}\n\nexport interface TitleHints {\n /** The hints now, keyed `titleHintKey()`; null while the owner's index is still loading. Never waits. */\n current(): ReadonlyMap<string, TitleHint> | null;\n /** Called after the hints change (a rename in the app, a new chat, the first load). */\n subscribe(listener: () => void): () => void;\n}\n\nexport function titleHintKey(provider: HintProvider, sessionId: string): string {\n return `${provider}:${sessionId}`;\n}\n\n/** A hints port the host fills by hand (tests, or an owner that pushes whole snapshots). */\nexport function createTitleHintsStore(): TitleHints & { set(hints: ReadonlyMap<string, TitleHint> | null): void } {\n let hints: ReadonlyMap<string, TitleHint> | null = null;\n const listeners = new Set<() => void>();\n return {\n current: () => hints,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n set(next) {\n hints = next;\n for (const l of listeners) l();\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/normalize/agent-block.ts","../../src/normalize/claude.ts","../../src/sessions/titles.ts","../../src/sessions/hints.ts"],"sourcesContent":["/**\n * The delimited AI Matrx agent block a coding session puts in front of a\n * person's message when a CLI must be TOLD about the agent (a Codex takeover,\n * an agent switch, after a compaction). Pure: the transcript strips it so the\n * screen shows what the person wrote, not how the CLI was told.\n */\n\n/** The marker every agent text we hand a CLI starts with. */\nexport const AGENT_BLOCK_MARKER = \"<ai_matrx_agent \";\n\n/**\n * Remove a leading agent block from a user message, so the transcript shows\n * what the PERSON wrote (the block is how the CLI was told, not their words).\n */\nexport function stripAgentBlock(text: string): string {\n const removed = text.match(/^<ai_matrx_agent_removed>[\\s\\S]*?<\\/ai_matrx_agent_removed>\\s*/);\n if (removed) return text.slice(removed[0].length);\n if (!text.startsWith(AGENT_BLOCK_MARKER)) return text;\n const end = text.indexOf(\"</ai_matrx_agent>\");\n if (end < 0) return text;\n let rest = text.slice(end + \"</ai_matrx_agent>\".length);\n // The Codex skills list rides after the block.\n const skills = rest.match(/^\\s*<skills_instructions>[\\s\\S]*?<\\/skills_instructions>/);\n if (skills) rest = rest.slice(skills[0].length);\n return rest.replace(/^\\s+/, \"\");\n}\n","/**\n * Claude Code → `TranscriptItem`.\n *\n * ONE normalizer reads both of Claude's shapes, because they are the same\n * shape: `claude --print --output-format stream-json` lines (live) and the\n * entries of the user's own session file `~/.claude/projects/<cwd>/<id>.jsonl`\n * (history) both carry `{ type: \"assistant\" | \"user\", message: { content } }`.\n * The live stream adds `system`/`stream_event`/`result`; the file adds its own\n * bookkeeping entries (titles, attachments, snapshots), which are skipped.\n *\n * Block identity: an assistant message arrives as several lines sharing one\n * `message.id`, one content block per line, in block order — the same order\n * the partial-message `content_block_start.index` counts. So a text block's id\n * is `<message id>:<index>` on both paths, and a streamed draft is replaced by\n * its final text rather than duplicated.\n */\n\nimport type { RunUsage, TranscriptItem } from \"../types\";\nimport { stripAgentBlock } from \"./agent-block\";\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction num(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/** User-role text Claude Code writes that the person never typed. */\nconst SYNTHETIC_USER_PREFIXES = [\n \"<command-name>\",\n \"<command-message>\",\n \"<command-args>\",\n \"<local-command-stdout>\",\n \"<local-command-stderr>\",\n \"<local-command-caveat>\",\n \"<system-reminder>\",\n \"<bash-input>\",\n \"<bash-stdout>\",\n \"<bash-stderr>\",\n \"<user-memory-input>\",\n \"<task-notification>\",\n \"Caveat: The messages below were generated by the user while running local commands\",\n \"[Request interrupted by user\",\n];\n\nexport function isSyntheticClaudeUserText(text: string): boolean {\n const trimmed = text.trimStart();\n return SYNTHETIC_USER_PREFIXES.some((prefix) => trimmed.startsWith(prefix));\n}\n\nfunction toolResultText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((block) => {\n if (!isObject(block)) return \"\";\n if (block.type === \"text\") return str(block.text) ?? \"\";\n if (block.type === \"image\") return \"[image]\";\n return \"\";\n })\n .filter(Boolean)\n .join(\"\\n\");\n }\n return content === undefined || content === null ? \"\" : JSON.stringify(content);\n}\n\nexport interface ClaudeNormalizer {\n /** Feed one parsed JSON line; returns the items it produced (often none). */\n push(entry: unknown): TranscriptItem[];\n /** The Claude session id, once a line has carried it. */\n readonly sessionId: string | null;\n}\n\nexport function createClaudeNormalizer(): ClaudeNormalizer {\n let sessionId: string | null = null;\n let streamingMessageId: string | null = null;\n const blockCounters = new Map<string, number>();\n let userCounter = 0;\n let noticeCounter = 0;\n\n function nextBlockIndex(messageId: string): number {\n const index = blockCounters.get(messageId) ?? 0;\n blockCounters.set(messageId, index + 1);\n return index;\n }\n\n function pushAssistant(entry: Json): TranscriptItem[] {\n const message = isObject(entry.message) ? entry.message : null;\n if (!message) return [];\n const messageId = str(message.id) ?? str(entry.uuid) ?? `assistant-${blockCounters.size}`;\n const content = Array.isArray(message.content) ? message.content : [];\n const items: TranscriptItem[] = [];\n for (const block of content) {\n if (!isObject(block)) continue;\n const index = nextBlockIndex(messageId);\n const id = `${messageId}:${index}`;\n if (block.type === \"text\") {\n const text = str(block.text) ?? \"\";\n if (text.length > 0) items.push({ kind: \"text\", id, text });\n } else if (block.type === \"thinking\") {\n // Claude Code writes thinking blocks with an EMPTY text and only a\n // signature unless thinking display is on; an empty aside is noise.\n const text = str(block.thinking) ?? \"\";\n if (text.trim().length > 0) items.push({ kind: \"reasoning\", id, text });\n } else if (block.type === \"tool_use\" || block.type === \"server_tool_use\") {\n items.push({\n kind: \"tool-call\",\n id: str(block.id) ?? id,\n name: str(block.name) ?? \"tool\",\n input: block.input ?? null,\n });\n }\n }\n return items;\n }\n\n function pushUser(entry: Json): TranscriptItem[] {\n if (entry.isMeta === true || entry.isCompactSummary === true) return [];\n const message = isObject(entry.message) ? entry.message : null;\n if (!message) return [];\n const at = str(entry.timestamp);\n const content = message.content;\n if (typeof content === \"string\") {\n if (isSyntheticClaudeUserText(content) || content.trim().length === 0) return [];\n userCounter += 1;\n // The agent block is how the CLI was told, not the person's words.\n return [{ kind: \"user\", id: str(entry.uuid) ?? `user-${userCounter}`, text: stripAgentBlock(content), at }];\n }\n if (!Array.isArray(content)) return [];\n const items: TranscriptItem[] = [];\n const typed: string[] = [];\n for (const block of content) {\n if (!isObject(block)) continue;\n if (block.type === \"tool_result\") {\n items.push({\n kind: \"tool-result\",\n id: str(block.tool_use_id) ?? \"unknown\",\n output: toolResultText(block.content),\n isError: block.is_error === true,\n });\n } else if (block.type === \"text\") {\n const text = str(block.text) ?? \"\";\n if (text.trim().length > 0 && !isSyntheticClaudeUserText(text)) typed.push(text);\n }\n }\n if (typed.length > 0) {\n userCounter += 1;\n items.unshift({\n kind: \"user\",\n id: str(entry.uuid) ?? `user-${userCounter}`,\n text: stripAgentBlock(typed.join(\"\\n\\n\")),\n at,\n });\n }\n return items;\n }\n\n function pushStreamEvent(entry: Json): TranscriptItem[] {\n const event = isObject(entry.event) ? entry.event : null;\n if (!event) return [];\n if (event.type === \"message_start\" && isObject(event.message)) {\n streamingMessageId = str(event.message.id);\n return [];\n }\n if (event.type === \"content_block_delta\" && streamingMessageId !== null) {\n const delta = isObject(event.delta) ? event.delta : null;\n if (delta?.type === \"text_delta\") {\n const text = str(delta.text) ?? \"\";\n if (text.length === 0) return [];\n return [{ kind: \"text-delta\", id: `${streamingMessageId}:${num(event.index)}`, text }];\n }\n }\n return [];\n }\n\n function pushResult(entry: Json): TranscriptItem[] {\n const items: TranscriptItem[] = [];\n const usage = isObject(entry.usage) ? entry.usage : null;\n if (usage) {\n const runUsage: RunUsage = {\n inputTokens: num(usage.input_tokens) + num(usage.cache_creation_input_tokens),\n outputTokens: num(usage.output_tokens),\n cachedInputTokens: num(usage.cache_read_input_tokens),\n };\n const cost = typeof entry.total_cost_usd === \"number\" ? entry.total_cost_usd : null;\n items.push({ kind: \"usage\", usage: runUsage, costUsd: cost });\n }\n if (entry.is_error === true || (typeof entry.subtype === \"string\" && entry.subtype !== \"success\")) {\n const text =\n str(entry.result) ??\n (Array.isArray(entry.errors) ? entry.errors.map(String).join(\"\\n\") : null) ??\n `Claude Code stopped with \"${String(entry.subtype)}\".`;\n noticeCounter += 1;\n items.push({ kind: \"error\", id: `claude-result-${noticeCounter}`, text });\n }\n return items;\n }\n\n return {\n get sessionId() {\n return sessionId;\n },\n push(raw: unknown): TranscriptItem[] {\n if (!isObject(raw)) return [];\n const lineSession = str(raw.session_id) ?? str(raw.sessionId);\n if (lineSession && sessionId === null) sessionId = lineSession;\n if (raw.isSidechain === true) return [];\n switch (raw.type) {\n case \"assistant\":\n return pushAssistant(raw);\n case \"user\":\n return pushUser(raw);\n case \"stream_event\":\n return pushStreamEvent(raw);\n case \"result\":\n return pushResult(raw);\n case \"system\":\n if (raw.subtype === \"compact_boundary\") {\n noticeCounter += 1;\n return [\n {\n kind: \"notice\",\n id: `claude-compact-${noticeCounter}`,\n level: \"info\",\n text: \"Claude Code summarized the earlier part of this conversation to make room.\",\n },\n ];\n }\n return [];\n default:\n return [];\n }\n },\n };\n}\n","/**\n * THE ONE TITLE RULE for a coding chat, shared by every reader (Matrx 2's Coding Sessions and\n * Code › Chats, the standalone host, search). Pure: no file access, browser-safe.\n *\n * What a chat is called is what the person sees in the coding app, so the app's own record leads;\n * the CLI's transcript entries fill in for chats the app never listed. The person's rename inside\n * Matrx sits on top and is applied ONCE, by whoever draws the row (never baked into the engine's\n * own `title`). Plan: common-docs/projects/matrx-2/apps-chats.md §12.\n */\n\nimport { createClaudeNormalizer } from \"../normalize/claude\";\n\n/** Every title is cut to this (the app tables' rule); the screen truncates further visually. */\nexport const TITLE_MAX = 300;\n/** What a row shows when no source has a title. */\nexport const UNTITLED = \"(untitled)\";\n\n/**\n * The name the AI Matrx coding-session bridge used to stamp on every mirrored Claude Code prompt\n * (`sessionTitle` from its UserPromptSubmit hook; stopped at the source 2026-10-06, still inside\n * old transcripts as `custom-title` / `agent-name` entries). Never a chat's title.\n */\nexport const BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;\n\n/** Where a title came from, strongest first. */\nexport const TITLE_PRECEDENCE = [\n /** The person's rename inside Matrx (Coding Sessions' preference). */\n \"override\",\n /** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */\n \"app\",\n /** The title the app last showed, remembered by the claude-accounts engine after the record went away. */\n \"app-remembered\",\n /** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */\n \"custom-title\",\n \"ai-title\",\n \"agent-name\",\n \"summary\",\n /** The person's first typed message (Codex: the thread's title / first user message). */\n \"first-prompt\",\n \"last-prompt\",\n] as const;\n\nexport type TitleSource = (typeof TITLE_PRECEDENCE)[number];\n\n/** Whatever each source says; absent, null and blank all mean \"no title here\". */\nexport type TitleSources = Partial<Record<TitleSource, string | null | undefined>>;\n\nexport interface ResolvedTitle {\n title: string;\n source: TitleSource;\n}\n\n/** Transcript-only sources the bridge placeholder can poison. */\nconst STAMPABLE: ReadonlySet<TitleSource> = new Set([\"custom-title\", \"ai-title\", \"agent-name\"]);\n\nexport function isBridgePlaceholder(value: unknown): boolean {\n return typeof value === \"string\" && BRIDGE_PLACEHOLDER.test(value.trim());\n}\n\n/** The first non-empty line, trimmed, cut to `TITLE_MAX` with \"…\"; null when there is none. */\nexport function cleanTitle(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const line = value\n .split(/\\r?\\n/)\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n if (!line) return null;\n return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}…` : line;\n}\n\n/** The longest title a list shows in full (the stored title stays exact; the full text goes on hover). */\nexport const DISPLAY_MAX = 100;\n\n/** A list marker, heading mark or opening quote a pasted first line starts with (\"1) \", \"- \", \"# \", \"> \", \"“\"). */\nconst LEADING_NOISE = /^(?:\\s*(?:\\d{1,3}[.)]|[-*•]|#{1,6}|>)\\s+|\\s*[\"“'‘`]+)+/;\n\n/**\n * How a list draws a title: leading list markers and quotes dropped, cut to `max` with \"…\". Only for\n * display — the stored title (and search, and the hover text) stays exactly `title`.\n */\nexport function displayTitle(title: string, max: number = DISPLAY_MAX): string {\n const stripped = title.replace(LEADING_NOISE, \"\").replace(/[\"”'’`]+$/, \"\").trim() || title.trim();\n return stripped.length > max ? `${stripped.slice(0, max - 1).trimEnd()}…` : stripped;\n}\n\n/** The title to show and where it came from: the first source in `TITLE_PRECEDENCE` that has one. */\nexport function resolveSessionTitle(sources: TitleSources): ResolvedTitle | null {\n for (const source of TITLE_PRECEDENCE) {\n const raw = sources[source];\n if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;\n const title = cleanTitle(raw);\n if (title) return { title, source };\n }\n return null;\n}\n\n/**\n * Claude Code's own title entries from transcript lines (already parsed JSON), plus the person's\n * first typed message from `head` (the same normalizer the transcript screen uses). Later entries\n * win within a kind; a bridge placeholder never replaces a real one.\n */\nexport function claudeTranscriptTitleSources(head: Iterable<unknown>, tail: Iterable<unknown> = []): TitleSources {\n const out: TitleSources = {};\n const take = (source: TitleSource, value: unknown) => {\n if (typeof value !== \"string\" || !value.trim()) return;\n if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;\n out[source] = value;\n };\n const normalizer = createClaudeNormalizer();\n let firstPrompt: string | null = null;\n const scan = (entries: Iterable<unknown>, findPrompt: boolean) => {\n for (const entry of entries) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const e = entry as Record<string, unknown>;\n if (e.type === \"custom-title\") take(\"custom-title\", e.customTitle);\n else if (e.type === \"ai-title\") take(\"ai-title\", e.aiTitle);\n else if (e.type === \"agent-name\") take(\"agent-name\", e.agentName);\n else if (e.type === \"summary\") take(\"summary\", e.summary);\n else if (e.type === \"last-prompt\") take(\"last-prompt\", e.lastPrompt);\n else if (findPrompt && firstPrompt === null) {\n for (const item of normalizer.push(entry)) {\n if (item.kind === \"user\" && item.text.trim()) {\n firstPrompt = item.text;\n break;\n }\n }\n }\n }\n };\n scan(head, true);\n scan(tail, false);\n if (firstPrompt !== null) out[\"first-prompt\"] = firstPrompt;\n return out;\n}\n","/**\n * TITLE HINTS — the port through which the owner of the coding apps' index (Matrx 2's core, which\n * reads Claude's records, Codex's database and the rest through its own `fs`/`exec` owners) tells\n * this package what each chat is called and whether the app hides it. The package never walks the\n * apps' records itself: it reads transcripts (titles by the transcript rules, content search,\n * activity) and overlays these hints as they arrive. Pure: browser-safe types and helpers.\n */\n\nimport type { TitleSource } from \"./titles\";\n\nexport type HintProvider = \"claude\" | \"codex\";\n\nexport interface TitleHint {\n /** The app's title (pre-override: the person's rename is applied by whoever draws the row). */\n title: string;\n /** Where the owner's title came from; \"unknown\" when the owner did not say (shown as such, never guessed). */\n source: TitleSource | \"unknown\";\n /** The app hides it: archived in the app (shown behind a toggle). */\n archived?: boolean | undefined;\n /** A scheduled or automated run (shown behind a toggle). */\n scheduled?: boolean | undefined;\n /** Listed by the app but its conversation file is gone: why (it is shown, never dropped). */\n unavailable?: string | null | undefined;\n /** Last activity the app records (ms), for chats only the app knows. */\n lastActiveMs?: number | null | undefined;\n}\n\nexport interface TitleHints {\n /** The hints now, keyed `titleHintKey()`; null while the owner's index is still loading. Never waits. */\n current(): ReadonlyMap<string, TitleHint> | null;\n /** Called after the hints change (a rename in the app, a new chat, the first load, a failure). */\n subscribe(listener: () => void): () => void;\n /** Why the apps' titles could not be had right now (shown with a Retry), or null. */\n problem?(): string | null;\n /** Ask the owner again now (the Retry). */\n retry?(): void;\n}\n\nexport function titleHintKey(provider: HintProvider, sessionId: string): string {\n return `${provider}:${sessionId}`;\n}\n\n/** A hints port the host fills by hand (tests, or an owner that pushes whole snapshots). */\nexport function createTitleHintsStore(): TitleHints & {\n set(hints: ReadonlyMap<string, TitleHint> | null): void;\n setProblem(problem: string | null): void;\n} {\n let hints: ReadonlyMap<string, TitleHint> | null = null;\n let problem: string | null = null;\n const listeners = new Set<() => void>();\n const tell = () => {\n for (const l of listeners) l();\n };\n return {\n current: () => hints,\n problem: () => problem,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n set(next) {\n hints = next;\n problem = null;\n tell();\n },\n setProblem(next) {\n if (next === problem) return;\n problem = next;\n tell();\n },\n };\n}\n\n/** The app titles a hints map carries, keyed `provider:id` (for title search and Codex names). */\nexport function appTitlesOf(hints: ReadonlyMap<string, TitleHint>): Map<string, string> {\n const out = new Map<string, string>();\n for (const [key, hint] of hints) out.set(key, hint.title);\n return out;\n}\n"],"mappings":";AAQO,IAAM,qBAAqB;AAM3B,SAAS,gBAAgB,MAAsB;AACpD,QAAM,UAAU,KAAK,MAAM,gEAAgE;AAC3F,MAAI,QAAS,QAAO,KAAK,MAAM,QAAQ,CAAC,EAAE,MAAM;AAChD,MAAI,CAAC,KAAK,WAAW,kBAAkB,EAAG,QAAO;AACjD,QAAM,MAAM,KAAK,QAAQ,mBAAmB;AAC5C,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,OAAO,KAAK,MAAM,MAAM,oBAAoB,MAAM;AAEtD,QAAM,SAAS,KAAK,MAAM,0DAA0D;AACpF,MAAI,OAAQ,QAAO,KAAK,MAAM,OAAO,CAAC,EAAE,MAAM;AAC9C,SAAO,KAAK,QAAQ,QAAQ,EAAE;AAChC;;;ACHA,SAAS,SAAS,OAA+B;AAC/C,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,IAAI,OAA+B;AAC1C,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEA,SAAS,IAAI,OAAwB;AACnC,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE;AAGA,IAAM,0BAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,0BAA0B,MAAuB;AAC/D,QAAM,UAAU,KAAK,UAAU;AAC/B,SAAO,wBAAwB,KAAK,CAAC,WAAW,QAAQ,WAAW,MAAM,CAAC;AAC5E;AAEA,SAAS,eAAe,SAA0B;AAChD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,WAAO,QACJ,IAAI,CAAC,UAAU;AACd,UAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,UAAI,MAAM,SAAS,OAAQ,QAAO,IAAI,MAAM,IAAI,KAAK;AACrD,UAAI,MAAM,SAAS,QAAS,QAAO;AACnC,aAAO;AAAA,IACT,CAAC,EACA,OAAO,OAAO,EACd,KAAK,IAAI;AAAA,EACd;AACA,SAAO,YAAY,UAAa,YAAY,OAAO,KAAK,KAAK,UAAU,OAAO;AAChF;AASO,SAAS,yBAA2C;AACzD,MAAI,YAA2B;AAC/B,MAAI,qBAAoC;AACxC,QAAM,gBAAgB,oBAAI,IAAoB;AAC9C,MAAI,cAAc;AAClB,MAAI,gBAAgB;AAEpB,WAAS,eAAe,WAA2B;AACjD,UAAM,QAAQ,cAAc,IAAI,SAAS,KAAK;AAC9C,kBAAc,IAAI,WAAW,QAAQ,CAAC;AACtC,WAAO;AAAA,EACT;AAEA,WAAS,cAAc,OAA+B;AACpD,UAAM,UAAU,SAAS,MAAM,OAAO,IAAI,MAAM,UAAU;AAC1D,QAAI,CAAC,QAAS,QAAO,CAAC;AACtB,UAAM,YAAY,IAAI,QAAQ,EAAE,KAAK,IAAI,MAAM,IAAI,KAAK,aAAa,cAAc,IAAI;AACvF,UAAM,UAAU,MAAM,QAAQ,QAAQ,OAAO,IAAI,QAAQ,UAAU,CAAC;AACpE,UAAM,QAA0B,CAAC;AACjC,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,SAAS,KAAK,EAAG;AACtB,YAAM,QAAQ,eAAe,SAAS;AACtC,YAAM,KAAK,GAAG,SAAS,IAAI,KAAK;AAChC,UAAI,MAAM,SAAS,QAAQ;AACzB,cAAM,OAAO,IAAI,MAAM,IAAI,KAAK;AAChC,YAAI,KAAK,SAAS,EAAG,OAAM,KAAK,EAAE,MAAM,QAAQ,IAAI,KAAK,CAAC;AAAA,MAC5D,WAAW,MAAM,SAAS,YAAY;AAGpC,cAAM,OAAO,IAAI,MAAM,QAAQ,KAAK;AACpC,YAAI,KAAK,KAAK,EAAE,SAAS,EAAG,OAAM,KAAK,EAAE,MAAM,aAAa,IAAI,KAAK,CAAC;AAAA,MACxE,WAAW,MAAM,SAAS,cAAc,MAAM,SAAS,mBAAmB;AACxE,cAAM,KAAK;AAAA,UACT,MAAM;AAAA,UACN,IAAI,IAAI,MAAM,EAAE,KAAK;AAAA,UACrB,MAAM,IAAI,MAAM,IAAI,KAAK;AAAA,UACzB,OAAO,MAAM,SAAS;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,WAAS,SAAS,OAA+B;AAC/C,QAAI,MAAM,WAAW,QAAQ,MAAM,qBAAqB,KAAM,QAAO,CAAC;AACtE,UAAM,UAAU,SAAS,MAAM,OAAO,IAAI,MAAM,UAAU;AAC1D,QAAI,CAAC,QAAS,QAAO,CAAC;AACtB,UAAM,KAAK,IAAI,MAAM,SAAS;AAC9B,UAAM,UAAU,QAAQ;AACxB,QAAI,OAAO,YAAY,UAAU;AAC/B,UAAI,0BAA0B,OAAO,KAAK,QAAQ,KAAK,EAAE,WAAW,EAAG,QAAO,CAAC;AAC/E,qBAAe;AAEf,aAAO,CAAC,EAAE,MAAM,QAAQ,IAAI,IAAI,MAAM,IAAI,KAAK,QAAQ,WAAW,IAAI,MAAM,gBAAgB,OAAO,GAAG,GAAG,CAAC;AAAA,IAC5G;AACA,QAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO,CAAC;AACrC,UAAM,QAA0B,CAAC;AACjC,UAAM,QAAkB,CAAC;AACzB,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,SAAS,KAAK,EAAG;AACtB,UAAI,MAAM,SAAS,eAAe;AAChC,cAAM,KAAK;AAAA,UACT,MAAM;AAAA,UACN,IAAI,IAAI,MAAM,WAAW,KAAK;AAAA,UAC9B,QAAQ,eAAe,MAAM,OAAO;AAAA,UACpC,SAAS,MAAM,aAAa;AAAA,QAC9B,CAAC;AAAA,MACH,WAAW,MAAM,SAAS,QAAQ;AAChC,cAAM,OAAO,IAAI,MAAM,IAAI,KAAK;AAChC,YAAI,KAAK,KAAK,EAAE,SAAS,KAAK,CAAC,0BAA0B,IAAI,EAAG,OAAM,KAAK,IAAI;AAAA,MACjF;AAAA,IACF;AACA,QAAI,MAAM,SAAS,GAAG;AACpB,qBAAe;AACf,YAAM,QAAQ;AAAA,QACZ,MAAM;AAAA,QACN,IAAI,IAAI,MAAM,IAAI,KAAK,QAAQ,WAAW;AAAA,QAC1C,MAAM,gBAAgB,MAAM,KAAK,MAAM,CAAC;AAAA,QACxC;AAAA,MACF,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAEA,WAAS,gBAAgB,OAA+B;AACtD,UAAM,QAAQ,SAAS,MAAM,KAAK,IAAI,MAAM,QAAQ;AACpD,QAAI,CAAC,MAAO,QAAO,CAAC;AACpB,QAAI,MAAM,SAAS,mBAAmB,SAAS,MAAM,OAAO,GAAG;AAC7D,2BAAqB,IAAI,MAAM,QAAQ,EAAE;AACzC,aAAO,CAAC;AAAA,IACV;AACA,QAAI,MAAM,SAAS,yBAAyB,uBAAuB,MAAM;AACvE,YAAM,QAAQ,SAAS,MAAM,KAAK,IAAI,MAAM,QAAQ;AACpD,UAAI,OAAO,SAAS,cAAc;AAChC,cAAM,OAAO,IAAI,MAAM,IAAI,KAAK;AAChC,YAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAC/B,eAAO,CAAC,EAAE,MAAM,cAAc,IAAI,GAAG,kBAAkB,IAAI,IAAI,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC;AAAA,MACvF;AAAA,IACF;AACA,WAAO,CAAC;AAAA,EACV;AAEA,WAAS,WAAW,OAA+B;AACjD,UAAM,QAA0B,CAAC;AACjC,UAAM,QAAQ,SAAS,MAAM,KAAK,IAAI,MAAM,QAAQ;AACpD,QAAI,OAAO;AACT,YAAM,WAAqB;AAAA,QACzB,aAAa,IAAI,MAAM,YAAY,IAAI,IAAI,MAAM,2BAA2B;AAAA,QAC5E,cAAc,IAAI,MAAM,aAAa;AAAA,QACrC,mBAAmB,IAAI,MAAM,uBAAuB;AAAA,MACtD;AACA,YAAM,OAAO,OAAO,MAAM,mBAAmB,WAAW,MAAM,iBAAiB;AAC/E,YAAM,KAAK,EAAE,MAAM,SAAS,OAAO,UAAU,SAAS,KAAK,CAAC;AAAA,IAC9D;AACA,QAAI,MAAM,aAAa,QAAS,OAAO,MAAM,YAAY,YAAY,MAAM,YAAY,WAAY;AACjG,YAAM,OACJ,IAAI,MAAM,MAAM,MACf,MAAM,QAAQ,MAAM,MAAM,IAAI,MAAM,OAAO,IAAI,MAAM,EAAE,KAAK,IAAI,IAAI,SACrE,6BAA6B,OAAO,MAAM,OAAO,CAAC;AACpD,uBAAiB;AACjB,YAAM,KAAK,EAAE,MAAM,SAAS,IAAI,iBAAiB,aAAa,IAAI,KAAK,CAAC;AAAA,IAC1E;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI,YAAY;AACd,aAAO;AAAA,IACT;AAAA,IACA,KAAK,KAAgC;AACnC,UAAI,CAAC,SAAS,GAAG,EAAG,QAAO,CAAC;AAC5B,YAAM,cAAc,IAAI,IAAI,UAAU,KAAK,IAAI,IAAI,SAAS;AAC5D,UAAI,eAAe,cAAc,KAAM,aAAY;AACnD,UAAI,IAAI,gBAAgB,KAAM,QAAO,CAAC;AACtC,cAAQ,IAAI,MAAM;AAAA,QAChB,KAAK;AACH,iBAAO,cAAc,GAAG;AAAA,QAC1B,KAAK;AACH,iBAAO,SAAS,GAAG;AAAA,QACrB,KAAK;AACH,iBAAO,gBAAgB,GAAG;AAAA,QAC5B,KAAK;AACH,iBAAO,WAAW,GAAG;AAAA,QACvB,KAAK;AACH,cAAI,IAAI,YAAY,oBAAoB;AACtC,6BAAiB;AACjB,mBAAO;AAAA,cACL;AAAA,gBACE,MAAM;AAAA,gBACN,IAAI,kBAAkB,aAAa;AAAA,gBACnC,OAAO;AAAA,gBACP,MAAM;AAAA,cACR;AAAA,YACF;AAAA,UACF;AACA,iBAAO,CAAC;AAAA,QACV;AACE,iBAAO,CAAC;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AACF;;;ACpOO,IAAM,YAAY;AAElB,IAAM,WAAW;AAOjB,IAAM,qBAAqB;AAG3B,IAAM,mBAAmB;AAAA;AAAA,EAE9B;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF;AAaA,IAAM,YAAsC,oBAAI,IAAI,CAAC,gBAAgB,YAAY,YAAY,CAAC;AAEvF,SAAS,oBAAoB,OAAyB;AAC3D,SAAO,OAAO,UAAU,YAAY,mBAAmB,KAAK,MAAM,KAAK,CAAC;AAC1E;AAGO,SAAS,WAAW,OAA+B;AACxD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,OAAO,MACV,MAAM,OAAO,EACb,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC;AAC3B,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,KAAK,SAAS,YAAY,GAAG,KAAK,MAAM,GAAG,YAAY,CAAC,CAAC,WAAM;AACxE;AAGO,IAAM,cAAc;AAG3B,IAAM,gBAAgB;AAMf,SAAS,aAAa,OAAe,MAAc,aAAqB;AAC7E,QAAM,WAAW,MAAM,QAAQ,eAAe,EAAE,EAAE,QAAQ,aAAa,EAAE,EAAE,KAAK,KAAK,MAAM,KAAK;AAChG,SAAO,SAAS,SAAS,MAAM,GAAG,SAAS,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,CAAC,WAAM;AAC9E;AAGO,SAAS,oBAAoB,SAA6C;AAC/E,aAAW,UAAU,kBAAkB;AACrC,UAAM,MAAM,QAAQ,MAAM;AAC1B,QAAI,UAAU,IAAI,MAAM,KAAK,oBAAoB,GAAG,EAAG;AACvD,UAAM,QAAQ,WAAW,GAAG;AAC5B,QAAI,MAAO,QAAO,EAAE,OAAO,OAAO;AAAA,EACpC;AACA,SAAO;AACT;AAOO,SAAS,6BAA6B,MAAyB,OAA0B,CAAC,GAAiB;AAChH,QAAM,MAAoB,CAAC;AAC3B,QAAM,OAAO,CAAC,QAAqB,UAAmB;AACpD,QAAI,OAAO,UAAU,YAAY,CAAC,MAAM,KAAK,EAAG;AAChD,QAAI,UAAU,IAAI,MAAM,KAAK,oBAAoB,KAAK,EAAG;AACzD,QAAI,MAAM,IAAI;AAAA,EAChB;AACA,QAAM,aAAa,uBAAuB;AAC1C,MAAI,cAA6B;AACjC,QAAM,OAAO,CAAC,SAA4B,eAAwB;AAChE,eAAW,SAAS,SAAS;AAC3B,UAAI,OAAO,UAAU,YAAY,UAAU,KAAM;AACjD,YAAM,IAAI;AACV,UAAI,EAAE,SAAS,eAAgB,MAAK,gBAAgB,EAAE,WAAW;AAAA,eACxD,EAAE,SAAS,WAAY,MAAK,YAAY,EAAE,OAAO;AAAA,eACjD,EAAE,SAAS,aAAc,MAAK,cAAc,EAAE,SAAS;AAAA,eACvD,EAAE,SAAS,UAAW,MAAK,WAAW,EAAE,OAAO;AAAA,eAC/C,EAAE,SAAS,cAAe,MAAK,eAAe,EAAE,UAAU;AAAA,eAC1D,cAAc,gBAAgB,MAAM;AAC3C,mBAAW,QAAQ,WAAW,KAAK,KAAK,GAAG;AACzC,cAAI,KAAK,SAAS,UAAU,KAAK,KAAK,KAAK,GAAG;AAC5C,0BAAc,KAAK;AACnB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,OAAK,MAAM,IAAI;AACf,OAAK,MAAM,KAAK;AAChB,MAAI,gBAAgB,KAAM,KAAI,cAAc,IAAI;AAChD,SAAO;AACT;;;AC/FO,SAAS,aAAa,UAAwB,WAA2B;AAC9E,SAAO,GAAG,QAAQ,IAAI,SAAS;AACjC;AAGO,SAAS,wBAGd;AACA,MAAI,QAA+C;AACnD,MAAI,UAAyB;AAC7B,QAAM,YAAY,oBAAI,IAAgB;AACtC,QAAM,OAAO,MAAM;AACjB,eAAW,KAAK,UAAW,GAAE;AAAA,EAC/B;AACA,SAAO;AAAA,IACL,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,UAAU,UAAU;AAClB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM,UAAU,OAAO,QAAQ;AAAA,IACxC;AAAA,IACA,IAAI,MAAM;AACR,cAAQ;AACR,gBAAU;AACV,WAAK;AAAA,IACP;AAAA,IACA,WAAW,MAAM;AACf,UAAI,SAAS,QAAS;AACtB,gBAAU;AACV,WAAK;AAAA,IACP;AAAA,EACF;AACF;AAGO,SAAS,YAAY,OAA4D;AACtF,QAAM,MAAM,oBAAI,IAAoB;AACpC,aAAW,CAAC,KAAK,IAAI,KAAK,MAAO,KAAI,IAAI,KAAK,KAAK,KAAK;AACxD,SAAO;AACT;","names":[]}
|
|
@@ -29,6 +29,13 @@ interface ResolvedTitle {
|
|
|
29
29
|
declare function isBridgePlaceholder(value: unknown): boolean;
|
|
30
30
|
/** The first non-empty line, trimmed, cut to `TITLE_MAX` with "…"; null when there is none. */
|
|
31
31
|
declare function cleanTitle(value: unknown): string | null;
|
|
32
|
+
/** The longest title a list shows in full (the stored title stays exact; the full text goes on hover). */
|
|
33
|
+
declare const DISPLAY_MAX = 100;
|
|
34
|
+
/**
|
|
35
|
+
* How a list draws a title: leading list markers and quotes dropped, cut to `max` with "…". Only for
|
|
36
|
+
* display — the stored title (and search, and the hover text) stays exactly `title`.
|
|
37
|
+
*/
|
|
38
|
+
declare function displayTitle(title: string, max?: number): string;
|
|
32
39
|
/** The title to show and where it came from: the first source in `TITLE_PRECEDENCE` that has one. */
|
|
33
40
|
declare function resolveSessionTitle(sources: TitleSources): ResolvedTitle | null;
|
|
34
41
|
/**
|
|
@@ -38,4 +45,4 @@ declare function resolveSessionTitle(sources: TitleSources): ResolvedTitle | nul
|
|
|
38
45
|
*/
|
|
39
46
|
declare function claudeTranscriptTitleSources(head: Iterable<unknown>, tail?: Iterable<unknown>): TitleSources;
|
|
40
47
|
|
|
41
|
-
export { BRIDGE_PLACEHOLDER as B, type ResolvedTitle as R, type TitleSource as T, UNTITLED as U, TITLE_MAX as a, TITLE_PRECEDENCE as b, type TitleSources as c, claudeTranscriptTitleSources as d, cleanTitle as e, isBridgePlaceholder as i, resolveSessionTitle as r };
|
|
48
|
+
export { BRIDGE_PLACEHOLDER as B, DISPLAY_MAX as D, type ResolvedTitle as R, type TitleSource as T, UNTITLED as U, TITLE_MAX as a, TITLE_PRECEDENCE as b, type TitleSources as c, claudeTranscriptTitleSources as d, cleanTitle as e, displayTitle as f, isBridgePlaceholder as i, resolveSessionTitle as r };
|
|
@@ -29,6 +29,13 @@ interface ResolvedTitle {
|
|
|
29
29
|
declare function isBridgePlaceholder(value: unknown): boolean;
|
|
30
30
|
/** The first non-empty line, trimmed, cut to `TITLE_MAX` with "…"; null when there is none. */
|
|
31
31
|
declare function cleanTitle(value: unknown): string | null;
|
|
32
|
+
/** The longest title a list shows in full (the stored title stays exact; the full text goes on hover). */
|
|
33
|
+
declare const DISPLAY_MAX = 100;
|
|
34
|
+
/**
|
|
35
|
+
* How a list draws a title: leading list markers and quotes dropped, cut to `max` with "…". Only for
|
|
36
|
+
* display — the stored title (and search, and the hover text) stays exactly `title`.
|
|
37
|
+
*/
|
|
38
|
+
declare function displayTitle(title: string, max?: number): string;
|
|
32
39
|
/** The title to show and where it came from: the first source in `TITLE_PRECEDENCE` that has one. */
|
|
33
40
|
declare function resolveSessionTitle(sources: TitleSources): ResolvedTitle | null;
|
|
34
41
|
/**
|
|
@@ -38,4 +45,4 @@ declare function resolveSessionTitle(sources: TitleSources): ResolvedTitle | nul
|
|
|
38
45
|
*/
|
|
39
46
|
declare function claudeTranscriptTitleSources(head: Iterable<unknown>, tail?: Iterable<unknown>): TitleSources;
|
|
40
47
|
|
|
41
|
-
export { BRIDGE_PLACEHOLDER as B, type ResolvedTitle as R, type TitleSource as T, UNTITLED as U, TITLE_MAX as a, TITLE_PRECEDENCE as b, type TitleSources as c, claudeTranscriptTitleSources as d, cleanTitle as e, isBridgePlaceholder as i, resolveSessionTitle as r };
|
|
48
|
+
export { BRIDGE_PLACEHOLDER as B, DISPLAY_MAX as D, type ResolvedTitle as R, type TitleSource as T, UNTITLED as U, TITLE_MAX as a, TITLE_PRECEDENCE as b, type TitleSources as c, claudeTranscriptTitleSources as d, cleanTitle as e, displayTitle as f, isBridgePlaceholder as i, resolveSessionTitle as r };
|