@pi-claudian/auto-save-to-markdown 0.1.3 → 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/README.md +10 -10
- package/README.zh.md +9 -9
- package/index.ts +96 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,12 +88,9 @@ The login page redirects in a loop after the auth refactor...
|
|
|
88
88
|
Assistant · 13:05:40 · claude-sonnet-4-5
|
|
89
89
|
===
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Let me check the redirect chain...
|
|
95
|
-
|
|
96
|
-
</details>
|
|
91
|
+
> [!tldr]- Thinking
|
|
92
|
+
>
|
|
93
|
+
> Let me check the redirect chain...
|
|
97
94
|
|
|
98
95
|
I'll trace the middleware order first.
|
|
99
96
|
|
|
@@ -106,10 +103,13 @@ I'll trace the middleware order first.
|
|
|
106
103
|
---
|
|
107
104
|
```
|
|
108
105
|
|
|
109
|
-
The body renders user and assistant messages in full (assistant thinking
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
The body renders user and assistant messages in full (assistant thinking and
|
|
107
|
+
per-turn tool calls are each folded into a collapsed Obsidian callout —
|
|
108
|
+
`> [!tldr]- Thinking` and `> [!quote]- Tool Calls · …`) and summarizes each
|
|
109
|
+
tool call and result in one line, so the file stays readable while still
|
|
110
|
+
showing what the agent did. Callouts are used instead of HTML `<details>`
|
|
111
|
+
because Obsidian renders markdown inside HTML blocks unreliably; outside
|
|
112
|
+
Obsidian the callouts degrade to plain blockquotes.
|
|
113
113
|
|
|
114
114
|
Each message block opens with a setext level-1 info header (`User · …`,
|
|
115
115
|
underlined with `===`) — one level above the `##` headings AI content
|
package/README.zh.md
CHANGED
|
@@ -78,12 +78,9 @@ auth 重构之后登录页一直重定向死循环……
|
|
|
78
78
|
Assistant · 13:05:40 · claude-sonnet-4-5
|
|
79
79
|
===
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
先看中间件的执行顺序……
|
|
85
|
-
|
|
86
|
-
</details>
|
|
81
|
+
> [!tldr]- Thinking
|
|
82
|
+
>
|
|
83
|
+
> 先看中间件的执行顺序……
|
|
87
84
|
|
|
88
85
|
我先追踪一下中间件链。
|
|
89
86
|
|
|
@@ -96,9 +93,12 @@ Assistant · 13:05:40 · claude-sonnet-4-5
|
|
|
96
93
|
---
|
|
97
94
|
```
|
|
98
95
|
|
|
99
|
-
正文完整渲染 user / assistant 消息(assistant 的 thinking
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
正文完整渲染 user / assistant 消息(assistant 的 thinking 与每轮工具调用
|
|
97
|
+
分别折叠在可折叠的 Obsidian callout 中——`> [!tldr]- Thinking` 和
|
|
98
|
+
`> [!quote]- Tool Calls · …`),每个工具调用和结果各压缩成一行摘要,既可读
|
|
99
|
+
又能看出 agent 做了什么。之所以用 callout 而不是 HTML `<details>`,是因为
|
|
100
|
+
Obsidian 对 HTML 块内嵌 Markdown 的渲染不可靠;在非 Obsidian 环境下 callout
|
|
101
|
+
退化为普通引用块。
|
|
102
102
|
|
|
103
103
|
每个消息块以 setext 一级信息头(`User · …`,下一行以 `===` 下划)开头——
|
|
104
104
|
高于 AI 内容常见的 `##` 二级标题,解析时也能与内容中的 `#` 一级标题区分
|
package/index.ts
CHANGED
|
@@ -24,6 +24,17 @@
|
|
|
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 collapsed Obsidian callout (`> [!quote]- Tool Calls · …`).
|
|
31
|
+
* Thinking folds the same way into `> [!tldr]- Thinking`. Callouts are used
|
|
32
|
+
* instead of HTML `<details>` because Obsidian's views render embedded
|
|
33
|
+
* markdown inside HTML blocks unreliably, while callouts fold and render
|
|
34
|
+
* markdown in both Live Preview and Reading view. Outside Obsidian the
|
|
35
|
+
* callouts degrade to plain blockquotes. A result whose call was saved in
|
|
36
|
+
* an earlier file (mid-turn manual save) falls back to a standalone
|
|
37
|
+
* one-line block.
|
|
27
38
|
* - Branching: each file records exactly ONE branch (the root→leaf path
|
|
28
39
|
* returned by sessionManager.getBranch()). State is persisted via
|
|
29
40
|
* `pi.appendEntry()` custom entries, which are part of the session tree
|
|
@@ -70,7 +81,7 @@ const NOTIFY_TAG = "[AutoSave]";
|
|
|
70
81
|
|
|
71
82
|
const MAX_TITLE_LENGTH = 60;
|
|
72
83
|
const TITLE_FALLBACK_LENGTH = 40;
|
|
73
|
-
const TOOL_RESULT_PREVIEW =
|
|
84
|
+
const TOOL_RESULT_PREVIEW = 500;
|
|
74
85
|
const TOOL_ARGS_PREVIEW = 160;
|
|
75
86
|
|
|
76
87
|
type AgentMessage = SessionMessageEntry["message"];
|
|
@@ -291,7 +302,66 @@ export default function (pi: ExtensionAPI) {
|
|
|
291
302
|
return s.replace(/^(?:[ \t]*\n)+/, "").replace(/\s+$/, "");
|
|
292
303
|
}
|
|
293
304
|
|
|
294
|
-
|
|
305
|
+
/** One tool call with its paired result preview (null when no result entry exists). */
|
|
306
|
+
interface RenderedToolCall {
|
|
307
|
+
name: string;
|
|
308
|
+
args: string;
|
|
309
|
+
result: string | null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Flattened, length-capped result preview with error status suffix. */
|
|
313
|
+
function resultPreview(m: ToolResultMessage): string {
|
|
314
|
+
const texts: string[] = [];
|
|
315
|
+
for (const b of m.content) {
|
|
316
|
+
if (b.type === "text") texts.push(b.text);
|
|
317
|
+
else texts.push(`_[image: ${b.mimeType}]_`);
|
|
318
|
+
}
|
|
319
|
+
const flat = texts.join(" ").replace(/\s+/g, " ").trim();
|
|
320
|
+
const capped =
|
|
321
|
+
flat.length > TOOL_RESULT_PREVIEW ? flat.slice(0, TOOL_RESULT_PREVIEW) + " …" : flat;
|
|
322
|
+
const status = m.isError ? " (error)" : "";
|
|
323
|
+
return `${capped}${status}`.trim();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Standalone one-line block for a result whose call is not in this file. */
|
|
327
|
+
function renderToolResult(m: ToolResultMessage): string {
|
|
328
|
+
return `> **Tool · ${m.toolName}** ${resultPreview(m)}`.trim();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** "read, web_search ×2" — tool names with repeat counts, first-seen order. */
|
|
332
|
+
function summarizeToolNames(names: string[]): string {
|
|
333
|
+
const counts = new Map<string, number>();
|
|
334
|
+
for (const n of names) counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
335
|
+
return [...counts].map(([n, c]) => (c > 1 ? `${n} ×${c}` : n)).join(", ");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Collapsed Obsidian callout (`> [!type]- title`) wrapping a markdown body:
|
|
340
|
+
* every body line is prefixed with `>` (empty lines become bare `>`), so the
|
|
341
|
+
* body keeps rendering as markdown while folding works in both Obsidian
|
|
342
|
+
* views. Outside Obsidian the callout degrades to a plain blockquote.
|
|
343
|
+
*/
|
|
344
|
+
function callout(type: string, title: string, body: string): string {
|
|
345
|
+
const lines = body.split("\n").map((line) => (line ? `> ${line}` : ">"));
|
|
346
|
+
return `> [!${type}]- ${title}\n${lines.join("\n")}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Fold tool calls and their paired results into one collapsed callout. */
|
|
350
|
+
function renderToolCallsCallout(calls: RenderedToolCall[]): string {
|
|
351
|
+
const summary = summarizeToolNames(calls.map((c) => c.name));
|
|
352
|
+
const items = calls.map((c) => {
|
|
353
|
+
const head = c.args ? `**\`${c.name}\`** \`${c.args}\`` : `**\`${c.name}\`**`;
|
|
354
|
+
const result = c.result === null ? "_(no result)_" : c.result || "_(empty result)_";
|
|
355
|
+
return `${head}\n\n> ${result}`;
|
|
356
|
+
});
|
|
357
|
+
return callout("quote", `Tool Calls · ${calls.length} (${summary})`, items.join("\n\n"));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function renderAssistant(
|
|
361
|
+
m: AssistantMessage,
|
|
362
|
+
t: string,
|
|
363
|
+
results: Map<string, ToolResultMessage>,
|
|
364
|
+
): string {
|
|
295
365
|
// Render blocks in their original chronological order: thinking always
|
|
296
366
|
// precedes the text it produced, instead of being grouped after the fact.
|
|
297
367
|
// Setext H1 (`===` underline): one level above the `##` headings AI
|
|
@@ -301,13 +371,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
301
371
|
const thinkings: string[] = [];
|
|
302
372
|
const flushThinking = () => {
|
|
303
373
|
if (thinkings.length) {
|
|
304
|
-
parts.push(
|
|
305
|
-
`<details>\n<summary>Thinking</summary>\n\n${thinkings.join("\n\n")}\n\n</details>`,
|
|
306
|
-
);
|
|
374
|
+
parts.push(callout("tldr", "Thinking", thinkings.join("\n\n")));
|
|
307
375
|
thinkings.length = 0;
|
|
308
376
|
}
|
|
309
377
|
};
|
|
310
|
-
const calls:
|
|
378
|
+
const calls: RenderedToolCall[] = [];
|
|
311
379
|
for (const b of m.content) {
|
|
312
380
|
if (b.type === "text") {
|
|
313
381
|
flushThinking();
|
|
@@ -316,32 +384,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
316
384
|
thinkings.push(repairThinking(b.thinking));
|
|
317
385
|
} else if (b.type === "toolCall") {
|
|
318
386
|
flushThinking();
|
|
319
|
-
|
|
387
|
+
const r = results.get(b.id);
|
|
388
|
+
results.delete(b.id);
|
|
389
|
+
calls.push({
|
|
390
|
+
name: b.name,
|
|
391
|
+
args: previewArgs(b.arguments),
|
|
392
|
+
result: r ? resultPreview(r) : null,
|
|
393
|
+
});
|
|
320
394
|
}
|
|
321
395
|
}
|
|
322
396
|
flushThinking();
|
|
323
|
-
if (calls.length) parts.push(
|
|
397
|
+
if (calls.length) parts.push(renderToolCallsCallout(calls));
|
|
324
398
|
if (m.errorMessage) parts.push(`> Error: ${m.errorMessage.replace(/\s+/g, " ").trim()}`);
|
|
325
399
|
if (parts.length === 0) parts.push("_(empty response)_");
|
|
326
400
|
return `${header}\n\n${parts.join("\n\n")}`;
|
|
327
401
|
}
|
|
328
402
|
|
|
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
403
|
/** Render a chronological list of message entries as markdown blocks. */
|
|
344
404
|
function renderEntries(entries: SessionMessageEntry[]): string {
|
|
405
|
+
// Tool calls sit in assistant entries while their results are separate
|
|
406
|
+
// toolResult entries, paired by toolCall id. Collect results first so
|
|
407
|
+
// each assistant block can fold its calls together with their results;
|
|
408
|
+
// results left unclaimed (their call was saved in an earlier file, e.g.
|
|
409
|
+
// a mid-turn manual save) render as standalone blocks.
|
|
410
|
+
const results = new Map<string, ToolResultMessage>();
|
|
411
|
+
for (const e of entries) {
|
|
412
|
+
if (e.message.role === "toolResult") results.set(e.message.toolCallId, e.message);
|
|
413
|
+
}
|
|
414
|
+
|
|
345
415
|
const blocks: string[] = [];
|
|
346
416
|
for (const e of entries) {
|
|
347
417
|
const m = e.message;
|
|
@@ -349,8 +419,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
349
419
|
if (m.role === "user") {
|
|
350
420
|
blocks.push(`User · ${t}\n===\n\n${userText(m.content)}`);
|
|
351
421
|
} else if (m.role === "assistant") {
|
|
352
|
-
blocks.push(renderAssistant(m, t));
|
|
422
|
+
blocks.push(renderAssistant(m, t, results));
|
|
353
423
|
} else if (m.role === "toolResult") {
|
|
424
|
+
// Claimed results (deleted from the map by their assistant block)
|
|
425
|
+
// were already folded inline; the rest have no call in this file.
|
|
426
|
+
if (!results.has(m.toolCallId)) continue;
|
|
354
427
|
blocks.push(renderToolResult(m));
|
|
355
428
|
}
|
|
356
429
|
// 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.3.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",
|