@pi-claudian/auto-save-to-markdown 0.1.3 → 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/index.ts +88 -20
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
* (`User · HH:MM:SS` / `Assistant · HH:MM:SS · model`, underlined with
|
|
25
25
|
* `===`, distinct from the `#`/`##` ATX headings AI content uses) and
|
|
26
26
|
* ends with a `---` separator wrapped in single blank lines.
|
|
27
|
+
* - Tool call/result folding: calls live in the assistant entry while their
|
|
28
|
+
* results are separate toolResult entries; saves pair them by toolCall id
|
|
29
|
+
* and fold each assistant block's calls, with a short result preview each,
|
|
30
|
+
* into one `<details><summary>Tool Calls</summary>` section. Previews stay
|
|
31
|
+
* short (500 chars): full content is one Obsidian link away, and oversized
|
|
32
|
+
* details bodies render unfolded in Obsidian. A result whose call was
|
|
33
|
+
* saved in an earlier file (mid-turn manual save) falls back to a
|
|
34
|
+
* standalone one-line block.
|
|
27
35
|
* - Branching: each file records exactly ONE branch (the root→leaf path
|
|
28
36
|
* returned by sessionManager.getBranch()). State is persisted via
|
|
29
37
|
* `pi.appendEntry()` custom entries, which are part of the session tree
|
|
@@ -70,7 +78,7 @@ const NOTIFY_TAG = "[AutoSave]";
|
|
|
70
78
|
|
|
71
79
|
const MAX_TITLE_LENGTH = 60;
|
|
72
80
|
const TITLE_FALLBACK_LENGTH = 40;
|
|
73
|
-
const TOOL_RESULT_PREVIEW =
|
|
81
|
+
const TOOL_RESULT_PREVIEW = 500;
|
|
74
82
|
const TOOL_ARGS_PREVIEW = 160;
|
|
75
83
|
|
|
76
84
|
type AgentMessage = SessionMessageEntry["message"];
|
|
@@ -291,7 +299,62 @@ export default function (pi: ExtensionAPI) {
|
|
|
291
299
|
return s.replace(/^(?:[ \t]*\n)+/, "").replace(/\s+$/, "");
|
|
292
300
|
}
|
|
293
301
|
|
|
294
|
-
|
|
302
|
+
/** One tool call with its paired result preview (null when no result entry exists). */
|
|
303
|
+
interface RenderedToolCall {
|
|
304
|
+
name: string;
|
|
305
|
+
args: string;
|
|
306
|
+
result: string | null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Flattened, length-capped result preview with error status suffix. */
|
|
310
|
+
function resultPreview(m: ToolResultMessage): string {
|
|
311
|
+
const texts: string[] = [];
|
|
312
|
+
for (const b of m.content) {
|
|
313
|
+
if (b.type === "text") texts.push(b.text);
|
|
314
|
+
else texts.push(`_[image: ${b.mimeType}]_`);
|
|
315
|
+
}
|
|
316
|
+
const flat = texts.join(" ").replace(/\s+/g, " ").trim();
|
|
317
|
+
const capped =
|
|
318
|
+
flat.length > TOOL_RESULT_PREVIEW ? flat.slice(0, TOOL_RESULT_PREVIEW) + " …" : flat;
|
|
319
|
+
const status = m.isError ? " (error)" : "";
|
|
320
|
+
return `${capped}${status}`.trim();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Standalone one-line block for a result whose call is not in this file. */
|
|
324
|
+
function renderToolResult(m: ToolResultMessage): string {
|
|
325
|
+
return `> **Tool · ${m.toolName}** ${resultPreview(m)}`.trim();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** "read, web_search ×2" — tool names with repeat counts, first-seen order. */
|
|
329
|
+
function summarizeToolNames(names: string[]): string {
|
|
330
|
+
const counts = new Map<string, number>();
|
|
331
|
+
for (const n of names) counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
332
|
+
return [...counts].map(([n, c]) => (c > 1 ? `${n} ×${c}` : n)).join(", ");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Fold tool calls and their paired results into one collapsible section.
|
|
337
|
+
* Deliberately compact: Obsidian only collapses a details body it can
|
|
338
|
+
* render in view, so long previews make the section render unfolded.
|
|
339
|
+
*/
|
|
340
|
+
function renderToolCallsDetails(calls: RenderedToolCall[]): string {
|
|
341
|
+
const summary = summarizeToolNames(calls.map((c) => c.name));
|
|
342
|
+
const items = calls.map((c) => {
|
|
343
|
+
const head = c.args ? `**\`${c.name}\`** \`${c.args}\`` : `**\`${c.name}\`**`;
|
|
344
|
+
const result = c.result === null ? "_(no result)_" : c.result || "_(empty result)_";
|
|
345
|
+
return `${head}\n\n> ${result}`;
|
|
346
|
+
});
|
|
347
|
+
return (
|
|
348
|
+
`<details>\n<summary>Tool Calls · ${calls.length} (${summary})</summary>\n\n` +
|
|
349
|
+
`${items.join("\n\n")}\n\n</details>`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function renderAssistant(
|
|
354
|
+
m: AssistantMessage,
|
|
355
|
+
t: string,
|
|
356
|
+
results: Map<string, ToolResultMessage>,
|
|
357
|
+
): string {
|
|
295
358
|
// Render blocks in their original chronological order: thinking always
|
|
296
359
|
// precedes the text it produced, instead of being grouped after the fact.
|
|
297
360
|
// Setext H1 (`===` underline): one level above the `##` headings AI
|
|
@@ -307,7 +370,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
307
370
|
thinkings.length = 0;
|
|
308
371
|
}
|
|
309
372
|
};
|
|
310
|
-
const calls:
|
|
373
|
+
const calls: RenderedToolCall[] = [];
|
|
311
374
|
for (const b of m.content) {
|
|
312
375
|
if (b.type === "text") {
|
|
313
376
|
flushThinking();
|
|
@@ -316,32 +379,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
316
379
|
thinkings.push(repairThinking(b.thinking));
|
|
317
380
|
} else if (b.type === "toolCall") {
|
|
318
381
|
flushThinking();
|
|
319
|
-
|
|
382
|
+
const r = results.get(b.id);
|
|
383
|
+
results.delete(b.id);
|
|
384
|
+
calls.push({
|
|
385
|
+
name: b.name,
|
|
386
|
+
args: previewArgs(b.arguments),
|
|
387
|
+
result: r ? resultPreview(r) : null,
|
|
388
|
+
});
|
|
320
389
|
}
|
|
321
390
|
}
|
|
322
391
|
flushThinking();
|
|
323
|
-
if (calls.length) parts.push(
|
|
392
|
+
if (calls.length) parts.push(renderToolCallsDetails(calls));
|
|
324
393
|
if (m.errorMessage) parts.push(`> Error: ${m.errorMessage.replace(/\s+/g, " ").trim()}`);
|
|
325
394
|
if (parts.length === 0) parts.push("_(empty response)_");
|
|
326
395
|
return `${header}\n\n${parts.join("\n\n")}`;
|
|
327
396
|
}
|
|
328
397
|
|
|
329
|
-
function renderToolResult(m: ToolResultMessage): string {
|
|
330
|
-
const texts: string[] = [];
|
|
331
|
-
for (const b of m.content) {
|
|
332
|
-
if (b.type === "text") texts.push(b.text);
|
|
333
|
-
else texts.push(`_[image: ${b.mimeType}]_`);
|
|
334
|
-
}
|
|
335
|
-
const flat = texts.join(" ").replace(/\s+/g, " ").trim();
|
|
336
|
-
const capped =
|
|
337
|
-
flat.length > TOOL_RESULT_PREVIEW ? flat.slice(0, TOOL_RESULT_PREVIEW) + " …" : flat;
|
|
338
|
-
const status = m.isError ? " (error)" : "";
|
|
339
|
-
const line = `> **Tool · ${m.toolName}${status}** ${capped}`.trim();
|
|
340
|
-
return line;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
398
|
/** Render a chronological list of message entries as markdown blocks. */
|
|
344
399
|
function renderEntries(entries: SessionMessageEntry[]): string {
|
|
400
|
+
// Tool calls sit in assistant entries while their results are separate
|
|
401
|
+
// toolResult entries, paired by toolCall id. Collect results first so
|
|
402
|
+
// each assistant block can fold its calls together with their results;
|
|
403
|
+
// results left unclaimed (their call was saved in an earlier file, e.g.
|
|
404
|
+
// a mid-turn manual save) render as standalone blocks.
|
|
405
|
+
const results = new Map<string, ToolResultMessage>();
|
|
406
|
+
for (const e of entries) {
|
|
407
|
+
if (e.message.role === "toolResult") results.set(e.message.toolCallId, e.message);
|
|
408
|
+
}
|
|
409
|
+
|
|
345
410
|
const blocks: string[] = [];
|
|
346
411
|
for (const e of entries) {
|
|
347
412
|
const m = e.message;
|
|
@@ -349,8 +414,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
349
414
|
if (m.role === "user") {
|
|
350
415
|
blocks.push(`User · ${t}\n===\n\n${userText(m.content)}`);
|
|
351
416
|
} else if (m.role === "assistant") {
|
|
352
|
-
blocks.push(renderAssistant(m, t));
|
|
417
|
+
blocks.push(renderAssistant(m, t, results));
|
|
353
418
|
} else if (m.role === "toolResult") {
|
|
419
|
+
// Claimed results (deleted from the map by their assistant block)
|
|
420
|
+
// were already folded inline; the rest have no call in this file.
|
|
421
|
+
if (!results.has(m.toolCallId)) continue;
|
|
354
422
|
blocks.push(renderToolResult(m));
|
|
355
423
|
}
|
|
356
424
|
// Other roles (custom, bashExecution, branchSummary, compactionSummary)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-claudian/auto-save-to-markdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pi extension that automatically saves each completed conversation turn as a markdown file with YAML frontmatter, one file per session-tree branch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|