omegon 0.6.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 (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: pi-extensions
3
+ description: Pi extension API reference and conventions. Use when creating, modifying, or debugging pi extensions — covers registerCommand, registerTool, event handlers, UI context, and common pitfalls.
4
+ ---
5
+
6
+ # Pi Extension API — Quick Reference
7
+
8
+ Extensions are TypeScript modules exporting a default function that receives `ExtensionAPI`.
9
+
10
+ ```typescript
11
+ import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
12
+
13
+ export default function (pi: ExtensionAPI) {
14
+ // register tools, commands, shortcuts, event handlers
15
+ }
16
+ ```
17
+
18
+ The factory can be `async` if initialization requires it.
19
+
20
+ ## Commands
21
+
22
+ **Method:** `pi.registerCommand(name, options)`
23
+
24
+ ```typescript
25
+ pi.registerCommand("my-command", {
26
+ description: "What this command does",
27
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
28
+ // args is the raw string after the command name
29
+ // ctx provides ui, sessionManager, model, etc.
30
+ ctx.ui.notify("Hello", "info");
31
+ },
32
+ });
33
+ ```
34
+
35
+ **⚠️ Common mistakes:**
36
+ - There is NO `addCommand` method. Only `registerCommand` exists.
37
+ - The handler signature is `handler(args, ctx)` — args first, context second.
38
+ - There is NO `ctx.say()` method. Use `ctx.ui.notify(message, type)`.
39
+ - There is NO `execute` property. The callback property is `handler`.
40
+
41
+ ### UI Methods on `ctx.ui`
42
+
43
+ | Method | Signature | Notes |
44
+ |--------|-----------|-------|
45
+ | `notify` | `(message: string, type?: "info" \| "warning" \| "error") => void` | Non-blocking notification |
46
+ | `confirm` | `(title: string, message: string, opts?) => Promise<boolean>` | **Two** string args required |
47
+ | `select` | `(title: string, options: string[], opts?) => Promise<string \| undefined>` | Returns selected item |
48
+ | `input` | `(title: string, placeholder?: string, opts?) => Promise<string \| undefined>` | Text input dialog |
49
+ | `setStatus` | `(key: string, text: string \| undefined) => void` | Footer status line |
50
+ | `setWidget` | `(key: string, content, options?) => void` | Widget above/below editor |
51
+ | `setFooter` | `(factory \| undefined) => void` | Custom footer component |
52
+
53
+ **⚠️ `confirm()` takes TWO string arguments** (title + message), not one. A single-arg call is a type error.
54
+
55
+ ## Tools
56
+
57
+ **Method:** `pi.registerTool(toolDefinition)`
58
+
59
+ ```typescript
60
+ import { Type } from "@sinclair/typebox";
61
+
62
+ pi.registerTool({
63
+ name: "my_tool",
64
+ label: "My Tool",
65
+ description: "What this tool does (shown to LLM)",
66
+ promptSnippet: "One-liner for Available Tools section",
67
+ promptGuidelines: ["Bullet points appended to system prompt Guidelines"],
68
+ parameters: Type.Object({
69
+ action: Type.String({ description: "What to do" }),
70
+ target: Type.Optional(Type.String({ description: "Optional target" })),
71
+ }),
72
+ execute: async (toolCallId, params, signal, onUpdate, ctx) => {
73
+ // params is typed from the schema
74
+ // signal: AbortSignal | undefined
75
+ // onUpdate: streaming partial results callback
76
+ // ctx: ExtensionContext (NOT ExtensionCommandContext)
77
+ return {
78
+ content: [{ type: "text", text: "result" }],
79
+ };
80
+ },
81
+ });
82
+ ```
83
+
84
+ **Key differences from commands:**
85
+ - Tools are called by the LLM, commands by the user via `/` prefix.
86
+ - Tool `execute` receives `(toolCallId, params, signal, onUpdate, ctx)`.
87
+ - Tool `ctx` is `ExtensionContext` (no `waitForIdle`, `newSession`, etc.).
88
+ - Tool results must return `{ content: Array<{type: "text", text: string}> }`.
89
+
90
+ ### StringEnum helper
91
+
92
+ For enum parameters, use the local helper (NOT `@cwilson613/pi-ai`):
93
+
94
+ ```typescript
95
+ import { StringEnum } from "../lib/typebox-helpers";
96
+
97
+ // In parameters:
98
+ action: StringEnum(["start", "stop", "status"], { description: "Action" }),
99
+ ```
100
+
101
+ ## Event Handlers
102
+
103
+ **Method:** `pi.on(eventName, handler)`
104
+
105
+ ```typescript
106
+ pi.on("session_start", async (event, ctx) => { ... });
107
+ pi.on("session_shutdown", async (event, ctx) => { ... });
108
+ ```
109
+
110
+ ### Valid Event Names
111
+
112
+ | Event | When | Can return |
113
+ |-------|------|------------|
114
+ | `session_start` | Session loaded | void |
115
+ | `session_shutdown` | Process exiting | void |
116
+ | `session_before_compact` | Before compaction | `{ cancel?, compaction? }` |
117
+ | `session_compact` | After compaction | void |
118
+ | `session_before_switch` | Before session switch | `{ cancel? }` |
119
+ | `session_switch` | After session switch | void |
120
+ | `session_before_fork` | Before fork | `{ cancel? }` |
121
+ | `session_fork` | After fork | void |
122
+ | `session_before_tree` | Before tree nav | `{ cancel?, summary? }` |
123
+ | `session_tree` | After tree nav | void |
124
+ | `context` | Before LLM call | `{ messages? }` |
125
+ | `before_agent_start` | After user input | `{ message?, systemPrompt? }` |
126
+ | `agent_start` | Agent loop begins | void |
127
+ | `agent_end` | Agent loop ends | void |
128
+ | `turn_start` / `turn_end` | Each turn | void |
129
+ | `message_start` / `message_update` / `message_end` | Message lifecycle | void |
130
+ | `tool_execution_start` / `tool_execution_update` / `tool_execution_end` | Tool lifecycle | void |
131
+ | `model_select` | Model changed | void |
132
+ | `tool_call` | Before tool executes | `{ block?, reason? }` |
133
+ | `tool_result` | After tool executes | `{ content?, isError? }` |
134
+ | `input` | User input received | `{ action: "continue" \| "transform" \| "handled" }` |
135
+ | `resources_discover` | After session_start | `{ skillPaths?, promptPaths?, themePaths? }` |
136
+
137
+ **⚠️ There is NO `session_end` event.** The cleanup event is `session_shutdown`.
138
+
139
+ ## Cross-Extension Communication
140
+
141
+ Use the shared event bus for decoupled extension-to-extension messaging:
142
+
143
+ ```typescript
144
+ // Emitter
145
+ pi.events.emit("my-event", data);
146
+
147
+ // Listener
148
+ pi.events.on("my-event", (data) => { ... });
149
+ ```
150
+
151
+ For shared state, use `globalThis` via `Symbol.for()`:
152
+
153
+ ```typescript
154
+ const STATE_KEY = Symbol.for("pi:shared-state");
155
+ (globalThis as any)[STATE_KEY] = sharedObject;
156
+ ```
157
+
158
+ ## Process Spawning in Extensions
159
+
160
+ When spawning child processes from extensions:
161
+
162
+ - **Use `stdio: "pipe"`** for processes whose output you capture. Never `stdio: "inherit"` — it corrupts the TUI.
163
+ - **Use `detached: true` + `child.unref()`** for background processes that should outlive the tool call.
164
+ - **Clean up in `session_shutdown`**: if you spawn a persistent process, kill it on shutdown.
165
+ - **Set `NONINTERACTIVE=1`** in env for install scripts to prevent stdin prompts hanging the TUI.
166
+ - **Respect `signal` (AbortSignal)**: register `signal.addEventListener("abort", ...)` to kill child processes on cancellation.
167
+
168
+ ```typescript
169
+ // Background process with cleanup
170
+ let child: ChildProcess | null = null;
171
+
172
+ function startProcess() {
173
+ child = spawn("my-binary", ["serve"], { stdio: "ignore", detached: true });
174
+ child.unref();
175
+ child.on("exit", () => { child = null; });
176
+ }
177
+
178
+ pi.on("session_shutdown", () => {
179
+ if (child) { child.kill("SIGTERM"); child = null; }
180
+ });
181
+ ```
182
+
183
+ ## Conventions
184
+
185
+ - **One extension per directory** under `extensions/`. Entry point is `index.ts`.
186
+ - **Testable domain logic** in separate files (e.g., `auth.ts`), index imports and re-exports.
187
+ - **Types** in `types.ts` when shared across files.
188
+ - **Config annotation** at top of file: `// @config KEY "description" [default: value]`
189
+ - **Extension load order** defined in `package.json` → `pi.extensions` array.
190
+ - **No circular imports** between extensions. Use shared-state or events for cross-extension data.
191
+ - **Import `type` separately** from runtime imports: `import type { ExtensionAPI } from "..."`.
@@ -0,0 +1,517 @@
1
+ ---
2
+ name: pi-tui
3
+ description: Pi TUI component patterns and conventions for building extension UIs. Covers the Component interface, overlays, built-in components, keyboard handling, theming, footer/widget/header APIs, and common pitfalls. Use when creating or modifying TUI components in pi extensions.
4
+ globs:
5
+ - extensions/**/overlay*.ts
6
+ - extensions/**/footer*.ts
7
+ - extensions/**/dashboard/**
8
+ ---
9
+
10
+ # Pi TUI — Extension UI Patterns
11
+
12
+ This skill covers the TUI layer for pi extensions. Read alongside `pi-extensions/SKILL.md` for the full extension API.
13
+
14
+ ## Package Geography
15
+
16
+ ```
17
+ @cwilson613/pi-coding-agent — ExtensionAPI, Theme, DynamicBorder, BorderedLoader, CustomEditor, etc.
18
+ @cwilson613/pi-tui — Component, Container, TUI, Text, Box, SelectList, etc.
19
+ @sinclair/typebox — Type schema for tool parameters
20
+ ```
21
+
22
+ All three are available via pi's jiti loader at runtime. **Do NOT `npm install` them** — they resolve through the alias map in the extension loader.
23
+
24
+ > ⚠️ Value imports from `@cwilson613/pi-tui` (e.g. `truncateToWidth`, `matchesKey`, `visibleWidth`) work inside pi but **fail under `tsx` or `node` directly** because pi-tui is a nested dependency of pi-coding-agent. Tests that need these must either mock them or run through pi.
25
+
26
+ ## Component Interface
27
+
28
+ Every TUI component implements:
29
+
30
+ ```typescript
31
+ interface Component {
32
+ render(width: number): string[]; // Lines ≤ width chars each
33
+ handleInput?(data: string): void; // Keyboard when focused
34
+ wantsKeyRelease?: boolean; // Kitty protocol key releases
35
+ invalidate(): void; // Clear cached render state
36
+ }
37
+ ```
38
+
39
+ **Critical rules:**
40
+ 1. Each line from `render()` must not exceed `width`. Use `truncateToWidth(line, width)`.
41
+ 2. Styles reset at line boundaries — reapply ANSI per line or use `wrapTextWithAnsi()`.
42
+ 3. Cache render output; invalidate on state change + call `tui.requestRender()`.
43
+
44
+ ## Imports Cheat Sheet
45
+
46
+ ```typescript
47
+ // Types (compile-time only — safe everywhere)
48
+ import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@cwilson613/pi-coding-agent";
49
+ import type { Component, OverlayHandle, OverlayOptions, TUI } from "@cwilson613/pi-tui";
50
+
51
+ // Values from pi-coding-agent (re-exported, always available)
52
+ import { DynamicBorder, BorderedLoader, CustomEditor } from "@cwilson613/pi-coding-agent";
53
+ import { getMarkdownTheme, getSelectListTheme, getSettingsListTheme, highlightCode } from "@cwilson613/pi-coding-agent";
54
+
55
+ // Values from pi-tui (work via jiti at runtime only)
56
+ import { matchesKey, Key, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@cwilson613/pi-tui";
57
+ import { Container, Text, Box, Spacer, Markdown, Image } from "@cwilson613/pi-tui";
58
+ import { SelectList, SettingsList } from "@cwilson613/pi-tui";
59
+ import type { SelectItem, SelectListTheme, SettingItem } from "@cwilson613/pi-tui";
60
+
61
+ // TypeBox for tool parameter schemas
62
+ import { Type } from "@sinclair/typebox";
63
+ ```
64
+
65
+ ## Displaying UI — The Five Surfaces
66
+
67
+ ### 1. `ctx.ui.custom<T>()` — Full-screen or Overlay Component
68
+
69
+ The primary way to show interactive UI. Blocks until `done()` is called.
70
+
71
+ ```typescript
72
+ const result = await ctx.ui.custom<string | null>(
73
+ (tui, theme, keybindings, done) => {
74
+ // Factory — return a Component (with optional dispose)
75
+ return new MyComponent(tui, theme, done);
76
+ },
77
+ {
78
+ overlay: true, // Render on top of chat
79
+ overlayOptions: { ... }, // Position/size
80
+ onHandle: (handle) => { ... }, // OverlayHandle for visibility control
81
+ }
82
+ );
83
+ ```
84
+
85
+ **Factory signature:** `(tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: T) => void) => Component & { dispose?(): void }`
86
+
87
+ **Return type options:**
88
+ - A class implementing `Component` + optional `dispose()`
89
+ - An inline object: `{ render, invalidate, handleInput, dispose? }`
90
+
91
+ ### 2. `ctx.ui.setFooter()` — Custom Footer
92
+
93
+ ```typescript
94
+ ctx.ui.setFooter((tui, theme, footerData) => ({
95
+ render(width: number): string[] {
96
+ const branch = footerData.getGitBranch();
97
+ const statuses = footerData.getExtensionStatuses();
98
+ return [truncateToWidth(`${branch} | ...`, width)];
99
+ },
100
+ invalidate() {},
101
+ dispose: footerData.onBranchChange(() => tui.requestRender()),
102
+ }));
103
+
104
+ ctx.ui.setFooter(undefined); // Restore default
105
+ ```
106
+
107
+ **`footerData` provides:** `getGitBranch(): string | null`, `getExtensionStatuses(): ReadonlyMap<string, string>`, `onBranchChange(cb): unsubscribe`.
108
+
109
+ > ⚠️ Default footer renders 2 lines. Your replacement must handle all display.
110
+
111
+ ### 3. `ctx.ui.setWidget()` — Persistent Content Above/Below Editor
112
+
113
+ ```typescript
114
+ // Simple string array
115
+ ctx.ui.setWidget("my-key", ["Line 1", "Line 2"], { placement: "belowEditor" });
116
+
117
+ // Component factory (has theme access)
118
+ ctx.ui.setWidget("my-key", (tui, theme) => ({
119
+ render: () => [theme.fg("accent", "● Active")],
120
+ invalidate: () => {},
121
+ }));
122
+
123
+ ctx.ui.setWidget("my-key", undefined); // Remove
124
+ ```
125
+
126
+ ### 4. `ctx.ui.setStatus()` — Footer Status Line
127
+
128
+ ```typescript
129
+ ctx.ui.setStatus("my-ext", theme.fg("accent", "● running"));
130
+ ctx.ui.setStatus("my-ext", undefined); // Clear
131
+ ```
132
+
133
+ ### 5. `ctx.ui.setHeader()` — Custom Header
134
+
135
+ ```typescript
136
+ ctx.ui.setHeader((tui, theme) => ({
137
+ render(width) { return [theme.fg("accent", "═".repeat(width))]; },
138
+ invalidate() {},
139
+ }));
140
+ ctx.ui.setHeader(undefined); // Restore default
141
+ ```
142
+
143
+ ## Overlay Positioning
144
+
145
+ ```typescript
146
+ overlayOptions: {
147
+ // Size
148
+ width: "40%", // SizeValue: number | `${number}%`
149
+ minWidth: 40, // Minimum columns
150
+ maxHeight: "80%", // Truncates render output
151
+
152
+ // Position — anchor-based (default: "center")
153
+ anchor: "right-center", // 9 positions: center, top-left, top-center, top-right,
154
+ // left-center, right-center, bottom-left, bottom-center, bottom-right
155
+ offsetX: -2, // Offset from anchor position
156
+ offsetY: 0,
157
+
158
+ // Position — absolute/percentage (alternative to anchor)
159
+ row: "25%", // From top
160
+ col: 10, // From left
161
+
162
+ // Margins
163
+ margin: 2, // All sides, or { top, right, bottom, left }
164
+
165
+ // Responsive hiding
166
+ visible: (termWidth, termHeight) => termWidth >= 100,
167
+ }
168
+ ```
169
+
170
+ **OverlayHandle** (from `onHandle` callback):
171
+ - `handle.setHidden(true/false)` — Toggle visibility
172
+ - `handle.isHidden()` — Check state
173
+ - `handle.hide()` — Permanently remove
174
+
175
+ ## Keyboard Handling
176
+
177
+ ```typescript
178
+ import { matchesKey, Key } from "@cwilson613/pi-tui";
179
+
180
+ handleInput(data: string): void {
181
+ if (matchesKey(data, Key.escape)) { /* ... */ }
182
+ if (matchesKey(data, Key.enter)) { /* ... */ }
183
+ if (matchesKey(data, Key.up)) { /* ... */ }
184
+ if (matchesKey(data, Key.down)) { /* ... */ }
185
+ if (matchesKey(data, Key.tab)) { /* ... */ }
186
+ if (matchesKey(data, Key.ctrl("c"))) { /* ... */ }
187
+ if (matchesKey(data, Key.ctrlShift("b"))) { /* ... */ }
188
+ // String format also works: "escape", "ctrl+c", "shift+tab"
189
+ }
190
+ ```
191
+
192
+ After handling input, call `tui.requestRender()` to trigger a redraw.
193
+
194
+ ## Built-in Components
195
+
196
+ | Component | Import | Use for |
197
+ |-----------|--------|---------|
198
+ | `Container` | pi-tui | Group children vertically |
199
+ | `Text` | pi-tui | Multi-line text with wrapping |
200
+ | `Box` | pi-tui | Container with padding/background |
201
+ | `Spacer` | pi-tui | Empty vertical space (`new Spacer(2)`) |
202
+ | `Markdown` | pi-tui | Rendered markdown with syntax highlighting |
203
+ | `Image` | pi-tui | Terminal images (Kitty/iTerm2/Ghostty/WezTerm) |
204
+ | `SelectList` | pi-tui | Interactive item selection |
205
+ | `SettingsList` | pi-tui | Toggle-style settings |
206
+ | `DynamicBorder` | pi-coding-agent | Styled horizontal border |
207
+ | `BorderedLoader` | pi-coding-agent | Spinner with cancel support |
208
+ | `CustomEditor` | pi-coding-agent | Base class for custom editors |
209
+
210
+ ### SelectList
211
+
212
+ ```typescript
213
+ import { SelectList } from "@cwilson613/pi-tui";
214
+ import type { SelectItem } from "@cwilson613/pi-tui";
215
+
216
+ const items: SelectItem[] = [
217
+ { value: "a", label: "Option A", description: "Details" },
218
+ { value: "b", label: "Option B" },
219
+ ];
220
+
221
+ const list = new SelectList(items, visibleCount, {
222
+ selectedPrefix: (t) => theme.fg("accent", t),
223
+ selectedText: (t) => theme.fg("accent", t),
224
+ description: (t) => theme.fg("muted", t),
225
+ scrollInfo: (t) => theme.fg("dim", t),
226
+ noMatch: (t) => theme.fg("warning", t),
227
+ });
228
+ list.onSelect = (item) => done(item.value);
229
+ list.onCancel = () => done(null);
230
+ ```
231
+
232
+ ## Theming
233
+
234
+ **Always use the `theme` from the callback** — never import or hardcode colors.
235
+
236
+ ```typescript
237
+ // Foreground
238
+ theme.fg("accent", "text") // Styled text
239
+ theme.fg("muted", "text") // Subdued
240
+ theme.fg("dim", "text") // Very subdued
241
+ theme.fg("success", "text") // Green
242
+ theme.fg("error", "text") // Red
243
+ theme.fg("warning", "text") // Yellow
244
+ theme.fg("border", "─") // Border color
245
+ theme.bold("text") // Bold
246
+
247
+ // Background
248
+ theme.bg("selectedBg", "text")
249
+ theme.bg("toolSuccessBg", "text")
250
+ ```
251
+
252
+ **Full color list:** text, accent, muted, dim, success, error, warning, border, borderAccent, borderMuted, mdHeading, mdLink, mdCode, syntaxKeyword, syntaxString, syntaxComment, etc.
253
+
254
+ ### Theme Change Handling
255
+
256
+ When the theme changes, `invalidate()` is called. If your component caches themed strings, rebuild them:
257
+
258
+ ```typescript
259
+ class MyComponent extends Container {
260
+ private message: string;
261
+ private theme: Theme;
262
+
263
+ constructor(message: string, theme: Theme) {
264
+ super();
265
+ this.message = message;
266
+ this.theme = theme;
267
+ this.rebuild();
268
+ }
269
+
270
+ setTheme(theme: Theme): void {
271
+ this.theme = theme;
272
+ this.rebuild();
273
+ }
274
+
275
+ private rebuild(): void {
276
+ this.clear();
277
+ // Use current theme colors — will be re-called on theme change
278
+ this.addChild(new Text(this.theme.fg("accent", this.message), 1, 0));
279
+ }
280
+
281
+ override invalidate(): void {
282
+ super.invalidate();
283
+ this.rebuild();
284
+ }
285
+ }
286
+ ```
287
+
288
+ ## Common Patterns
289
+
290
+ ### Bordered Box Helper
291
+
292
+ ```typescript
293
+ protected box(lines: string[], width: number, title?: string): string[] {
294
+ const th = this.theme;
295
+ const innerW = Math.max(1, width - 2);
296
+ const result: string[] = [];
297
+
298
+ const titleStr = title ? truncateToWidth(` ${title} `, innerW) : "";
299
+ const titleW = visibleWidth(titleStr);
300
+ const topLeft = "─".repeat(Math.floor((innerW - titleW) / 2));
301
+ const topRight = "─".repeat(Math.max(0, innerW - titleW - topLeft.length));
302
+ result.push(th.fg("border", `╭${topLeft}`) + th.fg("accent", titleStr) + th.fg("border", `${topRight}╮`));
303
+
304
+ for (const line of lines) {
305
+ result.push(th.fg("border", "│") + truncateToWidth(line, innerW, "…", true) + th.fg("border", "│"));
306
+ }
307
+
308
+ result.push(th.fg("border", `╰${"─".repeat(innerW)}╯`));
309
+ return result;
310
+ }
311
+ ```
312
+
313
+ ### Live-Updating Overlay (~30 FPS)
314
+
315
+ ```typescript
316
+ class LivePanel {
317
+ private interval: ReturnType<typeof setInterval> | null = null;
318
+
319
+ constructor(private tui: TUI, private theme: Theme, private done: () => void) {
320
+ this.interval = setInterval(() => {
321
+ this.tui.requestRender(); // Triggers render() call
322
+ }, 1000 / 30);
323
+ }
324
+
325
+ render(width: number): string[] { /* ... */ }
326
+ handleInput(data: string): void {
327
+ if (matchesKey(data, "escape")) { this.dispose(); this.done(); }
328
+ }
329
+ invalidate(): void {}
330
+ dispose(): void {
331
+ if (this.interval) { clearInterval(this.interval); this.interval = null; }
332
+ }
333
+ }
334
+ ```
335
+
336
+ ### Responsive Sidepanel
337
+
338
+ ```typescript
339
+ await ctx.ui.custom<void>(
340
+ (tui, theme, _kb, done) => new SidePanel(tui, theme, done),
341
+ {
342
+ overlay: true,
343
+ overlayOptions: {
344
+ anchor: "right-center",
345
+ width: "25%",
346
+ minWidth: 30,
347
+ margin: { right: 1 },
348
+ visible: (termWidth) => termWidth >= 100, // Auto-hide on narrow terminals
349
+ },
350
+ }
351
+ );
352
+ ```
353
+
354
+ ### Toggle Overlay Visibility
355
+
356
+ ```typescript
357
+ let handle: OverlayHandle | null = null;
358
+
359
+ await ctx.ui.custom<void>(
360
+ (tui, theme, _kb, done) => new MyPanel(tui, theme, done),
361
+ {
362
+ overlay: true,
363
+ overlayOptions: { anchor: "right-center", width: "30%" },
364
+ onHandle: (h) => { handle = h; },
365
+ }
366
+ );
367
+
368
+ // Elsewhere (e.g. in a shortcut handler):
369
+ handle?.setHidden(!handle.isHidden());
370
+ ```
371
+
372
+ ### Cross-Extension Event-Driven Updates
373
+
374
+ ```typescript
375
+ // In extension init:
376
+ const EVENT_NAME = "my-ext:update";
377
+ pi.events.emit(EVENT_NAME, { status: "active" });
378
+
379
+ // In overlay constructor:
380
+ this.unsubscribe = pi.events.on(EVENT_NAME, (data) => {
381
+ this.state = data;
382
+ this.tui.requestRender();
383
+ });
384
+
385
+ // In dispose:
386
+ dispose(): void { this.unsubscribe?.(); }
387
+ ```
388
+
389
+ ### Shared State via Symbol.for
390
+
391
+ ```typescript
392
+ const STATE_KEY = Symbol.for("pi:my-extension-state");
393
+
394
+ // Writer
395
+ (globalThis as any)[STATE_KEY] = { count: 0, items: [] };
396
+
397
+ // Reader (any extension)
398
+ const state = (globalThis as any)[Symbol.for("pi:my-extension-state")];
399
+ ```
400
+
401
+ ## Testability Pattern — ThemeFn Bridge
402
+
403
+ Separate pure rendering logic from pi-tui dependencies for unit testing:
404
+
405
+ ```typescript
406
+ // overlay-data.ts (pure logic, testable)
407
+ type ThemeFn = (color: string, text: string) => string;
408
+
409
+ export function renderStatusLine(state: MyState, thFn: ThemeFn, width: number): string {
410
+ return thFn("accent", state.label) + " " + thFn("dim", `${state.count} items`);
411
+ }
412
+
413
+ // overlay.ts (pi-tui bridge, not unit-tested)
414
+ import type { Theme } from "@cwilson613/pi-coding-agent";
415
+ const thFn: ThemeFn = (color, text) => theme.fg(color as any, text);
416
+ const line = renderStatusLine(state, thFn, width);
417
+ ```
418
+
419
+ In tests, use an identity `thFn`: `const thFn = (_c: string, t: string) => t;`
420
+
421
+ ## Pitfalls
422
+
423
+ | Pitfall | Fix |
424
+ |---------|-----|
425
+ | Lines exceed `width` | Always `truncateToWidth(line, width)` |
426
+ | Overlay doesn't update | Call `tui.requestRender()` after state changes |
427
+ | Theme colors stale after theme switch | Override `invalidate()` to rebuild themed content |
428
+ | `Ctrl+Shift+D` shortcut doesn't fire | Hardcoded as pi-tui debug key — use a different binding |
429
+ | `stdio: "inherit"` in child process | Use `stdio: "pipe"` — inherit corrupts TUI |
430
+ | Overlay reuse after close | Create fresh instances — overlays dispose on close |
431
+ | `process.stderr.write()` corrupts TUI | Write to a log file instead |
432
+ | Footer `render()` returns wrong line count | Default footer is 2 lines; match or replace entirely |
433
+ | pi-tui imports fail in tests | Mock them, or only test pure logic (ThemeFn pattern) |
434
+ | `ctx.ui.custom()` called without `overlay: true` | Takes over the full screen — usually you want overlay |
435
+
436
+ ## Render Caching Pattern
437
+
438
+ ```typescript
439
+ class CachedComponent {
440
+ private cachedWidth?: number;
441
+ private cachedLines?: string[];
442
+
443
+ render(width: number): string[] {
444
+ if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
445
+ this.cachedLines = this.computeLines(width);
446
+ this.cachedWidth = width;
447
+ return this.cachedLines;
448
+ }
449
+
450
+ invalidate(): void {
451
+ this.cachedWidth = undefined;
452
+ this.cachedLines = undefined;
453
+ }
454
+ }
455
+ ```
456
+
457
+ ## IME / Focusable Support
458
+
459
+ For components with text cursors (CJK input method support):
460
+
461
+ ```typescript
462
+ import { CURSOR_MARKER, type Focusable } from "@cwilson613/pi-tui";
463
+
464
+ class MyInput implements Component, Focusable {
465
+ focused = false;
466
+
467
+ render(width: number): string[] {
468
+ const marker = this.focused ? CURSOR_MARKER : "";
469
+ return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`];
470
+ }
471
+ }
472
+ ```
473
+
474
+ Container components with embedded inputs must propagate `focused` to child inputs for correct IME cursor positioning.
475
+
476
+ ## Custom Editor
477
+
478
+ Extend `CustomEditor` (not `Editor`) for vim-mode or alternative key handling:
479
+
480
+ ```typescript
481
+ import { CustomEditor } from "@cwilson613/pi-coding-agent";
482
+
483
+ class VimEditor extends CustomEditor {
484
+ private mode: "normal" | "insert" = "insert";
485
+
486
+ handleInput(data: string): void {
487
+ if (this.mode === "insert") { super.handleInput(data); return; }
488
+ // Normal mode: remap keys, call super.handleInput() for unhandled
489
+ if (data === "i") { this.mode = "insert"; return; }
490
+ if (data === "h") { super.handleInput("\x1b[D"); return; } // Left
491
+ super.handleInput(data);
492
+ }
493
+ }
494
+
495
+ // Register:
496
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
497
+ ctx.ui.setEditorComponent(undefined); // Restore default
498
+ ```
499
+
500
+ ## Tool Rendering
501
+
502
+ Tools can customize their display in the chat:
503
+
504
+ ```typescript
505
+ pi.registerTool({
506
+ name: "my_tool",
507
+ // ...
508
+ renderCall: (args, theme) => {
509
+ return new Text(theme.fg("accent", `Running: ${args.action}`), 1, 0);
510
+ },
511
+ renderResult: (result, options, theme) => {
512
+ if (!options.expanded) return undefined; // Collapsed = default
513
+ const mdTheme = getMarkdownTheme();
514
+ return new Markdown(result.content[0]?.text ?? "", 0, 0, mdTheme);
515
+ },
516
+ });
517
+ ```