pi-minimal 0.3.1 → 0.5.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/src/index.ts CHANGED
@@ -1,199 +1,29 @@
1
- import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent"
2
- import { loadPiMinimalConfig } from "./config.js"
3
- import { installHeader } from "./header.js"
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"
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
2
+ import { installCodeBox } from "./code-box.js"
3
+ import { createSession, type Session } from "./session.js"
10
4
  import { installToolRenderers, 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
5
  import { installTranscript } from "./transcript.js"
16
- import { createGitState } from "./git-state.js"
17
- import { installCodeBox } from "./code-box.js"
18
- import { setChromeMargin } from "./chrome.js"
19
- import { SettingsManager } from "@earendil-works/pi-coding-agent"
6
+ import { installTurnRule } from "./turn-rule.js"
7
+ import { installWorkingStatus } from "./working-status.js"
20
8
 
9
+ /**
10
+ * Process-wide hooks are installed once at load (renderers, transformers, the code-box patch);
11
+ * everything else lives on a `Session` created at session_start and disposed at session_shutdown
12
+ * or when the next session starts.
13
+ */
21
14
  export default function piMinimalExtension(pi: ExtensionAPI): void {
22
15
  const stopWorkingStatus = installWorkingStatus(pi)
23
16
  const stopTurnRule = installTurnRule(pi)
24
17
  installToolRenderers(pi)
25
18
  installTranscript(pi)
26
19
  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
20
 
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
-
109
- const hideForeignChrome = (ctx: ExtensionContext, reason: ReclaimReason) => {
110
- if (ctx.mode !== "tui") return
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
- }
132
-
133
- const installChromeFooter = (ctx: ExtensionContext) => {
134
- stopFooter()
135
- stopFooter = installFooter(ctx, {
136
- busy: () => busy,
137
- editorText: () => (typeof ctx.ui.getEditorText === "function" ? ctx.ui.getEditorText() : ""),
138
- contextPct: () => contextPct(ctx),
139
- }, contributions)
140
- }
141
-
142
- const installStatusLine = (ctx: ExtensionContext) => {
143
- installCwd(ctx, {
144
- gitState: gitState.get,
145
- siblings: () => siblings,
146
- onGitChange: gitState.onChange,
147
- })
148
- }
149
-
150
- const pinChrome = (ctx: ExtensionContext, reason: ReclaimReason) => {
151
- hideForeignChrome(ctx, reason)
152
- if (ctx.mode !== "tui" || !reclaimPolicy(reason).reinstallChrome) return
153
- ctx.ui.setWidget("tw-cwd", undefined)
154
- installStatusLine(ctx)
155
- }
21
+ let active: Session | undefined
22
+ let busy = false
156
23
 
157
- pi.on("session_start", async (_event, ctx) => {
158
- stopFooter()
159
- stopContributionEvents()
160
- sessionActive = true
161
- generation += 1
162
- stopContributionEvents = bindFooterContributionEvents(pi.events, contributions, ctx.sessionManager.getSessionId(), ctx.mode)
163
- if (ctx.mode !== "tui") return
164
- const config = loadPiMinimalConfig({
165
- agentDir: getAgentDir(),
166
- cwd: ctx.cwd,
167
- projectTrusted: ctx.isProjectTrusted(),
168
- })
169
- hideWidgets = config.hideGauntletWidgets
170
- currentCwd = ctx.cwd
171
- setSessionCwd(ctx.cwd)
172
- try {
173
- const settings = SettingsManager.create(ctx.cwd, undefined, { projectTrusted: ctx.isProjectTrusted() })
174
- setChromeMargin(settings.getOutputPad())
175
- } catch {
176
- // Keep the default margin when settings are unavailable.
177
- }
178
- installHeader(ctx)
179
- installChromeFooter(ctx)
180
- installStatusLine(ctx)
181
- void gitState.refresh(true)
182
- pinChrome(ctx, "session_start")
183
- ctx.ui.setHiddenThinkingLabel("◆ Thought")
184
- ctx.ui.setWorkingIndicator()
185
- const sessionGeneration = generation
186
- const settle = () => {
187
- if (sessionActive && generation === sessionGeneration) pinChrome(ctx, "session_settle")
188
- }
189
- setTimeout(settle, 0)
190
- setTimeout(settle, 50)
191
- if (!stealTimer) {
192
- stealTimer = setInterval(() => hideForeignChrome(ctx, "interval"), 200)
193
- }
194
- if (process.env.HERDR_ENV === "1") {
195
- await probe(ctx)
196
- }
24
+ pi.on("session_start", (_event, ctx) => {
25
+ active?.dispose()
26
+ active = createSession(pi, ctx, { busy: () => busy })
197
27
  })
198
28
 
199
29
  pi.on("agent_start", () => {
@@ -201,27 +31,23 @@ export default function piMinimalExtension(pi: ExtensionAPI): void {
201
31
  })
202
32
  pi.on("agent_end", () => {
203
33
  busy = false
204
- void gitState.refresh()
34
+ active?.refreshGit()
205
35
  })
206
- pi.on("turn_end", (_event, ctx) => {
207
- pinChrome(ctx, "turn_end")
208
- void gitState.refresh()
36
+ pi.on("turn_end", () => {
37
+ active?.pin("turn_end")
38
+ active?.refreshGit()
209
39
  })
210
- pi.on("tool_execution_end", (_event, ctx) => {
211
- pinChrome(ctx, "tool_execution_end")
212
- void gitState.refresh()
40
+ pi.on("tool_execution_end", () => {
41
+ active?.pin("tool_execution_end")
42
+ active?.refreshGit()
213
43
  })
214
44
 
215
- pi.on("session_shutdown", async () => {
216
- sessionActive = false
217
- stopFooter()
218
- stopContributionEvents()
45
+ pi.on("session_shutdown", () => {
46
+ active?.dispose()
47
+ active = undefined
219
48
  busy = false
220
- stopTimers()
221
49
  stopWorkingStatus()
222
50
  stopTurnRule()
223
51
  stopLiveWatch()
224
- generation += 1
225
- siblings = undefined
226
52
  })
227
53
  }
package/src/reclaim.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  export type ReclaimReason =
2
2
  | "session_start"
3
3
  | "session_settle"
4
- | "interval"
5
4
  | "turn_end"
6
5
  | "tool_execution_end"
7
6
 
@@ -14,9 +13,6 @@ export function reclaimPolicy(reason: ReclaimReason): ReclaimPolicy {
14
13
  if (reason === "session_start") {
15
14
  return { resetEditor: true, reinstallChrome: true }
16
15
  }
17
- if (reason === "interval") {
18
- return { resetEditor: false, reinstallChrome: false }
19
- }
20
16
  // session_settle / turn_end / tool_execution_end: restore chrome, keep editor
21
17
  return { resetEditor: false, reinstallChrome: true }
22
18
  }
package/src/session.ts ADDED
@@ -0,0 +1,159 @@
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 { listSiblings, parseAgentList, parseTabList, type Sibling } from "./herdr.js"
11
+ import { applyReclaim, reclaimPolicy, type ReclaimReason } from "./reclaim.js"
12
+ import { setSessionCwd } from "./tool-rows.js"
13
+ import { setEditDiffMode } from "./tools.js"
14
+ import { hideGauntletWidgets } from "./tracker.js"
15
+
16
+ export type SessionDeps = {
17
+ /** Whether an agent run is in progress; owned by the extension because agent events are not session-scoped. */
18
+ busy: () => boolean
19
+ }
20
+
21
+ export type Session = {
22
+ readonly id: string
23
+ /** Re-asserts chrome after an event that may have let another extension draw over it. */
24
+ pin(reason: ReclaimReason): void
25
+ refreshGit(force?: boolean): void
26
+ dispose(): void
27
+ }
28
+
29
+ const HERDR_FAST_MS = 2000
30
+ const HERDR_SLOW_MS = 10000
31
+ const HERDR_EXEC_TIMEOUT_MS = 3000
32
+
33
+ /**
34
+ * Everything pi-minimal owns for one session: config, cwd, the contribution registry, the git and
35
+ * Herdr pollers, and the header/footer/status-line chrome. Disposal stops every timer and makes
36
+ * late async results inert, so two sessions in one process never share or overwrite each other's
37
+ * state. Non-TUI sessions get a session too, but it only binds the contribution protocol (which
38
+ * replies `not-tui`) and installs no chrome.
39
+ */
40
+ export function createSession(pi: ExtensionAPI, ctx: ExtensionContext, deps: SessionDeps): Session {
41
+ const id = ctx.sessionManager.getSessionId()
42
+ let alive = true
43
+ const contributions = createFooterContributions()
44
+ const stopContributionEvents = bindFooterContributionEvents(pi.events, contributions, id, ctx.mode)
45
+ if (ctx.mode !== "tui") {
46
+ return { id, pin() {}, refreshGit() {}, dispose() { if (!alive) return; alive = false; stopContributionEvents() } }
47
+ }
48
+
49
+ const config = loadPiMinimalConfig({ agentDir: getAgentDir(), cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() })
50
+ setSessionCwd(ctx.cwd)
51
+ setEditDiffMode(config.editDiff)
52
+ try {
53
+ const settings = SettingsManager.create(ctx.cwd, undefined, { projectTrusted: ctx.isProjectTrusted() })
54
+ setChromeMargin(settings.getOutputPad())
55
+ } catch {
56
+ // Keep the current margin when settings are unavailable.
57
+ }
58
+
59
+ const gitState = createGitState(pi, () => ctx.cwd)
60
+ let stopFooter = () => {}
61
+ let siblings: Sibling[] | undefined
62
+ let herdrTimer: ReturnType<typeof setInterval> | undefined
63
+ let herdrPeriod = 0
64
+ let herdrInFlight: AbortController | undefined
65
+ const settleTimers: ReturnType<typeof setTimeout>[] = []
66
+
67
+ const contextPct = (): number | undefined => {
68
+ const usage = ctx.getContextUsage?.()
69
+ const window = usage?.contextWindow || ctx.model?.contextWindow
70
+ if (typeof usage?.tokens !== "number" || !window) return undefined
71
+ return Math.max(0, Math.min(100, Math.round((usage.tokens / window) * 100)))
72
+ }
73
+
74
+ const installChromeFooter = () => {
75
+ stopFooter()
76
+ stopFooter = installFooter(ctx, {
77
+ busy: deps.busy,
78
+ editorText: () => (typeof ctx.ui.getEditorText === "function" ? ctx.ui.getEditorText() : ""),
79
+ contextPct,
80
+ }, contributions)
81
+ }
82
+
83
+ const installStatusLine = () => {
84
+ installCwd(ctx, { gitState: gitState.get, siblings: () => siblings, onGitChange: gitState.onChange })
85
+ }
86
+
87
+ const pin = (reason: ReclaimReason) => {
88
+ if (!alive) return
89
+ applyReclaim(reason, ctx.ui, {
90
+ widgets: () => { if (config.hideGauntletWidgets) hideGauntletWidgets(ctx) },
91
+ chrome: () => { installHeader(ctx); installChromeFooter() },
92
+ })
93
+ if (!reclaimPolicy(reason).reinstallChrome) return
94
+ ctx.ui.setWidget("tw-cwd", undefined)
95
+ installStatusLine()
96
+ }
97
+
98
+ const scheduleHerdr = (periodMs: number) => {
99
+ if (!alive || herdrPeriod === periodMs) return
100
+ if (herdrTimer) clearInterval(herdrTimer)
101
+ herdrPeriod = periodMs
102
+ herdrTimer = setInterval(() => { void probeHerdr() }, periodMs)
103
+ }
104
+
105
+ const probeHerdr = async () => {
106
+ if (!alive || herdrInFlight) return
107
+ const ac = new AbortController()
108
+ herdrInFlight = ac
109
+ try {
110
+ const agentRun = await pi.exec("herdr", ["agent", "list"], { timeout: HERDR_EXEC_TIMEOUT_MS, signal: ac.signal })
111
+ if (!alive) return
112
+ const tabRun = await pi.exec("herdr", ["tab", "list"], { timeout: HERDR_EXEC_TIMEOUT_MS, signal: ac.signal })
113
+ if (!alive) return
114
+ if (agentRun.code !== 0 || tabRun.code !== 0 || agentRun.killed || tabRun.killed) {
115
+ siblings = undefined
116
+ scheduleHerdr(HERDR_SLOW_MS)
117
+ return
118
+ }
119
+ siblings = listSiblings(parseAgentList(agentRun.stdout), parseTabList(tabRun.stdout), process.env.HERDR_PANE_ID, process.env.HERDR_TAB_ID)
120
+ scheduleHerdr(siblings ? HERDR_FAST_MS : HERDR_SLOW_MS)
121
+ } catch {
122
+ if (!alive) return
123
+ siblings = undefined
124
+ scheduleHerdr(HERDR_SLOW_MS)
125
+ } finally {
126
+ if (herdrInFlight === ac) herdrInFlight = undefined
127
+ }
128
+ }
129
+
130
+ // Install once, then re-assert shortly after: other extensions' session_start handlers may draw after ours.
131
+ installHeader(ctx)
132
+ installChromeFooter()
133
+ installStatusLine()
134
+ void gitState.refresh(true)
135
+ pin("session_start")
136
+ ctx.ui.setHiddenThinkingLabel("◆ Thought")
137
+ ctx.ui.setWorkingIndicator()
138
+ for (const delay of [0, 50]) settleTimers.push(setTimeout(() => pin("session_settle"), delay))
139
+ if (process.env.HERDR_ENV === "1") void probeHerdr()
140
+
141
+ return {
142
+ id,
143
+ pin,
144
+ refreshGit(force) { if (alive) void gitState.refresh(force) },
145
+ dispose() {
146
+ if (!alive) return
147
+ alive = false
148
+ for (const timer of settleTimers.splice(0)) clearTimeout(timer)
149
+ if (herdrTimer) clearInterval(herdrTimer)
150
+ herdrTimer = undefined
151
+ herdrInFlight?.abort()
152
+ herdrInFlight = undefined
153
+ siblings = undefined
154
+ gitState.dispose()
155
+ stopFooter()
156
+ stopContributionEvents()
157
+ },
158
+ }
159
+ }