@esso0428/pi-subagents 0.17.23 → 0.17.25
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 +52 -37
- package/src/ui/agent-widget.ts +21 -28
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.25] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Suspended the roster key listener for the remaining widget-owned dialogs**: v0.17.24 routed `ctx.ui.select`, `ctx.ui.editor`, and `ctx.ui.custom` through `withModal()` but left `ctx.ui.input` and `ctx.ui.confirm` outside it. Those dialogs are reached only after a wrapped modal has already cleared roster navigation, so the listener could still swallow a stray `↓`, and any future path that opens one directly would lose `escape`/`↑`/`↓`/`enter` entirely. Every remaining modal call now runs inside `withModal()` as well.
|
|
14
|
+
|
|
15
|
+
## [0.17.24] - 2026-10-02
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **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.
|
|
19
|
+
|
|
10
20
|
## [0.17.23] - 2026-10-02
|
|
11
21
|
|
|
12
22
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esso0428/pi-subagents",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.25",
|
|
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");
|
|
@@ -2143,7 +2158,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2143
2158
|
}
|
|
2144
2159
|
} else if (choice === "Delete") {
|
|
2145
2160
|
if (file) {
|
|
2146
|
-
const confirmed = await ctx.ui.confirm("Delete agent", `Delete ${name} from ${file.location} (${file.path})?`);
|
|
2161
|
+
const confirmed = await withModal(() => ctx.ui.confirm("Delete agent", `Delete ${name} from ${file.location} (${file.path})?`));
|
|
2147
2162
|
if (confirmed) {
|
|
2148
2163
|
unlinkSync(file.path);
|
|
2149
2164
|
reloadCustomAgents();
|
|
@@ -2151,7 +2166,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2151
2166
|
}
|
|
2152
2167
|
}
|
|
2153
2168
|
} else if (choice === "Reset to default" && file) {
|
|
2154
|
-
const confirmed = await ctx.ui.confirm("Reset to default", `Delete override ${file.path} and restore embedded default?`);
|
|
2169
|
+
const confirmed = await withModal(() => ctx.ui.confirm("Reset to default", `Delete override ${file.path} and restore embedded default?`));
|
|
2155
2170
|
if (confirmed) {
|
|
2156
2171
|
unlinkSync(file.path);
|
|
2157
2172
|
reloadCustomAgents();
|
|
@@ -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();
|
|
@@ -2179,7 +2194,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2179
2194
|
|
|
2180
2195
|
const targetPath = join(targetDir, `${name}.md`);
|
|
2181
2196
|
if (existsSync(targetPath)) {
|
|
2182
|
-
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
2197
|
+
const overwrite = await withModal(() => ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`));
|
|
2183
2198
|
if (!overwrite) return;
|
|
2184
2199
|
}
|
|
2185
2200
|
|
|
@@ -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")) {
|
|
@@ -2292,17 +2307,17 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2292
2307
|
}
|
|
2293
2308
|
|
|
2294
2309
|
async function showGenerateWizard(ctx: ExtensionCommandContext, targetDir: string) {
|
|
2295
|
-
const description = await ctx.ui.input("Describe what this agent should do");
|
|
2310
|
+
const description = await withModal(() => ctx.ui.input("Describe what this agent should do"));
|
|
2296
2311
|
if (!description) return;
|
|
2297
2312
|
|
|
2298
|
-
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
2313
|
+
const name = await withModal(() => ctx.ui.input("Agent name (filename, no spaces)"));
|
|
2299
2314
|
if (!name) return;
|
|
2300
2315
|
|
|
2301
2316
|
mkdirSync(targetDir, { recursive: true });
|
|
2302
2317
|
|
|
2303
2318
|
const targetPath = join(targetDir, `${name}.md`);
|
|
2304
2319
|
if (existsSync(targetPath)) {
|
|
2305
|
-
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
2320
|
+
const overwrite = await withModal(() => ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`));
|
|
2306
2321
|
if (!overwrite) return;
|
|
2307
2322
|
}
|
|
2308
2323
|
|
|
@@ -2369,15 +2384,15 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
2369
2384
|
|
|
2370
2385
|
async function showManualWizard(ctx: ExtensionCommandContext, targetDir: string) {
|
|
2371
2386
|
// 1. Name
|
|
2372
|
-
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
2387
|
+
const name = await withModal(() => ctx.ui.input("Agent name (filename, no spaces)"));
|
|
2373
2388
|
if (!name) return;
|
|
2374
2389
|
|
|
2375
2390
|
// 2. Description
|
|
2376
|
-
const description = await ctx.ui.input("Description (one line)");
|
|
2391
|
+
const description = await withModal(() => ctx.ui.input("Description (one line)"));
|
|
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;
|
|
@@ -2388,19 +2403,19 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
2388
2403
|
} else if (toolChoice.startsWith("read-only")) {
|
|
2389
2404
|
tools = "read, bash, grep, find, ls";
|
|
2390
2405
|
} else {
|
|
2391
|
-
const customTools = await ctx.ui.input("Tools (comma-separated)", BUILTIN_TOOL_NAMES.join(", "));
|
|
2406
|
+
const customTools = await withModal(() => ctx.ui.input("Tools (comma-separated)", BUILTIN_TOOL_NAMES.join(", ")));
|
|
2392
2407
|
if (!customTools) return;
|
|
2393
2408
|
tools = customTools;
|
|
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 = "";
|
|
@@ -2408,20 +2423,20 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
2408
2423
|
else if (modelChoice === "sonnet") modelLine = "\nmodel: anthropic/claude-sonnet-4-6";
|
|
2409
2424
|
else if (modelChoice === "opus") modelLine = "\nmodel: anthropic/claude-opus-4-6";
|
|
2410
2425
|
else if (modelChoice === "custom...") {
|
|
2411
|
-
const customModel = await ctx.ui.input("Model (provider/modelId)");
|
|
2426
|
+
const customModel = await withModal(() => ctx.ui.input("Model (provider/modelId)"));
|
|
2412
2427
|
if (customModel) modelLine = `\nmodel: ${customModel}`;
|
|
2413
2428
|
}
|
|
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
|
|
@@ -2438,7 +2453,7 @@ ${systemPrompt}
|
|
|
2438
2453
|
const targetPath = join(targetDir, `${name}.md`);
|
|
2439
2454
|
|
|
2440
2455
|
if (existsSync(targetPath)) {
|
|
2441
|
-
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
2456
|
+
const overwrite = await withModal(() => ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`));
|
|
2442
2457
|
if (!overwrite) return;
|
|
2443
2458
|
}
|
|
2444
2459
|
|
|
@@ -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)) {
|
|
@@ -2665,7 +2680,7 @@ ${systemPrompt}
|
|
|
2665
2680
|
|
|
2666
2681
|
// Loop until user enters a valid integer or cancels (Esc / null).
|
|
2667
2682
|
// Silently trims whitespace; rejects non-numeric input by re-prompting.
|
|
2668
|
-
let input: string | undefined = await ctx.ui.input(label, current);
|
|
2683
|
+
let input: string | undefined = await withModal(() => ctx.ui.input(label, current));
|
|
2669
2684
|
while (input != null) {
|
|
2670
2685
|
const trimmed = input.trim();
|
|
2671
2686
|
const n = Number(trimmed);
|
|
@@ -2675,7 +2690,7 @@ ${systemPrompt}
|
|
|
2675
2690
|
return;
|
|
2676
2691
|
}
|
|
2677
2692
|
// Invalid — re-prompt with the user's last entry so they can edit it
|
|
2678
|
-
input = await ctx.ui.input(label, trimmed);
|
|
2693
|
+
input = await withModal(() => ctx.ui.input(label, trimmed));
|
|
2679
2694
|
}
|
|
2680
2695
|
}
|
|
2681
2696
|
}
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -238,24 +238,6 @@ export function describeActivity(activeTools: Map<string, string>, responseText?
|
|
|
238
238
|
return "thinking…";
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
/**
|
|
242
|
-
* Structural check for pi's prompt editor.
|
|
243
|
-
*
|
|
244
|
-
* `instanceof Editor` is unusable here: an extension can resolve its own copy
|
|
245
|
-
* of `@earendil-works/pi-tui` at a different version than the host's, and the
|
|
246
|
-
* two `Editor` class objects are then unrelated identities, so the check is
|
|
247
|
-
* always false. Modals opened with `ctx.ui.custom()` — including the `/agents`
|
|
248
|
-
* menus — focus a plain `{ render, invalidate, handleInput }` object instead,
|
|
249
|
-
* so the editor's text-buffer methods identify it across pi-tui copies.
|
|
250
|
-
*/
|
|
251
|
-
function isPromptEditor(component: unknown): boolean {
|
|
252
|
-
if (typeof component !== "object" || component === null) return false;
|
|
253
|
-
const candidate = component as { getText?: unknown; setText?: unknown; submitValue?: unknown };
|
|
254
|
-
return typeof candidate.getText === "function"
|
|
255
|
-
&& typeof candidate.setText === "function"
|
|
256
|
-
&& typeof candidate.submitValue === "function";
|
|
257
|
-
}
|
|
258
|
-
|
|
259
241
|
// ---- Widget manager ----
|
|
260
242
|
|
|
261
243
|
export class AgentWidget {
|
|
@@ -265,6 +247,8 @@ export class AgentWidget {
|
|
|
265
247
|
private inputUnsub: (() => void) | undefined;
|
|
266
248
|
/** Whether arrow keys currently navigate the agent roster. */
|
|
267
249
|
private navigationActive = false;
|
|
250
|
+
/** Depth of AgentWidget-owned modals currently holding the keyboard. */
|
|
251
|
+
private modalDepth = 0;
|
|
268
252
|
/** Stable identity of the selected row, so roster changes do not jump selection. */
|
|
269
253
|
private selectedAgentId: string | undefined;
|
|
270
254
|
/** Last logical roster index of the selected row, used when it disappears. */
|
|
@@ -392,16 +376,25 @@ export class AgentWidget {
|
|
|
392
376
|
}
|
|
393
377
|
|
|
394
378
|
/**
|
|
395
|
-
*
|
|
379
|
+
* Track an AgentWidget-owned modal (the `/agents` menus and friends).
|
|
396
380
|
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
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.
|
|
402
390
|
*/
|
|
403
|
-
|
|
404
|
-
|
|
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;
|
|
405
398
|
}
|
|
406
399
|
|
|
407
400
|
private selectedIndexOf(records: readonly AgentRecord[]): number {
|
|
@@ -443,7 +436,7 @@ export class AgentWidget {
|
|
|
443
436
|
if (records.length === 0 || !ui) return false;
|
|
444
437
|
|
|
445
438
|
if (!this.navigationActive) {
|
|
446
|
-
if (direction !== 1 || !this.
|
|
439
|
+
if (direction !== 1 || !this.keyboardAvailable() || (ui.getEditorText?.() ?? "") !== "") return false;
|
|
447
440
|
this.activate(records);
|
|
448
441
|
return true;
|
|
449
442
|
}
|
|
@@ -471,7 +464,7 @@ export class AgentWidget {
|
|
|
471
464
|
/** Handle terminal input before it reaches the focused prompt editor. */
|
|
472
465
|
handleKey(data: string): { consume?: boolean; data?: string } | undefined {
|
|
473
466
|
if (!this.uiCtx || isKeyRelease(data)) return undefined;
|
|
474
|
-
if (!this.
|
|
467
|
+
if (!this.keyboardAvailable()) {
|
|
475
468
|
if (this.navigationActive) this.deactivate();
|
|
476
469
|
return undefined;
|
|
477
470
|
}
|