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.
- package/.gitattributes +3 -0
- package/AGENTS.md +16 -0
- package/LICENSE +15 -0
- package/README.md +289 -0
- package/bin/pi.mjs +30 -0
- package/extensions/00-secrets/index.ts +1126 -0
- package/extensions/01-auth/auth.ts +401 -0
- package/extensions/01-auth/index.ts +289 -0
- package/extensions/auto-compact.ts +42 -0
- package/extensions/bootstrap/deps.ts +291 -0
- package/extensions/bootstrap/index.ts +811 -0
- package/extensions/chronos/chronos.sh +487 -0
- package/extensions/chronos/index.ts +148 -0
- package/extensions/cleave/assessment.ts +754 -0
- package/extensions/cleave/bridge.ts +31 -0
- package/extensions/cleave/conflicts.ts +250 -0
- package/extensions/cleave/dispatcher.ts +808 -0
- package/extensions/cleave/guardrails.ts +426 -0
- package/extensions/cleave/index.ts +3121 -0
- package/extensions/cleave/lifecycle-emitter.ts +20 -0
- package/extensions/cleave/openspec.ts +811 -0
- package/extensions/cleave/planner.ts +260 -0
- package/extensions/cleave/review.ts +579 -0
- package/extensions/cleave/skills.ts +355 -0
- package/extensions/cleave/types.ts +261 -0
- package/extensions/cleave/workspace.ts +861 -0
- package/extensions/cleave/worktree.ts +243 -0
- package/extensions/core-renderers.ts +253 -0
- package/extensions/dashboard/context-gauge.ts +58 -0
- package/extensions/dashboard/file-watch.ts +14 -0
- package/extensions/dashboard/footer.ts +1145 -0
- package/extensions/dashboard/git.ts +185 -0
- package/extensions/dashboard/index.ts +478 -0
- package/extensions/dashboard/memory-audit.ts +34 -0
- package/extensions/dashboard/overlay-data.ts +705 -0
- package/extensions/dashboard/overlay.ts +365 -0
- package/extensions/dashboard/render-utils.ts +54 -0
- package/extensions/dashboard/types.ts +191 -0
- package/extensions/dashboard/uri-helper.ts +45 -0
- package/extensions/debug.ts +69 -0
- package/extensions/defaults.ts +282 -0
- package/extensions/design-tree/dashboard-state.ts +161 -0
- package/extensions/design-tree/design-card.ts +362 -0
- package/extensions/design-tree/index.ts +2130 -0
- package/extensions/design-tree/lifecycle-emitter.ts +41 -0
- package/extensions/design-tree/tree.ts +1607 -0
- package/extensions/design-tree/types.ts +163 -0
- package/extensions/distill.ts +127 -0
- package/extensions/effort/index.ts +395 -0
- package/extensions/effort/tiers.ts +146 -0
- package/extensions/effort/types.ts +105 -0
- package/extensions/lib/git-state.ts +227 -0
- package/extensions/lib/local-models.ts +157 -0
- package/extensions/lib/model-preferences.ts +51 -0
- package/extensions/lib/model-routing.ts +720 -0
- package/extensions/lib/operator-fallback.ts +205 -0
- package/extensions/lib/operator-profile.ts +360 -0
- package/extensions/lib/slash-command-bridge.ts +253 -0
- package/extensions/lib/typebox-helpers.ts +16 -0
- package/extensions/local-inference/index.ts +727 -0
- package/extensions/mcp-bridge/README.md +220 -0
- package/extensions/mcp-bridge/index.ts +951 -0
- package/extensions/mcp-bridge/lib.ts +365 -0
- package/extensions/mcp-bridge/mcp.json +3 -0
- package/extensions/mcp-bridge/package.json +11 -0
- package/extensions/model-budget.ts +752 -0
- package/extensions/offline-driver.ts +403 -0
- package/extensions/openspec/archive-gate.ts +164 -0
- package/extensions/openspec/branch-cleanup.ts +64 -0
- package/extensions/openspec/dashboard-state.ts +50 -0
- package/extensions/openspec/index.ts +1917 -0
- package/extensions/openspec/lifecycle-emitter.ts +65 -0
- package/extensions/openspec/lifecycle-files.ts +70 -0
- package/extensions/openspec/lifecycle.ts +50 -0
- package/extensions/openspec/reconcile.ts +187 -0
- package/extensions/openspec/spec.ts +1385 -0
- package/extensions/openspec/types.ts +98 -0
- package/extensions/project-memory/DESIGN-global-mind.md +198 -0
- package/extensions/project-memory/README.md +202 -0
- package/extensions/project-memory/api-types.ts +382 -0
- package/extensions/project-memory/compaction-policy.ts +29 -0
- package/extensions/project-memory/core.ts +164 -0
- package/extensions/project-memory/embeddings.ts +230 -0
- package/extensions/project-memory/extraction-v2.ts +861 -0
- package/extensions/project-memory/factstore.ts +2177 -0
- package/extensions/project-memory/index.ts +3459 -0
- package/extensions/project-memory/injection-metrics.ts +91 -0
- package/extensions/project-memory/jsonl-io.ts +12 -0
- package/extensions/project-memory/lifecycle.ts +331 -0
- package/extensions/project-memory/migration.ts +293 -0
- package/extensions/project-memory/package.json +9 -0
- package/extensions/project-memory/sci-renderers.ts +7 -0
- package/extensions/project-memory/template.ts +103 -0
- package/extensions/project-memory/triggers.ts +52 -0
- package/extensions/project-memory/types.ts +102 -0
- package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
- package/extensions/render/composition/package-lock.json +534 -0
- package/extensions/render/composition/package.json +22 -0
- package/extensions/render/composition/render.mjs +246 -0
- package/extensions/render/composition/test-comp.tsx +87 -0
- package/extensions/render/composition/types.ts +24 -0
- package/extensions/render/excalidraw/UPSTREAM.md +81 -0
- package/extensions/render/excalidraw/elements.ts +764 -0
- package/extensions/render/excalidraw/index.ts +66 -0
- package/extensions/render/excalidraw/types.ts +223 -0
- package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
- package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
- package/extensions/render/excalidraw-renderer/render_template.html +59 -0
- package/extensions/render/index.ts +830 -0
- package/extensions/render/native-diagrams/index.ts +57 -0
- package/extensions/render/native-diagrams/motifs.ts +542 -0
- package/extensions/render/native-diagrams/raster.ts +8 -0
- package/extensions/render/native-diagrams/scene.ts +75 -0
- package/extensions/render/native-diagrams/spec.ts +204 -0
- package/extensions/render/native-diagrams/svg.ts +116 -0
- package/extensions/sci-ui.ts +304 -0
- package/extensions/session-log.ts +174 -0
- package/extensions/shared-state.ts +146 -0
- package/extensions/spinner-verbs.ts +91 -0
- package/extensions/style.ts +281 -0
- package/extensions/terminal-title.ts +191 -0
- package/extensions/tool-profile/index.ts +291 -0
- package/extensions/tool-profile/profiles.ts +290 -0
- package/extensions/types.d.ts +9 -0
- package/extensions/vault/index.ts +185 -0
- package/extensions/version-check.ts +90 -0
- package/extensions/view/index.ts +859 -0
- package/extensions/view/uri-resolver.ts +148 -0
- package/extensions/web-search/index.ts +182 -0
- package/extensions/web-search/providers.ts +121 -0
- package/extensions/web-ui/index.ts +110 -0
- package/extensions/web-ui/server.ts +265 -0
- package/extensions/web-ui/state.ts +462 -0
- package/extensions/web-ui/static/index.html +145 -0
- package/extensions/web-ui/types.ts +284 -0
- package/package.json +76 -0
- package/prompts/init.md +75 -0
- package/prompts/new-repo.md +54 -0
- package/prompts/oci-login.md +56 -0
- package/prompts/status.md +50 -0
- package/settings.json +4 -0
- package/skills/cleave/SKILL.md +218 -0
- package/skills/git/SKILL.md +209 -0
- package/skills/git/_reference/ci-validation.md +204 -0
- package/skills/oci/SKILL.md +338 -0
- package/skills/openspec/SKILL.md +346 -0
- package/skills/pi-extensions/SKILL.md +191 -0
- package/skills/pi-tui/SKILL.md +517 -0
- package/skills/python/SKILL.md +189 -0
- package/skills/rust/SKILL.md +268 -0
- package/skills/security/SKILL.md +206 -0
- package/skills/style/SKILL.md +264 -0
- package/skills/typescript/SKILL.md +225 -0
- package/skills/vault/SKILL.md +102 -0
- package/themes/alpharius-legacy.json +85 -0
- package/themes/alpharius.conf +59 -0
- 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
|
+
```
|