@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.
- package/README.md +3 -3
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
- package/node_modules/@czottmann/pi-automode/README.md +78 -112
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
- package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
- package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
- package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
- package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
- package/node_modules/@czottmann/pi-automode/package.json +9 -2
- package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +15 -13
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
- package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
- package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
- package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
- package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
- package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
- package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
- package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
- package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
- package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
- package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
- package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
- package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
- package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
- package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
- package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-recap/README.md +5 -3
- package/node_modules/@herbertgao/pi-recap/package.json +9 -3
- package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
- package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
- package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
- package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
- package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
- package/node_modules/@herbertgao/pi-rename/README.md +13 -11
- package/node_modules/@herbertgao/pi-rename/package.json +13 -3
- package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
- package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
- package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
- package/node_modules/@herbertgao/pi-stash/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
- package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
- package/node_modules/pi-web-access/CHANGELOG.md +13 -0
- package/node_modules/pi-web-access/README.md +2 -2
- package/node_modules/pi-web-access/chrome-cookies.ts +12 -7
- package/node_modules/pi-web-access/gemini-search.ts +28 -10
- package/node_modules/pi-web-access/index.ts +24 -10
- package/node_modules/pi-web-access/package.json +1 -1
- package/package.json +13 -12
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core"
|
|
2
|
-
import {
|
|
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
|
-
|
|
166
|
+
return completeText(
|
|
167
|
+
options.ctx,
|
|
275
168
|
model,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
372
|
-
ctx
|
|
373
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
.
|
|
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
|
|
117
|
+
const answer = await completeText(
|
|
118
|
+
ctx,
|
|
126
119
|
model,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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
|
-
|
|
5
|
+
Find Mermaid diagrams that Pi left unrendered and show one in a terminal image viewer.
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
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
|
|