pi-auto-save-to-markdown 0.8.2 → 0.9.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 CHANGED
@@ -92,11 +92,11 @@ original creation timestamp — and rewrites the frontmatter title and the
92
92
  document heading to match. The rename happens at most once: later `/name`
93
93
  changes never touch the filename, and manually renamed files are left alone.
94
94
 
95
- ```markdown
95
+ ````markdown
96
96
  ---
97
97
  title: "Fix login redirect loop"
98
98
  agent: "pi"
99
- format_version: "1.5"
99
+ format_version: "1.6"
100
100
  session_id: "d0a4f541-976d-4d1b-8e1c-30a1f2b3c4d5"
101
101
  session_key: "c2088d77"
102
102
  branch_last_entry_id: "019be3a2-1f4d-7c8a-9b01-d23e45f6a7b8"
@@ -141,21 +141,27 @@ I'll trace the middleware order first.
141
141
  > [!quote]- Tool Calls · 1 (read)
142
142
  > **`read`** `{"filePath":"/Users/me/project/src/auth/middleware.ts"}`
143
143
  >
144
- > > `import { NextResponse } from "next/server"; export function middleware(…) …`
144
+ > ```
145
+ > import { NextResponse } from "next/server";
146
+ > export function middleware(…) …
147
+ > ```
145
148
 
146
149
  ---
147
- ```
150
+ ````
148
151
 
149
152
  The body renders user and assistant messages in full (assistant thinking and
150
153
  per-turn tool calls are each folded into a collapsed Obsidian callout —
151
- `> [!tldr]- Thinking` and `> [!quote]- Tool Calls · …`) and summarizes each
152
- tool call and result in one line, so the file stays readable while still
153
- showing what the agent did. Callouts are used instead of HTML `<details>`
154
- because Obsidian renders markdown inside HTML blocks unreliably; outside
155
- Obsidian the callouts degrade to plain blockquotes. Result and argument
156
- previews are wrapped in inline code spans (with a delimiter sized to survive
157
- backticks inside the content), so raw tool output renders literally instead of
158
- being parsed as markdown.
154
+ `> [!tldr]- Thinking` and `> [!quote]- Tool Calls · …`), recording every tool
155
+ call with its full raw result: the file is a documentary record that may be
156
+ @-referenced back into a conversation, and a truncated half-result would be
157
+ wasted when the tool is called again and misleading when it is not, while
158
+ local reading (grep, ranged reads) makes size a non-issue. Callouts are used
159
+ instead of HTML `<details>` because Obsidian renders markdown inside HTML
160
+ blocks unreliably; outside Obsidian the callouts degrade to plain blockquotes.
161
+ Arguments render as full JSON in inline code spans and results verbatim —
162
+ whitespace intact, nothing capped — in fenced code blocks (with a delimiter
163
+ sized to survive backticks inside the content), so raw tool output renders
164
+ literally instead of being parsed as markdown.
159
165
 
160
166
  Prompt blocks the client or the agent injects into a user message — the
161
167
  editor's active selection, attached or referenced notes, loaded skills — are
package/README.zh.md CHANGED
@@ -66,11 +66,11 @@ PI_SAVE_CONVERSATION_DIR=notes/ai pi
66
66
 
67
67
  会话的真实名称在建文件之后才到达时(如 Claudian 在首轮回复后才生成标题),下一次保存会把文件一次性改名为 `<名称>-<key>-<原时间戳>.md`(保留原创建时间戳),并同步改写 frontmatter 标题与正文标题。改名至多发生一次:之后的 `/name` 改名不再影响文件名,手动整理过的文件名也不会被动。
68
68
 
69
- ```markdown
69
+ ````markdown
70
70
  ---
71
71
  title: "修复登录重定向死循环"
72
72
  agent: "pi"
73
- format_version: "1.5"
73
+ format_version: "1.6"
74
74
  session_id: "d0a4f541-976d-4d1b-8e1c-30a1f2b3c4d5"
75
75
  session_key: "c2088d77"
76
76
  branch_last_entry_id: "019be3a2-1f4d-7c8a-9b01-d23e45f6a7b8"
@@ -115,19 +115,24 @@ Assistant <span style="font-size: 0.5em; color: var(--text-faint);">2026-08-29 1
115
115
  > [!quote]- Tool Calls · 1 (read)
116
116
  > **`read`** `{"filePath":"/Users/me/project/src/auth/middleware.ts"}`
117
117
  >
118
- > > `import { NextResponse } from "next/server"; export function middleware(…) …`
118
+ > ```
119
+ > import { NextResponse } from "next/server";
120
+ > export function middleware(…) …
121
+ > ```
119
122
 
120
123
  ---
121
- ```
124
+ ````
122
125
 
123
126
  正文完整渲染 user / assistant 消息(assistant 的 thinking 与每轮工具调用
124
127
  分别折叠在可折叠的 Obsidian callout 中——`> [!tldr]- Thinking` 和
125
- `> [!quote]- Tool Calls · …`),每个工具调用和结果各压缩成一行摘要,既可读
126
- 又能看出 agent 做了什么。之所以用 callout 而不是 HTML `<details>`,是因为
127
- Obsidian 对 HTML 块内嵌 Markdown 的渲染不可靠;在非 Obsidian 环境下 callout
128
- 退化为普通引用块。结果与参数预览会包在 inline code 里(分隔符长度会自动
129
- 压过内容中的反引号序列),工具的原始输出因此按字面渲染,不会被当作
130
- Markdown 解析。
128
+ `> [!quote]- Tool Calls · …`),每次工具调用连同其完整原始结果一起记录:
129
+ 归档文件是可能被 @ 引回对话的史料,截断的半个结果在工具重调时是浪费、在
130
+ 不再调用时是误导,而局部阅读(grep、按行段读取)让体积不成问题。之所以用
131
+ callout 而不是 HTML `<details>`,是因为 Obsidian 对 HTML 块内嵌 Markdown 的
132
+ 渲染不可靠;在非 Obsidian 环境下 callout 退化为普通引用块。参数以完整 JSON
133
+ 包在 inline code 里,结果逐字保真——空白原样、不截断——放在 fenced code
134
+ block 中(分隔符长度会自动压过内容中的反引号序列),工具的原始输出因此按
135
+ 字面渲染,不会被当作 Markdown 解析。
131
136
 
132
137
  客户端或 agent 注入到用户消息中的提示块——编辑器当前选区、附加或引用的
133
138
  笔记、加载的 skill——会从原始 XML(Obsidian 无法渲染,只会显示成裸露的
package/index.ts CHANGED
@@ -51,18 +51,22 @@
51
51
  * wrapped in single blank lines.
52
52
  * - Tool call/result folding: calls live in the assistant entry while their
53
53
  * results are separate toolResult entries; saves pair them by toolCall id
54
- * and fold each assistant block's calls, with a short result preview each,
55
- * into one collapsed Obsidian callout (`> [!quote]- Tool Calls · …`).
56
- * Thinking folds the same way into `> [!tldr]- Thinking`. Callouts are used
57
- * instead of HTML `<details>` because Obsidian's views render embedded
58
- * markdown inside HTML blocks unreliably, while callouts fold and render
59
- * markdown in both Live Preview and Reading view. Outside Obsidian the
60
- * callouts degrade to plain blockquotes. Argument previews and result
61
- * previews are wrapped in inline code spans (delimiter sized to survive
62
- * backticks inside the content), so raw output renders literally instead
63
- * of being parsed as markdown. A result whose call was saved in
64
- * an earlier file (mid-turn manual save) falls back to a standalone
65
- * one-line block.
54
+ * and fold each assistant block's calls, with their FULL results, into one
55
+ * collapsed Obsidian callout (`> [!quote]- Tool Calls · …`). Thinking folds
56
+ * the same way into `> [!tldr]- Thinking`. Callouts are used instead of
57
+ * HTML `<details>` because Obsidian's views render embedded markdown
58
+ * inside HTML blocks unreliably, while callouts fold and render markdown
59
+ * in both Live Preview and Reading view. Outside Obsidian the callouts
60
+ * degrade to plain blockquotes. Arguments render as full JSON in inline
61
+ * code spans and results verbatim whitespace intact, nothing capped
62
+ * in fenced code blocks (delimiters sized to survive backticks inside the
63
+ * content), so raw output renders literally instead of being parsed as
64
+ * markdown. Nothing is truncated because the file is a documentary record
65
+ * that may be @-referenced back into a conversation: a half result is
66
+ * wasted when the tool is called again and misleading when it is not,
67
+ * while local reading (grep, ranged reads) makes size a non-issue. A
68
+ * result whose call was saved in an earlier file (mid-turn manual save)
69
+ * renders as a standalone block with the same full content.
66
70
  * - Injected prompt blocks: the host client and the agent runtime append
67
71
  * machine-readable XML to user messages — the editor's active selection
68
72
  * (CDATA content), note references and attachments (linked_note /
@@ -194,7 +198,13 @@ import * as fsSync from "node:fs";
194
198
  import * as os from "node:os";
195
199
  import * as path from "node:path";
196
200
  import { debug } from "./debug.js";
197
- import { callout, inlineCode, renderUserMessageText, stripInjectedBlocks } from "./markdown.js";
201
+ import {
202
+ callout,
203
+ fencedCode,
204
+ inlineCode,
205
+ renderUserMessageText,
206
+ stripInjectedBlocks,
207
+ } from "./markdown.js";
198
208
 
199
209
  const CUSTOM_TYPE = "pi-claudian-auto-save-markdown";
200
210
  const ENV_SUBDIR = "PI_SAVE_CONVERSATION_DIR";
@@ -234,7 +244,7 @@ const SAVE_STATE_SCHEMA = "1.2";
234
244
  * the frontmatter and the document heading); additive frontmatter fields do
235
245
  * NOT bump it — they are invisible to any within-major parser.
236
246
  */
237
- const FORMAT_VERSION = "1.5";
247
+ const FORMAT_VERSION = "1.6";
238
248
 
239
249
  /**
240
250
  * Package version of this extension, read best-effort from the adjacent
@@ -255,9 +265,6 @@ const EXTENSION_VERSION: string | null = (() => {
255
265
 
256
266
  const MAX_TITLE_LENGTH = 60;
257
267
  const TITLE_FALLBACK_LENGTH = 40;
258
- const TOOL_RESULT_PREVIEW = 500;
259
- const TOOL_ARGS_PREVIEW = 160;
260
-
261
268
  type AgentMessage = SessionMessageEntry["message"];
262
269
  type UserMessage = Extract<AgentMessage, { role: "user" }>;
263
270
  type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
@@ -959,15 +966,13 @@ export default function (pi: ExtensionAPI) {
959
966
  return "untitled";
960
967
  }
961
968
 
962
- function previewArgs(args: unknown): string {
963
- let s: string;
969
+ /** Full argument JSON on one line (JSON.stringify escapes newlines), never truncated. */
970
+ function renderArgs(args: unknown): string {
964
971
  try {
965
- s = JSON.stringify(args) ?? "";
972
+ return JSON.stringify(args) ?? "";
966
973
  } catch {
967
- s = String(args);
974
+ return String(args);
968
975
  }
969
- s = s.replace(/\s+/g, " ").trim();
970
- return s.length > TOOL_ARGS_PREVIEW ? s.slice(0, TOOL_ARGS_PREVIEW) + " …" : s;
971
976
  }
972
977
 
973
978
  // ---------- markdown rendering ----------
@@ -981,30 +986,37 @@ export default function (pi: ExtensionAPI) {
981
986
  return s.replace(/^(?:[ \t]*\n)+/, "").replace(/\s+$/, "");
982
987
  }
983
988
 
984
- /** One tool call with its paired result preview (null when no result entry exists). */
989
+ /** One tool call with its paired full result (null when no result entry exists). */
985
990
  interface RenderedToolCall {
986
991
  name: string;
987
992
  args: string;
988
993
  result: string | null;
994
+ error: boolean;
989
995
  }
990
996
 
991
- /** Flattened, length-capped result preview with error status suffix. */
992
- function resultPreview(m: ToolResultMessage): string {
997
+ /**
998
+ * Full raw result text: text blocks joined with blank lines, non-text
999
+ * blocks as placeholders. Error status stays OUT of the content (it rides
1000
+ * the call's head line) so the saved text is exactly what the tool
1001
+ * returned.
1002
+ */
1003
+ function resultText(m: ToolResultMessage): string {
993
1004
  const texts: string[] = [];
994
1005
  for (const b of m.content) {
995
1006
  if (b.type === "text") texts.push(b.text);
996
1007
  else texts.push(`_[image: ${b.mimeType}]_`);
997
1008
  }
998
- const flat = texts.join(" ").replace(/\s+/g, " ").trim();
999
- const capped =
1000
- flat.length > TOOL_RESULT_PREVIEW ? flat.slice(0, TOOL_RESULT_PREVIEW) + " …" : flat;
1001
- const status = m.isError ? " (error)" : "";
1002
- return `${capped}${status}`.trim();
1009
+ return texts.join("\n\n").trim();
1003
1010
  }
1004
1011
 
1005
- /** Standalone one-line block for a result whose call is not in this file. */
1012
+ /**
1013
+ * Standalone block for a result whose call is not in this file — same full
1014
+ * content as folded results.
1015
+ */
1006
1016
  function renderToolResult(m: ToolResultMessage): string {
1007
- return `> **Tool · ${m.toolName}** ${inlineCode(resultPreview(m))}`.trim();
1017
+ const text = resultText(m);
1018
+ const err = m.isError ? " (error)" : "";
1019
+ return `**Tool · ${m.toolName}**${err}\n\n${text ? fencedCode(text) : "_(empty result)_"}`;
1008
1020
  }
1009
1021
 
1010
1022
  /** "read, web_search ×2" — tool names with repeat counts, first-seen order. */
@@ -1016,16 +1028,20 @@ export default function (pi: ExtensionAPI) {
1016
1028
 
1017
1029
  /**
1018
1030
  * Fold tool calls and their paired results into one collapsed callout.
1019
- * Argument JSON and result previews are wrapped in inline code spans, so
1020
- * raw output renders literally instead of being parsed as markdown.
1031
+ * Arguments render as full JSON in inline code spans and results verbatim
1032
+ * in fenced code blocks, so raw output renders literally instead of being
1033
+ * parsed as markdown.
1021
1034
  */
1022
1035
  function renderToolCallsCallout(calls: RenderedToolCall[]): string {
1023
1036
  const summary = summarizeToolNames(calls.map((c) => c.name));
1024
1037
  const items = calls.map((c) => {
1025
- const head = c.args ? `**\`${c.name}\`** ${inlineCode(c.args)}` : `**\`${c.name}\`**`;
1038
+ const err = c.error ? " (error)" : "";
1039
+ const head = c.args
1040
+ ? `**\`${c.name}\`**${err} ${inlineCode(c.args)}`
1041
+ : `**\`${c.name}\`**${err}`;
1026
1042
  const result =
1027
- c.result === null ? "_(no result)_" : inlineCode(c.result || "_(empty result)_");
1028
- return `${head}\n\n> ${result}`;
1043
+ c.result === null ? "_(no result)_" : c.result ? fencedCode(c.result) : "_(empty result)_";
1044
+ return `${head}\n\n${result}`;
1029
1045
  });
1030
1046
  return callout("quote", `Tool Calls · ${calls.length} (${summary})`, items.join("\n\n"));
1031
1047
  }
@@ -1061,8 +1077,9 @@ export default function (pi: ExtensionAPI) {
1061
1077
  results.delete(b.id);
1062
1078
  calls.push({
1063
1079
  name: b.name,
1064
- args: previewArgs(b.arguments),
1065
- result: r ? resultPreview(r) : null,
1080
+ args: renderArgs(b.arguments),
1081
+ result: r ? resultText(r) : null,
1082
+ error: r ? r.isError : false,
1066
1083
  });
1067
1084
  }
1068
1085
  }
package/markdown.ts CHANGED
@@ -27,6 +27,19 @@ export function inlineCode(text: string): string {
27
27
  return `${fence}${text}${fence}`;
28
28
  }
29
29
 
30
+ /**
31
+ * Fenced code block for arbitrary raw output (full tool results): the fence
32
+ * is always one backtick longer than the longest backtick run inside the
33
+ * text (and at least three), so content that itself contains backticks
34
+ * cannot break out of the block. Tool output renders literally, whitespace
35
+ * intact, instead of being parsed as markdown.
36
+ */
37
+ export function fencedCode(text: string): string {
38
+ const longest = (text.match(/`+/g) ?? []).reduce((a, r) => Math.max(a, r.length), 0);
39
+ const fence = "`".repeat(Math.max(3, longest + 1));
40
+ return `${fence}\n${text}\n${fence}`;
41
+ }
42
+
30
43
  /**
31
44
  * Obsidian callout (`> [!type]- title`) wrapping a markdown body: every body
32
45
  * line is prefixed with `>` (empty lines become bare `>`), so the body keeps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-auto-save-to-markdown",
3
- "version": "0.8.2",
3
+ "version": "0.9.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",