@duetso/agent 0.1.34 → 0.1.35
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/dist/package.json +1 -1
- package/dist/src/cli/env.d.ts +18 -0
- package/dist/src/cli/env.d.ts.map +1 -0
- package/dist/src/cli/env.js +114 -0
- package/dist/src/cli/env.js.map +1 -0
- package/dist/src/cli/help.d.ts +8 -0
- package/dist/src/cli/help.d.ts.map +1 -0
- package/dist/src/cli/help.js +175 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/login.d.ts +13 -0
- package/dist/src/cli/login.d.ts.map +1 -0
- package/dist/src/cli/login.js +61 -0
- package/dist/src/cli/login.js.map +1 -0
- package/dist/src/cli/memories-db.d.ts +24 -0
- package/dist/src/cli/memories-db.d.ts.map +1 -0
- package/dist/src/cli/memories-db.js +74 -0
- package/dist/src/cli/memories-db.js.map +1 -0
- package/dist/src/cli/memories-tui.d.ts +11 -0
- package/dist/src/cli/memories-tui.d.ts.map +1 -0
- package/dist/src/cli/memories-tui.js +266 -0
- package/dist/src/cli/memories-tui.js.map +1 -0
- package/dist/src/cli/memories.d.ts +9 -0
- package/dist/src/cli/memories.d.ts.map +1 -0
- package/dist/src/cli/memories.js +38 -0
- package/dist/src/cli/memories.js.map +1 -0
- package/dist/src/cli/package-manager.d.ts +38 -0
- package/dist/src/cli/package-manager.d.ts.map +1 -0
- package/dist/src/cli/package-manager.js +78 -0
- package/dist/src/cli/package-manager.js.map +1 -0
- package/dist/src/cli/resume-hint.d.ts +22 -0
- package/dist/src/cli/resume-hint.d.ts.map +1 -0
- package/dist/src/cli/resume-hint.js +61 -0
- package/dist/src/cli/resume-hint.js.map +1 -0
- package/dist/src/cli/run.d.ts +43 -0
- package/dist/src/cli/run.d.ts.map +1 -0
- package/dist/src/cli/run.js +273 -0
- package/dist/src/cli/run.js.map +1 -0
- package/dist/src/cli/shared.d.ts +55 -0
- package/dist/src/cli/shared.d.ts.map +1 -0
- package/dist/src/cli/shared.js +125 -0
- package/dist/src/cli/shared.js.map +1 -0
- package/dist/src/cli/skills.d.ts +10 -0
- package/dist/src/cli/skills.d.ts.map +1 -0
- package/dist/src/cli/skills.js +42 -0
- package/dist/src/cli/skills.js.map +1 -0
- package/dist/src/cli/upgrade.d.ts +9 -0
- package/dist/src/cli/upgrade.d.ts.map +1 -0
- package/dist/src/cli/upgrade.js +52 -0
- package/dist/src/cli/upgrade.js.map +1 -0
- package/dist/src/cli/version-check.d.ts +22 -0
- package/dist/src/cli/version-check.d.ts.map +1 -0
- package/dist/src/cli/version-check.js +78 -0
- package/dist/src/cli/version-check.js.map +1 -0
- package/dist/src/cli.d.ts +20 -63
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +38 -887
- package/dist/src/cli.js.map +1 -1
- package/dist/src/memory/observational-prompts.d.ts.map +1 -1
- package/dist/src/memory/observational-prompts.js +11 -7
- package/dist/src/memory/observational-prompts.js.map +1 -1
- package/dist/src/tui/app.d.ts +7 -47
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +204 -389
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/autocomplete.d.ts +91 -0
- package/dist/src/tui/autocomplete.d.ts.map +1 -0
- package/dist/src/tui/autocomplete.js +177 -0
- package/dist/src/tui/autocomplete.js.map +1 -0
- package/dist/src/tui/file-index.d.ts +11 -0
- package/dist/src/tui/file-index.d.ts.map +1 -0
- package/dist/src/tui/file-index.js +75 -0
- package/dist/src/tui/file-index.js.map +1 -0
- package/dist/src/tui/history.d.ts +50 -0
- package/dist/src/tui/history.d.ts.map +1 -0
- package/dist/src/tui/history.js +132 -0
- package/dist/src/tui/history.js.map +1 -0
- package/dist/src/tui/sidebar.d.ts +20 -0
- package/dist/src/tui/sidebar.d.ts.map +1 -0
- package/dist/src/tui/sidebar.js +118 -0
- package/dist/src/tui/sidebar.js.map +1 -0
- package/dist/src/tui/theme.d.ts +15 -0
- package/dist/src/tui/theme.d.ts.map +1 -0
- package/dist/src/tui/theme.js +18 -0
- package/dist/src/tui/theme.js.map +1 -0
- package/dist/src/turn-runner/prompts.d.ts.map +1 -1
- package/dist/src/turn-runner/prompts.js +7 -0
- package/dist/src/turn-runner/prompts.js.map +1 -1
- package/dist/src/turn-runner/tools.d.ts +15 -1
- package/dist/src/turn-runner/tools.d.ts.map +1 -1
- package/dist/src/turn-runner/tools.js +42 -9
- package/dist/src/turn-runner/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/src/tui/app.js
CHANGED
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
import { BoxRenderable, createCliRenderer, fg, ScrollBoxRenderable, t, TextRenderable, TextareaRenderable, } from "@opentui/core";
|
|
2
2
|
import { formatCompactJson } from "../lib/compact-json.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const HINT_RUNNING = "Enter: steer · Shift+Enter: queue follow-up · Esc: interrupt and quit · Ctrl+C: force quit";
|
|
17
|
-
const SKILL_AUTOCOMPLETE_LIMIT = 8;
|
|
18
|
-
const SKILL_AUTOCOMPLETE_TOKEN = /^\/([A-Za-z0-9_.-]*)$/;
|
|
19
|
-
const SKILL_AUTOCOMPLETE_DESCRIPTION_WIDTH = 72;
|
|
20
|
-
const SKILL_AUTOCOMPLETE_DESCRIPTION_LINES = 2;
|
|
21
|
-
const QUESTION_OPTION_LIMIT = 8;
|
|
22
|
-
const QUESTION_OPTION_DESCRIPTION_WIDTH = 72;
|
|
3
|
+
import { activeFileAutocompleteToken, activeSkillAutocompleteToken, AUTOCOMPLETE_LIMITS, fileAutocompleteMatches, formatQuestionOptionDescription, formatSkillAutocompleteDescription, moveQuestionOptionSelection, moveSkillAutocompleteSelection, questionPickerAnswerPayload, skillAutocompleteMatches, } from "./autocomplete.js";
|
|
4
|
+
import { buildFileIndex } from "./file-index.js";
|
|
5
|
+
import { historyDisplayBlocks, limitHistoryDisplayBlocks, startupHeaderLines, } from "./history.js";
|
|
6
|
+
import { createSidebar } from "./sidebar.js";
|
|
7
|
+
import { COLORS, HINT_IDLE, HINT_RUNNING } from "./theme.js";
|
|
8
|
+
// Re-exports preserve the historical `tui/app.js` entry point used by tests
|
|
9
|
+
// and external callers; the implementations live in focused leaf modules.
|
|
10
|
+
export { activeFileAutocompleteToken, activeSkillAutocompleteToken, fileAutocompleteMatches, formatQuestionOptionDescription, formatSkillAutocompleteDescription, moveQuestionOptionSelection, moveSkillAutocompleteSelection, questionPickerAnswerPayload, replaceFileAutocompleteToken, replaceSkillAutocompleteToken, skillAutocompleteMatches, } from "./autocomplete.js";
|
|
11
|
+
export { formatSkillAutocompleteItem } from "./autocomplete.js";
|
|
12
|
+
export { historyDisplayBlocks, limitHistoryDisplayBlocks, startupHeaderLines } from "./history.js";
|
|
13
|
+
const SKILL_AUTOCOMPLETE_LIMIT = AUTOCOMPLETE_LIMITS.skill;
|
|
14
|
+
const FILE_AUTOCOMPLETE_LIMIT = AUTOCOMPLETE_LIMITS.file;
|
|
15
|
+
const QUESTION_OPTION_LIMIT = AUTOCOMPLETE_LIMITS.questionOption;
|
|
23
16
|
/**
|
|
24
17
|
* Runs the interactive TUI for a session. Resolves with the most recent
|
|
25
18
|
* terminal event (if any) when the user exits the UI.
|
|
@@ -49,84 +42,7 @@ export async function runTui(input) {
|
|
|
49
42
|
flexShrink: 1,
|
|
50
43
|
height: "100%",
|
|
51
44
|
});
|
|
52
|
-
|
|
53
|
-
// squashing the transcript. The two panels stack vertically inside.
|
|
54
|
-
const sidebar = new BoxRenderable(renderer, {
|
|
55
|
-
flexDirection: "column",
|
|
56
|
-
width: 36,
|
|
57
|
-
height: "100%",
|
|
58
|
-
flexShrink: 0,
|
|
59
|
-
});
|
|
60
|
-
const todoPanel = new BoxRenderable(renderer, {
|
|
61
|
-
flexDirection: "column",
|
|
62
|
-
border: true,
|
|
63
|
-
borderColor: COLORS.border,
|
|
64
|
-
padding: 1,
|
|
65
|
-
flexGrow: 1,
|
|
66
|
-
flexShrink: 1,
|
|
67
|
-
});
|
|
68
|
-
const todoTitle = new TextRenderable(renderer, {
|
|
69
|
-
content: "todos",
|
|
70
|
-
fg: COLORS.status,
|
|
71
|
-
height: 1,
|
|
72
|
-
flexShrink: 0,
|
|
73
|
-
});
|
|
74
|
-
const todoBody = new TextRenderable(renderer, {
|
|
75
|
-
content: "(none)",
|
|
76
|
-
fg: COLORS.hint,
|
|
77
|
-
flexGrow: 1,
|
|
78
|
-
flexShrink: 1,
|
|
79
|
-
});
|
|
80
|
-
todoPanel.add(todoTitle);
|
|
81
|
-
todoPanel.add(todoBody);
|
|
82
|
-
const smPanel = new BoxRenderable(renderer, {
|
|
83
|
-
flexDirection: "column",
|
|
84
|
-
border: true,
|
|
85
|
-
borderColor: COLORS.border,
|
|
86
|
-
padding: 1,
|
|
87
|
-
flexGrow: 1,
|
|
88
|
-
flexShrink: 1,
|
|
89
|
-
});
|
|
90
|
-
const smTitle = new TextRenderable(renderer, {
|
|
91
|
-
content: "state machine",
|
|
92
|
-
fg: COLORS.status,
|
|
93
|
-
height: 1,
|
|
94
|
-
flexShrink: 0,
|
|
95
|
-
});
|
|
96
|
-
const smBody = new TextRenderable(renderer, {
|
|
97
|
-
content: "(inactive)",
|
|
98
|
-
fg: COLORS.hint,
|
|
99
|
-
flexGrow: 1,
|
|
100
|
-
flexShrink: 1,
|
|
101
|
-
});
|
|
102
|
-
smPanel.add(smTitle);
|
|
103
|
-
smPanel.add(smBody);
|
|
104
|
-
const contextPanel = new BoxRenderable(renderer, {
|
|
105
|
-
flexDirection: "column",
|
|
106
|
-
border: true,
|
|
107
|
-
borderColor: COLORS.border,
|
|
108
|
-
paddingLeft: 1,
|
|
109
|
-
paddingRight: 1,
|
|
110
|
-
height: 5,
|
|
111
|
-
flexShrink: 0,
|
|
112
|
-
});
|
|
113
|
-
const contextTitle = new TextRenderable(renderer, {
|
|
114
|
-
content: "context",
|
|
115
|
-
fg: COLORS.status,
|
|
116
|
-
height: 1,
|
|
117
|
-
flexShrink: 0,
|
|
118
|
-
});
|
|
119
|
-
const contextBody = new TextRenderable(renderer, {
|
|
120
|
-
content: "(waiting for usage)",
|
|
121
|
-
fg: COLORS.hint,
|
|
122
|
-
flexGrow: 1,
|
|
123
|
-
flexShrink: 1,
|
|
124
|
-
});
|
|
125
|
-
contextPanel.add(contextTitle);
|
|
126
|
-
contextPanel.add(contextBody);
|
|
127
|
-
sidebar.add(todoPanel);
|
|
128
|
-
sidebar.add(smPanel);
|
|
129
|
-
sidebar.add(contextPanel);
|
|
45
|
+
const sidebar = createSidebar(renderer);
|
|
130
46
|
const transcript = new ScrollBoxRenderable(renderer, {
|
|
131
47
|
flexGrow: 1,
|
|
132
48
|
flexShrink: 1,
|
|
@@ -176,6 +92,37 @@ export async function runTui(input) {
|
|
|
176
92
|
for (const row of skillAutocompleteRows) {
|
|
177
93
|
skillAutocompletePanel.add(row);
|
|
178
94
|
}
|
|
95
|
+
// The @-file picker mirrors the slash picker's structure so the renderer
|
|
96
|
+
// logic and key handling can stay parallel between the two pickers.
|
|
97
|
+
const fileAutocompletePanel = new BoxRenderable(renderer, {
|
|
98
|
+
flexDirection: "column",
|
|
99
|
+
border: true,
|
|
100
|
+
borderColor: COLORS.border,
|
|
101
|
+
paddingLeft: 1,
|
|
102
|
+
paddingRight: 1,
|
|
103
|
+
flexShrink: 0,
|
|
104
|
+
});
|
|
105
|
+
fileAutocompletePanel.visible = false;
|
|
106
|
+
const fileAutocompleteTitle = new TextRenderable(renderer, {
|
|
107
|
+
content: "files",
|
|
108
|
+
fg: COLORS.status,
|
|
109
|
+
height: 1,
|
|
110
|
+
flexShrink: 0,
|
|
111
|
+
});
|
|
112
|
+
const fileAutocompleteRows = Array.from({ length: FILE_AUTOCOMPLETE_LIMIT }, () => {
|
|
113
|
+
const row = new TextRenderable(renderer, {
|
|
114
|
+
content: "",
|
|
115
|
+
fg: COLORS.hint,
|
|
116
|
+
height: 1,
|
|
117
|
+
flexShrink: 0,
|
|
118
|
+
});
|
|
119
|
+
row.visible = false;
|
|
120
|
+
return row;
|
|
121
|
+
});
|
|
122
|
+
fileAutocompletePanel.add(fileAutocompleteTitle);
|
|
123
|
+
for (const row of fileAutocompleteRows) {
|
|
124
|
+
fileAutocompletePanel.add(row);
|
|
125
|
+
}
|
|
179
126
|
const questionPanel = new BoxRenderable(renderer, {
|
|
180
127
|
flexDirection: "column",
|
|
181
128
|
border: true,
|
|
@@ -239,10 +186,11 @@ export async function runTui(input) {
|
|
|
239
186
|
layout.add(status);
|
|
240
187
|
layout.add(hint);
|
|
241
188
|
layout.add(skillAutocompletePanel);
|
|
189
|
+
layout.add(fileAutocompletePanel);
|
|
242
190
|
layout.add(questionPanel);
|
|
243
191
|
layout.add(inputBox);
|
|
244
192
|
root.add(layout);
|
|
245
|
-
root.add(sidebar);
|
|
193
|
+
root.add(sidebar.view);
|
|
246
194
|
renderer.root.add(root);
|
|
247
195
|
inputField.focus();
|
|
248
196
|
// ---- transcript helpers ----------------------------------------------------
|
|
@@ -263,7 +211,6 @@ export async function runTui(input) {
|
|
|
263
211
|
function appendLine(content, fg) {
|
|
264
212
|
if (!content)
|
|
265
213
|
return;
|
|
266
|
-
// ScrollBox children stack vertically; one Text per logical line keeps wrapping simple.
|
|
267
214
|
const line = new TextRenderable(renderer, { content, fg });
|
|
268
215
|
transcript.add(line);
|
|
269
216
|
scrollToBottomSoon();
|
|
@@ -387,80 +334,11 @@ export async function runTui(input) {
|
|
|
387
334
|
// ---- session subscription --------------------------------------------------
|
|
388
335
|
function refreshSidebar() {
|
|
389
336
|
const state = input.session.getState();
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
function renderTodoSidebar(todos) {
|
|
395
|
-
if (todos.length === 0) {
|
|
396
|
-
todoBody.content = "(none)";
|
|
397
|
-
todoBody.fg = COLORS.hint;
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
const lines = todos.map((todo) => `${todoStatusGlyph(todo.status)} ${todo.content}`);
|
|
401
|
-
todoBody.content = lines.join("\n");
|
|
402
|
-
todoBody.fg = COLORS.agent;
|
|
403
|
-
}
|
|
404
|
-
function todoStatusGlyph(status) {
|
|
405
|
-
if (status === "completed")
|
|
406
|
-
return "✓";
|
|
407
|
-
if (status === "in_progress")
|
|
408
|
-
return "●";
|
|
409
|
-
if (status === "failed")
|
|
410
|
-
return "✗";
|
|
411
|
-
return "○";
|
|
412
|
-
}
|
|
413
|
-
function renderStateMachineSidebar(session) {
|
|
414
|
-
if (!session) {
|
|
415
|
-
smBody.content = "(inactive)";
|
|
416
|
-
smBody.fg = COLORS.hint;
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
const current = session.currentState;
|
|
420
|
-
const lines = session.definition.states.map((state) => {
|
|
421
|
-
const marker = state.name === current ? "▶" : " ";
|
|
422
|
-
return `${marker} ${state.name}`;
|
|
423
|
-
});
|
|
424
|
-
if (session.terminal) {
|
|
425
|
-
lines.push("", `terminal: ${session.terminal.status}`);
|
|
426
|
-
}
|
|
427
|
-
smBody.content = lines.join("\n");
|
|
428
|
-
smBody.fg = COLORS.agent;
|
|
429
|
-
}
|
|
430
|
-
function renderContextUsageSidebar(usage) {
|
|
431
|
-
if (!usage) {
|
|
432
|
-
contextBody.content = "(waiting for usage)";
|
|
433
|
-
contextBody.fg = COLORS.hint;
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
const usedTokens = usage.usage.totalTokens;
|
|
437
|
-
const percent = Math.min(1, usedTokens / usage.contextWindow);
|
|
438
|
-
contextBody.content = [
|
|
439
|
-
progressBar(percent, 25),
|
|
440
|
-
`${formatTokenCount(usedTokens)} / ${formatTokenCount(usage.contextWindow)}`,
|
|
441
|
-
].join("\n");
|
|
442
|
-
contextBody.fg = usedTokens >= usage.contextWindow ? COLORS.error : COLORS.agent;
|
|
443
|
-
}
|
|
444
|
-
function progressBar(value, width) {
|
|
445
|
-
const clamped = Math.max(0, Math.min(1, value));
|
|
446
|
-
const filled = Math.round(clamped * width);
|
|
447
|
-
const empty = width - filled;
|
|
448
|
-
return `[${"█".repeat(filled)}${"░".repeat(empty)}] ${`${Math.round(clamped * 100)}%`.padStart(4)}`;
|
|
449
|
-
}
|
|
450
|
-
function formatTokenCount(tokens) {
|
|
451
|
-
if (tokens >= 1_000_000)
|
|
452
|
-
return `${formatCompactNumber(tokens / 1_000_000)}m`;
|
|
453
|
-
if (tokens >= 1_000)
|
|
454
|
-
return `${formatCompactNumber(tokens / 1_000)}k`;
|
|
455
|
-
return String(tokens);
|
|
456
|
-
}
|
|
457
|
-
function formatCompactNumber(value) {
|
|
458
|
-
const rounded = value >= 10 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
459
|
-
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
337
|
+
sidebar.setTodos(state?.todos ?? []);
|
|
338
|
+
sidebar.setStateMachine(state?.stateMachine);
|
|
339
|
+
sidebar.setContextUsage(latestContextUsage);
|
|
460
340
|
}
|
|
461
341
|
const unsubscribe = input.session.subscribe((event) => {
|
|
462
|
-
// Sidebar mirrors the runner's authoritative state, so refresh it on
|
|
463
|
-
// every event rather than threading specific updates through each branch.
|
|
464
342
|
refreshSidebar();
|
|
465
343
|
if (event.type === "step") {
|
|
466
344
|
renderStep(event.step);
|
|
@@ -476,7 +354,7 @@ export async function runTui(input) {
|
|
|
476
354
|
}
|
|
477
355
|
else if (event.type === "context_usage") {
|
|
478
356
|
latestContextUsage = event;
|
|
479
|
-
|
|
357
|
+
sidebar.setContextUsage(event);
|
|
480
358
|
}
|
|
481
359
|
else if (event.type === "system") {
|
|
482
360
|
appendBlock("[system]", event.message, COLORS.system);
|
|
@@ -487,6 +365,7 @@ export async function runTui(input) {
|
|
|
487
365
|
appendBlock("[question]", event.questions.map((q) => q.question).join("\n"), COLORS.system);
|
|
488
366
|
showQuestions(event.questions);
|
|
489
367
|
renderUsage(event.usage);
|
|
368
|
+
renderTurnElapsed();
|
|
490
369
|
lastTerminal = event;
|
|
491
370
|
markIdle();
|
|
492
371
|
}
|
|
@@ -494,24 +373,22 @@ export async function runTui(input) {
|
|
|
494
373
|
if (event.error) {
|
|
495
374
|
appendBlock("[error]", event.error, COLORS.error);
|
|
496
375
|
}
|
|
497
|
-
else if (event.result) {
|
|
498
|
-
// Result is also normally streamed via text steps; only show if no streaming happened
|
|
499
|
-
// for this turn (cheap heuristic: empty transcript-since-last-prompt).
|
|
500
|
-
// Always-append is fine too — duplicate text is harmless and clearer for short turns.
|
|
501
|
-
}
|
|
502
376
|
renderUsage(event.usage);
|
|
377
|
+
renderTurnElapsed();
|
|
503
378
|
lastTerminal = event;
|
|
504
379
|
markIdle();
|
|
505
380
|
}
|
|
506
381
|
else if (event.type === "interrupted") {
|
|
507
382
|
appendLine("[interrupted]", COLORS.system);
|
|
508
383
|
renderUsage(event.usage);
|
|
384
|
+
renderTurnElapsed();
|
|
509
385
|
lastTerminal = event;
|
|
510
386
|
markIdle();
|
|
511
387
|
}
|
|
512
388
|
else if (event.type === "sleep") {
|
|
513
389
|
appendLine(`[sleeping until ${new Date(event.wakeAt).toLocaleTimeString()}]`, COLORS.system);
|
|
514
390
|
renderUsage(event.usage);
|
|
391
|
+
renderTurnElapsed();
|
|
515
392
|
lastTerminal = event;
|
|
516
393
|
markIdle();
|
|
517
394
|
}
|
|
@@ -545,6 +422,11 @@ export async function runTui(input) {
|
|
|
545
422
|
const cost = usage.cost.total === 0 ? "" : ` · Cost: $${usage.cost.total.toFixed(4)}`;
|
|
546
423
|
appendLine(`[usage] Tokens: ${parts.join(" ")}${cost}`, COLORS.hint);
|
|
547
424
|
}
|
|
425
|
+
function renderTurnElapsed() {
|
|
426
|
+
if (workingStartedAt === undefined)
|
|
427
|
+
return;
|
|
428
|
+
appendLine(`● turn finished in ${formatElapsed(Date.now() - workingStartedAt)}`, COLORS.status);
|
|
429
|
+
}
|
|
548
430
|
function renderTodos(todos) {
|
|
549
431
|
if (todos.length === 0) {
|
|
550
432
|
appendBlock("[todos]", "No todos", COLORS.hint);
|
|
@@ -702,6 +584,14 @@ export async function runTui(input) {
|
|
|
702
584
|
let skillAutocompleteToken;
|
|
703
585
|
let skillAutocompleteItems = [];
|
|
704
586
|
let skillAutocompleteSelectedIndex = 0;
|
|
587
|
+
// File index loads lazily after the first @ trigger and never re-runs.
|
|
588
|
+
// Repos large enough to matter would block the first keystroke otherwise;
|
|
589
|
+
// a stale-by-a-few-files index is a fair trade for a snappy first paint.
|
|
590
|
+
let fileAutocompleteAllFiles = [];
|
|
591
|
+
let fileAutocompleteIndexPromise;
|
|
592
|
+
let fileAutocompleteToken;
|
|
593
|
+
let fileAutocompleteItems = [];
|
|
594
|
+
let fileAutocompleteSelectedIndex = 0;
|
|
705
595
|
let pendingQuestions = [];
|
|
706
596
|
let questionOptionSelectedIndex = 0;
|
|
707
597
|
let suppressNextEscapeExit = false;
|
|
@@ -731,6 +621,9 @@ export async function runTui(input) {
|
|
|
731
621
|
function skillAutocompleteIsOpen() {
|
|
732
622
|
return Boolean(skillAutocompleteToken && skillAutocompleteItems.length > 0);
|
|
733
623
|
}
|
|
624
|
+
function fileAutocompleteIsOpen() {
|
|
625
|
+
return Boolean(fileAutocompleteToken && fileAutocompleteItems.length > 0);
|
|
626
|
+
}
|
|
734
627
|
function questionPickerIsOpen() {
|
|
735
628
|
const question = pendingQuestions[0];
|
|
736
629
|
return Boolean(question && question.options.length > 0);
|
|
@@ -745,6 +638,16 @@ export async function runTui(input) {
|
|
|
745
638
|
row.content = "";
|
|
746
639
|
}
|
|
747
640
|
}
|
|
641
|
+
function hideFileAutocomplete() {
|
|
642
|
+
fileAutocompleteToken = undefined;
|
|
643
|
+
fileAutocompleteItems = [];
|
|
644
|
+
fileAutocompleteSelectedIndex = 0;
|
|
645
|
+
fileAutocompletePanel.visible = false;
|
|
646
|
+
for (const row of fileAutocompleteRows) {
|
|
647
|
+
row.visible = false;
|
|
648
|
+
row.content = "";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
748
651
|
function hideQuestions() {
|
|
749
652
|
pendingQuestions = [];
|
|
750
653
|
questionOptionSelectedIndex = 0;
|
|
@@ -799,6 +702,19 @@ export async function runTui(input) {
|
|
|
799
702
|
markRunning();
|
|
800
703
|
return true;
|
|
801
704
|
}
|
|
705
|
+
async function ensureFileIndex() {
|
|
706
|
+
if (fileAutocompleteAllFiles.length > 0)
|
|
707
|
+
return fileAutocompleteAllFiles;
|
|
708
|
+
if (!fileAutocompleteIndexPromise) {
|
|
709
|
+
fileAutocompleteIndexPromise = buildFileIndex(input.workDir).catch(() => []);
|
|
710
|
+
}
|
|
711
|
+
fileAutocompleteAllFiles = await fileAutocompleteIndexPromise;
|
|
712
|
+
return fileAutocompleteAllFiles;
|
|
713
|
+
}
|
|
714
|
+
function refreshAutocomplete() {
|
|
715
|
+
refreshSkillAutocomplete();
|
|
716
|
+
refreshFileAutocomplete();
|
|
717
|
+
}
|
|
802
718
|
function refreshSkillAutocomplete() {
|
|
803
719
|
const token = activeSkillAutocompleteToken(inputField.plainText, inputField.cursorOffset);
|
|
804
720
|
if (!token) {
|
|
@@ -822,6 +738,44 @@ export async function runTui(input) {
|
|
|
822
738
|
}
|
|
823
739
|
renderSkillAutocomplete();
|
|
824
740
|
}
|
|
741
|
+
function refreshFileAutocomplete() {
|
|
742
|
+
const token = activeFileAutocompleteToken(inputField.plainText, inputField.cursorOffset);
|
|
743
|
+
if (!token) {
|
|
744
|
+
hideFileAutocomplete();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
// Capture the token id we're looking up so a slow index resolution can
|
|
748
|
+
// tell whether the user has typed past the original query and bail out.
|
|
749
|
+
const targetStart = token.start;
|
|
750
|
+
const targetEnd = token.end;
|
|
751
|
+
const targetQuery = token.query;
|
|
752
|
+
void ensureFileIndex().then((files) => {
|
|
753
|
+
const stillCurrent = fileAutocompleteToken !== undefined
|
|
754
|
+
? fileAutocompleteToken.start === targetStart &&
|
|
755
|
+
fileAutocompleteToken.end === targetEnd &&
|
|
756
|
+
fileAutocompleteToken.query === targetQuery
|
|
757
|
+
: activeFileAutocompleteToken(inputField.plainText, inputField.cursorOffset)?.query ===
|
|
758
|
+
targetQuery;
|
|
759
|
+
if (!stillCurrent && fileAutocompleteToken === undefined)
|
|
760
|
+
return;
|
|
761
|
+
const items = fileAutocompleteMatches(files, targetQuery);
|
|
762
|
+
if (items.length === 0) {
|
|
763
|
+
hideFileAutocomplete();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const previousToken = fileAutocompleteToken;
|
|
767
|
+
fileAutocompleteToken = { start: targetStart, end: targetEnd, query: targetQuery };
|
|
768
|
+
fileAutocompleteItems = items;
|
|
769
|
+
const queryChanged = !previousToken ||
|
|
770
|
+
previousToken.start !== targetStart ||
|
|
771
|
+
previousToken.end !== targetEnd ||
|
|
772
|
+
previousToken.query !== targetQuery;
|
|
773
|
+
if (queryChanged || fileAutocompleteSelectedIndex >= items.length) {
|
|
774
|
+
fileAutocompleteSelectedIndex = 0;
|
|
775
|
+
}
|
|
776
|
+
renderFileAutocomplete();
|
|
777
|
+
});
|
|
778
|
+
}
|
|
825
779
|
function renderSkillAutocomplete() {
|
|
826
780
|
skillAutocompletePanel.visible = skillAutocompleteItems.length > 0;
|
|
827
781
|
for (const [index, row] of skillAutocompleteRows.entries()) {
|
|
@@ -842,6 +796,30 @@ export async function runTui(input) {
|
|
|
842
796
|
row.visible = true;
|
|
843
797
|
}
|
|
844
798
|
}
|
|
799
|
+
function renderFileAutocomplete() {
|
|
800
|
+
fileAutocompletePanel.visible = fileAutocompleteItems.length > 0;
|
|
801
|
+
for (const [index, row] of fileAutocompleteRows.entries()) {
|
|
802
|
+
const item = fileAutocompleteItems[index];
|
|
803
|
+
if (!item) {
|
|
804
|
+
row.visible = false;
|
|
805
|
+
row.content = "";
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const selected = index === fileAutocompleteSelectedIndex;
|
|
809
|
+
const nameColor = selected ? COLORS.status : COLORS.user;
|
|
810
|
+
const pathColor = selected ? COLORS.agent : COLORS.hint;
|
|
811
|
+
// Show basename + relative directory side-by-side. The directory
|
|
812
|
+
// portion is the path with the trailing basename removed; for files at
|
|
813
|
+
// the repo root this collapses to "./" so each row has a consistent
|
|
814
|
+
// shape.
|
|
815
|
+
const directory = item.relativePath.includes("/")
|
|
816
|
+
? item.relativePath.slice(0, item.relativePath.lastIndexOf("/") + 1)
|
|
817
|
+
: "./";
|
|
818
|
+
row.content = t `${fg(nameColor)(item.name)} ${fg(pathColor)(directory)}`;
|
|
819
|
+
row.fg = selected ? COLORS.agent : COLORS.hint;
|
|
820
|
+
row.visible = true;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
845
823
|
function completeSelectedSkillAutocomplete() {
|
|
846
824
|
const token = skillAutocompleteToken;
|
|
847
825
|
const item = skillAutocompleteItems[skillAutocompleteSelectedIndex];
|
|
@@ -856,6 +834,20 @@ export async function runTui(input) {
|
|
|
856
834
|
hideSkillAutocomplete();
|
|
857
835
|
return true;
|
|
858
836
|
}
|
|
837
|
+
function completeSelectedFileAutocomplete() {
|
|
838
|
+
const token = fileAutocompleteToken;
|
|
839
|
+
const item = fileAutocompleteItems[fileAutocompleteSelectedIndex];
|
|
840
|
+
if (!token || !item)
|
|
841
|
+
return false;
|
|
842
|
+
const insertion = inputField.plainText[token.end]?.match(/\s/)
|
|
843
|
+
? `@${item.relativePath}`
|
|
844
|
+
: `@${item.relativePath} `;
|
|
845
|
+
inputField.setSelection(token.start, token.end);
|
|
846
|
+
inputField.deleteSelection();
|
|
847
|
+
inputField.insertText(insertion);
|
|
848
|
+
hideFileAutocomplete();
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
859
851
|
const keyHandler = renderer._keyHandler;
|
|
860
852
|
keyHandler.onInternal("keypress", (key) => {
|
|
861
853
|
if (key.name !== "escape")
|
|
@@ -870,6 +862,11 @@ export async function runTui(input) {
|
|
|
870
862
|
hideSkillAutocomplete();
|
|
871
863
|
return;
|
|
872
864
|
}
|
|
865
|
+
if (fileAutocompleteIsOpen()) {
|
|
866
|
+
key.preventDefault();
|
|
867
|
+
hideFileAutocomplete();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
873
870
|
if (questionPickerIsOpen()) {
|
|
874
871
|
key.preventDefault();
|
|
875
872
|
hideQuestions();
|
|
@@ -895,7 +892,7 @@ export async function runTui(input) {
|
|
|
895
892
|
key.preventDefault();
|
|
896
893
|
return;
|
|
897
894
|
}
|
|
898
|
-
if (key.name === "return" || key.name === "enter") {
|
|
895
|
+
if (key.name === "return" || key.name === "enter" || key.name === "tab") {
|
|
899
896
|
key.preventDefault();
|
|
900
897
|
completeSelectedSkillAutocomplete();
|
|
901
898
|
return;
|
|
@@ -907,6 +904,31 @@ export async function runTui(input) {
|
|
|
907
904
|
return;
|
|
908
905
|
}
|
|
909
906
|
}
|
|
907
|
+
if (fileAutocompleteIsOpen()) {
|
|
908
|
+
if (key.name === "up") {
|
|
909
|
+
fileAutocompleteSelectedIndex = moveSkillAutocompleteSelection(fileAutocompleteSelectedIndex, fileAutocompleteItems.length, -1);
|
|
910
|
+
renderFileAutocomplete();
|
|
911
|
+
key.preventDefault();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (key.name === "down") {
|
|
915
|
+
fileAutocompleteSelectedIndex = moveSkillAutocompleteSelection(fileAutocompleteSelectedIndex, fileAutocompleteItems.length, 1);
|
|
916
|
+
renderFileAutocomplete();
|
|
917
|
+
key.preventDefault();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (key.name === "return" || key.name === "enter" || key.name === "tab") {
|
|
921
|
+
key.preventDefault();
|
|
922
|
+
completeSelectedFileAutocomplete();
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (key.name === "escape") {
|
|
926
|
+
key.preventDefault();
|
|
927
|
+
suppressNextEscapeExit = true;
|
|
928
|
+
hideFileAutocomplete();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
910
932
|
if (questionPickerIsOpen()) {
|
|
911
933
|
if (key.name === "up") {
|
|
912
934
|
questionOptionSelectedIndex = moveQuestionOptionSelection(questionOptionSelectedIndex, Math.min(pendingQuestions[0]?.options.length ?? 0, QUESTION_OPTION_LIMIT), -1);
|
|
@@ -929,9 +951,6 @@ export async function runTui(input) {
|
|
|
929
951
|
}
|
|
930
952
|
if (key.name === "return" || key.name === "enter") {
|
|
931
953
|
lastEnterShift = Boolean(key.shift);
|
|
932
|
-
// Take over Enter so the textarea does not insert a newline. We submit
|
|
933
|
-
// the current buffer contents and reset, regardless of shift state —
|
|
934
|
-
// shift only differentiates steer vs. queued follow-up.
|
|
935
954
|
const value = inputField.plainText.trim();
|
|
936
955
|
inputField.clear();
|
|
937
956
|
key.preventDefault();
|
|
@@ -949,20 +968,16 @@ export async function runTui(input) {
|
|
|
949
968
|
return;
|
|
950
969
|
}
|
|
951
970
|
};
|
|
952
|
-
inputField.onContentChange = () =>
|
|
953
|
-
inputField.onCursorChange = () =>
|
|
971
|
+
inputField.onContentChange = () => refreshAutocomplete();
|
|
972
|
+
inputField.onCursorChange = () => refreshAutocomplete();
|
|
954
973
|
function submit(message, shiftEnter) {
|
|
955
974
|
appendBlock("you:", message, COLORS.user);
|
|
956
975
|
hideQuestions();
|
|
957
976
|
if (running) {
|
|
958
|
-
// Mid-turn: Enter → steer, Shift+Enter → queued follow-up.
|
|
959
977
|
const behavior = shiftEnter ? "follow_up" : "steer";
|
|
960
978
|
void input.session.prompt({ message, behavior }).catch(reportError);
|
|
961
|
-
// Keep status as "working"; the existing turn continues.
|
|
962
979
|
return;
|
|
963
980
|
}
|
|
964
|
-
// Idle: dispatch a prompt against the already-set-up session. Setup
|
|
965
|
-
// happens before the TUI starts so skills are visible right away.
|
|
966
981
|
void input.session.prompt({ message, behavior: "follow_up" }).catch(reportError);
|
|
967
982
|
markRunning();
|
|
968
983
|
}
|
|
@@ -978,7 +993,7 @@ export async function runTui(input) {
|
|
|
978
993
|
description: skill.description,
|
|
979
994
|
path: skill.baseDir,
|
|
980
995
|
}));
|
|
981
|
-
|
|
996
|
+
refreshAutocomplete();
|
|
982
997
|
renderSetupIntro(skills, agentFiles);
|
|
983
998
|
refreshSidebar();
|
|
984
999
|
const resumeHistoryLines = input.resumeHistoryLines ?? Number.POSITIVE_INFINITY;
|
|
@@ -1001,8 +1016,6 @@ export async function runTui(input) {
|
|
|
1001
1016
|
markRunning();
|
|
1002
1017
|
}
|
|
1003
1018
|
else {
|
|
1004
|
-
// No initial prompt — wait for the user. Setup already ran above, so
|
|
1005
|
-
// the skill summary is rendered before the user types.
|
|
1006
1019
|
markIdle();
|
|
1007
1020
|
}
|
|
1008
1021
|
// ---- run renderer until the user quits -------------------------------------
|
|
@@ -1034,204 +1047,6 @@ export async function runTui(input) {
|
|
|
1034
1047
|
return COLORS.agent;
|
|
1035
1048
|
}
|
|
1036
1049
|
}
|
|
1037
|
-
export function historyDisplayBlocks(history) {
|
|
1038
|
-
const blocks = [];
|
|
1039
|
-
const activeToolBlockIndexes = new Map();
|
|
1040
|
-
for (const message of history) {
|
|
1041
|
-
if (!("role" in message))
|
|
1042
|
-
continue;
|
|
1043
|
-
if (message.role === "user") {
|
|
1044
|
-
const text = userMessageText(message.content);
|
|
1045
|
-
if (text)
|
|
1046
|
-
blocks.push({ kind: "user", content: `you:\n${text}` });
|
|
1047
|
-
}
|
|
1048
|
-
else if (message.role === "assistant") {
|
|
1049
|
-
for (const block of message.content) {
|
|
1050
|
-
if (block.type === "text") {
|
|
1051
|
-
blocks.push({ kind: "agent", content: block.text });
|
|
1052
|
-
}
|
|
1053
|
-
else if (block.type === "thinking") {
|
|
1054
|
-
const trimmed = block.thinking.trim();
|
|
1055
|
-
if (trimmed)
|
|
1056
|
-
blocks.push({ kind: "reasoning", content: `[reasoning]\n${trimmed}` });
|
|
1057
|
-
}
|
|
1058
|
-
else if (block.type === "toolCall") {
|
|
1059
|
-
const input = block.arguments === undefined ? "" : `\n${formatCompactJson(block.arguments)}`;
|
|
1060
|
-
activeToolBlockIndexes.set(block.id, blocks.length);
|
|
1061
|
-
blocks.push({ kind: "tool", content: `[tool ${block.name}] ⏳${input}` });
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (message.errorMessage) {
|
|
1065
|
-
blocks.push({ kind: "error", content: `[error]\n${message.errorMessage}` });
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
else if (message.role === "toolResult") {
|
|
1069
|
-
const text = textFromHistoryContent(message.content);
|
|
1070
|
-
const existingIndex = activeToolBlockIndexes.get(message.toolCallId);
|
|
1071
|
-
const marker = message.isError ? "✗" : "✓";
|
|
1072
|
-
const label = message.isError ? "[error]" : "[result]";
|
|
1073
|
-
if (existingIndex !== undefined) {
|
|
1074
|
-
const existing = blocks[existingIndex];
|
|
1075
|
-
const [, ...inputLines] = existing.content.split("\n");
|
|
1076
|
-
const input = inputLines.length > 0 ? `\n${inputLines.join("\n")}` : "";
|
|
1077
|
-
existing.kind = message.isError ? "error" : "tool";
|
|
1078
|
-
existing.content = text
|
|
1079
|
-
? `[tool ${message.toolName}] ${marker}${input}\n${label}\n${text}`
|
|
1080
|
-
: `[tool ${message.toolName}] ${marker}${input}`;
|
|
1081
|
-
activeToolBlockIndexes.delete(message.toolCallId);
|
|
1082
|
-
}
|
|
1083
|
-
else {
|
|
1084
|
-
const content = text
|
|
1085
|
-
? `[tool ${message.toolName}] ${marker}\n${label}\n${text}`
|
|
1086
|
-
: `[tool ${message.toolName}] ${marker}`;
|
|
1087
|
-
blocks.push({ kind: message.isError ? "error" : "tool", content });
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
return blocks;
|
|
1092
|
-
}
|
|
1093
|
-
export function startupHeaderLines(input) {
|
|
1094
|
-
const lines = [
|
|
1095
|
-
`[duet] v${input.packageVersion}`,
|
|
1096
|
-
`[cwd] ${input.workDir}`,
|
|
1097
|
-
`[session] ${input.sessionId}`,
|
|
1098
|
-
input.modelSource
|
|
1099
|
-
? `[model] ${input.modelName} — ${input.modelSource}`
|
|
1100
|
-
: `[model] ${input.modelName}`,
|
|
1101
|
-
input.memoryModelSource
|
|
1102
|
-
? `[memory model] ${input.memoryModelName} — ${input.memoryModelSource}`
|
|
1103
|
-
: `[memory model] ${input.memoryModelName}`,
|
|
1104
|
-
];
|
|
1105
|
-
if (input.newVersionNotice)
|
|
1106
|
-
lines.push(input.newVersionNotice);
|
|
1107
|
-
return lines;
|
|
1108
|
-
}
|
|
1109
|
-
export function limitHistoryDisplayBlocks(blocks, maxLines) {
|
|
1110
|
-
if (maxLines <= 0)
|
|
1111
|
-
return { blocks: [], omittedLines: countHistoryLines(blocks) };
|
|
1112
|
-
const selected = [];
|
|
1113
|
-
let remaining = maxLines;
|
|
1114
|
-
let omittedLines = 0;
|
|
1115
|
-
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
1116
|
-
const block = blocks[index];
|
|
1117
|
-
const lines = block.content.split("\n");
|
|
1118
|
-
if (lines.length <= remaining) {
|
|
1119
|
-
selected.unshift(block);
|
|
1120
|
-
remaining -= lines.length;
|
|
1121
|
-
continue;
|
|
1122
|
-
}
|
|
1123
|
-
if (remaining > 0) {
|
|
1124
|
-
selected.unshift({ ...block, content: lines.slice(-remaining).join("\n") });
|
|
1125
|
-
omittedLines += lines.length - remaining;
|
|
1126
|
-
remaining = 0;
|
|
1127
|
-
}
|
|
1128
|
-
else {
|
|
1129
|
-
omittedLines += lines.length;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
return { blocks: selected, omittedLines };
|
|
1133
|
-
}
|
|
1134
|
-
export function activeSkillAutocompleteToken(text, cursorOffset) {
|
|
1135
|
-
const boundedOffset = Math.max(0, Math.min(cursorOffset, text.length));
|
|
1136
|
-
const tokenStart = text.slice(0, boundedOffset).search(/(?:^|\s)\/[^\s]*$/);
|
|
1137
|
-
if (tokenStart < 0)
|
|
1138
|
-
return undefined;
|
|
1139
|
-
const start = text[tokenStart] === "/" ? tokenStart : tokenStart + 1;
|
|
1140
|
-
const tokenEnd = text.slice(boundedOffset).search(/\s/);
|
|
1141
|
-
const end = tokenEnd < 0 ? text.length : boundedOffset + tokenEnd;
|
|
1142
|
-
const token = text.slice(start, end);
|
|
1143
|
-
const match = token.match(SKILL_AUTOCOMPLETE_TOKEN);
|
|
1144
|
-
if (!match)
|
|
1145
|
-
return undefined;
|
|
1146
|
-
return { start, end, query: text.slice(start + 1, boundedOffset) };
|
|
1147
|
-
}
|
|
1148
|
-
export function skillAutocompleteMatches(skills, query, limit = SKILL_AUTOCOMPLETE_LIMIT) {
|
|
1149
|
-
const normalizedQuery = query.toLocaleLowerCase();
|
|
1150
|
-
return [...skills]
|
|
1151
|
-
.filter((skill) => skill.name.toLocaleLowerCase().startsWith(normalizedQuery))
|
|
1152
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
1153
|
-
.slice(0, limit);
|
|
1154
|
-
}
|
|
1155
|
-
export function formatSkillAutocompleteItem(item) {
|
|
1156
|
-
const path = item.path ? ` (${item.path})` : "";
|
|
1157
|
-
const lines = [`/${item.name}${path}`, formatSkillAutocompleteDescription(item.description)];
|
|
1158
|
-
return lines.filter((line) => line.length > 0).join("\n");
|
|
1159
|
-
}
|
|
1160
|
-
export function formatSkillAutocompleteDescription(description) {
|
|
1161
|
-
if (!description)
|
|
1162
|
-
return "";
|
|
1163
|
-
const wrapped = wrapText(description, SKILL_AUTOCOMPLETE_DESCRIPTION_WIDTH);
|
|
1164
|
-
const visible = wrapped.slice(0, SKILL_AUTOCOMPLETE_DESCRIPTION_LINES);
|
|
1165
|
-
if (wrapped.length > visible.length) {
|
|
1166
|
-
const lastIndex = visible.length - 1;
|
|
1167
|
-
visible[lastIndex] = `${visible[lastIndex].replace(/\s+$/, "")}...`;
|
|
1168
|
-
}
|
|
1169
|
-
return visible.join("\n");
|
|
1170
|
-
}
|
|
1171
|
-
function wrapText(text, width) {
|
|
1172
|
-
const words = text.trim().split(/\s+/);
|
|
1173
|
-
const lines = [];
|
|
1174
|
-
let current = "";
|
|
1175
|
-
for (const word of words) {
|
|
1176
|
-
if (!current) {
|
|
1177
|
-
current = word;
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
if (current.length + 1 + word.length <= width) {
|
|
1181
|
-
current = `${current} ${word}`;
|
|
1182
|
-
continue;
|
|
1183
|
-
}
|
|
1184
|
-
lines.push(current);
|
|
1185
|
-
current = word;
|
|
1186
|
-
}
|
|
1187
|
-
if (current)
|
|
1188
|
-
lines.push(current);
|
|
1189
|
-
return lines;
|
|
1190
|
-
}
|
|
1191
|
-
export function moveSkillAutocompleteSelection(selectedIndex, itemCount, direction) {
|
|
1192
|
-
if (itemCount <= 0)
|
|
1193
|
-
return 0;
|
|
1194
|
-
return (selectedIndex + direction + itemCount) % itemCount;
|
|
1195
|
-
}
|
|
1196
|
-
export function moveQuestionOptionSelection(selectedIndex, itemCount, direction) {
|
|
1197
|
-
if (itemCount <= 0)
|
|
1198
|
-
return 0;
|
|
1199
|
-
return (selectedIndex + direction + itemCount) % itemCount;
|
|
1200
|
-
}
|
|
1201
|
-
export function questionPickerAnswerPayload(questions, selectedIndex) {
|
|
1202
|
-
const firstQuestion = questions[0];
|
|
1203
|
-
const selectedOption = firstQuestion?.options[selectedIndex];
|
|
1204
|
-
if (!firstQuestion || !selectedOption)
|
|
1205
|
-
return undefined;
|
|
1206
|
-
return { [firstQuestion.question]: selectedOption.label };
|
|
1207
|
-
}
|
|
1208
|
-
export function formatQuestionOptionDescription(description) {
|
|
1209
|
-
if (!description)
|
|
1210
|
-
return "";
|
|
1211
|
-
return wrapText(description, QUESTION_OPTION_DESCRIPTION_WIDTH).join("\n");
|
|
1212
|
-
}
|
|
1213
|
-
export function replaceSkillAutocompleteToken(text, token, skillName) {
|
|
1214
|
-
const insertion = text[token.end]?.match(/\s/) ? `/${skillName}` : `/${skillName} `;
|
|
1215
|
-
const nextText = `${text.slice(0, token.start)}${insertion}${text.slice(token.end)}`;
|
|
1216
|
-
return { text: nextText, cursorOffset: token.start + insertion.length };
|
|
1217
|
-
}
|
|
1218
|
-
function countHistoryLines(blocks) {
|
|
1219
|
-
return blocks.reduce((count, block) => count + block.content.split("\n").length, 0);
|
|
1220
|
-
}
|
|
1221
|
-
function userMessageText(content) {
|
|
1222
|
-
if (typeof content === "string")
|
|
1223
|
-
return content;
|
|
1224
|
-
return content
|
|
1225
|
-
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
1226
|
-
.map((block) => block.text)
|
|
1227
|
-
.join("");
|
|
1228
|
-
}
|
|
1229
|
-
function textFromHistoryContent(content) {
|
|
1230
|
-
return content
|
|
1231
|
-
.filter((block) => block.type === "text")
|
|
1232
|
-
.map((block) => block.text)
|
|
1233
|
-
.join("\n");
|
|
1234
|
-
}
|
|
1235
1050
|
function restoreWindowGlobal(previousWindow) {
|
|
1236
1051
|
// OpenTUI installs `window.requestAnimationFrame` for browser-style
|
|
1237
1052
|
// animation compatibility. In Bun, the presence of `window` can send fetch
|