pi-minimal 0.4.0 → 0.6.0
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/CHANGELOG.md +81 -1
- package/CONTEXT.md +107 -0
- package/README.md +9 -6
- package/dist/background-overlay.js +95 -0
- package/dist/background.js +206 -0
- package/dist/config.js +10 -1
- package/dist/cwd.js +2 -2
- package/dist/edit-diff.js +74 -0
- package/dist/footer-contribution-events.js +5 -3
- package/dist/footer-contributions.js +15 -13
- package/dist/footer.js +15 -3
- package/dist/git-state.js +31 -11
- package/dist/herdr.js +78 -0
- package/dist/index.js +79 -202
- package/dist/listeners.js +23 -0
- package/dist/prs-command.js +76 -0
- package/dist/prs.js +270 -0
- package/dist/reclaim.js +0 -3
- package/dist/rex.js +289 -0
- package/dist/session.js +168 -0
- package/dist/siblings.js +14 -0
- package/dist/tools.js +213 -374
- package/dist/tracker.js +2 -2
- package/dist/working-status.js +2 -1
- package/doc/archive/README.md +3 -0
- package/doc/footer-contributions.md +7 -7
- package/package.json +5 -3
- package/release-notes.md +33 -12
- package/src/background-overlay.ts +103 -0
- package/src/background.ts +250 -0
- package/src/config.ts +10 -1
- package/src/cwd.ts +5 -4
- package/src/edit-diff.ts +73 -0
- package/src/footer-contribution-events.ts +7 -4
- package/src/footer-contributions.ts +15 -13
- package/src/footer.ts +23 -3
- package/src/git-state.ts +29 -10
- package/src/herdr.ts +78 -5
- package/src/index.ts +81 -195
- package/src/listeners.ts +22 -0
- package/src/prs-command.ts +81 -0
- package/src/prs.ts +348 -0
- package/src/reclaim.ts +0 -4
- package/src/rex.ts +304 -0
- package/src/session.ts +185 -0
- package/src/siblings.ts +38 -0
- package/src/tools.ts +261 -389
- package/src/tracker.ts +11 -5
- package/src/working-status.ts +2 -1
- package/test/background.test.ts +198 -0
- package/test/config.test.ts +24 -11
- package/test/footer-composition.test.ts +61 -0
- package/test/footer-contribution-events.test.ts +15 -0
- package/test/git-state.test.ts +35 -0
- package/test/index.test.ts +202 -4
- package/test/prs.test.ts +272 -0
- package/test/rex.test.ts +249 -0
- package/test/tools.test.ts +36 -3
- package/test/working-status.test.ts +53 -0
- /package/doc/{plans/2026-09-22-pi-minimal-package.md → archive/2026-09-22-plan-pi-minimal-package.md} +0 -0
- /package/doc/{specs/2026-09-22-pi-minimal-package.md → archive/2026-09-22-spec-pi-minimal-package.md} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,199 +1,84 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { installFooter } from "./footer.js"
|
|
5
|
-
import { createFooterContributions } from "./footer-contributions.js"
|
|
6
|
-
import { bindFooterContributionEvents } from "./footer-contribution-events.js"
|
|
7
|
-
import { installCwd } from "./cwd.js"
|
|
8
|
-
import { listSiblings, parseAgentList, parseTabList, type Sibling } from "./herdr.js"
|
|
9
|
-
import { hideGauntletWidgets } from "./tracker.js"
|
|
10
|
-
import { installToolRenderers, setEditDiffMode, stopLiveWatch } from "./tools.js"
|
|
11
|
-
import { setSessionCwd } from "./tool-rows.js"
|
|
12
|
-
import { applyReclaim, reclaimPolicy, type ReclaimReason } from "./reclaim.js"
|
|
13
|
-
import { installWorkingStatus } from "./working-status.js"
|
|
14
|
-
import { installTurnRule } from "./turn-rule.js"
|
|
15
|
-
import { installTranscript } from "./transcript.js"
|
|
16
|
-
import { createGitState } from "./git-state.js"
|
|
1
|
+
import { Type } from "@earendil-works/pi-ai"
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
3
|
+
import { showBackgroundOverlay } from "./background-overlay.js"
|
|
17
4
|
import { installCodeBox } from "./code-box.js"
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
5
|
+
import { isPrCreateCommand } from "./prs.js"
|
|
6
|
+
import { parsePrFilter, prsToolResult, runPrsCommand, type PrFilter } from "./prs-command.js"
|
|
7
|
+
import { createSession, type Session } from "./session.js"
|
|
8
|
+
import { installToolRenderers, stopLiveWatch } from "./tools.js"
|
|
9
|
+
import { installTranscript } from "./transcript.js"
|
|
10
|
+
import { installTurnRule } from "./turn-rule.js"
|
|
11
|
+
import { installWorkingStatus } from "./working-status.js"
|
|
20
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Process-wide hooks are installed once at load (renderers, transformers, the code-box patch);
|
|
15
|
+
* everything else lives on a `Session` created at session_start and disposed at session_shutdown
|
|
16
|
+
* or when the next session starts.
|
|
17
|
+
*/
|
|
21
18
|
export default function piMinimalExtension(pi: ExtensionAPI): void {
|
|
22
19
|
const stopWorkingStatus = installWorkingStatus(pi)
|
|
23
20
|
const stopTurnRule = installTurnRule(pi)
|
|
24
21
|
installToolRenderers(pi)
|
|
25
22
|
installTranscript(pi)
|
|
26
23
|
installCodeBox()
|
|
27
|
-
const contributions = createFooterContributions()
|
|
28
|
-
let stopFooter = () => {}
|
|
29
|
-
let stopContributionEvents = () => {}
|
|
30
|
-
let sessionActive = false
|
|
31
|
-
let fastTimer: ReturnType<typeof setInterval> | undefined
|
|
32
|
-
let slowTimer: ReturnType<typeof setInterval> | undefined
|
|
33
|
-
let stealTimer: ReturnType<typeof setInterval> | undefined
|
|
34
|
-
let inFlight: AbortController | undefined
|
|
35
|
-
let siblings: Sibling[] | undefined
|
|
36
|
-
let generation = 0
|
|
37
|
-
let hideWidgets = true
|
|
38
|
-
let busy = false
|
|
39
|
-
let currentCwd = process.cwd()
|
|
40
|
-
const gitState = createGitState(pi, () => currentCwd)
|
|
41
|
-
const stopTimers = () => {
|
|
42
|
-
if (fastTimer) clearInterval(fastTimer)
|
|
43
|
-
if (slowTimer) clearInterval(slowTimer)
|
|
44
|
-
if (stealTimer) clearInterval(stealTimer)
|
|
45
|
-
fastTimer = undefined
|
|
46
|
-
slowTimer = undefined
|
|
47
|
-
stealTimer = undefined
|
|
48
|
-
inFlight?.abort()
|
|
49
|
-
inFlight = undefined
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const scheduleFast = (ctx: ExtensionContext) => {
|
|
53
|
-
if (fastTimer) return
|
|
54
|
-
if (slowTimer) clearInterval(slowTimer)
|
|
55
|
-
slowTimer = undefined
|
|
56
|
-
fastTimer = setInterval(() => {
|
|
57
|
-
void probe(ctx)
|
|
58
|
-
}, 2000)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const scheduleSlow = (ctx: ExtensionContext) => {
|
|
62
|
-
if (slowTimer) return
|
|
63
|
-
if (fastTimer) clearInterval(fastTimer)
|
|
64
|
-
fastTimer = undefined
|
|
65
|
-
slowTimer = setInterval(() => {
|
|
66
|
-
void probe(ctx)
|
|
67
|
-
}, 10000)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const probe = async (ctx: ExtensionContext) => {
|
|
71
|
-
if (process.env.HERDR_ENV !== "1") return
|
|
72
|
-
if (inFlight) return
|
|
73
|
-
const ac = new AbortController()
|
|
74
|
-
const gen = generation
|
|
75
|
-
inFlight = ac
|
|
76
|
-
try {
|
|
77
|
-
const agentRun = await pi.exec("herdr", ["agent", "list"], {
|
|
78
|
-
timeout: 3000,
|
|
79
|
-
signal: ac.signal,
|
|
80
|
-
})
|
|
81
|
-
if (generation !== gen) return
|
|
82
|
-
const tabRun = await pi.exec("herdr", ["tab", "list"], {
|
|
83
|
-
timeout: 3000,
|
|
84
|
-
signal: ac.signal,
|
|
85
|
-
})
|
|
86
|
-
if (generation !== gen) return
|
|
87
|
-
if (agentRun.code !== 0 || tabRun.code !== 0 || agentRun.killed || tabRun.killed) {
|
|
88
|
-
siblings = undefined
|
|
89
|
-
scheduleSlow(ctx)
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
siblings = listSiblings(
|
|
93
|
-
parseAgentList(agentRun.stdout),
|
|
94
|
-
parseTabList(tabRun.stdout),
|
|
95
|
-
process.env.HERDR_PANE_ID,
|
|
96
|
-
process.env.HERDR_TAB_ID,
|
|
97
|
-
)
|
|
98
|
-
if (siblings) scheduleFast(ctx)
|
|
99
|
-
else scheduleSlow(ctx)
|
|
100
|
-
} catch {
|
|
101
|
-
if (generation !== gen) return
|
|
102
|
-
siblings = undefined
|
|
103
|
-
scheduleSlow(ctx)
|
|
104
|
-
} finally {
|
|
105
|
-
if (inFlight === ac) inFlight = undefined
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
24
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
applyReclaim(
|
|
112
|
-
reason,
|
|
113
|
-
ctx.ui,
|
|
114
|
-
{
|
|
115
|
-
widgets: () => {
|
|
116
|
-
if (hideWidgets) hideGauntletWidgets(ctx)
|
|
117
|
-
},
|
|
118
|
-
chrome: () => {
|
|
119
|
-
installHeader(ctx)
|
|
120
|
-
installChromeFooter(ctx)
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const contextPct = (ctx: ExtensionContext): number | undefined => {
|
|
127
|
-
const usage = ctx.getContextUsage?.()
|
|
128
|
-
const window = usage?.contextWindow || ctx.model?.contextWindow
|
|
129
|
-
if (typeof usage?.tokens !== "number" || !window) return undefined
|
|
130
|
-
return Math.max(0, Math.min(100, Math.round((usage.tokens / window) * 100)))
|
|
131
|
-
}
|
|
25
|
+
let active: Session | undefined
|
|
26
|
+
let busy = false
|
|
132
27
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}, contributions)
|
|
28
|
+
// Background processes: `/bg` and a shortcut (default ctrl+b, see `backgroundShortcut`). The
|
|
29
|
+
// shortcut is registered once per key at load, since pi has no unregister; it reads the active
|
|
30
|
+
// session's config when pressed.
|
|
31
|
+
const openBackground = async (ctx: ExtensionContext) => {
|
|
32
|
+
if (!active?.background) return
|
|
33
|
+
await showBackgroundOverlay(ctx, active.background, active.backgroundShortcut ?? undefined)
|
|
140
34
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
35
|
+
pi.registerCommand("bg", { description: "Show processes left running by bash tool calls", handler: (_args, ctx) => openBackground(ctx) })
|
|
36
|
+
// Open PRs for the repo in cwd: `/prs [mine|review|all]` and the `list_open_prs` tool.
|
|
37
|
+
pi.registerCommand("prs", {
|
|
38
|
+
description: "Open PRs in this repo that I authored or that need my review",
|
|
39
|
+
handler: (args, ctx) => runPrsCommand(args, ctx, pi, active?.prs),
|
|
40
|
+
})
|
|
41
|
+
pi.registerTool({
|
|
42
|
+
name: "list_open_prs",
|
|
43
|
+
label: "List Open PRs",
|
|
44
|
+
description: "List open pull requests in the current repository that the user authored or has been asked to review (via the gh CLI).",
|
|
45
|
+
parameters: Type.Object({
|
|
46
|
+
filter: Type.Optional(Type.Union([Type.Literal("all"), Type.Literal("mine"), Type.Literal("review")], { description: "all (default), mine, or review (waiting on my review)" })),
|
|
47
|
+
}),
|
|
48
|
+
async execute(_toolCallId, params) {
|
|
49
|
+
await active?.prs?.refresh()
|
|
50
|
+
const result = prsToolResult(active?.prs, (params.filter as PrFilter | undefined) ?? parsePrFilter(""))
|
|
51
|
+
const text = result.prs.length === 0
|
|
52
|
+
? `No open PRs${result.repo ? ` in ${result.repo}` : ""}${result.error ? ` (${result.error})` : ""}.`
|
|
53
|
+
: result.prs.map((pr) => `#${pr.number} [${pr.kind}${pr.isDraft ? ", draft" : ""}${pr.ciFailing ? ", CI failing" : ""}] ${pr.title} — ${pr.url}`).join("\n")
|
|
54
|
+
return { content: [{ type: "text", text }], details: result }
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
const registeredShortcuts = new Set<string>()
|
|
58
|
+
const ensureShortcut = () => {
|
|
59
|
+
const key = active?.backgroundShortcut
|
|
60
|
+
if (!key || registeredShortcuts.has(key) || typeof pi.registerShortcut !== "function") return
|
|
61
|
+
registeredShortcuts.add(key)
|
|
62
|
+
pi.registerShortcut(key as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
63
|
+
description: "Show background processes",
|
|
64
|
+
handler: (ctx) => (active?.backgroundShortcut === key ? openBackground(ctx) : undefined),
|
|
147
65
|
})
|
|
148
66
|
}
|
|
149
67
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
68
|
+
pi.on("session_start", (_event, ctx) => {
|
|
69
|
+
active?.dispose()
|
|
70
|
+
active = createSession(pi, ctx, { busy: () => busy })
|
|
71
|
+
ensureShortcut()
|
|
72
|
+
})
|
|
156
73
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
agentDir: getAgentDir(),
|
|
166
|
-
cwd: ctx.cwd,
|
|
167
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
168
|
-
})
|
|
169
|
-
hideWidgets = config.hideGauntletWidgets
|
|
170
|
-
setEditDiffMode(config.editDiff)
|
|
171
|
-
currentCwd = ctx.cwd
|
|
172
|
-
setSessionCwd(ctx.cwd)
|
|
173
|
-
try {
|
|
174
|
-
const settings = SettingsManager.create(ctx.cwd, undefined, { projectTrusted: ctx.isProjectTrusted() })
|
|
175
|
-
setChromeMargin(settings.getOutputPad())
|
|
176
|
-
} catch {
|
|
177
|
-
// Keep the default margin when settings are unavailable.
|
|
178
|
-
}
|
|
179
|
-
installHeader(ctx)
|
|
180
|
-
installChromeFooter(ctx)
|
|
181
|
-
installStatusLine(ctx)
|
|
182
|
-
void gitState.refresh(true)
|
|
183
|
-
pinChrome(ctx, "session_start")
|
|
184
|
-
ctx.ui.setHiddenThinkingLabel("◆ Thought")
|
|
185
|
-
ctx.ui.setWorkingIndicator()
|
|
186
|
-
const sessionGeneration = generation
|
|
187
|
-
const settle = () => {
|
|
188
|
-
if (sessionActive && generation === sessionGeneration) pinChrome(ctx, "session_settle")
|
|
189
|
-
}
|
|
190
|
-
setTimeout(settle, 0)
|
|
191
|
-
setTimeout(settle, 50)
|
|
192
|
-
if (!stealTimer) {
|
|
193
|
-
stealTimer = setInterval(() => hideForeignChrome(ctx, "interval"), 200)
|
|
194
|
-
}
|
|
195
|
-
if (process.env.HERDR_ENV === "1") {
|
|
196
|
-
await probe(ctx)
|
|
74
|
+
// Commands that create a PR: remember the URL they print so the chips follow the session
|
|
75
|
+
// even when cwd is another repo (worktrees) or no repo at all.
|
|
76
|
+
const prCreateCalls = new Map<string, true>()
|
|
77
|
+
pi.on("tool_execution_start", (event) => {
|
|
78
|
+
active?.background?.toolStart(event.toolCallId, event.toolName)
|
|
79
|
+
const command = (event.args as { command?: unknown } | undefined)?.command
|
|
80
|
+
if ((event.toolName === "bash" || event.toolName === "powershell") && typeof command === "string" && isPrCreateCommand(command)) {
|
|
81
|
+
prCreateCalls.set(event.toolCallId, true)
|
|
197
82
|
}
|
|
198
83
|
})
|
|
199
84
|
|
|
@@ -202,27 +87,28 @@ export default function piMinimalExtension(pi: ExtensionAPI): void {
|
|
|
202
87
|
})
|
|
203
88
|
pi.on("agent_end", () => {
|
|
204
89
|
busy = false
|
|
205
|
-
|
|
90
|
+
active?.refreshGit()
|
|
206
91
|
})
|
|
207
|
-
pi.on("turn_end", (
|
|
208
|
-
|
|
209
|
-
|
|
92
|
+
pi.on("turn_end", () => {
|
|
93
|
+
active?.pin("turn_end")
|
|
94
|
+
active?.refreshGit()
|
|
210
95
|
})
|
|
211
|
-
pi.on("tool_execution_end", (
|
|
212
|
-
|
|
213
|
-
|
|
96
|
+
pi.on("tool_execution_end", (event) => {
|
|
97
|
+
active?.background?.toolEnd(event.toolCallId, event.toolName)
|
|
98
|
+
if (prCreateCalls.delete(event.toolCallId) && !event.isError) {
|
|
99
|
+
const content = (event.result as { content?: Array<{ type?: string; text?: string }> } | undefined)?.content ?? []
|
|
100
|
+
active?.notePrUrls(content.map((part) => (part.type === "text" && typeof part.text === "string" ? part.text : "")).join("\n"))
|
|
101
|
+
}
|
|
102
|
+
active?.pin("tool_execution_end")
|
|
103
|
+
active?.refreshGit()
|
|
214
104
|
})
|
|
215
105
|
|
|
216
|
-
pi.on("session_shutdown",
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
stopContributionEvents()
|
|
106
|
+
pi.on("session_shutdown", () => {
|
|
107
|
+
active?.dispose()
|
|
108
|
+
active = undefined
|
|
220
109
|
busy = false
|
|
221
|
-
stopTimers()
|
|
222
110
|
stopWorkingStatus()
|
|
223
111
|
stopTurnRule()
|
|
224
112
|
stopLiveWatch()
|
|
225
|
-
generation += 1
|
|
226
|
-
siblings = undefined
|
|
227
113
|
})
|
|
228
114
|
}
|
package/src/listeners.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A tiny change-notification set shared by the git and sibling sources. */
|
|
2
|
+
export function createListeners() {
|
|
3
|
+
const listeners = new Set<() => void>()
|
|
4
|
+
return {
|
|
5
|
+
notify() {
|
|
6
|
+
for (const listener of listeners) {
|
|
7
|
+
try {
|
|
8
|
+
listener()
|
|
9
|
+
} catch {
|
|
10
|
+
// One listener's failure must not starve the others.
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
add(listener: () => void) {
|
|
15
|
+
listeners.add(listener)
|
|
16
|
+
return () => listeners.delete(listener)
|
|
17
|
+
},
|
|
18
|
+
clear() {
|
|
19
|
+
listeners.clear()
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
2
|
+
import { describePr, PR_LEGEND, type PrSource, type PullRequest } from "./prs.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `/prs [mine|review|all]` lists the current repo's open PRs that involve me and offers actions on
|
|
6
|
+
* the chosen one. The `list_open_prs` tool gives the model the same list.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type PrFilter = "all" | "mine" | "review"
|
|
10
|
+
|
|
11
|
+
export function parsePrFilter(args: string): PrFilter {
|
|
12
|
+
const word = args.trim().split(/\s+/)[0]?.toLowerCase() ?? ""
|
|
13
|
+
if (["mine", "author", "authored", "my"].includes(word)) return "mine"
|
|
14
|
+
if (["review", "reviews", "review-requested", "requested"].includes(word)) return "review"
|
|
15
|
+
return "all"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function filterPrs(prs: PullRequest[], filter: PrFilter): PullRequest[] {
|
|
19
|
+
if (filter === "all") return prs
|
|
20
|
+
return prs.filter((pr) => pr.kind === filter)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ACTIONS = ["Open in browser", "Paste URL into editor", "Checkout branch", "Cancel"] as const
|
|
24
|
+
|
|
25
|
+
export async function runPrsCommand(args: string, ctx: ExtensionContext, pi: ExtensionAPI, source: PrSource | undefined): Promise<void> {
|
|
26
|
+
if (!source) {
|
|
27
|
+
ctx.ui.notify("PRs are only tracked in interactive sessions.", "info")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
await source.refresh(true)
|
|
31
|
+
const snap = source.get()
|
|
32
|
+
if (!snap.repo && snap.prs.length === 0) {
|
|
33
|
+
ctx.ui.notify(snap.error ? `No GitHub repo here: ${snap.error}` : "No GitHub repo here.", "warning")
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
const filter = parsePrFilter(args)
|
|
37
|
+
const prs = filterPrs(snap.prs, filter)
|
|
38
|
+
const title = `${snap.repo?.nameWithOwner ?? "this session"}: ${prs.length} open PR${prs.length === 1 ? "" : "s"}${filter === "all" ? "" : ` (${filter})`}${snap.error ? ` · stale: ${snap.error}` : ""}`
|
|
39
|
+
if (prs.length === 0) {
|
|
40
|
+
ctx.ui.notify(title, "info")
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (ctx.mode !== "tui") {
|
|
44
|
+
ctx.ui.notify(`${title}\n${prs.map((pr) => `${describePr(pr, snap.repo ? `${snap.repo.host}/${snap.repo.nameWithOwner}` : undefined)}\n ${pr.url}`).join("\n")}`, "info")
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
const cwdRepo = snap.repo ? `${snap.repo.host}/${snap.repo.nameWithOwner}` : undefined
|
|
48
|
+
const labels = prs.map((pr) => describePr(pr, cwdRepo))
|
|
49
|
+
const chosen = await ctx.ui.select(`${title}\n${PR_LEGEND}`, labels)
|
|
50
|
+
if (chosen === undefined) return
|
|
51
|
+
const pr = prs[labels.indexOf(chosen)]
|
|
52
|
+
if (!pr) return
|
|
53
|
+
const action = await ctx.ui.select(`#${pr.number} ${pr.title}`, [...ACTIONS])
|
|
54
|
+
if (action === "Open in browser") {
|
|
55
|
+
const open = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"
|
|
56
|
+
const run = await pi.exec(open, [pr.url])
|
|
57
|
+
if (run.code !== 0) ctx.ui.notify(`Could not open browser: ${run.stderr.trim()}`, "error")
|
|
58
|
+
} else if (action === "Paste URL into editor") {
|
|
59
|
+
ctx.ui.pasteToEditor(pr.url)
|
|
60
|
+
} else if (action === "Checkout branch") {
|
|
61
|
+
const run = await pi.exec("gh", ["pr", "checkout", String(pr.number), "-R", pr.repo], { cwd: ctx.cwd, timeout: 60_000 })
|
|
62
|
+
ctx.ui.notify(run.code === 0 ? `Checked out #${pr.number}` : `Checkout failed: ${run.stderr.trim()}`, run.code === 0 ? "info" : "error")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function prsToolResult(source: PrSource | undefined, filter: PrFilter) {
|
|
67
|
+
const snap = source?.get()
|
|
68
|
+
const prs = filterPrs(snap?.prs ?? [], filter).map((pr) => ({
|
|
69
|
+
number: pr.number,
|
|
70
|
+
title: pr.title,
|
|
71
|
+
url: pr.url,
|
|
72
|
+
repo: pr.repo,
|
|
73
|
+
author: pr.author,
|
|
74
|
+
isDraft: pr.isDraft,
|
|
75
|
+
kind: pr.kind,
|
|
76
|
+
reviewDecision: pr.reviewDecision,
|
|
77
|
+
ciFailing: pr.ciFailing,
|
|
78
|
+
updatedAt: pr.updatedAt,
|
|
79
|
+
}))
|
|
80
|
+
return { repo: snap?.repo?.nameWithOwner, host: snap?.repo?.host, fetchedAt: snap?.fetchedAt, error: snap?.error, prs }
|
|
81
|
+
}
|