@tshu-w/pi-agents 0.1.1 → 0.2.1
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 +27 -20
- package/bin/daemon.mjs +32 -14
- package/bin/host.mjs +9 -1
- package/bin/pd.mjs +87 -42
- package/docs/HOSTS.md +111 -0
- package/docs/PROGRAM.md +39 -114
- package/docs/SPEC.md +109 -161
- package/package.json +7 -5
- package/src/agents/agents.ts +129 -55
- package/src/agents/registry.ts +62 -10
- package/src/agents/search.ts +21 -4
- package/src/agents/tool.ts +41 -13
- package/src/daemon/client.mjs +4 -1
- package/src/host/client.mjs +30 -5
- package/src/host/extension.ts +38 -14
- package/src/host/rpc-client.mjs +380 -0
- package/src/index.ts +44 -16
- package/src/output.ts +5 -1
- package/src/programs/codemode.ts +2 -2
- package/src/programs/execute.ts +150 -34
- package/src/programs/host-patches.ts +56 -27
- package/src/programs/loadout.ts +18 -3
- package/src/programs/programs.ts +128 -20
- package/src/programs/run-output.mjs +11 -0
- package/src/programs/sandbox.ts +17 -4
- package/src/programs/scope.ts +15 -7
- package/src/programs/tool.ts +172 -16
- package/src/render-call.ts +113 -15
- package/src/roots/guard.mjs +8 -2
- package/src/roots/paths.mjs +4 -4
- package/src/roots/queue.ts +52 -0
- package/src/roots/report.ts +12 -6
- package/src/roots/rpc.ts +618 -0
- package/src/roots/runtime.ts +17 -2
- package/src/roots/transport.mjs +41 -23
- package/src/ui/message.ts +24 -0
- package/src/ui/panel.ts +39 -7
- package/src/ui/queue.ts +47 -0
- package/src/ui/viewer.ts +182 -38
- package/src/workbench.mjs +91 -51
- package/docs/WORKBENCH.md +0 -167
package/src/ui/viewer.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AssistantMessageComponent,
|
|
3
|
+
BashExecutionComponent,
|
|
4
|
+
BranchSummaryMessageComponent,
|
|
3
5
|
CompactionSummaryMessageComponent,
|
|
4
6
|
CustomEditor,
|
|
5
7
|
CustomMessageComponent,
|
|
@@ -8,7 +10,8 @@ import {
|
|
|
8
10
|
getMarkdownTheme,
|
|
9
11
|
getSelectListTheme,
|
|
10
12
|
keyHint,
|
|
11
|
-
|
|
13
|
+
parseSkillBlock,
|
|
14
|
+
SkillInvocationMessageComponent,
|
|
12
15
|
ToolExecutionComponent,
|
|
13
16
|
UserMessageComponent,
|
|
14
17
|
type AgentSession,
|
|
@@ -22,7 +25,6 @@ import {
|
|
|
22
25
|
fuzzyFilter,
|
|
23
26
|
Input,
|
|
24
27
|
Loader,
|
|
25
|
-
matchesKey,
|
|
26
28
|
SelectList,
|
|
27
29
|
Spacer,
|
|
28
30
|
Text,
|
|
@@ -36,12 +38,22 @@ import {
|
|
|
36
38
|
} from "@earendil-works/pi-tui";
|
|
37
39
|
import { MESSAGE_TYPE, nodes, onTreeChange, programLabel, programsOf, type AgentNode, type Entry } from "../agents/registry.ts";
|
|
38
40
|
import { latestActivity } from "../agents/search.ts";
|
|
41
|
+
import { PROGRAM_TOOL_NAME } from "../programs/execute.ts";
|
|
42
|
+
import type { Programs } from "../programs/programs.ts";
|
|
43
|
+
import { agentSession } from "../roots/rpc.ts";
|
|
39
44
|
import { execFileSync } from "node:child_process";
|
|
40
45
|
|
|
41
46
|
type Message = AgentSession["messages"][number];
|
|
42
47
|
type ToolResult = Parameters<ToolExecutionComponent["updateResult"]>[0];
|
|
43
48
|
type StatusIndicator = Parameters<CustomEditor["setWorkingStatusIndicator"]>[0];
|
|
44
49
|
|
|
50
|
+
/** Stops the elapsed-time timer of a tool component the viewer drops; only a final result stops it otherwise. */
|
|
51
|
+
function stopTimer(tool: ToolExecutionComponent): void {
|
|
52
|
+
const state = (tool as unknown as { rendererState: { interval?: ReturnType<typeof setInterval> } }).rendererState;
|
|
53
|
+
clearInterval(state.interval);
|
|
54
|
+
state.interval = undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
45
57
|
/** A spinner and message for the editor's top border, like Pi's working status. */
|
|
46
58
|
class WorkingStatus extends Loader {
|
|
47
59
|
renderInBorder(width: number): string {
|
|
@@ -86,7 +98,7 @@ class AgentViewer implements Component, Focusable {
|
|
|
86
98
|
private live?: { component: AssistantMessageComponent; calls: string };
|
|
87
99
|
/** Tool components without a result from the last rebuild. */
|
|
88
100
|
private tools = new Map<string, ToolExecutionComponent>();
|
|
89
|
-
private running = new Map<string, {
|
|
101
|
+
private running = new Map<string, { partial?: ToolResult }>();
|
|
90
102
|
private expanded = false;
|
|
91
103
|
/** Lines scrolled up from the end. */
|
|
92
104
|
private scroll = 0;
|
|
@@ -101,7 +113,7 @@ class AgentViewer implements Component, Focusable {
|
|
|
101
113
|
constructor(
|
|
102
114
|
private tui: TUI,
|
|
103
115
|
private theme: Theme,
|
|
104
|
-
keys: KeybindingsManager,
|
|
116
|
+
private keys: KeybindingsManager,
|
|
105
117
|
private ctx: ExtensionContext,
|
|
106
118
|
private owner: AgentNode,
|
|
107
119
|
private entry: Entry,
|
|
@@ -142,8 +154,9 @@ class AgentViewer implements Component, Focusable {
|
|
|
142
154
|
|
|
143
155
|
handleInput(data: string): void {
|
|
144
156
|
const page = Math.max(1, this.tui.terminal.rows - 8);
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
// The page keys of Pi's fullscreen mode.
|
|
158
|
+
if (this.keys.matches(data, "tui.altScreen.pageUp")) this.scroll += page;
|
|
159
|
+
else if (this.keys.matches(data, "tui.altScreen.pageDown")) this.scroll = Math.max(0, this.scroll - page);
|
|
147
160
|
else this.editor.handleInput(data);
|
|
148
161
|
this.tui.requestRender();
|
|
149
162
|
}
|
|
@@ -179,6 +192,7 @@ class AgentViewer implements Component, Focusable {
|
|
|
179
192
|
dispose(): void {
|
|
180
193
|
this.indicator?.dispose();
|
|
181
194
|
this.footer.dispose();
|
|
195
|
+
for (const tool of this.tools.values()) stopTimer(tool);
|
|
182
196
|
for (const dispose of this.disposers.splice(0)) dispose();
|
|
183
197
|
}
|
|
184
198
|
|
|
@@ -206,7 +220,7 @@ class AgentViewer implements Component, Focusable {
|
|
|
206
220
|
this.disposers.push(session.subscribe((event) => {
|
|
207
221
|
if (event.type === "message_start" || event.type === "message_update") this.streaming = event.message;
|
|
208
222
|
else if (event.type === "message_end") this.streaming = undefined;
|
|
209
|
-
else if (event.type === "tool_execution_start") this.running.set(event.toolCallId, {
|
|
223
|
+
else if (event.type === "tool_execution_start") this.running.set(event.toolCallId, {});
|
|
210
224
|
else if (event.type === "tool_execution_update") {
|
|
211
225
|
const tool = this.running.get(event.toolCallId);
|
|
212
226
|
if (tool) tool.partial = event.partialResult as ToolResult;
|
|
@@ -223,7 +237,7 @@ class AgentViewer implements Component, Focusable {
|
|
|
223
237
|
if (!this.live || event.message.role !== "assistant") return false;
|
|
224
238
|
const calls = event.message.content.flatMap((part) => part.type === "toolCall" ? [part] : []);
|
|
225
239
|
if (calls.map((call) => call.id).join("\n") !== this.live.calls) return false;
|
|
226
|
-
this.live.component.updateContent(event.message);
|
|
240
|
+
this.live.component.updateContent(event.message, true);
|
|
227
241
|
for (const call of calls) this.tools.get(call.id)?.updateArgs(call.arguments);
|
|
228
242
|
return true;
|
|
229
243
|
}
|
|
@@ -243,20 +257,19 @@ class AgentViewer implements Component, Focusable {
|
|
|
243
257
|
this.tui.requestRender();
|
|
244
258
|
}
|
|
245
259
|
|
|
246
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* The working status in the editor's border, and its color from the Agent's thinking level, as in Pi: `Working`,
|
|
262
|
+
* spinner and text in the border's color.
|
|
263
|
+
*/
|
|
247
264
|
private syncStatus(conversation: ReturnType<AgentNode["agents"]["conversation"]>): void {
|
|
248
265
|
this.editor.borderColor = this.theme.getThinkingBorderColor((conversation?.thinkingLevel ?? "off") as never);
|
|
249
266
|
const state = this.owner.agents.counts(this.entry.id)?.state;
|
|
250
|
-
const
|
|
251
|
-
const tool = [...this.running.values()].at(-1);
|
|
252
|
-
const abort = `${keyText("app.interrupt")} to abort`;
|
|
253
|
-
const text = !busy ? "" : tool ? `Running ${tool.name}... (${abort})` : `Working... (${abort})`;
|
|
267
|
+
const text = state === "running" || state === "waiting" ? "Working" : "";
|
|
254
268
|
if (text === this.status) return;
|
|
255
269
|
this.status = text;
|
|
256
270
|
this.indicator?.dispose();
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
: undefined;
|
|
271
|
+
const color = (part: string) => this.editor.borderColor(part);
|
|
272
|
+
this.indicator = text ? new WorkingStatus(this.tui, color, color, text) : undefined;
|
|
260
273
|
this.editor.setWorkingStatusIndicator(this.indicator as unknown as StatusIndicator);
|
|
261
274
|
}
|
|
262
275
|
|
|
@@ -264,6 +277,7 @@ class AgentViewer implements Component, Focusable {
|
|
|
264
277
|
this.dirty = false;
|
|
265
278
|
this.transcript.clear();
|
|
266
279
|
this.live = undefined;
|
|
280
|
+
for (const tool of this.tools.values()) stopTimer(tool);
|
|
267
281
|
this.tools.clear();
|
|
268
282
|
if (!conversation) return;
|
|
269
283
|
const messages = [...conversation.messages];
|
|
@@ -273,23 +287,33 @@ class AgentViewer implements Component, Focusable {
|
|
|
273
287
|
this.outbox = this.outbox.filter(({ text, after }) => working && !messages.slice(after).some((message) =>
|
|
274
288
|
message.role === "custom" && message.content === text && (message.details as { user?: boolean } | undefined)?.user));
|
|
275
289
|
if (this.streaming && !messages.includes(this.streaming)) messages.push(this.streaming);
|
|
276
|
-
|
|
290
|
+
// As Pi's chat shows them, with the display settings of the Session the viewer is in.
|
|
291
|
+
const current = agentSession(this.ctx.sessionManager);
|
|
292
|
+
const settings = current?.settingsManager;
|
|
293
|
+
const markdown = settings ? { ...getMarkdownTheme(), codeBlockIndent: settings.getCodeBlockIndent() } : getMarkdownTheme();
|
|
294
|
+
const transformers = current?.extensionRunner.getMarkdownTransformers();
|
|
295
|
+
const pad = settings?.getOutputPad() ?? 1;
|
|
296
|
+
const runner = (conversation.session ?? current)?.extensionRunner;
|
|
297
|
+
const images = { showImages: settings?.getShowImages() ?? false, imageWidthCells: settings?.getImageWidthCells(), outputPad: pad };
|
|
277
298
|
for (const message of messages) {
|
|
278
299
|
if (message.role === "assistant") {
|
|
279
|
-
const component = new AssistantMessageComponent(message, false, markdown);
|
|
300
|
+
const component = new AssistantMessageComponent(message, settings?.getHideThinkingBlock() ?? false, markdown, undefined, pad, transformers);
|
|
280
301
|
if (message === this.streaming) {
|
|
302
|
+
// As Pi does, so Markdown transformers know the message is still streaming.
|
|
303
|
+
component.updateContent(message, true);
|
|
281
304
|
const calls = message.content.flatMap((part) => part.type === "toolCall" ? [part.id] : []);
|
|
282
305
|
this.live = { component, calls: calls.join("\n") };
|
|
283
306
|
}
|
|
284
307
|
this.transcript.addChild(component);
|
|
285
308
|
for (const part of message.content) {
|
|
286
309
|
if (part.type !== "toolCall") continue;
|
|
287
|
-
const tool = new ToolExecutionComponent(part.name, part.id, part.arguments,
|
|
310
|
+
const tool = new ToolExecutionComponent(part.name, part.id, part.arguments, images,
|
|
288
311
|
conversation.session?.getToolDefinition(part.name), this.tui, conversation.cwd);
|
|
289
312
|
tool.setExpanded(this.expanded);
|
|
290
313
|
this.transcript.addChild(tool);
|
|
291
314
|
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
292
|
-
|
|
315
|
+
const text = message.stopReason === "aborted" ? "Operation aborted" : message.errorMessage || "Error";
|
|
316
|
+
tool.updateResult({ content: [{ type: "text", text }], isError: true });
|
|
293
317
|
} else {
|
|
294
318
|
this.tools.set(part.id, tool);
|
|
295
319
|
}
|
|
@@ -300,23 +324,45 @@ class AgentViewer implements Component, Focusable {
|
|
|
300
324
|
} else if (message.role === "user") {
|
|
301
325
|
const text = typeof message.content === "string"
|
|
302
326
|
? message.content
|
|
303
|
-
: message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("
|
|
327
|
+
: message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("");
|
|
304
328
|
if (!text) continue;
|
|
305
329
|
this.transcript.addChild(new Spacer(1));
|
|
306
|
-
|
|
330
|
+
const skill = parseSkillBlock(text);
|
|
331
|
+
if (skill) {
|
|
332
|
+
const component = new SkillInvocationMessageComponent(skill, markdown, pad);
|
|
333
|
+
component.setExpanded(this.expanded);
|
|
334
|
+
this.transcript.addChild(component);
|
|
335
|
+
if (skill.userMessage) {
|
|
336
|
+
this.transcript.addChild(new Spacer(1));
|
|
337
|
+
this.transcript.addChild(new UserMessageComponent(skill.userMessage, markdown, pad, transformers));
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
this.transcript.addChild(new UserMessageComponent(text, markdown, pad, transformers));
|
|
341
|
+
}
|
|
307
342
|
} else if (message.role === "custom" && message.display) {
|
|
308
343
|
const user = message.customType === MESSAGE_TYPE && (message.details as { user?: boolean } | undefined)?.user;
|
|
309
|
-
this.transcript.addChild(new Spacer(1));
|
|
310
344
|
if (user && typeof message.content === "string") {
|
|
311
|
-
this.transcript.addChild(new
|
|
345
|
+
this.transcript.addChild(new Spacer(1));
|
|
346
|
+
this.transcript.addChild(new UserMessageComponent(message.content, markdown, pad, transformers));
|
|
312
347
|
} else {
|
|
313
|
-
|
|
348
|
+
// The component starts with its own blank line.
|
|
349
|
+
const component = new CustomMessageComponent(message, runner?.getMessageRenderer(message.customType), markdown, pad);
|
|
314
350
|
component.setExpanded(this.expanded);
|
|
315
351
|
this.transcript.addChild(component);
|
|
316
352
|
}
|
|
317
|
-
} else if (message.role === "
|
|
353
|
+
} else if (message.role === "bashExecution") {
|
|
354
|
+
const component = new BashExecutionComponent(message.command, this.tui, message.excludeFromContext, pad);
|
|
355
|
+
if (message.output) component.appendOutput(message.output);
|
|
356
|
+
component.setComplete(message.exitCode, message.cancelled, message.truncated ? { truncated: true } as never : undefined, message.fullOutputPath);
|
|
357
|
+
component.setExpanded(this.expanded);
|
|
358
|
+
this.transcript.addChild(component);
|
|
359
|
+
} else if (message.role === "compactionSummary" || message.role === "branchSummary") {
|
|
318
360
|
this.transcript.addChild(new Spacer(1));
|
|
319
|
-
|
|
361
|
+
const component = message.role === "compactionSummary"
|
|
362
|
+
? new CompactionSummaryMessageComponent(message, markdown, pad)
|
|
363
|
+
: new BranchSummaryMessageComponent(message, markdown, pad);
|
|
364
|
+
component.setExpanded(this.expanded);
|
|
365
|
+
this.transcript.addChild(component);
|
|
320
366
|
}
|
|
321
367
|
}
|
|
322
368
|
for (const [id, tool] of this.tools) {
|
|
@@ -328,6 +374,82 @@ class AgentViewer implements Component, Focusable {
|
|
|
328
374
|
}
|
|
329
375
|
}
|
|
330
376
|
|
|
377
|
+
/** The user's view of one background Program, as its tool call shows in the foreground. */
|
|
378
|
+
class ProgramViewer implements Component, Focusable {
|
|
379
|
+
private readonly tool: ToolExecutionComponent;
|
|
380
|
+
private expanded = false;
|
|
381
|
+
/** Lines scrolled up from the end. */
|
|
382
|
+
private scroll = 0;
|
|
383
|
+
private disposers: Array<() => void> = [];
|
|
384
|
+
focused = false;
|
|
385
|
+
|
|
386
|
+
constructor(
|
|
387
|
+
private tui: TUI,
|
|
388
|
+
private theme: Theme,
|
|
389
|
+
private keys: KeybindingsManager,
|
|
390
|
+
private ctx: ExtensionContext,
|
|
391
|
+
private programs: Programs,
|
|
392
|
+
private id: string,
|
|
393
|
+
private done: () => void,
|
|
394
|
+
) {
|
|
395
|
+
const code = programs.view(id)?.code;
|
|
396
|
+
const settings = agentSession(ctx.sessionManager)?.settingsManager;
|
|
397
|
+
this.tool = new ToolExecutionComponent(PROGRAM_TOOL_NAME, `program-${id}`, { action: "run", background: true, ...(code !== undefined && { code }) }, {
|
|
398
|
+
showImages: settings?.getShowImages() ?? false, imageWidthCells: settings?.getImageWidthCells(), outputPad: settings?.getOutputPad(),
|
|
399
|
+
}, agentSession(ctx.sessionManager)?.getToolDefinition(PROGRAM_TOOL_NAME), tui, ctx.cwd);
|
|
400
|
+
this.tool.markExecutionStarted();
|
|
401
|
+
this.sync();
|
|
402
|
+
this.disposers.push(programs.onView(() => {
|
|
403
|
+
this.sync();
|
|
404
|
+
this.tui.requestRender();
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
handleInput(data: string): void {
|
|
409
|
+
const page = Math.max(1, this.tui.terminal.rows - 4);
|
|
410
|
+
if (this.keys.matches(data, "app.clear" as never)) return this.done();
|
|
411
|
+
if (this.keys.matches(data, "app.interrupt" as never)) this.stop();
|
|
412
|
+
else if (this.keys.matches(data, "app.tools.expand" as never)) {
|
|
413
|
+
this.expanded = !this.expanded;
|
|
414
|
+
this.tool.setExpanded(this.expanded);
|
|
415
|
+
} else if (this.keys.matches(data, "tui.altScreen.pageUp")) this.scroll += page;
|
|
416
|
+
else if (this.keys.matches(data, "tui.altScreen.pageDown")) this.scroll = Math.max(0, this.scroll - page);
|
|
417
|
+
this.tui.requestRender();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
render(width: number): string[] {
|
|
421
|
+
const running = this.programs.view(this.id)?.record.state === "running";
|
|
422
|
+
const hints = [...running ? [keyHint("app.interrupt", "stop")] : [], keyHint("app.clear", "back")].join(" ");
|
|
423
|
+
const footer = truncateToWidth(" ".repeat(Math.max(0, width - visibleWidth(hints))) + hints, width);
|
|
424
|
+
const border = new DynamicBorder().render(width);
|
|
425
|
+
const budget = Math.max(1, this.tui.terminal.rows - border.length - 2);
|
|
426
|
+
const lines = this.tool.render(width);
|
|
427
|
+
this.scroll = Math.min(this.scroll, Math.max(0, lines.length - budget));
|
|
428
|
+
const end = lines.length - this.scroll;
|
|
429
|
+
// As tall as its content, so the Session shows above it.
|
|
430
|
+
return [...border, ...lines.slice(Math.max(0, end - budget), end), " ".repeat(width), footer];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
invalidate(): void {
|
|
434
|
+
this.tool.invalidate();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
dispose(): void {
|
|
438
|
+
stopTimer(this.tool);
|
|
439
|
+
for (const dispose of this.disposers.splice(0)) dispose();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private sync(): void {
|
|
443
|
+
const view = this.programs.view(this.id);
|
|
444
|
+
if (view?.result) this.tool.updateResult(view.result as ToolResult, view.record.state === "running");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private stop(): void {
|
|
448
|
+
this.programs.stop(this.programs.target(this.id)).catch((error: unknown) =>
|
|
449
|
+
this.ctx.ui.notify(error instanceof Error ? error.message : String(error), "error"));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
331
453
|
/** A filterable list in place of the editor, like Pi Durable's conversation switcher. */
|
|
332
454
|
class ListSelector extends Container implements Focusable {
|
|
333
455
|
private readonly input = new Input();
|
|
@@ -404,12 +526,17 @@ export function ownedEntries(self: AgentNode): Entry[] {
|
|
|
404
526
|
return entries.sort((a, b) => Number(b.state !== "idle") - Number(a.state !== "idle") || (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
|
|
405
527
|
}
|
|
406
528
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
529
|
+
const PROGRAM_ITEM = "program:";
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* `/tasks`: picks an Agent in the tree or a background Program, live ones first and newest first, and opens it in
|
|
533
|
+
* the viewer. `paddingX` is Pi's editor padding, so the viewer's editor lines up as Pi's does.
|
|
534
|
+
*/
|
|
535
|
+
export async function openTaskViewer(ctx: ExtensionContext, self: AgentNode, programs: Programs | undefined, paddingX: number): Promise<void> {
|
|
410
536
|
const entries = ownedEntries(self);
|
|
411
|
-
|
|
412
|
-
|
|
537
|
+
const records = programs?.list() ?? [];
|
|
538
|
+
if (entries.length === 0 && records.length === 0) {
|
|
539
|
+
ctx.ui.notify("No tasks.", "info");
|
|
413
540
|
return;
|
|
414
541
|
}
|
|
415
542
|
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
@@ -421,13 +548,30 @@ export async function openAgentViewer(ctx: ExtensionContext, self: AgentNode, pa
|
|
|
421
548
|
return program?.program ? ` ‹ ${programLabel(program)}${owners(program.ownerId)}` : "";
|
|
422
549
|
};
|
|
423
550
|
const path = (entry: Entry): string => `${entry.name ?? entry.id}${owners(entry.ownerId)}`;
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
551
|
+
const tasks = [
|
|
552
|
+
...entries.map((entry) => ({ live: entry.state !== "idle", at: entry.createdAt ?? "", item: {
|
|
553
|
+
value: entry.id,
|
|
554
|
+
label: path(entry),
|
|
555
|
+
description: [entry.state, latestActivity(entry.sessionFile) ?? ""].filter(Boolean).join(" "),
|
|
556
|
+
} })),
|
|
557
|
+
...records.map((record) => ({ live: record.state === "running", at: new Date(record.startedAt).toISOString(), item: {
|
|
558
|
+
value: PROGRAM_ITEM + record.id,
|
|
559
|
+
label: `Program ${programs!.label(record)}`,
|
|
560
|
+
description: record.state,
|
|
561
|
+
} })),
|
|
562
|
+
];
|
|
563
|
+
const items: SelectItem[] = tasks
|
|
564
|
+
.sort((a, b) => Number(b.live) - Number(a.live) || b.at.localeCompare(a.at))
|
|
565
|
+
.map((task) => task.item);
|
|
429
566
|
const id = await ctx.ui.custom<string | undefined>((_tui, theme, keys, done) =>
|
|
430
|
-
new ListSelector("Open
|
|
567
|
+
new ListSelector("Open task:", items, theme, keys, (value) => done(value), () => done(undefined)));
|
|
568
|
+
if (id?.startsWith(PROGRAM_ITEM) && programs) {
|
|
569
|
+
await ctx.ui.custom<void>(
|
|
570
|
+
(tui, theme, keys, done) => new ProgramViewer(tui, theme, keys, ctx, programs, id.slice(PROGRAM_ITEM.length), () => done()),
|
|
571
|
+
{ overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", anchor: "bottom-center" } },
|
|
572
|
+
);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
431
575
|
const entry = id === undefined ? undefined : byId.get(id);
|
|
432
576
|
const owner = entry?.ownerId === undefined ? undefined : nodes.get(entry.ownerId);
|
|
433
577
|
if (!entry || !owner) return;
|