@ferris1225/pi-subagents 0.31.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -74
- package/package.json +2 -2
- package/src/agents.ts +8 -13
- package/src/announcements.ts +59 -0
- package/src/background.ts +59 -6
- package/src/config.ts +14 -2
- package/src/dispatch.ts +1833 -845
- package/src/fixloop.ts +1 -1
- package/src/format.ts +30 -4
- package/src/index.ts +9 -10
- package/src/models.ts +184 -54
- package/src/monitor.ts +141 -93
- package/src/prompt.ts +3 -2
- package/src/recovery.ts +145 -0
- package/src/rpc-run.ts +991 -0
- package/src/runtime.ts +284 -145
- package/src/session-fork.ts +84 -0
- package/src/setup.ts +271 -184
- package/src/spawn.ts +557 -977
- package/src/tools.ts +730 -409
- package/src/trajectory.ts +312 -0
- package/src/ui.ts +32 -16
- package/src/worktree.ts +687 -0
- package/src/widget.ts +0 -182
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only lifecycle trajectories for logical sub-agent threads.
|
|
3
|
+
*
|
|
4
|
+
* Events cover dispatch, model candidates, retries, controls, tool activity,
|
|
5
|
+
* usage, worktrees, forks, and settlement. Each event keeps its generation and
|
|
6
|
+
* timestamp across resume/restart; mutable summary fields reset per generation.
|
|
7
|
+
* Tool arguments are reduced to a short terminal-safe summary with obvious
|
|
8
|
+
* credential fields and embedded secrets redacted.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { stripVTControlCharacters } from "node:util";
|
|
12
|
+
import type { UsageStats } from "./rpc-run.ts";
|
|
13
|
+
import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Event types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export const TRAJECTORY_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
/** Typed append-only trajectory events. */
|
|
22
|
+
export type TrajectoryEvent =
|
|
23
|
+
| { v: number; runId: number; generation: number; at: number; kind: "dispatch"; agent: string; task: string; model?: string; thinking?: string; pool?: readonly string[]; vision?: boolean; resumed?: boolean; isolation?: IsolationMode; originalCwd?: string; isolationCwd?: string }
|
|
24
|
+
| { v: number; runId: number; generation: number; at: number; kind: "status"; status: string; phase?: string }
|
|
25
|
+
| { v: number; runId: number; generation: number; at: number; kind: "candidate"; model?: string; index?: number; total?: number; fallbackFrom?: string }
|
|
26
|
+
| { v: number; runId: number; generation: number; at: number; kind: "retry"; reason: string; delayMs?: number }
|
|
27
|
+
| { v: number; runId: number; generation: number; at: number; kind: "steer"; instruction: string }
|
|
28
|
+
| { v: number; runId: number; generation: number; at: number; kind: "retarget"; objective: string }
|
|
29
|
+
| { v: number; runId: number; generation: number; at: number; kind: "park" }
|
|
30
|
+
| { v: number; runId: number; generation: number; at: number; kind: "resume"; objective?: string }
|
|
31
|
+
| { v: number; runId: number; generation: number; at: number; kind: "fork"; sourceRunId: number; childRunId: number; objective?: string }
|
|
32
|
+
| { v: number; runId: number; generation: number; at: number; kind: "stop"; reason?: string }
|
|
33
|
+
| { v: number; runId: number; generation: number; at: number; kind: "worktree"; status: "created" | WorktreeFinalizationStatus; originalCwd: string; isolationCwd?: string; worktreePath?: string; patchPath?: string; integrated?: boolean; error?: string }
|
|
34
|
+
| { v: number; runId: number; generation: number; at: number; kind: "settled"; status: "done" | "failed" | "stopped"; model?: string; isolation?: IsolationMode; integrationStatus?: WorktreeFinalizationStatus }
|
|
35
|
+
| { v: number; runId: number; generation: number; at: number; kind: "tool_start"; tool: string; toolCallId?: string; summary: string }
|
|
36
|
+
| { v: number; runId: number; generation: number; at: number; kind: "tool_end"; tool: string; toolCallId?: string; isError: boolean; error?: string }
|
|
37
|
+
| { v: number; runId: number; generation: number; at: number; kind: "usage"; usage: UsageStats; model?: string };
|
|
38
|
+
|
|
39
|
+
export type TrajectoryEventKind = TrajectoryEvent["kind"];
|
|
40
|
+
|
|
41
|
+
/** Distributive Omit: per-variant event payload without the envelope fields
|
|
42
|
+
* (v/runId/generation/at are stamped by TrajectoryLog.append). */
|
|
43
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
44
|
+
export type NewTrajectoryEvent = DistributiveOmit<TrajectoryEvent, "v" | "runId" | "generation" | "at">;
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Tool-arg safety: concise summary + redaction
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/** Truncation budget for one scalar argument value inside a summary. */
|
|
51
|
+
export const TOOL_ARG_VALUE_MAX = 48;
|
|
52
|
+
/** Total budget for a summarized tool-args blob. */
|
|
53
|
+
export const TOOL_ARG_SUMMARY_MAX = 160;
|
|
54
|
+
|
|
55
|
+
const SENSITIVE_KEY_RE = /token|password|authorization|api[-_]?key|apikey|secret|credential|cookie|bearer/i;
|
|
56
|
+
const REDACTED = "<redacted>";
|
|
57
|
+
|
|
58
|
+
function isSensitiveKey(key: string): boolean {
|
|
59
|
+
return SENSITIVE_KEY_RE.test(key);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Redact credentials embedded inside otherwise ordinary scalar fields such as
|
|
63
|
+
* `command`. Key-only filtering is insufficient for shell/header arguments. */
|
|
64
|
+
export function redactSensitiveText(value: string): string {
|
|
65
|
+
let text = stripVTControlCharacters(value);
|
|
66
|
+
text = text.replace(
|
|
67
|
+
/(\bauthorization\s*:\s*(?:bearer|basic)\s+)([^\s'"`;,]+)/giu,
|
|
68
|
+
`$1${REDACTED}`,
|
|
69
|
+
);
|
|
70
|
+
text = text.replace(
|
|
71
|
+
/(\bbearer\s+)([A-Za-z0-9._~+/=-]{6,})/giu,
|
|
72
|
+
`$1${REDACTED}`,
|
|
73
|
+
);
|
|
74
|
+
text = text.replace(
|
|
75
|
+
/(\b(?:api[-_]?key|apikey|access[-_]?token|refresh[-_]?token|token|password|passwd|secret|credential|cookie)\b\s*(?:=|:)\s*)(?:"[^"]*"|'[^']*'|[^\s;&,]+)/giu,
|
|
76
|
+
`$1${REDACTED}`,
|
|
77
|
+
);
|
|
78
|
+
text = text.replace(
|
|
79
|
+
/((?:--?(?:api[-_]?key|access[-_]?token|token|password|secret|credential))\s+)(?:"[^"]*"|'[^']*'|\S+)/giu,
|
|
80
|
+
`$1${REDACTED}`,
|
|
81
|
+
);
|
|
82
|
+
text = text.replace(
|
|
83
|
+
/\b(?:sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9]{8,}|AKIA[A-Z0-9]{16})\b/gu,
|
|
84
|
+
REDACTED,
|
|
85
|
+
);
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function summarizeScalar(value: unknown): string | undefined {
|
|
90
|
+
if (value === null || value === undefined || value === "") return undefined;
|
|
91
|
+
let text: string;
|
|
92
|
+
if (typeof value === "string") text = value;
|
|
93
|
+
else if (typeof value === "number" || typeof value === "boolean") text = String(value);
|
|
94
|
+
else return undefined; // arrays/objects are dropped from the summary
|
|
95
|
+
const oneLine = value.toString() === "[object Object]" ? "" : redactSensitiveText(text).replace(/\s+/g, " ").trim();
|
|
96
|
+
if (!oneLine) return undefined;
|
|
97
|
+
const chars = [...oneLine];
|
|
98
|
+
return chars.length > TOOL_ARG_VALUE_MAX ? `${chars.slice(0, TOOL_ARG_VALUE_MAX - 1).join("")}…` : oneLine;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Concise, display-safe one-line summary of a tool-args payload. Only the first
|
|
103
|
+
* few scalar fields are shown; sensitive-looking values are redacted; deep
|
|
104
|
+
* structures are collapsed. Never throws. Output is width-agnostic plain text
|
|
105
|
+
* (callers truncate to their display budget with truncateToWidth).
|
|
106
|
+
*/
|
|
107
|
+
export function summarizeToolArgs(args: unknown): string {
|
|
108
|
+
if (args === null || args === undefined) return "";
|
|
109
|
+
if (typeof args !== "object") {
|
|
110
|
+
const s = summarizeScalar(args);
|
|
111
|
+
return s ? truncateSummary(s) : "";
|
|
112
|
+
}
|
|
113
|
+
const record = args as Record<string, unknown>;
|
|
114
|
+
const parts: string[] = [];
|
|
115
|
+
for (const [key, value] of Object.entries(record)) {
|
|
116
|
+
if (parts.length >= 5) {
|
|
117
|
+
parts.push("…");
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (isSensitiveKey(key)) {
|
|
121
|
+
parts.push(`${key}=${REDACTED}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const scalar = summarizeScalar(value);
|
|
125
|
+
if (scalar !== undefined) parts.push(`${key}=${scalar}`);
|
|
126
|
+
else if (value !== undefined && value !== null) parts.push(`${key}={…}`);
|
|
127
|
+
}
|
|
128
|
+
return truncateSummary(parts.filter(Boolean).join(" "));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function truncateSummary(text: string): string {
|
|
132
|
+
const chars = [...text];
|
|
133
|
+
return chars.length > TOOL_ARG_SUMMARY_MAX ? `${chars.slice(0, TOOL_ARG_SUMMARY_MAX - 1).join("")}…` : text;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Trajectory log
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
export interface TrajectorySummaryRecord {
|
|
141
|
+
model?: string;
|
|
142
|
+
thinking?: string;
|
|
143
|
+
modelFallbackFrom?: string;
|
|
144
|
+
toolCount: number;
|
|
145
|
+
currentTool?: string;
|
|
146
|
+
activity?: string;
|
|
147
|
+
lastAt?: number;
|
|
148
|
+
endedAt?: number;
|
|
149
|
+
isolation?: IsolationMode;
|
|
150
|
+
integrationStatus?: WorktreeFinalizationStatus | "pending";
|
|
151
|
+
originalCwd?: string;
|
|
152
|
+
isolationCwd?: string;
|
|
153
|
+
forkedFromRunId?: number;
|
|
154
|
+
forkChildRunIds?: number[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Append-only event log for one thread id. `clearSummary()` retires the mutable
|
|
159
|
+
* latest-state fields at the start of a new generation while the event history
|
|
160
|
+
* stays. `clearAll()` (parent-session teardown only) drops everything.
|
|
161
|
+
*/
|
|
162
|
+
export class TrajectoryLog {
|
|
163
|
+
private readonly events: TrajectoryEvent[] = [];
|
|
164
|
+
/** Mutable latest-state summary; NOT append-only — reset per generation. */
|
|
165
|
+
private summaryRecord: TrajectorySummaryRecord = { toolCount: 0 };
|
|
166
|
+
|
|
167
|
+
constructor(
|
|
168
|
+
readonly runId: number,
|
|
169
|
+
private readonly notify: () => void,
|
|
170
|
+
) {}
|
|
171
|
+
|
|
172
|
+
get generation(): number {
|
|
173
|
+
return this.summaryGeneration;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Internal counter also stored on each event; bumped by restart(). Starts
|
|
177
|
+
* at 1 to mirror the thread's generation numbering in the runtime. */
|
|
178
|
+
private summaryGeneration = 1;
|
|
179
|
+
|
|
180
|
+
append(event: NewTrajectoryEvent, now: number = Date.now()): void {
|
|
181
|
+
const full = { v: TRAJECTORY_VERSION, runId: this.runId, generation: this.summaryGeneration, at: now, ...event } as TrajectoryEvent;
|
|
182
|
+
this.events.push(full);
|
|
183
|
+
this.applySummary(full);
|
|
184
|
+
try {
|
|
185
|
+
this.notify();
|
|
186
|
+
} catch {
|
|
187
|
+
/* observers must never break trajectory capture */
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** All events in append order (oldest first). Generation ascends monotonically. */
|
|
192
|
+
getEvents(): readonly TrajectoryEvent[] {
|
|
193
|
+
return this.events;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Events of the latest generation only (oldest first). */
|
|
197
|
+
getGenerationEvents(): readonly TrajectoryEvent[] {
|
|
198
|
+
let start = this.events.length;
|
|
199
|
+
for (let i = this.events.length - 1; i >= 0; i--) {
|
|
200
|
+
if (this.events[i].generation === this.summaryGeneration) start = i;
|
|
201
|
+
else break;
|
|
202
|
+
}
|
|
203
|
+
return this.events.slice(start);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
summary(): Readonly<TrajectorySummaryRecord> {
|
|
207
|
+
return this.summaryRecord;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Begin a new generation: same id, bumped generation, fresh mutable summary,
|
|
211
|
+
* untouched event history. Appends no event itself — dispatch appends the
|
|
212
|
+
* dispatch/resume event with the new-generation attributes. */
|
|
213
|
+
restart(): number {
|
|
214
|
+
this.summaryGeneration += 1;
|
|
215
|
+
this.summaryRecord = { toolCount: 0 };
|
|
216
|
+
return this.summaryGeneration;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private applySummary(event: TrajectoryEvent): void {
|
|
220
|
+
const s = this.summaryRecord;
|
|
221
|
+
s.lastAt = event.at;
|
|
222
|
+
switch (event.kind) {
|
|
223
|
+
case "dispatch":
|
|
224
|
+
if (event.model !== undefined) s.model = event.model;
|
|
225
|
+
if (event.isolation !== undefined) s.isolation = event.isolation;
|
|
226
|
+
if (event.originalCwd !== undefined) s.originalCwd = event.originalCwd;
|
|
227
|
+
if (event.isolationCwd !== undefined) s.isolationCwd = event.isolationCwd;
|
|
228
|
+
if (event.isolation === "worktree") s.integrationStatus = "pending";
|
|
229
|
+
if (event.thinking !== undefined) s.thinking = event.thinking;
|
|
230
|
+
s.modelFallbackFrom = undefined;
|
|
231
|
+
s.endedAt = undefined;
|
|
232
|
+
s.currentTool = undefined;
|
|
233
|
+
s.activity = undefined;
|
|
234
|
+
break;
|
|
235
|
+
case "candidate":
|
|
236
|
+
if (event.model !== undefined) s.model = event.model;
|
|
237
|
+
if (event.fallbackFrom !== undefined) s.modelFallbackFrom = event.fallbackFrom;
|
|
238
|
+
break;
|
|
239
|
+
case "tool_start":
|
|
240
|
+
s.toolCount += 1;
|
|
241
|
+
s.currentTool = event.tool;
|
|
242
|
+
s.activity = event.summary ? `${event.tool} ${event.summary}` : event.tool;
|
|
243
|
+
break;
|
|
244
|
+
case "tool_end":
|
|
245
|
+
if (s.currentTool === event.tool) s.currentTool = undefined;
|
|
246
|
+
break;
|
|
247
|
+
case "fork":
|
|
248
|
+
if (event.runId === event.childRunId) s.forkedFromRunId = event.sourceRunId;
|
|
249
|
+
if (event.runId === event.sourceRunId) {
|
|
250
|
+
s.forkChildRunIds ??= [];
|
|
251
|
+
if (!s.forkChildRunIds.includes(event.childRunId)) s.forkChildRunIds.push(event.childRunId);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
case "worktree":
|
|
255
|
+
s.isolation = "worktree";
|
|
256
|
+
s.originalCwd = event.originalCwd;
|
|
257
|
+
if (event.isolationCwd !== undefined) s.isolationCwd = event.isolationCwd;
|
|
258
|
+
if (event.status !== "created") s.integrationStatus = event.status;
|
|
259
|
+
break;
|
|
260
|
+
case "settled":
|
|
261
|
+
s.endedAt = event.at;
|
|
262
|
+
s.currentTool = undefined;
|
|
263
|
+
if (event.isolation !== undefined) s.isolation = event.isolation;
|
|
264
|
+
if (event.integrationStatus !== undefined) s.integrationStatus = event.integrationStatus;
|
|
265
|
+
break;
|
|
266
|
+
case "status":
|
|
267
|
+
s.activity = undefined;
|
|
268
|
+
if (event.status === "parked" || event.status === "done" || event.status === "failed") s.endedAt ??= event.at;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Parent-session teardown: wipe summary AND history. */
|
|
274
|
+
clearAll(): void {
|
|
275
|
+
this.events.length = 0;
|
|
276
|
+
this.summaryGeneration = 1;
|
|
277
|
+
this.summaryRecord = { toolCount: 0 };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export class ThreadTrajectoryState {
|
|
282
|
+
readonly trajectory: TrajectoryLog;
|
|
283
|
+
|
|
284
|
+
constructor(readonly runId: number) {
|
|
285
|
+
this.trajectory = new TrajectoryLog(runId, () => {});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
get generation(): number {
|
|
289
|
+
return this.trajectory.generation;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Session-scoped registry of logical-thread trajectories. */
|
|
294
|
+
export class TrajectoryStore {
|
|
295
|
+
private readonly states = new Map<number, ThreadTrajectoryState>();
|
|
296
|
+
|
|
297
|
+
get(runId: number): ThreadTrajectoryState {
|
|
298
|
+
let state = this.states.get(runId);
|
|
299
|
+
if (!state) {
|
|
300
|
+
state = new ThreadTrajectoryState(runId);
|
|
301
|
+
this.states.set(runId, state);
|
|
302
|
+
}
|
|
303
|
+
return state;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
clearAll(): void {
|
|
307
|
+
for (const state of this.states.values()) state.trajectory.clearAll();
|
|
308
|
+
this.states.clear();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const trajectoryStore = new TrajectoryStore();
|
package/src/ui.ts
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
fuzzyFilter,
|
|
18
|
-
getKeybindings,
|
|
19
18
|
truncateToWidth,
|
|
20
19
|
type Component,
|
|
21
20
|
type Focusable,
|
|
21
|
+
type KeybindingsManager,
|
|
22
22
|
type SelectItem,
|
|
23
23
|
type TUI,
|
|
24
24
|
} from "@earendil-works/pi-tui";
|
|
@@ -45,6 +45,14 @@ export interface PickerStyles {
|
|
|
45
45
|
filterEcho: (t: string) => string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface PickerItem extends SelectItem {
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function pickerItemSearchText(item: PickerItem): string {
|
|
53
|
+
return `${item.value} ${item.label} ${item.description ?? ""}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
interface PickerCallbacks {
|
|
49
57
|
/** single-select: fired with the highlighted value on Enter. */
|
|
50
58
|
onSelect?: (value: string) => void;
|
|
@@ -57,18 +65,22 @@ export class Picker implements Component, Focusable {
|
|
|
57
65
|
private _focused = false;
|
|
58
66
|
private query = "";
|
|
59
67
|
private cursor = 0;
|
|
60
|
-
private filtered:
|
|
68
|
+
private filtered: PickerItem[];
|
|
61
69
|
|
|
62
70
|
constructor(
|
|
63
|
-
private readonly items:
|
|
71
|
+
private readonly items: PickerItem[],
|
|
64
72
|
private readonly multi: boolean,
|
|
65
73
|
private readonly selected: Set<string>,
|
|
66
74
|
private readonly styles: PickerStyles,
|
|
67
75
|
private readonly headerLines: string[],
|
|
68
76
|
private readonly tui: TUI,
|
|
77
|
+
private readonly keybindings: KeybindingsManager,
|
|
69
78
|
private readonly cb: PickerCallbacks,
|
|
79
|
+
initialValue?: string,
|
|
70
80
|
) {
|
|
71
81
|
this.filtered = items;
|
|
82
|
+
const initialIndex = initialValue === undefined ? -1 : items.findIndex((item) => item.value === initialValue);
|
|
83
|
+
if (initialIndex >= 0) this.cursor = initialIndex;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
get focused(): boolean {
|
|
@@ -80,7 +92,7 @@ export class Picker implements Component, Focusable {
|
|
|
80
92
|
|
|
81
93
|
private recompute(): void {
|
|
82
94
|
const q = this.query.trim();
|
|
83
|
-
this.filtered = q ? fuzzyFilter(this.items, q,
|
|
95
|
+
this.filtered = q ? fuzzyFilter(this.items, q, pickerItemSearchText) : this.items;
|
|
84
96
|
this.cursor = Math.max(0, Math.min(this.cursor, this.filtered.length - 1));
|
|
85
97
|
}
|
|
86
98
|
|
|
@@ -106,10 +118,13 @@ export class Picker implements Component, Focusable {
|
|
|
106
118
|
const item = visible[i];
|
|
107
119
|
const isCursor = start + i === this.cursor;
|
|
108
120
|
const mark = isCursor ? s.cursorMark("❯ ") : " ";
|
|
109
|
-
const label =
|
|
121
|
+
const label = item.disabled
|
|
122
|
+
? s.dim(item.label)
|
|
123
|
+
: isCursor ? s.selectedLabel(item.label) : s.label(item.label);
|
|
124
|
+
const description = item.description ? s.dim(` — ${item.description}`) : "";
|
|
110
125
|
const line = this.multi
|
|
111
|
-
? mark + (this.selected.has(item.value) ? s.checked("[x] ") : s.unchecked("[ ] ")) + label
|
|
112
|
-
: mark + label;
|
|
126
|
+
? mark + (this.selected.has(item.value) ? s.checked("[x] ") : s.unchecked("[ ] ")) + label + description
|
|
127
|
+
: mark + label + description;
|
|
113
128
|
lines.push(fit(line));
|
|
114
129
|
}
|
|
115
130
|
const more = this.filtered.length > PAGE_SIZE ? " ↑/↓ move • PgUp/PgDn page" : "";
|
|
@@ -121,7 +136,7 @@ export class Picker implements Component, Focusable {
|
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
handleInput(data: string): void {
|
|
124
|
-
const kb =
|
|
139
|
+
const kb = this.keybindings;
|
|
125
140
|
if (kb.matches(data, "tui.select.up")) {
|
|
126
141
|
if (this.filtered.length > 0) this.cursor = this.cursor === 0 ? this.filtered.length - 1 : this.cursor - 1;
|
|
127
142
|
} else if (kb.matches(data, "tui.select.down")) {
|
|
@@ -134,7 +149,7 @@ export class Picker implements Component, Focusable {
|
|
|
134
149
|
if (this.multi) this.cb.onConfirm?.([...this.selected]);
|
|
135
150
|
else {
|
|
136
151
|
const item = this.filtered[this.cursor];
|
|
137
|
-
if (item) this.cb.onSelect?.(item.value);
|
|
152
|
+
if (item && !item.disabled) this.cb.onSelect?.(item.value);
|
|
138
153
|
}
|
|
139
154
|
return;
|
|
140
155
|
} else if (kb.matches(data, "tui.select.cancel")) {
|
|
@@ -203,16 +218,17 @@ export function promptSelectOne(
|
|
|
203
218
|
ctx: PickerContext,
|
|
204
219
|
title: string,
|
|
205
220
|
hint: string,
|
|
206
|
-
items:
|
|
221
|
+
items: PickerItem[],
|
|
222
|
+
initialValue?: string,
|
|
207
223
|
): Promise<string | undefined> {
|
|
208
224
|
if (!requireTui(ctx)) return Promise.resolve(undefined);
|
|
209
|
-
return ctx.ui.custom<string | undefined>((tui, theme,
|
|
225
|
+
return ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
|
|
210
226
|
const styles = makeStyles(theme);
|
|
211
227
|
const header = [styles.title(title), styles.hint(hint)];
|
|
212
|
-
return new Picker(items, false, new Set<string>(), styles, header, tui, {
|
|
228
|
+
return new Picker(items, false, new Set<string>(), styles, header, tui, keybindings, {
|
|
213
229
|
onSelect: (value) => done(value),
|
|
214
230
|
onCancel: () => done(undefined),
|
|
215
|
-
});
|
|
231
|
+
}, initialValue);
|
|
216
232
|
});
|
|
217
233
|
}
|
|
218
234
|
|
|
@@ -221,14 +237,14 @@ export function promptSelectMany(
|
|
|
221
237
|
ctx: PickerContext,
|
|
222
238
|
title: string,
|
|
223
239
|
hint: string,
|
|
224
|
-
items:
|
|
240
|
+
items: PickerItem[],
|
|
225
241
|
initialSelected: readonly string[],
|
|
226
242
|
): Promise<string[] | undefined> {
|
|
227
243
|
if (!requireTui(ctx)) return Promise.resolve(undefined);
|
|
228
|
-
return ctx.ui.custom<string[] | undefined>((tui, theme,
|
|
244
|
+
return ctx.ui.custom<string[] | undefined>((tui, theme, keybindings, done) => {
|
|
229
245
|
const styles = makeStyles(theme);
|
|
230
246
|
const header = [styles.title(title), styles.hint(hint)];
|
|
231
|
-
return new Picker(items, true, new Set<string>(initialSelected), styles, header, tui, {
|
|
247
|
+
return new Picker(items, true, new Set<string>(initialSelected), styles, header, tui, keybindings, {
|
|
232
248
|
onConfirm: (values) => done(values),
|
|
233
249
|
onCancel: () => done(undefined),
|
|
234
250
|
});
|