pi-minimal 0.1.0 → 0.2.2
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/.github/dependabot.yml +11 -0
- package/.github/workflows/publish.yml +178 -14
- package/.github/workflows/release.yml +42 -0
- package/CHANGELOG.md +81 -0
- package/README.md +28 -1
- package/SECURITY.md +21 -0
- package/dist/activity.js +0 -3
- package/dist/chrome.js +15 -0
- package/dist/code-box.js +69 -0
- package/dist/cwd.js +95 -53
- package/dist/footer.js +30 -29
- package/dist/git-state.js +63 -0
- package/dist/index.js +59 -6
- package/dist/tool-rows.js +143 -12
- package/dist/tools.js +146 -77
- package/dist/transcript.js +50 -0
- package/dist/turn-rule.js +121 -0
- package/package.json +14 -3
- package/release-notes.md +32 -0
- package/src/activity.ts +0 -4
- package/src/chrome.ts +18 -0
- package/src/code-box.ts +86 -0
- package/src/cwd.ts +104 -52
- package/src/footer.ts +38 -34
- package/src/git-state.ts +71 -0
- package/src/index.ts +60 -6
- package/src/tool-rows.ts +170 -19
- package/src/tools.ts +160 -105
- package/src/transcript.ts +51 -0
- package/src/turn-rule.ts +139 -0
- package/test/activity.test.ts +4 -15
- package/test/code-box.test.ts +55 -0
- package/test/footer.test.ts +38 -0
- package/test/git-state.test.ts +52 -0
- package/test/index.test.ts +40 -4
- package/test/package.test.ts +3 -2
- package/test/theme.test.ts +45 -0
- package/test/tool-rows.test.ts +109 -0
- package/test/tools.test.ts +49 -42
- package/test/transcript.test.ts +22 -0
- package/test/turn-rule.test.ts +33 -0
- package/themes/minimal-dark.json +84 -0
package/src/cwd.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
3
3
|
import path from "node:path"
|
|
4
|
-
import
|
|
4
|
+
import * as chrome from "./chrome.js"
|
|
5
|
+
import { formatGitState, type GitState } from "./git-state.js"
|
|
6
|
+
import type { Sibling } from "./herdr.js"
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
/** Tolerates a stale chrome module after /reload, where only CHROME_MARGIN may exist. */
|
|
9
|
+
const margin = (): number => (typeof chrome.chromeMargin === "function" ? chrome.chromeMargin() : (chrome.CHROME_MARGIN ?? 1))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function formatCwd(cwd: string): string {
|
|
7
13
|
const home = process.env.HOME || process.env.USERPROFILE || ""
|
|
8
14
|
if (!home) return cwd
|
|
9
15
|
const resolved = path.resolve(cwd)
|
|
@@ -14,7 +20,7 @@ function formatCwd(cwd: string): string {
|
|
|
14
20
|
return cwd
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
function formatTokens(n: number): string {
|
|
23
|
+
export function formatTokens(n: number): string {
|
|
18
24
|
if (n < 1000) return String(n)
|
|
19
25
|
if (n < 10000) return `${(n / 1000).toFixed(1)}K`
|
|
20
26
|
if (n < 1e6) return `${Math.round(n / 1000)}K`
|
|
@@ -22,7 +28,7 @@ function formatTokens(n: number): string {
|
|
|
22
28
|
return `${Math.round(n / 1e6)}M`
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
function modelIcon(id: string): string {
|
|
31
|
+
export function modelIcon(id: string): string {
|
|
26
32
|
const s = id.toLowerCase()
|
|
27
33
|
if (s.includes("grok")) return "✦"
|
|
28
34
|
if (
|
|
@@ -38,7 +44,7 @@ function modelIcon(id: string): string {
|
|
|
38
44
|
return "◆"
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
function shortModel(id: string): string {
|
|
47
|
+
export function shortModel(id: string): string {
|
|
42
48
|
return (id.split("/").pop() || id).replace(/^claude-/, "")
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -48,61 +54,107 @@ function thinkingColor(level: string): "dim" | "accent" | "warning" {
|
|
|
48
54
|
return "dim"
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
/** Context pressure band used for the meter and percentage colour. */
|
|
58
|
+
export function contextBand(pct: number): "accent" | "warning" | "error" {
|
|
59
|
+
if (pct >= 90) return "error"
|
|
60
|
+
if (pct >= 70) return "warning"
|
|
61
|
+
return "accent"
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
export function meter(percent: number, fill: string, empty: string, cells = 8): string {
|
|
65
|
+
const filled = Math.max(0, Math.min(cells, Math.round((percent / 100) * cells)))
|
|
66
|
+
return fill.repeat(filled) + empty.repeat(cells - filled)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fit `left` and `right` into `width`. Right-hand segments are dropped from the end first (they are
|
|
71
|
+
* the least important), then the left is truncated.
|
|
72
|
+
*/
|
|
73
|
+
export function splitRow(left: string, rightSegments: string[], width: number, sep = " "): string {
|
|
74
|
+
const segments = [...rightSegments.filter((s) => visibleWidth(s) > 0)]
|
|
75
|
+
let right = segments.join(sep)
|
|
76
|
+
while (segments.length > 0 && visibleWidth(left) + 2 + visibleWidth(right) > width) {
|
|
77
|
+
segments.pop()
|
|
78
|
+
right = segments.join(sep)
|
|
62
79
|
}
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
let L = left
|
|
81
|
+
if (visibleWidth(L) + (right ? 2 : 0) + visibleWidth(right) > width) {
|
|
82
|
+
L = truncateToWidth(L, Math.max(0, width - (right ? 2 : 0) - visibleWidth(right)), "")
|
|
65
83
|
}
|
|
66
|
-
const gap = Math.max(
|
|
67
|
-
return L + " ".repeat(gap) +
|
|
84
|
+
const gap = right ? Math.max(2, width - visibleWidth(L) - visibleWidth(right)) : 0
|
|
85
|
+
return L + " ".repeat(gap) + right
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
|
|
88
|
+
function siblingColor(status: string): "success" | "warning" | "error" | "accent" | "muted" {
|
|
89
|
+
if (status === "idle" || status === "done") return "success"
|
|
90
|
+
if (status === "working") return "accent"
|
|
91
|
+
if (status === "blocked") return "error"
|
|
92
|
+
return "muted"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type StatusLineSources = {
|
|
96
|
+
gitState: () => GitState | undefined
|
|
97
|
+
siblings: () => Sibling[] | undefined
|
|
98
|
+
onGitChange: (listener: () => void) => () => void
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function installCwd(ctx: ExtensionContext, sources: StatusLineSources): void {
|
|
71
102
|
ctx.ui.setWidget(
|
|
72
103
|
"tw-cwd",
|
|
73
|
-
(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
(tui, theme) => {
|
|
105
|
+
const unsub = sources.onGitChange(() => tui.requestRender())
|
|
106
|
+
return {
|
|
107
|
+
dispose: unsub,
|
|
108
|
+
invalidate() {},
|
|
109
|
+
render(width: number): string[] {
|
|
110
|
+
const inner = Math.max(8, width - margin() * 2)
|
|
111
|
+
const id = ctx.model?.id || "no-model"
|
|
112
|
+
const icon = theme.fg("accent", modelIcon(id))
|
|
113
|
+
const name = theme.fg("text", shortModel(id))
|
|
114
|
+
const thinking = ctx.thinkingLevel || ""
|
|
115
|
+
const think = thinking ? theme.fg(thinkingColor(thinking), thinking) : ""
|
|
116
|
+
|
|
117
|
+
const usage = ctx.getContextUsage?.()
|
|
118
|
+
const window = usage?.contextWindow || ctx.model?.contextWindow
|
|
119
|
+
const used = usage?.tokens
|
|
120
|
+
let bar = theme.fg("dim", "░░░░░░░░")
|
|
121
|
+
let pctStr = theme.fg("dim", "?%")
|
|
122
|
+
let tokenStr = theme.fg("dim", "?/?")
|
|
123
|
+
if (typeof used === "number" && window) {
|
|
124
|
+
const pct = Math.max(0, Math.min(100, Math.round((used / window) * 100)))
|
|
125
|
+
const band = contextBand(pct)
|
|
126
|
+
bar = meter(pct, theme.fg(band, "▓"), theme.fg("dim", "░"))
|
|
127
|
+
pctStr = theme.fg(band === "accent" ? "muted" : band, `${pct}%`)
|
|
128
|
+
tokenStr = theme.fg("dim", `${formatTokens(used)}/${formatTokens(window)}`)
|
|
129
|
+
} else if (used === null && window) {
|
|
130
|
+
tokenStr = theme.fg("dim", `?/${formatTokens(window)}`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const midot = theme.fg("dim", " · ")
|
|
134
|
+
const model = [`${icon} ${name}`, think].filter(Boolean).join(midot)
|
|
135
|
+
const context = `${bar} ${pctStr} ${tokenStr}`
|
|
136
|
+
const left = `${model} ${context}`
|
|
137
|
+
|
|
138
|
+
const right: string[] = []
|
|
139
|
+
if (ctx.cwd) right.push(theme.fg("muted", formatCwd(ctx.cwd)))
|
|
140
|
+
const git = formatGitState(sources.gitState())
|
|
141
|
+
if (git.branch) {
|
|
142
|
+
const dirty = git.dirty ? ` ${theme.fg("warning", git.dirty)}` : ""
|
|
143
|
+
right.push(`${theme.fg("dim", "⎇")} ${theme.fg("muted", git.branch)}${dirty}`)
|
|
144
|
+
}
|
|
145
|
+
const siblings = sources.siblings()
|
|
146
|
+
if (siblings && siblings.length > 0) {
|
|
147
|
+
right.push(
|
|
148
|
+
siblings
|
|
149
|
+
.map((s) => `${theme.fg("text", s.name)} ${theme.fg(siblingColor(s.status), s.status)}`)
|
|
150
|
+
.join(theme.fg("dim", " · ")),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
const row = splitRow(left, right, inner)
|
|
154
|
+
return [`${" ".repeat(margin())}${row}`]
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
},
|
|
106
158
|
{ placement: "aboveEditor" },
|
|
107
159
|
)
|
|
108
160
|
}
|
package/src/footer.ts
CHANGED
|
@@ -1,51 +1,55 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent"
|
|
3
|
-
import { truncateToWidth
|
|
4
|
-
import
|
|
5
|
-
import type { Sibling } from "./herdr.js"
|
|
3
|
+
import { truncateToWidth } from "@earendil-works/pi-tui"
|
|
4
|
+
import * as chrome from "./chrome.js"
|
|
6
5
|
import { formatTrackerLines } from "./tracker.js"
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
/** Tolerates a stale chrome module after /reload, where only CHROME_MARGIN may exist. */
|
|
8
|
+
const margin = (): number => (typeof chrome.chromeMargin === "function" ? chrome.chromeMargin() : (chrome.CHROME_MARGIN ?? 1))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export type FooterSources = {
|
|
12
|
+
/** True while an agent run is in progress. */
|
|
13
|
+
busy: () => boolean
|
|
14
|
+
/** Current editor text, when the host exposes it. */
|
|
15
|
+
editorText: () => string
|
|
16
|
+
/** Context usage percentage, when known. */
|
|
17
|
+
contextPct: () => number | undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Key hints are shown only when nothing else needs the line: idle agent, empty editor. Otherwise
|
|
22
|
+
* the footer collapses to zero lines (tracker lines excepted).
|
|
23
|
+
*/
|
|
24
|
+
export function footerHints(sources: FooterSources, hints: string[]): string[] {
|
|
25
|
+
if (sources.busy()) return []
|
|
26
|
+
if (sources.editorText().trim().length > 0) return []
|
|
27
|
+
const pct = sources.contextPct()
|
|
28
|
+
const out = [...hints]
|
|
29
|
+
if (pct !== undefined && pct >= 70) out.push("/compact")
|
|
30
|
+
return out
|
|
15
31
|
}
|
|
16
32
|
|
|
17
|
-
export function installFooter(
|
|
18
|
-
ctx: ExtensionContext,
|
|
19
|
-
getSiblings: () => Sibling[] | undefined,
|
|
20
|
-
): void {
|
|
33
|
+
export function installFooter(ctx: ExtensionContext, sources: FooterSources): void {
|
|
21
34
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
22
35
|
const unsub = footerData.onBranchChange(() => tui.requestRender())
|
|
23
36
|
return {
|
|
24
37
|
dispose: unsub,
|
|
25
38
|
invalidate() {},
|
|
26
39
|
render(width: number): string[] {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const st = theme.fg(statusColor(s.status), s.status)
|
|
38
|
-
return `${name} ${st}`
|
|
39
|
-
})
|
|
40
|
-
.join(theme.fg("dim", " · "))
|
|
40
|
+
const inner = Math.max(8, width - margin() * 2)
|
|
41
|
+
const out: string[] = []
|
|
42
|
+
const hints = footerHints(sources, [
|
|
43
|
+
keyHint("tui.input.submit", "send"),
|
|
44
|
+
keyHint("app.thinking.cycle", "thinking"),
|
|
45
|
+
keyHint("app.tools.expand", "expand tools"),
|
|
46
|
+
].filter(Boolean))
|
|
47
|
+
if (hints.length > 0) {
|
|
48
|
+
const line = theme.fg("dim", hints.join(" · "))
|
|
49
|
+
out.push(`${" ".repeat(margin())}${truncateToWidth(line, inner, "")}`)
|
|
41
50
|
}
|
|
42
|
-
|
|
43
|
-
const inner = Math.max(8, width - CHROME_MARGIN * 2)
|
|
44
|
-
const pad = Math.max(1, inner - visibleWidth(left) - visibleWidth(right))
|
|
45
|
-
const line = truncateToWidth(left + " ".repeat(pad) + right, inner, "")
|
|
46
|
-
const out = [`${" ".repeat(CHROME_MARGIN)}${line}`]
|
|
47
51
|
for (const extra of formatTrackerLines(ctx, theme as { fg: (name: string, s: string) => string })) {
|
|
48
|
-
out.push(`${" ".repeat(
|
|
52
|
+
out.push(`${" ".repeat(margin())}${truncateToWidth(extra, inner, "")}`)
|
|
49
53
|
}
|
|
50
54
|
return out
|
|
51
55
|
},
|
package/src/git-state.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
2
|
+
|
|
3
|
+
export type GitState = {
|
|
4
|
+
branch: string | undefined
|
|
5
|
+
/** Count of changed paths from `git status --porcelain`; undefined when unknown. */
|
|
6
|
+
dirty: number | undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseBranch(stdout: string): string | undefined {
|
|
10
|
+
const line = stdout.trim().split("\n")[0]?.trim()
|
|
11
|
+
if (!line || line === "HEAD") return undefined
|
|
12
|
+
return line
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parsePorcelain(stdout: string): number {
|
|
16
|
+
return stdout.split("\n").filter((line) => line.trim().length > 0).length
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatGitState(state: GitState | undefined): { branch: string; dirty: string } {
|
|
20
|
+
if (!state?.branch) return { branch: "", dirty: "" }
|
|
21
|
+
const dirty = state.dirty && state.dirty > 0 ? `±${state.dirty}` : ""
|
|
22
|
+
return { branch: state.branch, dirty }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Polls git for the current branch and dirty count. Call `refresh()` after events that may have
|
|
27
|
+
* changed the tree (tool results, turn end); `get()` is always synchronous.
|
|
28
|
+
*/
|
|
29
|
+
export function createGitState(pi: ExtensionAPI, cwd: () => string, minIntervalMs = 5000) {
|
|
30
|
+
let state: GitState | undefined
|
|
31
|
+
let lastRun = 0
|
|
32
|
+
let inFlight: Promise<void> | undefined
|
|
33
|
+
const listeners = new Set<() => void>()
|
|
34
|
+
|
|
35
|
+
const refresh = (force = false): Promise<void> => {
|
|
36
|
+
const now = Date.now()
|
|
37
|
+
if (inFlight) return inFlight
|
|
38
|
+
if (!force && now - lastRun < minIntervalMs) return Promise.resolve()
|
|
39
|
+
lastRun = now
|
|
40
|
+
inFlight = (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const branchRun = await pi.exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: cwd(), timeout: 2000 })
|
|
43
|
+
if (branchRun.code !== 0) {
|
|
44
|
+
state = undefined
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
const branch = parseBranch(branchRun.stdout)
|
|
48
|
+
const statusRun = await pi.exec("git", ["status", "--porcelain"], { cwd: cwd(), timeout: 3000 })
|
|
49
|
+
const dirty = statusRun.code === 0 ? parsePorcelain(statusRun.stdout) : undefined
|
|
50
|
+
state = { branch, dirty }
|
|
51
|
+
} catch {
|
|
52
|
+
state = undefined
|
|
53
|
+
} finally {
|
|
54
|
+
inFlight = undefined
|
|
55
|
+
for (const listener of listeners) listener()
|
|
56
|
+
}
|
|
57
|
+
})()
|
|
58
|
+
return inFlight
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
get: () => state,
|
|
63
|
+
refresh,
|
|
64
|
+
onChange(listener: () => void) {
|
|
65
|
+
listeners.add(listener)
|
|
66
|
+
return () => listeners.delete(listener)
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type GitStateHandle = ReturnType<typeof createGitState>
|
package/src/index.ts
CHANGED
|
@@ -5,12 +5,22 @@ import { installFooter } from "./footer.js"
|
|
|
5
5
|
import { installCwd } from "./cwd.js"
|
|
6
6
|
import { listSiblings, parseAgentList, parseTabList, type Sibling } from "./herdr.js"
|
|
7
7
|
import { hideGauntletWidgets } from "./tracker.js"
|
|
8
|
-
import {
|
|
8
|
+
import { installToolRenderers, stopLiveWatch } from "./tools.js"
|
|
9
9
|
import { applyReclaim, reclaimPolicy, type ReclaimReason } from "./reclaim.js"
|
|
10
10
|
import { installWorkingStatus } from "./working-status.js"
|
|
11
|
+
import { installTurnRule } from "./turn-rule.js"
|
|
12
|
+
import { installTranscript } from "./transcript.js"
|
|
13
|
+
import { createGitState } from "./git-state.js"
|
|
14
|
+
import { installCodeBox } from "./code-box.js"
|
|
15
|
+
import { setChromeMargin } from "./chrome.js"
|
|
16
|
+
import { SettingsManager } from "@earendil-works/pi-coding-agent"
|
|
11
17
|
|
|
12
18
|
export default function piMinimalExtension(pi: ExtensionAPI): void {
|
|
13
19
|
const stopWorkingStatus = installWorkingStatus(pi)
|
|
20
|
+
const stopTurnRule = installTurnRule(pi)
|
|
21
|
+
installToolRenderers(pi)
|
|
22
|
+
installTranscript(pi)
|
|
23
|
+
installCodeBox()
|
|
14
24
|
let fastTimer: ReturnType<typeof setInterval> | undefined
|
|
15
25
|
let slowTimer: ReturnType<typeof setInterval> | undefined
|
|
16
26
|
let stealTimer: ReturnType<typeof setInterval> | undefined
|
|
@@ -18,6 +28,9 @@ export default function piMinimalExtension(pi: ExtensionAPI): void {
|
|
|
18
28
|
let siblings: Sibling[] | undefined
|
|
19
29
|
let generation = 0
|
|
20
30
|
let hideWidgets = true
|
|
31
|
+
let busy = false
|
|
32
|
+
let currentCwd = process.cwd()
|
|
33
|
+
const gitState = createGitState(pi, () => currentCwd)
|
|
21
34
|
const stopTimers = () => {
|
|
22
35
|
if (fastTimer) clearInterval(fastTimer)
|
|
23
36
|
if (slowTimer) clearInterval(slowTimer)
|
|
@@ -97,17 +110,40 @@ export default function piMinimalExtension(pi: ExtensionAPI): void {
|
|
|
97
110
|
},
|
|
98
111
|
chrome: () => {
|
|
99
112
|
installHeader(ctx)
|
|
100
|
-
|
|
113
|
+
installChromeFooter(ctx)
|
|
101
114
|
},
|
|
102
115
|
},
|
|
103
116
|
)
|
|
104
117
|
}
|
|
105
118
|
|
|
119
|
+
const contextPct = (ctx: ExtensionContext): number | undefined => {
|
|
120
|
+
const usage = ctx.getContextUsage?.()
|
|
121
|
+
const window = usage?.contextWindow || ctx.model?.contextWindow
|
|
122
|
+
if (typeof usage?.tokens !== "number" || !window) return undefined
|
|
123
|
+
return Math.max(0, Math.min(100, Math.round((usage.tokens / window) * 100)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const installChromeFooter = (ctx: ExtensionContext) => {
|
|
127
|
+
installFooter(ctx, {
|
|
128
|
+
busy: () => busy,
|
|
129
|
+
editorText: () => (typeof ctx.ui.getEditorText === "function" ? ctx.ui.getEditorText() : ""),
|
|
130
|
+
contextPct: () => contextPct(ctx),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const installStatusLine = (ctx: ExtensionContext) => {
|
|
135
|
+
installCwd(ctx, {
|
|
136
|
+
gitState: gitState.get,
|
|
137
|
+
siblings: () => siblings,
|
|
138
|
+
onGitChange: gitState.onChange,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
const pinChrome = (ctx: ExtensionContext, reason: ReclaimReason) => {
|
|
107
143
|
hideForeignChrome(ctx, reason)
|
|
108
144
|
if (ctx.mode !== "tui" || !reclaimPolicy(reason).reinstallChrome) return
|
|
109
145
|
ctx.ui.setWidget("tw-cwd", undefined)
|
|
110
|
-
|
|
146
|
+
installStatusLine(ctx)
|
|
111
147
|
}
|
|
112
148
|
|
|
113
149
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -118,10 +154,17 @@ export default function piMinimalExtension(pi: ExtensionAPI): void {
|
|
|
118
154
|
projectTrusted: ctx.isProjectTrusted(),
|
|
119
155
|
})
|
|
120
156
|
hideWidgets = config.hideGauntletWidgets
|
|
157
|
+
currentCwd = ctx.cwd
|
|
158
|
+
try {
|
|
159
|
+
const settings = SettingsManager.create(ctx.cwd, undefined, { projectTrusted: ctx.isProjectTrusted() })
|
|
160
|
+
setChromeMargin(settings.getOutputPad())
|
|
161
|
+
} catch {
|
|
162
|
+
// Keep the default margin when settings are unavailable.
|
|
163
|
+
}
|
|
121
164
|
installHeader(ctx)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
165
|
+
installChromeFooter(ctx)
|
|
166
|
+
installStatusLine(ctx)
|
|
167
|
+
void gitState.refresh(true)
|
|
125
168
|
pinChrome(ctx, "session_start")
|
|
126
169
|
ctx.ui.setHiddenThinkingLabel("◆ Thought")
|
|
127
170
|
ctx.ui.setWorkingIndicator()
|
|
@@ -135,16 +178,27 @@ export default function piMinimalExtension(pi: ExtensionAPI): void {
|
|
|
135
178
|
}
|
|
136
179
|
})
|
|
137
180
|
|
|
181
|
+
pi.on("agent_start", () => {
|
|
182
|
+
busy = true
|
|
183
|
+
})
|
|
184
|
+
pi.on("agent_end", () => {
|
|
185
|
+
busy = false
|
|
186
|
+
void gitState.refresh()
|
|
187
|
+
})
|
|
138
188
|
pi.on("turn_end", (_event, ctx) => {
|
|
139
189
|
pinChrome(ctx, "turn_end")
|
|
190
|
+
void gitState.refresh()
|
|
140
191
|
})
|
|
141
192
|
pi.on("tool_execution_end", (_event, ctx) => {
|
|
142
193
|
pinChrome(ctx, "tool_execution_end")
|
|
194
|
+
void gitState.refresh()
|
|
143
195
|
})
|
|
144
196
|
|
|
145
197
|
pi.on("session_shutdown", async () => {
|
|
198
|
+
busy = false
|
|
146
199
|
stopTimers()
|
|
147
200
|
stopWorkingStatus()
|
|
201
|
+
stopTurnRule()
|
|
148
202
|
stopLiveWatch()
|
|
149
203
|
generation += 1
|
|
150
204
|
siblings = undefined
|