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.
- package/.github/workflows/ci.yml +25 -0
- package/.github/workflows/publish.yml +29 -0
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/activity.js +12 -0
- package/dist/chrome.js +1 -0
- package/dist/config.js +45 -0
- package/dist/cwd.js +103 -0
- package/dist/footer.js +46 -0
- package/dist/header.js +14 -0
- package/dist/herdr.js +44 -0
- package/dist/index.js +155 -0
- package/dist/live-watch.js +52 -0
- package/dist/reclaim.js +19 -0
- package/dist/tool-rows.js +34 -0
- package/dist/tools.js +103 -0
- package/dist/tracker.js +63 -0
- package/dist/working-status.js +191 -0
- package/doc/plans/2026-09-22-pi-minimal-package.md +354 -0
- package/doc/specs/2026-09-22-pi-minimal-package.md +94 -0
- package/eslint.config.js +8 -0
- package/package.json +37 -0
- package/src/activity.ts +16 -0
- package/src/chrome.ts +1 -0
- package/src/config.ts +59 -0
- package/src/cwd.ts +108 -0
- package/src/footer.ts +54 -0
- package/src/header.ts +16 -0
- package/src/herdr.ts +66 -0
- package/src/index.ts +152 -0
- package/src/live-watch.ts +59 -0
- package/src/reclaim.ts +34 -0
- package/src/tool-rows.ts +58 -0
- package/src/tools.ts +161 -0
- package/src/tracker.ts +91 -0
- package/src/working-status.ts +214 -0
- package/test/activity.test.ts +27 -0
- package/test/config.test.ts +114 -0
- package/test/index.test.ts +262 -0
- package/test/package.test.ts +18 -0
- package/test/tools.test.ts +48 -0
- package/test/tracker.test.ts +104 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tests.json +8 -0
|
@@ -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
|
package/eslint.config.js
ADDED
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
|
+
}
|
package/src/activity.ts
ADDED
|
@@ -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
|
+
}
|