@code-yeongyu/senpi 2026.5.29-4 → 2026.6.2
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/CHANGELOG.md +41 -1
- package/README.md +8 -2
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +13 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +5 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +18 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +3 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +9 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +4 -2
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +13 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +7 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +2 -0
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +29 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-attribution.d.ts +4 -0
- package/dist/core/provider-attribution.d.ts.map +1 -0
- package/dist/core/provider-attribution.js +73 -0
- package/dist/core/provider-attribution.js.map +1 -0
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +1 -0
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +8 -34
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +92 -68
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +7 -10
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +5 -7
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +6 -7
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +5 -2
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +17 -1
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +5 -6
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +8 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +25 -1
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +39 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +1 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/extensions.md +35 -11
- package/docs/providers.md +2 -0
- package/docs/quickstart.md +1 -0
- package/docs/rpc.md +3 -2
- package/docs/session-format.md +1 -1
- package/docs/sessions.md +8 -0
- package/docs/settings.md +1 -1
- package/docs/terminal-setup.md +2 -0
- package/docs/tui.md +12 -3
- package/docs/usage.md +6 -1
- package/examples/extensions/custom-header.ts +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +53 -2
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/doom-overlay/index.ts +1 -1
- package/examples/extensions/handoff.ts +1 -1
- package/examples/extensions/interactive-shell.ts +1 -1
- package/examples/extensions/overlay-qa-tests.ts +152 -81
- package/examples/extensions/qna.ts +1 -1
- package/examples/extensions/question.ts +1 -1
- package/examples/extensions/questionnaire.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/snake.ts +1 -1
- package/examples/extensions/space-invaders.ts +1 -1
- package/examples/extensions/summarize.ts +1 -1
- package/examples/extensions/tic-tac-toe.ts +1 -1
- package/examples/extensions/todo.ts +1 -1
- package/examples/extensions/tools.ts +5 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
package/docs/session-format.md
CHANGED
|
@@ -282,7 +282,7 @@ Set `label` to `undefined` to clear a label.
|
|
|
282
282
|
|
|
283
283
|
### SessionInfoEntry
|
|
284
284
|
|
|
285
|
-
Session metadata (e.g., user-defined display name). Set via `/name`
|
|
285
|
+
Session metadata (e.g., user-defined display name). Set via `/name`, `--name` / `-n`, or `pi.setSessionName()` in extensions.
|
|
286
286
|
|
|
287
287
|
```json
|
|
288
288
|
{"type":"session_info","id":"k1l2m3n4","parentId":"j0k1l2m3","timestamp":"2024-12-03T14:35:00.000Z","name":"Refactor auth module"}
|
package/docs/sessions.md
CHANGED
|
@@ -10,6 +10,7 @@ Sessions auto-save to `~/.pi/agent/sessions/`, organized by working directory. E
|
|
|
10
10
|
pi -c # Continue most recent session
|
|
11
11
|
pi -r # Browse and select from past sessions
|
|
12
12
|
pi --no-session # Ephemeral mode; do not save
|
|
13
|
+
pi --name "my task" # Set session display name at startup
|
|
13
14
|
pi --session <path|id> # Use a specific session file or partial session ID
|
|
14
15
|
pi --fork <path|id> # Fork a session file or partial session ID into a new session
|
|
15
16
|
```
|
|
@@ -56,6 +57,13 @@ Use `/name <name>` to set a human-readable session name:
|
|
|
56
57
|
/name Refactor auth module
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
Set the name at startup with `--name` or `-n`:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pi --name "Refactor auth module"
|
|
64
|
+
pi --name "CI audit" -p "Review this build failure"
|
|
65
|
+
```
|
|
66
|
+
|
|
59
67
|
Named sessions are easier to find in `/resume` and `pi -r`.
|
|
60
68
|
|
|
61
69
|
## Branching with `/tree`
|
package/docs/settings.md
CHANGED
|
@@ -60,7 +60,7 @@ When this value is anything other than `"auto"`, it overrides any model-level `p
|
|
|
60
60
|
| `treeFilterMode` | string | `"default"` | Default filter for `/tree`: `"default"`, `"no-tools"`, `"user-only"`, `"labeled-only"`, `"all"` |
|
|
61
61
|
| `editorPaddingX` | number | `0` | Horizontal padding for input editor (0-3) |
|
|
62
62
|
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
|
|
63
|
-
| `showHardwareCursor` | boolean | `false` | Show terminal cursor |
|
|
63
|
+
| `showHardwareCursor` | boolean | `false` | Show the terminal cursor while TUI positions it for IME support |
|
|
64
64
|
|
|
65
65
|
### Telemetry and update checks
|
|
66
66
|
|
package/docs/terminal-setup.md
CHANGED
|
@@ -49,6 +49,8 @@ config.enable_kitty_keyboard = true
|
|
|
49
49
|
return config
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
On WSL, WezTerm may require a visible hardware cursor for IME candidate window positioning. If CJK IME candidates do not follow the text cursor, set `PI_HARDWARE_CURSOR=1` before running pi or set `showHardwareCursor` to `true` in settings.
|
|
53
|
+
|
|
52
54
|
## VS Code (Integrated Terminal)
|
|
53
55
|
|
|
54
56
|
`keybindings.json` locations:
|
package/docs/tui.md
CHANGED
|
@@ -50,9 +50,9 @@ When a `Focusable` component has focus, TUI:
|
|
|
50
50
|
1. Sets `focused = true` on the component
|
|
51
51
|
2. Scans rendered output for `CURSOR_MARKER` (a zero-width APC escape sequence)
|
|
52
52
|
3. Positions the hardware terminal cursor at that location
|
|
53
|
-
4. Shows the hardware cursor
|
|
53
|
+
4. Shows the hardware cursor only when `showHardwareCursor` is enabled
|
|
54
54
|
|
|
55
|
-
This
|
|
55
|
+
The cursor remains hidden by default. This keeps the fake cursor rendering, while still positioning the hardware cursor for terminals that track IME candidate windows with hidden cursors. Some terminals require a visible hardware cursor for IME positioning; enable it with `showHardwareCursor`, `setShowHardwareCursor(true)`, or `PI_HARDWARE_CURSOR=1`. The `Editor` and `Input` built-in components already implement this interface.
|
|
56
56
|
|
|
57
57
|
### Container Components with Embedded Inputs
|
|
58
58
|
|
|
@@ -145,8 +145,11 @@ const result = await ctx.ui.custom<string | null>(
|
|
|
145
145
|
// Responsive: hide on narrow terminals
|
|
146
146
|
visible: (termWidth, termHeight) => termWidth >= 80,
|
|
147
147
|
},
|
|
148
|
-
// Get handle for programmatic visibility control
|
|
148
|
+
// Get handle for programmatic focus and visibility control
|
|
149
149
|
onHandle: (handle) => {
|
|
150
|
+
// handle.focus() - focus this overlay and bring it to the visual front
|
|
151
|
+
// handle.unfocus() - release input to normal fallback
|
|
152
|
+
// handle.unfocus({ target }) - release input to a specific component or null
|
|
150
153
|
// handle.setHidden(true/false) - toggle visibility
|
|
151
154
|
// handle.hide() - permanently remove
|
|
152
155
|
},
|
|
@@ -154,6 +157,12 @@ const result = await ctx.ui.custom<string | null>(
|
|
|
154
157
|
);
|
|
155
158
|
```
|
|
156
159
|
|
|
160
|
+
### Overlay Focus
|
|
161
|
+
|
|
162
|
+
A focused visible overlay keeps input ownership across temporary non-overlay UI. If an overlay opens another `ctx.ui.custom()` component without `{ overlay: true }`, that replacement UI receives input while it is active; when it closes, the focused overlay can reclaim input.
|
|
163
|
+
|
|
164
|
+
Use `handle.unfocus()` when a visible overlay should stop owning input and let TUI fall back to another visible capturing overlay or the previous focus target. Use `handle.unfocus({ target })` when a specific component should receive input while the overlay stays visible. Passing `{ target: null }` intentionally leaves no focused component until focus is set again.
|
|
165
|
+
|
|
157
166
|
### Overlay Lifecycle
|
|
158
167
|
|
|
159
168
|
Overlay components are disposed when closed. Don't reuse references - create fresh instances:
|
package/docs/usage.md
CHANGED
|
@@ -76,6 +76,7 @@ Sessions are saved automatically to `~/.pi/agent/sessions/`, organized by workin
|
|
|
76
76
|
pi -c # Continue most recent session
|
|
77
77
|
pi -r # Browse and select a session
|
|
78
78
|
pi --no-session # Ephemeral mode; do not save
|
|
79
|
+
pi --name "my task" # Set session display name at startup
|
|
79
80
|
pi --session <path|id> # Use a specific session file or session ID
|
|
80
81
|
pi --fork <path|id> # Fork a session into a new session file
|
|
81
82
|
```
|
|
@@ -178,6 +179,7 @@ cat README.md | pi -p "Summarize this text"
|
|
|
178
179
|
| `--fork <path\|id>` | Fork a session file or partial UUID into a new session |
|
|
179
180
|
| `--session-dir <dir>` | Custom session storage directory |
|
|
180
181
|
| `--no-session` | Ephemeral mode; do not save |
|
|
182
|
+
| `--name <name>`, `-n <name>` | Set session display name at startup |
|
|
181
183
|
|
|
182
184
|
### Tool Options
|
|
183
185
|
|
|
@@ -242,6 +244,9 @@ pi -p "Summarize this codebase"
|
|
|
242
244
|
# Non-interactive with piped stdin
|
|
243
245
|
cat README.md | pi -p "Summarize this text"
|
|
244
246
|
|
|
247
|
+
# Named one-shot session
|
|
248
|
+
pi --name "release audit" -p "Audit this repository"
|
|
249
|
+
|
|
245
250
|
# Different model
|
|
246
251
|
pi --provider openai --model gpt-4o "Help me refactor"
|
|
247
252
|
|
|
@@ -270,7 +275,7 @@ pi --exclude-tools ask_question
|
|
|
270
275
|
| `PI_PACKAGE_DIR` | Override package directory, useful for Nix/Guix store paths |
|
|
271
276
|
| `PI_OFFLINE` | Disable startup network operations, including update checks, package update checks, and install/update telemetry |
|
|
272
277
|
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
|
|
273
|
-
| `PI_TELEMETRY` | Override install/update telemetry: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable update checks |
|
|
278
|
+
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable update checks |
|
|
274
279
|
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache where supported |
|
|
275
280
|
| `VISUAL`, `EDITOR` | External editor for Ctrl+G |
|
|
276
281
|
|
|
@@ -47,7 +47,7 @@ function getPiMascot(theme: Theme): string[] {
|
|
|
47
47
|
export default function (pi: ExtensionAPI) {
|
|
48
48
|
// Set custom header immediately on load (if UI is available)
|
|
49
49
|
pi.on("session_start", async (_event, ctx) => {
|
|
50
|
-
if (ctx.
|
|
50
|
+
if (ctx.mode === "tui") {
|
|
51
51
|
ctx.ui.setHeader((_tui, theme) => {
|
|
52
52
|
return {
|
|
53
53
|
render(_width: number): string[] {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extension-custom-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pi-extension-custom-provider",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.78.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.52.0"
|
|
12
12
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type SimpleStreamOptions,
|
|
22
22
|
streamSimpleAnthropic,
|
|
23
23
|
streamSimpleOpenAIResponses,
|
|
24
|
+
type ThinkingLevelMap,
|
|
24
25
|
} from "@earendil-works/pi-ai";
|
|
25
26
|
|
|
26
27
|
// =============================================================================
|
|
@@ -49,6 +50,7 @@ interface GitLabModel {
|
|
|
49
50
|
backend: Backend;
|
|
50
51
|
baseUrl: string;
|
|
51
52
|
reasoning: boolean;
|
|
53
|
+
thinkingLevelMap?: ThinkingLevelMap;
|
|
52
54
|
input: ("text" | "image")[];
|
|
53
55
|
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
54
56
|
contextWindow: number;
|
|
@@ -57,12 +59,37 @@ interface GitLabModel {
|
|
|
57
59
|
|
|
58
60
|
export const MODELS: GitLabModel[] = [
|
|
59
61
|
// Anthropic
|
|
62
|
+
{
|
|
63
|
+
id: "claude-opus-4-8",
|
|
64
|
+
name: "Claude Opus 4.8",
|
|
65
|
+
backend: "anthropic",
|
|
66
|
+
baseUrl: ANTHROPIC_PROXY_URL,
|
|
67
|
+
reasoning: true,
|
|
68
|
+
thinkingLevelMap: { xhigh: "max" },
|
|
69
|
+
input: ["text", "image"],
|
|
70
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
71
|
+
contextWindow: 1000000,
|
|
72
|
+
maxTokens: 128000,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "claude-sonnet-4-6",
|
|
76
|
+
name: "Claude Sonnet 4.6",
|
|
77
|
+
backend: "anthropic",
|
|
78
|
+
baseUrl: ANTHROPIC_PROXY_URL,
|
|
79
|
+
reasoning: true,
|
|
80
|
+
thinkingLevelMap: { xhigh: "max" },
|
|
81
|
+
input: ["text", "image"],
|
|
82
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
83
|
+
contextWindow: 1000000,
|
|
84
|
+
maxTokens: 64000,
|
|
85
|
+
},
|
|
60
86
|
{
|
|
61
87
|
id: "claude-opus-4-5-20251101",
|
|
62
88
|
name: "Claude Opus 4.5",
|
|
63
89
|
backend: "anthropic",
|
|
64
90
|
baseUrl: ANTHROPIC_PROXY_URL,
|
|
65
91
|
reasoning: true,
|
|
92
|
+
thinkingLevelMap: { xhigh: "max" },
|
|
66
93
|
input: ["text", "image"],
|
|
67
94
|
cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
68
95
|
contextWindow: 200000,
|
|
@@ -74,6 +101,7 @@ export const MODELS: GitLabModel[] = [
|
|
|
74
101
|
backend: "anthropic",
|
|
75
102
|
baseUrl: ANTHROPIC_PROXY_URL,
|
|
76
103
|
reasoning: true,
|
|
104
|
+
thinkingLevelMap: { xhigh: "max" },
|
|
77
105
|
input: ["text", "image"],
|
|
78
106
|
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
79
107
|
contextWindow: 200000,
|
|
@@ -85,12 +113,24 @@ export const MODELS: GitLabModel[] = [
|
|
|
85
113
|
backend: "anthropic",
|
|
86
114
|
baseUrl: ANTHROPIC_PROXY_URL,
|
|
87
115
|
reasoning: true,
|
|
116
|
+
thinkingLevelMap: { xhigh: "max" },
|
|
88
117
|
input: ["text", "image"],
|
|
89
118
|
cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
90
119
|
contextWindow: 200000,
|
|
91
120
|
maxTokens: 8192,
|
|
92
121
|
},
|
|
93
122
|
// OpenAI (all use Responses API)
|
|
123
|
+
{
|
|
124
|
+
id: "gpt-5.5-2026-04-23",
|
|
125
|
+
name: "GPT-5.5",
|
|
126
|
+
backend: "openai",
|
|
127
|
+
baseUrl: OPENAI_PROXY_URL,
|
|
128
|
+
reasoning: true,
|
|
129
|
+
input: ["text", "image"],
|
|
130
|
+
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
|
131
|
+
contextWindow: 272000,
|
|
132
|
+
maxTokens: 128000,
|
|
133
|
+
},
|
|
94
134
|
{
|
|
95
135
|
id: "gpt-5.1-2025-11-13",
|
|
96
136
|
name: "GPT-5.1",
|
|
@@ -285,7 +325,17 @@ export function streamGitLabDuo(
|
|
|
285
325
|
|
|
286
326
|
const innerStream =
|
|
287
327
|
cfg.backend === "anthropic"
|
|
288
|
-
? streamSimpleAnthropic(
|
|
328
|
+
? streamSimpleAnthropic(
|
|
329
|
+
{
|
|
330
|
+
...(modelWithBaseUrl as Model<"anthropic-messages">),
|
|
331
|
+
compat: {
|
|
332
|
+
...(modelWithBaseUrl as Model<"anthropic-messages">).compat,
|
|
333
|
+
forceAdaptiveThinking: true,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
context,
|
|
337
|
+
streamOptions,
|
|
338
|
+
)
|
|
289
339
|
: streamSimpleOpenAIResponses(modelWithBaseUrl as Model<"openai-responses">, context, streamOptions);
|
|
290
340
|
|
|
291
341
|
for await (const event of innerStream) stream.push(event);
|
|
@@ -329,10 +379,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
329
379
|
baseUrl: AI_GATEWAY_URL,
|
|
330
380
|
apiKey: "$GITLAB_TOKEN",
|
|
331
381
|
api: "gitlab-duo-api",
|
|
332
|
-
models: MODELS.map(({ id, name, reasoning, input, cost, contextWindow, maxTokens }) => ({
|
|
382
|
+
models: MODELS.map(({ id, name, reasoning, thinkingLevelMap, input, cost, contextWindow, maxTokens }) => ({
|
|
333
383
|
id,
|
|
334
384
|
name,
|
|
335
385
|
reasoning,
|
|
386
|
+
thinkingLevelMap,
|
|
336
387
|
input,
|
|
337
388
|
cost,
|
|
338
389
|
contextWindow,
|
|
@@ -23,7 +23,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
23
23
|
description: "Play DOOM as an overlay. Q to pause and exit.",
|
|
24
24
|
|
|
25
25
|
handler: async (args, ctx) => {
|
|
26
|
-
if (
|
|
26
|
+
if (ctx.mode !== "tui") {
|
|
27
27
|
ctx.ui.notify("DOOM requires interactive mode", "error");
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
@@ -81,7 +81,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
81
81
|
pi.registerCommand("handoff", {
|
|
82
82
|
description: "Transfer context to a new focused session",
|
|
83
83
|
handler: async (args, ctx) => {
|
|
84
|
-
if (
|
|
84
|
+
if (ctx.mode !== "tui") {
|
|
85
85
|
ctx.ui.notify("handoff requires interactive mode", "error");
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
@@ -146,7 +146,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// No UI available (print mode, RPC, etc.)
|
|
149
|
-
if (
|
|
149
|
+
if (ctx.mode !== "tui") {
|
|
150
150
|
return {
|
|
151
151
|
result: { output: "(interactive commands require TUI)", exitCode: 1, cancelled: false, truncated: false },
|
|
152
152
|
};
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
* /overlay-sidepanel - Responsive sidepanel (hides when terminal < 100 cols)
|
|
16
16
|
* /overlay-toggle - Toggle visibility demo (demonstrates OverlayHandle.setHidden)
|
|
17
17
|
* /overlay-passive - Non-capturing overlay demo (passive info panel alongside active overlay)
|
|
18
|
-
* /overlay-focus - Focus cycling and rendering order with
|
|
18
|
+
* /overlay-focus - Focus cycling, input routing, dismissal, and rendering order with overlays
|
|
19
19
|
* /overlay-streaming - Multiple input panels with simulated streaming (Tab to cycle focus)
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@code-yeongyu/senpi";
|
|
23
23
|
import type { Component, OverlayAnchor, OverlayHandle, OverlayOptions, TUI } from "@earendil-works/pi-tui";
|
|
24
|
-
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
24
|
+
import { Input, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
25
25
|
import { spawn } from "child_process";
|
|
26
26
|
|
|
27
27
|
// Global handle for toggle demo (in real code, use a more elegant pattern)
|
|
@@ -272,9 +272,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
272
272
|
},
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
// Focus cycling demo - demonstrates focus(),
|
|
275
|
+
// Focus cycling demo - demonstrates focus(), input routing, per-panel dismissal, and rendering order
|
|
276
276
|
pi.registerCommand("overlay-focus", {
|
|
277
|
-
description: "Test focus cycling and rendering order with
|
|
277
|
+
description: "Test focus cycling, input routing, dismissal, and rendering order with overlays",
|
|
278
278
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
279
279
|
ctx.ui.setEditorText("");
|
|
280
280
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => new FocusDemoController(tui, theme, done), {
|
|
@@ -1031,57 +1031,66 @@ class TimerPanel extends BaseOverlay {
|
|
|
1031
1031
|
|
|
1032
1032
|
// === Focus cycling demo ===
|
|
1033
1033
|
|
|
1034
|
+
type FocusPanelColor = "error" | "success" | "accent";
|
|
1035
|
+
type FocusPanelConfig = { label: string; color: FocusPanelColor; options: OverlayOptions };
|
|
1036
|
+
type FocusPanelEntry = { panel: FocusPanel; handle: OverlayHandle };
|
|
1037
|
+
|
|
1038
|
+
const FOCUS_PANEL_CONFIGS = [
|
|
1039
|
+
{ label: "Alpha", color: "error", options: { row: 2, col: 4, width: 34 } },
|
|
1040
|
+
{ label: "Beta", color: "success", options: { row: 5, col: 28, width: 34 } },
|
|
1041
|
+
{ label: "Gamma", color: "accent", options: { row: 8, col: 52, width: 34 } },
|
|
1042
|
+
] satisfies FocusPanelConfig[];
|
|
1043
|
+
|
|
1034
1044
|
class FocusDemoController extends BaseOverlay {
|
|
1035
|
-
private tui: TUI;
|
|
1036
|
-
private
|
|
1037
|
-
private
|
|
1038
|
-
private
|
|
1039
|
-
private done: () => void;
|
|
1045
|
+
private readonly tui: TUI;
|
|
1046
|
+
private entries: FocusPanelEntry[] = [];
|
|
1047
|
+
private readonly done: () => void;
|
|
1048
|
+
private closed = false;
|
|
1040
1049
|
|
|
1041
1050
|
constructor(tui: TUI, theme: Theme, done: () => void) {
|
|
1042
1051
|
super(theme);
|
|
1043
1052
|
this.tui = tui;
|
|
1044
1053
|
this.done = done;
|
|
1045
|
-
const colors = ["error", "success", "accent"] as const;
|
|
1046
|
-
const labels = ["Alpha", "Beta", "Gamma"];
|
|
1047
1054
|
|
|
1048
|
-
for (
|
|
1049
|
-
const panel = new FocusPanel(
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
colors[i]!,
|
|
1053
|
-
() => this.cycleFocus(),
|
|
1054
|
-
() => this.close(),
|
|
1055
|
-
);
|
|
1056
|
-
const handle = this.tui.showOverlay(panel, {
|
|
1057
|
-
nonCapturing: true,
|
|
1058
|
-
row: 2,
|
|
1059
|
-
col: 5 + i * 6,
|
|
1060
|
-
width: 28,
|
|
1061
|
-
});
|
|
1062
|
-
panel.handle = handle;
|
|
1063
|
-
this.panels.push(panel);
|
|
1064
|
-
this.handles.push(handle);
|
|
1055
|
+
for (const config of FOCUS_PANEL_CONFIGS) {
|
|
1056
|
+
const panel = new FocusPanel({ theme, config, controller: this });
|
|
1057
|
+
const handle = this.tui.showOverlay(panel, { nonCapturing: true, ...config.options });
|
|
1058
|
+
this.entries.push({ panel, handle });
|
|
1065
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
this.focusFirstOpenPanel();
|
|
1066
1062
|
}
|
|
1067
1063
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1064
|
+
focusNext(current: FocusPanel, direction: 1 | -1 = 1): void {
|
|
1065
|
+
const openEntries = this.openEntries();
|
|
1066
|
+
const currentOpenPosition = openEntries.findIndex((entry) => entry.panel === current);
|
|
1067
|
+
if (currentOpenPosition === -1) throw new Error(`Panel ${current.label} is not open`);
|
|
1068
|
+
const nextOpenPosition = (currentOpenPosition + direction + openEntries.length) % openEntries.length;
|
|
1069
|
+
this.focusEntryAt(openEntries, nextOpenPosition);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
dismiss(panel: FocusPanel): void {
|
|
1073
|
+
const openEntries = this.openEntries();
|
|
1074
|
+
const currentOpenPosition = openEntries.findIndex((candidate) => candidate.panel === panel);
|
|
1075
|
+
if (currentOpenPosition === -1) return;
|
|
1076
|
+
const entry = openEntries[currentOpenPosition];
|
|
1077
|
+
if (!entry) throw new Error(`Invalid focus panel index ${currentOpenPosition}`);
|
|
1078
|
+
const remainingEntries = openEntries.filter((candidate) => candidate.panel !== panel);
|
|
1079
|
+
|
|
1080
|
+
entry.panel.closed = true;
|
|
1081
|
+
entry.handle.hide();
|
|
1082
|
+
if (remainingEntries.length === 0) {
|
|
1083
|
+
this.close();
|
|
1084
|
+
return;
|
|
1077
1085
|
}
|
|
1078
|
-
|
|
1086
|
+
|
|
1087
|
+
this.focusEntryAt(remainingEntries, currentOpenPosition % remainingEntries.length);
|
|
1079
1088
|
}
|
|
1080
1089
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
this.
|
|
1084
|
-
this.
|
|
1090
|
+
close(): void {
|
|
1091
|
+
if (this.closed) return;
|
|
1092
|
+
this.closed = true;
|
|
1093
|
+
this.hidePanels();
|
|
1085
1094
|
this.done();
|
|
1086
1095
|
}
|
|
1087
1096
|
|
|
@@ -1089,86 +1098,148 @@ class FocusDemoController extends BaseOverlay {
|
|
|
1089
1098
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
1090
1099
|
this.close();
|
|
1091
1100
|
} else if (matchesKey(data, "tab")) {
|
|
1092
|
-
this.
|
|
1101
|
+
this.focusFirstOpenPanel();
|
|
1093
1102
|
}
|
|
1094
1103
|
}
|
|
1095
1104
|
|
|
1096
1105
|
render(width: number): string[] {
|
|
1097
1106
|
const th = this.theme;
|
|
1098
|
-
const focused = this.
|
|
1107
|
+
const focused = this.entries.find((entry) => entry.handle.isFocused())?.panel.label ?? "Controller";
|
|
1099
1108
|
return this.box(
|
|
1100
1109
|
[
|
|
1101
1110
|
"",
|
|
1102
1111
|
` Current focus: ${th.fg("accent", focused)}`,
|
|
1103
1112
|
"",
|
|
1104
1113
|
" Three overlapping panels above are",
|
|
1105
|
-
`
|
|
1106
|
-
"
|
|
1114
|
+
` ${th.fg("accent", "nonCapturing")} overlays controlled with`,
|
|
1115
|
+
" raw OverlayHandle.focus()/hide().",
|
|
1107
1116
|
"",
|
|
1108
|
-
"
|
|
1109
|
-
"
|
|
1117
|
+
" Type in the focused panel's input.",
|
|
1118
|
+
" Focused panel renders on top.",
|
|
1110
1119
|
"",
|
|
1111
|
-
th.fg("dim", " Tab = cycle
|
|
1120
|
+
th.fg("dim", " Tab/Shift+Tab = cycle panels"),
|
|
1121
|
+
th.fg("dim", " Esc/Ctrl+D = dismiss panel"),
|
|
1122
|
+
th.fg("dim", " Ctrl+C = close all"),
|
|
1112
1123
|
"",
|
|
1113
1124
|
],
|
|
1114
1125
|
width,
|
|
1115
|
-
"Focus Demo",
|
|
1126
|
+
"Focus + Input Demo",
|
|
1116
1127
|
);
|
|
1117
1128
|
}
|
|
1118
1129
|
|
|
1119
1130
|
override dispose(): void {
|
|
1120
|
-
|
|
1131
|
+
if (this.closed) return;
|
|
1132
|
+
this.closed = true;
|
|
1133
|
+
this.hidePanels();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private focusFirstOpenPanel(): void {
|
|
1137
|
+
const firstOpen = this.openEntries()[0];
|
|
1138
|
+
if (firstOpen) {
|
|
1139
|
+
firstOpen.handle.focus();
|
|
1140
|
+
this.tui.requestRender();
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
private focusEntryAt(entries: FocusPanelEntry[], index: number): void {
|
|
1145
|
+
const entry = entries[index];
|
|
1146
|
+
if (!entry) throw new Error(`Invalid focus panel index ${index}`);
|
|
1147
|
+
entry.handle.focus();
|
|
1148
|
+
this.tui.requestRender();
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
private hidePanels(): void {
|
|
1152
|
+
for (const entry of this.entries) {
|
|
1153
|
+
if (!entry.panel.closed) {
|
|
1154
|
+
entry.panel.closed = true;
|
|
1155
|
+
entry.handle.hide();
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
this.entries = [];
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
private openEntries(): FocusPanelEntry[] {
|
|
1162
|
+
return this.entries.filter((entry) => !entry.panel.closed);
|
|
1121
1163
|
}
|
|
1122
1164
|
}
|
|
1123
1165
|
|
|
1124
1166
|
class FocusPanel extends BaseOverlay {
|
|
1125
|
-
|
|
1167
|
+
focused = false;
|
|
1168
|
+
closed = false;
|
|
1126
1169
|
readonly label: string;
|
|
1127
|
-
private color:
|
|
1128
|
-
private
|
|
1129
|
-
private
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1170
|
+
private readonly color: FocusPanelColor;
|
|
1171
|
+
private readonly controller: FocusDemoController;
|
|
1172
|
+
private readonly input = new Input();
|
|
1173
|
+
private inputs: string[] = [];
|
|
1174
|
+
|
|
1175
|
+
constructor({
|
|
1176
|
+
theme,
|
|
1177
|
+
config,
|
|
1178
|
+
controller,
|
|
1179
|
+
}: {
|
|
1180
|
+
theme: Theme;
|
|
1181
|
+
config: FocusPanelConfig;
|
|
1182
|
+
controller: FocusDemoController;
|
|
1183
|
+
}) {
|
|
1138
1184
|
super(theme);
|
|
1139
|
-
this.label = label;
|
|
1140
|
-
this.color = color;
|
|
1141
|
-
this.
|
|
1142
|
-
this.onClose = onClose;
|
|
1185
|
+
this.label = config.label;
|
|
1186
|
+
this.color = config.color;
|
|
1187
|
+
this.controller = controller;
|
|
1143
1188
|
}
|
|
1144
1189
|
|
|
1145
1190
|
handleInput(data: string): void {
|
|
1146
1191
|
if (matchesKey(data, "tab")) {
|
|
1147
|
-
this.
|
|
1148
|
-
} else if (matchesKey(data, "
|
|
1149
|
-
this.
|
|
1192
|
+
this.controller.focusNext(this);
|
|
1193
|
+
} else if (matchesKey(data, "shift+tab")) {
|
|
1194
|
+
this.controller.focusNext(this, -1);
|
|
1195
|
+
} else if (matchesKey(data, "escape") || matchesKey(data, "ctrl+d")) {
|
|
1196
|
+
this.controller.dismiss(this);
|
|
1197
|
+
} else if (matchesKey(data, "ctrl+c")) {
|
|
1198
|
+
this.controller.close();
|
|
1199
|
+
} else if (matchesKey(data, "return")) {
|
|
1200
|
+
this.inputs.push("Enter");
|
|
1201
|
+
} else if (matchesKey(data, "up")) {
|
|
1202
|
+
this.inputs.push("↑");
|
|
1203
|
+
} else if (matchesKey(data, "down")) {
|
|
1204
|
+
this.inputs.push("↓");
|
|
1205
|
+
} else if (matchesKey(data, "left")) {
|
|
1206
|
+
this.input.handleInput(data);
|
|
1207
|
+
this.inputs.push("←");
|
|
1208
|
+
} else if (matchesKey(data, "right")) {
|
|
1209
|
+
this.input.handleInput(data);
|
|
1210
|
+
this.inputs.push("→");
|
|
1211
|
+
} else if (matchesKey(data, "backspace")) {
|
|
1212
|
+
this.input.handleInput(data);
|
|
1213
|
+
this.inputs.push("Backspace");
|
|
1214
|
+
} else {
|
|
1215
|
+
this.input.handleInput(data);
|
|
1216
|
+
this.inputs.push(JSON.stringify(data));
|
|
1150
1217
|
}
|
|
1151
1218
|
}
|
|
1152
1219
|
|
|
1153
1220
|
render(width: number): string[] {
|
|
1154
1221
|
const th = this.theme;
|
|
1155
|
-
const focused = this.handle?.isFocused() ?? false;
|
|
1156
1222
|
const innerW = Math.max(1, width - 2);
|
|
1157
|
-
const border = (c: string) => th.fg(this.color, c);
|
|
1223
|
+
const border = (c: string) => th.fg(this.focused ? this.color : "dim", c);
|
|
1158
1224
|
const padLine = (s: string) => truncateToWidth(s, innerW, "...", true);
|
|
1225
|
+
const recent = this.inputs.length === 0 ? "(none)" : this.inputs.slice(-6).join(" ");
|
|
1159
1226
|
const lines: string[] = [];
|
|
1160
1227
|
|
|
1228
|
+
this.input.focused = this.focused;
|
|
1229
|
+
const [inputLine = ""] = this.input.render(Math.max(1, innerW - 8));
|
|
1161
1230
|
lines.push(border(`╭${"─".repeat(innerW)}╮`));
|
|
1162
|
-
lines.push(
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
lines.push(border("│") + padLine(th.fg("dim", " (passive)")) + border("│"));
|
|
1170
|
-
}
|
|
1231
|
+
lines.push(
|
|
1232
|
+
border("│") +
|
|
1233
|
+
padLine(
|
|
1234
|
+
` ${th.fg(this.color, this.label)} ${this.focused ? th.fg("success", "FOCUSED") : th.fg("dim", "visible")}`,
|
|
1235
|
+
) +
|
|
1236
|
+
border("│"),
|
|
1237
|
+
);
|
|
1171
1238
|
lines.push(border("│") + padLine("") + border("│"));
|
|
1239
|
+
lines.push(border("│") + padLine(` Input: ${inputLine}`) + border("│"));
|
|
1240
|
+
lines.push(border("│") + padLine(` Keys: ${recent}`) + border("│"));
|
|
1241
|
+
lines.push(border("│") + padLine(th.fg("dim", " Tab/Shift+Tab focus")) + border("│"));
|
|
1242
|
+
lines.push(border("│") + padLine(th.fg("dim", " Esc/Ctrl+D dismiss")) + border("│"));
|
|
1172
1243
|
lines.push(border(`╰${"─".repeat(innerW)}╯`));
|
|
1173
1244
|
|
|
1174
1245
|
return lines;
|
|
@@ -31,7 +31,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
31
31
|
pi.registerCommand("qna", {
|
|
32
32
|
description: "Extract questions from last assistant message into editor",
|
|
33
33
|
handler: async (_args, ctx) => {
|
|
34
|
-
if (
|
|
34
|
+
if (ctx.mode !== "tui") {
|
|
35
35
|
ctx.ui.notify("qna requires interactive mode", "error");
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
@@ -41,7 +41,7 @@ export default function question(pi: ExtensionAPI) {
|
|
|
41
41
|
parameters: QuestionParams,
|
|
42
42
|
|
|
43
43
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
44
|
-
if (
|
|
44
|
+
if (ctx.mode !== "tui") {
|
|
45
45
|
return {
|
|
46
46
|
content: [{ type: "text", text: "Error: UI not available (running in non-interactive mode)" }],
|
|
47
47
|
details: {
|