@esso0428/pi-subagents 0.17.22 → 0.17.24
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 +10 -0
- package/package.json +1 -1
- package/src/index.ts +39 -24
- package/src/ui/agent-widget.ts +25 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.17.24] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Restored agents roster ↑/↓ selection and stopped it stealing keys from this extension's modals**: v0.17.23 replaced the widget's focus probe with a structural check for the prompt editor, which broke roster navigation outright. The probe is unworkable in general — pi can replace the focused editor through `ctx.ui.setEditorComponent()` (pi-tmux-cursor-focus wraps it whenever pi runs under tmux), and an extension may also resolve its own copy of `@earendil-works/pi-tui` at a different version than the host's, so neither `instanceof` nor a structural test can identify it. The widget now tracks the state it actually controls: every `ctx.ui.select`, `ctx.ui.editor`, and `ctx.ui.custom` call in this extension runs inside `withModal()`, which suspends the widget's global terminal listener for as long as the modal holds the keyboard. Roster selection works again, and open modals receive `↑`/`↓`/`enter`/`escape` unimpeded.
|
|
14
|
+
|
|
15
|
+
## [0.17.23] - 2026-10-02
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Stopped the agents roster from stealing keys from open modals**: the widget's focus detection used `focused instanceof Editor`, which is always false when the extension resolves its own copy of `@earendil-works/pi-tui` at a different version than pi's host (for example a hoisted peer pinned to an older release). The check silently degraded to "focused is null", so the widget's global `onTerminalInput` listener claimed the keyboard during teardown and transient render states and consumed `↑`/`↓`/`enter`/`escape` from modals such as the `/agents` menus. Focus detection now identifies the prompt editor structurally and fails closed, so the listener only claims input while the editor provably owns it.
|
|
19
|
+
|
|
10
20
|
## [0.17.22] - 2026-10-01
|
|
11
21
|
|
|
12
22
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esso0428/pi-subagents",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.24",
|
|
4
4
|
"description": "A pi extension that brings smart Claude Code-style autonomous sub-agents to pi, with npm:pi-subagents-style JSON agent overrides.",
|
|
5
5
|
"author": "ESSO0428",
|
|
6
6
|
"repository": {
|
package/src/index.ts
CHANGED
|
@@ -674,6 +674,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
674
674
|
},
|
|
675
675
|
},
|
|
676
676
|
);
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Run an AgentWidget-owned modal while the widget's global key listener
|
|
680
|
+
* stands down. The listener runs before the focused component, so without
|
|
681
|
+
* this it would consume ↑/↓/enter/escape from the modal itself.
|
|
682
|
+
*/
|
|
683
|
+
async function withModal<T>(open: () => Promise<T>): Promise<T> {
|
|
684
|
+
widget.setModalOpen(true);
|
|
685
|
+
try {
|
|
686
|
+
return await open();
|
|
687
|
+
} finally {
|
|
688
|
+
widget.setModalOpen(false);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
677
692
|
function setWidgetMode(m: WidgetMode): void {
|
|
678
693
|
widgetMode = m;
|
|
679
694
|
widget.update();
|
|
@@ -1845,7 +1860,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1845
1860
|
ctx.ui.notify(noAgentsMsg, "info");
|
|
1846
1861
|
}
|
|
1847
1862
|
|
|
1848
|
-
const choice = await ctx.ui.select("Agents", options);
|
|
1863
|
+
const choice = await withModal(() => ctx.ui.select("Agents", options));
|
|
1849
1864
|
if (!choice) return;
|
|
1850
1865
|
|
|
1851
1866
|
if (choice.startsWith("Running agents (")) {
|
|
@@ -1909,7 +1924,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1909
1924
|
if (hasCustom) legendParts.push("• = project ◦ = global");
|
|
1910
1925
|
if (hasDisabled) legendParts.push("✕ = disabled");
|
|
1911
1926
|
|
|
1912
|
-
const selected = await ctx.ui.custom<string | undefined>((_tui, _theme, _kb, done) => {
|
|
1927
|
+
const selected = await withModal(() => ctx.ui.custom<string | undefined>((_tui, _theme, _kb, done) => {
|
|
1913
1928
|
const slTheme = getSettingsListTheme();
|
|
1914
1929
|
const list = new SettingsList(
|
|
1915
1930
|
items,
|
|
@@ -1928,7 +1943,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1928
1943
|
invalidate: () => container.invalidate(),
|
|
1929
1944
|
handleInput: (data: string) => list.handleInput?.(data),
|
|
1930
1945
|
};
|
|
1931
|
-
});
|
|
1946
|
+
}));
|
|
1932
1947
|
|
|
1933
1948
|
if (selected && getAgentConfig(selected)) {
|
|
1934
1949
|
await showAgentDetail(ctx, selected);
|
|
@@ -1978,7 +1993,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1978
1993
|
}
|
|
1979
1994
|
};
|
|
1980
1995
|
|
|
1981
|
-
const choice = await ctx.ui.custom<string | undefined>((_tui, _theme, _kb, done) => {
|
|
1996
|
+
const choice = await withModal(() => ctx.ui.custom<string | undefined>((_tui, _theme, _kb, done) => {
|
|
1982
1997
|
const list = new SelectList(
|
|
1983
1998
|
options,
|
|
1984
1999
|
Math.min(options.length, 10),
|
|
@@ -2003,7 +2018,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2003
2018
|
invalidate: () => container.invalidate(),
|
|
2004
2019
|
handleInput: (data: string) => list.handleInput(data),
|
|
2005
2020
|
};
|
|
2006
|
-
});
|
|
2021
|
+
}));
|
|
2007
2022
|
|
|
2008
2023
|
if (!choice) return undefined;
|
|
2009
2024
|
return pairs.find(({ record }) => record.id === choice)?.record;
|
|
@@ -2082,7 +2097,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2082
2097
|
|
|
2083
2098
|
const activity = agentActivity.get(record.id);
|
|
2084
2099
|
const isLive = mode === "live";
|
|
2085
|
-
await ctx.ui.custom<undefined>(
|
|
2100
|
+
await withModal(() => ctx.ui.custom<undefined>(
|
|
2086
2101
|
(tui, theme, keybindings, done) => {
|
|
2087
2102
|
return new ConversationViewer(tui, session, record, activity, theme, done,
|
|
2088
2103
|
isLive ? () => {
|
|
@@ -2098,7 +2113,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2098
2113
|
overlay: true,
|
|
2099
2114
|
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
|
2100
2115
|
},
|
|
2101
|
-
);
|
|
2116
|
+
));
|
|
2102
2117
|
}
|
|
2103
2118
|
|
|
2104
2119
|
async function showAgentDetail(ctx: ExtensionCommandContext, name: string) {
|
|
@@ -2129,12 +2144,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2129
2144
|
menuOptions = ["Edit", "Disable", "Delete", "Back"];
|
|
2130
2145
|
}
|
|
2131
2146
|
|
|
2132
|
-
const choice = await ctx.ui.select(name, menuOptions);
|
|
2147
|
+
const choice = await withModal(() => ctx.ui.select(name, menuOptions));
|
|
2133
2148
|
if (!choice || choice === "Back") return;
|
|
2134
2149
|
|
|
2135
2150
|
if (choice === "Edit" && file) {
|
|
2136
2151
|
const content = readFileSync(file.path, "utf-8");
|
|
2137
|
-
const edited = await ctx.ui.editor(`Edit ${name}`, content);
|
|
2152
|
+
const edited = await withModal(() => ctx.ui.editor(`Edit ${name}`, content));
|
|
2138
2153
|
if (edited !== undefined && edited !== content) {
|
|
2139
2154
|
const { writeFileSync } = await import("node:fs");
|
|
2140
2155
|
writeFileSync(file.path, edited, "utf-8");
|
|
@@ -2168,10 +2183,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2168
2183
|
|
|
2169
2184
|
/** Eject a default agent: write its embedded config as a .md file. */
|
|
2170
2185
|
async function ejectAgent(ctx: ExtensionCommandContext, name: string, cfg: AgentConfig) {
|
|
2171
|
-
const location = await ctx.ui.select("Choose location", [
|
|
2186
|
+
const location = await withModal(() => ctx.ui.select("Choose location", [
|
|
2172
2187
|
"Project (.pi/agents/)",
|
|
2173
2188
|
`Personal (${personalAgentsDir()})`,
|
|
2174
|
-
]);
|
|
2189
|
+
]));
|
|
2175
2190
|
if (!location) return;
|
|
2176
2191
|
|
|
2177
2192
|
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
@@ -2232,10 +2247,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2232
2247
|
}
|
|
2233
2248
|
|
|
2234
2249
|
// No file (built-in default) — create a stub
|
|
2235
|
-
const location = await ctx.ui.select("Choose location", [
|
|
2250
|
+
const location = await withModal(() => ctx.ui.select("Choose location", [
|
|
2236
2251
|
"Project (.pi/agents/)",
|
|
2237
2252
|
`Personal (${personalAgentsDir()})`,
|
|
2238
|
-
]);
|
|
2253
|
+
]));
|
|
2239
2254
|
if (!location) return;
|
|
2240
2255
|
|
|
2241
2256
|
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
@@ -2270,18 +2285,18 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2270
2285
|
}
|
|
2271
2286
|
|
|
2272
2287
|
async function showCreateWizard(ctx: ExtensionCommandContext) {
|
|
2273
|
-
const location = await ctx.ui.select("Choose location", [
|
|
2288
|
+
const location = await withModal(() => ctx.ui.select("Choose location", [
|
|
2274
2289
|
"Project (.pi/agents/)",
|
|
2275
2290
|
`Personal (${personalAgentsDir()})`,
|
|
2276
|
-
]);
|
|
2291
|
+
]));
|
|
2277
2292
|
if (!location) return;
|
|
2278
2293
|
|
|
2279
2294
|
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
2280
2295
|
|
|
2281
|
-
const method = await ctx.ui.select("Creation method", [
|
|
2296
|
+
const method = await withModal(() => ctx.ui.select("Creation method", [
|
|
2282
2297
|
"Generate with Claude (recommended)",
|
|
2283
2298
|
"Manual configuration",
|
|
2284
|
-
]);
|
|
2299
|
+
]));
|
|
2285
2300
|
if (!method) return;
|
|
2286
2301
|
|
|
2287
2302
|
if (method.startsWith("Generate")) {
|
|
@@ -2377,7 +2392,7 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
2377
2392
|
if (!description) return;
|
|
2378
2393
|
|
|
2379
2394
|
// 3. Tools
|
|
2380
|
-
const toolChoice = await ctx.ui.select("Tools", ["all", "none", "read-only (read, bash, grep, find, ls)", "custom..."]);
|
|
2395
|
+
const toolChoice = await withModal(() => ctx.ui.select("Tools", ["all", "none", "read-only (read, bash, grep, find, ls)", "custom..."]));
|
|
2381
2396
|
if (!toolChoice) return;
|
|
2382
2397
|
|
|
2383
2398
|
let tools: string;
|
|
@@ -2394,13 +2409,13 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
2394
2409
|
}
|
|
2395
2410
|
|
|
2396
2411
|
// 4. Model
|
|
2397
|
-
const modelChoice = await ctx.ui.select("Model", [
|
|
2412
|
+
const modelChoice = await withModal(() => ctx.ui.select("Model", [
|
|
2398
2413
|
"inherit (parent model)",
|
|
2399
2414
|
"haiku",
|
|
2400
2415
|
"sonnet",
|
|
2401
2416
|
"opus",
|
|
2402
2417
|
"custom...",
|
|
2403
|
-
]);
|
|
2418
|
+
]));
|
|
2404
2419
|
if (!modelChoice) return;
|
|
2405
2420
|
|
|
2406
2421
|
let modelLine = "";
|
|
@@ -2414,14 +2429,14 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
2414
2429
|
|
|
2415
2430
|
// 5. Thinking
|
|
2416
2431
|
// "inherit" is a UI-only pseudo-choice (omit the field); the rest mirror pi.
|
|
2417
|
-
const thinkingChoice = await ctx.ui.select("Thinking level", ["inherit", ...THINKING_LEVELS]);
|
|
2432
|
+
const thinkingChoice = await withModal(() => ctx.ui.select("Thinking level", ["inherit", ...THINKING_LEVELS]));
|
|
2418
2433
|
if (!thinkingChoice) return;
|
|
2419
2434
|
|
|
2420
2435
|
let thinkingLine = "";
|
|
2421
2436
|
if (thinkingChoice !== "inherit") thinkingLine = `\nthinking: ${thinkingChoice}`;
|
|
2422
2437
|
|
|
2423
2438
|
// 6. System prompt
|
|
2424
|
-
const systemPrompt = await ctx.ui.editor("System prompt", "");
|
|
2439
|
+
const systemPrompt = await withModal(() => ctx.ui.editor("System prompt", ""));
|
|
2425
2440
|
if (systemPrompt === undefined) return;
|
|
2426
2441
|
|
|
2427
2442
|
// Build the file
|
|
@@ -2610,7 +2625,7 @@ ${systemPrompt}
|
|
|
2610
2625
|
// Updated on arrow keys so Enter knows which field is selected immediately.
|
|
2611
2626
|
let currentIndex = 0;
|
|
2612
2627
|
|
|
2613
|
-
const result = await ctx.ui.custom<string | undefined>((_tui, _theme, _kb, done) => {
|
|
2628
|
+
const result = await withModal(() => ctx.ui.custom<string | undefined>((_tui, _theme, _kb, done) => {
|
|
2614
2629
|
const items = buildItems();
|
|
2615
2630
|
|
|
2616
2631
|
list = new SettingsList(
|
|
@@ -2647,7 +2662,7 @@ ${systemPrompt}
|
|
|
2647
2662
|
list.handleInput?.(data);
|
|
2648
2663
|
},
|
|
2649
2664
|
};
|
|
2650
|
-
});
|
|
2665
|
+
}));
|
|
2651
2666
|
|
|
2652
2667
|
// If a numeric field ID was returned, prompt for typed input
|
|
2653
2668
|
if (result && NUMERIC_IDS.has(result)) {
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Uses the callback form of setWidget for themed rendering.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { AgentManager } from "../agent-manager.js";
|
|
10
10
|
import { getConfig } from "../agent-types.js";
|
|
11
11
|
import type { AgentInvocation, AgentRecord, SubagentType, WidgetMode } from "../types.js";
|
|
@@ -247,6 +247,8 @@ export class AgentWidget {
|
|
|
247
247
|
private inputUnsub: (() => void) | undefined;
|
|
248
248
|
/** Whether arrow keys currently navigate the agent roster. */
|
|
249
249
|
private navigationActive = false;
|
|
250
|
+
/** Depth of AgentWidget-owned modals currently holding the keyboard. */
|
|
251
|
+
private modalDepth = 0;
|
|
250
252
|
/** Stable identity of the selected row, so roster changes do not jump selection. */
|
|
251
253
|
private selectedAgentId: string | undefined;
|
|
252
254
|
/** Last logical roster index of the selected row, used when it disappears. */
|
|
@@ -373,10 +375,26 @@ export class AgentWidget {
|
|
|
373
375
|
return [...running, ...queued, ...finished];
|
|
374
376
|
}
|
|
375
377
|
|
|
376
|
-
/**
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Track an AgentWidget-owned modal (the `/agents` menus and friends).
|
|
380
|
+
*
|
|
381
|
+
* The widget's terminal listener runs before the focused component, so while
|
|
382
|
+
* a modal is open the listener must not claim ↑/↓/enter/escape. Whether pi's
|
|
383
|
+
* prompt editor currently owns the keyboard is deliberately not consulted:
|
|
384
|
+
* pi can replace it through `ctx.ui.setEditorComponent()` (for example
|
|
385
|
+
* pi-tmux-cursor-focus wraps it under tmux), and the extension may also
|
|
386
|
+
* resolve its own copy of `@earendil-works/pi-tui` at a different version
|
|
387
|
+
* than the host's. Neither `instanceof` nor a structural probe can identify
|
|
388
|
+
* the editor reliably in that environment, and a wrong answer silently
|
|
389
|
+
* disables roster navigation entirely.
|
|
390
|
+
*/
|
|
391
|
+
setModalOpen(open: boolean): void {
|
|
392
|
+
this.modalDepth = open ? this.modalDepth + 1 : Math.max(0, this.modalDepth - 1);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** True when no AgentWidget-owned modal is holding the keyboard. */
|
|
396
|
+
private keyboardAvailable(): boolean {
|
|
397
|
+
return this.modalDepth === 0;
|
|
380
398
|
}
|
|
381
399
|
|
|
382
400
|
private selectedIndexOf(records: readonly AgentRecord[]): number {
|
|
@@ -418,7 +436,7 @@ export class AgentWidget {
|
|
|
418
436
|
if (records.length === 0 || !ui) return false;
|
|
419
437
|
|
|
420
438
|
if (!this.navigationActive) {
|
|
421
|
-
if (direction !== 1 || !this.
|
|
439
|
+
if (direction !== 1 || !this.keyboardAvailable() || (ui.getEditorText?.() ?? "") !== "") return false;
|
|
422
440
|
this.activate(records);
|
|
423
441
|
return true;
|
|
424
442
|
}
|
|
@@ -446,7 +464,7 @@ export class AgentWidget {
|
|
|
446
464
|
/** Handle terminal input before it reaches the focused prompt editor. */
|
|
447
465
|
handleKey(data: string): { consume?: boolean; data?: string } | undefined {
|
|
448
466
|
if (!this.uiCtx || isKeyRelease(data)) return undefined;
|
|
449
|
-
if (!this.
|
|
467
|
+
if (!this.keyboardAvailable()) {
|
|
450
468
|
if (this.navigationActive) this.deactivate();
|
|
451
469
|
return undefined;
|
|
452
470
|
}
|