@aeondave/pi-persona 1.17.0 → 1.17.3
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/AGENTS.md +79 -29
- package/CHANGELOG.md +264 -0
- package/README.md +27 -12
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +223 -57
- package/docs/EXPERIENCE.md +19 -9
- package/docs/MONITORS.md +62 -3
- package/docs/REFERENCE.md +189 -35
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +9 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +20 -0
- package/src/bridge.ts +3 -2
- package/src/core/brief.ts +429 -356
- package/src/core/config.ts +16 -11
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +251 -0
- package/src/core/timer.ts +34 -9
- package/src/engine/adapter.ts +12 -8
- package/src/engine/async.ts +195 -136
- package/src/engine/child.ts +1 -1
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +11 -6
- package/src/engine/stream.ts +3 -0
- package/src/exocom/install.ts +2 -1
- package/src/extension/companion-settings.ts +29 -0
- package/src/extension/engine.ts +43 -16
- package/src/extension/hooks.ts +106 -36
- package/src/extension/persona-model-settings.ts +336 -0
- package/src/extension/persona-settings.ts +459 -0
- package/src/extension/shared.ts +15 -7
- package/src/extension.ts +642 -267
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +26 -5
- package/src/orchestration/flow-run.ts +38 -13
- package/src/orchestration/flow.ts +9 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/sdk.ts +17 -3
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/persona/config-store.ts +6 -195
- package/src/persona/model-participants.ts +1 -1
- package/src/persona/persona.ts +7 -3
- package/src/persona/settings-store.ts +636 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +77 -52
- package/src/tools/delegate-tool.ts +315 -285
- package/src/tools/delegate.ts +22 -210
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +15 -19
- package/src/tools/flow.ts +85 -28
- package/src/tools/intercom-tool.ts +108 -118
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +19 -9
- package/src/tools/monitor.ts +39 -6
- package/src/tools/timer.ts +11 -13
- package/src/ui/agent-overlay.ts +55 -14
- package/src/ui/agent-tree.ts +84 -18
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/persona-settings.ts +328 -0
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +189 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
- package/src/persona/state.ts +0 -28
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { compactInlineText, sanitizeTerminalText } from "./presentation.ts";
|
|
2
|
+
|
|
3
|
+
const MAX_INSPECTED_CHARS = 16_384;
|
|
4
|
+
const MAX_SUMMARY_COLUMNS = 140;
|
|
5
|
+
|
|
6
|
+
/** Display-only cause preview. The caller retains the original error for diagnostics. */
|
|
7
|
+
export function summarizeFailure(error: string): string {
|
|
8
|
+
let source = sanitizeTerminalText(error.slice(0, MAX_INSPECTED_CHARS)).trim();
|
|
9
|
+
// Omit only the runtime's identity/model wrapper, not meaningful tags such as [BLOCKED].
|
|
10
|
+
const wrapperEnd = source.startsWith("[") ? source.indexOf("]") : -1;
|
|
11
|
+
if (wrapperEnd > 0 && wrapperEnd <= 512 && source.slice(1, wrapperEnd).includes(" · ")) source = source.slice(wrapperEnd + 1).trim();
|
|
12
|
+
const reason = sanitizeTerminalText(jsonCause(source) ?? source).replace(/\s+/g, " ").trim();
|
|
13
|
+
if (/^(?:(?:the|this)\s+)?(?:['"`][^'"`]{1,200}['"`]\s+)?model\s+is\s+not\s+supported\s+when\s+using\s+Codex\s+with\s+a\s+ChatGPT\s+account[.!]?$/i.test(reason)) {
|
|
14
|
+
return "Model not supported by this ChatGPT account.";
|
|
15
|
+
}
|
|
16
|
+
return compactInlineText(reason, { maxChars: MAX_SUMMARY_COLUMNS });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Scan one bounded object, respecting JSON strings; never retry parsing at every brace. */
|
|
20
|
+
function jsonCause(source: string): string | undefined {
|
|
21
|
+
const start = source.indexOf("{");
|
|
22
|
+
if (start < 0) return undefined;
|
|
23
|
+
let depth = 0;
|
|
24
|
+
let quoted = false;
|
|
25
|
+
let escaped = false;
|
|
26
|
+
for (let index = start; index < source.length; index++) {
|
|
27
|
+
const char = source[index];
|
|
28
|
+
if (quoted) {
|
|
29
|
+
if (escaped) escaped = false;
|
|
30
|
+
else if (char === "\\") escaped = true;
|
|
31
|
+
else if (char === '"') quoted = false;
|
|
32
|
+
} else if (char === '"') quoted = true;
|
|
33
|
+
else if (char === "{") depth++;
|
|
34
|
+
else if (char === "}" && --depth === 0) {
|
|
35
|
+
try { return objectCause(JSON.parse(source.slice(start, index + 1)) as unknown); }
|
|
36
|
+
catch { return undefined; }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function objectCause(value: unknown): string | undefined {
|
|
43
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
44
|
+
const object = value as Record<string, unknown>;
|
|
45
|
+
for (const field of [object.detail, object.message, object.error]) {
|
|
46
|
+
if (typeof field === "string" && field.trim()) return field;
|
|
47
|
+
}
|
|
48
|
+
if (object.error !== null && typeof object.error === "object" && !Array.isArray(object.error)) {
|
|
49
|
+
const error = object.error as Record<string, unknown>;
|
|
50
|
+
for (const field of [error.message, error.detail]) {
|
|
51
|
+
if (typeof field === "string" && field.trim()) return field;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** One compact renderer for literal control receipts and diagnostic fallbacks. Pi owns the shell. */
|
|
2
|
+
import { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import { compactVisibleText, sanitizeTerminalText } from "./presentation.ts";
|
|
5
|
+
|
|
6
|
+
interface LiteralResult {
|
|
7
|
+
content: readonly { type: string; text?: string }[];
|
|
8
|
+
/** Legacy direct renderer calls may carry this; the native host passes it in context. */
|
|
9
|
+
isError?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function renderLiteralToolResult(
|
|
13
|
+
result: LiteralResult,
|
|
14
|
+
{ expanded }: { expanded: boolean },
|
|
15
|
+
theme: Theme,
|
|
16
|
+
context?: { isError: boolean },
|
|
17
|
+
): Text {
|
|
18
|
+
const full = sanitizeTerminalText(result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n")) || "(no output)";
|
|
19
|
+
const failed = context?.isError ?? result.isError ?? false;
|
|
20
|
+
const preview = expanded ? undefined : compactVisibleText(full, { maxLines: 4, maxLineChars: 100 });
|
|
21
|
+
const heading = failed ? `${theme.fg("error", theme.bold("failed"))}\n` : "";
|
|
22
|
+
const hint = preview?.truncated ? `\n${theme.fg("dim", keyHint("app.tools.expand", "to expand"))}` : "";
|
|
23
|
+
return new Text(`${heading}${theme.fg("toolOutput", preview?.text ?? full)}${hint}`, 0, 0);
|
|
24
|
+
}
|
package/src/ui/model-picker.ts
CHANGED
|
@@ -5,12 +5,32 @@
|
|
|
5
5
|
* hundreds of models) and the viewport *follows the selection* — the highlighted
|
|
6
6
|
* row is always on screen, so picking never jumps off the top.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Machine-facing filtering/ordering remain separate from human fuzzy search.
|
|
9
|
+
* Only caller-supplied options are eligible; metadata never adds choices.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
12
|
+
import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
Container, CURSOR_MARKER, type Focusable, fuzzyFilter, getKeybindings, Input,
|
|
15
|
+
renderFakeCursor, setKeybindings, type TUI, truncateToWidth, visibleWidth,
|
|
16
|
+
} from "@earendil-works/pi-tui";
|
|
17
|
+
import { sanitizeTerminalText } from "./presentation.ts";
|
|
18
|
+
|
|
19
|
+
/** Structural subset of host snapshots; no registry lookup or availability policy here. */
|
|
20
|
+
export interface ModelPickerMetadata {
|
|
21
|
+
provider: string;
|
|
22
|
+
id: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
contextWindow?: number;
|
|
25
|
+
reasoning?: boolean;
|
|
26
|
+
input?: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ModelPickerConfig {
|
|
30
|
+
currentRef?: string;
|
|
31
|
+
models?: readonly ModelPickerMetadata[];
|
|
32
|
+
keybindings?: KeybindingsManager;
|
|
33
|
+
}
|
|
14
34
|
|
|
15
35
|
const VIEWPORT = 12; // rows shown at once (the rest scrolls)
|
|
16
36
|
|
|
@@ -38,24 +58,48 @@ export function orderModelRefs(refs: string[], sessionProvider?: string): string
|
|
|
38
58
|
return [...refs].sort((a, b) => Number(!isSession(a)) - Number(!isSession(b)) || a.localeCompare(b));
|
|
39
59
|
}
|
|
40
60
|
|
|
41
|
-
/**
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
/** One safe display line; never use this to rewrite a returned model ref. */
|
|
62
|
+
function displayText(value: string): string {
|
|
63
|
+
return sanitizeTerminalText(value)
|
|
64
|
+
.replace(/[\n\r\t\u2028\u2029]/g, " ")
|
|
65
|
+
.replace(/[\u202a-\u202e\u2066-\u2069]/g, "");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function metadataText(model: ModelPickerMetadata | undefined): string {
|
|
69
|
+
if (!model) return "";
|
|
70
|
+
const parts: string[] = [];
|
|
71
|
+
const context = model.contextWindow;
|
|
72
|
+
if (context !== undefined && Number.isSafeInteger(context) && context > 0) {
|
|
73
|
+
const size = context >= 1_000_000 ? `${Math.round(context / 100_000) / 10}m`
|
|
74
|
+
: context >= 1000 ? `${Math.round(context / 100) / 10}k` : String(context);
|
|
75
|
+
parts.push(`${size} ctx`);
|
|
76
|
+
}
|
|
77
|
+
if (model.reasoning === true) parts.push("reasoning");
|
|
78
|
+
if (model.input?.includes("image")) parts.push("vision");
|
|
79
|
+
// Functional metadata gets scarce columns before a potentially long/duplicated display name.
|
|
80
|
+
if (model.name && model.name !== model.id) parts.push(truncateToWidth(displayText(model.name), 32));
|
|
81
|
+
return parts.length ? ` · ${parts.join(" · ")}` : "";
|
|
46
82
|
}
|
|
47
83
|
|
|
48
|
-
export class ModelPicker extends Container {
|
|
84
|
+
export class ModelPicker extends Container implements Focusable {
|
|
49
85
|
private theme: Theme;
|
|
50
86
|
private title: string;
|
|
51
87
|
private options: string[];
|
|
52
88
|
private sessionProvider: string | undefined;
|
|
89
|
+
private currentRef: string | undefined;
|
|
53
90
|
private done: (ref: string | undefined) => void;
|
|
54
91
|
private tui: TUI;
|
|
55
|
-
private
|
|
92
|
+
private input: Input;
|
|
93
|
+
private keybindings: ReturnType<typeof getKeybindings>;
|
|
94
|
+
private models = new Map<string, ModelPickerMetadata>();
|
|
95
|
+
private list: string[];
|
|
56
96
|
private selected = 0;
|
|
57
97
|
private scroll = 0;
|
|
58
|
-
private
|
|
98
|
+
private pastePending = false;
|
|
99
|
+
private pasteTail = "";
|
|
100
|
+
|
|
101
|
+
get focused(): boolean { return this.input.focused; }
|
|
102
|
+
set focused(value: boolean) { this.input.focused = value; }
|
|
59
103
|
|
|
60
104
|
constructor(
|
|
61
105
|
tui: TUI,
|
|
@@ -64,97 +108,130 @@ export class ModelPicker extends Container {
|
|
|
64
108
|
options: string[],
|
|
65
109
|
sessionProvider: string | undefined,
|
|
66
110
|
done: (ref: string | undefined) => void,
|
|
111
|
+
config: ModelPickerConfig = {},
|
|
67
112
|
) {
|
|
68
113
|
super();
|
|
69
114
|
this.tui = tui;
|
|
70
115
|
this.theme = theme;
|
|
71
|
-
this.title = title;
|
|
72
|
-
this.options = options;
|
|
116
|
+
this.title = displayText(title);
|
|
117
|
+
this.options = [...options];
|
|
118
|
+
this.list = this.options;
|
|
73
119
|
this.sessionProvider = sessionProvider;
|
|
120
|
+
this.currentRef = config.currentRef;
|
|
74
121
|
this.done = done;
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
private filtered(): string[] {
|
|
84
|
-
return filterModels(this.options, this.query);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
override render(width: number): string[] {
|
|
88
|
-
this.lastWidth = width;
|
|
89
|
-
const inner = this.inner();
|
|
90
|
-
const b = (s: string): string => this.theme.fg("accent", s);
|
|
91
|
-
const framed = super.render(inner).map((line) => {
|
|
92
|
-
const pad = " ".repeat(Math.max(0, inner - visibleWidth(line)));
|
|
93
|
-
return `${b("│")} ${line}${pad} ${b("│")}`;
|
|
122
|
+
this.keybindings = config.keybindings ?? getKeybindings();
|
|
123
|
+
for (const model of config.models ?? []) this.models.set(`${model.provider}/${model.id}`, model);
|
|
124
|
+
this.selected = Math.max(0, this.list.indexOf(config.currentRef ?? ""));
|
|
125
|
+
this.input = new Input({
|
|
126
|
+
prompt: "",
|
|
127
|
+
placeholder: "Search name, id or provider",
|
|
128
|
+
placeholderStyle: (text) => this.theme.fg("dim", text),
|
|
94
129
|
});
|
|
95
|
-
|
|
130
|
+
this.addChild(this.input);
|
|
96
131
|
}
|
|
97
132
|
|
|
98
|
-
private
|
|
99
|
-
|
|
133
|
+
private search(): void {
|
|
134
|
+
const previous = this.list[this.selected];
|
|
135
|
+
const query = this.input.getValue().trim();
|
|
136
|
+
this.list = fuzzyFilter(this.options, query, (ref) =>
|
|
137
|
+
displayText(`${ref} ${this.models.get(ref)?.name ?? ""}`));
|
|
138
|
+
// Full qualified refs beat name/token scores, without changing the caller's order at rest.
|
|
139
|
+
if (query) {
|
|
140
|
+
const literal = this.list.indexOf(query);
|
|
141
|
+
const exact = literal >= 0 ? literal : this.list.findIndex((ref) => ref.toLowerCase() === query.toLowerCase());
|
|
142
|
+
if (exact >= 0) {
|
|
143
|
+
if (exact > 0) this.list.unshift(...this.list.splice(exact, 1));
|
|
144
|
+
this.selected = 0; // typing an exact ref must not confirm a surviving fuzzy sibling
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.selected = previous === undefined ? 0 : Math.max(0, this.list.indexOf(previous));
|
|
100
149
|
}
|
|
101
150
|
|
|
102
|
-
|
|
103
|
-
|
|
151
|
+
override render(width: number): string[] {
|
|
152
|
+
const outer = Math.max(0, Math.min(104, Math.floor(width)));
|
|
153
|
+
if (outer === 0) return [];
|
|
154
|
+
const framed = outer >= 8;
|
|
155
|
+
const inner = framed ? outer - 4 : outer;
|
|
104
156
|
const t = this.theme;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
157
|
+
const fit = (line: string): string => truncateToWidth(line, inner, "…");
|
|
158
|
+
let inputLine = this.input.render(inner)[0] ?? "";
|
|
159
|
+
// Native Input can overrun a one-column viewport with a wide cursor grapheme.
|
|
160
|
+
if (visibleWidth(inputLine) > inner) {
|
|
161
|
+
inputLine = `${this.focused ? CURSOR_MARKER : ""}${renderFakeCursor(" ")}`;
|
|
162
|
+
}
|
|
163
|
+
const lines = [
|
|
164
|
+
t.fg("accent", t.bold(fit(this.title))),
|
|
165
|
+
t.fg("dim", fit(`${this.list.length}/${this.options.length} · fuzzy search`)),
|
|
166
|
+
inputLine,
|
|
167
|
+
"",
|
|
168
|
+
];
|
|
169
|
+
this.scroll = visibleWindow(this.selected, VIEWPORT, this.list.length, this.scroll);
|
|
170
|
+
if (!this.list.length) {
|
|
171
|
+
lines.push(t.fg("dim", fit("(no model matches — edit search to widen)")));
|
|
118
172
|
} else {
|
|
119
|
-
const end = Math.min(list.length, this.scroll + VIEWPORT);
|
|
120
|
-
if (this.scroll > 0)
|
|
173
|
+
const end = Math.min(this.list.length, this.scroll + VIEWPORT);
|
|
174
|
+
if (this.scroll > 0) lines.push(t.fg("dim", fit(`▲ ${this.scroll} above`)));
|
|
121
175
|
for (let i = this.scroll; i < end; i++) {
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
this.
|
|
176
|
+
const ref = this.list[i]!;
|
|
177
|
+
const current = ref === this.currentRef ? "✓" : " ";
|
|
178
|
+
const star = this.sessionProvider && ref.startsWith(`${this.sessionProvider}/`) ? "★" : " ";
|
|
179
|
+
// Ref comes first; friendly name and bounded, explicit metadata spend only spare columns.
|
|
180
|
+
const row = fit(`${i === this.selected ? "▸" : " "} ${current}${star} ${displayText(ref)}${metadataText(this.models.get(ref))}`);
|
|
181
|
+
lines.push(i === this.selected ? t.fg("accent", row) : row);
|
|
127
182
|
}
|
|
128
|
-
if (end < list.length)
|
|
183
|
+
if (end < this.list.length) lines.push(t.fg("dim", fit(`▼ ${this.list.length - end} below`)));
|
|
129
184
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
185
|
+
const keys = (action: Parameters<typeof this.keybindings.getKeys>[0]): string => this.keybindings.getKeys(action).join("/");
|
|
186
|
+
lines.push("", t.fg("dim", fit("✓ current · ★ session provider")), t.fg("dim", fit(
|
|
187
|
+
`${keys("tui.select.up")}/${keys("tui.select.down")} move · ${keys("tui.select.confirm")} select · ${keys("tui.select.cancel")} keep default`,
|
|
188
|
+
)));
|
|
189
|
+
if (!framed) return lines;
|
|
190
|
+
const b = (text: string): string => t.fg("accent", text);
|
|
191
|
+
return [
|
|
192
|
+
b(`┌${"─".repeat(inner + 2)}┐`),
|
|
193
|
+
...lines.map((line) => `${b("│")} ${line}${" ".repeat(Math.max(0, inner - visibleWidth(line)))} ${b("│")}`),
|
|
194
|
+
b(`└${"─".repeat(inner + 2)}┘`),
|
|
195
|
+
];
|
|
133
196
|
}
|
|
134
197
|
|
|
135
198
|
handleInput(keyData: string): void {
|
|
136
|
-
const kb =
|
|
137
|
-
|
|
138
|
-
|
|
199
|
+
const kb = this.keybindings;
|
|
200
|
+
// While Input buffers a bracketed paste, even a standalone Enter/Escape
|
|
201
|
+
// chunk is text, not a picker action. Retain only enough tail to detect
|
|
202
|
+
// an end marker split across chunks; Input owns the actual paste buffer.
|
|
203
|
+
const pasteData = this.pasteTail + keyData;
|
|
204
|
+
const editingPaste = this.pastePending || keyData.includes("\x1b[200~");
|
|
205
|
+
this.pastePending = editingPaste && !pasteData.includes("\x1b[201~");
|
|
206
|
+
this.pasteTail = this.pastePending ? pasteData.slice(-5) : "";
|
|
207
|
+
if (!editingPaste && kb.matches(keyData, "tui.select.up")) {
|
|
139
208
|
this.selected = Math.max(0, this.selected - 1);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
209
|
+
} else if (!editingPaste && kb.matches(keyData, "tui.select.down")) {
|
|
210
|
+
this.selected = Math.min(Math.max(0, this.list.length - 1), this.selected + 1);
|
|
211
|
+
} else if (!editingPaste && kb.matches(keyData, "tui.select.confirm")) {
|
|
212
|
+
const ref = this.list[this.selected];
|
|
213
|
+
if (ref !== undefined) this.done(ref);
|
|
214
|
+
return;
|
|
215
|
+
} else if (!editingPaste && kb.matches(keyData, "tui.select.cancel")) {
|
|
147
216
|
this.done(undefined);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
217
|
+
return;
|
|
218
|
+
} else {
|
|
219
|
+
const before = this.input.getValue();
|
|
220
|
+
// Input has no per-instance keybindings API (Pi 1.3/1.4). Scope its global
|
|
221
|
+
// lookup to this synchronous call and restore it even if handling throws.
|
|
222
|
+
const global = getKeybindings();
|
|
223
|
+
try {
|
|
224
|
+
if (global !== kb) setKeybindings(kb);
|
|
225
|
+
this.input.handleInput(keyData);
|
|
226
|
+
} finally {
|
|
227
|
+
if (global !== kb) setKeybindings(global);
|
|
153
228
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
229
|
+
const value = this.input.getValue();
|
|
230
|
+
const safe = displayText(value);
|
|
231
|
+
if (safe !== value) this.input.setValue(safe);
|
|
232
|
+
if (before !== safe) this.search();
|
|
158
233
|
}
|
|
234
|
+
this.invalidate();
|
|
235
|
+
this.tui.requestRender();
|
|
159
236
|
}
|
|
160
237
|
}
|