@duetso/agent 0.1.20
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/LICENSE +189 -0
- package/README.md +315 -0
- package/dist/package.json +84 -0
- package/dist/src/cli.d.ts +23 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +754 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core/serializer.d.ts +3 -0
- package/dist/src/core/serializer.d.ts.map +1 -0
- package/dist/src/core/serializer.js +22 -0
- package/dist/src/core/serializer.js.map +1 -0
- package/dist/src/core/structured-output.d.ts +13 -0
- package/dist/src/core/structured-output.d.ts.map +1 -0
- package/dist/src/core/structured-output.js +41 -0
- package/dist/src/core/structured-output.js.map +1 -0
- package/dist/src/guardrails/firewall.d.ts +7 -0
- package/dist/src/guardrails/firewall.d.ts.map +1 -0
- package/dist/src/guardrails/firewall.js +31 -0
- package/dist/src/guardrails/firewall.js.map +1 -0
- package/dist/src/guardrails/pattern.d.ts +13 -0
- package/dist/src/guardrails/pattern.d.ts.map +1 -0
- package/dist/src/guardrails/pattern.js +70 -0
- package/dist/src/guardrails/pattern.js.map +1 -0
- package/dist/src/guardrails/semantic.d.ts +14 -0
- package/dist/src/guardrails/semantic.d.ts.map +1 -0
- package/dist/src/guardrails/semantic.js +47 -0
- package/dist/src/guardrails/semantic.js.map +1 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +20 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib/compact-json.d.ts +11 -0
- package/dist/src/lib/compact-json.d.ts.map +1 -0
- package/dist/src/lib/compact-json.js +36 -0
- package/dist/src/lib/compact-json.js.map +1 -0
- package/dist/src/lib/xml.d.ts +3 -0
- package/dist/src/lib/xml.d.ts.map +1 -0
- package/dist/src/lib/xml.js +9 -0
- package/dist/src/lib/xml.js.map +1 -0
- package/dist/src/memory/observation-groups.d.ts +15 -0
- package/dist/src/memory/observation-groups.d.ts.map +1 -0
- package/dist/src/memory/observation-groups.js +159 -0
- package/dist/src/memory/observation-groups.js.map +1 -0
- package/dist/src/memory/observational-prompts.d.ts +27 -0
- package/dist/src/memory/observational-prompts.d.ts.map +1 -0
- package/dist/src/memory/observational-prompts.js +237 -0
- package/dist/src/memory/observational-prompts.js.map +1 -0
- package/dist/src/memory/observational.d.ts +63 -0
- package/dist/src/memory/observational.d.ts.map +1 -0
- package/dist/src/memory/observational.js +605 -0
- package/dist/src/memory/observational.js.map +1 -0
- package/dist/src/memory/storage.d.ts +3 -0
- package/dist/src/memory/storage.d.ts.map +1 -0
- package/dist/src/memory/storage.js +127 -0
- package/dist/src/memory/storage.js.map +1 -0
- package/dist/src/memory/store.d.ts +13 -0
- package/dist/src/memory/store.d.ts.map +1 -0
- package/dist/src/memory/store.js +106 -0
- package/dist/src/memory/store.js.map +1 -0
- package/dist/src/model-resolution/duet-gateway.d.ts +35 -0
- package/dist/src/model-resolution/duet-gateway.d.ts.map +1 -0
- package/dist/src/model-resolution/duet-gateway.js +56 -0
- package/dist/src/model-resolution/duet-gateway.js.map +1 -0
- package/dist/src/model-resolution/index.d.ts +31 -0
- package/dist/src/model-resolution/index.d.ts.map +1 -0
- package/dist/src/model-resolution/index.js +129 -0
- package/dist/src/model-resolution/index.js.map +1 -0
- package/dist/src/session/session-manager.d.ts +45 -0
- package/dist/src/session/session-manager.d.ts.map +1 -0
- package/dist/src/session/session-manager.js +94 -0
- package/dist/src/session/session-manager.js.map +1 -0
- package/dist/src/session/session.d.ts +113 -0
- package/dist/src/session/session.d.ts.map +1 -0
- package/dist/src/session/session.js +308 -0
- package/dist/src/session/session.js.map +1 -0
- package/dist/src/tui/app.d.ts +60 -0
- package/dist/src/tui/app.d.ts.map +1 -0
- package/dist/src/tui/app.js +742 -0
- package/dist/src/tui/app.js.map +1 -0
- package/dist/src/turn-runner/agent-events.d.ts +5 -0
- package/dist/src/turn-runner/agent-events.d.ts.map +1 -0
- package/dist/src/turn-runner/agent-events.js +59 -0
- package/dist/src/turn-runner/agent-events.js.map +1 -0
- package/dist/src/turn-runner/prompts.d.ts +13 -0
- package/dist/src/turn-runner/prompts.d.ts.map +1 -0
- package/dist/src/turn-runner/prompts.js +79 -0
- package/dist/src/turn-runner/prompts.js.map +1 -0
- package/dist/src/turn-runner/shell-state-handle.d.ts +32 -0
- package/dist/src/turn-runner/shell-state-handle.d.ts.map +1 -0
- package/dist/src/turn-runner/shell-state-handle.js +168 -0
- package/dist/src/turn-runner/shell-state-handle.js.map +1 -0
- package/dist/src/turn-runner/skill-context.d.ts +26 -0
- package/dist/src/turn-runner/skill-context.d.ts.map +1 -0
- package/dist/src/turn-runner/skill-context.js +110 -0
- package/dist/src/turn-runner/skill-context.js.map +1 -0
- package/dist/src/turn-runner/skills.d.ts +35 -0
- package/dist/src/turn-runner/skills.d.ts.map +1 -0
- package/dist/src/turn-runner/skills.js +130 -0
- package/dist/src/turn-runner/skills.js.map +1 -0
- package/dist/src/turn-runner/state-machine-controller.d.ts +90 -0
- package/dist/src/turn-runner/state-machine-controller.d.ts.map +1 -0
- package/dist/src/turn-runner/state-machine-controller.js +289 -0
- package/dist/src/turn-runner/state-machine-controller.js.map +1 -0
- package/dist/src/turn-runner/state-machine-session.d.ts +27 -0
- package/dist/src/turn-runner/state-machine-session.d.ts.map +1 -0
- package/dist/src/turn-runner/state-machine-session.js +189 -0
- package/dist/src/turn-runner/state-machine-session.js.map +1 -0
- package/dist/src/turn-runner/tools.d.ts +193 -0
- package/dist/src/turn-runner/tools.d.ts.map +1 -0
- package/dist/src/turn-runner/tools.js +509 -0
- package/dist/src/turn-runner/tools.js.map +1 -0
- package/dist/src/turn-runner/turn-runner.d.ts +160 -0
- package/dist/src/turn-runner/turn-runner.d.ts.map +1 -0
- package/dist/src/turn-runner/turn-runner.js +907 -0
- package/dist/src/turn-runner/turn-runner.js.map +1 -0
- package/dist/src/turn-runner/turn-state.d.ts +6 -0
- package/dist/src/turn-runner/turn-state.d.ts.map +1 -0
- package/dist/src/turn-runner/turn-state.js +32 -0
- package/dist/src/turn-runner/turn-state.js.map +1 -0
- package/dist/src/turn-runner/usage-accounting.d.ts +7 -0
- package/dist/src/turn-runner/usage-accounting.d.ts.map +1 -0
- package/dist/src/turn-runner/usage-accounting.js +49 -0
- package/dist/src/turn-runner/usage-accounting.js.map +1 -0
- package/dist/src/types/agent.d.ts +15 -0
- package/dist/src/types/agent.d.ts.map +1 -0
- package/dist/src/types/agent.js +2 -0
- package/dist/src/types/agent.js.map +1 -0
- package/dist/src/types/config.d.ts +37 -0
- package/dist/src/types/config.d.ts.map +1 -0
- package/dist/src/types/config.js +2 -0
- package/dist/src/types/config.js.map +1 -0
- package/dist/src/types/guardrails.d.ts +34 -0
- package/dist/src/types/guardrails.d.ts.map +1 -0
- package/dist/src/types/guardrails.js +2 -0
- package/dist/src/types/guardrails.js.map +1 -0
- package/dist/src/types/memory.d.ts +151 -0
- package/dist/src/types/memory.d.ts.map +1 -0
- package/dist/src/types/memory.js +2 -0
- package/dist/src/types/memory.js.map +1 -0
- package/dist/src/types/protocol.d.ts +426 -0
- package/dist/src/types/protocol.d.ts.map +1 -0
- package/dist/src/types/protocol.js +2 -0
- package/dist/src/types/protocol.js.map +1 -0
- package/dist/src/types/state-machine.d.ts +344 -0
- package/dist/src/types/state-machine.d.ts.map +1 -0
- package/dist/src/types/state-machine.js +2 -0
- package/dist/src/types/state-machine.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
import { BoxRenderable, createCliRenderer, ScrollBoxRenderable, TextRenderable, TextareaRenderable, } from "@opentui/core";
|
|
2
|
+
import { formatCompactJson } from "../lib/compact-json.js";
|
|
3
|
+
const COLORS = {
|
|
4
|
+
user: "#7DD3FC",
|
|
5
|
+
agent: "#FFFFFF",
|
|
6
|
+
reasoning: "#9CA3AF",
|
|
7
|
+
tool: "#A78BFA",
|
|
8
|
+
system: "#FBBF24",
|
|
9
|
+
error: "#F87171",
|
|
10
|
+
hint: "#6B7280",
|
|
11
|
+
status: "#34D399",
|
|
12
|
+
border: "#374151",
|
|
13
|
+
};
|
|
14
|
+
const HINT_IDLE = "Enter: send · Esc: quit · Ctrl+C: force quit";
|
|
15
|
+
const HINT_RUNNING = "Enter: steer · Shift+Enter: queue follow-up · Esc: interrupt and quit · Ctrl+C: force quit";
|
|
16
|
+
/**
|
|
17
|
+
* Runs the interactive TUI for a session. Resolves with the most recent
|
|
18
|
+
* terminal event (if any) when the user exits the UI.
|
|
19
|
+
*
|
|
20
|
+
* Differentiating Enter vs Shift+Enter requires the terminal to report
|
|
21
|
+
* modifier keys with Enter, which most terminals only do when the Kitty
|
|
22
|
+
* keyboard protocol is enabled. We opt into it via `useKittyKeyboard`.
|
|
23
|
+
*/
|
|
24
|
+
export async function runTui(input) {
|
|
25
|
+
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
26
|
+
const renderer = await createCliRenderer({
|
|
27
|
+
exitOnCtrlC: true,
|
|
28
|
+
useKittyKeyboard: {},
|
|
29
|
+
targetFps: 60,
|
|
30
|
+
});
|
|
31
|
+
restoreWindowGlobal(previousWindow);
|
|
32
|
+
// Outer row wraps the main column and a right-side sidebar that surfaces
|
|
33
|
+
// the runner's current todo list and state-machine progress.
|
|
34
|
+
const root = new BoxRenderable(renderer, {
|
|
35
|
+
flexDirection: "row",
|
|
36
|
+
width: "100%",
|
|
37
|
+
height: "100%",
|
|
38
|
+
});
|
|
39
|
+
const layout = new BoxRenderable(renderer, {
|
|
40
|
+
flexDirection: "column",
|
|
41
|
+
flexGrow: 1,
|
|
42
|
+
flexShrink: 1,
|
|
43
|
+
height: "100%",
|
|
44
|
+
});
|
|
45
|
+
// Fixed width keeps the sidebar legible on narrow terminals without
|
|
46
|
+
// squashing the transcript. The two panels stack vertically inside.
|
|
47
|
+
const sidebar = new BoxRenderable(renderer, {
|
|
48
|
+
flexDirection: "column",
|
|
49
|
+
width: 36,
|
|
50
|
+
height: "100%",
|
|
51
|
+
flexShrink: 0,
|
|
52
|
+
});
|
|
53
|
+
const todoPanel = new BoxRenderable(renderer, {
|
|
54
|
+
flexDirection: "column",
|
|
55
|
+
border: true,
|
|
56
|
+
borderColor: COLORS.border,
|
|
57
|
+
padding: 1,
|
|
58
|
+
flexGrow: 1,
|
|
59
|
+
flexShrink: 1,
|
|
60
|
+
});
|
|
61
|
+
const todoTitle = new TextRenderable(renderer, {
|
|
62
|
+
content: "todos",
|
|
63
|
+
fg: COLORS.status,
|
|
64
|
+
height: 1,
|
|
65
|
+
flexShrink: 0,
|
|
66
|
+
});
|
|
67
|
+
const todoBody = new TextRenderable(renderer, {
|
|
68
|
+
content: "(none)",
|
|
69
|
+
fg: COLORS.hint,
|
|
70
|
+
flexGrow: 1,
|
|
71
|
+
flexShrink: 1,
|
|
72
|
+
});
|
|
73
|
+
todoPanel.add(todoTitle);
|
|
74
|
+
todoPanel.add(todoBody);
|
|
75
|
+
const smPanel = new BoxRenderable(renderer, {
|
|
76
|
+
flexDirection: "column",
|
|
77
|
+
border: true,
|
|
78
|
+
borderColor: COLORS.border,
|
|
79
|
+
padding: 1,
|
|
80
|
+
flexGrow: 1,
|
|
81
|
+
flexShrink: 1,
|
|
82
|
+
});
|
|
83
|
+
const smTitle = new TextRenderable(renderer, {
|
|
84
|
+
content: "state machine",
|
|
85
|
+
fg: COLORS.status,
|
|
86
|
+
height: 1,
|
|
87
|
+
flexShrink: 0,
|
|
88
|
+
});
|
|
89
|
+
const smBody = new TextRenderable(renderer, {
|
|
90
|
+
content: "(inactive)",
|
|
91
|
+
fg: COLORS.hint,
|
|
92
|
+
flexGrow: 1,
|
|
93
|
+
flexShrink: 1,
|
|
94
|
+
});
|
|
95
|
+
smPanel.add(smTitle);
|
|
96
|
+
smPanel.add(smBody);
|
|
97
|
+
sidebar.add(todoPanel);
|
|
98
|
+
sidebar.add(smPanel);
|
|
99
|
+
const transcript = new ScrollBoxRenderable(renderer, {
|
|
100
|
+
flexGrow: 1,
|
|
101
|
+
flexShrink: 1,
|
|
102
|
+
scrollY: true,
|
|
103
|
+
border: true,
|
|
104
|
+
borderColor: COLORS.border,
|
|
105
|
+
padding: 1,
|
|
106
|
+
});
|
|
107
|
+
const status = new TextRenderable(renderer, {
|
|
108
|
+
content: "",
|
|
109
|
+
fg: COLORS.status,
|
|
110
|
+
height: 1,
|
|
111
|
+
flexShrink: 0,
|
|
112
|
+
});
|
|
113
|
+
const hint = new TextRenderable(renderer, {
|
|
114
|
+
content: HINT_IDLE,
|
|
115
|
+
fg: COLORS.hint,
|
|
116
|
+
height: 1,
|
|
117
|
+
flexShrink: 0,
|
|
118
|
+
});
|
|
119
|
+
const inputBox = new BoxRenderable(renderer, {
|
|
120
|
+
flexDirection: "row",
|
|
121
|
+
border: true,
|
|
122
|
+
borderColor: COLORS.border,
|
|
123
|
+
paddingLeft: 1,
|
|
124
|
+
paddingRight: 1,
|
|
125
|
+
flexShrink: 0,
|
|
126
|
+
});
|
|
127
|
+
const prompt = new TextRenderable(renderer, {
|
|
128
|
+
content: "> ",
|
|
129
|
+
fg: COLORS.user,
|
|
130
|
+
width: 2,
|
|
131
|
+
});
|
|
132
|
+
// Textarea (rather than Input) so long messages soft-wrap visually. Enter
|
|
133
|
+
// is intercepted in onKeyDown below to submit instead of inserting a newline.
|
|
134
|
+
const inputField = new TextareaRenderable(renderer, {
|
|
135
|
+
placeholder: "Type a message and press Enter…",
|
|
136
|
+
flexGrow: 1,
|
|
137
|
+
minHeight: 1,
|
|
138
|
+
maxHeight: 10,
|
|
139
|
+
wrapMode: "word",
|
|
140
|
+
});
|
|
141
|
+
inputBox.add(prompt);
|
|
142
|
+
inputBox.add(inputField);
|
|
143
|
+
layout.add(transcript);
|
|
144
|
+
layout.add(status);
|
|
145
|
+
layout.add(hint);
|
|
146
|
+
layout.add(inputBox);
|
|
147
|
+
root.add(layout);
|
|
148
|
+
root.add(sidebar);
|
|
149
|
+
renderer.root.add(root);
|
|
150
|
+
inputField.focus();
|
|
151
|
+
// ---- transcript helpers ----------------------------------------------------
|
|
152
|
+
// ScrollBox.scrollHeight is only refreshed after the next layout pass, so
|
|
153
|
+
// setting scrollTop synchronously right after adding a child reads stale
|
|
154
|
+
// dimensions and leaves the view a few lines short of the bottom. Coalesce
|
|
155
|
+
// scroll-to-bottom requests onto a single deferred tick instead.
|
|
156
|
+
let scrollPending = false;
|
|
157
|
+
function scrollToBottomSoon() {
|
|
158
|
+
if (scrollPending)
|
|
159
|
+
return;
|
|
160
|
+
scrollPending = true;
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
scrollPending = false;
|
|
163
|
+
transcript.scrollTop = transcript.scrollHeight;
|
|
164
|
+
}, 0);
|
|
165
|
+
}
|
|
166
|
+
function appendLine(content, fg) {
|
|
167
|
+
if (!content)
|
|
168
|
+
return;
|
|
169
|
+
// ScrollBox children stack vertically; one Text per logical line keeps wrapping simple.
|
|
170
|
+
const line = new TextRenderable(renderer, { content, fg });
|
|
171
|
+
transcript.add(line);
|
|
172
|
+
scrollToBottomSoon();
|
|
173
|
+
}
|
|
174
|
+
// Tool results can be huge (file dumps, search output). Show only the head
|
|
175
|
+
// in the transcript so the conversation flow stays readable; the full
|
|
176
|
+
// payload remains in session history for the model.
|
|
177
|
+
const TOOL_RESULT_MAX_LINES = 3;
|
|
178
|
+
function truncateToolResult(text) {
|
|
179
|
+
const lines = text.split("\n");
|
|
180
|
+
if (lines.length <= TOOL_RESULT_MAX_LINES)
|
|
181
|
+
return text;
|
|
182
|
+
const head = lines.slice(0, TOOL_RESULT_MAX_LINES).join("\n");
|
|
183
|
+
const remaining = lines.length - TOOL_RESULT_MAX_LINES;
|
|
184
|
+
return `${head}\n… (+${remaining} more line${remaining === 1 ? "" : "s"})`;
|
|
185
|
+
}
|
|
186
|
+
function appendBlock(label, body, fg) {
|
|
187
|
+
beginBlock();
|
|
188
|
+
const text = label ? `${label}\n${body}` : body;
|
|
189
|
+
for (const line of text.split("\n"))
|
|
190
|
+
appendLine(line, fg);
|
|
191
|
+
}
|
|
192
|
+
// Insert a blank separator before each new logical block so distinct steps
|
|
193
|
+
// (text, reasoning, tool calls, system messages) are easy to tell apart.
|
|
194
|
+
// The first block in the transcript skips the separator.
|
|
195
|
+
let hasRenderedAnyBlock = false;
|
|
196
|
+
function beginBlock() {
|
|
197
|
+
if (hasRenderedAnyBlock)
|
|
198
|
+
appendLine(" ", COLORS.hint);
|
|
199
|
+
hasRenderedAnyBlock = true;
|
|
200
|
+
}
|
|
201
|
+
function setStatus(text) {
|
|
202
|
+
status.content = text;
|
|
203
|
+
}
|
|
204
|
+
function setHint(running) {
|
|
205
|
+
hint.content = running ? HINT_RUNNING : HINT_IDLE;
|
|
206
|
+
}
|
|
207
|
+
// ---- runtime state ---------------------------------------------------------
|
|
208
|
+
let running = false;
|
|
209
|
+
let lastTerminal;
|
|
210
|
+
let activeTextStream;
|
|
211
|
+
let activeReasoningStream;
|
|
212
|
+
// Tool calls fire twice (running → completed/error). Track the rendered
|
|
213
|
+
// block by toolCallId so the second event updates the same line in place
|
|
214
|
+
// — swapping the spinner for a check/cross and appending the result —
|
|
215
|
+
// instead of pushing a separate block.
|
|
216
|
+
const activeToolBlocks = new Map();
|
|
217
|
+
function markRunning() {
|
|
218
|
+
running = true;
|
|
219
|
+
setHint(true);
|
|
220
|
+
setStatus("● working… (Esc to interrupt, Ctrl+C to force quit)");
|
|
221
|
+
}
|
|
222
|
+
function markIdle() {
|
|
223
|
+
running = false;
|
|
224
|
+
setHint(false);
|
|
225
|
+
setStatus("");
|
|
226
|
+
}
|
|
227
|
+
function reportError(error) {
|
|
228
|
+
appendBlock("[error]", error instanceof Error ? error.message : String(error), COLORS.error);
|
|
229
|
+
markIdle();
|
|
230
|
+
}
|
|
231
|
+
// ---- session subscription --------------------------------------------------
|
|
232
|
+
function refreshSidebar() {
|
|
233
|
+
const state = input.session.getState();
|
|
234
|
+
renderTodoSidebar(state?.todos ?? []);
|
|
235
|
+
renderStateMachineSidebar(state?.stateMachine);
|
|
236
|
+
}
|
|
237
|
+
function renderTodoSidebar(todos) {
|
|
238
|
+
if (todos.length === 0) {
|
|
239
|
+
todoBody.content = "(none)";
|
|
240
|
+
todoBody.fg = COLORS.hint;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const lines = todos.map((todo) => `${todoStatusGlyph(todo.status)} ${todo.content}`);
|
|
244
|
+
todoBody.content = lines.join("\n");
|
|
245
|
+
todoBody.fg = COLORS.agent;
|
|
246
|
+
}
|
|
247
|
+
function todoStatusGlyph(status) {
|
|
248
|
+
if (status === "completed")
|
|
249
|
+
return "✓";
|
|
250
|
+
if (status === "in_progress")
|
|
251
|
+
return "●";
|
|
252
|
+
if (status === "failed")
|
|
253
|
+
return "✗";
|
|
254
|
+
return "○";
|
|
255
|
+
}
|
|
256
|
+
function renderStateMachineSidebar(session) {
|
|
257
|
+
if (!session) {
|
|
258
|
+
smBody.content = "(inactive)";
|
|
259
|
+
smBody.fg = COLORS.hint;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const current = session.currentState;
|
|
263
|
+
const lines = session.definition.states.map((state) => {
|
|
264
|
+
const marker = state.name === current ? "▶" : " ";
|
|
265
|
+
return `${marker} ${state.name}`;
|
|
266
|
+
});
|
|
267
|
+
if (session.terminal) {
|
|
268
|
+
lines.push("", `terminal: ${session.terminal.status}`);
|
|
269
|
+
}
|
|
270
|
+
smBody.content = lines.join("\n");
|
|
271
|
+
smBody.fg = COLORS.agent;
|
|
272
|
+
}
|
|
273
|
+
const unsubscribe = input.session.subscribe((event) => {
|
|
274
|
+
// Sidebar mirrors the runner's authoritative state, so refresh it on
|
|
275
|
+
// every event rather than threading specific updates through each branch.
|
|
276
|
+
refreshSidebar();
|
|
277
|
+
if (event.type === "step") {
|
|
278
|
+
renderStep(event.step);
|
|
279
|
+
}
|
|
280
|
+
else if (event.type === "todos") {
|
|
281
|
+
renderTodos(event.todos);
|
|
282
|
+
}
|
|
283
|
+
else if (event.type === "follow_up_queue") {
|
|
284
|
+
renderFollowUpQueue(event.prompts);
|
|
285
|
+
}
|
|
286
|
+
else if (event.type === "memory") {
|
|
287
|
+
renderMemoryStatus(event);
|
|
288
|
+
}
|
|
289
|
+
else if (event.type === "system") {
|
|
290
|
+
appendBlock("[system]", event.message, COLORS.system);
|
|
291
|
+
if (event.level === "error")
|
|
292
|
+
markIdle();
|
|
293
|
+
}
|
|
294
|
+
else if (event.type === "ask") {
|
|
295
|
+
appendBlock("[question]", event.questions.map((q) => q.question).join("\n"), COLORS.system);
|
|
296
|
+
renderUsage(event.usage);
|
|
297
|
+
lastTerminal = event;
|
|
298
|
+
markIdle();
|
|
299
|
+
}
|
|
300
|
+
else if (event.type === "complete") {
|
|
301
|
+
if (event.error) {
|
|
302
|
+
appendBlock("[error]", event.error, COLORS.error);
|
|
303
|
+
}
|
|
304
|
+
else if (event.result) {
|
|
305
|
+
// Result is also normally streamed via text steps; only show if no streaming happened
|
|
306
|
+
// for this turn (cheap heuristic: empty transcript-since-last-prompt).
|
|
307
|
+
// Always-append is fine too — duplicate text is harmless and clearer for short turns.
|
|
308
|
+
}
|
|
309
|
+
renderUsage(event.usage);
|
|
310
|
+
lastTerminal = event;
|
|
311
|
+
markIdle();
|
|
312
|
+
}
|
|
313
|
+
else if (event.type === "interrupted") {
|
|
314
|
+
appendLine("[interrupted]", COLORS.system);
|
|
315
|
+
renderUsage(event.usage);
|
|
316
|
+
lastTerminal = event;
|
|
317
|
+
markIdle();
|
|
318
|
+
}
|
|
319
|
+
else if (event.type === "sleep") {
|
|
320
|
+
appendLine(`[sleeping until ${new Date(event.wakeAt).toLocaleTimeString()}]`, COLORS.system);
|
|
321
|
+
renderUsage(event.usage);
|
|
322
|
+
lastTerminal = event;
|
|
323
|
+
markIdle();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
function renderSetupIntro(skills, agentFiles, skillCollisions) {
|
|
327
|
+
const [title, ...details] = startupHeaderLines(input);
|
|
328
|
+
appendLine(title ?? "[duet]", COLORS.status);
|
|
329
|
+
for (const line of details) {
|
|
330
|
+
appendLine(line, line === input.newVersionNotice ? COLORS.system : COLORS.hint);
|
|
331
|
+
}
|
|
332
|
+
if (agentFiles.length === 0) {
|
|
333
|
+
appendLine("[agent file] none", COLORS.hint);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
appendLine(`[agent file] ${agentFiles.map((file) => file.name).join(", ")}`, COLORS.hint);
|
|
337
|
+
}
|
|
338
|
+
if (skills.length === 0) {
|
|
339
|
+
appendLine("[skills] none", COLORS.hint);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const names = skills.map((skill) => skill.name).join(", ");
|
|
343
|
+
appendLine(`[skills] ${skills.length} loaded: ${names}`, COLORS.hint);
|
|
344
|
+
}
|
|
345
|
+
for (const collision of skillCollisions) {
|
|
346
|
+
appendLine(`[skill collision] "${collision.name}": kept ${collision.winnerPath}, ignored ${collision.loserPath}`, COLORS.system);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function renderUsage(usage) {
|
|
350
|
+
if (!usage)
|
|
351
|
+
return;
|
|
352
|
+
const parts = [`in=${usage.inputTokens}`, `out=${usage.outputTokens}`];
|
|
353
|
+
if (usage.cachedInputTokens !== undefined)
|
|
354
|
+
parts.push(`cached=${usage.cachedInputTokens}`);
|
|
355
|
+
const cost = usage.costUsd === undefined ? "" : ` · Cost: $${usage.costUsd.toFixed(4)}`;
|
|
356
|
+
appendLine(`[usage] Tokens: ${parts.join(" ")}${cost}`, COLORS.hint);
|
|
357
|
+
}
|
|
358
|
+
function renderTodos(todos) {
|
|
359
|
+
if (todos.length === 0) {
|
|
360
|
+
appendBlock("[todos]", "No todos", COLORS.hint);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
appendBlock("[todos]", todos.map((todo) => `${todo.status} ${todo.id}: ${todo.content}`).join("\n"), COLORS.status);
|
|
364
|
+
}
|
|
365
|
+
function renderFollowUpQueue(prompts) {
|
|
366
|
+
if (prompts.length === 0) {
|
|
367
|
+
setStatus(running ? "● working… (Esc to interrupt, Ctrl+C to force quit)" : "");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
setStatus(`queued follow-ups: ${prompts.length}`);
|
|
371
|
+
appendBlock("[follow-up queue]", prompts.map((prompt, index) => `${index + 1}. ${prompt}`).join("\n"), COLORS.hint);
|
|
372
|
+
}
|
|
373
|
+
function renderStep(step) {
|
|
374
|
+
if (step.type === "text_delta") {
|
|
375
|
+
activeTextStream = renderDelta(activeTextStream, null, step.delta, COLORS.agent);
|
|
376
|
+
}
|
|
377
|
+
else if (step.type === "reasoning_delta") {
|
|
378
|
+
activeReasoningStream = renderDelta(activeReasoningStream, "[reasoning]", step.delta, COLORS.reasoning, true);
|
|
379
|
+
}
|
|
380
|
+
else if (step.type === "text") {
|
|
381
|
+
if (activeTextStream) {
|
|
382
|
+
finalizeDelta(activeTextStream, step.text);
|
|
383
|
+
activeTextStream = undefined;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
appendBlock(null, step.text, COLORS.agent);
|
|
387
|
+
}
|
|
388
|
+
else if (step.type === "reasoning") {
|
|
389
|
+
const trimmed = step.text.trim();
|
|
390
|
+
if (activeReasoningStream) {
|
|
391
|
+
finalizeDelta(activeReasoningStream, trimmed);
|
|
392
|
+
activeReasoningStream = undefined;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (trimmed)
|
|
396
|
+
appendBlock("[reasoning]", truncateToolResult(trimmed), COLORS.reasoning);
|
|
397
|
+
}
|
|
398
|
+
else if (step.type === "tool_call") {
|
|
399
|
+
renderToolCall(step);
|
|
400
|
+
}
|
|
401
|
+
else if (step.type === "system") {
|
|
402
|
+
appendBlock("[system]", step.message, COLORS.system);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function renderDelta(block, label, delta, fg, truncate = false) {
|
|
406
|
+
const next = block ??
|
|
407
|
+
{
|
|
408
|
+
line: new TextRenderable(renderer, { content: "", fg }),
|
|
409
|
+
label,
|
|
410
|
+
body: "",
|
|
411
|
+
truncate,
|
|
412
|
+
};
|
|
413
|
+
if (!block) {
|
|
414
|
+
beginBlock();
|
|
415
|
+
transcript.add(next.line);
|
|
416
|
+
}
|
|
417
|
+
next.body += delta;
|
|
418
|
+
updateStreamingBlock(next);
|
|
419
|
+
return next;
|
|
420
|
+
}
|
|
421
|
+
// Render a tool call as a single, self-updating block. The first event
|
|
422
|
+
// (`status: "running"`) creates the block with a spinner; the second event
|
|
423
|
+
// (`completed` or `error`) replaces the spinner with ✓/✗ and appends the
|
|
424
|
+
// truncated result inline so the call and its outcome stay visually paired.
|
|
425
|
+
function renderToolCall(step) {
|
|
426
|
+
const existing = activeToolBlocks.get(step.toolCallId);
|
|
427
|
+
if (!existing) {
|
|
428
|
+
const inputBody = step.input === undefined ? "" : formatCompactJson(step.input);
|
|
429
|
+
const header = `[tool ${step.toolName}] ⏳`;
|
|
430
|
+
const fg = step.status === "error" ? COLORS.error : COLORS.tool;
|
|
431
|
+
const line = new TextRenderable(renderer, {
|
|
432
|
+
content: inputBody ? `${header}\n${inputBody}` : header,
|
|
433
|
+
fg,
|
|
434
|
+
});
|
|
435
|
+
beginBlock();
|
|
436
|
+
transcript.add(line);
|
|
437
|
+
const block = { line, toolName: step.toolName, inputBody };
|
|
438
|
+
activeToolBlocks.set(step.toolCallId, block);
|
|
439
|
+
scrollToBottomSoon();
|
|
440
|
+
// The same event may already carry a terminal status (cached/replayed
|
|
441
|
+
// history). Fall through to finalize against the just-created block.
|
|
442
|
+
if (step.status !== "running" && step.status !== "pending") {
|
|
443
|
+
finalizeToolCall(step, block);
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
finalizeToolCall(step, existing);
|
|
448
|
+
}
|
|
449
|
+
function finalizeToolCall(step, block) {
|
|
450
|
+
const isError = step.status === "error";
|
|
451
|
+
const marker = isError ? "✗" : "✓";
|
|
452
|
+
const header = `[tool ${block.toolName}] ${marker}`;
|
|
453
|
+
const sections = [block.inputBody ? `${header}\n${block.inputBody}` : header];
|
|
454
|
+
if (step.output && step.output.length > 0) {
|
|
455
|
+
const text = textFromContent(step.output);
|
|
456
|
+
if (text) {
|
|
457
|
+
const label = isError ? "[error]" : "[result]";
|
|
458
|
+
sections.push(`${label}\n${truncateToolResult(text)}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
block.line.content = sections.join("\n");
|
|
462
|
+
block.line.fg = isError ? COLORS.error : COLORS.tool;
|
|
463
|
+
activeToolBlocks.delete(step.toolCallId);
|
|
464
|
+
scrollToBottomSoon();
|
|
465
|
+
}
|
|
466
|
+
function finalizeDelta(block, body) {
|
|
467
|
+
block.body = body;
|
|
468
|
+
updateStreamingBlock(block);
|
|
469
|
+
}
|
|
470
|
+
function updateStreamingBlock(block) {
|
|
471
|
+
const body = block.truncate ? truncateToolResult(block.body) : block.body;
|
|
472
|
+
block.line.content = block.label ? `${block.label}\n${body}` : body;
|
|
473
|
+
scrollToBottomSoon();
|
|
474
|
+
}
|
|
475
|
+
function renderMemoryStatus(event) {
|
|
476
|
+
if (event.status === "running") {
|
|
477
|
+
setStatus(`● ${event.message} (Esc to interrupt, Ctrl+C to force quit)`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (running) {
|
|
481
|
+
setStatus("● working… (Esc to interrupt, Ctrl+C to force quit)");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// ---- input handling --------------------------------------------------------
|
|
485
|
+
// Track shift state for the most recent Enter keypress. The focused
|
|
486
|
+
// InputRenderable handles its own `enter` event after onKeyDown fires, so we
|
|
487
|
+
// capture the modifier here and read it during the ENTER event below.
|
|
488
|
+
let lastEnterShift = false;
|
|
489
|
+
let closingAfterInterrupt = false;
|
|
490
|
+
const requestExit = async () => {
|
|
491
|
+
if (running) {
|
|
492
|
+
if (closingAfterInterrupt)
|
|
493
|
+
return;
|
|
494
|
+
closingAfterInterrupt = true;
|
|
495
|
+
setStatus("● interrupting…");
|
|
496
|
+
try {
|
|
497
|
+
await input.session.interrupt();
|
|
498
|
+
await input.session.waitForTerminal();
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
reportError(error);
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
renderer.destroy();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
renderer.destroy();
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const keyHandler = renderer._keyHandler;
|
|
512
|
+
keyHandler.onInternal("keypress", (key) => {
|
|
513
|
+
if (key.name !== "escape")
|
|
514
|
+
return;
|
|
515
|
+
key.preventDefault();
|
|
516
|
+
void requestExit();
|
|
517
|
+
});
|
|
518
|
+
// Attach directly to the focused InputRenderable. The Textarea-based input
|
|
519
|
+
// consumes escape via its own keybindings before any global keypress handler
|
|
520
|
+
// fires, so we intercept at the Renderable's onKeyDown hook which runs first.
|
|
521
|
+
inputField.onKeyDown = (key) => {
|
|
522
|
+
if (key.name === "return" || key.name === "enter") {
|
|
523
|
+
lastEnterShift = Boolean(key.shift);
|
|
524
|
+
// Take over Enter so the textarea does not insert a newline. We submit
|
|
525
|
+
// the current buffer contents and reset, regardless of shift state —
|
|
526
|
+
// shift only differentiates steer vs. queued follow-up.
|
|
527
|
+
const value = inputField.plainText.trim();
|
|
528
|
+
inputField.clear();
|
|
529
|
+
key.preventDefault();
|
|
530
|
+
if (value)
|
|
531
|
+
submit(value, lastEnterShift);
|
|
532
|
+
lastEnterShift = false;
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (key.name === "escape") {
|
|
536
|
+
void requestExit();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
function submit(message, shiftEnter) {
|
|
541
|
+
appendBlock("you:", message, COLORS.user);
|
|
542
|
+
if (running) {
|
|
543
|
+
// Mid-turn: Enter → steer, Shift+Enter → queued follow-up.
|
|
544
|
+
const behavior = shiftEnter ? "follow_up" : "steer";
|
|
545
|
+
void input.session.prompt({ message, behavior }).catch(reportError);
|
|
546
|
+
// Keep status as "working"; the existing turn continues.
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Idle: dispatch a prompt against the already-set-up session. Setup
|
|
550
|
+
// happens before the TUI starts so skills are visible right away.
|
|
551
|
+
void input.session.prompt({ message, behavior: "follow_up" }).catch(reportError);
|
|
552
|
+
markRunning();
|
|
553
|
+
}
|
|
554
|
+
// ---- replay history on resume ---------------------------------------------
|
|
555
|
+
// Setup already ran before the TUI launched, so we can read the resolved
|
|
556
|
+
// skills/agent-files synchronously through the session getters.
|
|
557
|
+
const [skills, agentFiles, skillCollisions] = await Promise.all([
|
|
558
|
+
input.session.getSkills(),
|
|
559
|
+
input.session.getResolvedAgentFiles(),
|
|
560
|
+
input.session.getSkillCollisions(),
|
|
561
|
+
]);
|
|
562
|
+
renderSetupIntro(skills, agentFiles, skillCollisions);
|
|
563
|
+
refreshSidebar();
|
|
564
|
+
const resumeHistoryLines = input.resumeHistoryLines ?? Number.POSITIVE_INFINITY;
|
|
565
|
+
if (resumeHistoryLines > 0 && input.history && input.history.length > 0) {
|
|
566
|
+
const limited = limitHistoryDisplayBlocks(historyDisplayBlocks(input.history), resumeHistoryLines);
|
|
567
|
+
if (limited.omittedLines > 0) {
|
|
568
|
+
appendLine(`[resume] showing last ${resumeHistoryLines} lines of prior session history`, COLORS.hint);
|
|
569
|
+
}
|
|
570
|
+
for (const block of limited.blocks) {
|
|
571
|
+
appendDisplayBlock(block);
|
|
572
|
+
}
|
|
573
|
+
scrollToBottomSoon();
|
|
574
|
+
}
|
|
575
|
+
// ---- bootstrap initial prompt ----------------------------------------------
|
|
576
|
+
if (input.initialPrompt) {
|
|
577
|
+
appendBlock("you:", input.initialPrompt, COLORS.user);
|
|
578
|
+
void input.session
|
|
579
|
+
.prompt({ message: input.initialPrompt, behavior: "follow_up" })
|
|
580
|
+
.catch(reportError);
|
|
581
|
+
markRunning();
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// No initial prompt — wait for the user. Setup already ran above, so
|
|
585
|
+
// the skill summary is rendered before the user types.
|
|
586
|
+
markIdle();
|
|
587
|
+
}
|
|
588
|
+
// ---- run renderer until the user quits -------------------------------------
|
|
589
|
+
await new Promise((resolve) => {
|
|
590
|
+
const onDestroy = () => resolve();
|
|
591
|
+
renderer.once("destroy", onDestroy);
|
|
592
|
+
});
|
|
593
|
+
unsubscribe();
|
|
594
|
+
return lastTerminal;
|
|
595
|
+
// --------------------------------------------------------------------------
|
|
596
|
+
function textFromContent(content) {
|
|
597
|
+
return content
|
|
598
|
+
.filter((b) => b.type === "text")
|
|
599
|
+
.map((b) => b.text)
|
|
600
|
+
.join("\n");
|
|
601
|
+
}
|
|
602
|
+
function appendDisplayBlock(block) {
|
|
603
|
+
appendBlock(null, block.content, colorForHistoryBlock(block.kind));
|
|
604
|
+
}
|
|
605
|
+
function colorForHistoryBlock(kind) {
|
|
606
|
+
if (kind === "user")
|
|
607
|
+
return COLORS.user;
|
|
608
|
+
if (kind === "reasoning")
|
|
609
|
+
return COLORS.reasoning;
|
|
610
|
+
if (kind === "tool")
|
|
611
|
+
return COLORS.tool;
|
|
612
|
+
if (kind === "error")
|
|
613
|
+
return COLORS.error;
|
|
614
|
+
return COLORS.agent;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
export function historyDisplayBlocks(history) {
|
|
618
|
+
const blocks = [];
|
|
619
|
+
const activeToolBlockIndexes = new Map();
|
|
620
|
+
for (const message of history) {
|
|
621
|
+
if (!("role" in message))
|
|
622
|
+
continue;
|
|
623
|
+
if (message.role === "user") {
|
|
624
|
+
const text = userMessageText(message.content);
|
|
625
|
+
if (text)
|
|
626
|
+
blocks.push({ kind: "user", content: `you:\n${text}` });
|
|
627
|
+
}
|
|
628
|
+
else if (message.role === "assistant") {
|
|
629
|
+
for (const block of message.content) {
|
|
630
|
+
if (block.type === "text") {
|
|
631
|
+
blocks.push({ kind: "agent", content: block.text });
|
|
632
|
+
}
|
|
633
|
+
else if (block.type === "thinking") {
|
|
634
|
+
const trimmed = block.thinking.trim();
|
|
635
|
+
if (trimmed)
|
|
636
|
+
blocks.push({ kind: "reasoning", content: `[reasoning]\n${trimmed}` });
|
|
637
|
+
}
|
|
638
|
+
else if (block.type === "toolCall") {
|
|
639
|
+
const input = block.arguments === undefined ? "" : `\n${formatCompactJson(block.arguments)}`;
|
|
640
|
+
activeToolBlockIndexes.set(block.id, blocks.length);
|
|
641
|
+
blocks.push({ kind: "tool", content: `[tool ${block.name}] ⏳${input}` });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (message.errorMessage) {
|
|
645
|
+
blocks.push({ kind: "error", content: `[error]\n${message.errorMessage}` });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
else if (message.role === "toolResult") {
|
|
649
|
+
const text = textFromHistoryContent(message.content);
|
|
650
|
+
const existingIndex = activeToolBlockIndexes.get(message.toolCallId);
|
|
651
|
+
const marker = message.isError ? "✗" : "✓";
|
|
652
|
+
const label = message.isError ? "[error]" : "[result]";
|
|
653
|
+
if (existingIndex !== undefined) {
|
|
654
|
+
const existing = blocks[existingIndex];
|
|
655
|
+
const [, ...inputLines] = existing.content.split("\n");
|
|
656
|
+
const input = inputLines.length > 0 ? `\n${inputLines.join("\n")}` : "";
|
|
657
|
+
existing.kind = message.isError ? "error" : "tool";
|
|
658
|
+
existing.content = text
|
|
659
|
+
? `[tool ${message.toolName}] ${marker}${input}\n${label}\n${text}`
|
|
660
|
+
: `[tool ${message.toolName}] ${marker}${input}`;
|
|
661
|
+
activeToolBlockIndexes.delete(message.toolCallId);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
const content = text
|
|
665
|
+
? `[tool ${message.toolName}] ${marker}\n${label}\n${text}`
|
|
666
|
+
: `[tool ${message.toolName}] ${marker}`;
|
|
667
|
+
blocks.push({ kind: message.isError ? "error" : "tool", content });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return blocks;
|
|
672
|
+
}
|
|
673
|
+
export function startupHeaderLines(input) {
|
|
674
|
+
const lines = [
|
|
675
|
+
`[duet] v${input.packageVersion}`,
|
|
676
|
+
`[cwd] ${input.workDir}`,
|
|
677
|
+
`[session] ${input.sessionId}`,
|
|
678
|
+
input.modelSource
|
|
679
|
+
? `[model] ${input.modelName} — ${input.modelSource}`
|
|
680
|
+
: `[model] ${input.modelName}`,
|
|
681
|
+
input.memoryModelSource
|
|
682
|
+
? `[memory model] ${input.memoryModelName} — ${input.memoryModelSource}`
|
|
683
|
+
: `[memory model] ${input.memoryModelName}`,
|
|
684
|
+
];
|
|
685
|
+
if (input.newVersionNotice)
|
|
686
|
+
lines.push(input.newVersionNotice);
|
|
687
|
+
return lines;
|
|
688
|
+
}
|
|
689
|
+
export function limitHistoryDisplayBlocks(blocks, maxLines) {
|
|
690
|
+
if (maxLines <= 0)
|
|
691
|
+
return { blocks: [], omittedLines: countHistoryLines(blocks) };
|
|
692
|
+
const selected = [];
|
|
693
|
+
let remaining = maxLines;
|
|
694
|
+
let omittedLines = 0;
|
|
695
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
696
|
+
const block = blocks[index];
|
|
697
|
+
const lines = block.content.split("\n");
|
|
698
|
+
if (lines.length <= remaining) {
|
|
699
|
+
selected.unshift(block);
|
|
700
|
+
remaining -= lines.length;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (remaining > 0) {
|
|
704
|
+
selected.unshift({ ...block, content: lines.slice(-remaining).join("\n") });
|
|
705
|
+
omittedLines += lines.length - remaining;
|
|
706
|
+
remaining = 0;
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
omittedLines += lines.length;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return { blocks: selected, omittedLines };
|
|
713
|
+
}
|
|
714
|
+
function countHistoryLines(blocks) {
|
|
715
|
+
return blocks.reduce((count, block) => count + block.content.split("\n").length, 0);
|
|
716
|
+
}
|
|
717
|
+
function userMessageText(content) {
|
|
718
|
+
if (typeof content === "string")
|
|
719
|
+
return content;
|
|
720
|
+
return content
|
|
721
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
722
|
+
.map((block) => block.text)
|
|
723
|
+
.join("");
|
|
724
|
+
}
|
|
725
|
+
function textFromHistoryContent(content) {
|
|
726
|
+
return content
|
|
727
|
+
.filter((block) => block.type === "text")
|
|
728
|
+
.map((block) => block.text)
|
|
729
|
+
.join("\n");
|
|
730
|
+
}
|
|
731
|
+
function restoreWindowGlobal(previousWindow) {
|
|
732
|
+
// OpenTUI installs `window.requestAnimationFrame` for browser-style
|
|
733
|
+
// animation compatibility. In Bun, the presence of `window` can send fetch
|
|
734
|
+
// internals down browser-only paths, while `global.requestAnimationFrame`
|
|
735
|
+
// remains enough for OpenTUI after initialization.
|
|
736
|
+
if (previousWindow) {
|
|
737
|
+
Object.defineProperty(globalThis, "window", previousWindow);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
delete globalThis.window;
|
|
741
|
+
}
|
|
742
|
+
//# sourceMappingURL=app.js.map
|