@ai-matrx/coding-sessions 0.1.165 → 0.2.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 +26 -0
- package/dist/engine/index.cjs +224 -54
- 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 +220 -53
- package/dist/engine/index.js.map +1 -1
- package/dist/{index-CDeDKQHe.d.cts → index-29t7n472.d.cts} +81 -3
- package/dist/{index-CDeDKQHe.d.ts → index-29t7n472.d.ts} +81 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +15 -3
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +23 -0
- package/dist/react/index.d.ts +23 -0
- package/dist/react/index.js +15 -3
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +201 -52
- 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 +201 -52
- package/dist/server/index.js.map +1 -1
- package/dist/sessions/index.cjs +335 -0
- package/dist/sessions/index.cjs.map +1 -0
- package/dist/sessions/index.d.cts +63 -0
- package/dist/sessions/index.d.ts +63 -0
- package/dist/sessions/index.js +312 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/titles-eCnA5sY7.d.cts +41 -0
- package/dist/titles-eCnA5sY7.d.ts +41 -0
- package/dist/ui/assets/{index-5Iix3spw.js → index-SuRr0-D2.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/package.json +13 -3
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// src/normalize/agent-block.ts
|
|
2
|
+
var AGENT_BLOCK_MARKER = "<ai_matrx_agent ";
|
|
3
|
+
function stripAgentBlock(text) {
|
|
4
|
+
const removed = text.match(/^<ai_matrx_agent_removed>[\s\S]*?<\/ai_matrx_agent_removed>\s*/);
|
|
5
|
+
if (removed) return text.slice(removed[0].length);
|
|
6
|
+
if (!text.startsWith(AGENT_BLOCK_MARKER)) return text;
|
|
7
|
+
const end = text.indexOf("</ai_matrx_agent>");
|
|
8
|
+
if (end < 0) return text;
|
|
9
|
+
let rest = text.slice(end + "</ai_matrx_agent>".length);
|
|
10
|
+
const skills = rest.match(/^\s*<skills_instructions>[\s\S]*?<\/skills_instructions>/);
|
|
11
|
+
if (skills) rest = rest.slice(skills[0].length);
|
|
12
|
+
return rest.replace(/^\s+/, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/normalize/claude.ts
|
|
16
|
+
function isObject(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function str(value) {
|
|
20
|
+
return typeof value === "string" ? value : null;
|
|
21
|
+
}
|
|
22
|
+
function num(value) {
|
|
23
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
24
|
+
}
|
|
25
|
+
var SYNTHETIC_USER_PREFIXES = [
|
|
26
|
+
"<command-name>",
|
|
27
|
+
"<command-message>",
|
|
28
|
+
"<command-args>",
|
|
29
|
+
"<local-command-stdout>",
|
|
30
|
+
"<local-command-stderr>",
|
|
31
|
+
"<local-command-caveat>",
|
|
32
|
+
"<system-reminder>",
|
|
33
|
+
"<bash-input>",
|
|
34
|
+
"<bash-stdout>",
|
|
35
|
+
"<bash-stderr>",
|
|
36
|
+
"<user-memory-input>",
|
|
37
|
+
"<task-notification>",
|
|
38
|
+
"Caveat: The messages below were generated by the user while running local commands",
|
|
39
|
+
"[Request interrupted by user"
|
|
40
|
+
];
|
|
41
|
+
function isSyntheticClaudeUserText(text) {
|
|
42
|
+
const trimmed = text.trimStart();
|
|
43
|
+
return SYNTHETIC_USER_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
|
|
44
|
+
}
|
|
45
|
+
function toolResultText(content) {
|
|
46
|
+
if (typeof content === "string") return content;
|
|
47
|
+
if (Array.isArray(content)) {
|
|
48
|
+
return content.map((block) => {
|
|
49
|
+
if (!isObject(block)) return "";
|
|
50
|
+
if (block.type === "text") return str(block.text) ?? "";
|
|
51
|
+
if (block.type === "image") return "[image]";
|
|
52
|
+
return "";
|
|
53
|
+
}).filter(Boolean).join("\n");
|
|
54
|
+
}
|
|
55
|
+
return content === void 0 || content === null ? "" : JSON.stringify(content);
|
|
56
|
+
}
|
|
57
|
+
function createClaudeNormalizer() {
|
|
58
|
+
let sessionId = null;
|
|
59
|
+
let streamingMessageId = null;
|
|
60
|
+
const blockCounters = /* @__PURE__ */ new Map();
|
|
61
|
+
let userCounter = 0;
|
|
62
|
+
let noticeCounter = 0;
|
|
63
|
+
function nextBlockIndex(messageId) {
|
|
64
|
+
const index = blockCounters.get(messageId) ?? 0;
|
|
65
|
+
blockCounters.set(messageId, index + 1);
|
|
66
|
+
return index;
|
|
67
|
+
}
|
|
68
|
+
function pushAssistant(entry) {
|
|
69
|
+
const message = isObject(entry.message) ? entry.message : null;
|
|
70
|
+
if (!message) return [];
|
|
71
|
+
const messageId = str(message.id) ?? str(entry.uuid) ?? `assistant-${blockCounters.size}`;
|
|
72
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
73
|
+
const items = [];
|
|
74
|
+
for (const block of content) {
|
|
75
|
+
if (!isObject(block)) continue;
|
|
76
|
+
const index = nextBlockIndex(messageId);
|
|
77
|
+
const id = `${messageId}:${index}`;
|
|
78
|
+
if (block.type === "text") {
|
|
79
|
+
const text = str(block.text) ?? "";
|
|
80
|
+
if (text.length > 0) items.push({ kind: "text", id, text });
|
|
81
|
+
} else if (block.type === "thinking") {
|
|
82
|
+
const text = str(block.thinking) ?? "";
|
|
83
|
+
if (text.trim().length > 0) items.push({ kind: "reasoning", id, text });
|
|
84
|
+
} else if (block.type === "tool_use" || block.type === "server_tool_use") {
|
|
85
|
+
items.push({
|
|
86
|
+
kind: "tool-call",
|
|
87
|
+
id: str(block.id) ?? id,
|
|
88
|
+
name: str(block.name) ?? "tool",
|
|
89
|
+
input: block.input ?? null
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return items;
|
|
94
|
+
}
|
|
95
|
+
function pushUser(entry) {
|
|
96
|
+
if (entry.isMeta === true || entry.isCompactSummary === true) return [];
|
|
97
|
+
const message = isObject(entry.message) ? entry.message : null;
|
|
98
|
+
if (!message) return [];
|
|
99
|
+
const at = str(entry.timestamp);
|
|
100
|
+
const content = message.content;
|
|
101
|
+
if (typeof content === "string") {
|
|
102
|
+
if (isSyntheticClaudeUserText(content) || content.trim().length === 0) return [];
|
|
103
|
+
userCounter += 1;
|
|
104
|
+
return [{ kind: "user", id: str(entry.uuid) ?? `user-${userCounter}`, text: stripAgentBlock(content), at }];
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(content)) return [];
|
|
107
|
+
const items = [];
|
|
108
|
+
const typed = [];
|
|
109
|
+
for (const block of content) {
|
|
110
|
+
if (!isObject(block)) continue;
|
|
111
|
+
if (block.type === "tool_result") {
|
|
112
|
+
items.push({
|
|
113
|
+
kind: "tool-result",
|
|
114
|
+
id: str(block.tool_use_id) ?? "unknown",
|
|
115
|
+
output: toolResultText(block.content),
|
|
116
|
+
isError: block.is_error === true
|
|
117
|
+
});
|
|
118
|
+
} else if (block.type === "text") {
|
|
119
|
+
const text = str(block.text) ?? "";
|
|
120
|
+
if (text.trim().length > 0 && !isSyntheticClaudeUserText(text)) typed.push(text);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (typed.length > 0) {
|
|
124
|
+
userCounter += 1;
|
|
125
|
+
items.unshift({
|
|
126
|
+
kind: "user",
|
|
127
|
+
id: str(entry.uuid) ?? `user-${userCounter}`,
|
|
128
|
+
text: stripAgentBlock(typed.join("\n\n")),
|
|
129
|
+
at
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return items;
|
|
133
|
+
}
|
|
134
|
+
function pushStreamEvent(entry) {
|
|
135
|
+
const event = isObject(entry.event) ? entry.event : null;
|
|
136
|
+
if (!event) return [];
|
|
137
|
+
if (event.type === "message_start" && isObject(event.message)) {
|
|
138
|
+
streamingMessageId = str(event.message.id);
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
if (event.type === "content_block_delta" && streamingMessageId !== null) {
|
|
142
|
+
const delta = isObject(event.delta) ? event.delta : null;
|
|
143
|
+
if (delta?.type === "text_delta") {
|
|
144
|
+
const text = str(delta.text) ?? "";
|
|
145
|
+
if (text.length === 0) return [];
|
|
146
|
+
return [{ kind: "text-delta", id: `${streamingMessageId}:${num(event.index)}`, text }];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
function pushResult(entry) {
|
|
152
|
+
const items = [];
|
|
153
|
+
const usage = isObject(entry.usage) ? entry.usage : null;
|
|
154
|
+
if (usage) {
|
|
155
|
+
const runUsage = {
|
|
156
|
+
inputTokens: num(usage.input_tokens) + num(usage.cache_creation_input_tokens),
|
|
157
|
+
outputTokens: num(usage.output_tokens),
|
|
158
|
+
cachedInputTokens: num(usage.cache_read_input_tokens)
|
|
159
|
+
};
|
|
160
|
+
const cost = typeof entry.total_cost_usd === "number" ? entry.total_cost_usd : null;
|
|
161
|
+
items.push({ kind: "usage", usage: runUsage, costUsd: cost });
|
|
162
|
+
}
|
|
163
|
+
if (entry.is_error === true || typeof entry.subtype === "string" && entry.subtype !== "success") {
|
|
164
|
+
const text = str(entry.result) ?? (Array.isArray(entry.errors) ? entry.errors.map(String).join("\n") : null) ?? `Claude Code stopped with "${String(entry.subtype)}".`;
|
|
165
|
+
noticeCounter += 1;
|
|
166
|
+
items.push({ kind: "error", id: `claude-result-${noticeCounter}`, text });
|
|
167
|
+
}
|
|
168
|
+
return items;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
get sessionId() {
|
|
172
|
+
return sessionId;
|
|
173
|
+
},
|
|
174
|
+
push(raw) {
|
|
175
|
+
if (!isObject(raw)) return [];
|
|
176
|
+
const lineSession = str(raw.session_id) ?? str(raw.sessionId);
|
|
177
|
+
if (lineSession && sessionId === null) sessionId = lineSession;
|
|
178
|
+
if (raw.isSidechain === true) return [];
|
|
179
|
+
switch (raw.type) {
|
|
180
|
+
case "assistant":
|
|
181
|
+
return pushAssistant(raw);
|
|
182
|
+
case "user":
|
|
183
|
+
return pushUser(raw);
|
|
184
|
+
case "stream_event":
|
|
185
|
+
return pushStreamEvent(raw);
|
|
186
|
+
case "result":
|
|
187
|
+
return pushResult(raw);
|
|
188
|
+
case "system":
|
|
189
|
+
if (raw.subtype === "compact_boundary") {
|
|
190
|
+
noticeCounter += 1;
|
|
191
|
+
return [
|
|
192
|
+
{
|
|
193
|
+
kind: "notice",
|
|
194
|
+
id: `claude-compact-${noticeCounter}`,
|
|
195
|
+
level: "info",
|
|
196
|
+
text: "Claude Code summarized the earlier part of this conversation to make room."
|
|
197
|
+
}
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
return [];
|
|
201
|
+
default:
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/sessions/titles.ts
|
|
209
|
+
var TITLE_MAX = 300;
|
|
210
|
+
var UNTITLED = "(untitled)";
|
|
211
|
+
var BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;
|
|
212
|
+
var TITLE_PRECEDENCE = [
|
|
213
|
+
/** The person's rename inside Matrx (Coding Sessions' preference). */
|
|
214
|
+
"override",
|
|
215
|
+
/** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */
|
|
216
|
+
"app",
|
|
217
|
+
/** The title the app last showed, remembered by the claude-accounts engine after the record went away. */
|
|
218
|
+
"app-remembered",
|
|
219
|
+
/** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */
|
|
220
|
+
"custom-title",
|
|
221
|
+
"ai-title",
|
|
222
|
+
"agent-name",
|
|
223
|
+
"summary",
|
|
224
|
+
/** The person's first typed message (Codex: the thread's title / first user message). */
|
|
225
|
+
"first-prompt",
|
|
226
|
+
"last-prompt"
|
|
227
|
+
];
|
|
228
|
+
var STAMPABLE = /* @__PURE__ */ new Set(["custom-title", "ai-title", "agent-name"]);
|
|
229
|
+
function isBridgePlaceholder(value) {
|
|
230
|
+
return typeof value === "string" && BRIDGE_PLACEHOLDER.test(value.trim());
|
|
231
|
+
}
|
|
232
|
+
function cleanTitle(value) {
|
|
233
|
+
if (typeof value !== "string") return null;
|
|
234
|
+
const line = value.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
|
|
235
|
+
if (!line) return null;
|
|
236
|
+
return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}\u2026` : line;
|
|
237
|
+
}
|
|
238
|
+
function resolveSessionTitle(sources) {
|
|
239
|
+
for (const source of TITLE_PRECEDENCE) {
|
|
240
|
+
const raw = sources[source];
|
|
241
|
+
if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;
|
|
242
|
+
const title = cleanTitle(raw);
|
|
243
|
+
if (title) return { title, source };
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
function claudeTranscriptTitleSources(head, tail = []) {
|
|
248
|
+
const out = {};
|
|
249
|
+
const take = (source, value) => {
|
|
250
|
+
if (typeof value !== "string" || !value.trim()) return;
|
|
251
|
+
if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;
|
|
252
|
+
out[source] = value;
|
|
253
|
+
};
|
|
254
|
+
const normalizer = createClaudeNormalizer();
|
|
255
|
+
let firstPrompt = null;
|
|
256
|
+
const scan = (entries, findPrompt) => {
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
259
|
+
const e = entry;
|
|
260
|
+
if (e.type === "custom-title") take("custom-title", e.customTitle);
|
|
261
|
+
else if (e.type === "ai-title") take("ai-title", e.aiTitle);
|
|
262
|
+
else if (e.type === "agent-name") take("agent-name", e.agentName);
|
|
263
|
+
else if (e.type === "summary") take("summary", e.summary);
|
|
264
|
+
else if (e.type === "last-prompt") take("last-prompt", e.lastPrompt);
|
|
265
|
+
else if (findPrompt && firstPrompt === null) {
|
|
266
|
+
for (const item of normalizer.push(entry)) {
|
|
267
|
+
if (item.kind === "user" && item.text.trim()) {
|
|
268
|
+
firstPrompt = item.text;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
scan(head, true);
|
|
276
|
+
scan(tail, false);
|
|
277
|
+
if (firstPrompt !== null) out["first-prompt"] = firstPrompt;
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/sessions/hints.ts
|
|
282
|
+
function titleHintKey(provider, sessionId) {
|
|
283
|
+
return `${provider}:${sessionId}`;
|
|
284
|
+
}
|
|
285
|
+
function createTitleHintsStore() {
|
|
286
|
+
let hints = null;
|
|
287
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
288
|
+
return {
|
|
289
|
+
current: () => hints,
|
|
290
|
+
subscribe(listener) {
|
|
291
|
+
listeners.add(listener);
|
|
292
|
+
return () => listeners.delete(listener);
|
|
293
|
+
},
|
|
294
|
+
set(next) {
|
|
295
|
+
hints = next;
|
|
296
|
+
for (const l of listeners) l();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
export {
|
|
301
|
+
BRIDGE_PLACEHOLDER,
|
|
302
|
+
TITLE_MAX,
|
|
303
|
+
TITLE_PRECEDENCE,
|
|
304
|
+
UNTITLED,
|
|
305
|
+
claudeTranscriptTitleSources,
|
|
306
|
+
cleanTitle,
|
|
307
|
+
createTitleHintsStore,
|
|
308
|
+
isBridgePlaceholder,
|
|
309
|
+
resolveSessionTitle,
|
|
310
|
+
titleHintKey
|
|
311
|
+
};
|
|
312
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ONE TITLE RULE for a coding chat, shared by every reader (Matrx 2's Coding Sessions and
|
|
3
|
+
* Code › Chats, the standalone host, search). Pure: no file access, browser-safe.
|
|
4
|
+
*
|
|
5
|
+
* What a chat is called is what the person sees in the coding app, so the app's own record leads;
|
|
6
|
+
* the CLI's transcript entries fill in for chats the app never listed. The person's rename inside
|
|
7
|
+
* Matrx sits on top and is applied ONCE, by whoever draws the row (never baked into the engine's
|
|
8
|
+
* own `title`). Plan: common-docs/projects/matrx-2/apps-chats.md §12.
|
|
9
|
+
*/
|
|
10
|
+
/** Every title is cut to this (the app tables' rule); the screen truncates further visually. */
|
|
11
|
+
declare const TITLE_MAX = 300;
|
|
12
|
+
/** What a row shows when no source has a title. */
|
|
13
|
+
declare const UNTITLED = "(untitled)";
|
|
14
|
+
/**
|
|
15
|
+
* The name the AI Matrx coding-session bridge used to stamp on every mirrored Claude Code prompt
|
|
16
|
+
* (`sessionTitle` from its UserPromptSubmit hook; stopped at the source 2026-10-06, still inside
|
|
17
|
+
* old transcripts as `custom-title` / `agent-name` entries). Never a chat's title.
|
|
18
|
+
*/
|
|
19
|
+
declare const BRIDGE_PLACEHOLDER: RegExp;
|
|
20
|
+
/** Where a title came from, strongest first. */
|
|
21
|
+
declare const TITLE_PRECEDENCE: readonly ["override", "app", "app-remembered", "custom-title", "ai-title", "agent-name", "summary", "first-prompt", "last-prompt"];
|
|
22
|
+
type TitleSource = (typeof TITLE_PRECEDENCE)[number];
|
|
23
|
+
/** Whatever each source says; absent, null and blank all mean "no title here". */
|
|
24
|
+
type TitleSources = Partial<Record<TitleSource, string | null | undefined>>;
|
|
25
|
+
interface ResolvedTitle {
|
|
26
|
+
title: string;
|
|
27
|
+
source: TitleSource;
|
|
28
|
+
}
|
|
29
|
+
declare function isBridgePlaceholder(value: unknown): boolean;
|
|
30
|
+
/** The first non-empty line, trimmed, cut to `TITLE_MAX` with "…"; null when there is none. */
|
|
31
|
+
declare function cleanTitle(value: unknown): string | null;
|
|
32
|
+
/** The title to show and where it came from: the first source in `TITLE_PRECEDENCE` that has one. */
|
|
33
|
+
declare function resolveSessionTitle(sources: TitleSources): ResolvedTitle | null;
|
|
34
|
+
/**
|
|
35
|
+
* Claude Code's own title entries from transcript lines (already parsed JSON), plus the person's
|
|
36
|
+
* first typed message from `head` (the same normalizer the transcript screen uses). Later entries
|
|
37
|
+
* win within a kind; a bridge placeholder never replaces a real one.
|
|
38
|
+
*/
|
|
39
|
+
declare function claudeTranscriptTitleSources(head: Iterable<unknown>, tail?: Iterable<unknown>): TitleSources;
|
|
40
|
+
|
|
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 };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ONE TITLE RULE for a coding chat, shared by every reader (Matrx 2's Coding Sessions and
|
|
3
|
+
* Code › Chats, the standalone host, search). Pure: no file access, browser-safe.
|
|
4
|
+
*
|
|
5
|
+
* What a chat is called is what the person sees in the coding app, so the app's own record leads;
|
|
6
|
+
* the CLI's transcript entries fill in for chats the app never listed. The person's rename inside
|
|
7
|
+
* Matrx sits on top and is applied ONCE, by whoever draws the row (never baked into the engine's
|
|
8
|
+
* own `title`). Plan: common-docs/projects/matrx-2/apps-chats.md §12.
|
|
9
|
+
*/
|
|
10
|
+
/** Every title is cut to this (the app tables' rule); the screen truncates further visually. */
|
|
11
|
+
declare const TITLE_MAX = 300;
|
|
12
|
+
/** What a row shows when no source has a title. */
|
|
13
|
+
declare const UNTITLED = "(untitled)";
|
|
14
|
+
/**
|
|
15
|
+
* The name the AI Matrx coding-session bridge used to stamp on every mirrored Claude Code prompt
|
|
16
|
+
* (`sessionTitle` from its UserPromptSubmit hook; stopped at the source 2026-10-06, still inside
|
|
17
|
+
* old transcripts as `custom-title` / `agent-name` entries). Never a chat's title.
|
|
18
|
+
*/
|
|
19
|
+
declare const BRIDGE_PLACEHOLDER: RegExp;
|
|
20
|
+
/** Where a title came from, strongest first. */
|
|
21
|
+
declare const TITLE_PRECEDENCE: readonly ["override", "app", "app-remembered", "custom-title", "ai-title", "agent-name", "summary", "first-prompt", "last-prompt"];
|
|
22
|
+
type TitleSource = (typeof TITLE_PRECEDENCE)[number];
|
|
23
|
+
/** Whatever each source says; absent, null and blank all mean "no title here". */
|
|
24
|
+
type TitleSources = Partial<Record<TitleSource, string | null | undefined>>;
|
|
25
|
+
interface ResolvedTitle {
|
|
26
|
+
title: string;
|
|
27
|
+
source: TitleSource;
|
|
28
|
+
}
|
|
29
|
+
declare function isBridgePlaceholder(value: unknown): boolean;
|
|
30
|
+
/** The first non-empty line, trimmed, cut to `TITLE_MAX` with "…"; null when there is none. */
|
|
31
|
+
declare function cleanTitle(value: unknown): string | null;
|
|
32
|
+
/** The title to show and where it came from: the first source in `TITLE_PRECEDENCE` that has one. */
|
|
33
|
+
declare function resolveSessionTitle(sources: TitleSources): ResolvedTitle | null;
|
|
34
|
+
/**
|
|
35
|
+
* Claude Code's own title entries from transcript lines (already parsed JSON), plus the person's
|
|
36
|
+
* first typed message from `head` (the same normalizer the transcript screen uses). Later entries
|
|
37
|
+
* win within a kind; a bridge placeholder never replaces a real one.
|
|
38
|
+
*/
|
|
39
|
+
declare function claudeTranscriptTitleSources(head: Iterable<unknown>, tail?: Iterable<unknown>): TitleSources;
|
|
40
|
+
|
|
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 };
|