pi-auto-save-to-markdown 0.8.0 → 0.8.2
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 +21 -10
- package/README.zh.md +17 -8
- package/index.ts +61 -26
- package/markdown.ts +86 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,10 @@ pi install npm:pi-auto-save-to-markdown
|
|
|
28
28
|
Automatic: after every settled agent turn (`agent_settled`), the current
|
|
29
29
|
conversation branch is written to `<cwd>/<folder>/<title>-<key>-<time>.md`.
|
|
30
30
|
|
|
31
|
+
Sessions Pi does not persist (in-memory, `--no-session` — including the
|
|
32
|
+
ephemeral auxiliary agents host clients spawn next to the real conversation,
|
|
33
|
+
such as Claudian's title generation) are skipped by every automatic save; only
|
|
34
|
+
the explicit `/save-conversation` command archives such a session on demand.
|
|
31
35
|
Manual: run `/save-conversation` to save the current branch immediately and
|
|
32
36
|
report the file path.
|
|
33
37
|
|
|
@@ -92,7 +96,7 @@ changes never touch the filename, and manually renamed files are left alone.
|
|
|
92
96
|
---
|
|
93
97
|
title: "Fix login redirect loop"
|
|
94
98
|
agent: "pi"
|
|
95
|
-
format_version: "1.
|
|
99
|
+
format_version: "1.5"
|
|
96
100
|
session_id: "d0a4f541-976d-4d1b-8e1c-30a1f2b3c4d5"
|
|
97
101
|
session_key: "c2088d77"
|
|
98
102
|
branch_last_entry_id: "019be3a2-1f4d-7c8a-9b01-d23e45f6a7b8"
|
|
@@ -119,7 +123,7 @@ User <span style="font-size: 0.5em; color: var(--text-faint);">2026-08-29 13:05:
|
|
|
119
123
|
The login page redirects in a loop after the auth refactor...
|
|
120
124
|
|
|
121
125
|
> [!quote]- Editor Selection
|
|
122
|
-
>
|
|
126
|
+
> [[src/auth/middleware.ts|middleware.ts]] · **lines**: `14-22`
|
|
123
127
|
>
|
|
124
128
|
> export function middleware(request) { … }
|
|
125
129
|
|
|
@@ -158,14 +162,21 @@ editor's active selection, attached or referenced notes, loaded skills — are
|
|
|
158
162
|
re-rendered from their raw XML (which Obsidian cannot render and would show
|
|
159
163
|
as literal angle-bracket text) into generic callouts. No block is parsed
|
|
160
164
|
individually: the title is the tag name in words (`editor_selection` →
|
|
161
|
-
Editor Selection),
|
|
162
|
-
(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
Editor Selection), and the body opens with vault-shaped `path`/`location`
|
|
166
|
+
values as bare Obsidian wikilinks (the aliased filename speaks for itself —
|
|
167
|
+
no `path:` label), followed by the remaining attributes as
|
|
168
|
+
`**name**: value` items, then the content. Every callout is preset-collapsed
|
|
169
|
+
— user-provided blocks (selections, note attachments) as `> [!quote]-`, even
|
|
170
|
+
when they carry only attributes (the client emits note references as
|
|
171
|
+
self-closing tags carrying just a path), agent-side skill traces as a
|
|
172
|
+
`> [!note]- Skill · <name>` marker (the loaded skill's name rides the title,
|
|
173
|
+
so the collapsed marker still says which skill; the location follows in the
|
|
174
|
+
body, the content is dropped) — and consecutive blocks of
|
|
175
|
+
the same tag (nothing but whitespace between them) merge into one callout,
|
|
176
|
+
so a run of note references collapses into a single list. Unknown markup is
|
|
177
|
+
left verbatim, so XML pasted as content is never mangled — and the fallback
|
|
178
|
+
filename slug derives from the typed message with every known block
|
|
179
|
+
stripped.
|
|
169
180
|
|
|
170
181
|
Each message block opens with a setext level-1 info header (`User`,
|
|
171
182
|
`Assistant`) underlined with `===` — one level above the `##` headings AI
|
package/README.zh.md
CHANGED
|
@@ -21,6 +21,10 @@ pi install npm:pi-auto-save-to-markdown
|
|
|
21
21
|
|
|
22
22
|
自动:每个 agent 轮次完全结束(`agent_settled`,含自动重试与压缩全部完成)后,当前对话分支写入 `<cwd>/<文件夹>/<标题>-<key>-<时间>.md`。
|
|
23
23
|
|
|
24
|
+
Pi 未持久化的会话(内存态、`--no-session`——包括宿主客户端在真实对话旁派生的临时辅助
|
|
25
|
+
agent,例如 Claudian 的标题生成)会被所有自动保存路径跳过;只有显式的
|
|
26
|
+
`/save-conversation` 命令才会按需归档此类会话。
|
|
27
|
+
|
|
24
28
|
手动:运行 `/save-conversation` 立即保存当前分支并显示文件路径。
|
|
25
29
|
|
|
26
30
|
批量:运行 `/save-conversation-all` 保存**当前项目的全部 session**——即项目的
|
|
@@ -66,7 +70,7 @@ PI_SAVE_CONVERSATION_DIR=notes/ai pi
|
|
|
66
70
|
---
|
|
67
71
|
title: "修复登录重定向死循环"
|
|
68
72
|
agent: "pi"
|
|
69
|
-
format_version: "1.
|
|
73
|
+
format_version: "1.5"
|
|
70
74
|
session_id: "d0a4f541-976d-4d1b-8e1c-30a1f2b3c4d5"
|
|
71
75
|
session_key: "c2088d77"
|
|
72
76
|
branch_last_entry_id: "019be3a2-1f4d-7c8a-9b01-d23e45f6a7b8"
|
|
@@ -93,7 +97,7 @@ User <span style="font-size: 0.5em; color: var(--text-faint);">2026-08-29 13:05:
|
|
|
93
97
|
auth 重构之后登录页一直重定向死循环……
|
|
94
98
|
|
|
95
99
|
> [!quote]- Editor Selection
|
|
96
|
-
>
|
|
100
|
+
> [[src/auth/middleware.ts|middleware.ts]] · **lines**: `14-22`
|
|
97
101
|
>
|
|
98
102
|
> export function middleware(request) { … }
|
|
99
103
|
|
|
@@ -128,12 +132,17 @@ Markdown 解析。
|
|
|
128
132
|
客户端或 agent 注入到用户消息中的提示块——编辑器当前选区、附加或引用的
|
|
129
133
|
笔记、加载的 skill——会从原始 XML(Obsidian 无法渲染,只会显示成裸露的
|
|
130
134
|
尖括号文本)重新渲染为通用 callout。不做任何逐块解析:标题取标记名的分词
|
|
131
|
-
(`editor_selection` → Editor Selection
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
(`editor_selection` → Editor Selection),正文以形似 vault 相对笔记路径的
|
|
136
|
+
`path`/`location` 值开头——直接渲染为不带标签的 Obsidian wikilink(别名
|
|
137
|
+
文件名自解释,`path:` 标签反而冗余),其余属性以 `**属性**: 值` 跟随,
|
|
138
|
+
最后是引用内容。所有 callout 一律预设折叠——用户提供的块(选区、笔记附件)
|
|
139
|
+
为 `> [!quote]-`(仅有属性的自闭合笔记引用同样折叠),agent 侧痕迹(skill)
|
|
140
|
+
为 `> [!note]- Skill · <名称>` 标记(加载的 skill 名称直接进标题,折叠状态
|
|
141
|
+
也能看到是哪个 skill;location 跟在正文,内容丢弃);连续的同标签块(中间
|
|
142
|
+
只有空白)合并进同一个 callout,一串笔记引用因此收拢为一份列表(skill 标记
|
|
143
|
+
不合并:各自标注各自的 skill)。未知标记原样保留,用户粘贴的
|
|
144
|
+
XML 内容绝不会被误改;回退文件名 slug 也从剥离全部已知块后的纯键入文本
|
|
145
|
+
推导。
|
|
137
146
|
|
|
138
147
|
每个消息块以 setext 一级信息头(`User`、`Assistant`,下一行以 `===` 下划)
|
|
139
148
|
开头——高于 AI 内容常见的 `##` 二级标题,解析时也能与内容中的 `#` 一级标题
|
package/index.ts
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* - Trigger: `agent_settled` — fires once per user prompt, after the turn is
|
|
10
10
|
* fully done (including automatic retries and compaction), so each save
|
|
11
11
|
* captures a settled state of the conversation.
|
|
12
|
+
* - Skip rule: every automatic path (the `agent_settled` save and the batch
|
|
13
|
+
* command's live save) skips sessions Pi does not persist — no session
|
|
14
|
+
* file, i.e. an in-memory SessionManager (`--no-session`): those are
|
|
15
|
+
* ephemeral auxiliary agents host clients spawn next to the real
|
|
16
|
+
* conversation (Claudian's title generation, instruction refinement,
|
|
17
|
+
* inline edits) or one-shot `pi --no-session` runs, not project
|
|
18
|
+
* conversations, and archiving them would mint junk "User's request …"
|
|
19
|
+
* files. The manual /save-conversation command still saves such a session
|
|
20
|
+
* on explicit demand; the batch never sees them (no jsonl on disk).
|
|
12
21
|
* - Location: a subfolder of the session's working directory (`ctx.cwd`, the
|
|
13
22
|
* directory the session was started in), defaulting to `ai-conversations`.
|
|
14
23
|
* Override with the PI_SAVE_CONVERSATION_DIR environment variable; set it to
|
|
@@ -61,12 +70,21 @@
|
|
|
61
70
|
* Obsidian cannot render (unknown tags are not HTML; CDATA is XML), so
|
|
62
71
|
* every block in a known vocabulary is re-rendered generically (see
|
|
63
72
|
* markdown.ts) — no per-tag formatting: the callout title is the tag name
|
|
64
|
-
* in words,
|
|
65
|
-
* (
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
73
|
+
* in words, the body opens with vault-shaped path/location values as
|
|
74
|
+
* bare wikilinks (the aliased filename is self-explanatory — a `path:`
|
|
75
|
+
* label is noise) followed by the remaining attributes as
|
|
76
|
+
* "**name**: value" lines, then the content (the client's `]]>`
|
|
77
|
+
* split-escaping reversed). Every callout is preset-collapsed: visible
|
|
78
|
+
* blocks (selections, note references, attachments) as `> [!quote]-`,
|
|
79
|
+
* whether they carry content or only attributes (the client emits note
|
|
80
|
+
* references as self-closing tags whose whole payload is a path
|
|
81
|
+
* attribute), agent-side traces (skills) as a `> [!note]- Skill · <name>`
|
|
82
|
+
* marker — the loaded skill's name rides the title so the collapsed
|
|
83
|
+
* marker still says which skill, the location follows in the body, the
|
|
84
|
+
* content is dropped — and consecutive visible same-tag blocks (nothing
|
|
85
|
+
* but whitespace between them) merge into one callout, so a run of note
|
|
86
|
+
* references collapses into a single list (skill markers never merge:
|
|
87
|
+
* each names its own skill). Title
|
|
70
88
|
* derivation strips every known block — the typed message is the title.
|
|
71
89
|
* Unknown markup is left verbatim so XML pasted as content is never
|
|
72
90
|
* mangled.
|
|
@@ -216,7 +234,7 @@ const SAVE_STATE_SCHEMA = "1.2";
|
|
|
216
234
|
* the frontmatter and the document heading); additive frontmatter fields do
|
|
217
235
|
* NOT bump it — they are invisible to any within-major parser.
|
|
218
236
|
*/
|
|
219
|
-
const FORMAT_VERSION = "1.
|
|
237
|
+
const FORMAT_VERSION = "1.5";
|
|
220
238
|
|
|
221
239
|
/**
|
|
222
240
|
* Package version of this extension, read best-effort from the adjacent
|
|
@@ -1916,6 +1934,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1916
1934
|
|
|
1917
1935
|
// 1. Automatic: save after every settled agent turn.
|
|
1918
1936
|
pi.on("agent_settled", async (_event: AgentSettledEvent, ctx: ExtensionContext) => {
|
|
1937
|
+
// Non-persisted sessions (in-memory, `--no-session`) are ephemeral
|
|
1938
|
+
// auxiliary agents — see the skip rule in the header. The manual
|
|
1939
|
+
// /save-conversation command below is the explicit-demand escape hatch.
|
|
1940
|
+
if (!ctx.sessionManager.getSessionFile()) {
|
|
1941
|
+
debug(
|
|
1942
|
+
"agent_settled — session has no session file (in-memory / --no-session); skipping auto-save",
|
|
1943
|
+
);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1919
1946
|
debug("agent_settled — saving conversation");
|
|
1920
1947
|
try {
|
|
1921
1948
|
const live = liveContext(ctx);
|
|
@@ -1996,28 +2023,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
1996
2023
|
|
|
1997
2024
|
// The live session first, through the normal path: its in-memory tree may
|
|
1998
2025
|
// be fresher than disk, and its state entry goes through pi.appendEntry.
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2026
|
+
// A non-persisted current session (in-memory / --no-session) is skipped —
|
|
2027
|
+
// see the skip rule in the header.
|
|
2028
|
+
if (!ctx.sessionManager.getSessionFile()) {
|
|
2029
|
+
debug(
|
|
2030
|
+
"/" + COMMAND_ALL + " — current session not persisted (in-memory / --no-session); skipped",
|
|
2031
|
+
);
|
|
2032
|
+
} else {
|
|
2033
|
+
try {
|
|
2034
|
+
const r = await runSave(live);
|
|
2035
|
+
count(r);
|
|
2036
|
+
if (ctx.hasUI) {
|
|
2037
|
+
const target = r.file ? relativeForUser(live, r.file) : "";
|
|
2038
|
+
ctx.ui.notify(`${NOTIFY_TAG} ${r.message}${target ? ` → ${target}` : ""}`, "info");
|
|
2039
|
+
if (r.recovered && r.file) {
|
|
2040
|
+
ctx.ui.notify(`${NOTIFY_TAG} ${r.recovered} → ${target}`, "warning");
|
|
2041
|
+
}
|
|
2042
|
+
if (r.switchedFrom && r.file) {
|
|
2043
|
+
ctx.ui.notify(
|
|
2044
|
+
`${NOTIFY_TAG} branch changed — new branch file ${target}; the earlier branch file ${r.switchedFrom} is kept`,
|
|
2045
|
+
"info",
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2007
2048
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
);
|
|
2049
|
+
} catch (e) {
|
|
2050
|
+
failed.push("<current session>");
|
|
2051
|
+
debug("/" + COMMAND_ALL + " live save failed:", String(e));
|
|
2052
|
+
if (ctx.hasUI) {
|
|
2053
|
+
ctx.ui.notify(`${NOTIFY_TAG} save failed: ${String(e)}`, "error");
|
|
2013
2054
|
}
|
|
2014
2055
|
}
|
|
2015
|
-
} catch (e) {
|
|
2016
|
-
failed.push("<current session>");
|
|
2017
|
-
debug("/" + COMMAND_ALL + " live save failed:", String(e));
|
|
2018
|
-
if (ctx.hasUI) {
|
|
2019
|
-
ctx.ui.notify(`${NOTIFY_TAG} save failed: ${String(e)}`, "error");
|
|
2020
|
-
}
|
|
2021
2056
|
}
|
|
2022
2057
|
|
|
2023
2058
|
// The project's sessions directory — every session jsonl lives there.
|
package/markdown.ts
CHANGED
|
@@ -54,8 +54,11 @@ export function callout(type: string, title: string, body: string, fold = true):
|
|
|
54
54
|
* references (linked_note / linked_content — the client's attachment
|
|
55
55
|
* mechanism; a typed @-mention stays as plain text in the message, these
|
|
56
56
|
* are the machine copy).
|
|
57
|
-
* - Hidden: agent-side traces (loaded skills) — rendered as a
|
|
58
|
-
*
|
|
57
|
+
* - Hidden: agent-side traces (loaded skills) — rendered as a folded
|
|
58
|
+
* `> [!note]- Skill · <name>` marker with the name riding the title
|
|
59
|
+
* (which skill was loaded is the marker's whole meaning and must survive
|
|
60
|
+
* the collapsed view), the content dropped (a skill is re-loadable from
|
|
61
|
+
* its location).
|
|
59
62
|
*
|
|
60
63
|
* Unknown tags are left verbatim: this is both the safety boundary against
|
|
61
64
|
* mangling pasted XML and a soft failure mode for future client tags (they
|
|
@@ -151,60 +154,105 @@ function wikilinkOf(path: string): string {
|
|
|
151
154
|
return base && base !== path ? `[[${path}|${base}]]` : `[[${path}]]`;
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
/** Whether an attribute renders as a bare wikilink (a vault-shaped path/location). */
|
|
158
|
+
function isWikilinkAttr(p: { name: string; value: string }): boolean {
|
|
159
|
+
return (p.name === "path" || p.name === "location") && isVaultNotePath(p.value);
|
|
160
|
+
}
|
|
161
|
+
|
|
154
162
|
/**
|
|
155
|
-
*
|
|
156
|
-
* like vault-relative note
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
163
|
+
* Attribute pairs of one injected block as body items: a `path`/`location`
|
|
164
|
+
* value shaped like a vault-relative note path becomes a BARE wikilink —
|
|
165
|
+
* no `**name**:` prefix, because the aliased filename is self-explanatory
|
|
166
|
+
* (the file is user-provided material) and the label only adds noise — and
|
|
167
|
+
* sorts first, so the reference leads the line; every other attribute stays
|
|
168
|
+
* a labeled `**name**: value` item in source order.
|
|
160
169
|
*/
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
function attrItems(attrs: string): string[] {
|
|
171
|
+
const pairs = parseAttrs(attrs);
|
|
172
|
+
return [
|
|
173
|
+
...pairs.filter(isWikilinkAttr).map((p) => wikilinkOf(p.value)),
|
|
174
|
+
...pairs.filter((p) => !isWikilinkAttr(p)).map((p) => `**${p.name}**: ${inlineCode(p.value)}`),
|
|
175
|
+
];
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
* Hidden block as its own self-contained marker callout: the `name`
|
|
180
|
+
* attribute joins the title — WHICH skill was loaded is the marker's whole
|
|
181
|
+
* meaning, and a collapsed `Skill` alone would say nothing — while the
|
|
182
|
+
* remaining attributes (the location) form the body and the content is
|
|
183
|
+
* dropped (a skill is re-loadable from its location). A hidden block with
|
|
184
|
+
* no attributes at all carries no information and is removed.
|
|
185
|
+
*/
|
|
186
|
+
function hiddenCallout(tag: string, attrs: string): string {
|
|
187
|
+
const pairs = parseAttrs(attrs);
|
|
188
|
+
if (pairs.length === 0) return "";
|
|
189
|
+
const name = pairs.find((p) => p.name === "name")?.value;
|
|
190
|
+
const body = pairs
|
|
191
|
+
.filter((p) => p.name !== "name")
|
|
192
|
+
.map((p) => (isWikilinkAttr(p) ? wikilinkOf(p.value) : `**${p.name}**: ${inlineCode(p.value)}`))
|
|
193
|
+
.join(" · ");
|
|
194
|
+
return callout("note", name ? `${tagTitle(tag)} · ${name}` : tagTitle(tag), body);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One VISIBLE injected block's rendered body (no callout wrapper; hidden
|
|
199
|
+
* blocks render through hiddenCallout): the attribute items, then (after a
|
|
200
|
+
* blank line) the content.
|
|
177
201
|
*
|
|
178
202
|
* linked_note is the one content concession: its whole payload IS a note
|
|
179
203
|
* reference, so content shaped like a vault note path renders as a wikilink
|
|
180
204
|
* too — the same mechanical shape test, no semantics.
|
|
181
205
|
*/
|
|
182
|
-
function
|
|
183
|
-
const attrLine =
|
|
184
|
-
.map((p) => `**${p.name}**: ${formatAttrValue(p.name, p.value)}`)
|
|
185
|
-
.join(" · ");
|
|
186
|
-
const title = tagTitle(tag);
|
|
187
|
-
if (HIDDEN_TAGS.has(tag)) return callout("note", title, attrLine, false);
|
|
206
|
+
function injectedBlockBody(tag: string, attrs: string, content: string): string {
|
|
207
|
+
const attrLine = attrItems(attrs).join(" · ");
|
|
188
208
|
const text = content ? unwrapCDATA(content).trim() : "";
|
|
189
209
|
const body = tag === "linked_note" && isVaultNotePath(text) ? wikilinkOf(text) : text;
|
|
190
|
-
return
|
|
191
|
-
"quote",
|
|
192
|
-
title,
|
|
193
|
-
attrLine ? (body ? `${attrLine}\n\n${body}` : attrLine) : body,
|
|
194
|
-
Boolean(body),
|
|
195
|
-
);
|
|
210
|
+
return attrLine ? (body ? `${attrLine}\n\n${body}` : attrLine) : body;
|
|
196
211
|
}
|
|
197
212
|
|
|
198
213
|
/**
|
|
199
214
|
* User message text for the saved body: every known injected block renders
|
|
200
|
-
* as a callout
|
|
215
|
+
* as a preset-collapsed callout — visible blocks as `> [!quote]-`, hidden
|
|
216
|
+
* blocks as a `> [!note]-` marker — and a RUN of visible same-tag blocks
|
|
217
|
+
* with nothing but whitespace between them merges into ONE callout (a user
|
|
218
|
+
* attaching five notes saves one Linked Content callout listing five
|
|
219
|
+
* wikilinks, not five callouts; hidden markers never merge — each names its
|
|
220
|
+
* own skill). Anything else stays verbatim.
|
|
201
221
|
*/
|
|
202
222
|
export function renderUserMessageText(text: string): string {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
223
|
+
const re = new RegExp(INJECTED_BLOCK_RE, "g");
|
|
224
|
+
let out = "";
|
|
225
|
+
let pos = 0;
|
|
226
|
+
let runTag: string | null = null;
|
|
227
|
+
const runBodies: string[] = [];
|
|
228
|
+
const flushRun = () => {
|
|
229
|
+
if (runTag === null) return;
|
|
230
|
+
out += callout("quote", tagTitle(runTag), runBodies.join("\n\n"));
|
|
231
|
+
runTag = null;
|
|
232
|
+
runBodies.length = 0;
|
|
233
|
+
};
|
|
234
|
+
let m: RegExpExecArray | null;
|
|
235
|
+
while ((m = re.exec(text)) !== null) {
|
|
236
|
+
const tag = m[1];
|
|
237
|
+
const gap = text.slice(pos, m.index);
|
|
238
|
+
const mergeable = runTag !== null && tag === runTag && gap.trim() === "";
|
|
239
|
+
if (!mergeable) {
|
|
240
|
+
// A different tag, real text between the blocks, or a hidden block
|
|
241
|
+
// (self-contained markers never merge) ends the run; the gap is
|
|
242
|
+
// emitted only then — inside a run it is whitespace.
|
|
243
|
+
flushRun();
|
|
244
|
+
out += gap;
|
|
245
|
+
}
|
|
246
|
+
if (HIDDEN_TAGS.has(tag)) {
|
|
247
|
+
out += hiddenCallout(tag, m[2]);
|
|
248
|
+
} else {
|
|
249
|
+
if (runTag === null) runTag = tag;
|
|
250
|
+
runBodies.push(injectedBlockBody(tag, m[2], m[3] ?? ""));
|
|
251
|
+
}
|
|
252
|
+
pos = re.lastIndex;
|
|
253
|
+
}
|
|
254
|
+
flushRun();
|
|
255
|
+
return out + text.slice(pos);
|
|
208
256
|
}
|
|
209
257
|
|
|
210
258
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-auto-save-to-markdown",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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",
|