@pi-claudian/auto-save-to-markdown 0.1.2 → 0.1.3

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
@@ -78,11 +78,15 @@ session_file: "~/.pi/agent/sessions/--Users-me-project-20260829-050500_ab12.json
78
78
 
79
79
  # Fix login redirect loop
80
80
 
81
- ## User · 13:05:12
81
+ User · 13:05:12
82
+ ===
82
83
 
83
84
  The login page redirects in a loop after the auth refactor...
84
85
 
85
- ## Assistant · 13:05:40 · claude-sonnet-4-5
86
+ ---
87
+
88
+ Assistant · 13:05:40 · claude-sonnet-4-5
89
+ ===
86
90
 
87
91
  <details>
88
92
  <summary>Thinking</summary>
@@ -98,6 +102,8 @@ I'll trace the middleware order first.
98
102
  - `read` — {"filePath":"/Users/me/project/src/auth/middleware.ts"}
99
103
 
100
104
  > **Tool · read** /Users/me/project/src/auth/middleware.ts — 120 lines …
105
+
106
+ ---
101
107
  ```
102
108
 
103
109
  The body renders user and assistant messages in full (assistant thinking is
@@ -105,6 +111,13 @@ kept in a collapsible `<details>` block) and summarizes each tool call and
105
111
  result in one line, so the file stays readable while still showing what the
106
112
  agent did.
107
113
 
114
+ Each message block opens with a setext level-1 info header (`User · …`,
115
+ underlined with `===`) — one level above the `##` headings AI content
116
+ typically starts with, and distinguishable from content `#` headings when
117
+ parsing — and ends with a `---` separator wrapped in single blank lines
118
+ (extra blank lines are trimmed), so blocks are easy to tell apart both when
119
+ reading and when splitting the file programmatically.
120
+
108
121
  ### Fragmented thinking repair
109
122
 
110
123
  Some upstream reasoning streams (observed with z-ai/GLM via OpenRouter) store
package/README.zh.md CHANGED
@@ -68,11 +68,15 @@ session_file: "~/.pi/agent/sessions/--Users-me-project-20260829-050500_ab12.json
68
68
 
69
69
  # 修复登录重定向死循环
70
70
 
71
- ## User · 13:05:12
71
+ User · 13:05:12
72
+ ===
72
73
 
73
74
  auth 重构之后登录页一直重定向死循环……
74
75
 
75
- ## Assistant · 13:05:40 · claude-sonnet-4-5
76
+ ---
77
+
78
+ Assistant · 13:05:40 · claude-sonnet-4-5
79
+ ===
76
80
 
77
81
  <details>
78
82
  <summary>Thinking</summary>
@@ -88,12 +92,19 @@ auth 重构之后登录页一直重定向死循环……
88
92
  - `read` — {"filePath":"/Users/me/project/src/auth/middleware.ts"}
89
93
 
90
94
  > **Tool · read** /Users/me/project/src/auth/middleware.ts — 120 lines …
95
+
96
+ ---
91
97
  ```
92
98
 
93
99
  正文完整渲染 user / assistant 消息(assistant 的 thinking 放在可折叠的
94
100
  `<details>` 块中),每个工具调用和结果各压缩成一行摘要,既可读又能看出
95
101
  agent 做了什么。
96
102
 
103
+ 每个消息块以 setext 一级信息头(`User · …`,下一行以 `===` 下划)开头——
104
+ 高于 AI 内容常见的 `##` 二级标题,解析时也能与内容中的 `#` 一级标题区分
105
+ 开——并以"上下各一个空行"包裹的 `---` 分隔线结尾(多余空行会被裁剪),
106
+ 无论是阅读还是程序化切分,都能清楚地区分每个消息块。
107
+
97
108
  ### 碎片化 thinking 修复
98
109
 
99
110
  部分上游推理流(在 z-ai/GLM 经 OpenRouter 的场景中观察到)会把 thinking
package/index.ts CHANGED
@@ -20,6 +20,10 @@
20
20
  * - Frontmatter: title, session id, tree (branch key), model, provider,
21
21
  * cumulative cost and tokens (input, output, cache read/write), message
22
22
  * count, created/updated timestamps, project root and session file.
23
+ * - Body format: every message block opens with a setext-H1 info header
24
+ * (`User · HH:MM:SS` / `Assistant · HH:MM:SS · model`, underlined with
25
+ * `===`, distinct from the `#`/`##` ATX headings AI content uses) and
26
+ * ends with a `---` separator wrapped in single blank lines.
23
27
  * - Branching: each file records exactly ONE branch (the root→leaf path
24
28
  * returned by sessionManager.getBranch()). State is persisted via
25
29
  * `pi.appendEntry()` custom entries, which are part of the session tree
@@ -278,10 +282,21 @@ export default function (pi: ExtensionAPI) {
278
282
 
279
283
  // ---------- markdown rendering ----------
280
284
 
285
+ /**
286
+ * Strip leading blank lines and trailing whitespace from a rendered block,
287
+ * so joins and separators always keep exactly one blank line around them
288
+ * no matter what blank lines the content itself starts or ends with.
289
+ */
290
+ function tighten(s: string): string {
291
+ return s.replace(/^(?:[ \t]*\n)+/, "").replace(/\s+$/, "");
292
+ }
293
+
281
294
  function renderAssistant(m: AssistantMessage, t: string): string {
282
295
  // Render blocks in their original chronological order: thinking always
283
296
  // precedes the text it produced, instead of being grouped after the fact.
284
- const header = `## Assistant · ${t}${m.model ? ` · ${m.model}` : ""}`;
297
+ // Setext H1 (`===` underline): one level above the `##` headings AI
298
+ // content typically starts with, and distinct from content `#` headings.
299
+ const header = `Assistant · ${t}${m.model ? ` · ${m.model}` : ""}\n===`;
285
300
  const parts: string[] = [];
286
301
  const thinkings: string[] = [];
287
302
  const flushThinking = () => {
@@ -332,7 +347,7 @@ export default function (pi: ExtensionAPI) {
332
347
  const m = e.message;
333
348
  const t = clock(e.timestamp);
334
349
  if (m.role === "user") {
335
- blocks.push(`## User · ${t}\n\n${userText(m.content)}`);
350
+ blocks.push(`User · ${t}\n===\n\n${userText(m.content)}`);
336
351
  } else if (m.role === "assistant") {
337
352
  blocks.push(renderAssistant(m, t));
338
353
  } else if (m.role === "toolResult") {
@@ -341,7 +356,12 @@ export default function (pi: ExtensionAPI) {
341
356
  // Other roles (custom, bashExecution, branchSummary, compactionSummary)
342
357
  // are not part of the rendered conversation record.
343
358
  }
344
- return blocks.join("\n\n");
359
+ if (blocks.length === 0) return "";
360
+ // Every block ends with a `---` separator wrapped in single blank lines
361
+ // (the blank line above also keeps `---` from turning the last content
362
+ // line into a setext H2). The trailing separator after the final block
363
+ // makes later appends uniform: new blocks simply continue after it.
364
+ return `${blocks.map(tighten).join("\n\n---\n\n")}\n\n---\n`;
345
365
  }
346
366
 
347
367
  // ---------- frontmatter ----------
@@ -560,8 +580,8 @@ export default function (pi: ExtensionAPI) {
560
580
 
561
581
  if (fullCreate) {
562
582
  const meta = computeMeta(ctx, plan.pathMessages, plan.branchKey, undefined);
563
- const body = renderEntries(plan.pathMessages);
564
- const content = `${frontmatter(meta)}\n\n# ${meta.title}\n\n${body}\n`;
583
+ const body = renderEntries(plan.pathMessages); // ends with the trailing separator
584
+ const content = `${frontmatter(meta)}\n\n# ${meta.title}\n\n${body}`;
565
585
  await atomicWrite(filePath, content);
566
586
  debug("created conversation file:", filePath);
567
587
  return {
@@ -584,10 +604,15 @@ export default function (pi: ExtensionAPI) {
584
604
 
585
605
  const existing = await fs.readFile(filePath, "utf-8");
586
606
  const meta = computeMeta(ctx, plan.pathMessages, plan.branchKey, parseCreated(existing));
587
- const appended = renderEntries(plan.appendEntries);
607
+ const appended = renderEntries(plan.appendEntries); // ends with the trailing separator
588
608
  let updated = replaceFrontmatter(existing, frontmatter(meta));
589
- if (!updated.endsWith("\n")) updated += "\n";
590
- updated += `\n${appended}\n`;
609
+ // Collapse trailing blank lines to a single newline so the separator
610
+ // always has exactly one blank line above it, whatever earlier saves
611
+ // (or a manual edit) left behind.
612
+ updated = updated.replace(/\s*$/, "\n");
613
+ // Files written by the old format end without a `---` separator; add one
614
+ // at the boundary so old and new content stay delimited.
615
+ updated += updated.endsWith("---\n") ? `\n${appended}` : `\n---\n\n${appended}`;
591
616
  await atomicWrite(filePath, updated);
592
617
  debug("appended", plan.appendEntries.length, "entries to:", filePath);
593
618
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-claudian/auto-save-to-markdown",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",