pi-minimal 0.1.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.
@@ -0,0 +1,354 @@
1
+ # pi-minimal Package Implementation Plan
2
+
3
+ > **REQUIRED SUB-SKILL:** Use the subagent-driven-development skill to implement this plan task-by-task.
4
+
5
+ **Goal:** Package the current Grok-style Pi interface as an installable `pi-minimal` extension without changing its visual behavior except thinking-block visibility.
6
+
7
+ **Architecture:** Session-path TypeScript lives in `src/`, emits committed JavaScript to `dist/`, and loads layered JSON configuration before installing chrome. Tests compile separately and exercise configuration, tracker visibility, and session wiring without touching the live Pi setup.
8
+
9
+ **Tech Stack:** TypeScript, Node.js 22.19.0+, Node test runner, Pi extension APIs.
10
+
11
+ **Spec:** `doc/specs/2026-09-22-pi-minimal-package.md`
12
+
13
+ **Verification:** `npm test && npm run build && git diff --exit-code -- dist && PI_CODING_AGENT_DIR="$(mktemp -d)" pi --no-extensions -e ./dist/index.js --version`
14
+
15
+ ---
16
+
17
+ ## Files
18
+
19
+ **Create:**
20
+ - `package.json`
21
+ - `package-lock.json`
22
+ - `tsconfig.json`
23
+ - `tsconfig.tests.json`
24
+ - `.gitignore`
25
+ - `LICENSE`
26
+ - `README.md`
27
+ - `src/activity.ts`
28
+ - `src/chrome.ts`
29
+ - `src/config.ts`
30
+ - `src/cwd.ts`
31
+ - `src/footer.ts`
32
+ - `src/header.ts`
33
+ - `src/herdr.ts`
34
+ - `src/index.ts`
35
+ - `src/live-watch.ts`
36
+ - `src/reclaim.ts`
37
+ - `src/tool-rows.ts`
38
+ - `src/tools.ts`
39
+ - `src/tracker.ts`
40
+ - `src/working-status.ts`
41
+ - `test/activity.test.ts`
42
+ - `test/config.test.ts`
43
+ - `test/index.test.ts`
44
+ - `test/tracker.test.ts`
45
+ - `dist/` generated extension output
46
+
47
+ **Modify:** none
48
+
49
+ **Delete:** none
50
+
51
+ ## Wave 1 — Foundations
52
+
53
+ Parallel-safe: Tasks 1–4 own disjoint files.
54
+
55
+ ### Task 1: Package metadata
56
+
57
+ **TDD scenario:** New feature — full TDD cycle
58
+
59
+ **Spec:** doc/specs/2026-09-22-pi-minimal-package.md § "Package contract" L19-L35
60
+
61
+ **Files:**
62
+ - Create: `package.json`
63
+ - Create: `tsconfig.json`
64
+ - Create: `tsconfig.tests.json`
65
+ - Create: `.gitignore`
66
+ - Create: `package-lock.json`
67
+
68
+ - [ ] **Step 1: Write the failing package test**
69
+
70
+ ```js
71
+ import assert from "node:assert/strict"
72
+ import test from "node:test"
73
+ import { readFileSync } from "node:fs"
74
+
75
+ test("package manifest exposes the committed extension", () => {
76
+ const pkg = JSON.parse(readFileSync("package.json", "utf8"))
77
+ assert.equal(pkg.name, "pi-minimal")
78
+ assert.equal(pkg.version, "0.1.0")
79
+ assert.equal(pkg.license, "MIT")
80
+ assert.equal(pkg.type, "module")
81
+ assert.deepEqual(pkg.engines, { node: ">=22.19.0" })
82
+ assert.deepEqual(pkg.keywords, ["pi-package"])
83
+ assert.deepEqual(pkg.pi, { extensions: ["./dist/index.js"] })
84
+ assert.deepEqual(pkg.peerDependencies, {
85
+ "@earendil-works/pi-coding-agent": "*",
86
+ "@earendil-works/pi-tui": "*",
87
+ })
88
+ })
89
+ ```
90
+
91
+ - [ ] **Step 2: Run test, confirm failure**
92
+
93
+ Run: `node --test test/package.test.js`
94
+ Expected: FAIL because `package.json` does not exist.
95
+
96
+ - [ ] **Step 3: Add the package files**
97
+
98
+ Create `package.json` with the asserted fields, `"private": false`, devDependencies `typescript`, `@types/node`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui`, and scripts `"build": "tsc -p tsconfig.json"` and `"test": "tsc -p tsconfig.tests.json && node --test test-dist/*.test.js"`.
99
+
100
+ Create `tsconfig.json` with `rootDir: src`, `outDir: dist`, `module: NodeNext`, `moduleResolution: NodeNext`, `target: ES2022`, `strict: true`, and `include: ["src"]`.
101
+
102
+ Create `tsconfig.tests.json` extending it with `rootDir: .`, `outDir: test-dist`, and `include: ["src", "test"]`.
103
+
104
+ Create `.gitignore` containing `node_modules/` and `test-dist/`, but not `dist/`.
105
+
106
+ - [ ] **Step 4: Run test, confirm pass**
107
+
108
+ Run: `npm install && node --test test/package.test.js`
109
+ Expected: PASS
110
+
111
+ - [ ] **Step 5: Commit**
112
+
113
+ ```bash
114
+ git add package.json package-lock.json tsconfig.json tsconfig.tests.json .gitignore test/package.test.js
115
+ git commit -m "Add pi-minimal package manifest"
116
+ ```
117
+
118
+ ### Task 2: Configuration loader
119
+
120
+ **TDD scenario:** New feature — full TDD cycle
121
+
122
+ **Spec:** doc/specs/2026-09-22-pi-minimal-package.md § "Configuration" L47-L64
123
+
124
+ **Files:**
125
+ - Create: `src/config.ts`
126
+ - Test: `test/config.test.ts`
127
+
128
+ - [ ] **Step 1: Write the failing config tests**
129
+
130
+ Test `loadPiMinimalConfig({ agentDir, cwd, projectTrusted })` with temporary directories. Assert default `{ hideGauntletWidgets: true }`; global false overrides default; trusted project true overrides global false; untrusted project false does not override global true; omitted keys and unknown keys do not override; invalid JSON, a non-object root, and non-boolean `hideGauntletWidgets` call `console.error` and leave the lower-precedence value unchanged.
131
+
132
+ - [ ] **Step 2: Run test, confirm failure**
133
+
134
+ Run: `npx tsc -p tsconfig.tests.json && node --test test-dist/test/config.test.js`
135
+ Expected: FAIL because `src/config.ts` does not exist.
136
+
137
+ - [ ] **Step 3: Implement the loader**
138
+
139
+ Export `PiMinimalConfig` and `loadPiMinimalConfig`. Read `join(agentDir, "pi-minimal.json")`, then `join(cwd, CONFIG_DIR_NAME, "pi-minimal.json")` only when `projectTrusted` is true. Parse with `JSON.parse`, validate only `hideGauntletWidgets`, and use `console.error` for invalid layers.
140
+
141
+ - [ ] **Step 4: Run test, confirm pass**
142
+
143
+ Run: `npx tsc -p tsconfig.tests.json && node --test test-dist/test/config.test.js`
144
+ Expected: PASS
145
+
146
+ - [ ] **Step 5: Commit**
147
+
148
+ ```bash
149
+ git add src/config.ts test/config.test.ts
150
+ git commit -m "Add layered pi-minimal configuration"
151
+ ```
152
+
153
+ ### Task 3: Tracker visibility
154
+
155
+ **TDD scenario:** New feature — full TDD cycle
156
+
157
+ **Spec:** doc/specs/2026-09-22-pi-minimal-package.md § "Runtime behavior" L37-L45
158
+
159
+ **Files:**
160
+ - Create: `src/tracker.ts`
161
+ - Test: `test/tracker.test.ts`
162
+
163
+ - [ ] **Step 1: Write the failing tracker tests**
164
+
165
+ Assert a phase line appears only when one phase is not `pending`, an empty or all-pending snapshot emits no phase line, a nonempty task list emits a task line, and an empty task list emits none. Assert `hideGauntletWidgets` calls `setWidget("phase_tracker", undefined)` and `setWidget("plan_tracker", undefined)`.
166
+
167
+ - [ ] **Step 2: Run test, confirm failure**
168
+
169
+ Run: `npx tsc -p tsconfig.tests.json && node --test test-dist/test/tracker.test.js`
170
+ Expected: FAIL because `src/tracker.ts` does not exist.
171
+
172
+ - [ ] **Step 3: Copy and preserve the tracker**
173
+
174
+ Copy `formatTrackerLines` and `hideGauntletWidgets` from the live extension without changing their visibility rules.
175
+
176
+ - [ ] **Step 4: Run test, confirm pass**
177
+
178
+ Run: `npx tsc -p tsconfig.tests.json && node --test test-dist/test/tracker.test.js`
179
+ Expected: PASS
180
+
181
+ - [ ] **Step 5: Commit**
182
+
183
+ ```bash
184
+ git add src/tracker.ts test/tracker.test.ts
185
+ git commit -m "Preserve tracker footer visibility"
186
+ ```
187
+
188
+ ### Task 4: Activity rendering
189
+
190
+ **TDD scenario:** New feature — full TDD cycle
191
+
192
+ **Spec:** doc/specs/2026-09-22-pi-minimal-package.md § "Source and compatibility decisions" L66-L72
193
+
194
+ **Files:**
195
+ - Create: `src/activity.ts`
196
+ - Test: `test/activity.test.ts`
197
+
198
+ - [ ] **Step 1: Write the failing activity tests**
199
+
200
+ Assert `rowIsRunning` is false when `isPartial` is false and true when it is true. Assert `activityBullet(false, 0)`, `activityBullet(false, 1)`, and `activityBullet(true, 0)` return `"◆"`, while `activityBullet(true, 1)` returns `"◇"`.
201
+
202
+ - [ ] **Step 2: Run test, confirm failure**
203
+
204
+ Run: `npx tsc -p tsconfig.tests.json && node --test test-dist/test/activity.test.js`
205
+ Expected: FAIL because `src/activity.ts` does not exist.
206
+
207
+ - [ ] **Step 3: Copy the activity helpers**
208
+
209
+ Copy `ActivityContext`, `rowIsRunning`, and `activityBullet` unchanged. Do not add `paintActivityLines`.
210
+
211
+ - [ ] **Step 4: Run test, confirm pass**
212
+
213
+ Run: `npx tsc -p tsconfig.tests.json && node --test test-dist/test/activity.test.js`
214
+ Expected: PASS
215
+
216
+ - [ ] **Step 5: Commit**
217
+
218
+ ```bash
219
+ git add src/activity.ts test/activity.test.ts
220
+ git commit -m "Preserve activity rendering helpers"
221
+ ```
222
+
223
+ ## Wave 2 — Session wiring
224
+
225
+ Solo: Task 5 imports the configuration, tracker, and activity modules created in Wave 1.
226
+
227
+ Depends on Wave 1: Task 5 consumes those modules.
228
+
229
+ ### Task 5: Extension session wiring
230
+
231
+ **TDD scenario:** New feature — full TDD cycle
232
+
233
+ **Spec:** doc/specs/2026-09-22-pi-minimal-package.md § "Runtime behavior" L37-L45, § "Verification" L74-L86
234
+
235
+ **Files:**
236
+ - Create: `src/index.ts`
237
+ - Create: `src/header.ts`
238
+ - Create: `src/footer.ts`
239
+ - Create: `src/cwd.ts`
240
+ - Create: `src/chrome.ts`
241
+ - Create: `src/herdr.ts`
242
+ - Create: `src/tools.ts`
243
+ - Create: `src/tool-rows.ts`
244
+ - Create: `src/working-status.ts`
245
+ - Create: `src/reclaim.ts`
246
+ - Create: `src/live-watch.ts`
247
+ - Test: `test/index.test.ts`
248
+
249
+ - [ ] **Step 1: Write the failing session tests**
250
+
251
+ Drive the default export with a fake `ExtensionAPI`, emit `session_start`, and assert `setHiddenThinkingLabel("◆ Thought")`, no-argument `setWorkingIndicator()`, header, footer, and `tw-cwd` installation. Assert both Gauntlet widgets are cleared by default and neither is cleared when config disables hiding. Assert `setHideThinkingBlock` is absent from all calls. Assert no `herdr` execution when `HERDR_ENV` is unset and one probe when it equals `"1"`.
252
+
253
+ - [ ] **Step 2: Run test, confirm failure**
254
+
255
+ Run: `npx tsc -p tsconfig.tests.json && node --test test-dist/test/index.test.js`
256
+ Expected: FAIL because `src/index.ts` does not exist.
257
+
258
+ - [ ] **Step 3: Copy the session path and apply the deltas**
259
+
260
+ Copy the live session-path modules. Replace `editor.ts` with `src/chrome.ts` exporting `CHROME_MARGIN = 1`, and update imports to `./chrome.js`. Remove the `SettingsManager` import and `setHideThinkingBlock` call. Load config from `getAgentDir()` and `ctx.isProjectTrusted()` before the first reclaim, and skip widget clearing when `hideGauntletWidgets` is false.
261
+
262
+ - [ ] **Step 4: Run test, confirm pass**
263
+
264
+ Run: `npx tsc -p tsconfig.tests.json && node --test test-dist/test/index.test.js`
265
+ Expected: PASS
266
+
267
+ - [ ] **Step 5: Commit**
268
+
269
+ ```bash
270
+ git add src test/index.test.ts
271
+ git commit -m "Wire the pi-minimal session chrome"
272
+ ```
273
+
274
+ ## Wave 3 — Release artifact
275
+
276
+ Solo: Task 6 builds the extension entry created in Wave 2.
277
+
278
+ Depends on Wave 2: Task 6 emits `dist/index.js` from `src/index.ts`.
279
+
280
+ ### Task 6: Build and installation smoke
281
+
282
+ **TDD scenario:** Trivial change — use judgment
283
+
284
+ **Spec:** doc/specs/2026-09-22-pi-minimal-package.md § "Verification" L74-L86
285
+
286
+ **Files:**
287
+ - Create: `dist/index.js`
288
+ - Create: `dist/` supporting emitted modules
289
+
290
+ - [ ] **Step 1: Build the extension**
291
+
292
+ Run: `npm test && npm run build`
293
+ Expected: tests pass and `dist/index.js` exists.
294
+
295
+ - [ ] **Step 2: Confirm the artifact is stable**
296
+
297
+ Run: `git diff --exit-code -- dist`
298
+ Expected: exit 0 after `dist/` is committed.
299
+
300
+ - [ ] **Step 3: Load it in an isolated Pi directory**
301
+
302
+ Run: `PI_CODING_AGENT_DIR="$(mktemp -d)" pi --no-extensions -e ./dist/index.js --version`
303
+ Expected: Pi starts with the extension and exits successfully without reading or writing the live agent directory.
304
+
305
+ - [ ] **Step 4: Commit**
306
+
307
+ ```bash
308
+ git add dist
309
+ git commit -m "Add committed pi-minimal build"
310
+ ```
311
+
312
+ ## Wave 4 — Documentation
313
+
314
+ Solo: lone remaining task.
315
+
316
+ Depends on Waves 1–3: the README documents the completed package contract and artifact.
317
+
318
+ ### Task 7: README and license
319
+
320
+ **TDD scenario:** Trivial change — use judgment
321
+
322
+ **Spec:** doc/specs/2026-09-22-pi-minimal-package.md § "Documentation impact" L88-L91
323
+
324
+ **Files:**
325
+ - Create: `README.md`
326
+ - Create: `LICENSE`
327
+
328
+ - [ ] **Step 1: Add the MIT license**
329
+
330
+ Use the standard MIT license with `Copyright (c) 2026 raviy06`.
331
+
332
+ - [ ] **Step 2: Document installation and configuration**
333
+
334
+ Document Node.js `>=22.19.0`, Pi `>=0.84.4`, `pi install git:github.com/raviy06/pi-minimal`, `hideGauntletWidgets`, and the warning not to load this package beside the live `grok-chrome` extension.
335
+
336
+ - [ ] **Step 3: Commit**
337
+
338
+ ```bash
339
+ git add README.md LICENSE
340
+ git commit -m "Document pi-minimal installation"
341
+ ```
342
+
343
+ ## Spec coverage
344
+
345
+ | anchor | requirement (short) | owner |
346
+ |---|---|---|
347
+ | § "Package contract" L19-L35 | publish the Pi package manifest and peers | Task 1 |
348
+ | § "Runtime behavior" L37-L45 | preserve chrome and gate widget hiding | Task 5 |
349
+ | § "Runtime behavior" L37-L45 | preserve tracker line visibility | Task 3 |
350
+ | § "Configuration" L47-L64 | layer and validate configuration | Task 2 |
351
+ | § "Source and compatibility decisions" L66-L72 | build NodeNext output and test activity helpers | Task 4, Task 6 |
352
+ | § "Verification" L74-L86 | isolate tests and installation smoke | Task 5, Task 6 |
353
+ | § "Documentation impact" L88-L91 | add README and license | Task 7 |
354
+ | § "Out of scope" L93-L99 | exclude publishing, cutover, and unrelated migrations | waived: out of scope per spec |
@@ -0,0 +1,94 @@
1
+ # pi-minimal Package
2
+
3
+ ## Goal
4
+
5
+ Package the current Grok-style Pi interface as the public Pi package `pi-minimal`, so another machine can install it with Pi's normal package installer while retaining the current visual behavior, except thinking-block visibility. The package never reads or writes `hideThinkingBlock`; that display follows the user's existing Pi setting. `setHiddenThinkingLabel` restyles a hidden label and does not hide thinking blocks.
6
+
7
+ ## Scope
8
+
9
+ `pi-minimal` version `0.1.0` includes:
10
+
11
+ - The current header, footer, cwd widget, compact built-in tool rendering, working-status display, and optional Herdr integration.
12
+ - Compact phase and task lines in the package footer when current tracker rules make them visible.
13
+ - A TypeScript build targeting Node.js 22.19.0 or newer and Pi 0.84.4 or newer.
14
+ - Package metadata, MIT license, README, and automated tests.
15
+
16
+ The package does not include Omarchy themes, Omarchy skills, credentials, sessions, agentmemory, MCP configuration, or other local Pi customizations.
17
+
18
+ The live extension at `~/.pi/agent/extensions/grok-chrome` remains untouched during implementation and verification. Cutover is a separate user action after the packaged extension is verified.
19
+
20
+ ## Package contract
21
+
22
+ The repository root contains `package.json` with:
23
+
24
+ - `"name": "pi-minimal"`
25
+ - `"version": "0.1.0"`
26
+ - `"license": "MIT"`
27
+ - `"type": "module"`
28
+ - `"engines": { "node": ">=22.19.0" }`
29
+ - `"keywords": ["pi-package"]`
30
+ - `"pi": { "extensions": ["./dist/index.js"] }`
31
+ - `peerDependencies` of `"*"` for `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui`
32
+
33
+ `typescript` and `@types/node` are development dependencies. The Pi packages remain wildcard peers and are not bundled. `npm run build` refreshes the committed `dist/` artifact. `dist/` is not gitignored, so a normal Git installation receives `dist/index.js` without a consumer build or npm publication.
34
+
35
+ The MIT license uses `Copyright (c) 2026 raviy06`. The README documents `pi install git:github.com/raviy06/pi-minimal`, the Node and Pi requirements, configuration, and the cutover warning.
36
+
37
+ ## Runtime behavior
38
+
39
+ On TUI `session_start`, before the first reclaim, the extension loads configuration and installs the current visual chrome and compact renderers. It calls `ctx.ui.setHiddenThinkingLabel("◆ Thought")` and no-argument `ctx.ui.setWorkingIndicator()`. It never reads or writes Pi's `hideThinkingBlock` setting.
40
+
41
+ Gauntlet widget hiding is enabled unless configuration explicitly disables it. When enabled, the extension clears the `phase_tracker` and `plan_tracker` widgets and continues reclaiming that space. When disabled, it does not clear or reclaim those widgets. Footer visibility follows the current tracker rules regardless of that setting: the phase line appears only when at least one phase is not `pending`, and the task line appears only when the task list is nonempty.
42
+
43
+ Herdr probing and sibling display occur only when `HERDR_ENV=1`, as they do now. Print, JSON, and RPC modes do not install TUI components.
44
+
45
+ ## Configuration
46
+
47
+ The extension loads configuration in this order:
48
+
49
+ 1. Built-in defaults.
50
+ 2. `join(getAgentDir(), "pi-minimal.json")`, when present. This is `~/.pi/agent/pi-minimal.json` unless Pi's agent directory is overridden.
51
+ 3. `join(ctx.cwd, CONFIG_DIR_NAME, "pi-minimal.json")`, only when `ctx.isProjectTrusted()` returns true.
52
+
53
+ Project values override global values. A missing file, omitted `hideGauntletWidgets` key, or invalid value does not override the lower-precedence value. Invalid JSON, a non-object root, or a non-boolean `hideGauntletWidgets` value produces a `console.error` warning and is ignored at that layer. Unknown keys are ignored.
54
+
55
+ The only supported key is:
56
+
57
+ ```json
58
+ { "hideGauntletWidgets": false }
59
+ ```
60
+
61
+ Its default is `true`. A non-boolean value is invalid for that key, produces a warning, and leaves the lower-precedence value unchanged.
62
+
63
+ ## Source and compatibility decisions
64
+
65
+ Session-path sources live in `src/` and emit to `dist/`. The implementation copies the current modules needed by the live session path and excludes unused modules. TypeScript uses `NodeNext` module settings, and tests are excluded from the extension entry. Relative `.js` import specifiers are preserved. Runtime and tests do not require Node's TypeScript strip-types flag.
66
+
67
+ The activity test asserts the exported `activityBullet` and `rowIsRunning` behavior. `paintActivityLines` is not added.
68
+
69
+ ## Verification
70
+
71
+ Verification uses temporary directories only. It sets `PI_CODING_AGENT_DIR` and must not modify the user's live Pi configuration, authentication, sessions, or `grok-chrome` directory. Any Pi process also uses `--no-extensions -e ./dist/index.js` so the live extension cannot load.
72
+
73
+ `npm test` builds the tests and runs `node --test` against emitted `.js` specifiers. It covers default widget hiding, the `false` opt-out, trusted project override, untrusted project skip, omitted keys, invalid JSON, active and empty tracker fixtures, named chrome calls, `HERDR_ENV` gating, and a spy proving `setHideThinkingBlock` is never called.
74
+
75
+ The required release checks are:
76
+
77
+ - `npm test` and `npm run build` pass on Node.js 22.19.0 using emitted JavaScript.
78
+ - `dist/index.js` is committed and matches the build output.
79
+ - A clean checkout loads through `pi install` without a consumer build.
80
+ - The installed extension loads under Pi 0.84.4 or newer.
81
+
82
+ ## Documentation impact
83
+
84
+ - Feature / user-facing docs introduced: README.md — operations and package configuration
85
+ - Materially amended existing docs: none
86
+ - Derived / memory docs invalidated: none
87
+
88
+ ## Out of scope
89
+
90
+ - npm registry publication
91
+ - Git tag or GitHub release creation
92
+ - automatic removal or disabling of the live extension
93
+ - migration of unrelated Pi settings and extensions
94
+ - changes to Pi core or Gauntlet
@@ -0,0 +1,8 @@
1
+ import js from "@eslint/js"
2
+ import tseslint from "typescript-eslint"
3
+
4
+ export default tseslint.config(
5
+ { ignores: ["dist/**", "test-dist/**", "node_modules/**"] },
6
+ js.configs.recommended,
7
+ ...tseslint.configs.recommended,
8
+ )
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "pi-minimal",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "private": false,
7
+ "engines": {
8
+ "node": ">=22.19.0"
9
+ },
10
+ "keywords": [
11
+ "pi-package"
12
+ ],
13
+ "pi": {
14
+ "extensions": [
15
+ "./dist/index.js"
16
+ ]
17
+ },
18
+ "peerDependencies": {
19
+ "@earendil-works/pi-coding-agent": "*",
20
+ "@earendil-works/pi-tui": "*"
21
+ },
22
+ "devDependencies": {
23
+ "@earendil-works/pi-coding-agent": "*",
24
+ "@earendil-works/pi-tui": "*",
25
+ "@eslint/js": "^10.0.1",
26
+ "@types/node": "*",
27
+ "eslint": "^10.11.0",
28
+ "typescript": "*",
29
+ "typescript-eslint": "^8.70.1"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json",
33
+ "test": "tsc -p tsconfig.tests.json && node --test test-dist/test/*.test.js",
34
+ "lint": "eslint .",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit"
36
+ }
37
+ }
@@ -0,0 +1,16 @@
1
+ export type ActivityContext = {
2
+ isPartial?: boolean
3
+ isError?: boolean
4
+ state?: { done?: boolean; running?: boolean }
5
+ }
6
+
7
+ export function rowIsRunning(context: ActivityContext): boolean {
8
+ if (context.isError) return false
9
+ if (context.state?.done) return false
10
+ if (context.isPartial === false) return false
11
+ return true
12
+ }
13
+
14
+ export function activityBullet(running: boolean, frame: 0 | 1): "◆" | "◇" {
15
+ return running && frame === 1 ? "◇" : "◆"
16
+ }
package/src/chrome.ts ADDED
@@ -0,0 +1 @@
1
+ export const CHROME_MARGIN = 1
package/src/config.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent"
4
+
5
+ export type PiMinimalConfig = {
6
+ hideGauntletWidgets: boolean
7
+ }
8
+
9
+ const DEFAULTS: PiMinimalConfig = {
10
+ hideGauntletWidgets: true,
11
+ }
12
+
13
+ export function loadPiMinimalConfig(options: {
14
+ agentDir: string
15
+ cwd: string
16
+ projectTrusted: boolean
17
+ }): PiMinimalConfig {
18
+ const config: PiMinimalConfig = { ...DEFAULTS }
19
+ applyLayer(config, join(options.agentDir, "pi-minimal.json"))
20
+ if (options.projectTrusted) {
21
+ applyLayer(config, join(options.cwd, CONFIG_DIR_NAME, "pi-minimal.json"))
22
+ }
23
+ return config
24
+ }
25
+
26
+ function applyLayer(config: PiMinimalConfig, path: string): void {
27
+ let text: string
28
+ try {
29
+ text = readFileSync(path, "utf8")
30
+ } catch {
31
+ return
32
+ }
33
+
34
+ let parsed: unknown
35
+ try {
36
+ parsed = JSON.parse(text)
37
+ } catch {
38
+ console.error(`pi-minimal: invalid JSON in ${path}`)
39
+ return
40
+ }
41
+
42
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
43
+ console.error(`pi-minimal: expected an object in ${path}`)
44
+ return
45
+ }
46
+
47
+ const record = parsed as Record<string, unknown>
48
+ if (!Object.hasOwn(record, "hideGauntletWidgets")) {
49
+ return
50
+ }
51
+
52
+ const value = record.hideGauntletWidgets
53
+ if (typeof value !== "boolean") {
54
+ console.error(`pi-minimal: hideGauntletWidgets must be a boolean in ${path}`)
55
+ return
56
+ }
57
+
58
+ config.hideGauntletWidgets = value
59
+ }
package/src/cwd.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent"
3
+ import path from "node:path"
4
+ import { CHROME_MARGIN } from "./chrome.js"
5
+
6
+ function formatCwd(cwd: string): string {
7
+ const home = process.env.HOME || process.env.USERPROFILE || ""
8
+ if (!home) return cwd
9
+ const resolved = path.resolve(cwd)
10
+ const rel = path.relative(path.resolve(home), resolved)
11
+ if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
12
+ return rel ? `~/${rel}` : "~"
13
+ }
14
+ return cwd
15
+ }
16
+
17
+ function formatTokens(n: number): string {
18
+ if (n < 1000) return String(n)
19
+ if (n < 10000) return `${(n / 1000).toFixed(1)}K`
20
+ if (n < 1e6) return `${Math.round(n / 1000)}K`
21
+ if (n < 1e7) return `${(n / 1e6).toFixed(1)}M`
22
+ return `${Math.round(n / 1e6)}M`
23
+ }
24
+
25
+ function modelIcon(id: string): string {
26
+ const s = id.toLowerCase()
27
+ if (s.includes("grok")) return "✦"
28
+ if (
29
+ s.includes("claude") ||
30
+ s.includes("fable") ||
31
+ s.includes("sonnet") ||
32
+ s.includes("opus") ||
33
+ s.includes("haiku")
34
+ ) {
35
+ return "✳"
36
+ }
37
+ if (s.includes("gpt") || s.includes("codex")) return "◈"
38
+ return "◆"
39
+ }
40
+
41
+ function shortModel(id: string): string {
42
+ return (id.split("/").pop() || id).replace(/^claude-/, "")
43
+ }
44
+
45
+ function thinkingColor(level: string): "dim" | "accent" | "warning" {
46
+ if (level === "high" || level === "xhigh" || level === "max") return "warning"
47
+ if (level === "medium") return "accent"
48
+ return "dim"
49
+ }
50
+
51
+ function meter(percent: number, fill: string, empty: string): string {
52
+ const n = 8
53
+ const filled = Math.max(0, Math.min(n, Math.round((percent / 100) * n)))
54
+ return fill.repeat(filled) + empty.repeat(n - filled)
55
+ }
56
+
57
+ function splitRow(left: string, right: string, width: number): string {
58
+ let L = left
59
+ let R = right
60
+ while (visibleWidth(L) + 1 + visibleWidth(R) > width && visibleWidth(R) > 0) {
61
+ R = truncateToWidth(R, Math.max(0, visibleWidth(R) - 1), "")
62
+ }
63
+ while (visibleWidth(L) + 1 + visibleWidth(R) > width && visibleWidth(L) > 0) {
64
+ L = truncateToWidth(L, Math.max(0, visibleWidth(L) - 1), "")
65
+ }
66
+ const gap = Math.max(1, width - visibleWidth(L) - visibleWidth(R))
67
+ return L + " ".repeat(gap) + R
68
+ }
69
+
70
+ export function installCwd(ctx: ExtensionContext): void {
71
+ ctx.ui.setWidget(
72
+ "tw-cwd",
73
+ (_tui, theme) => ({
74
+ invalidate() {},
75
+ render(width: number): string[] {
76
+ const inner = Math.max(8, width - CHROME_MARGIN * 2)
77
+ const id = ctx.model?.id || "no-model"
78
+ const icon = theme.fg("accent", modelIcon(id))
79
+ const name = theme.fg("text", shortModel(id))
80
+ const usage = ctx.getContextUsage?.()
81
+ const window = usage?.contextWindow || ctx.model?.contextWindow
82
+ const used = usage?.tokens
83
+ let tokenStr = theme.fg("dim", "?/?")
84
+ let bar = theme.fg("dim", "░░░░░░░░")
85
+ let pctStr = theme.fg("dim", "?%")
86
+ if (typeof used === "number" && window) {
87
+ const pct = Math.max(0, Math.min(100, Math.round((used / window) * 100)))
88
+ tokenStr = theme.fg("dim", `${formatTokens(used)}/${formatTokens(window)}`)
89
+ const hot = pct >= 85
90
+ bar = meter(pct, theme.fg(hot ? "warning" : "accent", "█"), theme.fg("dim", "░"))
91
+ pctStr = theme.fg(hot ? "warning" : "muted", `${pct}%`)
92
+ } else if (used === null && window) {
93
+ tokenStr = theme.fg("dim", `?/${formatTokens(window)}`)
94
+ }
95
+ const thinking = ctx.thinkingLevel || ""
96
+ const think = thinking ? theme.fg(thinkingColor(thinking), thinking) : ""
97
+ const midot = theme.fg("dim", " · ")
98
+ const left = [`${icon} ${name}`, tokenStr, `${bar} ${pctStr}`, think]
99
+ .filter(Boolean)
100
+ .join(midot)
101
+ const right = ctx.cwd ? theme.fg("accent", formatCwd(ctx.cwd)) : ""
102
+ const row = splitRow(left, right, inner)
103
+ return [`${" ".repeat(CHROME_MARGIN)}${row}`]
104
+ },
105
+ }),
106
+ { placement: "aboveEditor" },
107
+ )
108
+ }