@herbertgao/pi-extensions 2026.8.7 → 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 (161) hide show
  1. package/README.md +8 -6
  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/compact-thinking.ts +63 -15
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +30 -17
  33. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  34. package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
  35. package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
  36. package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
  37. package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
  38. package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
  39. package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
  40. package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
  41. package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
  42. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
  43. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
  44. package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
  45. package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
  46. package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
  47. package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
  48. package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
  49. package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
  50. package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
  51. package/node_modules/@herbertgao/pi-recap/README.md +5 -3
  52. package/node_modules/@herbertgao/pi-recap/package.json +9 -3
  53. package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
  54. package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
  55. package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
  56. package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
  57. package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
  58. package/node_modules/@herbertgao/pi-rename/README.md +13 -11
  59. package/node_modules/@herbertgao/pi-rename/package.json +13 -3
  60. package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
  61. package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
  62. package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
  63. package/node_modules/@herbertgao/pi-stash/package.json +3 -3
  64. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  65. package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
  66. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  67. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
  68. package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
  69. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
  70. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
  71. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
  72. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
  73. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
  74. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
  75. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
  76. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
  77. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  78. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
  79. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
  80. package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
  81. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
  82. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
  83. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
  84. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
  85. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
  86. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
  87. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
  88. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
  89. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  90. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
  91. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  92. package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
  93. package/node_modules/pi-lens/CHANGELOG.md +90 -0
  94. package/node_modules/pi-lens/README.md +106 -0
  95. package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
  96. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
  97. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
  98. package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
  99. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
  100. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
  101. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
  102. package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
  119. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
  120. package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
  121. package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
  122. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
  123. package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
  124. package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
  125. package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
  126. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
  127. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
  128. package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
  129. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
  130. package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
  131. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
  132. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
  133. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
  134. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
  135. package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
  136. package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
  137. package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
  138. package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
  139. package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
  140. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
  141. package/node_modules/pi-lens/dist/index.js +1224 -393
  142. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
  143. package/node_modules/pi-lens/package.json +18 -5
  144. package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
  145. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
  146. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
  147. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
  148. package/node_modules/pi-web-access/CHANGELOG.md +27 -0
  149. package/node_modules/pi-web-access/README.md +8 -7
  150. package/node_modules/pi-web-access/chrome-cookies.ts +96 -20
  151. package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
  152. package/node_modules/pi-web-access/extract.ts +12 -2
  153. package/node_modules/pi-web-access/firecrawl.ts +18 -2
  154. package/node_modules/pi-web-access/gemini-search.ts +28 -10
  155. package/node_modules/pi-web-access/github-extract.ts +65 -36
  156. package/node_modules/pi-web-access/index.ts +43 -14
  157. package/node_modules/pi-web-access/openai-search.ts +17 -6
  158. package/node_modules/pi-web-access/package.json +1 -1
  159. package/node_modules/pi-web-access/pdf-extract.ts +18 -5
  160. package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
  161. package/package.json +19 -18
@@ -1,8 +1,28 @@
1
- import { spawn } from "node:child_process"
2
- import { mkdir } from "node:fs/promises"
1
+ import { execFile, spawn } from "node:child_process"
2
+ import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises"
3
3
  import os from "node:os"
4
4
  import path from "node:path"
5
- import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
5
+ import { fileURLToPath } from "node:url"
6
+ import { promisify } from "node:util"
7
+ import { diagramKind, render as renderNativeMermaid } from "grok-mermaid"
8
+ import {
9
+ Box,
10
+ CancellableLoader,
11
+ deleteKittyImage,
12
+ getCapabilities,
13
+ Image,
14
+ matchesKey,
15
+ Spacer,
16
+ Text,
17
+ type Component,
18
+ type TUI,
19
+ } from "@earendil-works/pi-tui"
20
+ import {
21
+ getAgentDir,
22
+ type ExtensionAPI,
23
+ type ExtensionCommandContext,
24
+ type Theme,
25
+ } from "@earendil-works/pi-coding-agent"
6
26
 
7
27
  type TextBlock = {
8
28
  type: "text"
@@ -20,30 +40,53 @@ type MessageEntry = {
20
40
  message: unknown
21
41
  }
22
42
 
43
+ type MermaidFenceLanguage = "mermaid" | "mmd"
44
+
45
+ type NativeMermaidMode = "off" | "final" | "streaming"
46
+
47
+ type NativeStatus =
48
+ | { kind: "rendered"; width: number }
49
+ | { kind: "skipped"; reason: SkipReason }
50
+
51
+ type SkipReason =
52
+ | { kind: "disabled" }
53
+ | { kind: "mmd-fence" }
54
+ | { kind: "unsupported" }
55
+ | { kind: "invalid" }
56
+ | { kind: "too-wide"; width: number; availableWidth: number }
57
+ | { kind: "warnings" }
58
+
23
59
  type MermaidDiagram = {
24
60
  source: string
25
61
  diagramType: string
26
62
  label: string
63
+ fenceLanguage: MermaidFenceLanguage
27
64
  messageOffset: number
28
65
  discoveredIndex: number
66
+ nativeStatus: NativeStatus
67
+ }
68
+
69
+ type NativeMermaidConfig = {
70
+ mode: NativeMermaidMode
71
+ availableWidth: number
29
72
  }
30
73
 
31
- const SCAN_ASSISTANT_MESSAGE_LIMIT = 50
74
+ type PiSettings = {
75
+ outputPad?: 0 | 1
76
+ markdown?: {
77
+ mermaid?: NativeMermaidMode
78
+ }
79
+ }
80
+
81
+ const execFileAsync = promisify(execFile)
32
82
  const OUTPUT_DIR = path.join(getAgentDir(), "artifacts", "mermaid")
83
+ const HERDR_PLUGIN_ID = "pi-mermaid-open"
84
+ const HERDR_PLUGIN_ROOT = path.resolve(
85
+ path.dirname(fileURLToPath(import.meta.url)),
86
+ "..",
87
+ "herdr-plugin",
88
+ )
33
89
  const MERMAID_FENCE_PATTERN = /```\s*(mermaid|mmd)\b[^\n]*\n([\s\S]*?)```/gi
34
- const KNOWN_TYPES = [
35
- "sequenceDiagram",
36
- "classDiagram",
37
- "stateDiagram",
38
- "erDiagram",
39
- "flowchart",
40
- "graph",
41
- "gantt",
42
- "journey",
43
- "pie",
44
- "mindmap",
45
- "timeline",
46
- ]
47
90
 
48
91
  function isTextBlock(value: unknown): value is TextBlock {
49
92
  return (
@@ -99,18 +142,7 @@ function classifyDiagram(source: string): string {
99
142
  .map((line) => line.trim())
100
143
  .find((line) => line.length > 0 && !line.startsWith("%%"))
101
144
 
102
- if (!firstMeaningfulLine) return "mermaid"
103
-
104
- for (const type of KNOWN_TYPES) {
105
- if (
106
- firstMeaningfulLine === type ||
107
- firstMeaningfulLine.startsWith(`${type} `)
108
- ) {
109
- return type
110
- }
111
- }
112
-
113
- return firstMeaningfulLine.split(/\s+/)[0] ?? "mermaid"
145
+ return firstMeaningfulLine?.split(/\s+/)[0] ?? "mermaid"
114
146
  }
115
147
 
116
148
  function extractTitle(source: string): string | undefined {
@@ -152,12 +184,71 @@ function conciseError(stderr: string, stdout: string): string {
152
184
  return text || "Unknown error"
153
185
  }
154
186
 
155
- function discoverDiagrams(entries: readonly unknown[]): MermaidDiagram[] {
187
+ function getNativeMermaidMode(settings: PiSettings): NativeMermaidMode {
188
+ const mode = settings.markdown?.mermaid
189
+ return mode === "off" || mode === "final" ? mode : "streaming"
190
+ }
191
+
192
+ async function readNativeMermaidConfig(): Promise<NativeMermaidConfig> {
193
+ let settings: PiSettings = {}
194
+ try {
195
+ settings = JSON.parse(
196
+ await readFile(path.join(getAgentDir(), "settings.json"), "utf8"),
197
+ ) as PiSettings
198
+ } catch {
199
+ // Pi's defaults are streaming Mermaid with one column of output padding.
200
+ }
201
+
202
+ const columns = process.stdout.columns ?? 80
203
+ const outputPad = settings.outputPad ?? 1
204
+
205
+ return {
206
+ mode: getNativeMermaidMode(settings),
207
+ availableWidth: Math.max(1, columns - outputPad * 2),
208
+ }
209
+ }
210
+
211
+ export function getNativeStatus(
212
+ source: string,
213
+ fenceLanguage: MermaidFenceLanguage,
214
+ config: NativeMermaidConfig,
215
+ ): NativeStatus {
216
+ if (config.mode === "off")
217
+ return { kind: "skipped", reason: { kind: "disabled" } }
218
+ if (fenceLanguage !== "mermaid") {
219
+ return { kind: "skipped", reason: { kind: "mmd-fence" } }
220
+ }
221
+ if (diagramKind(source) === null) {
222
+ return { kind: "skipped", reason: { kind: "unsupported" } }
223
+ }
224
+
225
+ const art = renderNativeMermaid(source)
226
+ if (!art) return { kind: "skipped", reason: { kind: "invalid" } }
227
+ if (art.width > config.availableWidth) {
228
+ return {
229
+ kind: "skipped",
230
+ reason: {
231
+ kind: "too-wide",
232
+ width: art.width,
233
+ availableWidth: config.availableWidth,
234
+ },
235
+ }
236
+ }
237
+ if (art.warnings.length > 0) {
238
+ return { kind: "skipped", reason: { kind: "warnings" } }
239
+ }
240
+
241
+ return { kind: "rendered", width: art.width }
242
+ }
243
+
244
+ function discoverDiagrams(
245
+ entries: readonly unknown[],
246
+ nativeConfig: NativeMermaidConfig,
247
+ ): MermaidDiagram[] {
156
248
  const assistantMessages = entries
157
249
  .filter(isMessageEntry)
158
250
  .map((entry) => entry.message)
159
251
  .filter(isAssistantMessage)
160
- .slice(-SCAN_ASSISTANT_MESSAGE_LIMIT)
161
252
  .toReversed()
162
253
 
163
254
  const diagrams: MermaidDiagram[] = []
@@ -168,6 +259,9 @@ function discoverDiagrams(entries: readonly unknown[]): MermaidDiagram[] {
168
259
  const source = trimOuterBlankLines(match[2] ?? "")
169
260
  if (!source) continue
170
261
 
262
+ const fenceLanguage = (
263
+ match[1] ?? "mermaid"
264
+ ).toLowerCase() as MermaidFenceLanguage
171
265
  const discoveredIndex = diagrams.length + 1
172
266
  const diagramType = classifyDiagram(source)
173
267
  const label = extractTitle(source) ?? `${diagramType} ${discoveredIndex}`
@@ -176,8 +270,10 @@ function discoverDiagrams(entries: readonly unknown[]): MermaidDiagram[] {
176
270
  source,
177
271
  diagramType,
178
272
  label,
273
+ fenceLanguage,
179
274
  messageOffset: messageIndex + 1,
180
275
  discoveredIndex,
276
+ nativeStatus: getNativeStatus(source, fenceLanguage, nativeConfig),
181
277
  })
182
278
  }
183
279
  })
@@ -185,36 +281,72 @@ function discoverDiagrams(entries: readonly unknown[]): MermaidDiagram[] {
185
281
  return diagrams
186
282
  }
187
283
 
284
+ export function formatNativeStatus(status: NativeStatus): string {
285
+ if (status.kind === "rendered") return `✓ rendered · ${status.width} cols`
286
+
287
+ switch (status.reason.kind) {
288
+ case "disabled":
289
+ return "✗ skipped · Mermaid disabled"
290
+ case "mmd-fence":
291
+ return "✗ skipped · mmd fence"
292
+ case "unsupported":
293
+ return "✗ skipped · unsupported"
294
+ case "invalid":
295
+ return "✗ skipped · invalid"
296
+ case "too-wide":
297
+ return `✗ skipped · too wide (${status.reason.width} > ${status.reason.availableWidth})`
298
+ case "warnings":
299
+ return "⚠ skipped · parser warnings"
300
+ }
301
+ }
302
+
188
303
  function pickerLabel(diagram: MermaidDiagram): string {
189
304
  return [
190
- `${diagram.discoveredIndex}. ${relativeMessageLabel(diagram.messageOffset)}`,
305
+ formatNativeStatus(diagram.nativeStatus),
191
306
  diagram.diagramType,
192
307
  diagram.label,
308
+ relativeMessageLabel(diagram.messageOffset),
193
309
  ].join(" · ")
194
310
  }
195
311
 
196
- async function createSvgArtifactPath(diagram: MermaidDiagram): Promise<string> {
312
+ async function createPngArtifactPath(diagram: MermaidDiagram): Promise<string> {
197
313
  await mkdir(OUTPUT_DIR, { recursive: true })
198
314
  const baseName = [
199
315
  timestampForFilename(),
200
316
  slugify(diagram.diagramType),
201
317
  slugify(diagram.label),
202
318
  ].join("-")
203
- return path.join(OUTPUT_DIR, `${baseName}.svg`)
319
+ return path.join(OUTPUT_DIR, `${baseName}.png`)
204
320
  }
205
321
 
206
- async function renderMermaidToSvg(
322
+ async function renderMermaidToPng(
207
323
  source: string,
208
- svgPath: string,
324
+ pngPath: string,
209
325
  ): Promise<{
210
326
  ok: boolean
211
327
  error?: string
212
328
  }> {
329
+ const bunx = os.platform() === "win32" ? "bunx.cmd" : "bunx"
330
+ let command = bunx
331
+ try {
332
+ await execFileAsync(bunx, ["--version"])
333
+ } catch {
334
+ command = os.platform() === "win32" ? "npx.cmd" : "npx"
335
+ }
336
+
213
337
  return new Promise((resolve) => {
214
- const command = os.platform() === "win32" ? "bunx.cmd" : "bunx"
215
338
  const child = spawn(
216
339
  command,
217
- ["-y", "@mermaid-js/mermaid-cli", "-i", "-", "-o", svgPath],
340
+ [
341
+ "-y",
342
+ "@mermaid-js/mermaid-cli",
343
+ "-i",
344
+ "-",
345
+ "-o",
346
+ pngPath,
347
+ "--scale",
348
+ "4",
349
+ ],
218
350
  {
219
351
  env: { ...process.env, PUPPETEER_CHROME_SKIP_DOWNLOAD: "true" },
220
352
  },
@@ -248,45 +380,126 @@ async function renderMermaidToSvg(
248
380
  })
249
381
  }
250
382
 
251
- async function openSvg(
252
- pi: ExtensionAPI,
253
- svgPath: string,
254
- ): Promise<{
255
- ok: boolean
256
- error?: string
257
- }> {
258
- const platform = os.platform()
259
- if (platform === "darwin") {
260
- const result = await pi.exec("open", [svgPath])
261
- return result.code === 0
262
- ? { ok: true }
263
- : { ok: false, error: conciseError(result.stderr, result.stdout) }
383
+ class MermaidImageViewer implements Component {
384
+ private readonly box: Box
385
+ private readonly done: () => void
386
+ private readonly image: Image
387
+ private readonly tui: TUI
388
+
389
+ constructor(
390
+ base64Data: string,
391
+ pngPath: string,
392
+ diagram: MermaidDiagram,
393
+ tui: TUI,
394
+ theme: Theme,
395
+ done: () => void,
396
+ ) {
397
+ this.done = done
398
+ this.tui = tui
399
+ this.box = new Box(1, 1, (text) => theme.bg("customMessageBg", text))
400
+ this.box.addChild(new Text(theme.fg("accent", diagram.label), 0, 0))
401
+ this.box.addChild(
402
+ new Text(theme.fg("dim", formatNativeStatus(diagram.nativeStatus)), 0, 0),
403
+ )
404
+ this.box.addChild(new Spacer(1))
405
+ this.image = new Image(
406
+ base64Data,
407
+ "image/png",
408
+ { fallbackColor: (text) => theme.fg("muted", text) },
409
+ {
410
+ maxWidthCells: Math.max(10, tui.terminal.columns - 8),
411
+ maxHeightCells: Math.max(4, tui.terminal.rows - 8),
412
+ filename: pngPath,
413
+ },
414
+ )
415
+ this.box.addChild(this.image)
416
+ this.box.addChild(new Spacer(1))
417
+ this.box.addChild(new Text(theme.fg("dim", "Esc or Enter to close"), 0, 0))
418
+ }
419
+
420
+ handleInput(data: string): void {
421
+ if (matchesKey(data, "escape") || matchesKey(data, "return")) this.done()
422
+ }
423
+
424
+ render(width: number): string[] {
425
+ return this.box.render(width)
426
+ }
427
+
428
+ invalidate(): void {
429
+ this.box.invalidate()
264
430
  }
265
431
 
266
- if (platform === "linux") {
267
- const result = await pi.exec("xdg-open", [svgPath])
268
- return result.code === 0
269
- ? { ok: true }
270
- : { ok: false, error: conciseError(result.stderr, result.stdout) }
432
+ dispose(): void {
433
+ const imageId = this.image.getImageId()
434
+ if (imageId !== undefined) {
435
+ this.tui.terminal.write(deleteKittyImage(imageId))
436
+ }
271
437
  }
438
+ }
272
439
 
273
- if (platform === "win32") {
274
- const result = await pi.exec("cmd", ["/c", "start", "", svgPath])
275
- return result.code === 0
276
- ? { ok: true }
277
- : { ok: false, error: conciseError(result.stderr, result.stdout) }
440
+ async function showPngInHerdrOverlay(pngPath: string): Promise<boolean> {
441
+ try {
442
+ await execFileAsync("herdr", [
443
+ "plugin",
444
+ "link",
445
+ HERDR_PLUGIN_ROOT,
446
+ "--enabled",
447
+ ])
448
+ await execFileAsync("herdr", [
449
+ "plugin",
450
+ "pane",
451
+ "open",
452
+ "--plugin",
453
+ HERDR_PLUGIN_ID,
454
+ "--entrypoint",
455
+ "viewer",
456
+ "--placement",
457
+ "overlay",
458
+ "--env",
459
+ `PI_MERMAID_OPEN_PNG=${pngPath}`,
460
+ "--focus",
461
+ ])
462
+ return true
463
+ } catch {
464
+ return false
278
465
  }
466
+ }
279
467
 
280
- return { ok: false, error: `Unsupported platform: ${platform}` }
468
+ async function showPngInTerminal(
469
+ ctx: ExtensionCommandContext,
470
+ pngPath: string,
471
+ diagram: MermaidDiagram,
472
+ ): Promise<void> {
473
+ const base64Data = (await readFile(pngPath)).toString("base64")
474
+ await ctx.ui.custom<void>(
475
+ (tui, theme, _keybindings, done) =>
476
+ new MermaidImageViewer(base64Data, pngPath, diagram, tui, theme, done),
477
+ {
478
+ overlay: true,
479
+ overlayOptions: {
480
+ width: "92%",
481
+ maxHeight: "92%",
482
+ margin: 2,
483
+ },
484
+ },
485
+ )
281
486
  }
282
487
 
283
488
  export default function (pi: ExtensionAPI): void {
284
489
  pi.registerCommand("mermaid-open", {
285
- description: "Open a Mermaid diagram from recent assistant messages as SVG",
490
+ description: "Open a skipped Mermaid diagram in a terminal popup",
286
491
  handler: async (_args, ctx) => {
287
492
  await ctx.waitForIdle()
288
493
 
289
- const diagrams = discoverDiagrams(ctx.sessionManager.getBranch())
494
+ const nativeConfig = await readNativeMermaidConfig()
495
+ const diagrams = discoverDiagrams(
496
+ ctx.sessionManager.getBranch(),
497
+ nativeConfig,
498
+ )
499
+ const skippedDiagrams = diagrams.filter(
500
+ (diagram) => diagram.nativeStatus.kind === "skipped",
501
+ )
502
+
290
503
  if (diagrams.length === 0) {
291
504
  ctx.ui.notify(
292
505
  "No Mermaid diagrams found in recent assistant messages.",
@@ -294,52 +507,89 @@ export default function (pi: ExtensionAPI): void {
294
507
  )
295
508
  return
296
509
  }
297
-
298
- let selected = diagrams[0]
299
- if (ctx.mode === "tui" && diagrams.length > 1) {
300
- const labels = diagrams.map(pickerLabel)
301
- const choice = await ctx.ui.select("Open Mermaid diagram:", labels)
302
- if (!choice) return
303
- selected = diagrams[labels.indexOf(choice)]
304
- }
305
-
306
- if (!selected) return
307
-
308
- let svgPath: string
309
- try {
310
- svgPath = await createSvgArtifactPath(selected)
311
- } catch (error) {
312
- ctx.ui.notify(
313
- `Failed to create Mermaid artifact path: ${error instanceof Error ? error.message : String(error)}`,
314
- "error",
315
- )
510
+ if (skippedDiagrams.length === 0) {
511
+ ctx.ui.notify("No skipped Mermaid diagrams found.", "info")
316
512
  return
317
513
  }
318
514
 
319
- const renderResult = await renderMermaidToSvg(selected.source, svgPath)
320
- if (!renderResult.ok) {
321
- ctx.ui.notify(
322
- `Mermaid render failed\nError: ${renderResult.error ?? "Unknown error"}`,
323
- "error",
515
+ let selected = skippedDiagrams[0]
516
+ if (ctx.mode === "tui" && skippedDiagrams.length > 1) {
517
+ const labels = skippedDiagrams.map(pickerLabel)
518
+ const choice = await ctx.ui.select(
519
+ "Open skipped Mermaid diagram:",
520
+ labels,
324
521
  )
325
- return
522
+ if (!choice) return
523
+ selected = skippedDiagrams[labels.indexOf(choice)]
326
524
  }
327
525
 
328
- if (ctx.mode !== "tui") {
329
- ctx.ui.notify(`Rendered Mermaid SVG: ${svgPath}`, "info")
330
- return
331
- }
526
+ if (!selected) return
332
527
 
333
- const openResult = await openSvg(pi, svgPath)
334
- if (!openResult.ok) {
335
- ctx.ui.notify(
336
- `Rendered SVG but failed to open it.\nSVG: ${svgPath}\nError: ${openResult.error ?? "Unknown error"}`,
337
- "warning",
338
- )
339
- return
528
+ const imageCapabilities = getCapabilities()
529
+ const canDisplayInHerdr =
530
+ ctx.mode === "tui" &&
531
+ Boolean(process.env["HERDR_PANE_ID"]) &&
532
+ imageCapabilities.images === "kitty"
533
+ const canDisplayInTerminal =
534
+ ctx.mode === "tui" && Boolean(imageCapabilities.images)
535
+ const temporaryDirectory = canDisplayInTerminal
536
+ ? await mkdtemp(path.join(os.tmpdir(), "pi-mermaid-open-"))
537
+ : undefined
538
+ const pngPath = temporaryDirectory
539
+ ? path.join(temporaryDirectory, "diagram.png")
540
+ : await createPngArtifactPath(selected)
541
+
542
+ if (ctx.mode === "tui") {
543
+ ctx.ui.setWidget("pi-mermaid-open", (tui, theme) => {
544
+ const color = (text: string) => theme.fg("dim", text)
545
+ return new CancellableLoader(
546
+ tui,
547
+ color,
548
+ color,
549
+ "rendering Mermaid...",
550
+ )
551
+ })
340
552
  }
341
553
 
342
- ctx.ui.notify(`Opened Mermaid SVG: ${svgPath}`, "info")
554
+ let handedOffToHerdr = false
555
+ try {
556
+ const renderResult = await renderMermaidToPng(selected.source, pngPath)
557
+ if (!renderResult.ok) {
558
+ ctx.ui.notify(
559
+ `Mermaid render failed\nError: ${renderResult.error ?? "Unknown error"}`,
560
+ "error",
561
+ )
562
+ return
563
+ }
564
+
565
+ if (canDisplayInHerdr) {
566
+ handedOffToHerdr = await showPngInHerdrOverlay(pngPath)
567
+ if (handedOffToHerdr) return
568
+ ctx.ui.notify(
569
+ "Herdr overlay unavailable; opening Mermaid in Pi instead.",
570
+ "warning",
571
+ )
572
+ }
573
+
574
+ if (ctx.mode !== "tui" || !canDisplayInTerminal) {
575
+ ctx.ui.notify(
576
+ canDisplayInTerminal
577
+ ? `Rendered Mermaid PNG: ${pngPath}`
578
+ : `Saved Mermaid PNG: ${pngPath}\nKitty graphics are unavailable.`,
579
+ canDisplayInTerminal ? "info" : "warning",
580
+ )
581
+ return
582
+ }
583
+
584
+ await showPngInTerminal(ctx, pngPath, selected)
585
+ } finally {
586
+ if (ctx.mode === "tui") {
587
+ ctx.ui.setWidget("pi-mermaid-open", undefined)
588
+ }
589
+ if (temporaryDirectory && !handedOffToHerdr) {
590
+ await rm(temporaryDirectory, { recursive: true, force: true })
591
+ }
592
+ }
343
593
  },
344
594
  })
345
595
  }
@@ -1,5 +1,17 @@
1
1
  # @tifan/pi-preferred-thinking
2
2
 
3
+ ## 0.4.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)! - Adopt Pi 0.84.2 native per-model thinking pins from enabledModels and retire the extension-specific preference file.
8
+
9
+ ## 1.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - [`92208f9`](https://github.com/tifandotme/pi-extensions/commit/92208f9bc3679afbb6a055d91e3860c984a2f2af) Thanks [@tifandotme](https://github.com/tifandotme)! - Move per-model thinking preferences to Pi's native `enabledModels` setting and require Pi 0.84.2 or newer.
14
+
3
15
  ## 0.3.0
4
16
 
5
17
  ### Minor Changes
@@ -2,11 +2,7 @@
2
2
 
3
3
  > HerbertGao-maintained fork of [@tifan/pi-preferred-thinking](https://github.com/tifandotme/pi-extensions/tree/master/packages/pi-preferred-thinking), distributed under MIT with the original attribution preserved.
4
4
 
5
- Apply per-model thinking levels from `~/.config/pi/extensions/pi-preferred-thinking.json` when sessions start or models change.
6
-
7
- This extension is for model-specific preferences. Pi's built-in `defaultThinkingLevel` remains global, while `preferredThinking` lets you choose different levels for different models. Invalid or missing values are ignored.
8
-
9
- ![Preferred thinking picker showing model-specific thinking levels](https://raw.githubusercontent.com/tifandotme/pi-extensions/refs/heads/master/packages/pi-preferred-thinking/assets/picker.webp)
5
+ Set and apply per-model thinking levels from Pi's native `enabledModels` setting.
10
6
 
11
7
  ## Install
12
8
 
@@ -14,26 +10,46 @@ This extension is for model-specific preferences. Pi's built-in `defaultThinking
14
10
  pi install npm:@herbertgao/pi-preferred-thinking
15
11
  ```
16
12
 
17
- ## Configuration
13
+ This package requires Pi 0.84.2 or newer.
18
14
 
19
- Run `/preferred-thinking` to set or unset the preferred thinking level for the current model.
15
+ ## How it works
20
16
 
21
- Valid levels: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`.
17
+ Run `/preferred-thinking` to choose a thinking level for the current model. The
18
+ extension writes the model pin to Pi's `settings.json` and reloads Pi so the
19
+ native model scope uses it.
22
20
 
23
- The extension saves preferences in `~/.config/pi/extensions/pi-preferred-thinking.json`:
21
+ When Pi selects a scoped model, the extension applies its native thinking pin.
22
+ It also applies the pin when the model picker is used, which keeps model cycling
23
+ and model selection consistent.
24
+
25
+ If a reasoning model has no saved pin, the extension shows a short hint above
26
+ the editor. It does not change the current thinking level in that case.
27
+
28
+ ## Configuration
29
+
30
+ Add models to Pi's `enabledModels` setting. The extension preserves other
31
+ entries and updates only the selected model:
24
32
 
25
33
  ```json
26
34
  {
27
- "preferredThinking": {
28
- "anthropic/claude-opus-4-8": "high",
29
- "openai-codex/gpt-5.6-luna": "minimal"
30
- }
35
+ "enabledModels": [
36
+ "openai-codex/gpt-5.6-luna:xhigh",
37
+ "openai-codex/gpt-5.6-terra:high"
38
+ ]
31
39
  }
32
40
  ```
33
41
 
42
+ The extension requires `enabledModels` to exist before it can save a pin. If
43
+ the selected model is not listed, it adds an exact entry without removing other
44
+ models or patterns.
45
+
46
+ The old `~/.config/pi/extensions/pi-preferred-thinking.json` file is no longer
47
+ read. Existing users must move preferences to `enabledModels` or set them with
48
+ `/preferred-thinking`.
49
+
34
50
  ## Release notes
35
51
 
36
- See [CHANGELOG.md](CHANGELOG.md)
52
+ See [CHANGELOG.md](CHANGELOG.md).
37
53
 
38
54
  ## License
39
55
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-preferred-thinking",
3
- "version": "0.3.0",
4
- "description": "Persist preferred thinking level per model in pi.",
3
+ "version": "0.4.0",
4
+ "description": "Set and apply per-model thinking levels from Pi's native settings.",
5
5
  "keywords": [
6
6
  "pi",
7
7
  "pi-extension",
@@ -32,6 +32,9 @@
32
32
  "access": "public",
33
33
  "provenance": true
34
34
  },
35
+ "peerDependencies": {
36
+ "@earendil-works/pi-coding-agent": ">=0.84.2"
37
+ },
35
38
  "engines": {
36
39
  "node": ">=20"
37
40
  },
@@ -42,8 +45,8 @@
42
45
  },
43
46
  "x-upstream": {
44
47
  "package": "@tifan/pi-preferred-thinking",
45
- "version": "0.3.0",
48
+ "version": "1.0.0",
46
49
  "repository": "https://github.com/tifandotme/pi-extensions",
47
- "commit": "460d580"
50
+ "commit": "b39d1e6359c78718cbf8fe5c74c82b89065e9a0b"
48
51
  }
49
52
  }