@herbertgao/pi-extensions 2026.8.8 → 2026.8.9

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.
Files changed (74) hide show
  1. package/README.md +3 -3
  2. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
  3. package/node_modules/@czottmann/pi-automode/README.md +78 -112
  4. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
  5. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
  6. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
  7. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
  8. package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
  9. package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
  10. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
  11. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
  12. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
  13. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
  14. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
  15. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
  16. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
  17. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
  18. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
  19. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
  20. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
  21. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
  22. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
  23. package/node_modules/@czottmann/pi-automode/package.json +9 -2
  24. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
  25. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
  26. package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +15 -13
  31. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  32. package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
  33. package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
  34. package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
  35. package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
  36. package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
  37. package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
  38. package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
  39. package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
  40. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
  41. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
  42. package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
  43. package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
  44. package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
  45. package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
  46. package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
  47. package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
  48. package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
  49. package/node_modules/@herbertgao/pi-recap/README.md +5 -3
  50. package/node_modules/@herbertgao/pi-recap/package.json +9 -3
  51. package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
  52. package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
  53. package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
  54. package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
  55. package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
  56. package/node_modules/@herbertgao/pi-rename/README.md +13 -11
  57. package/node_modules/@herbertgao/pi-rename/package.json +13 -3
  58. package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
  59. package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
  60. package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
  61. package/node_modules/@herbertgao/pi-stash/package.json +3 -3
  62. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  63. package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
  64. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  65. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
  66. package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
  67. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
  68. package/node_modules/pi-web-access/CHANGELOG.md +13 -0
  69. package/node_modules/pi-web-access/README.md +2 -2
  70. package/node_modules/pi-web-access/chrome-cookies.ts +12 -7
  71. package/node_modules/pi-web-access/gemini-search.ts +28 -10
  72. package/node_modules/pi-web-access/index.ts +24 -10
  73. package/node_modules/pi-web-access/package.json +1 -1
  74. package/package.json +13 -12
@@ -1,27 +1,25 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core"
2
- import { complete, type Message } from "@earendil-works/pi-ai/compat"
3
- import {
4
- generateRename,
5
- getUserMessageContext,
6
- sanitizeRenameText,
7
- } from "@herbertgao/pi-rename/naming"
2
+ import type { Message } from "@earendil-works/pi-ai/compat"
3
+ import { sanitizeRenameText } from "@herbertgao/pi-rename/naming"
8
4
  import type {
9
5
  ExtensionAPI,
10
6
  ExtensionCommandContext,
11
- SessionEntry,
12
7
  SlashCommandInfo,
13
8
  } from "@earendil-works/pi-coding-agent"
14
9
  import {
15
10
  BorderedLoader,
16
11
  convertToLlm,
17
12
  serializeConversation,
13
+ sessionEntryToContextMessages,
18
14
  } from "@earendil-works/pi-coding-agent"
15
+ import { completeText } from "./complete-text.js"
19
16
  import { existsSync } from "node:fs"
20
17
  import { mkdir, readFile, writeFile } from "node:fs/promises"
21
18
  import { tmpdir } from "node:os"
22
19
  import { join } from "node:path"
23
20
 
24
21
  const HANDOFF_SKILL_COMMAND = "skill:handoff"
22
+ const HANDOFF_BRIDGE_COMMAND = "__pi-handoff-session"
25
23
 
26
24
  const HANDOFF_SYSTEM_PROMPT = `You are a context transfer assistant. Generate a handoff markdown document for a fresh coding agent.
27
25
 
@@ -42,50 +40,6 @@ type HandoffSkill = {
42
40
  instructions: string
43
41
  }
44
42
 
45
- function entryToMessage(entry: SessionEntry): AgentMessage | undefined {
46
- if (entry.type === "message") return entry.message
47
- if (entry.type === "compaction") {
48
- return {
49
- role: "compactionSummary",
50
- summary: entry.summary,
51
- tokensBefore: entry.tokensBefore,
52
- timestamp: new Date(entry.timestamp).getTime(),
53
- }
54
- }
55
- return undefined
56
- }
57
-
58
- function getHandoffMessages(branch: SessionEntry[]): AgentMessage[] {
59
- let compactionIndex = -1
60
- for (let index = branch.length - 1; index >= 0; index -= 1) {
61
- if (branch[index]?.type === "compaction") {
62
- compactionIndex = index
63
- break
64
- }
65
- }
66
-
67
- if (compactionIndex < 0) {
68
- return branch.map(entryToMessage).filter((message) => message !== undefined)
69
- }
70
-
71
- const compaction = branch[compactionIndex]
72
- const firstKeptIndex =
73
- compaction?.type === "compaction"
74
- ? branch.findIndex((entry) => entry.id === compaction.firstKeptEntryId)
75
- : -1
76
- const compactedBranch = [
77
- compaction,
78
- ...(firstKeptIndex >= 0
79
- ? branch.slice(firstKeptIndex, compactionIndex)
80
- : []),
81
- ...branch.slice(compactionIndex + 1),
82
- ].filter((entry) => entry !== undefined)
83
-
84
- return compactedBranch
85
- .map(entryToMessage)
86
- .filter((message) => message !== undefined)
87
- }
88
-
89
43
  async function loadHandoffSkill(
90
44
  pi: ExtensionAPI,
91
45
  ): Promise<HandoffSkill | undefined> {
@@ -104,28 +58,6 @@ async function loadHandoffSkill(
104
58
  }
105
59
  }
106
60
 
107
- function textFromMessage(message: AgentMessage): string | undefined {
108
- if (message.role !== "user") return undefined
109
- const content = message.content
110
- if (typeof content === "string") return content.trim()
111
- if (!Array.isArray(content)) return undefined
112
-
113
- return content
114
- .filter((part): part is { type: "text"; text: string } => {
115
- return (
116
- typeof part === "object" &&
117
- part !== null &&
118
- "type" in part &&
119
- part.type === "text" &&
120
- "text" in part &&
121
- typeof part.text === "string"
122
- )
123
- })
124
- .map((part) => part.text)
125
- .join("\n")
126
- .trim()
127
- }
128
-
129
61
  function dateStamp(): string {
130
62
  const [date] = new Date().toISOString().split("T")
131
63
  return date ?? "handoff"
@@ -145,17 +77,6 @@ async function nextHandoffPath(slug: string): Promise<string> {
145
77
  throw new Error("Could not allocate a unique handoff file path")
146
78
  }
147
79
 
148
- async function generateSessionName(
149
- ctx: ExtensionCommandContext,
150
- messages: readonly AgentMessage[],
151
- ): Promise<string> {
152
- const context = getUserMessageContext(messages)
153
- if (!context) return "handoff-session"
154
-
155
- const result = await generateRename(ctx, { kind: "missing" }, context)
156
- return result?.name ?? "handoff-session"
157
- }
158
-
159
80
  function buildNewSessionPrompt(handoffPath: string): string {
160
81
  return [
161
82
  "Continue from this handoff:",
@@ -164,35 +85,11 @@ function buildNewSessionPrompt(handoffPath: string): string {
164
85
  ].join("\n\n")
165
86
  }
166
87
 
167
- function summarizePromptContext(ctx: ExtensionCommandContext): string {
168
- const promptOptions = ctx.getSystemPromptOptions()
169
- const skills = (promptOptions.skills ?? [])
170
- .map((skill) => skill.name)
171
- .toSorted()
172
- const contextFiles = (promptOptions.contextFiles ?? [])
173
- .map((file) => file.path)
174
- .toSorted()
175
-
176
- const lines = ["## Loaded pi context", ""]
177
- lines.push("### Skills", "")
178
- lines.push(
179
- ...(skills.length > 0 ? skills.map((name) => `- \`${name}\``) : ["- None"]),
180
- )
181
- lines.push("", "### Context files", "")
182
- lines.push(
183
- ...(contextFiles.length > 0
184
- ? contextFiles.map((filePath) => `- \`${filePath}\``)
185
- : ["- None"]),
186
- )
187
- return lines.join("\n")
188
- }
189
-
190
88
  function buildDocumentWithMetadata(options: {
191
89
  generated: string
192
90
  focus: string
193
91
  handoffSkillPath: string
194
92
  parentSession: string | undefined
195
- promptContext: string
196
93
  }): string {
197
94
  const previousSession = options.parentSession
198
95
  ? [
@@ -216,8 +113,6 @@ function buildDocumentWithMetadata(options: {
216
113
  "",
217
114
  previousSession,
218
115
  "",
219
- options.promptContext,
220
- "",
221
116
  "## Handoff policy",
222
117
  "",
223
118
  "- Skill: `handoff`",
@@ -239,9 +134,6 @@ async function generateHandoffDocument(options: {
239
134
  const model = options.ctx.model
240
135
  if (!model) throw new Error("No model selected")
241
136
 
242
- const auth = await options.ctx.modelRegistry.getApiKeyAndHeaders(model)
243
- if (!auth.ok) throw new Error(auth.error)
244
-
245
137
  const conversationText = serializeConversation(convertToLlm(options.messages))
246
138
  const userMessage: Message = {
247
139
  role: "user",
@@ -271,140 +163,143 @@ async function generateHandoffDocument(options: {
271
163
  timestamp: Date.now(),
272
164
  }
273
165
 
274
- const response = await complete(
166
+ return completeText(
167
+ options.ctx,
275
168
  model,
276
- { systemPrompt: HANDOFF_SYSTEM_PROMPT, messages: [userMessage] },
277
- {
278
- ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
279
- ...(auth.headers ? { headers: auth.headers } : {}),
280
- ...(options.signal ? { signal: options.signal } : {}),
281
- },
169
+ HANDOFF_SYSTEM_PROMPT,
170
+ userMessage,
171
+ options.signal,
282
172
  )
283
-
284
- if (response.stopReason === "aborted") return null
285
-
286
- return response.content
287
- .filter((content): content is { type: "text"; text: string } => {
288
- return content.type === "text"
289
- })
290
- .map((content) => content.text)
291
- .join("\n")
292
- .trim()
293
173
  }
294
174
 
295
- export default function (pi: ExtensionAPI): void {
296
- pi.registerCommand("handoff-session", {
297
- description:
298
- "Create a new session from a handoff generated by the handoff skill",
299
- handler: async (_args, ctx) => {
300
- if (ctx.mode !== "tui") {
301
- ctx.ui.notify("handoff-session requires interactive TUI mode.", "error")
302
- return
303
- }
175
+ const DEFAULT_HANDOFF_FOCUS = "continue the current work"
176
+ const HANDOFF_REQUEST_RE = /(^|\s)-handoff(?=\s|$)/
304
177
 
305
- if (!ctx.model) {
306
- ctx.ui.notify("No model selected.", "error")
307
- return
308
- }
178
+ function getHandoffFocus(text: string): string | undefined {
179
+ if (!HANDOFF_REQUEST_RE.test(text)) return undefined
180
+ return text.replace(HANDOFF_REQUEST_RE, " ").trim() || DEFAULT_HANDOFF_FOCUS
181
+ }
309
182
 
310
- const handoffSkill = await loadHandoffSkill(pi)
311
- if (!handoffSkill) {
312
- ctx.ui.notify(
313
- "handoff-session requires a discoverable skill named exactly `handoff`. Install it, then run /reload.",
314
- "error",
315
- )
316
- return
317
- }
183
+ async function runHandoff(
184
+ pi: ExtensionAPI,
185
+ ctx: ExtensionCommandContext,
186
+ focus: string,
187
+ ): Promise<void> {
188
+ if (!ctx.model) {
189
+ ctx.ui.notify("No model selected.", "error")
190
+ return
191
+ }
318
192
 
319
- const focus = (
320
- await ctx.ui.input(
321
- "What will the next session be used for?",
322
- "continue the current work",
323
- )
324
- )?.trim()
325
- if (!focus) {
326
- ctx.ui.notify("Handoff session cancelled.", "warning")
327
- return
328
- }
193
+ const handoffSkill = await loadHandoffSkill(pi)
194
+ if (!handoffSkill) {
195
+ ctx.ui.notify(
196
+ "Handoff requires a discoverable skill named exactly `handoff`. Install it, then run /reload.",
197
+ "error",
198
+ )
199
+ return
200
+ }
329
201
 
330
- const branch = ctx.sessionManager.getBranch()
331
- const messages = getHandoffMessages(branch)
332
- if (messages.length === 0) {
333
- ctx.ui.notify("No conversation to hand off.", "error")
334
- return
335
- }
202
+ const messages = ctx.sessionManager
203
+ .buildContextEntries()
204
+ .flatMap(sessionEntryToContextMessages)
205
+ if (messages.length === 0) {
206
+ ctx.ui.notify("No conversation to hand off.", "error")
207
+ return
208
+ }
336
209
 
337
- const parentSession = ctx.sessionManager.getSessionFile() ?? undefined
338
- const generated = await ctx.ui.custom<string | null>(
339
- (tui, theme, _kb, done) => {
340
- const model = ctx.model
341
- const modelLabel = model
342
- ? `${model.provider}/${"id" in model ? model.id : "selected"}`
343
- : "selected model"
344
- const loader = new BorderedLoader(
345
- tui,
346
- theme,
347
- `Generating handoff with ${modelLabel}...`,
348
- )
349
- loader.onAbort = () => done(null)
350
-
351
- generateHandoffDocument({
352
- ctx: ctx as ExtensionCommandContext,
353
- handoffSkill,
354
- messages,
355
- focus,
356
- parentSession,
357
- signal: loader.signal,
358
- })
359
- .then(done)
360
- .catch((error) => {
361
- console.error("Handoff generation failed:", error)
362
- done(
363
- `__ERROR__${error instanceof Error ? error.message : String(error)}`,
364
- )
365
- })
366
-
367
- return loader
368
- },
210
+ const parentSession = ctx.sessionManager.getSessionFile() ?? undefined
211
+ const generated = await ctx.ui.custom<string | null>(
212
+ (tui, theme, _kb, done) => {
213
+ const model = ctx.model
214
+ const modelLabel = model
215
+ ? `${model.provider}/${"id" in model ? model.id : "selected"}`
216
+ : "selected model"
217
+ const loader = new BorderedLoader(
218
+ tui,
219
+ theme,
220
+ `Generating handoff with ${modelLabel}...`,
369
221
  )
222
+ loader.onAbort = () => done(null)
370
223
 
371
- if (generated === null) {
372
- ctx.ui.notify("Handoff session cancelled.", "warning")
373
- return
374
- }
375
- if (generated.startsWith("__ERROR__")) {
376
- ctx.ui.notify(generated.slice("__ERROR__".length), "error")
377
- return
378
- }
379
-
380
- const sessionName = await generateSessionName(
381
- ctx as ExtensionCommandContext,
224
+ generateHandoffDocument({
225
+ ctx,
226
+ handoffSkill,
382
227
  messages,
383
- )
384
- const firstUserText = messages.map(textFromMessage).find(Boolean)
385
- const handoffSlug =
386
- sanitizeRenameText(pi.getSessionName() ?? firstUserText ?? focus) ||
387
- "session"
388
- const handoffPath = await nextHandoffPath(handoffSlug)
389
- const document = buildDocumentWithMetadata({
390
- generated,
391
228
  focus,
392
- handoffSkillPath: handoffSkill.path,
393
229
  parentSession,
394
- promptContext: summarizePromptContext(ctx as ExtensionCommandContext),
395
- })
396
- await writeFile(handoffPath, document, "utf8")
397
-
398
- await (ctx as ExtensionCommandContext).newSession({
399
- ...(parentSession ? { parentSession } : {}),
400
- setup: async (sessionManager) => {
401
- sessionManager.appendSessionInfo(sessionName)
402
- },
403
- withSession: async (newCtx) => {
404
- newCtx.ui.setEditorText(buildNewSessionPrompt(handoffPath))
405
- newCtx.ui.notify(`Handoff written: ${handoffPath}`, "info")
406
- },
230
+ signal: loader.signal,
407
231
  })
232
+ .then(done)
233
+ .catch((error) => {
234
+ console.error("Handoff generation failed:", error)
235
+ done(
236
+ `__ERROR__${error instanceof Error ? error.message : String(error)}`,
237
+ )
238
+ })
239
+
240
+ return loader
241
+ },
242
+ )
243
+
244
+ if (generated === null) {
245
+ ctx.ui.notify("Handoff session cancelled.", "warning")
246
+ return
247
+ }
248
+ if (generated.startsWith("__ERROR__")) {
249
+ ctx.ui.notify(generated.slice("__ERROR__".length), "error")
250
+ return
251
+ }
252
+
253
+ const sessionName =
254
+ sanitizeRenameText(ctx.sessionManager.getSessionName() ?? focus) ||
255
+ "handoff-session"
256
+ const handoffPath = await nextHandoffPath(sessionName)
257
+ const document = buildDocumentWithMetadata({
258
+ generated,
259
+ focus,
260
+ handoffSkillPath: handoffSkill.path,
261
+ parentSession,
262
+ })
263
+ await writeFile(handoffPath, document, "utf8")
264
+
265
+ await ctx.newSession({
266
+ ...(parentSession ? { parentSession } : {}),
267
+ setup: async (sessionManager) => {
268
+ sessionManager.appendSessionInfo(sessionName)
269
+ },
270
+ withSession: async (newCtx) => {
271
+ newCtx.ui.notify(`Handoff written: ${handoffPath}`, "info")
272
+ await newCtx.sendUserMessage(buildNewSessionPrompt(handoffPath))
273
+ },
274
+ })
275
+ }
276
+
277
+ export default function (pi: ExtensionAPI): void {
278
+ pi.registerCommand(HANDOFF_BRIDGE_COMMAND, {
279
+ description: "Internal handoff bridge; use the -handoff marker instead.",
280
+ handler: async (args, ctx) => {
281
+ const focus = args.trim() || DEFAULT_HANDOFF_FOCUS
282
+ try {
283
+ await runHandoff(pi, ctx, focus)
284
+ } catch (error) {
285
+ const message = error instanceof Error ? error.message : String(error)
286
+ console.error("Handoff session failed:", error)
287
+ ctx.ui.notify(message, "error")
288
+ }
408
289
  },
409
290
  })
291
+
292
+ pi.on("input", (event, ctx) => {
293
+ if (event.source === "extension" || ctx.mode !== "tui") {
294
+ return { action: "continue" }
295
+ }
296
+
297
+ const focus = getHandoffFocus(event.text)
298
+ if (focus === undefined) return { action: "continue" }
299
+
300
+ pi.sendUserMessage(`/${HANDOFF_BRIDGE_COMMAND} ${focus}`, {
301
+ expandPromptTemplates: true,
302
+ })
303
+ return { action: "handled" }
304
+ })
410
305
  }
@@ -1,11 +1,12 @@
1
- import { complete, type Message } from "@earendil-works/pi-ai/compat"
1
+ import type { Message } from "@earendil-works/pi-ai/compat"
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
3
3
  import {
4
4
  SessionManager,
5
5
  convertToLlm,
6
6
  serializeConversation,
7
- type SessionEntry,
7
+ sessionEntryToContextMessages,
8
8
  } from "@earendil-works/pi-coding-agent"
9
+ import { completeText } from "./complete-text.js"
9
10
  import { Text } from "@earendil-works/pi-tui"
10
11
  import { Type } from "typebox"
11
12
 
@@ -79,13 +80,9 @@ export default function (pi: ExtensionAPI): void {
79
80
  return errorResult(`Error loading session: ${err}`)
80
81
  }
81
82
 
82
- const branch = sessionManager.getBranch()
83
- const messages = branch
84
- .filter(
85
- (entry): entry is SessionEntry & { type: "message" } =>
86
- entry.type === "message",
87
- )
88
- .map((entry) => entry.message)
83
+ const messages = sessionManager
84
+ .buildContextEntries()
85
+ .flatMap(sessionEntryToContextMessages)
89
86
 
90
87
  if (messages.length === 0) {
91
88
  return {
@@ -104,11 +101,6 @@ export default function (pi: ExtensionAPI): void {
104
101
  )
105
102
  }
106
103
 
107
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model)
108
- if (!auth.ok) {
109
- return errorResult(`Error: Query model unavailable: ${auth.error}`)
110
- }
111
-
112
104
  const conversationText = serializeConversation(convertToLlm(messages))
113
105
  const userMessage: Message = {
114
106
  role: "user",
@@ -122,29 +114,20 @@ export default function (pi: ExtensionAPI): void {
122
114
  }
123
115
 
124
116
  try {
125
- const response = await complete(
117
+ const answer = await completeText(
118
+ ctx,
126
119
  model,
127
- { systemPrompt: QUERY_SYSTEM_PROMPT, messages: [userMessage] },
128
- {
129
- ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
130
- ...(auth.headers ? { headers: auth.headers } : {}),
131
- ...(signal ? { signal } : {}),
132
- },
120
+ QUERY_SYSTEM_PROMPT,
121
+ userMessage,
122
+ signal,
133
123
  )
134
-
135
- if (response.stopReason === "aborted") {
124
+ if (answer === null) {
136
125
  return {
137
126
  content: [{ type: "text" as const, text: "Query was cancelled." }],
138
127
  details: { cancelled: true },
139
128
  }
140
129
  }
141
130
 
142
- const answer = response.content
143
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
144
- .map((c) => c.text)
145
- .join("\n")
146
- .trim()
147
-
148
131
  return {
149
132
  content: [{ type: "text" as const, text: answer }],
150
133
  details: {
@@ -154,7 +137,9 @@ export default function (pi: ExtensionAPI): void {
154
137
  },
155
138
  }
156
139
  } catch (err) {
157
- return errorResult(`Error querying session: ${err}`)
140
+ return errorResult(
141
+ `Error querying session: ${err instanceof Error ? err.message : String(err)}`,
142
+ )
158
143
  }
159
144
  },
160
145
  })
@@ -1,5 +1,19 @@
1
1
  # @tifan/pi-mermaid-open
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#116](https://github.com/HerbertGao/pi-extensions/pull/116) [`ecda398`](https://github.com/HerbertGao/pi-extensions/commit/ecda398b9a95212c512d53325c617e485e6ded8d) Thanks [@HerbertGao](https://github.com/HerbertGao)! - Open Mermaid diagrams skipped by Pi's native renderer with reason-aware selection, terminal image viewing, and an optional Herdr overlay.
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`b9d0e7e`](https://github.com/tifandotme/pi-extensions/commit/b9d0e7ec0e5e74afafba59e38334311f50e8615f) Thanks [@tifandotme](https://github.com/tifandotme)! - Add a Herdr overlay for viewing rendered Mermaid diagrams with Kitty graphics, zoom, and pan controls.
14
+
15
+ - [`efc30c3`](https://github.com/tifandotme/pi-extensions/commit/efc30c353f435081efa00d3d30c0cd5293b33f8b) Thanks [@tifandotme](https://github.com/tifandotme)! - Update `/mermaid-open` to focus on diagrams Pi skipped, label skip reasons, show progress while rendering, and display selected diagrams as temporary PNGs in a terminal image popup instead of opening a browser.
16
+
3
17
  ## 0.1.3
4
18
 
5
19
  ### Patch Changes
@@ -2,9 +2,19 @@
2
2
 
3
3
  > HerbertGao-maintained fork of [@tifan/pi-mermaid-open](https://github.com/tifandotme/pi-extensions/tree/master/packages/pi-mermaid-open), distributed under MIT with the original attribution preserved.
4
4
 
5
- Extract Mermaid diagrams from recent pi assistant messages, render them to SVG, and open them in the system viewer.
5
+ Find Mermaid diagrams that Pi left unrendered and show one in a terminal image viewer.
6
6
 
7
- The command scans the last 50 assistant messages for ` ```mermaid ` or ` ```mmd ` fences. If it finds more than one diagram, pi shows a picker with the message offset, diagram type, and title. Rendering uses `@mermaid-js/mermaid-cli` via `bunx`. SVGs are written under `<agent-dir>/artifacts/mermaid/` and opened with `open` on macOS, `xdg-open` on Linux, or `start` on Windows.
7
+ <https://github.com/user-attachments/assets/d0c1d46f-0cba-4bf3-a44e-9aa0f4b8e3fd>
8
+
9
+ ## How it works
10
+
11
+ - Scans assistant messages in the current branch for ` ```mermaid ` and ` ```mmd ` fences.
12
+ - Hides diagrams that Pi already rendered and labels skipped diagrams by reason.
13
+ - Renders diagrams as 4x PNGs with `@mermaid-js/mermaid-cli`.
14
+ - Uses a non-blocking Herdr overlay with Kitty graphics when available.
15
+ - Uses Pi's image viewer in other TUI sessions.
16
+ - Saves PNGs under `<agent-dir>/artifacts/mermaid/` outside TUI mode.
17
+ - In Herdr, use `+`/`=` and `-` to zoom, `h`/`j`/`k`/`l` or the arrow keys to pan, `0` to reset, and Enter or Escape to close.
8
18
 
9
19
  ## Install
10
20
 
@@ -12,13 +22,20 @@ The command scans the last 50 assistant messages for ` ```mermaid ` or ` ```mmd
12
22
  pi install npm:@herbertgao/pi-mermaid-open
13
23
  ```
14
24
 
25
+ ## Requirements
26
+
27
+ - Bun is optional. The extension uses `bunx -y @mermaid-js/mermaid-cli` when Bun is available and falls back to `npx -y @mermaid-js/mermaid-cli`.
28
+ - Herdr with Kitty graphics support is optional. In a compatible Herdr TUI session, `/mermaid-open` automatically links the bundled Herdr plugin, so you do not need to install it separately. Other TUI sessions use Pi's image viewer.
29
+ - Network access on first use unless Mermaid CLI is already cached.
30
+ - A Chromium browser available to Puppeteer. The extension skips Puppeteer's automatic browser download, so Puppeteer must find an existing cache or configured browser executable.
31
+
15
32
  ## Commands
16
33
 
17
- - `/mermaid-open`: Pick a Mermaid diagram from recent assistant messages, render it, and open the SVG.
34
+ - `/mermaid-open`: Pick a Mermaid diagram from recent assistant messages, render it, and open the PNG.
18
35
 
19
36
  ## Release notes
20
37
 
21
- See [CHANGELOG.md](CHANGELOG.md)
38
+ See [CHANGELOG.md](CHANGELOG.md).
22
39
 
23
40
  ## License
24
41
 
@@ -0,0 +1,11 @@
1
+ id = "pi-mermaid-open"
2
+ name = "Pi Mermaid Open"
3
+ version = "0.1.3"
4
+ min_herdr_version = "0.8.0"
5
+ platforms = ["linux", "macos", "windows"]
6
+
7
+ [[panes]]
8
+ id = "viewer"
9
+ title = "Mermaid"
10
+ command = ["node", "viewer.mjs"]
11
+ placement = "overlay"