@ricky-stevens/context-guardian 2.1.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/.claude-plugin/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +63 -0
- package/.github/workflows/ci.yml +66 -0
- package/CLAUDE.md +132 -0
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/biome.json +34 -0
- package/bun.lock +31 -0
- package/hooks/precompact.mjs +73 -0
- package/hooks/session-start.mjs +133 -0
- package/hooks/stop.mjs +172 -0
- package/hooks/submit.mjs +133 -0
- package/lib/checkpoint.mjs +258 -0
- package/lib/compact-cli.mjs +124 -0
- package/lib/compact-output.mjs +350 -0
- package/lib/config.mjs +40 -0
- package/lib/content.mjs +33 -0
- package/lib/diagnostics.mjs +221 -0
- package/lib/estimate.mjs +254 -0
- package/lib/extract-helpers.mjs +869 -0
- package/lib/handoff.mjs +329 -0
- package/lib/logger.mjs +34 -0
- package/lib/mcp-tools.mjs +200 -0
- package/lib/paths.mjs +90 -0
- package/lib/stats.mjs +81 -0
- package/lib/statusline.mjs +123 -0
- package/lib/synthetic-session.mjs +273 -0
- package/lib/tokens.mjs +170 -0
- package/lib/tool-summary.mjs +399 -0
- package/lib/transcript.mjs +939 -0
- package/lib/trim.mjs +158 -0
- package/package.json +22 -0
- package/skills/compact/SKILL.md +20 -0
- package/skills/config/SKILL.md +70 -0
- package/skills/handoff/SKILL.md +26 -0
- package/skills/prune/SKILL.md +20 -0
- package/skills/stats/SKILL.md +100 -0
- package/sonar-project.properties +12 -0
- package/test/checkpoint.test.mjs +171 -0
- package/test/compact-cli.test.mjs +230 -0
- package/test/compact-output.test.mjs +284 -0
- package/test/compaction-e2e.test.mjs +809 -0
- package/test/content.test.mjs +86 -0
- package/test/diagnostics.test.mjs +188 -0
- package/test/edge-cases.test.mjs +543 -0
- package/test/estimate.test.mjs +262 -0
- package/test/extract-helpers-coverage.test.mjs +333 -0
- package/test/extract-helpers.test.mjs +234 -0
- package/test/handoff.test.mjs +738 -0
- package/test/integration.test.mjs +582 -0
- package/test/logger.test.mjs +70 -0
- package/test/manual-compaction-test.md +426 -0
- package/test/mcp-tools.test.mjs +443 -0
- package/test/paths.test.mjs +250 -0
- package/test/quick-compaction-test.md +191 -0
- package/test/stats.test.mjs +88 -0
- package/test/statusline.test.mjs +222 -0
- package/test/submit.test.mjs +232 -0
- package/test/synthetic-session.test.mjs +600 -0
- package/test/tokens.test.mjs +293 -0
- package/test/tool-summary.test.mjs +771 -0
- package/test/transcript-coverage.test.mjs +369 -0
- package/test/transcript.test.mjs +596 -0
- package/test/trim.test.mjs +356 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-aware content summarisation for Claude Code transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Generates compact representations of tool_use invocations and tool_result
|
|
5
|
+
* outputs, preserving decision-relevant content while removing re-obtainable
|
|
6
|
+
* noise. This is the core of Context Guardian's "noise removal, not
|
|
7
|
+
* summarisation" approach.
|
|
8
|
+
*
|
|
9
|
+
* Built-in tool rules live here; MCP-specific rules are in mcp-tools.mjs.
|
|
10
|
+
*
|
|
11
|
+
* @module tool-summary
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
isSerenaReadTool,
|
|
16
|
+
isSerenaWriteTool,
|
|
17
|
+
summarizeMcpToolUse,
|
|
18
|
+
} from "./mcp-tools.mjs";
|
|
19
|
+
import {
|
|
20
|
+
isErrorResponse,
|
|
21
|
+
isShortErrorResponse,
|
|
22
|
+
startEndTrim,
|
|
23
|
+
} from "./trim.mjs";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Constants — size thresholds for different content types
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** Maximum chars for edit diffs before start+end trim. */
|
|
30
|
+
const EDIT_LIMIT = 3000;
|
|
31
|
+
/** Maximum chars for write content before start+end trim. */
|
|
32
|
+
const WRITE_LIMIT = 3000;
|
|
33
|
+
/** Maximum chars for Bash commands before start+end trim (heredocs). */
|
|
34
|
+
const BASH_CMD_LIMIT = 3000;
|
|
35
|
+
/** Maximum chars for Bash output before start+end trim. */
|
|
36
|
+
const BASH_OUTPUT_LIMIT = 5000;
|
|
37
|
+
/** Maximum chars for agent results before start+end trim. */
|
|
38
|
+
const AGENT_RESULT_LIMIT = 2000;
|
|
39
|
+
/** Maximum chars for unknown tool results — kept if under, trimmed if over. */
|
|
40
|
+
const UNKNOWN_RESULT_LIMIT = 1000;
|
|
41
|
+
/** Maximum chars for unknown MCP tool inputs before start+end trim. */
|
|
42
|
+
const UNKNOWN_INPUT_LIMIT = 1000;
|
|
43
|
+
/** Maximum chars for web search results before start+end trim. */
|
|
44
|
+
const WEB_SEARCH_LIMIT = 5000;
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Edit diff formatting
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format an Edit tool invocation as a compact old/new diff block.
|
|
52
|
+
* Uses labeled old:/new: format (NOT unified diff) to avoid doubling
|
|
53
|
+
* content size — each line appears only once.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} filePath - The file being edited
|
|
56
|
+
* @param {string} oldStr - The old_string being replaced
|
|
57
|
+
* @param {string} newStr - The new_string replacement
|
|
58
|
+
* @returns {string} Formatted diff string
|
|
59
|
+
*/
|
|
60
|
+
export function formatEditDiff(filePath, oldStr, newStr) {
|
|
61
|
+
const parts = [`→ Edit \`${filePath}\`:`];
|
|
62
|
+
|
|
63
|
+
if (oldStr && newStr) {
|
|
64
|
+
const oldTrimmed = startEndTrim(oldStr, EDIT_LIMIT / 2);
|
|
65
|
+
const newTrimmed = startEndTrim(newStr, EDIT_LIMIT / 2);
|
|
66
|
+
parts.push(
|
|
67
|
+
indent("old: |", 4),
|
|
68
|
+
indent(oldTrimmed, 6),
|
|
69
|
+
indent("new: |", 4),
|
|
70
|
+
indent(newTrimmed, 6),
|
|
71
|
+
);
|
|
72
|
+
} else if (newStr && !oldStr) {
|
|
73
|
+
const newTrimmed = startEndTrim(newStr, EDIT_LIMIT);
|
|
74
|
+
parts.push(indent("new: |", 4), indent(newTrimmed, 6));
|
|
75
|
+
} else if (oldStr && !newStr) {
|
|
76
|
+
const oldTrimmed = startEndTrim(oldStr, EDIT_LIMIT);
|
|
77
|
+
parts.push(indent("old: | [deleted]", 4), indent(oldTrimmed, 6));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return parts.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Indent every line of a text block by a given number of spaces.
|
|
85
|
+
* @param {string} text - Text to indent
|
|
86
|
+
* @param {number} spaces - Number of leading spaces
|
|
87
|
+
* @returns {string} Indented text
|
|
88
|
+
*/
|
|
89
|
+
function indent(text, spaces) {
|
|
90
|
+
const pad = " ".repeat(spaces);
|
|
91
|
+
return text
|
|
92
|
+
.split("\n")
|
|
93
|
+
.map((line) => pad + line)
|
|
94
|
+
.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Bash command classification
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Read-like Bash commands whose output is re-obtainable from disk.
|
|
103
|
+
* Results from these are stripped (like Read/Grep), keeping only errors.
|
|
104
|
+
* Action commands (tests, builds, curl, etc.) keep their full output.
|
|
105
|
+
*/
|
|
106
|
+
const READ_LIKE_BASH_RE =
|
|
107
|
+
/^\s*(?:ls|cat|head|tail|find|wc|du|date|pwd|which|file|stat|echo|tree|realpath)\b/;
|
|
108
|
+
const READ_LIKE_GIT_RE =
|
|
109
|
+
/^\s*git\s+(?:log|show|diff|status|branch|tag|remote|config)\b/;
|
|
110
|
+
|
|
111
|
+
function isReadLikeBash(command) {
|
|
112
|
+
if (!command) return false;
|
|
113
|
+
// For piped/chained commands, check the first command
|
|
114
|
+
const first = command.split(/[|;&]/).shift().trim();
|
|
115
|
+
return READ_LIKE_BASH_RE.test(first) || READ_LIKE_GIT_RE.test(first);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Tool use summarisation — per-tool handlers
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Summarise an Edit tool invocation.
|
|
124
|
+
*/
|
|
125
|
+
function summarizeEdit(input) {
|
|
126
|
+
return formatEditDiff(
|
|
127
|
+
input?.file_path || input?.path || "unknown",
|
|
128
|
+
input?.old_string || "",
|
|
129
|
+
input?.new_string || "",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Summarise a Write tool invocation.
|
|
135
|
+
*/
|
|
136
|
+
function summarizeWrite(input) {
|
|
137
|
+
const fp = input?.file_path || input?.path || "unknown";
|
|
138
|
+
const content = input?.content || "";
|
|
139
|
+
if (content.length <= WRITE_LIMIT) {
|
|
140
|
+
return `→ Write \`${fp}\`:\n${indent(content, 4)}`;
|
|
141
|
+
}
|
|
142
|
+
return `→ Write \`${fp}\` (${content.length} chars):\n${indent(startEndTrim(content, WRITE_LIMIT), 4)}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Summarise a Read tool invocation.
|
|
147
|
+
*/
|
|
148
|
+
function summarizeRead(input) {
|
|
149
|
+
const fp = input?.file_path || input?.path || "unknown";
|
|
150
|
+
const rangeInfo = input?.offset ? ` (from line ${input.offset})` : "";
|
|
151
|
+
return `→ Read \`${fp}\`${rangeInfo}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Summarise a NotebookEdit tool invocation.
|
|
156
|
+
*/
|
|
157
|
+
function summarizeNotebookEdit(input) {
|
|
158
|
+
const content = input?.new_source || input?.source || "";
|
|
159
|
+
if (content.length <= WRITE_LIMIT) {
|
|
160
|
+
return `→ NotebookEdit cell:\n${indent(content, 4)}`;
|
|
161
|
+
}
|
|
162
|
+
return `→ NotebookEdit cell (${content.length} chars):\n${indent(startEndTrim(content, WRITE_LIMIT), 4)}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Tool use summarisation (assistant message → tool_use blocks)
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate a compact summary string for a tool_use content block.
|
|
171
|
+
* Returns the formatted summary line(s) to include in the checkpoint.
|
|
172
|
+
*
|
|
173
|
+
* @param {object} block - A content block with type: "tool_use"
|
|
174
|
+
* @returns {string|null} Formatted tool summary, or null to omit
|
|
175
|
+
*/
|
|
176
|
+
export function summarizeToolUse(block) {
|
|
177
|
+
const { name, input } = block;
|
|
178
|
+
if (!name) return `→ Tool: [unknown]`;
|
|
179
|
+
|
|
180
|
+
// ── Built-in Claude Code tools ────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
if (name === "Edit") return summarizeEdit(input);
|
|
183
|
+
if (name === "Write") return summarizeWrite(input);
|
|
184
|
+
if (name === "Read") return summarizeRead(input);
|
|
185
|
+
|
|
186
|
+
if (name === "Bash") {
|
|
187
|
+
const cmd = input?.command || "";
|
|
188
|
+
return `→ Ran \`${startEndTrim(cmd, BASH_CMD_LIMIT)}\``;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (name === "Grep") {
|
|
192
|
+
const pattern = input?.pattern || "";
|
|
193
|
+
const searchPath = input?.path || "";
|
|
194
|
+
const pathSuffix = searchPath ? ` in \`${searchPath}\`` : "";
|
|
195
|
+
return `→ Grep \`${pattern}\`${pathSuffix}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (name === "Glob") return `→ Glob \`${input?.pattern || ""}\``;
|
|
199
|
+
|
|
200
|
+
if (name === "Agent")
|
|
201
|
+
return `→ Agent: ${input?.description || "[no description]"}`;
|
|
202
|
+
|
|
203
|
+
if (name === "AskUserQuestion") {
|
|
204
|
+
const question = input?.question || input?.text || JSON.stringify(input);
|
|
205
|
+
return `→ Asked user: ${startEndTrim(question, 500)}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (name === "WebSearch") return `→ WebSearch: \`${input?.query || ""}\``;
|
|
209
|
+
if (name === "WebFetch") return `→ WebFetch: \`${input?.url || ""}\``;
|
|
210
|
+
|
|
211
|
+
if (name === "NotebookEdit") return summarizeNotebookEdit(input);
|
|
212
|
+
|
|
213
|
+
// ── MCP tools — delegate to mcp-tools.mjs ─────────────────────────────
|
|
214
|
+
if (name.startsWith("mcp__")) {
|
|
215
|
+
return summarizeMcpToolUse(name, input, indent, summarizeUnknownTool);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Unknown built-in tools — conservative: preserve key params ────────
|
|
219
|
+
return summarizeUnknownTool(name, input);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Unknown tool fallback
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generate a conservative summary for an unrecognised tool.
|
|
228
|
+
* Always preserves the tool name and key input parameters.
|
|
229
|
+
*/
|
|
230
|
+
function summarizeUnknownTool(name, input) {
|
|
231
|
+
const inputStr = input ? JSON.stringify(input) : "";
|
|
232
|
+
if (inputStr.length <= UNKNOWN_INPUT_LIMIT) {
|
|
233
|
+
return `→ Tool: \`${name}\` ${inputStr}`;
|
|
234
|
+
}
|
|
235
|
+
return `→ Tool: \`${name}\` ${startEndTrim(inputStr, UNKNOWN_INPUT_LIMIT)}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Tool result summarisation — per-tool handlers
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Handle result for AskUserQuestion — always kept.
|
|
244
|
+
*/
|
|
245
|
+
function handleAskUserResult(content) {
|
|
246
|
+
return `← User answered: ${content}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Handle result for re-obtainable tools — only keep short errors.
|
|
251
|
+
*/
|
|
252
|
+
function handleReObtainableResult(content) {
|
|
253
|
+
return isShortErrorResponse(content) ? `← Error: ${content}` : null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Handle result for Bash tool — keep action output, strip read-like output.
|
|
258
|
+
*/
|
|
259
|
+
function handleBashResult(content, toolInfo) {
|
|
260
|
+
if (!content) return null;
|
|
261
|
+
if (isReadLikeBash(toolInfo?.input?.command)) {
|
|
262
|
+
return isShortErrorResponse(content) ? `← Error: ${content}` : null;
|
|
263
|
+
}
|
|
264
|
+
return `← ${startEndTrim(content, BASH_OUTPUT_LIMIT)}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Handle result for Agent tool — keep with trim.
|
|
269
|
+
*/
|
|
270
|
+
function handleAgentResult(content) {
|
|
271
|
+
return content
|
|
272
|
+
? `← Agent result:\n${startEndTrim(content, AGENT_RESULT_LIMIT)}`
|
|
273
|
+
: null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Handle result for unknown/fallback tools — keep small, trim large.
|
|
278
|
+
*/
|
|
279
|
+
function handleUnknownResult(content) {
|
|
280
|
+
if (!content) return null;
|
|
281
|
+
return content.length < UNKNOWN_RESULT_LIMIT
|
|
282
|
+
? `← ${content}`
|
|
283
|
+
: `← ${startEndTrim(content, UNKNOWN_RESULT_LIMIT)}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Tool result summarisation (user message → tool_result blocks)
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
/** Re-obtainable tools — results can be fetched again from disk. */
|
|
291
|
+
const RE_OBTAINABLE_TOOLS = new Set([
|
|
292
|
+
"Read",
|
|
293
|
+
"Grep",
|
|
294
|
+
"Glob",
|
|
295
|
+
"WebFetch",
|
|
296
|
+
"NotebookEdit",
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
/** Tools whose results are just success/failure confirmations or internal setup. */
|
|
300
|
+
const DISPOSABLE_RESULT_TOOLS = new Set(["Edit", "Write", "ToolSearch"]);
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Generate a summary for a tool_result content block, or null to remove it.
|
|
304
|
+
* Decision is based on the originating tool type (looked up from the ID map).
|
|
305
|
+
*
|
|
306
|
+
* @param {object} resultBlock - A content block with type: "tool_result"
|
|
307
|
+
* @param {object|null} toolInfo - The originating tool's {name, input}, or null
|
|
308
|
+
* @returns {string|null} Formatted result summary, or null to omit
|
|
309
|
+
*/
|
|
310
|
+
export function summarizeToolResult(resultBlock, toolInfo) {
|
|
311
|
+
const content = extractResultText(resultBlock);
|
|
312
|
+
const toolName = toolInfo?.name || "";
|
|
313
|
+
|
|
314
|
+
// AskUserQuestion — ALWAYS keep (user decision channel)
|
|
315
|
+
if (toolName === "AskUserQuestion") return handleAskUserResult(content);
|
|
316
|
+
|
|
317
|
+
// Re-obtainable — only keep short error responses
|
|
318
|
+
if (RE_OBTAINABLE_TOOLS.has(toolName) || isSerenaReadTool(toolName)) {
|
|
319
|
+
return handleReObtainableResult(content);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Write tools — just success/failure
|
|
323
|
+
if (DISPOSABLE_RESULT_TOOLS.has(toolName) || isSerenaWriteTool(toolName))
|
|
324
|
+
return null;
|
|
325
|
+
|
|
326
|
+
// WebSearch — ephemeral results, keep
|
|
327
|
+
if (toolName === "WebSearch")
|
|
328
|
+
return `← Search results:\n${startEndTrim(content, WEB_SEARCH_LIMIT)}`;
|
|
329
|
+
|
|
330
|
+
// Bash — keep action output (tests, builds), strip read-like output (ls, cat, find)
|
|
331
|
+
if (toolName === "Bash") return handleBashResult(content, toolInfo);
|
|
332
|
+
|
|
333
|
+
// Agent — keep with trim
|
|
334
|
+
if (toolName === "Agent") return handleAgentResult(content);
|
|
335
|
+
|
|
336
|
+
// Sequential Thinking — redundant with input
|
|
337
|
+
if (toolName.includes("sequential-thinking")) return null;
|
|
338
|
+
// Context-mode — sandbox-internal
|
|
339
|
+
if (toolName.includes("context-mode")) return null;
|
|
340
|
+
// Serena memory — externally persisted
|
|
341
|
+
if (toolName.includes("serena") && toolName.includes("memory")) return null;
|
|
342
|
+
|
|
343
|
+
// Non-re-obtainable with errors — always keep
|
|
344
|
+
if (content && isErrorResponse(content))
|
|
345
|
+
return `← Error: ${startEndTrim(content, BASH_OUTPUT_LIMIT)}`;
|
|
346
|
+
|
|
347
|
+
// Unknown — conservative: keep small, trim large
|
|
348
|
+
return handleUnknownResult(content);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// Helpers
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Extract text content from a tool_result block.
|
|
357
|
+
* The content field can be a string or an array of content blocks.
|
|
358
|
+
*/
|
|
359
|
+
function extractResultText(block) {
|
|
360
|
+
if (!block) return "";
|
|
361
|
+
const c = block.content;
|
|
362
|
+
if (!c) return "";
|
|
363
|
+
if (typeof c === "string") return c;
|
|
364
|
+
if (Array.isArray(c)) {
|
|
365
|
+
return c
|
|
366
|
+
.filter((b) => b.type === "text")
|
|
367
|
+
.map((b) => b.text)
|
|
368
|
+
.join("\n");
|
|
369
|
+
}
|
|
370
|
+
return "";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Generate a placeholder for non-text, non-tool content blocks
|
|
375
|
+
* (images, documents, unknown types).
|
|
376
|
+
*
|
|
377
|
+
* @param {object} block - A content block
|
|
378
|
+
* @returns {string|null} Placeholder text, or null if not applicable
|
|
379
|
+
*/
|
|
380
|
+
export function contentBlockPlaceholder(block) {
|
|
381
|
+
if (!block?.type) return null;
|
|
382
|
+
if (block.type === "image") return "[User shared an image]";
|
|
383
|
+
if (block.type === "document") {
|
|
384
|
+
const name = block.source?.filename || block.filename || null;
|
|
385
|
+
return name
|
|
386
|
+
? `[User shared a document: ${name}]`
|
|
387
|
+
: "[User shared a document]";
|
|
388
|
+
}
|
|
389
|
+
if (
|
|
390
|
+
block.type !== "text" &&
|
|
391
|
+
block.type !== "tool_use" &&
|
|
392
|
+
block.type !== "tool_result" &&
|
|
393
|
+
block.type !== "thinking" &&
|
|
394
|
+
block.type !== "redacted_thinking"
|
|
395
|
+
) {
|
|
396
|
+
return `[Unknown content block: ${block.type}]`;
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|