pi-minimal 0.4.0 → 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,200 +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"
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 { ExtensionAPI } from "@earendil-works/pi-coding-agent"
17
2
  import { installCodeBox } from "./code-box.js"
18
- import { setChromeMargin } from "./chrome.js"
19
- import { SettingsManager } from "@earendil-works/pi-coding-agent"
3
+ import { createSession, type Session } from "./session.js"
4
+ import { installToolRenderers, stopLiveWatch } from "./tools.js"
5
+ import { installTranscript } from "./transcript.js"
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
- 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)
197
- }
24
+ pi.on("session_start", (_event, ctx) => {
25
+ active?.dispose()
26
+ active = createSession(pi, ctx, { busy: () => busy })
198
27
  })
199
28
 
200
29
  pi.on("agent_start", () => {
@@ -202,27 +31,23 @@ export default function piMinimalExtension(pi: ExtensionAPI): void {
202
31
  })
203
32
  pi.on("agent_end", () => {
204
33
  busy = false
205
- void gitState.refresh()
34
+ active?.refreshGit()
206
35
  })
207
- pi.on("turn_end", (_event, ctx) => {
208
- pinChrome(ctx, "turn_end")
209
- void gitState.refresh()
36
+ pi.on("turn_end", () => {
37
+ active?.pin("turn_end")
38
+ active?.refreshGit()
210
39
  })
211
- pi.on("tool_execution_end", (_event, ctx) => {
212
- pinChrome(ctx, "tool_execution_end")
213
- void gitState.refresh()
40
+ pi.on("tool_execution_end", () => {
41
+ active?.pin("tool_execution_end")
42
+ active?.refreshGit()
214
43
  })
215
44
 
216
- pi.on("session_shutdown", async () => {
217
- sessionActive = false
218
- stopFooter()
219
- stopContributionEvents()
45
+ pi.on("session_shutdown", () => {
46
+ active?.dispose()
47
+ active = undefined
220
48
  busy = false
221
- stopTimers()
222
49
  stopWorkingStatus()
223
50
  stopTurnRule()
224
51
  stopLiveWatch()
225
- generation += 1
226
- siblings = undefined
227
52
  })
228
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
+ }