pi-editor-footer 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.
Files changed (46) hide show
  1. package/AGENTS.md +19 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CONTEXT.md +29 -0
  4. package/README.md +84 -0
  5. package/docs/adr/0001-tracking-editor-for-skill-descriptions.md +18 -0
  6. package/docs/adr/0002-own-editor-slot-port-model-info-glow.md +18 -0
  7. package/docs/agents/domain.md +51 -0
  8. package/docs/agents/issue-tracker.md +45 -0
  9. package/docs/agents/triage-labels.md +15 -0
  10. package/docs/reference/pi-tui-internals.md +144 -0
  11. package/docs/specs/01-config.md +57 -0
  12. package/docs/specs/02-identity.md +20 -0
  13. package/docs/specs/03-border-telemetry.md +48 -0
  14. package/docs/specs/04-header.md +30 -0
  15. package/docs/specs/05-footer.md +30 -0
  16. package/docs/specs/06-git.md +36 -0
  17. package/docs/specs/07-runtime.md +28 -0
  18. package/docs/specs/theme-overview.md +116 -0
  19. package/package.json +16 -0
  20. package/src/config.ts +184 -0
  21. package/src/detail-render.ts +119 -0
  22. package/src/footer.ts +479 -0
  23. package/src/git.ts +170 -0
  24. package/src/header.ts +185 -0
  25. package/src/icons.ts +197 -0
  26. package/src/index.ts +607 -0
  27. package/src/model-info.ts +341 -0
  28. package/src/runtime.ts +318 -0
  29. package/src/state.ts +144 -0
  30. package/src/telemetry.ts +437 -0
  31. package/src/theme-settings.ts +461 -0
  32. package/src/tracking-editor.ts +352 -0
  33. package/src/utils-workspace.ts +48 -0
  34. package/src/utils.ts +388 -0
  35. package/src/window-presentation.ts +56 -0
  36. package/test/config.test.ts +146 -0
  37. package/test/detail-render.test.ts +202 -0
  38. package/test/footer.test.ts +86 -0
  39. package/test/git.test.ts +45 -0
  40. package/test/header.test.ts +169 -0
  41. package/test/icons.test.ts +24 -0
  42. package/test/runtime.test.ts +71 -0
  43. package/test/telemetry.test.ts +199 -0
  44. package/test/utils.test.ts +71 -0
  45. package/test/window-presentation.test.ts +73 -0
  46. package/tsconfig.json +13 -0
package/AGENTS.md ADDED
@@ -0,0 +1,19 @@
1
+ ## Agent skills
2
+
3
+ ### Issue tracker
4
+
5
+ Issues and specs live as GitHub issues, managed via the `gh` CLI. See `docs/agents/issue-tracker.md`.
6
+
7
+ ### Triage labels
8
+
9
+ The five canonical triage roles use their default label names (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
10
+
11
+ ### Domain docs
12
+
13
+ Single-context — one `CONTEXT.md` and `docs/adr/` at the repo root. See `docs/agents/domain.md`.
14
+
15
+ ## Pi editor replacement — sync contract
16
+
17
+ This extension **replaces pi's default input editor**: `TrackingEditor` (`src/tracking-editor.ts`) is the actual editor in the input box. It replicates pi's `CustomEditor` inline and observes the completion popup through two private pi-tui internals (`autocompleteList`, `applyAutocompleteSuggestions`).
18
+
19
+ **Pi editor features are NOT inherited — on every pi update, or whenever pi changes or extends editor behaviour (keybindings, IME, autocomplete, border rendering), diff pi's `CustomEditor` and `Editor` against `src/tracking-editor.ts` and port the changes over.** The exact sources to diff, the internals to watch, and the pty verification loop are in `docs/reference/pi-tui-internals.md` ("Sync contract" section). Why the editor is replaced at all: ADR-0001 and ADR-0002.
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-08-20
8
+
9
+ ### Added
10
+
11
+ - Full TUI theme `pi-editor-footer` rebuilt on `TrackingEditor`: project-aware footer (`cwd` · `git` • `runtime` left, `tokens` next to `context` right), model-info border glow top, live theme respect, detail window preserved
12
+ - Cursor styles `block`/`bar`/`underline` with real-time preview and hardware cursor handling
13
+ - Settings dialog `/pi-footer` (General/Appearance/Footer/Telemetry, `workspaceDisplay` path/name toggle, `enabled` restores default footer)
14
+ - Git state engine (branch/ahead/behind + staged/modified/untracked/conflicted/stashed) and 10+ runtime detection via lockfiles
15
+ - Telemetry bottom border (right-aligned, toggleable `tps`/`ttft`/`duration`/`tokens`/`stalls`/`cost`)
16
+ - Bottom border clean (no `cwd`), footer single line with `•`/`·` separators
17
+
18
+ ### Changed
19
+
20
+ - Renamed package `pi-skill-desc` → `pi-editor-footer` and `/pi-lsz-theme` → `/pi-footer` (removed `/theme` alias)
21
+ - Footer now single line `cwd·git•runtime` left / `tokens·context` right; bottom border no longer shows `cwd`
22
+
23
+ ### Fixed
24
+
25
+ - Cursor white background when switching styles and invisible cursor after switch — hardware cursor handling with `CURSOR_MARKER` preview
26
+ - Footer `*` git icon removed, footer empty at startup until `readGitStatus` now populated, and disabled extension correctly restores default footer
package/CONTEXT.md ADDED
@@ -0,0 +1,29 @@
1
+ # pi-skill-desc
2
+
3
+ A pi coding-agent extension that augments the built-in slash-command completion with a full description preview for the highlighted candidate.
4
+
5
+ ## Language
6
+
7
+ **Completion popup**:
8
+ Pi's built-in slash-command tab-completion list (`/` + tab), which lists commands, tools, and skills with truncated descriptions.
9
+ _Avoid_: autocomplete, picker
10
+
11
+ **Candidate**:
12
+ An item in the completion popup — a skill, a tool, or a slash command.
13
+ _Avoid_: suggestion, entry, item
14
+
15
+ **Detail window**:
16
+ The extension's popup above the input box that mirrors the completion popup's highlight. Capped at 5 lines, scrollable with shift+up/down, showing the full description of the highlighted candidate.
17
+ _Avoid_: description window, tooltip, preview
18
+
19
+ **Highlight**:
20
+ The row currently selected in the completion popup. The detail window mirrors it — cycling the highlight swaps the window's content.
21
+ _Avoid_: selection, cursor
22
+
23
+ **Editor slot**:
24
+ The single custom-editor position in pi — the last `setEditorComponent` writer wins. pi-skill-desc must own it to observe the popup (ADR-0001, ADR-0002).
25
+ _Avoid_: editor position, editor hook
26
+
27
+ **TrackingEditor**:
28
+ This extension's custom editor — the actual input editor in the box. Replaces pi's `CustomEditor` (replicated inline) and adds popup-highlight observation plus the model-info border glow.
29
+ _Avoid_: custom editor, wrapper
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ <h1 align="center">pi-editor-footer</h1>
2
+
3
+ <p align="center">A pi TUI theme — project-aware footer, model border, and skill detail window.</p>
4
+
5
+ A [pi](https://pi.dev) extension that turns the editor chrome into a project-aware theme while preserving the skill-description detail window. It owns pi's single custom-editor slot via `TrackingEditor` and renders entirely against pi's live theme (no hardcoded colors).
6
+
7
+ ## Features
8
+
9
+ **Detail window** — bordered window above the input that mirrors the completion popup's highlight and shows the candidate's full description (wrapped at `width − 4`, cap 5 lines, `…` ellipsis, `offset/total` header, shrinks when empty). Lifecycle mirrors the popup (opens/closes with it); hidden for candidates without a description. Scroll with **shift+up/down** (fallback **alt+j/k**).
10
+
11
+ **Model border** — top border shows `provider/model · thinking · contextWindow` with thinking-level glow via `getThinkingBorderColor`. Toggle with `/model-info` (off restores pi's stock border).
12
+
13
+ **Footer** — single line below the input, responsive via `fitSegmentsByPriority` and `alignRight`:
14
+ - Left: `cwd` (`·` `git branch` + status `[! ? + ↑↓]` + stashed/conflicted) `•` `runtime` (`node`/`python`/`rust`/`go`… + version) `•` `timer` (`working`/`done`)
15
+ - Right: `tokens` (` input |  output |  $cost`) immediately next to `context` (` [bar] % · tokens/contextWindow`)
16
+ - Separators: `·` between `cwd` and `git`, `•` as default between other left components; `tokens` sits directly left of the context bar
17
+ - `cwd` respects `workspaceDisplay` (`~/development/ai/pi-skill-desc` vs `pi-skill-desc`, switchable in settings)
18
+ - `context` bar uses `stressColor` and `renderBar` (12 cols max) with `•`/`·` handling
19
+ - Extension statuses line (`wrapTextWithAnsi`) when `footerSegments.extensionStatuses`
20
+
21
+ All segments are toggleable via `footerSegments` (`cwd`, `sessionName`, `gitBranch`, `gitStatus`, `gitCommit`, `runtime`, `context`, `tokens`, `cost`, `extensionStatuses`). Footer is installed via `ctx.ui.setFooter` when available, fallback to `setWidget("theme-footer", …, {placement:"belowEditor"})`, and is fully removed when the extension is disabled (`enabled: false` restores pi's default footer).
22
+
23
+ **Cursor** — `block` (software reverse, `setShowHardwareCursor(false)`), `bar` (`\x1b[6 q`), `underline` (`\x1b[4 q`) with hardware cursor (`setShowHardwareCursor(true)`). Style is previewed in real time: when the settings overlay is open (`!focused` + `previewHardwareCursor`), the software cursor ` \x1b[7m…\x1b[0m` is replaced with `CURSOR_MARKER` so the layout shows the hardware shape instantly; when focused the software cursor is removed and the hardware sequence is written.
24
+
25
+ **Input border** — bottom border is kept clean (no `cwd`); right side shows telemetry when `telemetry.enabled` (`TPS`, `TTFT`, `duration`, `tokens`, `stalls`, `cost` via `TurnTelemetryTracker`).
26
+
27
+ **Config** — single JSON `~/.pi/agent/pi-skill-desc.json` (validated at load, `DEFAULT_CONFIG` with `enabled:true`, `workspaceDisplay:"path"`, `cursorStyle:"block"`, `icons:"auto"`, all `footerSegments`/`telemetry` on). Settings dialog `/pi-footer` (General/Appearance/Footer/Telemetry, English, `Tab`/`↑↓`/`Enter`/`Esc`) live-edits and persists via `saveConfig`, with `cursorStyle` and `enabled` applying instantly and `workspaceDisplay` switching `cwd` format.
28
+
29
+ ## Quick start
30
+
31
+ ```bash
32
+ pi -e ./src/index.ts
33
+ ```
34
+
35
+ Type `/` → popup opens, detail window appears above input, header shows model top, footer shows `cwd · git • runtime` left and `tokens context` right. `/pi-footer` opens settings; `/model-info` toggles border glow.
36
+
37
+ ## Why you need this
38
+
39
+ The native popup truncates long skill descriptions mid-sentence; the detail window shows the full text without guessing. The native footer shows only context; this footer adds the project context you actually need — which branch, how many files changed, which runtime, how many tokens — while keeping the context bar for the limit.
40
+
41
+ ## Breakage mode
42
+
43
+ Highlight tracking observes two private `pi-tui` internals (`autocompleteList`, `applyAutocompleteSuggestions` per ADR-0001). At load it asserts both and warns `[pi-skill-desc] pi-tui internals changed…` if pi renames them; the window then never appears rather than showing wrong content. Single editor slot ownership is per ADR-0002.
44
+
45
+ ## Manual verification
46
+
47
+ 1. `pi -e ./src/index.ts` (or `pi -ne -e ./src/index.ts` to isolate)
48
+ 2. `/` → tab through candidates, detail window follows; `shift+down` scrolls long text
49
+ 3. Footer: `cwd` (`·` `git`) `•` `runtime` left, `tokens` right next to `[bar] %` right; `/pi-footer` → General → `Workspace display` toggles `~/…/pi-skill-desc` vs `pi-skill-desc`
50
+ 4. Cursor: `/pi-footer` → General → `Cursor style` cycles `block`/`bar`/`underline`, shape updates in real time (bar shows `|`, underline `_`)
51
+ 5. Disable: `/pi-footer` → General → `Enabled: Off` → footer reverts to pi's default (relative time / model); re-enable restores theme
52
+
53
+ ## Project structure
54
+
55
+ ```
56
+ pi-editor-footer/
57
+ ├── src/
58
+ │ ├── index.ts # extension entry, widget/header/footer install, /model-info
59
+ │ ├── tracking-editor.ts # TrackingEditor (Editor slot, highlight, border, cursor)
60
+ │ ├── footer.ts # renderFooter / installFooter (cwd·git • runtime • tokens·context)
61
+ │ ├── header.ts # header disabled, cwd preserved in footer
62
+ │ ├── detail-render.ts # wrap/scroll/ellipsis (tested)
63
+ │ ├── window-presentation.ts # bordered themed box (tested)
64
+ │ ├── model-info.ts # top border glow+label
65
+ │ ├── git.ts # readGitStatus (branch/ahead/behind + staged/modified/...)
66
+ │ ├── runtime.ts # readRuntimeInfo (node/python/rust/go/… via lockfiles)
67
+ │ ├── telemetry.ts # TurnTelemetryTracker + formatTurnTelemetry
68
+ │ ├── config.ts # ThemeConfig + load/save
69
+ │ ├── theme-settings.ts # /pi-footer dialog
70
+ │ └── state.ts # FooterState
71
+ ├── test/
72
+ └── docs/
73
+ ├── adr/0001-tracking-editor-for-skill-descriptions.md
74
+ └── adr/0002-own-editor-slot-port-model-info-glow.md
75
+ ```
76
+
77
+ ## Development
78
+
79
+ - `npm test` — `node:test` + `tsx` (87 cases)
80
+ - `npm run typecheck` — strict TS
81
+
82
+ ## License
83
+
84
+ No license declared yet; all rights reserved. (Private repository.)
@@ -0,0 +1,18 @@
1
+ # Track the native completion popup's highlight via a custom editor
2
+
3
+ Pi's extension API exposes no event for which row is selected in the built-in slash-command completion popup, yet the goal is a detail window that mirrors that highlight as the user cycles. We install a custom editor — `TrackingEditor`, a subclass of pi-tui's exported `Editor` replicating the app's `CustomEditor` — that keeps the native popup untouched and observes its selection through `SelectList.onSelectionChange` (a public callback the editor leaves unwired), plus one instance-level patch on the private `applyAutocompleteSuggestions` to re-hook the list when suggestions refresh. The detail window renders via `setWidget(..., { placement: "aboveEditor" })`; `shift+up` / `shift+down` (unbound keys) scroll long descriptions.
4
+
5
+ Status: accepted
6
+
7
+ ## Considered Options
8
+
9
+ - **Own picker overlay** (`ctx.ui.custom` with a candidate list + detail pane): rejected — the user explicitly does not want a finder/picker; the native `/`+tab flow must be augmented, not replaced.
10
+ - **Data-only autocomplete provider wrapper**: rejected — `addAutocompleteProvider` supplies items but no selection-change signal, so a detail window could not follow the highlight.
11
+ - **Reading the selection via runtime casts into private fields**: chosen — the only path that preserves the native flow; the blast radius is two internals (`autocompleteList` field, `applyAutocompleteSuggestions` method), and failures are loud (the patch stops firing), never silently wrong.
12
+
13
+ ## Consequences
14
+
15
+ - The extension reaches into two private pi-tui internals; a rename upstream breaks it loudly and needs a one-line fix.
16
+ - Terminal support for modified-arrow sequences (Kitty protocol) is required for shift+up/down to be distinct from plain arrows; legacy terminals will alias them.
17
+ - Tools are not covered — the `/` completion popup lists slash commands, templates, extension commands, and skills only.
18
+ - Description data needs no catalog lookups: the popup's `SelectItem` carries the full (non-truncated) description.
@@ -0,0 +1,18 @@
1
+ # Own the single editor slot; port model-info-widget's border glow
2
+
3
+ Pi allows exactly one custom input editor — the last `setEditorComponent` writer wins. The user's `model-info-widget` extension also installs one on `session_start`, and its handler ran after ours, silently replacing the TrackingEditor and killing the detail window ("no popup" — the extension's render was never even called). We defer our install (`setTimeout 0`, after every other extension's synchronous `session_start` handler) plus a 1s watchdog that re-asserts ownership whenever the focused input editor is not ours, and we ported model-info-widget's border glow/label rendering into `src/model-info.ts` so its visual behavior survives even though its own editor install is now inert.
4
+
5
+ Status: accepted
6
+
7
+ ## Considered Options
8
+
9
+ - **Let model-info-widget keep the slot**: impossible — highlight tracking must live on the owning editor; there is no API to observe the popup from outside.
10
+ - **Defer our install only**: fragile against other deferring extensions; the watchdog makes slot ownership self-healing.
11
+ - **Watchdog only**: works, but the first second of every session would run with the wrong editor.
12
+ - **Port the glow (chosen)**: keeps both features without touching model-info-widget's files; the port is self-contained (color math + label rendering, verbatim from the original).
13
+
14
+ ## Consequences
15
+
16
+ - model-info-widget's editor install is now inert; its rendering lives in `src/model-info.ts`, and its `/model-info` toggle is reimplemented by pi-skill-desc's own `/model-info` command — so the widget can be deleted from `~/.pi/agent/extensions/`.
17
+ - Intentional code duplication with model-info-widget; a future cleanup could merge the extensions.
18
+ - The watchdog only fights input editors (CustomEditor duck-type: `actionHandlers` is a `Map`) — selectors, dialogs, and overlays are never disturbed.
@@ -0,0 +1,51 @@
1
+ # Domain Docs
2
+
3
+ How the engineering skills should consume this repo's domain documentation when exploring the codebase.
4
+
5
+ ## Before exploring, read these
6
+
7
+ - **`CONTEXT.md`** at the repo root, or
8
+ - **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
9
+ - **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
10
+
11
+ If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
12
+
13
+ ## File structure
14
+
15
+ Single-context repo (most repos):
16
+
17
+ ```
18
+ /
19
+ ├── CONTEXT.md
20
+ ├── docs/adr/
21
+ │ ├── 0001-event-sourced-orders.md
22
+ │ └── 0002-postgres-for-write-model.md
23
+ └── src/
24
+ ```
25
+
26
+ Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
27
+
28
+ ```
29
+ /
30
+ ├── CONTEXT-MAP.md
31
+ ├── docs/adr/ ← system-wide decisions
32
+ └── src/
33
+ ├── ordering/
34
+ │ ├── CONTEXT.md
35
+ │ └── docs/adr/ ← context-specific decisions
36
+ └── billing/
37
+ ├── CONTEXT.md
38
+ └── docs/adr/
39
+ ```
40
+
41
+ ## Use the glossary's vocabulary
42
+
43
+ When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
44
+
45
+ If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
46
+
47
+ ## Flag ADR conflicts
48
+
49
+ If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
50
+
51
+ > _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
@@ -0,0 +1,45 @@
1
+ # Issue tracker: GitHub
2
+
3
+ Issues and specs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
4
+
5
+ ## Conventions
6
+
7
+ - **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
8
+ - **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
9
+ - **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
10
+ - **Comment on an issue**: `gh issue comment <number> --body "..."`
11
+ - **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
12
+ - **Close**: `gh issue close <number> --comment "..."`
13
+
14
+ Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone.
15
+
16
+ ## Pull requests as a triage surface
17
+
18
+ **PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
19
+
20
+ When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
21
+
22
+ - **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
23
+ - **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`).
24
+ - **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
25
+
26
+ GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`.
27
+
28
+ ## When a skill says "publish to the issue tracker"
29
+
30
+ Create a GitHub issue.
31
+
32
+ ## When a skill says "fetch the relevant ticket"
33
+
34
+ Run `gh issue view <number> --comments`.
35
+
36
+ ## Wayfinding operations
37
+
38
+ Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
39
+
40
+ - **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
41
+ - **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
42
+ - **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
43
+ - **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
44
+ - **Claim**: `gh issue edit <n> --add-assignee @me` — the session's first write.
45
+ - **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
@@ -0,0 +1,15 @@
1
+ # Triage Labels
2
+
3
+ The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
4
+
5
+ | Label in mattpocock/skills | Label in our tracker | Meaning |
6
+ | -------------------------- | -------------------- | ---------------------------------------- |
7
+ | `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
8
+ | `needs-info` | `needs-info` | Waiting on reporter for more information |
9
+ | `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
10
+ | `ready-for-human` | `ready-for-human` | Requires human implementation |
11
+ | `wontfix` | `wontfix` | Will not be actioned |
12
+
13
+ When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
14
+
15
+ Edit the right-hand column to match whatever vocabulary you actually use.
@@ -0,0 +1,144 @@
1
+ # pi-tui internals reference (verified against installed packages)
2
+
3
+ Implementation reference for the Detail-window extension. All facts below were verified by reading the **installed** packages on 2026-08-16:
4
+
5
+ - `@earendil-works/pi-coding-agent` — dist types + interactive-mode source
6
+ - `@earendil-works/pi-tui` **v0.84.2** — dist source
7
+
8
+ Pin dev dependency `@earendil-works/pi-tui@0.84.2` for types. The extension imports at runtime only from `@earendil-works/pi-tui` (pi provides it).
9
+
10
+ ## Extension model
11
+
12
+ - Extensions are TypeScript modules with a **default-exported factory**: `export default function (pi: ExtensionAPI): void | Promise<void>`.
13
+ - Auto-discovered from `~/.pi/agent/extensions/` or `.pi/extensions/` (project), or `pi -e ./path.ts`.
14
+ - `ExtensionAPI` members used here: `registerShortcut(KeyId, {description?, handler(ctx)})`, `on(event, handler)`, `getAllTools()`, `getCommands()`.
15
+ - Event handlers receive `ctx: ExtensionContext` with `ctx.ui: ExtensionUIContext` and `ctx.mode` (`"tui"` guards terminal-only UI).
16
+
17
+ ## ExtensionUIContext members (verified in dist/core/extensions/types.d.ts)
18
+
19
+ - `setWidget(key, content, options?)` where `content` is `string[]` or a component factory `(tui, theme) => Component & {dispose?()}`; `options.placement: "aboveEditor" | "belowEditor"` (default aboveEditor).
20
+ - `setStatus(key, text | undefined)` — status/footer text.
21
+ - `custom<T>(factory, {overlay?, overlayOptions?, onHandle?})` — focus-taking overlay (not used by this extension).
22
+ - `onTerminalInput(handler): unsubscribe` — raw keystrokes before the focused component; `{consume}` / `{data}`.
23
+ - `setEditorComponent(factory | undefined)` where `factory = (tui, theme, keybindings) => EditorComponent`.
24
+ - `addAutocompleteProvider(factory)` — stacks on the built-in provider (data only; no selection signal).
25
+
26
+ ## The editor contract (pi-tui EditorComponent)
27
+
28
+ `EditorComponent` interface: `getText()/setText()/handleInput(data)/onSubmit?/onChange?/addToHistory?/insertTextAtCursor?/getExpandedText?/setAutocompleteProvider?/borderColor?/setPaddingX?/setAutocompleteMaxVisible?`.
29
+
30
+ `Editor` class is **exported** from `@earendil-works/pi-tui`: `constructor(tui, theme, options?: EditorOptions)`; `EditorOptions = { paddingX?, autocompleteMaxVisible? }`. Public methods used: `getText()`, `setText()`, `isShowingAutocomplete()`, `onSubmit`, `onChange`, `setAutocompleteProvider`, `handleInput`.
31
+
32
+ ## How pi's CustomEditor works (replicate exactly)
33
+
34
+ pi's interactive mode uses `CustomEditor extends Editor` (`dist/modes/interactive/components/custom-editor.js`). Full source:
35
+
36
+ ```ts
37
+ import { Editor } from "@earendil-works/pi-tui";
38
+
39
+ export class CustomEditor extends Editor {
40
+ keybindings;
41
+ actionHandlers = new Map();
42
+ onEscape;
43
+ onCtrlD;
44
+ onPasteImage;
45
+ /** Handler for extension-registered shortcuts. Returns true if handled. */
46
+ onExtensionShortcut;
47
+ constructor(tui, theme, keybindings, options) {
48
+ super(tui, theme, options);
49
+ this.keybindings = keybindings;
50
+ }
51
+ onAction(action, handler) { this.actionHandlers.set(action, handler); }
52
+ handleInput(data) {
53
+ if (this.onExtensionShortcut?.(data)) return;
54
+ if (this.keybindings.matches(data, "app.clipboard.pasteImage")) { this.onPasteImage?.(); return; }
55
+ if (this.keybindings.matches(data, "app.interrupt")) {
56
+ if (!this.isShowingAutocomplete()) {
57
+ const handler = this.onEscape ?? this.actionHandlers.get("app.interrupt");
58
+ if (handler) { handler(); return; }
59
+ }
60
+ super.handleInput(data);
61
+ return;
62
+ }
63
+ if (this.keybindings.matches(data, "app.exit")) {
64
+ if (this.getText().length === 0) {
65
+ const handler = this.onCtrlD ?? this.actionHandlers.get("app.exit");
66
+ if (handler) handler();
67
+ return;
68
+ }
69
+ }
70
+ if (this.keybindings.matches(data, "tui.editor.historyPrevious") ||
71
+ this.keybindings.matches(data, "tui.editor.historyNext")) {
72
+ super.handleInput(data);
73
+ return;
74
+ }
75
+ for (const [action, handler] of this.actionHandlers) {
76
+ if (action !== "app.interrupt" && action !== "app.exit" && this.keybindings.matches(data, action)) {
77
+ handler();
78
+ return;
79
+ }
80
+ }
81
+ super.handleInput(data);
82
+ }
83
+ }
84
+ ```
85
+
86
+ ## setEditorComponent wiring (dist/modes/interactive/interactive-mode.js `setCustomEditorComponent`)
87
+
88
+ The factory is called `factory(this.ui, getEditorTheme(), this.keybindings)`. After creation, interactive mode **duck-types**: if the new editor has an `actionHandlers` property that is a `Map`, it copies all app-level handlers onto it — `onEscape`, `onCtrlD`, `onPasteImage`, `onExtensionShortcut` (falling back to the default editor's), and every entry of the default editor's `actionHandlers` map. It also wires `onSubmit`, `onChange`, copies text and appearance, and calls `setAutocompleteProvider` when supported.
89
+
90
+ → A custom editor with `actionHandlers = new Map()` + `onAction(action, handler)` + the fields above behaves identically to the default editor.
91
+
92
+ ## The autocomplete popup internals (pi-tui components/editor.js + components/select-list.js)
93
+
94
+ - The popup is a `SelectList` stored in the private field **`autocompleteList`**.
95
+ - The list is (re)created in the private method **`applyAutocompleteSuggestions(suggestions, state)`** — the only construction site:
96
+
97
+ ```ts
98
+ applyAutocompleteSuggestions(suggestions, state) {
99
+ this.autocompletePrefix = suggestions.prefix;
100
+ this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
101
+ const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
102
+ if (bestMatchIndex >= 0) this.autocompleteList.setSelectedIndex(bestMatchIndex);
103
+ this.autocompleteState = state;
104
+ }
105
+ ```
106
+
107
+ - `clearAutocompleteUi()` sets `autocompleteList = undefined`, `autocompleteState = null`.
108
+ - Editor `handleInput` delegates to `this.autocompleteList.handleInput(data)` while the popup is open (up/down/tab/enter).
109
+ - `SelectList` is exported from pi-tui. Public members: `onSelectionChange?: (item: SelectItem) => void` (**left unwired by the editor — ours to use**), `getSelectedItem(): SelectItem | null`, `setSelectedIndex(index)`, `handleInput(keyData)`, `render(width)`, `setFilter(filter)`.
110
+ - `SelectList.handleInput` fires `notifySelectionChange()` on up/down (wraps around), which calls `onSelectionChange(selectedItem)`. Programmatic `setSelectedIndex` (initial best-match) does **not** fire it — read `getSelectedItem()` after wiring.
111
+ - `SelectItem = { value, label, description? }` — the description is the **full, untruncated** text (truncation is display-only). Slash-command items carry `description`; file-completion items may not.
112
+
113
+ **Tracking recipe**: subclass `Editor`; in the constructor, instance-patch `applyAutocompleteSuggestions` (cast to `any` — TS-private) so that after each `super.applyAutocompleteSuggestions(...)` call you wire `list.onSelectionChange` on the fresh list and emit the current `getSelectedItem()`; in an overridden `handleInput`, call `super.handleInput(data)` then read `(this as any).autocompleteList` — wire if new, and emit `getSelectedItem()` (or `null` when the popup is closed, i.e. the field is undefined). Emit through a public `onHighlight?: (item: SelectItem | null) => void` property.
114
+
115
+ **Blast radius (ADR-0001)**: two internal names — `autocompleteList` (field) and `applyAutocompleteSuggestions` (method). Verify both exist at extension load and warn loudly if not.
116
+
117
+ ## Keybindings
118
+
119
+ - `pi.registerShortcut(shortcut: KeyId, {description?, handler(ctx)})`; `KeyId` strings like `"shift+up"`, `"shift+down"`.
120
+ - `shift+up` / `shift+down` are **unbound** in pi's defaults and not in the reserved list → free to register. (`ctrl+shift+up/down` = alt-screen prompt nav, `alt+up` = dequeue — avoid.)
121
+ - Conflict policy: reserved keys are silently skipped with a warning; non-reserved bound keys are overridden with a warning; unbound keys are clean.
122
+ - `matchesKey(data, "shift+up")` handles Kitty modified-arrow sequences (`\x1b[1;2A`). Terminals without modified-arrow reporting alias shift+arrows to plain arrows — scroll degrades to no-op.
123
+
124
+ ## Data
125
+
126
+ - The completion popup lists slash commands, templates, extension commands, and skills (`skill:<name>`, description = SKILL.md frontmatter, prefixed with source info). **Tools never appear** — they are agent-invoked, not slash-invocable.
127
+ - No catalog lookups needed: the highlighted `SelectItem` carries its own full description.
128
+
129
+ ## Sync contract — keeping the editor current with pi updates
130
+
131
+ This extension **replaces pi's default input editor** via `setEditorComponent`. `TrackingEditor` (`src/tracking-editor.ts`) is the actual editor in the input box: it replicates pi's `CustomEditor` inline and reads two private pi-tui internals — so **pi editor changes are NOT inherited automatically.** On every pi update, and whenever pi adds or changes editor behaviour, sync manually:
132
+
133
+ 1. **Diff the replicated base.** Compare pi's `CustomEditor` (`dist/modes/interactive/components/custom-editor.js` in `@earendil-works/pi-coding-agent` — source in the "How pi's CustomEditor works" section above) and pi-tui's `Editor` (`components/editor.js` in `@earendil-works/pi-tui`) against `src/tracking-editor.ts`: port any new/changed app-keybinding branches, fields, or methods in `handleInput`; keep the highlight-sync additions after each `super.handleInput(data)`.
134
+ 2. **Check the private internals.** Confirm `autocompleteList` and `applyAutocompleteSuggestions` still exist with the same names on pi-tui's `Editor`. The load-time `assertInternals()` in `src/index.ts` warns if they vanish — if it warns, fix the tracking in `src/tracking-editor.ts`, don't silence the warning.
135
+ 3. **Verify live** with the scripted pty loop (the only seam for the editor wiring): from the repo root,
136
+ `(sleep 18; printf '/'; sleep 1.5; printf '\033[B'; sleep 2; printf '\033'; sleep 2) | timeout 45 script -q /tmp/psd.log pi -e ./src/index.ts --no-session`,
137
+ then grep `/tmp/psd.log` for the bordered detail window (`┌…┐`, `· command`/`· skill` rows) following the highlight. Add `sh -c 'stty cols 40; …'` around the command for a narrow terminal that triggers the scroll/ellipsis paths.
138
+ 4. **Regression:** `npm test` (renderer/presentation seams) and `npm run typecheck` must stay green.
139
+
140
+ The replacement itself is deliberate — see ADR-0001 (why the popup internals are read) and ADR-0002 (why we own the editor slot).
141
+
142
+ ## Glossary / decisions
143
+
144
+ See `CONTEXT.md` (Completion popup, Candidate, Detail window, Highlight) and `docs/adr/0001-tracking-editor-for-skill-descriptions.md` (accepted) before implementing.
@@ -0,0 +1,57 @@
1
+ # Spec 01 — Config Schema & Persistence
2
+
3
+ Ticket: #7 · Type: grilling (HITL) · Branch: `feat/config`
4
+
5
+ ## Question
6
+
7
+ What is the single config contract every subsystem reads?
8
+
9
+ ## Decision
10
+
11
+ - File: `~/.pi/agent/pi-skill-desc.json` (keep existing name for backward compat; identity rename may add alias — decide in #6).
12
+ - Shape:
13
+
14
+ ```ts
15
+ export type WorkspaceDisplay = "path" | "name";
16
+ export type CursorStyle = "block" | "bar" | "underline";
17
+ export type IconMode = "auto" | "nerd" | "ascii";
18
+
19
+ export interface ThemeConfig {
20
+ enabled: boolean;
21
+ workspaceDisplay: WorkspaceDisplay;
22
+ cursorStyle: CursorStyle;
23
+ icons: { mode: IconMode };
24
+ telemetry: {
25
+ enabled: boolean;
26
+ tps: boolean;
27
+ ttft: boolean;
28
+ duration: boolean;
29
+ tokens: boolean;
30
+ stalls: boolean;
31
+ cost: boolean;
32
+ };
33
+ footerSegments: {
34
+ cwd: boolean;
35
+ sessionName: boolean;
36
+ gitBranch: boolean;
37
+ gitStatus: boolean;
38
+ gitCommit: boolean;
39
+ runtime: boolean;
40
+ context: boolean;
41
+ tokens: boolean;
42
+ cost: boolean;
43
+ extensionStatuses: boolean;
44
+ };
45
+ }
46
+ ```
47
+
48
+ - Defaults mirror `tmp/pi-open-tui` DEFAULT_CONFIG except `workspaceDisplay` defaults to `"path"`, `icons.mode` to `"auto"`.
49
+ - Helpers: `loadConfig()`, `saveConfig(patch)`, `DEFAULT_CONFIG`, validation (Zod or manual) at admission boundary; typed inside.
50
+ - Live reload: `saveConfig` writes file + notifies `requestRender` / re-apply cursor/style.
51
+
52
+ ## Acceptance
53
+
54
+ - [ ] `src/config.ts` exists, typed, validated at load, with `loadConfig`/`saveConfig`/`DEFAULT_CONFIG`
55
+ - [ ] Malformed file falls back to defaults with warning, never crashes
56
+ - [ ] `workspaceDisplay` controls header/footer cwd rendering (consumers read it)
57
+ - [ ] `npm run typecheck` passes, unit test for load/save + defaults
@@ -0,0 +1,20 @@
1
+ # Spec 02 — Theme Identity & Rename
2
+
3
+ Ticket: #6 · Type: grilling · Branch: `feat/identity`
4
+
5
+ ## Question
6
+
7
+ What does the renamed theme call itself and how does rename land?
8
+
9
+ ## Decision
10
+
11
+ - Display name: `pi-tui-theme` (proposed) or keep `pi-skill-desc` with theme description — owner picks via grilling.
12
+ - `package.json` `name` stays `pi-skill-desc` for now (avoid registry churn); `description` and README header change to theme framing.
13
+ - Command: keep `/model-info` for compat, add `/theme` (lightweight English dialog) that subsumes it. Old command delegates to new.
14
+ - Install path `~/.pi/agent/extensions/pi-skill-desc` unchanged; future rename is a symlink alias, not a break.
15
+
16
+ ## Acceptance
17
+
18
+ - [ ] README header reflects theme identity, install still works via existing path
19
+ - [ ] `/theme` command exists (even if dialog lands later — stub with notify)
20
+ - [ ] No break for users symlinked to old path
@@ -0,0 +1,48 @@
1
+ # Spec 03 — Editor Border: Model-Label Top + Telemetry Bottom Right (+ Telemetry Engine + Cursor)
2
+
3
+ Tickets: #8 (prototype), #12 (telemetry engine), #11 (cursor) · Branches: `feat/border-telemetry`, `feat/cursor` (may merge)
4
+
5
+ ## Questions
6
+
7
+ - How do model label (top) and telemetry (bottom right) share the editor border?
8
+ - What pi events produce the six measurements?
9
+ - How do cursor styles fold into TrackingEditor?
10
+
11
+ ## Decisions
12
+
13
+ ### Border Layout
14
+
15
+ - `TrackingEditor.render(width)` decorates `super.render(width)` lines:
16
+ - Top border hosts model-info label + thinking glow (`applyModelInfo` preserved, reads live `Theme` via `getLiveTheme()`).
17
+ - Bottom border hosts telemetry segment right-aligned, theme-respecting, truncated with `truncateToWidth`.
18
+ - Both survive narrow widths: left truncated first, right telemetry priority.
19
+ - Model top and telemetry bottom do not collide — prototype sketch required for sign-off.
20
+
21
+ ### Telemetry Engine
22
+
23
+ Rebuild `tmp/pi-open-tui/extensions/open-tui/telemetry.ts` bespoke:
24
+
25
+ ```ts
26
+ class TelemetryTracker {
27
+ handle(event: TelemetryEvent): TurnTelemetry | undefined
28
+ }
29
+ ```
30
+
31
+ - Events: `agent_start`, `turn_start`, `message_start`, `message_update`, `message_end`, `turn_end`, `agent_settled`, `tool_execution_start`.
32
+ - Metrics: `tps` (output tokens / generationMs), `ttftMs`, `totalMs`, `inputTokens/outputTokens`, `stallCount/stallMs` (stall = gap > 1000ms between updates), `costUsd` + `rateUsdPerMTokens` from `usage.cost.total`.
33
+ - Provider seam: tracker exposes `getLastTelemetry(): TurnTelemetry | null` for border + footer.
34
+ - Theme glyphs via `icons.mode` (`resolveGlyphs`).
35
+
36
+ ### Cursor Styles
37
+
38
+ - Fold `tmp/pi-open-tui/.../editor.ts` cursor logic into `TrackingEditor`:
39
+ - `cursorStyle: "block"` (default, software cursor), `"bar"` (`\x1b[6 q`, hardware cursor), `"underline"` (`\x1b[4 q`).
40
+ - `setCursorStyle(style)` + `config.cursorStyle` wiring.
41
+ - Bar/underline suppress software cursor marker, enable `tui.setShowHardwareCursor(true)`, write sequence.
42
+
43
+ ## Acceptance
44
+
45
+ - [ ] `src/telemetry.ts` pure tracker with unit tests (TPS/TTFT/stall maths, fixtures)
46
+ - [ ] `TrackingEditor` supports `cursorStyle`, toggled via config, no break to highlight tracking
47
+ - [ ] Border renders model top + telemetry bottom-right, respects live theme, truncates correctly
48
+ - [ ] `npm run typecheck` + `npm test` pass
@@ -0,0 +1,30 @@
1
+ # Spec 04 — Header: cwd + Hints (No Model)
2
+
3
+ Ticket: #9 · Type: grilling · Branch: `feat/header`
4
+
5
+ ## Question
6
+
7
+ What does the header contain and where does it sit, given the border already owns model/thinking and the detail window already lives above the editor?
8
+
9
+ ## Decision
10
+
11
+ - Content: `cwd` (honouring `workspaceDisplay: "path" | "name"`) + slash-command hints. **No model/thinking** — border owns it.
12
+ - `cwd`: `formatCwd(cwd)` when `workspaceDisplay==="path"`, `basenamePath(formatCwd(cwd))` when `"name"`. Truncated via `truncatePath` at narrow widths.
13
+ - Hints: 2–3 slash-command tips (e.g. `/theme`, `/model-info`) via `pickSlashCommandTips` or static list; dimmed.
14
+ - Placement: `setWidget("theme-header", ..., {placement:"aboveEditor"})`, always on when `config.enabled`. Must not overlap detail window: header renders first, detail window renders above editor but below header (or header is topmost). Use TUI widget ordering.
15
+ - Theme: reads live `Theme`, glyphs via `icons.mode` (`resolveGlyphs`), respects `theme.fg("dim")` for hints.
16
+
17
+ ## Reference
18
+
19
+ - `tmp/pi-open-tui/extensions/open-tui/header.ts` (~200 lines, logo + cwd + hints)
20
+ - `tmp/pi-open-tui/extensions/open-tui/utils.ts` (`formatCwd`, `basenamePath`, `truncatePath`, `pickSlashCommandTips`)
21
+ - `tmp/pi-open-tui/extensions/open-tui/icons.ts`
22
+
23
+ Rebuild bespoke, simplify: no animated logo (defer to fog), just cwd + hints line(s).
24
+
25
+ ## Acceptance
26
+
27
+ - [ ] `src/header.ts` exports `installHeader` / `renderHeader` with cwd + hints, workspaceDisplay-aware
28
+ - [ ] Respects live theme + icon mode
29
+ - [ ] No overlap with detail window when popup open
30
+ - [ ] `npm run typecheck` + `npm test` pass (pure render unit test)