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/session.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { getAgentDir, SettingsManager, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
2
|
+
import { setChromeMargin } from "./chrome.js"
|
|
3
|
+
import { loadPiMinimalConfig } from "./config.js"
|
|
4
|
+
import { installCwd } from "./cwd.js"
|
|
5
|
+
import { installFooter } from "./footer.js"
|
|
6
|
+
import { bindFooterContributionEvents } from "./footer-contribution-events.js"
|
|
7
|
+
import { createFooterContributions } from "./footer-contributions.js"
|
|
8
|
+
import { createGitState } from "./git-state.js"
|
|
9
|
+
import { installHeader } from "./header.js"
|
|
10
|
+
import { createBackgroundTracker, type BackgroundTracker } from "./background.js"
|
|
11
|
+
import { backgroundSegment } from "./background-overlay.js"
|
|
12
|
+
import { createPrSource, findPrUrls, prSegment, type PrSource } from "./prs.js"
|
|
13
|
+
import { applyReclaim, reclaimPolicy, type ReclaimReason } from "./reclaim.js"
|
|
14
|
+
import type { RexSpawn } from "./rex.js"
|
|
15
|
+
import { createSiblingSource, type SiblingSource } from "./siblings.js"
|
|
16
|
+
import { setSessionCwd } from "./tool-rows.js"
|
|
17
|
+
import { setEditDiffMode } from "./tools.js"
|
|
18
|
+
import { hideGauntletWidgets } from "./tracker.js"
|
|
19
|
+
|
|
20
|
+
/** Custom session entry remembering a PR this session created, so a resumed session shows it. */
|
|
21
|
+
export const PR_ENTRY_TYPE = "pi-minimal.pr"
|
|
22
|
+
|
|
23
|
+
function readSessionPrs(ctx: ExtensionContext): string[] {
|
|
24
|
+
const urls: string[] = []
|
|
25
|
+
try {
|
|
26
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
27
|
+
if (entry.type !== "custom" || entry.customType !== PR_ENTRY_TYPE) continue
|
|
28
|
+
const url = (entry.data as { url?: unknown } | undefined)?.url
|
|
29
|
+
if (typeof url === "string") urls.push(url)
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// No branch available (tests, odd hosts): start empty.
|
|
33
|
+
}
|
|
34
|
+
return urls
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SessionDeps = {
|
|
38
|
+
/** Whether an agent run is in progress; owned by the extension because agent events are not session-scoped. */
|
|
39
|
+
busy: () => boolean
|
|
40
|
+
/** Test seam for the Rex event stream; defaults to node:child_process.spawn. */
|
|
41
|
+
spawn?: RexSpawn
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type Session = {
|
|
45
|
+
readonly id: string
|
|
46
|
+
/** Background processes left behind by bash tool calls; undefined outside TUI mode. */
|
|
47
|
+
readonly background?: BackgroundTracker
|
|
48
|
+
readonly backgroundShortcut?: string | null
|
|
49
|
+
/** Open PRs for the repo in cwd plus ones this session created; undefined outside TUI mode. */
|
|
50
|
+
readonly prs?: PrSource
|
|
51
|
+
/** Records PR URLs found in text (e.g. `gh pr create` output) as this session's PRs. */
|
|
52
|
+
notePrUrls(text: string): void
|
|
53
|
+
/** Re-asserts chrome after an event that may have let another extension draw over it. */
|
|
54
|
+
pin(reason: ReclaimReason): void
|
|
55
|
+
refreshGit(force?: boolean): void
|
|
56
|
+
dispose(): void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Everything pi-minimal owns for one session: config, cwd, the contribution registry, the git
|
|
61
|
+
* poller, the sibling source (Herdr or Rex), and the header/footer/status-line chrome. Disposal stops every timer and makes
|
|
62
|
+
* late async results inert, so two sessions in one process never share or overwrite each other's
|
|
63
|
+
* state. Non-TUI sessions get a session too, but it only binds the contribution protocol (which
|
|
64
|
+
* replies `not-tui`) and installs no chrome.
|
|
65
|
+
*/
|
|
66
|
+
export function createSession(pi: ExtensionAPI, ctx: ExtensionContext, deps: SessionDeps): Session {
|
|
67
|
+
const id = ctx.sessionManager.getSessionId()
|
|
68
|
+
let alive = true
|
|
69
|
+
const contributions = createFooterContributions()
|
|
70
|
+
const stopContributionEvents = bindFooterContributionEvents(pi.events, contributions, id, ctx.mode)
|
|
71
|
+
if (ctx.mode !== "tui") {
|
|
72
|
+
return { id, pin() {}, refreshGit() {}, notePrUrls() {}, dispose() { if (!alive) return; alive = false; stopContributionEvents() } }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const config = loadPiMinimalConfig({ agentDir: getAgentDir(), cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() })
|
|
76
|
+
setSessionCwd(ctx.cwd)
|
|
77
|
+
setEditDiffMode(config.editDiff)
|
|
78
|
+
try {
|
|
79
|
+
const settings = SettingsManager.create(ctx.cwd, undefined, { projectTrusted: ctx.isProjectTrusted() })
|
|
80
|
+
setChromeMargin(settings.getOutputPad())
|
|
81
|
+
} catch {
|
|
82
|
+
// Keep the current margin when settings are unavailable.
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const gitState = createGitState(pi, () => ctx.cwd)
|
|
86
|
+
let stopFooter = () => {}
|
|
87
|
+
let siblings: SiblingSource | undefined
|
|
88
|
+
const background = createBackgroundTracker({ pi })
|
|
89
|
+
const prs = createPrSource({ pi, cwd: () => ctx.cwd, sessionPrs: readSessionPrs(ctx) })
|
|
90
|
+
const settleTimers: ReturnType<typeof setTimeout>[] = []
|
|
91
|
+
|
|
92
|
+
const contextPct = (): number | undefined => {
|
|
93
|
+
const usage = ctx.getContextUsage?.()
|
|
94
|
+
const window = usage?.contextWindow || ctx.model?.contextWindow
|
|
95
|
+
if (typeof usage?.tokens !== "number" || !window) return undefined
|
|
96
|
+
return Math.max(0, Math.min(100, Math.round((usage.tokens / window) * 100)))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const installChromeFooter = () => {
|
|
100
|
+
stopFooter()
|
|
101
|
+
stopFooter = installFooter(ctx, {
|
|
102
|
+
busy: deps.busy,
|
|
103
|
+
editorText: () => (typeof ctx.ui.getEditorText === "function" ? ctx.ui.getEditorText() : ""),
|
|
104
|
+
contextPct,
|
|
105
|
+
prs: (theme) => {
|
|
106
|
+
const snap = prs.get()
|
|
107
|
+
return prSegment(snap.prs, theme, 4, snap.repo ? `${snap.repo.host}/${snap.repo.nameWithOwner}` : undefined)
|
|
108
|
+
},
|
|
109
|
+
onPrsChange: prs.onChange,
|
|
110
|
+
background: () => backgroundSegment(background.get()),
|
|
111
|
+
onBackgroundChange: background.onChange,
|
|
112
|
+
}, contributions)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const installStatusLine = () => {
|
|
116
|
+
installCwd(ctx, {
|
|
117
|
+
gitState: gitState.get,
|
|
118
|
+
siblings: () => siblings?.get(),
|
|
119
|
+
onChange(listener) {
|
|
120
|
+
const offGit = gitState.onChange(listener)
|
|
121
|
+
const offSiblings = siblings?.onChange(listener) ?? (() => {})
|
|
122
|
+
return () => { offGit(); offSiblings() }
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const pin = (reason: ReclaimReason) => {
|
|
128
|
+
if (!alive) return
|
|
129
|
+
applyReclaim(reason, ctx.ui, {
|
|
130
|
+
widgets: () => { if (config.hideGauntletWidgets) hideGauntletWidgets(ctx) },
|
|
131
|
+
chrome: () => { installHeader(ctx); installChromeFooter() },
|
|
132
|
+
})
|
|
133
|
+
if (!reclaimPolicy(reason).reinstallChrome) return
|
|
134
|
+
ctx.ui.setWidget("tw-cwd", undefined)
|
|
135
|
+
installStatusLine()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The source is chosen from the environment (HERDR_ENV, else REX_SESSION/REX_BLOCK); start it
|
|
139
|
+
// before the status line so its onChange subscription is live.
|
|
140
|
+
siblings = createSiblingSource({ pi, spawn: deps.spawn })
|
|
141
|
+
|
|
142
|
+
// Install once, then re-assert shortly after: other extensions' session_start handlers may draw after ours.
|
|
143
|
+
installHeader(ctx)
|
|
144
|
+
installChromeFooter()
|
|
145
|
+
installStatusLine()
|
|
146
|
+
void gitState.refresh(true)
|
|
147
|
+
pin("session_start")
|
|
148
|
+
ctx.ui.setHiddenThinkingLabel("◆ Thought")
|
|
149
|
+
ctx.ui.setWorkingIndicator()
|
|
150
|
+
for (const delay of [0, 50]) settleTimers.push(setTimeout(() => pin("session_settle"), delay))
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
background,
|
|
155
|
+
backgroundShortcut: config.backgroundShortcut,
|
|
156
|
+
prs,
|
|
157
|
+
notePrUrls(text) {
|
|
158
|
+
if (!alive) return
|
|
159
|
+
const known = new Set(prs.sessionPrs())
|
|
160
|
+
for (const url of findPrUrls(text)) {
|
|
161
|
+
if (known.has(url)) continue
|
|
162
|
+
prs.addSessionPr(url)
|
|
163
|
+
try {
|
|
164
|
+
pi.appendEntry?.(PR_ENTRY_TYPE, { url })
|
|
165
|
+
} catch {
|
|
166
|
+
// A session that cannot accept entries just forgets the PR on restart.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
pin,
|
|
171
|
+
refreshGit(force) { if (alive) { void gitState.refresh(force); void prs.refresh() } },
|
|
172
|
+
dispose() {
|
|
173
|
+
if (!alive) return
|
|
174
|
+
alive = false
|
|
175
|
+
for (const timer of settleTimers.splice(0)) clearTimeout(timer)
|
|
176
|
+
background.dispose()
|
|
177
|
+
prs.dispose()
|
|
178
|
+
siblings?.dispose()
|
|
179
|
+
siblings = undefined
|
|
180
|
+
gitState.dispose()
|
|
181
|
+
stopFooter()
|
|
182
|
+
stopContributionEvents()
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/siblings.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
2
|
+
import { createHerdrSiblings } from "./herdr.js"
|
|
3
|
+
import { createRexSiblings, type RexSpawn } from "./rex.js"
|
|
4
|
+
|
|
5
|
+
/** One other agent in the same workspace, as shown on the status line. */
|
|
6
|
+
export type Sibling = {
|
|
7
|
+
name: string
|
|
8
|
+
/** `idle | working | blocked | done | error`, or whatever the host reports; colour falls back to muted. */
|
|
9
|
+
status: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Where sibling status comes from. `get()` is synchronous; `onChange` fires when the list may have
|
|
14
|
+
* changed so the status line can re-render; `dispose()` stops timers and child processes and makes
|
|
15
|
+
* late results inert.
|
|
16
|
+
*/
|
|
17
|
+
export type SiblingSource = {
|
|
18
|
+
get(): Sibling[] | undefined
|
|
19
|
+
onChange(listener: () => void): () => void
|
|
20
|
+
dispose(): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SiblingSourceDeps = {
|
|
24
|
+
pi: ExtensionAPI
|
|
25
|
+
env?: NodeJS.ProcessEnv
|
|
26
|
+
spawn?: RexSpawn
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Picks the host the session runs in. Herdr wins when both are present (Rex inside a Herdr pane):
|
|
31
|
+
* Herdr knows every agent it launched, Rex only the programs that report status.
|
|
32
|
+
*/
|
|
33
|
+
export function createSiblingSource(deps: SiblingSourceDeps): SiblingSource | undefined {
|
|
34
|
+
const env = deps.env ?? process.env
|
|
35
|
+
if (env.HERDR_ENV === "1") return createHerdrSiblings(deps.pi, env)
|
|
36
|
+
if (env.REX_SESSION && env.REX_BLOCK) return createRexSiblings({ pi: deps.pi, env, spawn: deps.spawn })
|
|
37
|
+
return undefined
|
|
38
|
+
}
|