@microboxlabs/miot-chat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +200 -0
- package/dist/cli.js +3321 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +3097 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3097 @@
|
|
|
1
|
+
// src/repl/loop.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4
|
+
import { createInterface } from "readline";
|
|
5
|
+
import {
|
|
6
|
+
MiotHarnessApiError
|
|
7
|
+
} from "@microboxlabs/miot-harness-client";
|
|
8
|
+
|
|
9
|
+
// src/output.ts
|
|
10
|
+
var ESC = "\x1B[";
|
|
11
|
+
var RESET = `${ESC}0m`;
|
|
12
|
+
var CLEAR_LINE = `\r${ESC}K`;
|
|
13
|
+
function useColor(opts = {}) {
|
|
14
|
+
if (opts.noColor === true) return false;
|
|
15
|
+
if (opts.isTTY === false) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function dim(s, opts = {}) {
|
|
19
|
+
return useColor(opts) ? `${ESC}2m${s}${RESET}` : s;
|
|
20
|
+
}
|
|
21
|
+
function bold(s, opts = {}) {
|
|
22
|
+
return useColor(opts) ? `${ESC}1m${s}${RESET}` : s;
|
|
23
|
+
}
|
|
24
|
+
function red(s, opts = {}) {
|
|
25
|
+
return useColor(opts) ? `${ESC}31m${s}${RESET}` : s;
|
|
26
|
+
}
|
|
27
|
+
function yellow(s, opts = {}) {
|
|
28
|
+
return useColor(opts) ? `${ESC}33m${s}${RESET}` : s;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/repl/renderer.ts
|
|
32
|
+
import {
|
|
33
|
+
TERMINAL_EVENT_TYPES
|
|
34
|
+
} from "@microboxlabs/miot-harness-client";
|
|
35
|
+
function statusPrefix(state) {
|
|
36
|
+
if (!state.hasStatusLine) return "";
|
|
37
|
+
return useColor(state.color) ? CLEAR_LINE : "\n";
|
|
38
|
+
}
|
|
39
|
+
function statusSuffix(color) {
|
|
40
|
+
return useColor(color) ? "" : "\n";
|
|
41
|
+
}
|
|
42
|
+
function initialState(color = {}) {
|
|
43
|
+
return {
|
|
44
|
+
pendingAnswer: null,
|
|
45
|
+
hasStatusLine: false,
|
|
46
|
+
pendingThinking: "",
|
|
47
|
+
hasThinkingBlock: false,
|
|
48
|
+
color
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function renderEvent(state, event) {
|
|
52
|
+
switch (event.type) {
|
|
53
|
+
case "answer.completed":
|
|
54
|
+
return {
|
|
55
|
+
state: {
|
|
56
|
+
...state,
|
|
57
|
+
pendingAnswer: extractAnswerText(event) ?? state.pendingAnswer
|
|
58
|
+
},
|
|
59
|
+
output: ""
|
|
60
|
+
};
|
|
61
|
+
case "run.completed":
|
|
62
|
+
case "run.failed":
|
|
63
|
+
return clearStatus(state);
|
|
64
|
+
case "thinking.delta": {
|
|
65
|
+
const delta = typeof event.data.delta === "string" ? event.data.delta : "";
|
|
66
|
+
if (!delta) return { state, output: "" };
|
|
67
|
+
const prefix = state.hasStatusLine ? statusPrefix(state) : "";
|
|
68
|
+
return {
|
|
69
|
+
state: {
|
|
70
|
+
...state,
|
|
71
|
+
hasStatusLine: false,
|
|
72
|
+
pendingThinking: state.pendingThinking + delta,
|
|
73
|
+
hasThinkingBlock: true
|
|
74
|
+
},
|
|
75
|
+
output: `${prefix}${dim(delta, state.color)}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
case "thinking.completed": {
|
|
79
|
+
const output = state.hasThinkingBlock ? "\n" : "";
|
|
80
|
+
return {
|
|
81
|
+
state: { ...state, hasThinkingBlock: false },
|
|
82
|
+
output
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
default: {
|
|
86
|
+
if (TERMINAL_EVENT_TYPES.has(event.type)) {
|
|
87
|
+
return { state, output: "" };
|
|
88
|
+
}
|
|
89
|
+
const summary = statusFor(event);
|
|
90
|
+
if (summary === null) return { state, output: "" };
|
|
91
|
+
const prefix = statusPrefix(state);
|
|
92
|
+
const line = colorize(event.type, summary, state.color);
|
|
93
|
+
const suffix = statusSuffix(state.color);
|
|
94
|
+
return {
|
|
95
|
+
state: {
|
|
96
|
+
...state,
|
|
97
|
+
hasStatusLine: useColor(state.color) ? true : false
|
|
98
|
+
},
|
|
99
|
+
output: `${prefix}${line}${suffix}`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function clearStatus(state) {
|
|
105
|
+
if (!state.hasStatusLine) return { state, output: "" };
|
|
106
|
+
return { state: { ...state, hasStatusLine: false }, output: statusPrefix(state) };
|
|
107
|
+
}
|
|
108
|
+
function renderAuthoritativeAnswer(state, answer) {
|
|
109
|
+
const prefix = statusPrefix(state);
|
|
110
|
+
const text = answer ?? state.pendingAnswer ?? "(no answer recorded)";
|
|
111
|
+
const lead = state.hasThinkingBlock ? "\n" : "";
|
|
112
|
+
return {
|
|
113
|
+
state: {
|
|
114
|
+
...state,
|
|
115
|
+
hasStatusLine: false,
|
|
116
|
+
hasThinkingBlock: false,
|
|
117
|
+
pendingAnswer: text
|
|
118
|
+
},
|
|
119
|
+
output: `${lead}${prefix}${bold(text, state.color)}
|
|
120
|
+
`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function renderRunFailure(state, message) {
|
|
124
|
+
const prefix = statusPrefix(state);
|
|
125
|
+
return {
|
|
126
|
+
state: { ...state, hasStatusLine: false },
|
|
127
|
+
output: `${prefix}${red(`error: ${message || "run failed"}`, state.color)}
|
|
128
|
+
`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function extractAnswerText(event) {
|
|
132
|
+
const data = event.data;
|
|
133
|
+
if (typeof data?.text === "string" && data.text.length > 0) return data.text;
|
|
134
|
+
if (typeof data?.answer === "string" && data.answer.length > 0) {
|
|
135
|
+
return data.answer;
|
|
136
|
+
}
|
|
137
|
+
return event.message.length > 0 ? event.message : null;
|
|
138
|
+
}
|
|
139
|
+
function colorize(type, text, color) {
|
|
140
|
+
if (type === "freshness.warning") return yellow(text, color);
|
|
141
|
+
if (type === "tool.failed") return red(text, color);
|
|
142
|
+
if (type === "agent.started" || type === "agent.completed") {
|
|
143
|
+
return bold(text, color);
|
|
144
|
+
}
|
|
145
|
+
return dim(text, color);
|
|
146
|
+
}
|
|
147
|
+
function statusFor(event) {
|
|
148
|
+
switch (event.type) {
|
|
149
|
+
case "run.started":
|
|
150
|
+
return "starting\u2026";
|
|
151
|
+
case "route.selected": {
|
|
152
|
+
const route = typeof event.data.route === "string" ? event.data.route : event.message;
|
|
153
|
+
return route ? `route: ${route}` : null;
|
|
154
|
+
}
|
|
155
|
+
case "agent.turn": {
|
|
156
|
+
const agent = typeof event.data.agent === "string" ? event.data.agent : event.message;
|
|
157
|
+
return agent ? `agent: ${agent}` : null;
|
|
158
|
+
}
|
|
159
|
+
case "agent.started": {
|
|
160
|
+
const agent = typeof event.data.agent === "string" ? event.data.agent : event.message;
|
|
161
|
+
return agent ? `\u25B6 ${agent}` : null;
|
|
162
|
+
}
|
|
163
|
+
case "agent.completed": {
|
|
164
|
+
const agent = typeof event.data.agent === "string" ? event.data.agent : event.message;
|
|
165
|
+
const ms = typeof event.data.duration_ms === "number" ? event.data.duration_ms : null;
|
|
166
|
+
if (!agent) return null;
|
|
167
|
+
return ms !== null ? `\u2713 ${agent} (${ms}ms)` : `\u2713 ${agent}`;
|
|
168
|
+
}
|
|
169
|
+
case "plan.created":
|
|
170
|
+
return event.message || "plan ready";
|
|
171
|
+
case "tool.started": {
|
|
172
|
+
const name = toolName(event);
|
|
173
|
+
if (!name) return null;
|
|
174
|
+
const keys = Array.isArray(event.data.input_keys) ? event.data.input_keys.join(",") : "";
|
|
175
|
+
return keys ? `tool: ${name}(${keys})` : `tool: ${name}`;
|
|
176
|
+
}
|
|
177
|
+
case "tool.completed": {
|
|
178
|
+
const name = toolName(event);
|
|
179
|
+
const shape = event.data.result_shape;
|
|
180
|
+
const tail = shape && typeof shape.type === "string" ? ` \u2192 ${shape.type}[${shape.length ?? 0}]` : "";
|
|
181
|
+
if (!name) return "tool ok";
|
|
182
|
+
return `tool ok: ${name}${tail}`;
|
|
183
|
+
}
|
|
184
|
+
case "tool.failed": {
|
|
185
|
+
const name = toolName(event);
|
|
186
|
+
return name ? `tool failed: ${name}` : "tool failed";
|
|
187
|
+
}
|
|
188
|
+
case "freshness.warning":
|
|
189
|
+
return event.message || "stale data";
|
|
190
|
+
case "approval.requested":
|
|
191
|
+
return event.message || "approval needed";
|
|
192
|
+
case "artifact.created": {
|
|
193
|
+
const kind = typeof event.data.kind === "string" ? event.data.kind : "artifact";
|
|
194
|
+
return `artifact: ${kind}`;
|
|
195
|
+
}
|
|
196
|
+
case "usage.recorded": {
|
|
197
|
+
const agent = typeof event.data.agent === "string" ? event.data.agent : "";
|
|
198
|
+
const inT = typeof event.data.input_tokens === "number" ? event.data.input_tokens : 0;
|
|
199
|
+
const outT = typeof event.data.output_tokens === "number" ? event.data.output_tokens : 0;
|
|
200
|
+
return agent ? `usage: ${agent} in=${inT} out=${outT}` : null;
|
|
201
|
+
}
|
|
202
|
+
default:
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function toolName(event) {
|
|
207
|
+
if (typeof event.data.tool === "string") return event.data.tool;
|
|
208
|
+
if (typeof event.data.name === "string") return event.data.name;
|
|
209
|
+
return event.message;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/repl/conversation.ts
|
|
213
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
214
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
215
|
+
|
|
216
|
+
// src/config.ts
|
|
217
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
218
|
+
import { homedir as homedir2 } from "os";
|
|
219
|
+
import { dirname, join as join2 } from "path";
|
|
220
|
+
|
|
221
|
+
// src/miotrc.ts
|
|
222
|
+
import { readFileSync } from "fs";
|
|
223
|
+
import { homedir } from "os";
|
|
224
|
+
import { join } from "path";
|
|
225
|
+
function readMiotrcProfile(opts) {
|
|
226
|
+
const env = opts?.env ?? process.env;
|
|
227
|
+
const home = (env.HOME ?? homedir()) || ".";
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(
|
|
230
|
+
readFileSync(join(home, ".miotrc.json"), "utf-8")
|
|
231
|
+
);
|
|
232
|
+
const name = opts?.profile ?? parsed.defaultProfile;
|
|
233
|
+
const profile = name ? parsed.profiles?.[name] : void 0;
|
|
234
|
+
if (!profile?.baseUrl || !profile.token) return null;
|
|
235
|
+
return {
|
|
236
|
+
baseUrl: profile.baseUrl,
|
|
237
|
+
token: profile.token,
|
|
238
|
+
...typeof profile.organizationId === "string" && {
|
|
239
|
+
organizationId: profile.organizationId
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/config.ts
|
|
248
|
+
var VALID_MODES = /* @__PURE__ */ new Set(["auto", "canned", "meta", "agentic"]);
|
|
249
|
+
var DEFAULT_CONFIG = {
|
|
250
|
+
defaultProfile: "local",
|
|
251
|
+
profiles: {
|
|
252
|
+
local: {
|
|
253
|
+
baseUrl: "http://localhost:8000",
|
|
254
|
+
token: null,
|
|
255
|
+
tenantId: "demo-tenant",
|
|
256
|
+
userId: "demo-user"
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
function getConfigDir(env) {
|
|
261
|
+
const home = (env?.HOME ?? homedir2()) || ".";
|
|
262
|
+
return join2(home, ".miot-chat");
|
|
263
|
+
}
|
|
264
|
+
function getConfigPath(env) {
|
|
265
|
+
return join2(getConfigDir(env), "config.json");
|
|
266
|
+
}
|
|
267
|
+
function readConfig(opts) {
|
|
268
|
+
const dir = opts?.configDir ?? getConfigDir();
|
|
269
|
+
const path = join2(dir, "config.json");
|
|
270
|
+
if (!existsSync(path)) return cloneDefault();
|
|
271
|
+
try {
|
|
272
|
+
const raw = readFileSync2(path, "utf-8");
|
|
273
|
+
const parsed = JSON.parse(raw);
|
|
274
|
+
return normalize(parsed);
|
|
275
|
+
} catch {
|
|
276
|
+
return cloneDefault();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function writeConfig(cfg, opts) {
|
|
280
|
+
const dir = opts?.configDir ?? getConfigDir();
|
|
281
|
+
const path = join2(dir, "config.json");
|
|
282
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
283
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
284
|
+
}
|
|
285
|
+
function resolveConfig(opts = {}) {
|
|
286
|
+
const env = opts.env ?? process.env;
|
|
287
|
+
const flags = opts.flags ?? {};
|
|
288
|
+
const cfg = readConfig({ configDir: opts.configDir ?? getConfigDir(env) });
|
|
289
|
+
const profileName = flags.profile ?? env.MIOT_CHAT_PROFILE ?? cfg.defaultProfile ?? "local";
|
|
290
|
+
const profile = cfg.profiles[profileName] ?? cfg.profiles[cfg.defaultProfile] ?? DEFAULT_CONFIG.profiles["local"];
|
|
291
|
+
const tokenFlag = flags.token ?? env.MIOT_CHAT_TOKEN;
|
|
292
|
+
const platform = tokenFlag === void 0 && (profile.token ?? null) === null ? readMiotrcProfile({ env }) : null;
|
|
293
|
+
const baseUrl = flags.baseUrl ?? env.MIOT_CHAT_BASE_URL ?? (platform ? platform.baseUrl : profile.baseUrl);
|
|
294
|
+
const token = tokenFlag ?? profile.token ?? platform?.token ?? null;
|
|
295
|
+
const tenantId = flags.tenant ?? env.MIOT_CHAT_TENANT_ID ?? profile.tenantId;
|
|
296
|
+
const userId = flags.user ?? env.MIOT_CHAT_USER_ID ?? profile.userId;
|
|
297
|
+
const modeRaw = flags.mode ?? env.MIOT_CHAT_MODE ?? profile.mode ?? "auto";
|
|
298
|
+
const mode = VALID_MODES.has(modeRaw) ? modeRaw : "auto";
|
|
299
|
+
const debug = Boolean(
|
|
300
|
+
flags.debug ?? (env.MIOT_CHAT_DEBUG ? env.MIOT_CHAT_DEBUG !== "0" : false)
|
|
301
|
+
);
|
|
302
|
+
const orgSlug = nonEmpty(flags.org) ?? nonEmpty(env.MIOT_CHAT_ORG) ?? nonEmpty(profile.orgSlug) ?? nonEmpty(platform?.organizationId) ?? null;
|
|
303
|
+
const harnessBaseUrl = orgSlug ? `${trimTrailingSlashes(baseUrl)}/api/v1/orgs/${encodeURIComponent(orgSlug)}/harness` : baseUrl;
|
|
304
|
+
return {
|
|
305
|
+
baseUrl,
|
|
306
|
+
token,
|
|
307
|
+
tenantId,
|
|
308
|
+
userId,
|
|
309
|
+
mode,
|
|
310
|
+
profileName,
|
|
311
|
+
theme: cfg.theme ?? null,
|
|
312
|
+
debug,
|
|
313
|
+
orgSlug,
|
|
314
|
+
harnessBaseUrl
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function nonEmpty(v) {
|
|
318
|
+
return v === void 0 || v === "" ? void 0 : v;
|
|
319
|
+
}
|
|
320
|
+
function cloneDefault() {
|
|
321
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
322
|
+
}
|
|
323
|
+
function trimTrailingSlashes(s) {
|
|
324
|
+
let end = s.length;
|
|
325
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
|
|
326
|
+
return end === s.length ? s : s.slice(0, end);
|
|
327
|
+
}
|
|
328
|
+
function normalize(parsed) {
|
|
329
|
+
const profiles = {};
|
|
330
|
+
const sourceProfiles = parsed.profiles ?? {};
|
|
331
|
+
for (const [name, p] of Object.entries(sourceProfiles)) {
|
|
332
|
+
profiles[name] = {
|
|
333
|
+
baseUrl: p.baseUrl ?? "http://localhost:8000",
|
|
334
|
+
token: p.token ?? null,
|
|
335
|
+
tenantId: p.tenantId ?? "demo-tenant",
|
|
336
|
+
userId: p.userId ?? "demo-user",
|
|
337
|
+
mode: VALID_MODES.has(p.mode) ? p.mode : void 0,
|
|
338
|
+
...typeof p.orgSlug === "string" && p.orgSlug !== "" ? { orgSlug: p.orgSlug } : void 0
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (Object.keys(profiles).length === 0) {
|
|
342
|
+
profiles.local = { ...DEFAULT_CONFIG.profiles.local };
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
defaultProfile: parsed.defaultProfile && profiles[parsed.defaultProfile] ? parsed.defaultProfile : Object.keys(profiles)[0],
|
|
346
|
+
profiles,
|
|
347
|
+
theme: normalizeTheme(parsed.theme)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function normalizeTheme(theme) {
|
|
351
|
+
if (theme === void 0 || theme === null) return void 0;
|
|
352
|
+
if (typeof theme === "string") return theme;
|
|
353
|
+
if (typeof theme === "object") {
|
|
354
|
+
const out = {};
|
|
355
|
+
if (typeof theme.name === "string") out.name = theme.name;
|
|
356
|
+
if (theme.tokens && typeof theme.tokens === "object") {
|
|
357
|
+
const tokens = {};
|
|
358
|
+
for (const [k, v] of Object.entries(
|
|
359
|
+
theme.tokens
|
|
360
|
+
)) {
|
|
361
|
+
if (typeof v === "string") tokens[k] = v;
|
|
362
|
+
}
|
|
363
|
+
if (Object.keys(tokens).length > 0) out.tokens = tokens;
|
|
364
|
+
}
|
|
365
|
+
return out.name || out.tokens ? out : void 0;
|
|
366
|
+
}
|
|
367
|
+
return void 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/repl/conversation.ts
|
|
371
|
+
var FILENAME = "last-conversation";
|
|
372
|
+
function conversationFilePath(configDir) {
|
|
373
|
+
return join3(configDir ?? getConfigDir(), FILENAME);
|
|
374
|
+
}
|
|
375
|
+
function writeLastConversation(conversationId, configDir) {
|
|
376
|
+
const path = conversationFilePath(configDir);
|
|
377
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
378
|
+
writeFileSync2(path, `${conversationId}
|
|
379
|
+
`, { mode: 384 });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/repl/slash.ts
|
|
383
|
+
var AGENTIC_TENANT_LOCK = "mintral";
|
|
384
|
+
var VALID_MODES2 = /* @__PURE__ */ new Set([
|
|
385
|
+
"auto",
|
|
386
|
+
"canned",
|
|
387
|
+
"meta",
|
|
388
|
+
"agentic"
|
|
389
|
+
]);
|
|
390
|
+
function parseSlash(line, state) {
|
|
391
|
+
const trimmed = line.trim();
|
|
392
|
+
if (!trimmed.startsWith("/")) return { kind: "noop" };
|
|
393
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
394
|
+
const head = parts[0]?.toLowerCase() ?? "";
|
|
395
|
+
const rest = parts.slice(1);
|
|
396
|
+
switch (head) {
|
|
397
|
+
case "":
|
|
398
|
+
return { kind: "invalid", reason: "empty slash command" };
|
|
399
|
+
case "exit":
|
|
400
|
+
case "quit":
|
|
401
|
+
return { kind: "exit" };
|
|
402
|
+
case "reset":
|
|
403
|
+
return { kind: "reset" };
|
|
404
|
+
case "mode": {
|
|
405
|
+
const value = rest[0];
|
|
406
|
+
if (value === void 0) {
|
|
407
|
+
return {
|
|
408
|
+
kind: "invalid",
|
|
409
|
+
reason: "usage: /mode <auto|canned|meta|agentic>"
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (!VALID_MODES2.has(value)) {
|
|
413
|
+
return { kind: "invalid", reason: `unknown mode: ${value}` };
|
|
414
|
+
}
|
|
415
|
+
const newMode = value;
|
|
416
|
+
return {
|
|
417
|
+
kind: "set-mode",
|
|
418
|
+
mode: newMode,
|
|
419
|
+
warnAgenticTenantMismatch: newMode === "agentic" && state.tenant !== AGENTIC_TENANT_LOCK
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
case "tenant": {
|
|
423
|
+
const value = rest[0];
|
|
424
|
+
if (value === void 0 || value.length === 0) {
|
|
425
|
+
return { kind: "invalid", reason: "usage: /tenant <id>" };
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
kind: "set-tenant",
|
|
429
|
+
tenant: value,
|
|
430
|
+
warnAgenticTenantMismatch: state.mode === "agentic" && value !== AGENTIC_TENANT_LOCK
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
case "save": {
|
|
434
|
+
const path = rest.join(" ");
|
|
435
|
+
if (path.length === 0) {
|
|
436
|
+
return { kind: "invalid", reason: "usage: /save <file>" };
|
|
437
|
+
}
|
|
438
|
+
return { kind: "save", path };
|
|
439
|
+
}
|
|
440
|
+
default:
|
|
441
|
+
return { kind: "invalid", reason: `unknown command: /${head}` };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/repl/loop.ts
|
|
446
|
+
async function runRepl(opts) {
|
|
447
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
448
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
449
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
450
|
+
const color = {
|
|
451
|
+
noColor: opts.noColor ?? Boolean(process.env.NO_COLOR),
|
|
452
|
+
isTTY: "isTTY" in stdout ? stdout.isTTY : false
|
|
453
|
+
};
|
|
454
|
+
const session = {
|
|
455
|
+
mode: opts.config.mode,
|
|
456
|
+
tenant: opts.config.tenantId,
|
|
457
|
+
user: opts.config.userId,
|
|
458
|
+
conversationId: opts.conversationId ?? randomUUID(),
|
|
459
|
+
debug: opts.config.debug,
|
|
460
|
+
transcript: []
|
|
461
|
+
};
|
|
462
|
+
const rl = createInterface({ input: stdin, output: stdout, terminal: false });
|
|
463
|
+
let currentAbort = null;
|
|
464
|
+
let exitCode = 0;
|
|
465
|
+
rl.on("SIGINT", () => {
|
|
466
|
+
if (currentAbort) {
|
|
467
|
+
currentAbort.abort();
|
|
468
|
+
stdout.write(`${CLEAR_LINE}${dim("aborted.", color)}
|
|
469
|
+
`);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
if (opts.greet !== false) {
|
|
473
|
+
stdout.write(
|
|
474
|
+
`${dim(`miot-chat \u2192 ${opts.config.harnessBaseUrl} (${session.mode} / ${session.tenant})`, color)}
|
|
475
|
+
`
|
|
476
|
+
);
|
|
477
|
+
stdout.write(`${dim(`conversation: ${session.conversationId}`, color)}
|
|
478
|
+
`);
|
|
479
|
+
warnIfAgenticMismatch(session, color, stdout);
|
|
480
|
+
}
|
|
481
|
+
const promptFn = () => promptFor(session, color);
|
|
482
|
+
rl.setPrompt(promptFn());
|
|
483
|
+
try {
|
|
484
|
+
for await (const line of iterateLines(rl, stdout, promptFn)) {
|
|
485
|
+
const slashAction = parseSlash(line, slashStateOf(session));
|
|
486
|
+
if (slashAction.kind === "noop" && line.trim().length === 0) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (slashAction.kind === "exit") {
|
|
490
|
+
try {
|
|
491
|
+
writeLastConversation(session.conversationId, opts.configDir);
|
|
492
|
+
} catch (e) {
|
|
493
|
+
stderr.write(
|
|
494
|
+
`${red(`could not persist last conversation: ${describeError(e)}`, color)}
|
|
495
|
+
`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
if (slashAction.kind === "reset") {
|
|
501
|
+
session.conversationId = randomUUID();
|
|
502
|
+
session.transcript = [];
|
|
503
|
+
stdout.write(
|
|
504
|
+
`${dim(`new conversation: ${session.conversationId}`, color)}
|
|
505
|
+
`
|
|
506
|
+
);
|
|
507
|
+
rl.setPrompt(promptFn());
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (slashAction.kind === "set-mode") {
|
|
511
|
+
session.mode = slashAction.mode;
|
|
512
|
+
stdout.write(`${dim(`mode = ${session.mode}`, color)}
|
|
513
|
+
`);
|
|
514
|
+
if (slashAction.warnAgenticTenantMismatch) {
|
|
515
|
+
stdout.write(
|
|
516
|
+
`${yellow(`heads-up: agentic mode is gated to tenant '${AGENTIC_TENANT_LOCK}'; current tenant is '${session.tenant}'.`, color)}
|
|
517
|
+
`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
rl.setPrompt(promptFn());
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (slashAction.kind === "set-tenant") {
|
|
524
|
+
session.tenant = slashAction.tenant;
|
|
525
|
+
stdout.write(`${dim(`tenant = ${session.tenant}`, color)}
|
|
526
|
+
`);
|
|
527
|
+
if (slashAction.warnAgenticTenantMismatch) {
|
|
528
|
+
stdout.write(
|
|
529
|
+
`${yellow(`heads-up: agentic mode is gated to tenant '${AGENTIC_TENANT_LOCK}'; this run will be denied.`, color)}
|
|
530
|
+
`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
rl.setPrompt(promptFn());
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (slashAction.kind === "save") {
|
|
537
|
+
try {
|
|
538
|
+
writeFileSync3(
|
|
539
|
+
slashAction.path,
|
|
540
|
+
JSON.stringify(
|
|
541
|
+
{
|
|
542
|
+
conversation_id: session.conversationId,
|
|
543
|
+
transcript: session.transcript
|
|
544
|
+
},
|
|
545
|
+
null,
|
|
546
|
+
2
|
|
547
|
+
),
|
|
548
|
+
{ encoding: "utf-8" }
|
|
549
|
+
);
|
|
550
|
+
stdout.write(`${dim(`saved transcript to ${slashAction.path}`, color)}
|
|
551
|
+
`);
|
|
552
|
+
} catch (e) {
|
|
553
|
+
stderr.write(
|
|
554
|
+
`${red(`save failed: ${describeError(e)}`, color)}
|
|
555
|
+
`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (slashAction.kind === "invalid") {
|
|
561
|
+
stderr.write(`${red(slashAction.reason, color)}
|
|
562
|
+
`);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
currentAbort = new AbortController();
|
|
566
|
+
try {
|
|
567
|
+
await runOneTurn(line, session, opts.client, {
|
|
568
|
+
color,
|
|
569
|
+
stdout,
|
|
570
|
+
stderr,
|
|
571
|
+
signal: currentAbort.signal
|
|
572
|
+
});
|
|
573
|
+
} catch (e) {
|
|
574
|
+
if (e.name === "AbortError") {
|
|
575
|
+
} else if (e instanceof MiotHarnessApiError) {
|
|
576
|
+
stderr.write(`${red(`error: ${e.message}`, color)}
|
|
577
|
+
`);
|
|
578
|
+
exitCode = 1;
|
|
579
|
+
} else {
|
|
580
|
+
stderr.write(`${red(`unexpected: ${describeError(e)}`, color)}
|
|
581
|
+
`);
|
|
582
|
+
exitCode = 1;
|
|
583
|
+
}
|
|
584
|
+
} finally {
|
|
585
|
+
currentAbort = null;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} finally {
|
|
589
|
+
rl.close();
|
|
590
|
+
}
|
|
591
|
+
return exitCode;
|
|
592
|
+
}
|
|
593
|
+
async function runOneTurn(prompt, session, client, ctx) {
|
|
594
|
+
const req = {
|
|
595
|
+
message: prompt,
|
|
596
|
+
tenant_id: session.tenant,
|
|
597
|
+
user_id: session.user,
|
|
598
|
+
mode: session.mode,
|
|
599
|
+
conversation_id: session.conversationId,
|
|
600
|
+
...session.debug ? { debug: true } : {}
|
|
601
|
+
};
|
|
602
|
+
const { run_id } = await client.runs.create(req, { signal: ctx.signal });
|
|
603
|
+
let state = initialState(ctx.color);
|
|
604
|
+
let terminal = null;
|
|
605
|
+
let failureMessage = "";
|
|
606
|
+
const seenEvents = [];
|
|
607
|
+
for await (const event of client.runs.stream(run_id, { signal: ctx.signal })) {
|
|
608
|
+
seenEvents.push(event);
|
|
609
|
+
const r = renderEvent(state, event);
|
|
610
|
+
state = r.state;
|
|
611
|
+
if (r.output.length > 0) ctx.stdout.write(r.output);
|
|
612
|
+
if (event.type === "run.completed") {
|
|
613
|
+
terminal = "completed";
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
if (event.type === "run.failed") {
|
|
617
|
+
terminal = "failed";
|
|
618
|
+
failureMessage = event.message;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (terminal === "completed") {
|
|
623
|
+
let record = null;
|
|
624
|
+
try {
|
|
625
|
+
record = await client.runs.get(run_id);
|
|
626
|
+
} catch {
|
|
627
|
+
record = null;
|
|
628
|
+
}
|
|
629
|
+
const finalRender = renderAuthoritativeAnswer(state, record?.answer ?? null);
|
|
630
|
+
ctx.stdout.write(finalRender.output);
|
|
631
|
+
session.transcript.push({ prompt, runId: run_id, record });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (terminal === "failed") {
|
|
635
|
+
const failureRender = renderRunFailure(state, failureMessage);
|
|
636
|
+
ctx.stdout.write(failureRender.output);
|
|
637
|
+
session.transcript.push({ prompt, runId: run_id, record: null });
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
throw new Error("stream ended without a terminal event");
|
|
641
|
+
}
|
|
642
|
+
function slashStateOf(s) {
|
|
643
|
+
return { mode: s.mode, tenant: s.tenant };
|
|
644
|
+
}
|
|
645
|
+
function promptFor(s, color) {
|
|
646
|
+
const tag = `${s.conversationId.slice(0, 6)}:${s.mode}`;
|
|
647
|
+
return `${dim(`[${tag}]`, color)} > `;
|
|
648
|
+
}
|
|
649
|
+
function warnIfAgenticMismatch(s, color, out) {
|
|
650
|
+
if (s.mode === "agentic" && s.tenant !== AGENTIC_TENANT_LOCK) {
|
|
651
|
+
out.write(
|
|
652
|
+
`${yellow(`heads-up: agentic mode is gated to tenant '${AGENTIC_TENANT_LOCK}'; current tenant is '${s.tenant}'.`, color)}
|
|
653
|
+
`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function describeError(e) {
|
|
658
|
+
if (e instanceof Error) return e.message;
|
|
659
|
+
return String(e);
|
|
660
|
+
}
|
|
661
|
+
async function* iterateLines(rl, stdout, prompt) {
|
|
662
|
+
stdout.write(prompt());
|
|
663
|
+
for await (const line of rl) {
|
|
664
|
+
yield line;
|
|
665
|
+
stdout.write(prompt());
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/tui/runTui.ts
|
|
670
|
+
import { render } from "ink";
|
|
671
|
+
import { createElement } from "react";
|
|
672
|
+
|
|
673
|
+
// src/tui/App.tsx
|
|
674
|
+
import { Box as Box12 } from "ink";
|
|
675
|
+
import { useCallback as useCallback2, useMemo as useMemo2, useState as useState7 } from "react";
|
|
676
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
677
|
+
|
|
678
|
+
// src/tui/input/Editor.tsx
|
|
679
|
+
import { Box, Text, useInput } from "ink";
|
|
680
|
+
import { useReducer, useState } from "react";
|
|
681
|
+
|
|
682
|
+
// src/tui/input/history.ts
|
|
683
|
+
import {
|
|
684
|
+
chmodSync,
|
|
685
|
+
existsSync as existsSync3,
|
|
686
|
+
readFileSync as readFileSync4,
|
|
687
|
+
writeFileSync as writeFileSync4
|
|
688
|
+
} from "fs";
|
|
689
|
+
var HISTORY_CAP = 200;
|
|
690
|
+
function initialHistory() {
|
|
691
|
+
return { entries: [], cursor: -1 };
|
|
692
|
+
}
|
|
693
|
+
function appendHistory(store, text) {
|
|
694
|
+
if (text.trim().length === 0) return resetCursor(store);
|
|
695
|
+
const lastIdx = store.entries.length - 1;
|
|
696
|
+
if (lastIdx >= 0 && store.entries[lastIdx] === text) {
|
|
697
|
+
return resetCursor(store);
|
|
698
|
+
}
|
|
699
|
+
const next = store.entries.length >= HISTORY_CAP ? [...store.entries.slice(store.entries.length - HISTORY_CAP + 1), text] : [...store.entries, text];
|
|
700
|
+
return { entries: next, cursor: -1 };
|
|
701
|
+
}
|
|
702
|
+
function resetCursor(store) {
|
|
703
|
+
if (store.cursor === -1) return store;
|
|
704
|
+
return { entries: store.entries, cursor: -1 };
|
|
705
|
+
}
|
|
706
|
+
function navUp(store) {
|
|
707
|
+
if (store.entries.length === 0) {
|
|
708
|
+
return { store, text: null };
|
|
709
|
+
}
|
|
710
|
+
const target = store.cursor === -1 ? store.entries.length - 1 : Math.max(0, store.cursor - 1);
|
|
711
|
+
const text = store.entries[target] ?? null;
|
|
712
|
+
return { store: { entries: store.entries, cursor: target }, text };
|
|
713
|
+
}
|
|
714
|
+
function navDown(store) {
|
|
715
|
+
if (store.cursor === -1) {
|
|
716
|
+
return { store, text: null };
|
|
717
|
+
}
|
|
718
|
+
const next = store.cursor + 1;
|
|
719
|
+
if (next >= store.entries.length) {
|
|
720
|
+
return { store: { entries: store.entries, cursor: -1 }, text: "" };
|
|
721
|
+
}
|
|
722
|
+
const text = store.entries[next] ?? null;
|
|
723
|
+
return { store: { entries: store.entries, cursor: next }, text };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/tui/input/keymap.ts
|
|
727
|
+
function mapKey(input, key) {
|
|
728
|
+
if (key.ctrl && input === "c") return { kind: "CANCEL" };
|
|
729
|
+
if (key.return && key.meta) return { kind: "NEWLINE" };
|
|
730
|
+
if (key.return) return { kind: "SUBMIT" };
|
|
731
|
+
if (key.backspace) return { kind: "BACKSPACE" };
|
|
732
|
+
if (key.delete) return { kind: "DELETE_FORWARD" };
|
|
733
|
+
if (key.leftArrow && key.ctrl) return { kind: "MOVE_WORD_LEFT" };
|
|
734
|
+
if (key.rightArrow && key.ctrl) return { kind: "MOVE_WORD_RIGHT" };
|
|
735
|
+
if (key.leftArrow) return { kind: "MOVE_LEFT" };
|
|
736
|
+
if (key.rightArrow) return { kind: "MOVE_RIGHT" };
|
|
737
|
+
if (key.upArrow) return { kind: "MOVE_UP" };
|
|
738
|
+
if (key.downArrow) return { kind: "MOVE_DOWN" };
|
|
739
|
+
if (key.ctrl && input === "a") return { kind: "MOVE_HOME" };
|
|
740
|
+
if (key.ctrl && input === "e") return { kind: "MOVE_END" };
|
|
741
|
+
if (key.ctrl && input === "k") return { kind: "KILL_LINE" };
|
|
742
|
+
if (key.tab) return null;
|
|
743
|
+
if (key.escape) return null;
|
|
744
|
+
if (key.pageUp || key.pageDown) return null;
|
|
745
|
+
if (input.length > 0 && !key.ctrl && !key.meta) {
|
|
746
|
+
return { kind: "INSERT", text: input };
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/tui/input/reducer.ts
|
|
752
|
+
var WORD_RE = /[A-Za-z0-9_]/;
|
|
753
|
+
function initialEditor() {
|
|
754
|
+
return { lines: [""], cursor: { row: 0, col: 0 }, selectionAnchor: null };
|
|
755
|
+
}
|
|
756
|
+
function bufferText(state) {
|
|
757
|
+
return state.lines.join("\n");
|
|
758
|
+
}
|
|
759
|
+
function applyEditor(state, action) {
|
|
760
|
+
switch (action.kind) {
|
|
761
|
+
case "INSERT":
|
|
762
|
+
return insertAtCursor(state, action.text);
|
|
763
|
+
case "BACKSPACE":
|
|
764
|
+
return backspace(state);
|
|
765
|
+
case "DELETE_FORWARD":
|
|
766
|
+
return deleteForward(state);
|
|
767
|
+
case "MOVE_LEFT":
|
|
768
|
+
return moveLeft(state);
|
|
769
|
+
case "MOVE_RIGHT":
|
|
770
|
+
return moveRight(state);
|
|
771
|
+
case "MOVE_UP":
|
|
772
|
+
return moveUp(state);
|
|
773
|
+
case "MOVE_DOWN":
|
|
774
|
+
return moveDown(state);
|
|
775
|
+
case "MOVE_WORD_LEFT":
|
|
776
|
+
return moveWordLeft(state);
|
|
777
|
+
case "MOVE_WORD_RIGHT":
|
|
778
|
+
return moveWordRight(state);
|
|
779
|
+
case "MOVE_HOME":
|
|
780
|
+
return setCursor(state, state.cursor.row, 0);
|
|
781
|
+
case "MOVE_END":
|
|
782
|
+
return setCursor(state, state.cursor.row, lineAt(state, state.cursor.row).length);
|
|
783
|
+
case "MOVE_DOC_HOME":
|
|
784
|
+
return setCursor(state, 0, 0);
|
|
785
|
+
case "MOVE_DOC_END": {
|
|
786
|
+
const lastRow = state.lines.length - 1;
|
|
787
|
+
return setCursor(state, lastRow, lineAt(state, lastRow).length);
|
|
788
|
+
}
|
|
789
|
+
case "NEWLINE":
|
|
790
|
+
return splitLine(state);
|
|
791
|
+
case "KILL_LINE":
|
|
792
|
+
return killLine(state);
|
|
793
|
+
case "PASTE":
|
|
794
|
+
return pasteAtCursor(state, action.text);
|
|
795
|
+
case "CLEAR":
|
|
796
|
+
return initialEditor();
|
|
797
|
+
case "SET_TEXT":
|
|
798
|
+
return setText(action.text);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function lineAt(state, row) {
|
|
802
|
+
return state.lines[row] ?? "";
|
|
803
|
+
}
|
|
804
|
+
function setCursor(state, row, col) {
|
|
805
|
+
return { ...state, cursor: { row, col } };
|
|
806
|
+
}
|
|
807
|
+
function insertAtCursor(state, text) {
|
|
808
|
+
const { row, col } = state.cursor;
|
|
809
|
+
const line = lineAt(state, row);
|
|
810
|
+
const next = line.slice(0, col) + text + line.slice(col);
|
|
811
|
+
const lines = state.lines.slice();
|
|
812
|
+
lines[row] = next;
|
|
813
|
+
return { ...state, lines, cursor: { row, col: col + text.length } };
|
|
814
|
+
}
|
|
815
|
+
function backspace(state) {
|
|
816
|
+
const { row, col } = state.cursor;
|
|
817
|
+
if (col > 0) {
|
|
818
|
+
const line = lineAt(state, row);
|
|
819
|
+
const next = line.slice(0, col - 1) + line.slice(col);
|
|
820
|
+
const lines2 = state.lines.slice();
|
|
821
|
+
lines2[row] = next;
|
|
822
|
+
return { ...state, lines: lines2, cursor: { row, col: col - 1 } };
|
|
823
|
+
}
|
|
824
|
+
if (row === 0) return state;
|
|
825
|
+
const prev = lineAt(state, row - 1);
|
|
826
|
+
const curr = lineAt(state, row);
|
|
827
|
+
const merged = prev + curr;
|
|
828
|
+
const lines = state.lines.slice();
|
|
829
|
+
lines.splice(row - 1, 2, merged);
|
|
830
|
+
return { ...state, lines, cursor: { row: row - 1, col: prev.length } };
|
|
831
|
+
}
|
|
832
|
+
function deleteForward(state) {
|
|
833
|
+
const { row, col } = state.cursor;
|
|
834
|
+
const line = lineAt(state, row);
|
|
835
|
+
if (col < line.length) {
|
|
836
|
+
const next = line.slice(0, col) + line.slice(col + 1);
|
|
837
|
+
const lines2 = state.lines.slice();
|
|
838
|
+
lines2[row] = next;
|
|
839
|
+
return { ...state, lines: lines2 };
|
|
840
|
+
}
|
|
841
|
+
if (row >= state.lines.length - 1) return state;
|
|
842
|
+
const merged = line + lineAt(state, row + 1);
|
|
843
|
+
const lines = state.lines.slice();
|
|
844
|
+
lines.splice(row, 2, merged);
|
|
845
|
+
return { ...state, lines };
|
|
846
|
+
}
|
|
847
|
+
function moveLeft(state) {
|
|
848
|
+
const { row, col } = state.cursor;
|
|
849
|
+
if (col > 0) return setCursor(state, row, col - 1);
|
|
850
|
+
if (row === 0) return state;
|
|
851
|
+
return setCursor(state, row - 1, lineAt(state, row - 1).length);
|
|
852
|
+
}
|
|
853
|
+
function moveRight(state) {
|
|
854
|
+
const { row, col } = state.cursor;
|
|
855
|
+
const line = lineAt(state, row);
|
|
856
|
+
if (col < line.length) return setCursor(state, row, col + 1);
|
|
857
|
+
if (row >= state.lines.length - 1) return state;
|
|
858
|
+
return setCursor(state, row + 1, 0);
|
|
859
|
+
}
|
|
860
|
+
function moveUp(state) {
|
|
861
|
+
const { row, col } = state.cursor;
|
|
862
|
+
if (row === 0) return setCursor(state, 0, 0);
|
|
863
|
+
const target = lineAt(state, row - 1);
|
|
864
|
+
return setCursor(state, row - 1, Math.min(col, target.length));
|
|
865
|
+
}
|
|
866
|
+
function moveDown(state) {
|
|
867
|
+
const { row, col } = state.cursor;
|
|
868
|
+
if (row >= state.lines.length - 1) {
|
|
869
|
+
return setCursor(state, row, lineAt(state, row).length);
|
|
870
|
+
}
|
|
871
|
+
const target = lineAt(state, row + 1);
|
|
872
|
+
return setCursor(state, row + 1, Math.min(col, target.length));
|
|
873
|
+
}
|
|
874
|
+
function isWordChar(ch) {
|
|
875
|
+
return WORD_RE.test(ch);
|
|
876
|
+
}
|
|
877
|
+
function moveWordLeft(state) {
|
|
878
|
+
let { row, col } = state.cursor;
|
|
879
|
+
if (col === 0) {
|
|
880
|
+
if (row === 0) return state;
|
|
881
|
+
row -= 1;
|
|
882
|
+
col = lineAt(state, row).length;
|
|
883
|
+
}
|
|
884
|
+
const line = lineAt(state, row);
|
|
885
|
+
let i = col;
|
|
886
|
+
while (i > 0 && !isWordChar(line.charAt(i - 1))) i -= 1;
|
|
887
|
+
while (i > 0 && isWordChar(line.charAt(i - 1))) i -= 1;
|
|
888
|
+
return setCursor(state, row, i);
|
|
889
|
+
}
|
|
890
|
+
function moveWordRight(state) {
|
|
891
|
+
const { row, col } = state.cursor;
|
|
892
|
+
const line = lineAt(state, row);
|
|
893
|
+
if (col >= line.length) {
|
|
894
|
+
if (row >= state.lines.length - 1) return state;
|
|
895
|
+
return setCursor(state, row + 1, 0);
|
|
896
|
+
}
|
|
897
|
+
let i = col;
|
|
898
|
+
while (i < line.length && isWordChar(line.charAt(i))) i += 1;
|
|
899
|
+
while (i < line.length && !isWordChar(line.charAt(i))) i += 1;
|
|
900
|
+
return setCursor(state, row, i);
|
|
901
|
+
}
|
|
902
|
+
function splitLine(state) {
|
|
903
|
+
const { row, col } = state.cursor;
|
|
904
|
+
const line = lineAt(state, row);
|
|
905
|
+
const before = line.slice(0, col);
|
|
906
|
+
const after = line.slice(col);
|
|
907
|
+
const lines = state.lines.slice();
|
|
908
|
+
lines.splice(row, 1, before, after);
|
|
909
|
+
return { ...state, lines, cursor: { row: row + 1, col: 0 } };
|
|
910
|
+
}
|
|
911
|
+
function killLine(state) {
|
|
912
|
+
const { row, col } = state.cursor;
|
|
913
|
+
const line = lineAt(state, row);
|
|
914
|
+
if (col < line.length) {
|
|
915
|
+
const lines2 = state.lines.slice();
|
|
916
|
+
lines2[row] = line.slice(0, col);
|
|
917
|
+
return { ...state, lines: lines2 };
|
|
918
|
+
}
|
|
919
|
+
if (row >= state.lines.length - 1) return state;
|
|
920
|
+
const merged = line + lineAt(state, row + 1);
|
|
921
|
+
const lines = state.lines.slice();
|
|
922
|
+
lines.splice(row, 2, merged);
|
|
923
|
+
return { ...state, lines };
|
|
924
|
+
}
|
|
925
|
+
function pasteAtCursor(state, text) {
|
|
926
|
+
if (!text.includes("\n")) return insertAtCursor(state, text);
|
|
927
|
+
const segments = text.split("\n");
|
|
928
|
+
const { row, col } = state.cursor;
|
|
929
|
+
const line = lineAt(state, row);
|
|
930
|
+
const before = line.slice(0, col);
|
|
931
|
+
const after = line.slice(col);
|
|
932
|
+
const head = segments[0] ?? "";
|
|
933
|
+
const tail = segments[segments.length - 1] ?? "";
|
|
934
|
+
const middle = segments.slice(1, -1);
|
|
935
|
+
const lines = state.lines.slice();
|
|
936
|
+
const newLines = [before + head, ...middle, tail + after];
|
|
937
|
+
lines.splice(row, 1, ...newLines);
|
|
938
|
+
const newRow = row + newLines.length - 1;
|
|
939
|
+
const newCol = tail.length;
|
|
940
|
+
return { ...state, lines, cursor: { row: newRow, col: newCol } };
|
|
941
|
+
}
|
|
942
|
+
function setText(text) {
|
|
943
|
+
if (text.length === 0) return initialEditor();
|
|
944
|
+
const lines = text.split("\n");
|
|
945
|
+
const lastRow = lines.length - 1;
|
|
946
|
+
const lastLine = lines[lastRow] ?? "";
|
|
947
|
+
return {
|
|
948
|
+
lines,
|
|
949
|
+
cursor: { row: lastRow, col: lastLine.length },
|
|
950
|
+
selectionAnchor: null
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/tui/input/Editor.tsx
|
|
955
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
956
|
+
function Editor(props) {
|
|
957
|
+
const promptPrefix = props.prompt ?? "\u203A ";
|
|
958
|
+
const [editor, dispatch] = useReducer(
|
|
959
|
+
(state, action) => {
|
|
960
|
+
if (action.kind === "SUBMIT" || action.kind === "CANCEL") {
|
|
961
|
+
return state;
|
|
962
|
+
}
|
|
963
|
+
return applyEditor(state, action);
|
|
964
|
+
},
|
|
965
|
+
initialEditor()
|
|
966
|
+
);
|
|
967
|
+
const [history, setHistory] = useState(
|
|
968
|
+
props.initialHistory ?? initialHistory()
|
|
969
|
+
);
|
|
970
|
+
function handle(action) {
|
|
971
|
+
if (action.kind === "CANCEL") {
|
|
972
|
+
if (props.onCancel) props.onCancel();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (action.kind === "SUBMIT") {
|
|
976
|
+
const text = bufferText(editor);
|
|
977
|
+
if (text.trim().length === 0) return;
|
|
978
|
+
setHistory(appendHistory(history, text));
|
|
979
|
+
dispatch({ kind: "CLEAR" });
|
|
980
|
+
props.onSubmit(text);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (action.kind === "MOVE_UP" && editor.cursor.row === 0) {
|
|
984
|
+
const r = navUp(history);
|
|
985
|
+
if (r.text !== null) {
|
|
986
|
+
setHistory(r.store);
|
|
987
|
+
dispatch({ kind: "SET_TEXT", text: r.text });
|
|
988
|
+
}
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (action.kind === "MOVE_DOWN" && editor.cursor.row === editor.lines.length - 1) {
|
|
992
|
+
const r = navDown(history);
|
|
993
|
+
if (r.text !== null) {
|
|
994
|
+
setHistory(r.store);
|
|
995
|
+
dispatch({ kind: "SET_TEXT", text: r.text });
|
|
996
|
+
}
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
dispatch(action);
|
|
1000
|
+
}
|
|
1001
|
+
useInput(
|
|
1002
|
+
(input, key) => {
|
|
1003
|
+
const action = mapKey(input, key);
|
|
1004
|
+
if (action) handle(action);
|
|
1005
|
+
},
|
|
1006
|
+
{ isActive: props.isFocused ?? true }
|
|
1007
|
+
);
|
|
1008
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: editor.lines.map((line, row) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
1009
|
+
row === 0 ? promptPrefix : " ",
|
|
1010
|
+
renderLine(line, row === editor.cursor.row ? editor.cursor.col : null)
|
|
1011
|
+
] }, row)) });
|
|
1012
|
+
}
|
|
1013
|
+
function renderLine(line, cursorCol) {
|
|
1014
|
+
if (cursorCol === null) return line.length > 0 ? line : " ";
|
|
1015
|
+
if (cursorCol >= line.length) {
|
|
1016
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1017
|
+
line,
|
|
1018
|
+
/* @__PURE__ */ jsx(Text, { inverse: true, children: " " })
|
|
1019
|
+
] });
|
|
1020
|
+
}
|
|
1021
|
+
const before = line.slice(0, cursorCol);
|
|
1022
|
+
const at = line.charAt(cursorCol);
|
|
1023
|
+
const after = line.slice(cursorCol + 1);
|
|
1024
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1025
|
+
before,
|
|
1026
|
+
/* @__PURE__ */ jsx(Text, { inverse: true, children: at }),
|
|
1027
|
+
after
|
|
1028
|
+
] });
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/tui/header/Header.tsx
|
|
1032
|
+
import { Box as Box2, Text as Text3 } from "ink";
|
|
1033
|
+
|
|
1034
|
+
// src/tui/session/agentic.ts
|
|
1035
|
+
var AGENTIC_TENANT_LOCK2 = "mintral";
|
|
1036
|
+
function isAgenticTenantMismatch(mode, tenant) {
|
|
1037
|
+
return mode === "agentic" && tenant !== AGENTIC_TENANT_LOCK2;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/tui/transcript/Spinner.tsx
|
|
1041
|
+
import { Text as Text2 } from "ink";
|
|
1042
|
+
import { useEffect, useState as useState2 } from "react";
|
|
1043
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1044
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1045
|
+
var MIN_INTERVAL_MS = 50;
|
|
1046
|
+
function Spinner(props) {
|
|
1047
|
+
const interval = Math.max(MIN_INTERVAL_MS, props.intervalMs ?? 125);
|
|
1048
|
+
const [frame, setFrame] = useState2(0);
|
|
1049
|
+
useEffect(() => {
|
|
1050
|
+
const id = setInterval(() => {
|
|
1051
|
+
setFrame((f) => (f + 1) % FRAMES.length);
|
|
1052
|
+
}, interval);
|
|
1053
|
+
return () => {
|
|
1054
|
+
clearInterval(id);
|
|
1055
|
+
};
|
|
1056
|
+
}, [interval]);
|
|
1057
|
+
return /* @__PURE__ */ jsx2(Text2, { color: props.color, children: FRAMES[frame] });
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/tui/header/Header.tsx
|
|
1061
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1062
|
+
var SEPARATOR = " \xB7 ";
|
|
1063
|
+
function Header(props) {
|
|
1064
|
+
const { meta, streaming, pendingApprovals } = props;
|
|
1065
|
+
const shortConv = meta.conversationId.slice(0, 8);
|
|
1066
|
+
const agenticWarn = isAgenticTenantMismatch(meta.mode, meta.tenantId);
|
|
1067
|
+
const chips = [];
|
|
1068
|
+
chips.push(/* @__PURE__ */ jsxs2(Text3, { children: [
|
|
1069
|
+
"tenant=",
|
|
1070
|
+
meta.tenantId
|
|
1071
|
+
] }, "tenant"));
|
|
1072
|
+
chips.push(/* @__PURE__ */ jsxs2(Text3, { children: [
|
|
1073
|
+
"user=",
|
|
1074
|
+
meta.userId
|
|
1075
|
+
] }, "user"));
|
|
1076
|
+
chips.push(/* @__PURE__ */ jsxs2(Text3, { children: [
|
|
1077
|
+
"conv=",
|
|
1078
|
+
shortConv
|
|
1079
|
+
] }, "conv"));
|
|
1080
|
+
chips.push(
|
|
1081
|
+
/* @__PURE__ */ jsxs2(Text3, { color: agenticWarn ? "yellow" : void 0, children: [
|
|
1082
|
+
agenticWarn ? "\u26A0 " : "",
|
|
1083
|
+
"mode=",
|
|
1084
|
+
meta.mode
|
|
1085
|
+
] }, "mode")
|
|
1086
|
+
);
|
|
1087
|
+
chips.push(/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: meta.baseUrl }, "url"));
|
|
1088
|
+
if (meta.profileName) {
|
|
1089
|
+
chips.push(/* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
|
|
1090
|
+
"profile=",
|
|
1091
|
+
meta.profileName
|
|
1092
|
+
] }, "profile"));
|
|
1093
|
+
}
|
|
1094
|
+
if (pendingApprovals > 0) {
|
|
1095
|
+
chips.push(
|
|
1096
|
+
/* @__PURE__ */ jsxs2(Text3, { color: "yellow", children: [
|
|
1097
|
+
"approvals=",
|
|
1098
|
+
pendingApprovals
|
|
1099
|
+
] }, "appr")
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
if (typeof props.turns === "number") {
|
|
1103
|
+
chips.push(/* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
|
|
1104
|
+
"turns=",
|
|
1105
|
+
props.turns
|
|
1106
|
+
] }, "turns"));
|
|
1107
|
+
}
|
|
1108
|
+
if (typeof props.approxTokens === "number") {
|
|
1109
|
+
const pct = typeof props.contextPercent === "number" ? ` (${props.contextPercent}%)` : "";
|
|
1110
|
+
chips.push(
|
|
1111
|
+
/* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
|
|
1112
|
+
"ctx\u2248",
|
|
1113
|
+
props.approxTokens,
|
|
1114
|
+
"tok",
|
|
1115
|
+
pct
|
|
1116
|
+
] }, "tok")
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
if (props.usageTotals && (props.usageTotals.inputTokens > 0 || props.usageTotals.outputTokens > 0)) {
|
|
1120
|
+
const u = props.usageTotals;
|
|
1121
|
+
const cost = u.costUsd > 0 ? ` $${u.costUsd.toFixed(4)}` : "";
|
|
1122
|
+
chips.push(
|
|
1123
|
+
/* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
|
|
1124
|
+
"usage=",
|
|
1125
|
+
u.inputTokens,
|
|
1126
|
+
"\u2192",
|
|
1127
|
+
u.outputTokens,
|
|
1128
|
+
cost
|
|
1129
|
+
] }, "usage")
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
if (streaming) {
|
|
1133
|
+
chips.push(/* @__PURE__ */ jsx3(Spinner, { color: "cyan" }, "spinner"));
|
|
1134
|
+
}
|
|
1135
|
+
return /* @__PURE__ */ jsx3(Box2, { borderStyle: "round", paddingX: 1, flexDirection: "row", flexWrap: "wrap", children: interpose(chips, (i) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: SEPARATOR }, `sep-${i}`)) });
|
|
1136
|
+
}
|
|
1137
|
+
function interpose(nodes, sep) {
|
|
1138
|
+
const out = [];
|
|
1139
|
+
nodes.forEach((node, i) => {
|
|
1140
|
+
if (i > 0) out.push(sep(i));
|
|
1141
|
+
out.push(node);
|
|
1142
|
+
});
|
|
1143
|
+
return out;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/tui/transcript/Transcript.tsx
|
|
1147
|
+
import { Box as Box6, Text as Text7 } from "ink";
|
|
1148
|
+
|
|
1149
|
+
// src/tui/transcript/AssistantTurn.tsx
|
|
1150
|
+
import { Box as Box3, Text as Text4 } from "ink";
|
|
1151
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1152
|
+
function AssistantTurn(props) {
|
|
1153
|
+
const { text, status } = props.item;
|
|
1154
|
+
const color = status === "failed" ? "red" : status === "complete" ? "green" : "white";
|
|
1155
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "row", marginTop: 1, children: [
|
|
1156
|
+
status === "streaming" ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1157
|
+
/* @__PURE__ */ jsx4(Spinner, { color: "cyan" }),
|
|
1158
|
+
/* @__PURE__ */ jsxs3(Text4, { color, bold: true, children: [
|
|
1159
|
+
" ",
|
|
1160
|
+
"miot",
|
|
1161
|
+
" "
|
|
1162
|
+
] })
|
|
1163
|
+
] }) : /* @__PURE__ */ jsxs3(Text4, { color, bold: true, children: [
|
|
1164
|
+
status === "failed" ? "\u2717 " : "\u2713 ",
|
|
1165
|
+
"miot",
|
|
1166
|
+
" "
|
|
1167
|
+
] }),
|
|
1168
|
+
/* @__PURE__ */ jsx4(Text4, { color, children: text })
|
|
1169
|
+
] });
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/tui/transcript/ToolCall.tsx
|
|
1173
|
+
import { Box as Box4, Text as Text5 } from "ink";
|
|
1174
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1175
|
+
function ToolCall(props) {
|
|
1176
|
+
const { name, status, message } = props.item;
|
|
1177
|
+
const color = status === "failed" ? "red" : status === "ok" ? "green" : "yellow";
|
|
1178
|
+
const glyph = status === "running" ? null : status === "ok" ? "\u2713" : "\u2717";
|
|
1179
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", children: [
|
|
1180
|
+
/* @__PURE__ */ jsx5(Text5, { color, children: glyph !== null ? `${glyph} ` : "" }),
|
|
1181
|
+
status === "running" ? /* @__PURE__ */ jsx5(Spinner, { color }) : null,
|
|
1182
|
+
/* @__PURE__ */ jsxs4(Text5, { color, children: [
|
|
1183
|
+
" ",
|
|
1184
|
+
"tool: ",
|
|
1185
|
+
name,
|
|
1186
|
+
message && status !== "running" ? ` \u2014 ${message}` : ""
|
|
1187
|
+
] })
|
|
1188
|
+
] });
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/tui/transcript/UserTurn.tsx
|
|
1192
|
+
import { Box as Box5, Text as Text6 } from "ink";
|
|
1193
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1194
|
+
function UserTurn(props) {
|
|
1195
|
+
const { text } = props.item;
|
|
1196
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", marginTop: 1, children: [
|
|
1197
|
+
/* @__PURE__ */ jsxs5(Text6, { color: "cyan", bold: true, children: [
|
|
1198
|
+
"you",
|
|
1199
|
+
" "
|
|
1200
|
+
] }),
|
|
1201
|
+
/* @__PURE__ */ jsx6(Text6, { children: text })
|
|
1202
|
+
] });
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/tui/transcript/Transcript.tsx
|
|
1206
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1207
|
+
function Transcript(props) {
|
|
1208
|
+
const activeChainIdx = props.isStreaming ? findActiveChainIndex(props.items) : -1;
|
|
1209
|
+
return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", children: props.items.map((item, i) => /* @__PURE__ */ jsx7(
|
|
1210
|
+
TranscriptItemView,
|
|
1211
|
+
{
|
|
1212
|
+
item,
|
|
1213
|
+
isActive: i === activeChainIdx
|
|
1214
|
+
},
|
|
1215
|
+
item.id
|
|
1216
|
+
)) });
|
|
1217
|
+
}
|
|
1218
|
+
function findActiveChainIndex(items) {
|
|
1219
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
1220
|
+
const item = items[i];
|
|
1221
|
+
if (!item) continue;
|
|
1222
|
+
if (item.kind === "route" || item.kind === "plan" || item.kind === "agent" || item.kind === "artifact" || item.kind === "freshness") {
|
|
1223
|
+
return i;
|
|
1224
|
+
}
|
|
1225
|
+
if (item.kind === "user") return -1;
|
|
1226
|
+
if (item.kind === "assistant") return -1;
|
|
1227
|
+
}
|
|
1228
|
+
return -1;
|
|
1229
|
+
}
|
|
1230
|
+
function TranscriptItemView(props) {
|
|
1231
|
+
const { item, isActive } = props;
|
|
1232
|
+
switch (item.kind) {
|
|
1233
|
+
case "user":
|
|
1234
|
+
return /* @__PURE__ */ jsx7(UserTurn, { item });
|
|
1235
|
+
case "assistant":
|
|
1236
|
+
return /* @__PURE__ */ jsx7(AssistantTurn, { item });
|
|
1237
|
+
case "tool":
|
|
1238
|
+
return /* @__PURE__ */ jsx7(ToolCall, { item });
|
|
1239
|
+
case "route":
|
|
1240
|
+
return /* @__PURE__ */ jsx7(ChainRow, { prefix: "route:", isActive, children: item.route });
|
|
1241
|
+
case "agent":
|
|
1242
|
+
return /* @__PURE__ */ jsx7(ChainRow, { prefix: "agent:", isActive, children: item.agent });
|
|
1243
|
+
case "thinking":
|
|
1244
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "row", children: [
|
|
1245
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \u22EE " }),
|
|
1246
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: item.text })
|
|
1247
|
+
] });
|
|
1248
|
+
case "plan":
|
|
1249
|
+
return /* @__PURE__ */ jsx7(ChainRow, { prefix: "plan:", isActive, children: item.message });
|
|
1250
|
+
case "freshness":
|
|
1251
|
+
return /* @__PURE__ */ jsxs6(Text7, { color: "yellow", children: [
|
|
1252
|
+
"\u26A0 ",
|
|
1253
|
+
item.message
|
|
1254
|
+
] });
|
|
1255
|
+
case "artifact":
|
|
1256
|
+
return /* @__PURE__ */ jsx7(ChainRow, { prefix: "artifact:", isActive, children: item.artifactKind });
|
|
1257
|
+
case "system":
|
|
1258
|
+
return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: item.text });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
function ChainRow(props) {
|
|
1262
|
+
if (props.isActive) {
|
|
1263
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "row", children: [
|
|
1264
|
+
/* @__PURE__ */ jsx7(Spinner, { color: "cyan" }),
|
|
1265
|
+
/* @__PURE__ */ jsxs6(Text7, { color: "cyan", bold: true, children: [
|
|
1266
|
+
" ",
|
|
1267
|
+
props.prefix,
|
|
1268
|
+
" ",
|
|
1269
|
+
props.children
|
|
1270
|
+
] })
|
|
1271
|
+
] });
|
|
1272
|
+
}
|
|
1273
|
+
return /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
|
|
1274
|
+
"\xB7 ",
|
|
1275
|
+
props.prefix,
|
|
1276
|
+
" ",
|
|
1277
|
+
props.children
|
|
1278
|
+
] });
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/tui/modals/ContextModal.tsx
|
|
1282
|
+
import { Box as Box7, Text as Text8, useInput as useInput2 } from "ink";
|
|
1283
|
+
|
|
1284
|
+
// src/tui/session/selectors.ts
|
|
1285
|
+
function isStreaming(state) {
|
|
1286
|
+
return state.currentRunId !== null;
|
|
1287
|
+
}
|
|
1288
|
+
function pendingApprovalCount(state) {
|
|
1289
|
+
return state.pendingApprovals.length;
|
|
1290
|
+
}
|
|
1291
|
+
function turnCount(state) {
|
|
1292
|
+
return state.transcript.filter((i) => i.kind === "user").length;
|
|
1293
|
+
}
|
|
1294
|
+
function approxTokenCount(state) {
|
|
1295
|
+
let chars = 0;
|
|
1296
|
+
for (const item of state.transcript) {
|
|
1297
|
+
switch (item.kind) {
|
|
1298
|
+
case "user":
|
|
1299
|
+
case "system":
|
|
1300
|
+
chars += item.text.length;
|
|
1301
|
+
break;
|
|
1302
|
+
case "assistant":
|
|
1303
|
+
chars += item.text.length;
|
|
1304
|
+
break;
|
|
1305
|
+
case "tool":
|
|
1306
|
+
chars += item.name.length + (item.message?.length ?? 0);
|
|
1307
|
+
break;
|
|
1308
|
+
case "route":
|
|
1309
|
+
chars += item.route.length;
|
|
1310
|
+
break;
|
|
1311
|
+
case "agent":
|
|
1312
|
+
chars += item.agent.length;
|
|
1313
|
+
break;
|
|
1314
|
+
case "plan":
|
|
1315
|
+
case "freshness":
|
|
1316
|
+
chars += item.message.length;
|
|
1317
|
+
break;
|
|
1318
|
+
case "artifact":
|
|
1319
|
+
chars += item.artifactKind.length;
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return Math.ceil(chars / 4);
|
|
1324
|
+
}
|
|
1325
|
+
var ASSUMED_CONTEXT_WINDOW = 2e5;
|
|
1326
|
+
function contextPercent(state) {
|
|
1327
|
+
return Math.min(
|
|
1328
|
+
100,
|
|
1329
|
+
Math.round(approxTokenCount(state) / ASSUMED_CONTEXT_WINDOW * 100)
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// src/tui/modals/ContextModal.tsx
|
|
1334
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1335
|
+
function ContextModal(props) {
|
|
1336
|
+
const { session, lastRunId } = props;
|
|
1337
|
+
useInput2(
|
|
1338
|
+
(_input, key) => {
|
|
1339
|
+
if (key.escape || key.return) props.onClose();
|
|
1340
|
+
},
|
|
1341
|
+
{ isActive: props.isFocused ?? true }
|
|
1342
|
+
);
|
|
1343
|
+
const fields = [
|
|
1344
|
+
["tenant", session.meta.tenantId],
|
|
1345
|
+
["user", session.meta.userId],
|
|
1346
|
+
["conv", session.meta.conversationId],
|
|
1347
|
+
["mode", session.meta.mode],
|
|
1348
|
+
["baseUrl", session.meta.baseUrl]
|
|
1349
|
+
];
|
|
1350
|
+
if (session.meta.profileName) {
|
|
1351
|
+
fields.push(["profile", session.meta.profileName]);
|
|
1352
|
+
}
|
|
1353
|
+
fields.push(["turns", String(turnCount(session))]);
|
|
1354
|
+
fields.push(["last run", lastRunId ?? "(none)"]);
|
|
1355
|
+
fields.push([
|
|
1356
|
+
"pending approvals",
|
|
1357
|
+
String(session.pendingApprovals.length)
|
|
1358
|
+
]);
|
|
1359
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
1360
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "session context" }),
|
|
1361
|
+
fields.map(([key, value]) => /* @__PURE__ */ jsxs7(Text8, { children: [
|
|
1362
|
+
key.padEnd(18),
|
|
1363
|
+
" ",
|
|
1364
|
+
value
|
|
1365
|
+
] }, key)),
|
|
1366
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "esc/enter to close" })
|
|
1367
|
+
] });
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// src/tui/modals/ResumePicker.tsx
|
|
1371
|
+
import { Box as Box8, Text as Text9, useInput as useInput3 } from "ink";
|
|
1372
|
+
import { useState as useState3 } from "react";
|
|
1373
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1374
|
+
function ResumePicker(props) {
|
|
1375
|
+
const maxRows = props.maxRows ?? 10;
|
|
1376
|
+
const [index, setIndex] = useState3(0);
|
|
1377
|
+
const summaries = props.summaries;
|
|
1378
|
+
const cap = Math.max(0, summaries.length - 1);
|
|
1379
|
+
useInput3(
|
|
1380
|
+
(_input, key) => {
|
|
1381
|
+
if (key.escape) {
|
|
1382
|
+
props.onCancel();
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (key.upArrow) {
|
|
1386
|
+
setIndex((i) => Math.max(0, i - 1));
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (key.downArrow) {
|
|
1390
|
+
setIndex((i) => Math.min(cap, i + 1));
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (key.return) {
|
|
1394
|
+
const chosen = summaries[Math.min(index, cap)];
|
|
1395
|
+
if (chosen) props.onSelect(chosen.id);
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
{ isActive: props.isFocused ?? true }
|
|
1399
|
+
);
|
|
1400
|
+
if (summaries.length === 0) {
|
|
1401
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
1402
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: "resume session" }),
|
|
1403
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "(no saved sessions)" }),
|
|
1404
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "esc to close" })
|
|
1405
|
+
] });
|
|
1406
|
+
}
|
|
1407
|
+
const visible = summaries.slice(0, maxRows);
|
|
1408
|
+
const truncated = summaries.length - visible.length;
|
|
1409
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
1410
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: "resume session" }),
|
|
1411
|
+
visible.map((s, i) => /* @__PURE__ */ jsx9(Text9, { inverse: i === index, children: summarize(s) }, s.id)),
|
|
1412
|
+
truncated > 0 ? /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
|
|
1413
|
+
"\u2026 ",
|
|
1414
|
+
truncated,
|
|
1415
|
+
" more"
|
|
1416
|
+
] }) : null,
|
|
1417
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "\u2191\u2193 navigate \xB7 enter select \xB7 esc cancel" })
|
|
1418
|
+
] });
|
|
1419
|
+
}
|
|
1420
|
+
function summarize(s) {
|
|
1421
|
+
const idShort = s.id.slice(0, 8);
|
|
1422
|
+
const turns = `${s.lastTurn} turn${s.lastTurn === 1 ? "" : "s"}`;
|
|
1423
|
+
const date = new Date(s.mtime).toISOString().slice(0, 16).replace("T", " ");
|
|
1424
|
+
const prompt = s.lastPrompt ? truncate(s.lastPrompt, 40) : "(empty)";
|
|
1425
|
+
return `${date} ${idShort} ${turns.padEnd(8)} ${prompt}`;
|
|
1426
|
+
}
|
|
1427
|
+
function truncate(text, max) {
|
|
1428
|
+
if (text.length <= max) return text;
|
|
1429
|
+
return `${text.slice(0, max - 1)}\u2026`;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/tui/modals/ThemePicker.tsx
|
|
1433
|
+
import { Box as Box9, Text as Text10, useInput as useInput4 } from "ink";
|
|
1434
|
+
import { useState as useState4 } from "react";
|
|
1435
|
+
|
|
1436
|
+
// src/tui/theme/themes.ts
|
|
1437
|
+
var DARK_THEME = {
|
|
1438
|
+
accent: "cyan",
|
|
1439
|
+
assistant: "white",
|
|
1440
|
+
user: "cyan",
|
|
1441
|
+
dim: "gray",
|
|
1442
|
+
warn: "yellow",
|
|
1443
|
+
err: "red",
|
|
1444
|
+
ok: "green",
|
|
1445
|
+
border: "gray",
|
|
1446
|
+
prompt: "cyan",
|
|
1447
|
+
spinner: "cyan"
|
|
1448
|
+
};
|
|
1449
|
+
var LIGHT_THEME = {
|
|
1450
|
+
accent: "blue",
|
|
1451
|
+
assistant: "black",
|
|
1452
|
+
user: "blue",
|
|
1453
|
+
dim: "gray",
|
|
1454
|
+
warn: "yellow",
|
|
1455
|
+
err: "red",
|
|
1456
|
+
ok: "green",
|
|
1457
|
+
border: "gray",
|
|
1458
|
+
prompt: "blue",
|
|
1459
|
+
spinner: "blue"
|
|
1460
|
+
};
|
|
1461
|
+
var HIGH_CONTRAST_THEME = {
|
|
1462
|
+
accent: "white",
|
|
1463
|
+
assistant: "white",
|
|
1464
|
+
user: "white",
|
|
1465
|
+
dim: "white",
|
|
1466
|
+
warn: "yellow",
|
|
1467
|
+
err: "red",
|
|
1468
|
+
ok: "green",
|
|
1469
|
+
border: "white",
|
|
1470
|
+
prompt: "white",
|
|
1471
|
+
spinner: "white"
|
|
1472
|
+
};
|
|
1473
|
+
var BUILTIN_THEMES = {
|
|
1474
|
+
dark: DARK_THEME,
|
|
1475
|
+
light: LIGHT_THEME,
|
|
1476
|
+
"high-contrast": HIGH_CONTRAST_THEME
|
|
1477
|
+
};
|
|
1478
|
+
var DEFAULT_THEME_NAME = "dark";
|
|
1479
|
+
function builtinThemeNames() {
|
|
1480
|
+
return Object.keys(BUILTIN_THEMES).sort();
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/tui/modals/ThemePicker.tsx
|
|
1484
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1485
|
+
function ThemePicker(props) {
|
|
1486
|
+
const names = builtinThemeNames();
|
|
1487
|
+
const initial = props.initialName ? Math.max(0, names.indexOf(props.initialName)) : 0;
|
|
1488
|
+
const [index, setIndex] = useState4(initial);
|
|
1489
|
+
const cap = names.length - 1;
|
|
1490
|
+
useInput4(
|
|
1491
|
+
(_input, key) => {
|
|
1492
|
+
if (key.escape) {
|
|
1493
|
+
props.onCancel();
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (key.upArrow) {
|
|
1497
|
+
setIndex((i) => Math.max(0, i - 1));
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (key.downArrow) {
|
|
1501
|
+
setIndex((i) => Math.min(cap, i + 1));
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (key.return) {
|
|
1505
|
+
const name = names[Math.min(index, cap)];
|
|
1506
|
+
if (name) props.onSelect(name);
|
|
1507
|
+
}
|
|
1508
|
+
},
|
|
1509
|
+
{ isActive: props.isFocused ?? true }
|
|
1510
|
+
);
|
|
1511
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
1512
|
+
/* @__PURE__ */ jsx10(Text10, { bold: true, children: "theme" }),
|
|
1513
|
+
names.map((name, i) => {
|
|
1514
|
+
const t = BUILTIN_THEMES[name];
|
|
1515
|
+
const sample = t ? `accent=${t.accent} user=${t.user}` : "";
|
|
1516
|
+
return /* @__PURE__ */ jsxs9(Text10, { inverse: i === index, children: [
|
|
1517
|
+
name.padEnd(15),
|
|
1518
|
+
" ",
|
|
1519
|
+
sample
|
|
1520
|
+
] }, name);
|
|
1521
|
+
}),
|
|
1522
|
+
/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2191\u2193 navigate \xB7 enter apply \xB7 esc cancel" })
|
|
1523
|
+
] });
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// src/tui/modals/ApprovalModal.tsx
|
|
1527
|
+
import { Box as Box10, Text as Text11, useInput as useInput5 } from "ink";
|
|
1528
|
+
|
|
1529
|
+
// src/tui/session/approvals.ts
|
|
1530
|
+
var APPROVALS_UI_ENV = "MIOT_CHAT_APPROVALS_UI";
|
|
1531
|
+
function isApprovalsUiEnabled(env = process.env) {
|
|
1532
|
+
const raw = env[APPROVALS_UI_ENV];
|
|
1533
|
+
if (raw === void 0) return false;
|
|
1534
|
+
return raw === "1" || raw.toLowerCase() === "true";
|
|
1535
|
+
}
|
|
1536
|
+
var APPROVAL_REPLY_PLACEHOLDER = "approval reply not yet supported by harness";
|
|
1537
|
+
|
|
1538
|
+
// src/tui/modals/ApprovalModal.tsx
|
|
1539
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1540
|
+
function ApprovalModal(props) {
|
|
1541
|
+
const { approval, onResolve } = props;
|
|
1542
|
+
useInput5(
|
|
1543
|
+
(input, key) => {
|
|
1544
|
+
const ch = input.toLowerCase();
|
|
1545
|
+
if (key.escape) onResolve("later", approval.id);
|
|
1546
|
+
else if (ch === "y") onResolve("approve", approval.id);
|
|
1547
|
+
else if (ch === "n") onResolve("deny", approval.id);
|
|
1548
|
+
},
|
|
1549
|
+
{ isActive: props.isFocused ?? true }
|
|
1550
|
+
);
|
|
1551
|
+
const data = JSON.stringify(approval.data, null, 2);
|
|
1552
|
+
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
1553
|
+
/* @__PURE__ */ jsx11(Text11, { bold: true, color: "yellow", children: "approval requested" }),
|
|
1554
|
+
/* @__PURE__ */ jsx11(Text11, { children: approval.message || "(no message)" }),
|
|
1555
|
+
/* @__PURE__ */ jsx11(Text11, { dimColor: true, children: data }),
|
|
1556
|
+
/* @__PURE__ */ jsx11(Text11, { children: "[Y] approve [N] deny [Esc] later" }),
|
|
1557
|
+
/* @__PURE__ */ jsx11(Text11, { dimColor: true, children: APPROVAL_REPLY_PLACEHOLDER })
|
|
1558
|
+
] });
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/tui/modals/RunsPicker.tsx
|
|
1562
|
+
import { Box as Box11, Text as Text12, useInput as useInput6 } from "ink";
|
|
1563
|
+
import { useState as useState5 } from "react";
|
|
1564
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1565
|
+
function RunsPicker(props) {
|
|
1566
|
+
const maxRows = props.maxRows ?? 10;
|
|
1567
|
+
const [index, setIndex] = useState5(0);
|
|
1568
|
+
const cap = Math.max(0, props.runs.length - 1);
|
|
1569
|
+
useInput6(
|
|
1570
|
+
(_input, key) => {
|
|
1571
|
+
if (key.escape) {
|
|
1572
|
+
props.onCancel();
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
if (key.upArrow) {
|
|
1576
|
+
setIndex((i) => Math.max(0, i - 1));
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (key.downArrow) {
|
|
1580
|
+
setIndex((i) => Math.min(cap, i + 1));
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
if (key.return) {
|
|
1584
|
+
const chosen = props.runs[Math.min(index, cap)];
|
|
1585
|
+
if (chosen) props.onSelect(chosen.runId);
|
|
1586
|
+
}
|
|
1587
|
+
},
|
|
1588
|
+
{ isActive: props.isFocused ?? true }
|
|
1589
|
+
);
|
|
1590
|
+
if (props.runs.length === 0) {
|
|
1591
|
+
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
1592
|
+
/* @__PURE__ */ jsx12(Text12, { bold: true, children: "recent runs" }),
|
|
1593
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "(no runs in this session)" }),
|
|
1594
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "esc to close" })
|
|
1595
|
+
] });
|
|
1596
|
+
}
|
|
1597
|
+
const visible = props.runs.slice(0, maxRows);
|
|
1598
|
+
const truncated = props.runs.length - visible.length;
|
|
1599
|
+
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
1600
|
+
/* @__PURE__ */ jsx12(Text12, { bold: true, children: "recent runs" }),
|
|
1601
|
+
visible.map((r, i) => /* @__PURE__ */ jsx12(Text12, { inverse: i === index, children: formatRow(r) }, r.runId)),
|
|
1602
|
+
truncated > 0 ? /* @__PURE__ */ jsxs11(Text12, { dimColor: true, children: [
|
|
1603
|
+
"\u2026 ",
|
|
1604
|
+
truncated,
|
|
1605
|
+
" more"
|
|
1606
|
+
] }) : null,
|
|
1607
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "\u2191\u2193 navigate \xB7 enter replay \xB7 esc cancel" })
|
|
1608
|
+
] });
|
|
1609
|
+
}
|
|
1610
|
+
function formatRow(r) {
|
|
1611
|
+
const short = r.runId.slice(0, 12);
|
|
1612
|
+
const status = r.status === "unknown" ? "?" : statusGlyph(r.status);
|
|
1613
|
+
const prompt = r.prompt ? truncate2(r.prompt, 50) : "(no prompt)";
|
|
1614
|
+
return `${status} ${short.padEnd(12)} ${prompt}`;
|
|
1615
|
+
}
|
|
1616
|
+
function statusGlyph(s) {
|
|
1617
|
+
if (s === "complete") return "\u2713";
|
|
1618
|
+
if (s === "failed") return "\u2717";
|
|
1619
|
+
return "\u2026";
|
|
1620
|
+
}
|
|
1621
|
+
function truncate2(text, max) {
|
|
1622
|
+
if (text.length <= max) return text;
|
|
1623
|
+
return `${text.slice(0, max - 1)}\u2026`;
|
|
1624
|
+
}
|
|
1625
|
+
function summarizeRuns(state) {
|
|
1626
|
+
const out = [];
|
|
1627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1628
|
+
for (let i = state.transcript.length - 1; i >= 0; i -= 1) {
|
|
1629
|
+
const item = state.transcript[i];
|
|
1630
|
+
if (!item || item.kind !== "assistant") continue;
|
|
1631
|
+
if (seen.has(item.runId)) continue;
|
|
1632
|
+
seen.add(item.runId);
|
|
1633
|
+
const prompt = findPrecedingPrompt(state.transcript, i);
|
|
1634
|
+
out.push({ runId: item.runId, prompt, status: item.status });
|
|
1635
|
+
}
|
|
1636
|
+
return out;
|
|
1637
|
+
}
|
|
1638
|
+
function findPrecedingPrompt(items, startAt) {
|
|
1639
|
+
for (let i = startAt - 1; i >= 0; i -= 1) {
|
|
1640
|
+
const item = items[i];
|
|
1641
|
+
if (item && item.kind === "user") return item.text;
|
|
1642
|
+
}
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/tui/theme/ThemeProvider.tsx
|
|
1647
|
+
import { createContext, useContext, useMemo, useState as useState6 } from "react";
|
|
1648
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
1649
|
+
var ThemeContext = createContext({
|
|
1650
|
+
theme: DARK_THEME,
|
|
1651
|
+
setTheme: () => void 0
|
|
1652
|
+
});
|
|
1653
|
+
function ThemeProvider(props) {
|
|
1654
|
+
const [theme, setTheme] = useState6(
|
|
1655
|
+
props.initialTheme ?? DARK_THEME
|
|
1656
|
+
);
|
|
1657
|
+
const value = useMemo(
|
|
1658
|
+
() => ({ theme, setTheme }),
|
|
1659
|
+
[theme]
|
|
1660
|
+
);
|
|
1661
|
+
return /* @__PURE__ */ jsx13(ThemeContext.Provider, { value, children: props.children });
|
|
1662
|
+
}
|
|
1663
|
+
function useTheme() {
|
|
1664
|
+
return useContext(ThemeContext);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/tui/theme/loadUserTheme.ts
|
|
1668
|
+
function loadUserTheme(config) {
|
|
1669
|
+
if (config === void 0 || config === null) {
|
|
1670
|
+
return { theme: DARK_THEME, warning: null };
|
|
1671
|
+
}
|
|
1672
|
+
if (typeof config === "string") {
|
|
1673
|
+
return resolveByName(config);
|
|
1674
|
+
}
|
|
1675
|
+
if (typeof config !== "object") {
|
|
1676
|
+
return {
|
|
1677
|
+
theme: DARK_THEME,
|
|
1678
|
+
warning: `theme config must be a string or object, got ${typeof config}`
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
const name = config.name ?? DEFAULT_THEME_NAME;
|
|
1682
|
+
const baseResult = resolveByName(name);
|
|
1683
|
+
const overrides = config.tokens ?? {};
|
|
1684
|
+
return {
|
|
1685
|
+
theme: { ...baseResult.theme, ...overrides },
|
|
1686
|
+
warning: baseResult.warning
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
function resolveByName(name) {
|
|
1690
|
+
const found = BUILTIN_THEMES[name];
|
|
1691
|
+
if (found) return { theme: found, warning: null };
|
|
1692
|
+
return {
|
|
1693
|
+
theme: DARK_THEME,
|
|
1694
|
+
warning: `unknown theme: ${name}, falling back to ${DEFAULT_THEME_NAME}`
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/tui/useSession.ts
|
|
1699
|
+
import { useCallback, useEffect as useEffect2, useReducer as useReducer2, useRef } from "react";
|
|
1700
|
+
import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
|
|
1701
|
+
import { dirname as dirname3 } from "path";
|
|
1702
|
+
|
|
1703
|
+
// src/tui/transcript/project.ts
|
|
1704
|
+
function applyHarnessEvent(slice, event, runId, ctx) {
|
|
1705
|
+
switch (event.type) {
|
|
1706
|
+
case "run.started":
|
|
1707
|
+
return { ...slice, currentRunId: runId };
|
|
1708
|
+
case "run.completed":
|
|
1709
|
+
case "run.failed":
|
|
1710
|
+
return slice;
|
|
1711
|
+
case "answer.completed":
|
|
1712
|
+
return upsertAssistantItem(slice, event, runId, ctx);
|
|
1713
|
+
case "tool.started":
|
|
1714
|
+
return appendToolItem(slice, event, "running", ctx);
|
|
1715
|
+
case "tool.completed":
|
|
1716
|
+
return flipOrAppendTool(slice, event, "ok", ctx);
|
|
1717
|
+
case "tool.failed":
|
|
1718
|
+
return flipOrAppendTool(slice, event, "failed", ctx);
|
|
1719
|
+
case "approval.requested": {
|
|
1720
|
+
const ts = ctx.now();
|
|
1721
|
+
const message = event.message || "approval needed";
|
|
1722
|
+
const systemItem = {
|
|
1723
|
+
kind: "system",
|
|
1724
|
+
id: ctx.uuid(),
|
|
1725
|
+
text: `approval requested: ${message}`,
|
|
1726
|
+
ts
|
|
1727
|
+
};
|
|
1728
|
+
const approval = {
|
|
1729
|
+
id: event.id,
|
|
1730
|
+
runId,
|
|
1731
|
+
message: event.message,
|
|
1732
|
+
data: event.data,
|
|
1733
|
+
ts
|
|
1734
|
+
};
|
|
1735
|
+
return {
|
|
1736
|
+
...slice,
|
|
1737
|
+
transcript: [...slice.transcript, systemItem],
|
|
1738
|
+
pendingApprovals: [...slice.pendingApprovals, approval]
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
case "route.selected": {
|
|
1742
|
+
const route = typeof event.data.route === "string" && event.data.route.length > 0 ? event.data.route : event.message;
|
|
1743
|
+
if (!route) return slice;
|
|
1744
|
+
return appendItem(slice, {
|
|
1745
|
+
kind: "route",
|
|
1746
|
+
id: ctx.uuid(),
|
|
1747
|
+
route,
|
|
1748
|
+
ts: ctx.now()
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
case "agent.turn":
|
|
1752
|
+
case "agent.started": {
|
|
1753
|
+
const agent = typeof event.data.agent === "string" && event.data.agent.length > 0 ? event.data.agent : event.message;
|
|
1754
|
+
if (!agent) return slice;
|
|
1755
|
+
return appendItem(slice, {
|
|
1756
|
+
kind: "agent",
|
|
1757
|
+
id: ctx.uuid(),
|
|
1758
|
+
agent,
|
|
1759
|
+
ts: ctx.now()
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
case "agent.completed":
|
|
1763
|
+
return slice;
|
|
1764
|
+
case "thinking.delta": {
|
|
1765
|
+
const delta = typeof event.data.delta === "string" ? event.data.delta : "";
|
|
1766
|
+
if (!delta) return slice;
|
|
1767
|
+
const agent = typeof event.data.agent === "string" ? event.data.agent : "synthesizer";
|
|
1768
|
+
const existingId = slice.currentThinkingItemId;
|
|
1769
|
+
if (existingId) {
|
|
1770
|
+
return {
|
|
1771
|
+
...slice,
|
|
1772
|
+
transcript: slice.transcript.map(
|
|
1773
|
+
(item2) => item2.kind === "thinking" && item2.id === existingId ? { ...item2, text: item2.text + delta } : item2
|
|
1774
|
+
)
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
const id = ctx.uuid();
|
|
1778
|
+
const item = {
|
|
1779
|
+
kind: "thinking",
|
|
1780
|
+
id,
|
|
1781
|
+
agent,
|
|
1782
|
+
text: delta,
|
|
1783
|
+
status: "streaming",
|
|
1784
|
+
ts: ctx.now()
|
|
1785
|
+
};
|
|
1786
|
+
return {
|
|
1787
|
+
...slice,
|
|
1788
|
+
currentThinkingItemId: id,
|
|
1789
|
+
transcript: [...slice.transcript, item]
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
case "thinking.completed": {
|
|
1793
|
+
const existingId = slice.currentThinkingItemId;
|
|
1794
|
+
if (!existingId) return slice;
|
|
1795
|
+
return {
|
|
1796
|
+
...slice,
|
|
1797
|
+
currentThinkingItemId: null,
|
|
1798
|
+
transcript: slice.transcript.map(
|
|
1799
|
+
(item) => item.kind === "thinking" && item.id === existingId ? { ...item, status: "complete" } : item
|
|
1800
|
+
)
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
case "usage.recorded": {
|
|
1804
|
+
const inT = typeof event.data.input_tokens === "number" ? event.data.input_tokens : 0;
|
|
1805
|
+
const outT = typeof event.data.output_tokens === "number" ? event.data.output_tokens : 0;
|
|
1806
|
+
const cacheR = typeof event.data.cache_read_input_tokens === "number" ? event.data.cache_read_input_tokens : 0;
|
|
1807
|
+
const cacheC = typeof event.data.cache_creation_input_tokens === "number" ? event.data.cache_creation_input_tokens : 0;
|
|
1808
|
+
const cost = typeof event.data.cost_usd === "number" ? event.data.cost_usd : 0;
|
|
1809
|
+
const agent = typeof event.data.agent === "string" ? event.data.agent : null;
|
|
1810
|
+
return {
|
|
1811
|
+
...slice,
|
|
1812
|
+
usageTotals: {
|
|
1813
|
+
inputTokens: slice.usageTotals.inputTokens + inT,
|
|
1814
|
+
outputTokens: slice.usageTotals.outputTokens + outT,
|
|
1815
|
+
cacheReadTokens: slice.usageTotals.cacheReadTokens + cacheR,
|
|
1816
|
+
cacheCreationTokens: slice.usageTotals.cacheCreationTokens + cacheC,
|
|
1817
|
+
costUsd: slice.usageTotals.costUsd + cost,
|
|
1818
|
+
lastAgent: agent,
|
|
1819
|
+
lastCostUsd: typeof event.data.cost_usd === "number" ? event.data.cost_usd : null
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
case "plan.created":
|
|
1824
|
+
return appendItem(slice, {
|
|
1825
|
+
kind: "plan",
|
|
1826
|
+
id: ctx.uuid(),
|
|
1827
|
+
message: event.message || "plan ready",
|
|
1828
|
+
ts: ctx.now()
|
|
1829
|
+
});
|
|
1830
|
+
case "freshness.warning":
|
|
1831
|
+
return appendItem(slice, {
|
|
1832
|
+
kind: "freshness",
|
|
1833
|
+
id: ctx.uuid(),
|
|
1834
|
+
message: event.message || "stale data",
|
|
1835
|
+
ts: ctx.now()
|
|
1836
|
+
});
|
|
1837
|
+
case "artifact.created": {
|
|
1838
|
+
const artifactKind = typeof event.data.kind === "string" && event.data.kind.length > 0 ? event.data.kind : "artifact";
|
|
1839
|
+
return appendItem(slice, {
|
|
1840
|
+
kind: "artifact",
|
|
1841
|
+
id: ctx.uuid(),
|
|
1842
|
+
artifactKind,
|
|
1843
|
+
ts: ctx.now()
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
function appendItem(slice, item) {
|
|
1849
|
+
return { ...slice, transcript: [...slice.transcript, item] };
|
|
1850
|
+
}
|
|
1851
|
+
function extractAnswerText2(event) {
|
|
1852
|
+
const data = event.data;
|
|
1853
|
+
if (typeof data?.text === "string" && data.text.length > 0) return data.text;
|
|
1854
|
+
if (typeof data?.answer === "string" && data.answer.length > 0) {
|
|
1855
|
+
return data.answer;
|
|
1856
|
+
}
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
function upsertAssistantItem(slice, event, runId, ctx) {
|
|
1860
|
+
const text = extractAnswerText2(event);
|
|
1861
|
+
const existingId = slice.currentAssistantItemId;
|
|
1862
|
+
if (existingId) {
|
|
1863
|
+
if (text === null) return slice;
|
|
1864
|
+
return {
|
|
1865
|
+
...slice,
|
|
1866
|
+
transcript: slice.transcript.map(
|
|
1867
|
+
(item2) => item2.kind === "assistant" && item2.id === existingId ? { ...item2, text, status: "streaming" } : item2
|
|
1868
|
+
)
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
const id = ctx.uuid();
|
|
1872
|
+
const item = {
|
|
1873
|
+
kind: "assistant",
|
|
1874
|
+
id,
|
|
1875
|
+
runId,
|
|
1876
|
+
text: text ?? "",
|
|
1877
|
+
status: "streaming",
|
|
1878
|
+
ts: ctx.now()
|
|
1879
|
+
};
|
|
1880
|
+
return {
|
|
1881
|
+
...slice,
|
|
1882
|
+
currentAssistantItemId: id,
|
|
1883
|
+
transcript: [...slice.transcript, item]
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
var TOOL_VERB_PREFIX_RE = /^(Starting|Started|Completed|Finished|Failed|Running|Executing)\s+/i;
|
|
1887
|
+
function normalizeToolName(raw) {
|
|
1888
|
+
return raw.replace(TOOL_VERB_PREFIX_RE, "").trim();
|
|
1889
|
+
}
|
|
1890
|
+
function extractToolName(event) {
|
|
1891
|
+
const raw = typeof event.data.tool === "string" && event.data.tool.length > 0 ? event.data.tool : typeof event.data.name === "string" && event.data.name.length > 0 ? event.data.name : event.message;
|
|
1892
|
+
return normalizeToolName(raw);
|
|
1893
|
+
}
|
|
1894
|
+
function appendToolItem(slice, event, status, ctx) {
|
|
1895
|
+
const name = extractToolName(event);
|
|
1896
|
+
const item = {
|
|
1897
|
+
kind: "tool",
|
|
1898
|
+
id: ctx.uuid(),
|
|
1899
|
+
name,
|
|
1900
|
+
status,
|
|
1901
|
+
message: event.message.length > 0 ? event.message : null,
|
|
1902
|
+
ts: ctx.now()
|
|
1903
|
+
};
|
|
1904
|
+
return appendItem(slice, item);
|
|
1905
|
+
}
|
|
1906
|
+
function flipOrAppendTool(slice, event, status, ctx) {
|
|
1907
|
+
const name = extractToolName(event);
|
|
1908
|
+
for (let i = slice.transcript.length - 1; i >= 0; i -= 1) {
|
|
1909
|
+
const item = slice.transcript[i];
|
|
1910
|
+
if (item && item.kind === "tool" && item.status === "running" && item.name === name) {
|
|
1911
|
+
const message = event.message.length > 0 ? event.message : item.message;
|
|
1912
|
+
const updated = { ...item, status, message };
|
|
1913
|
+
const next = slice.transcript.slice();
|
|
1914
|
+
next[i] = updated;
|
|
1915
|
+
return { ...slice, transcript: next };
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return appendToolItem(slice, event, status, ctx);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// src/tui/session/types.ts
|
|
1922
|
+
var ZERO_USAGE = {
|
|
1923
|
+
inputTokens: 0,
|
|
1924
|
+
outputTokens: 0,
|
|
1925
|
+
cacheReadTokens: 0,
|
|
1926
|
+
cacheCreationTokens: 0,
|
|
1927
|
+
costUsd: 0,
|
|
1928
|
+
lastAgent: null,
|
|
1929
|
+
lastCostUsd: null
|
|
1930
|
+
};
|
|
1931
|
+
|
|
1932
|
+
// src/tui/session/reducer.ts
|
|
1933
|
+
function initialSession(init, ctx) {
|
|
1934
|
+
return {
|
|
1935
|
+
meta: {
|
|
1936
|
+
conversationId: ctx.uuid(),
|
|
1937
|
+
tenantId: init.tenantId,
|
|
1938
|
+
userId: init.userId,
|
|
1939
|
+
mode: init.mode,
|
|
1940
|
+
baseUrl: init.baseUrl,
|
|
1941
|
+
profileName: init.profileName ?? null,
|
|
1942
|
+
debug: init.debug ?? false
|
|
1943
|
+
},
|
|
1944
|
+
transcript: [],
|
|
1945
|
+
pendingApprovals: [],
|
|
1946
|
+
resolvedApprovals: [],
|
|
1947
|
+
currentRunId: null,
|
|
1948
|
+
currentAssistantItemId: null,
|
|
1949
|
+
currentThinkingItemId: null,
|
|
1950
|
+
usageTotals: { ...ZERO_USAGE },
|
|
1951
|
+
warnAgenticTenantMismatch: isAgenticTenantMismatch(
|
|
1952
|
+
init.mode,
|
|
1953
|
+
init.tenantId
|
|
1954
|
+
),
|
|
1955
|
+
lastSubmittedPrompt: null
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
function reduce(state, action, ctx) {
|
|
1959
|
+
switch (action.kind) {
|
|
1960
|
+
case "BEGIN_TURN": {
|
|
1961
|
+
const item = {
|
|
1962
|
+
kind: "user",
|
|
1963
|
+
id: ctx.uuid(),
|
|
1964
|
+
text: action.prompt,
|
|
1965
|
+
ts: ctx.now()
|
|
1966
|
+
};
|
|
1967
|
+
return {
|
|
1968
|
+
...state,
|
|
1969
|
+
transcript: [...state.transcript, item],
|
|
1970
|
+
lastSubmittedPrompt: action.prompt,
|
|
1971
|
+
currentAssistantItemId: null,
|
|
1972
|
+
currentThinkingItemId: null,
|
|
1973
|
+
currentRunId: `pending:${ctx.uuid()}`
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
case "STREAM_EVENT": {
|
|
1977
|
+
const slice = applyHarnessEvent(
|
|
1978
|
+
{
|
|
1979
|
+
transcript: state.transcript,
|
|
1980
|
+
currentAssistantItemId: state.currentAssistantItemId,
|
|
1981
|
+
currentThinkingItemId: state.currentThinkingItemId,
|
|
1982
|
+
pendingApprovals: state.pendingApprovals,
|
|
1983
|
+
currentRunId: state.currentRunId,
|
|
1984
|
+
usageTotals: state.usageTotals
|
|
1985
|
+
},
|
|
1986
|
+
action.event,
|
|
1987
|
+
action.runId,
|
|
1988
|
+
ctx
|
|
1989
|
+
);
|
|
1990
|
+
return {
|
|
1991
|
+
...state,
|
|
1992
|
+
transcript: slice.transcript,
|
|
1993
|
+
currentAssistantItemId: slice.currentAssistantItemId,
|
|
1994
|
+
currentThinkingItemId: slice.currentThinkingItemId,
|
|
1995
|
+
pendingApprovals: slice.pendingApprovals,
|
|
1996
|
+
currentRunId: slice.currentRunId,
|
|
1997
|
+
usageTotals: slice.usageTotals
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
case "END_TURN":
|
|
2001
|
+
return applyEndTurn(state, action.record, action.failureMessage, ctx);
|
|
2002
|
+
case "SET_MODE": {
|
|
2003
|
+
const tenant = state.meta.tenantId;
|
|
2004
|
+
return {
|
|
2005
|
+
...state,
|
|
2006
|
+
meta: { ...state.meta, mode: action.mode },
|
|
2007
|
+
warnAgenticTenantMismatch: isAgenticTenantMismatch(
|
|
2008
|
+
action.mode,
|
|
2009
|
+
tenant
|
|
2010
|
+
)
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
case "SET_TENANT":
|
|
2014
|
+
return {
|
|
2015
|
+
...state,
|
|
2016
|
+
meta: { ...state.meta, tenantId: action.tenant },
|
|
2017
|
+
warnAgenticTenantMismatch: isAgenticTenantMismatch(
|
|
2018
|
+
state.meta.mode,
|
|
2019
|
+
action.tenant
|
|
2020
|
+
)
|
|
2021
|
+
};
|
|
2022
|
+
case "SET_USER":
|
|
2023
|
+
return { ...state, meta: { ...state.meta, userId: action.user } };
|
|
2024
|
+
case "RESET_CONVERSATION":
|
|
2025
|
+
return {
|
|
2026
|
+
...state,
|
|
2027
|
+
meta: { ...state.meta, conversationId: ctx.uuid() },
|
|
2028
|
+
transcript: [],
|
|
2029
|
+
pendingApprovals: [],
|
|
2030
|
+
resolvedApprovals: [],
|
|
2031
|
+
currentRunId: null,
|
|
2032
|
+
currentAssistantItemId: null,
|
|
2033
|
+
currentThinkingItemId: null,
|
|
2034
|
+
usageTotals: { ...ZERO_USAGE },
|
|
2035
|
+
lastSubmittedPrompt: null
|
|
2036
|
+
};
|
|
2037
|
+
case "CLEAR":
|
|
2038
|
+
return {
|
|
2039
|
+
...state,
|
|
2040
|
+
transcript: [],
|
|
2041
|
+
currentAssistantItemId: null,
|
|
2042
|
+
currentThinkingItemId: null,
|
|
2043
|
+
usageTotals: { ...ZERO_USAGE }
|
|
2044
|
+
};
|
|
2045
|
+
case "LOAD_SESSION":
|
|
2046
|
+
return action.state;
|
|
2047
|
+
case "RECORD_APPROVAL": {
|
|
2048
|
+
const approval = {
|
|
2049
|
+
id: action.approval.id,
|
|
2050
|
+
runId: action.approval.runId,
|
|
2051
|
+
message: action.approval.message,
|
|
2052
|
+
data: action.approval.data,
|
|
2053
|
+
ts: action.approval.ts ?? ctx.now()
|
|
2054
|
+
};
|
|
2055
|
+
return {
|
|
2056
|
+
...state,
|
|
2057
|
+
pendingApprovals: [...state.pendingApprovals, approval]
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
case "RESOLVE_APPROVAL":
|
|
2061
|
+
return {
|
|
2062
|
+
...state,
|
|
2063
|
+
pendingApprovals: state.pendingApprovals.filter(
|
|
2064
|
+
(a) => a.id !== action.id
|
|
2065
|
+
),
|
|
2066
|
+
resolvedApprovals: [
|
|
2067
|
+
...state.resolvedApprovals,
|
|
2068
|
+
{ id: action.id, decision: action.decision, ts: ctx.now() }
|
|
2069
|
+
]
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
function applyEndTurn(state, record, failureMessage, ctx) {
|
|
2074
|
+
const ts = ctx.now();
|
|
2075
|
+
const assistantId = state.currentAssistantItemId;
|
|
2076
|
+
if (failureMessage !== void 0) {
|
|
2077
|
+
const transcript2 = state.transcript.map(
|
|
2078
|
+
(item) => item.kind === "assistant" && item.id === assistantId ? { ...item, status: "failed" } : item
|
|
2079
|
+
);
|
|
2080
|
+
const systemItem = {
|
|
2081
|
+
kind: "system",
|
|
2082
|
+
id: ctx.uuid(),
|
|
2083
|
+
text: `error: ${failureMessage}`,
|
|
2084
|
+
ts
|
|
2085
|
+
};
|
|
2086
|
+
return {
|
|
2087
|
+
...state,
|
|
2088
|
+
transcript: [...transcript2, systemItem],
|
|
2089
|
+
currentRunId: null,
|
|
2090
|
+
currentAssistantItemId: null,
|
|
2091
|
+
currentThinkingItemId: null
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
if (assistantId === null) {
|
|
2095
|
+
if (record && typeof record.answer === "string" && record.answer.length > 0) {
|
|
2096
|
+
const item = {
|
|
2097
|
+
kind: "assistant",
|
|
2098
|
+
id: ctx.uuid(),
|
|
2099
|
+
runId: state.currentRunId ?? "",
|
|
2100
|
+
text: record.answer,
|
|
2101
|
+
status: "complete",
|
|
2102
|
+
ts
|
|
2103
|
+
};
|
|
2104
|
+
return {
|
|
2105
|
+
...state,
|
|
2106
|
+
transcript: [...state.transcript, item],
|
|
2107
|
+
currentRunId: null,
|
|
2108
|
+
currentAssistantItemId: null,
|
|
2109
|
+
currentThinkingItemId: null
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
const placeholder = {
|
|
2113
|
+
kind: "system",
|
|
2114
|
+
id: ctx.uuid(),
|
|
2115
|
+
text: "(no answer recorded)",
|
|
2116
|
+
ts
|
|
2117
|
+
};
|
|
2118
|
+
return {
|
|
2119
|
+
...state,
|
|
2120
|
+
transcript: [...state.transcript, placeholder],
|
|
2121
|
+
currentRunId: null,
|
|
2122
|
+
currentAssistantItemId: null,
|
|
2123
|
+
currentThinkingItemId: null
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
const transcript = state.transcript.map((item) => {
|
|
2127
|
+
if (item.kind !== "assistant" || item.id !== assistantId) return item;
|
|
2128
|
+
const text = record && typeof record.answer === "string" && record.answer.length > 0 ? record.answer : item.text;
|
|
2129
|
+
return { ...item, text, status: "complete" };
|
|
2130
|
+
});
|
|
2131
|
+
return {
|
|
2132
|
+
...state,
|
|
2133
|
+
transcript,
|
|
2134
|
+
currentRunId: null,
|
|
2135
|
+
currentAssistantItemId: null,
|
|
2136
|
+
currentThinkingItemId: null
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// src/tui/persistence/paths.ts
|
|
2141
|
+
import { homedir as homedir3 } from "os";
|
|
2142
|
+
import { join as join4 } from "path";
|
|
2143
|
+
function defaultMiotChatHome() {
|
|
2144
|
+
return join4(homedir3(), ".miot-chat");
|
|
2145
|
+
}
|
|
2146
|
+
function sessionsDir(home) {
|
|
2147
|
+
return join4(home, "sessions");
|
|
2148
|
+
}
|
|
2149
|
+
function sessionFile(home, conversationId) {
|
|
2150
|
+
return join4(sessionsDir(home), `${conversationId}.json`);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/tui/useSession.ts
|
|
2154
|
+
var DEBUG_ENV = "MIOT_CHAT_TUI_DEBUG";
|
|
2155
|
+
function debugEnabled() {
|
|
2156
|
+
return process.env[DEBUG_ENV] === "1";
|
|
2157
|
+
}
|
|
2158
|
+
function debugLogPath() {
|
|
2159
|
+
return `${defaultMiotChatHome()}/last-run.log`;
|
|
2160
|
+
}
|
|
2161
|
+
function debugLog(line) {
|
|
2162
|
+
if (!debugEnabled()) return;
|
|
2163
|
+
try {
|
|
2164
|
+
const path = debugLogPath();
|
|
2165
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
2166
|
+
appendFileSync(
|
|
2167
|
+
path,
|
|
2168
|
+
`${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...line })}
|
|
2169
|
+
`
|
|
2170
|
+
);
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
function useSession(opts) {
|
|
2175
|
+
const [state, dispatch] = useReducer2(
|
|
2176
|
+
(s, a) => reduce(s, a, opts.ctx),
|
|
2177
|
+
null,
|
|
2178
|
+
() => initialSession(opts.initial, opts.ctx)
|
|
2179
|
+
);
|
|
2180
|
+
const stateRef = useRef(state);
|
|
2181
|
+
useEffect2(() => {
|
|
2182
|
+
stateRef.current = state;
|
|
2183
|
+
}, [state]);
|
|
2184
|
+
const abortRef = useRef(null);
|
|
2185
|
+
const submit = useCallback(
|
|
2186
|
+
async (prompt) => {
|
|
2187
|
+
const meta = stateRef.current.meta;
|
|
2188
|
+
const trimmed = prompt.trim();
|
|
2189
|
+
if (trimmed.length === 0) return;
|
|
2190
|
+
debugLog({ event: "submit", prompt, meta });
|
|
2191
|
+
dispatch({ kind: "BEGIN_TURN", prompt });
|
|
2192
|
+
const controller = new AbortController();
|
|
2193
|
+
abortRef.current = controller;
|
|
2194
|
+
let runId;
|
|
2195
|
+
try {
|
|
2196
|
+
const created = await opts.client.runs.create(
|
|
2197
|
+
{
|
|
2198
|
+
message: prompt,
|
|
2199
|
+
tenant_id: meta.tenantId,
|
|
2200
|
+
user_id: meta.userId,
|
|
2201
|
+
mode: meta.mode,
|
|
2202
|
+
conversation_id: meta.conversationId,
|
|
2203
|
+
...meta.debug ? { debug: true } : {}
|
|
2204
|
+
},
|
|
2205
|
+
{ signal: controller.signal }
|
|
2206
|
+
);
|
|
2207
|
+
runId = created.run_id;
|
|
2208
|
+
debugLog({ event: "create.ok", runId });
|
|
2209
|
+
} catch (err) {
|
|
2210
|
+
debugLog({
|
|
2211
|
+
event: "create.err",
|
|
2212
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2213
|
+
});
|
|
2214
|
+
dispatch({
|
|
2215
|
+
kind: "END_TURN",
|
|
2216
|
+
failureMessage: err instanceof Error ? err.message : String(err)
|
|
2217
|
+
});
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
try {
|
|
2221
|
+
for await (const event of opts.client.runs.stream(runId, {
|
|
2222
|
+
signal: controller.signal
|
|
2223
|
+
})) {
|
|
2224
|
+
debugLog({
|
|
2225
|
+
event: "stream.event",
|
|
2226
|
+
runId,
|
|
2227
|
+
type: event.type,
|
|
2228
|
+
seq: event.seq,
|
|
2229
|
+
message: event.message,
|
|
2230
|
+
data: event.data
|
|
2231
|
+
});
|
|
2232
|
+
dispatch({ kind: "STREAM_EVENT", event, runId });
|
|
2233
|
+
if (event.type === "run.completed" || event.type === "run.failed") {
|
|
2234
|
+
break;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
debugLog({ event: "stream.end", runId });
|
|
2238
|
+
} catch (err) {
|
|
2239
|
+
debugLog({
|
|
2240
|
+
event: "stream.err",
|
|
2241
|
+
runId,
|
|
2242
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2243
|
+
});
|
|
2244
|
+
dispatch({
|
|
2245
|
+
kind: "END_TURN",
|
|
2246
|
+
failureMessage: err instanceof Error ? err.message : String(err)
|
|
2247
|
+
});
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
const recordController = new AbortController();
|
|
2251
|
+
const RECORD_TIMEOUT_MS = 8e3;
|
|
2252
|
+
let timedOut = false;
|
|
2253
|
+
const timer = setTimeout(() => {
|
|
2254
|
+
timedOut = true;
|
|
2255
|
+
recordController.abort();
|
|
2256
|
+
}, RECORD_TIMEOUT_MS);
|
|
2257
|
+
debugLog({ event: "get.start", runId });
|
|
2258
|
+
try {
|
|
2259
|
+
const record = await opts.client.runs.get(runId, {
|
|
2260
|
+
signal: recordController.signal
|
|
2261
|
+
});
|
|
2262
|
+
debugLog({
|
|
2263
|
+
event: "get.ok",
|
|
2264
|
+
runId,
|
|
2265
|
+
answerLength: record.answer?.length ?? null,
|
|
2266
|
+
status: record.status
|
|
2267
|
+
});
|
|
2268
|
+
dispatch({ kind: "END_TURN", record });
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
const detail = timedOut ? `run record fetch timed out after ${RECORD_TIMEOUT_MS}ms \u2014 check that GET /runs/${runId} returns promptly` : err instanceof Error ? err.message : String(err);
|
|
2271
|
+
debugLog({ event: "get.err", runId, timedOut, message: detail });
|
|
2272
|
+
dispatch({ kind: "END_TURN", failureMessage: detail });
|
|
2273
|
+
} finally {
|
|
2274
|
+
clearTimeout(timer);
|
|
2275
|
+
}
|
|
2276
|
+
},
|
|
2277
|
+
[opts.client]
|
|
2278
|
+
);
|
|
2279
|
+
const abort = useCallback(() => {
|
|
2280
|
+
abortRef.current?.abort();
|
|
2281
|
+
}, []);
|
|
2282
|
+
return { state, dispatch, submit, abort };
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// src/tui/slash/parse.ts
|
|
2286
|
+
function parseSlash2(line) {
|
|
2287
|
+
const trimmed = line.trim();
|
|
2288
|
+
if (!trimmed.startsWith("/")) return null;
|
|
2289
|
+
const body = trimmed.slice(1);
|
|
2290
|
+
if (body.length === 0) return null;
|
|
2291
|
+
const tokens = body.split(/\s+/);
|
|
2292
|
+
const head = (tokens[0] ?? "").toLowerCase();
|
|
2293
|
+
if (head.length === 0) return null;
|
|
2294
|
+
return { name: head, args: tokens.slice(1) };
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// src/tui/slash/registry.ts
|
|
2298
|
+
var SlashRegistry = class {
|
|
2299
|
+
commands = /* @__PURE__ */ new Map();
|
|
2300
|
+
register(cmd) {
|
|
2301
|
+
this.commands.set(cmd.name, cmd);
|
|
2302
|
+
return this;
|
|
2303
|
+
}
|
|
2304
|
+
has(name) {
|
|
2305
|
+
return this.commands.has(name);
|
|
2306
|
+
}
|
|
2307
|
+
get(name) {
|
|
2308
|
+
return this.commands.get(name);
|
|
2309
|
+
}
|
|
2310
|
+
all() {
|
|
2311
|
+
return [...this.commands.values()].sort(
|
|
2312
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Substring-match across command names AND summaries.
|
|
2317
|
+
* Empty prefix returns all commands.
|
|
2318
|
+
* Matches in the name field are ranked above matches in the summary.
|
|
2319
|
+
*/
|
|
2320
|
+
findMatches(prefix) {
|
|
2321
|
+
const q = prefix.toLowerCase();
|
|
2322
|
+
if (q.length === 0) return this.all();
|
|
2323
|
+
const nameHits = [];
|
|
2324
|
+
const summaryHits = [];
|
|
2325
|
+
for (const cmd of this.all()) {
|
|
2326
|
+
if (cmd.name.toLowerCase().includes(q)) nameHits.push(cmd);
|
|
2327
|
+
else if (cmd.summary.toLowerCase().includes(q)) summaryHits.push(cmd);
|
|
2328
|
+
}
|
|
2329
|
+
return [...nameHits, ...summaryHits];
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Tab-completion target: returns the unique match's name when there is
|
|
2333
|
+
* exactly one, otherwise null. The palette uses this to decide whether
|
|
2334
|
+
* Tab should auto-complete or just keep the dropdown open.
|
|
2335
|
+
*/
|
|
2336
|
+
tabCompletion(prefix) {
|
|
2337
|
+
const matches = this.findMatches(prefix);
|
|
2338
|
+
return matches.length === 1 && matches[0] ? matches[0].name : null;
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
|
|
2342
|
+
// src/tui/slash/handlers/help.ts
|
|
2343
|
+
function isHelpCtx(ctx) {
|
|
2344
|
+
return typeof ctx.registry === "object" && ctx.registry !== null && "all" in ctx.registry && typeof ctx.now === "function" && typeof ctx.uuid === "function";
|
|
2345
|
+
}
|
|
2346
|
+
var helpCommand = {
|
|
2347
|
+
name: "help",
|
|
2348
|
+
summary: "List slash commands",
|
|
2349
|
+
usage: "/help",
|
|
2350
|
+
handle: (_args, ctx) => {
|
|
2351
|
+
if (!isHelpCtx(ctx)) {
|
|
2352
|
+
return { error: "help: registry/now/uuid not bound on SlashContext" };
|
|
2353
|
+
}
|
|
2354
|
+
const lines = ctx.registry.all().map((c) => ` ${c.usage.padEnd(22)} ${c.summary}`);
|
|
2355
|
+
const item = {
|
|
2356
|
+
kind: "system",
|
|
2357
|
+
id: ctx.uuid(),
|
|
2358
|
+
text: ["available commands:", ...lines].join("\n"),
|
|
2359
|
+
ts: ctx.now()
|
|
2360
|
+
};
|
|
2361
|
+
return { output: item };
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
// src/tui/slash/handlers/clear.ts
|
|
2366
|
+
var clearCommand = {
|
|
2367
|
+
name: "clear",
|
|
2368
|
+
summary: "Clear the transcript",
|
|
2369
|
+
usage: "/clear",
|
|
2370
|
+
handle: () => ({ dispatch: { kind: "CLEAR" } })
|
|
2371
|
+
};
|
|
2372
|
+
|
|
2373
|
+
// src/tui/slash/handlers/reset.ts
|
|
2374
|
+
var resetCommand = {
|
|
2375
|
+
name: "reset",
|
|
2376
|
+
summary: "Start a new conversation",
|
|
2377
|
+
usage: "/reset",
|
|
2378
|
+
handle: () => ({ dispatch: { kind: "RESET_CONVERSATION" } })
|
|
2379
|
+
};
|
|
2380
|
+
|
|
2381
|
+
// src/tui/slash/handlers/exit.ts
|
|
2382
|
+
var exitCommand = {
|
|
2383
|
+
name: "exit",
|
|
2384
|
+
summary: "Quit the chat",
|
|
2385
|
+
usage: "/exit",
|
|
2386
|
+
handle: () => ({ abort: true })
|
|
2387
|
+
};
|
|
2388
|
+
|
|
2389
|
+
// src/tui/slash/handlers/mode.ts
|
|
2390
|
+
var VALID_MODES3 = [
|
|
2391
|
+
"auto",
|
|
2392
|
+
"canned",
|
|
2393
|
+
"meta",
|
|
2394
|
+
"agentic"
|
|
2395
|
+
];
|
|
2396
|
+
var modeCommand = {
|
|
2397
|
+
name: "mode",
|
|
2398
|
+
summary: "Change the run mode (auto/canned/meta/agentic)",
|
|
2399
|
+
usage: "/mode <auto|canned|meta|agentic>",
|
|
2400
|
+
argSchema: [
|
|
2401
|
+
{
|
|
2402
|
+
name: "mode",
|
|
2403
|
+
required: true,
|
|
2404
|
+
choices: VALID_MODES3
|
|
2405
|
+
}
|
|
2406
|
+
],
|
|
2407
|
+
handle: (args) => {
|
|
2408
|
+
const value = args[0];
|
|
2409
|
+
if (value === void 0) {
|
|
2410
|
+
return { error: "usage: /mode <auto|canned|meta|agentic>" };
|
|
2411
|
+
}
|
|
2412
|
+
if (!VALID_MODES3.includes(value)) {
|
|
2413
|
+
return { error: `unknown mode: ${value}` };
|
|
2414
|
+
}
|
|
2415
|
+
return { dispatch: { kind: "SET_MODE", mode: value } };
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
|
|
2419
|
+
// src/tui/slash/handlers/tenant.ts
|
|
2420
|
+
var tenantCommand = {
|
|
2421
|
+
name: "tenant",
|
|
2422
|
+
summary: "Switch the active tenant id",
|
|
2423
|
+
usage: "/tenant <id>",
|
|
2424
|
+
argSchema: [{ name: "tenant", required: true }],
|
|
2425
|
+
handle: (args) => {
|
|
2426
|
+
const value = args[0];
|
|
2427
|
+
if (value === void 0 || value.length === 0) {
|
|
2428
|
+
return { error: "usage: /tenant <id>" };
|
|
2429
|
+
}
|
|
2430
|
+
return { dispatch: { kind: "SET_TENANT", tenant: value } };
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
|
|
2434
|
+
// src/tui/slash/handlers/user.ts
|
|
2435
|
+
var userCommand = {
|
|
2436
|
+
name: "user",
|
|
2437
|
+
summary: "Set the active user id",
|
|
2438
|
+
usage: "/user <id>",
|
|
2439
|
+
argSchema: [{ name: "user", required: true }],
|
|
2440
|
+
handle: (args) => {
|
|
2441
|
+
const value = args[0];
|
|
2442
|
+
if (value === void 0 || value.length === 0) {
|
|
2443
|
+
return { error: "usage: /user <id>" };
|
|
2444
|
+
}
|
|
2445
|
+
return { dispatch: { kind: "SET_USER", user: value } };
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
// src/tui/slash/handlers/save.ts
|
|
2450
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
2451
|
+
function isSaveCtx(ctx) {
|
|
2452
|
+
return typeof ctx.session === "object" && ctx.session !== null && typeof ctx.now === "function" && typeof ctx.uuid === "function";
|
|
2453
|
+
}
|
|
2454
|
+
var saveCommand = {
|
|
2455
|
+
name: "save",
|
|
2456
|
+
summary: "Write the current transcript to a JSON file",
|
|
2457
|
+
usage: "/save <path>",
|
|
2458
|
+
argSchema: [{ name: "path", required: true }],
|
|
2459
|
+
handle: (args, ctx) => {
|
|
2460
|
+
const path = args.join(" ").trim();
|
|
2461
|
+
if (path.length === 0) {
|
|
2462
|
+
return { error: "usage: /save <path>" };
|
|
2463
|
+
}
|
|
2464
|
+
if (!isSaveCtx(ctx)) {
|
|
2465
|
+
return { error: "save: session/now/uuid not bound on SlashContext" };
|
|
2466
|
+
}
|
|
2467
|
+
const body = JSON.stringify(
|
|
2468
|
+
{
|
|
2469
|
+
conversation_id: ctx.session.meta.conversationId,
|
|
2470
|
+
transcript: ctx.session.transcript
|
|
2471
|
+
},
|
|
2472
|
+
null,
|
|
2473
|
+
2
|
|
2474
|
+
);
|
|
2475
|
+
const write = ctx.writeFile ?? defaultWrite;
|
|
2476
|
+
try {
|
|
2477
|
+
write(path, body);
|
|
2478
|
+
} catch (err) {
|
|
2479
|
+
return {
|
|
2480
|
+
error: `save failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
const item = {
|
|
2484
|
+
kind: "system",
|
|
2485
|
+
id: ctx.uuid(),
|
|
2486
|
+
text: `saved transcript to ${path}`,
|
|
2487
|
+
ts: ctx.now()
|
|
2488
|
+
};
|
|
2489
|
+
return { output: item };
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
function defaultWrite(path, body) {
|
|
2493
|
+
writeFileSync5(path, body);
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// src/tui/slash/handlers/context.ts
|
|
2497
|
+
var contextCommand = {
|
|
2498
|
+
name: "context",
|
|
2499
|
+
summary: "Show session context modal",
|
|
2500
|
+
usage: "/context",
|
|
2501
|
+
handle: () => ({ modal: { kind: "context" } })
|
|
2502
|
+
};
|
|
2503
|
+
|
|
2504
|
+
// src/tui/slash/handlers/whoami.ts
|
|
2505
|
+
function isWhoamiCtx(ctx) {
|
|
2506
|
+
return typeof ctx.session === "object" && ctx.session !== null && typeof ctx.now === "function" && typeof ctx.uuid === "function";
|
|
2507
|
+
}
|
|
2508
|
+
var whoamiCommand = {
|
|
2509
|
+
name: "whoami",
|
|
2510
|
+
summary: "Print user/tenant/conversation ids",
|
|
2511
|
+
usage: "/whoami",
|
|
2512
|
+
handle: (_args, ctx) => {
|
|
2513
|
+
if (!isWhoamiCtx(ctx)) {
|
|
2514
|
+
return { error: "whoami: session/now/uuid not bound on SlashContext" };
|
|
2515
|
+
}
|
|
2516
|
+
const m = ctx.session.meta;
|
|
2517
|
+
const item = {
|
|
2518
|
+
kind: "system",
|
|
2519
|
+
id: ctx.uuid(),
|
|
2520
|
+
text: `user=${m.userId} tenant=${m.tenantId} conv=${m.conversationId}`,
|
|
2521
|
+
ts: ctx.now()
|
|
2522
|
+
};
|
|
2523
|
+
return { output: item };
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
|
|
2527
|
+
// src/tui/slash/handlers/theme.ts
|
|
2528
|
+
var themeCommand = {
|
|
2529
|
+
name: "theme",
|
|
2530
|
+
summary: "Pick or set the active color theme",
|
|
2531
|
+
usage: "/theme [name]",
|
|
2532
|
+
handle: (args) => {
|
|
2533
|
+
const name = args[0];
|
|
2534
|
+
if (name && name.length > 0) {
|
|
2535
|
+
return { modal: { kind: "theme", payload: { name } } };
|
|
2536
|
+
}
|
|
2537
|
+
return { modal: { kind: "theme" } };
|
|
2538
|
+
}
|
|
2539
|
+
};
|
|
2540
|
+
|
|
2541
|
+
// src/tui/slash/handlers/resume.ts
|
|
2542
|
+
var resumeCommand = {
|
|
2543
|
+
name: "resume",
|
|
2544
|
+
summary: "Pick a saved session to reload",
|
|
2545
|
+
usage: "/resume",
|
|
2546
|
+
handle: () => ({ modal: { kind: "resume" } })
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
// src/tui/slash/handlers/export.ts
|
|
2550
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
2551
|
+
|
|
2552
|
+
// src/tui/persistence/exportMarkdown.ts
|
|
2553
|
+
function toMarkdown(state) {
|
|
2554
|
+
const lines = [];
|
|
2555
|
+
const m = state.meta;
|
|
2556
|
+
lines.push(`# miot-chat session ${m.conversationId.slice(0, 8)}`);
|
|
2557
|
+
lines.push("");
|
|
2558
|
+
lines.push(`- conversation_id: \`${m.conversationId}\``);
|
|
2559
|
+
lines.push(`- tenant: \`${m.tenantId}\``);
|
|
2560
|
+
lines.push(`- user: \`${m.userId}\``);
|
|
2561
|
+
lines.push(`- mode: \`${m.mode}\``);
|
|
2562
|
+
lines.push(`- baseUrl: ${m.baseUrl}`);
|
|
2563
|
+
if (m.profileName) lines.push(`- profile: \`${m.profileName}\``);
|
|
2564
|
+
lines.push("");
|
|
2565
|
+
const turns = splitIntoTurns(state.transcript);
|
|
2566
|
+
if (turns.length === 0) {
|
|
2567
|
+
lines.push("_(no turns)_");
|
|
2568
|
+
lines.push("");
|
|
2569
|
+
return lines.join("\n");
|
|
2570
|
+
}
|
|
2571
|
+
turns.forEach((turn, idx) => {
|
|
2572
|
+
lines.push(`## Turn ${idx + 1}`);
|
|
2573
|
+
lines.push("");
|
|
2574
|
+
for (const item of turn) {
|
|
2575
|
+
lines.push(...renderItem(item));
|
|
2576
|
+
}
|
|
2577
|
+
lines.push("");
|
|
2578
|
+
});
|
|
2579
|
+
return lines.join("\n");
|
|
2580
|
+
}
|
|
2581
|
+
function splitIntoTurns(items) {
|
|
2582
|
+
const turns = [];
|
|
2583
|
+
let current = [];
|
|
2584
|
+
for (const item of items) {
|
|
2585
|
+
if (item.kind === "user") {
|
|
2586
|
+
if (current.length > 0) turns.push(current);
|
|
2587
|
+
current = [item];
|
|
2588
|
+
} else {
|
|
2589
|
+
current.push(item);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (current.length > 0) turns.push(current);
|
|
2593
|
+
return turns;
|
|
2594
|
+
}
|
|
2595
|
+
function renderItem(item) {
|
|
2596
|
+
switch (item.kind) {
|
|
2597
|
+
case "user":
|
|
2598
|
+
return [`**you:** ${item.text}`, ""];
|
|
2599
|
+
case "assistant":
|
|
2600
|
+
return [`**miot${statusSuffix2(item.status)}:** ${item.text}`, ""];
|
|
2601
|
+
case "tool":
|
|
2602
|
+
return [`> tool ${item.name} ${toolGlyph(item.status)}${item.message ? ` \u2014 ${item.message}` : ""}`];
|
|
2603
|
+
case "route":
|
|
2604
|
+
return [`> route: ${item.route}`];
|
|
2605
|
+
case "agent":
|
|
2606
|
+
return [`> agent: ${item.agent}`];
|
|
2607
|
+
case "thinking":
|
|
2608
|
+
return [`> _thinking (${item.agent}):_ ${item.text}`];
|
|
2609
|
+
case "plan":
|
|
2610
|
+
return [`> plan: ${item.message}`];
|
|
2611
|
+
case "freshness":
|
|
2612
|
+
return [`> \u26A0 ${item.message}`];
|
|
2613
|
+
case "artifact":
|
|
2614
|
+
return [`> artifact: ${item.artifactKind}`];
|
|
2615
|
+
case "system":
|
|
2616
|
+
return [`> _${item.text}_`];
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
function statusSuffix2(status) {
|
|
2620
|
+
if (status === "failed") return " (failed)";
|
|
2621
|
+
if (status === "streaming") return " (streaming)";
|
|
2622
|
+
return "";
|
|
2623
|
+
}
|
|
2624
|
+
function toolGlyph(status) {
|
|
2625
|
+
if (status === "ok") return "\u2713";
|
|
2626
|
+
if (status === "failed") return "\u2717";
|
|
2627
|
+
return "\u2026";
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// src/tui/slash/handlers/export.ts
|
|
2631
|
+
function isExportCtx(ctx) {
|
|
2632
|
+
return typeof ctx.session === "object" && ctx.session !== null && typeof ctx.now === "function" && typeof ctx.uuid === "function";
|
|
2633
|
+
}
|
|
2634
|
+
var exportCommand = {
|
|
2635
|
+
name: "export",
|
|
2636
|
+
summary: "Write the transcript as markdown to a file",
|
|
2637
|
+
usage: "/export <path>",
|
|
2638
|
+
argSchema: [{ name: "path", required: true }],
|
|
2639
|
+
handle: (args, ctx) => {
|
|
2640
|
+
const path = args.join(" ").trim();
|
|
2641
|
+
if (path.length === 0) {
|
|
2642
|
+
return { error: "usage: /export <path>" };
|
|
2643
|
+
}
|
|
2644
|
+
if (!isExportCtx(ctx)) {
|
|
2645
|
+
return { error: "export: session/now/uuid not bound on SlashContext" };
|
|
2646
|
+
}
|
|
2647
|
+
const body = toMarkdown(ctx.session);
|
|
2648
|
+
const write = ctx.writeFile ?? defaultWrite2;
|
|
2649
|
+
try {
|
|
2650
|
+
write(path, body);
|
|
2651
|
+
} catch (err) {
|
|
2652
|
+
return {
|
|
2653
|
+
error: `export failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
const item = {
|
|
2657
|
+
kind: "system",
|
|
2658
|
+
id: ctx.uuid(),
|
|
2659
|
+
text: `exported transcript to ${path}`,
|
|
2660
|
+
ts: ctx.now()
|
|
2661
|
+
};
|
|
2662
|
+
return { output: item };
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
function defaultWrite2(path, body) {
|
|
2666
|
+
writeFileSync6(path, body);
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// src/tui/slash/handlers/runs.ts
|
|
2670
|
+
var runsCommand = {
|
|
2671
|
+
name: "runs",
|
|
2672
|
+
summary: "Pick a recent run to replay",
|
|
2673
|
+
usage: "/runs",
|
|
2674
|
+
handle: () => ({ modal: { kind: "runs" } })
|
|
2675
|
+
};
|
|
2676
|
+
|
|
2677
|
+
// src/tui/slash/handlers/approve.ts
|
|
2678
|
+
var VALID_DECISIONS = [
|
|
2679
|
+
"approve",
|
|
2680
|
+
"deny",
|
|
2681
|
+
"later"
|
|
2682
|
+
];
|
|
2683
|
+
var approveCommand = {
|
|
2684
|
+
name: "approve",
|
|
2685
|
+
summary: "Resolve a pending approval (approve|deny|later) <id>",
|
|
2686
|
+
usage: "/approve <approve|deny|later> <id>",
|
|
2687
|
+
argSchema: [
|
|
2688
|
+
{ name: "decision", required: true, choices: VALID_DECISIONS },
|
|
2689
|
+
{ name: "id", required: true }
|
|
2690
|
+
],
|
|
2691
|
+
handle: (args) => {
|
|
2692
|
+
const decision = args[0];
|
|
2693
|
+
const id = args[1];
|
|
2694
|
+
if (decision === void 0 || id === void 0) {
|
|
2695
|
+
return { error: "usage: /approve <approve|deny|later> <id>" };
|
|
2696
|
+
}
|
|
2697
|
+
if (!VALID_DECISIONS.includes(decision)) {
|
|
2698
|
+
return { error: `unknown decision: ${decision}` };
|
|
2699
|
+
}
|
|
2700
|
+
return {
|
|
2701
|
+
dispatch: {
|
|
2702
|
+
kind: "RESOLVE_APPROVAL",
|
|
2703
|
+
id,
|
|
2704
|
+
decision
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
|
|
2710
|
+
// src/tui/persistence/store.ts
|
|
2711
|
+
import {
|
|
2712
|
+
chmodSync as chmodSync2,
|
|
2713
|
+
existsSync as existsSync4,
|
|
2714
|
+
mkdirSync as mkdirSync4,
|
|
2715
|
+
readFileSync as readFileSync5,
|
|
2716
|
+
readdirSync,
|
|
2717
|
+
renameSync,
|
|
2718
|
+
statSync,
|
|
2719
|
+
unlinkSync,
|
|
2720
|
+
writeFileSync as writeFileSync7
|
|
2721
|
+
} from "fs";
|
|
2722
|
+
import { extname, join as join5 } from "path";
|
|
2723
|
+
function readSession(home, conversationId) {
|
|
2724
|
+
const path = sessionFile(home, conversationId);
|
|
2725
|
+
if (!existsSync4(path)) return null;
|
|
2726
|
+
const raw = readFileSync5(path, "utf8");
|
|
2727
|
+
try {
|
|
2728
|
+
return JSON.parse(raw);
|
|
2729
|
+
} catch {
|
|
2730
|
+
return null;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
function listSessions(home) {
|
|
2734
|
+
const dir = sessionsDir(home);
|
|
2735
|
+
if (!existsSync4(dir)) return [];
|
|
2736
|
+
const entries = readdirSync(dir);
|
|
2737
|
+
const summaries = [];
|
|
2738
|
+
for (const name of entries) {
|
|
2739
|
+
if (extname(name) !== ".json") continue;
|
|
2740
|
+
if (name.endsWith(".tmp.json")) continue;
|
|
2741
|
+
const id = name.slice(0, -".json".length);
|
|
2742
|
+
const path = join5(dir, name);
|
|
2743
|
+
let state = null;
|
|
2744
|
+
try {
|
|
2745
|
+
state = JSON.parse(readFileSync5(path, "utf8"));
|
|
2746
|
+
} catch {
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
const mtime = statSync(path).mtimeMs;
|
|
2750
|
+
summaries.push({
|
|
2751
|
+
id,
|
|
2752
|
+
lastTurn: lastTurnCount(state),
|
|
2753
|
+
lastPrompt: lastUserPrompt(state),
|
|
2754
|
+
mtime
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
summaries.sort((a, b) => b.mtime - a.mtime);
|
|
2758
|
+
return summaries;
|
|
2759
|
+
}
|
|
2760
|
+
function lastTurnCount(state) {
|
|
2761
|
+
if (!state) return 0;
|
|
2762
|
+
return state.transcript.filter((i) => i.kind === "user").length;
|
|
2763
|
+
}
|
|
2764
|
+
function lastUserPrompt(state) {
|
|
2765
|
+
if (!state) return null;
|
|
2766
|
+
for (let i = state.transcript.length - 1; i >= 0; i -= 1) {
|
|
2767
|
+
const item = state.transcript[i];
|
|
2768
|
+
if (item && item.kind === "user") return item.text;
|
|
2769
|
+
}
|
|
2770
|
+
return null;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// src/tui/App.tsx
|
|
2774
|
+
import { Text as Text13 } from "ink";
|
|
2775
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2776
|
+
function App(props) {
|
|
2777
|
+
const themeResult = useMemo2(
|
|
2778
|
+
() => loadUserTheme(props.config.theme),
|
|
2779
|
+
[props.config.theme]
|
|
2780
|
+
);
|
|
2781
|
+
return /* @__PURE__ */ jsx14(ThemeProvider, { initialTheme: themeResult.theme, children: /* @__PURE__ */ jsx14(AppInner, { ...props, themeWarning: themeResult.warning }) });
|
|
2782
|
+
}
|
|
2783
|
+
function AppInner(props) {
|
|
2784
|
+
const home = props.home ?? defaultMiotChatHome();
|
|
2785
|
+
const now = props.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
2786
|
+
const uuid = props.uuid ?? (() => randomUUID2());
|
|
2787
|
+
const ctx = useMemo2(() => ({ now, uuid }), [now, uuid]);
|
|
2788
|
+
const session = useSession({
|
|
2789
|
+
initial: {
|
|
2790
|
+
tenantId: props.config.tenantId,
|
|
2791
|
+
userId: props.config.userId,
|
|
2792
|
+
mode: props.config.mode,
|
|
2793
|
+
baseUrl: props.config.baseUrl,
|
|
2794
|
+
profileName: props.config.profileName,
|
|
2795
|
+
debug: props.config.debug
|
|
2796
|
+
},
|
|
2797
|
+
ctx,
|
|
2798
|
+
client: props.client
|
|
2799
|
+
});
|
|
2800
|
+
const { setTheme } = useTheme();
|
|
2801
|
+
const [extraItems, setExtraItems] = useState7([]);
|
|
2802
|
+
const [modal, setModalState] = useState7({ spec: null });
|
|
2803
|
+
const registry = useMemo2(() => {
|
|
2804
|
+
const reg = new SlashRegistry();
|
|
2805
|
+
reg.register(helpCommand).register(clearCommand).register(resetCommand).register(exitCommand).register(modeCommand).register(tenantCommand).register(userCommand).register(saveCommand).register(contextCommand).register(whoamiCommand).register(themeCommand).register(resumeCommand).register(exportCommand).register(runsCommand).register(approveCommand);
|
|
2806
|
+
return reg;
|
|
2807
|
+
}, []);
|
|
2808
|
+
const appendSystem = useCallback2(
|
|
2809
|
+
(text) => {
|
|
2810
|
+
setExtraItems((prev) => [
|
|
2811
|
+
...prev,
|
|
2812
|
+
{ kind: "system", id: uuid(), text, ts: now() }
|
|
2813
|
+
]);
|
|
2814
|
+
},
|
|
2815
|
+
[now, uuid]
|
|
2816
|
+
);
|
|
2817
|
+
const dispatchSlash = useCallback2(
|
|
2818
|
+
async (line) => {
|
|
2819
|
+
const parsed = parseSlash2(line);
|
|
2820
|
+
if (!parsed) return;
|
|
2821
|
+
const cmd = registry.get(parsed.name);
|
|
2822
|
+
if (!cmd) {
|
|
2823
|
+
appendSystem(`unknown command: /${parsed.name}`);
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
const slashCtx = {
|
|
2827
|
+
registry,
|
|
2828
|
+
session: session.state,
|
|
2829
|
+
now,
|
|
2830
|
+
uuid,
|
|
2831
|
+
home,
|
|
2832
|
+
client: props.client
|
|
2833
|
+
};
|
|
2834
|
+
try {
|
|
2835
|
+
const result = await cmd.handle(parsed.args, slashCtx);
|
|
2836
|
+
if (result.error) appendSystem(result.error);
|
|
2837
|
+
if (result.dispatch) session.dispatch(result.dispatch);
|
|
2838
|
+
if (result.output) {
|
|
2839
|
+
setExtraItems((prev) => [...prev, result.output]);
|
|
2840
|
+
}
|
|
2841
|
+
if (result.modal) setModalState({ spec: result.modal });
|
|
2842
|
+
if (result.abort && props.onExit) props.onExit();
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
appendSystem(
|
|
2845
|
+
`error in /${parsed.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
},
|
|
2849
|
+
[
|
|
2850
|
+
registry,
|
|
2851
|
+
session,
|
|
2852
|
+
now,
|
|
2853
|
+
uuid,
|
|
2854
|
+
home,
|
|
2855
|
+
props.client,
|
|
2856
|
+
props.onExit,
|
|
2857
|
+
appendSystem
|
|
2858
|
+
]
|
|
2859
|
+
);
|
|
2860
|
+
const handleSubmit = useCallback2(
|
|
2861
|
+
(text) => {
|
|
2862
|
+
if (text.trim().startsWith("/")) {
|
|
2863
|
+
void dispatchSlash(text);
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
void session.submit(text);
|
|
2867
|
+
},
|
|
2868
|
+
[dispatchSlash, session]
|
|
2869
|
+
);
|
|
2870
|
+
const closeModal = useCallback2(
|
|
2871
|
+
() => setModalState({ spec: null }),
|
|
2872
|
+
[]
|
|
2873
|
+
);
|
|
2874
|
+
const allItems = useMemo2(
|
|
2875
|
+
() => [...session.state.transcript, ...extraItems],
|
|
2876
|
+
[session.state.transcript, extraItems]
|
|
2877
|
+
);
|
|
2878
|
+
const modalSpec = modal.spec;
|
|
2879
|
+
const editorActive = modalSpec === null;
|
|
2880
|
+
return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
|
|
2881
|
+
props.themeWarning ? /* @__PURE__ */ jsx14(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx14(SystemNote, { text: `theme: ${props.themeWarning}` }) }) : null,
|
|
2882
|
+
/* @__PURE__ */ jsx14(
|
|
2883
|
+
Transcript,
|
|
2884
|
+
{
|
|
2885
|
+
items: allItems,
|
|
2886
|
+
isStreaming: isStreaming(session.state)
|
|
2887
|
+
}
|
|
2888
|
+
),
|
|
2889
|
+
modalSpec?.kind === "context" ? /* @__PURE__ */ jsx14(
|
|
2890
|
+
ContextModal,
|
|
2891
|
+
{
|
|
2892
|
+
session: session.state,
|
|
2893
|
+
lastRunId: session.state.currentRunId,
|
|
2894
|
+
onClose: closeModal
|
|
2895
|
+
}
|
|
2896
|
+
) : null,
|
|
2897
|
+
modalSpec?.kind === "resume" ? /* @__PURE__ */ jsx14(
|
|
2898
|
+
ResumePicker,
|
|
2899
|
+
{
|
|
2900
|
+
summaries: listSessions(home),
|
|
2901
|
+
onSelect: (id) => {
|
|
2902
|
+
const loaded = readSession(home, id);
|
|
2903
|
+
if (loaded) session.dispatch({ kind: "LOAD_SESSION", state: loaded });
|
|
2904
|
+
else appendSystem(`could not read session ${id}`);
|
|
2905
|
+
closeModal();
|
|
2906
|
+
},
|
|
2907
|
+
onCancel: closeModal
|
|
2908
|
+
}
|
|
2909
|
+
) : null,
|
|
2910
|
+
modalSpec?.kind === "theme" ? /* @__PURE__ */ jsx14(
|
|
2911
|
+
ThemePicker,
|
|
2912
|
+
{
|
|
2913
|
+
initialName: typeof modalSpec.payload?.name === "string" ? modalSpec.payload.name : void 0,
|
|
2914
|
+
onSelect: (name) => {
|
|
2915
|
+
const next = BUILTIN_THEMES[name];
|
|
2916
|
+
if (next) {
|
|
2917
|
+
setTheme(next);
|
|
2918
|
+
appendSystem(`theme: ${name}`);
|
|
2919
|
+
}
|
|
2920
|
+
closeModal();
|
|
2921
|
+
},
|
|
2922
|
+
onCancel: closeModal
|
|
2923
|
+
}
|
|
2924
|
+
) : null,
|
|
2925
|
+
modalSpec?.kind === "approval" && isApprovalsUiEnabled() && session.state.pendingApprovals.length > 0 ? /* @__PURE__ */ jsx14(
|
|
2926
|
+
ApprovalModal,
|
|
2927
|
+
{
|
|
2928
|
+
approval: session.state.pendingApprovals[0],
|
|
2929
|
+
onResolve: (decision, id) => {
|
|
2930
|
+
session.dispatch({ kind: "RESOLVE_APPROVAL", id, decision });
|
|
2931
|
+
closeModal();
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
) : null,
|
|
2935
|
+
modalSpec?.kind === "runs" ? /* @__PURE__ */ jsx14(
|
|
2936
|
+
RunsPickerWrapped,
|
|
2937
|
+
{
|
|
2938
|
+
state: session.state,
|
|
2939
|
+
onSelect: (runId) => {
|
|
2940
|
+
appendSystem(`replay run ${runId} \u2014 not yet implemented`);
|
|
2941
|
+
closeModal();
|
|
2942
|
+
},
|
|
2943
|
+
onCancel: closeModal
|
|
2944
|
+
}
|
|
2945
|
+
) : null,
|
|
2946
|
+
/* @__PURE__ */ jsx14(
|
|
2947
|
+
Header,
|
|
2948
|
+
{
|
|
2949
|
+
meta: session.state.meta,
|
|
2950
|
+
streaming: isStreaming(session.state),
|
|
2951
|
+
pendingApprovals: pendingApprovalCount(session.state),
|
|
2952
|
+
turns: turnCount(session.state),
|
|
2953
|
+
approxTokens: approxTokenCount(session.state),
|
|
2954
|
+
contextPercent: contextPercent(session.state),
|
|
2955
|
+
usageTotals: session.state.usageTotals
|
|
2956
|
+
}
|
|
2957
|
+
),
|
|
2958
|
+
/* @__PURE__ */ jsx14(Editor, { onSubmit: handleSubmit, isFocused: editorActive })
|
|
2959
|
+
] });
|
|
2960
|
+
}
|
|
2961
|
+
function RunsPickerWrapped(props) {
|
|
2962
|
+
const runs = summarizeRuns(props.state);
|
|
2963
|
+
return /* @__PURE__ */ jsx14(RunsPicker, { runs, onSelect: props.onSelect, onCancel: props.onCancel });
|
|
2964
|
+
}
|
|
2965
|
+
function SystemNote(props) {
|
|
2966
|
+
return /* @__PURE__ */ jsx14(Box12, { children: /* @__PURE__ */ jsx14(Text13, { dimColor: true, children: props.text }) });
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
// src/tui/runTui.ts
|
|
2970
|
+
function runTui(opts) {
|
|
2971
|
+
const instance = render(
|
|
2972
|
+
createElement(App, {
|
|
2973
|
+
config: opts.config,
|
|
2974
|
+
client: opts.client,
|
|
2975
|
+
onExit: () => instance.unmount()
|
|
2976
|
+
})
|
|
2977
|
+
);
|
|
2978
|
+
return {
|
|
2979
|
+
waitUntilExit: async () => {
|
|
2980
|
+
await instance.waitUntilExit();
|
|
2981
|
+
},
|
|
2982
|
+
unmount: () => instance.unmount()
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
// src/runMiotChat.ts
|
|
2987
|
+
function shouldUseTui(env, stdin, stdout) {
|
|
2988
|
+
if (env.MIOT_CHAT_NO_TUI === "1") return false;
|
|
2989
|
+
return Boolean(stdin.isTTY && stdout.isTTY);
|
|
2990
|
+
}
|
|
2991
|
+
async function runMiotChat(opts) {
|
|
2992
|
+
const env = opts.env ?? process.env;
|
|
2993
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
2994
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
2995
|
+
if (shouldUseTui(env, stdin, stdout)) {
|
|
2996
|
+
const handle = runTui({ config: opts.config, client: opts.client });
|
|
2997
|
+
await handle.waitUntilExit();
|
|
2998
|
+
return 0;
|
|
2999
|
+
}
|
|
3000
|
+
return runRepl({
|
|
3001
|
+
config: opts.config,
|
|
3002
|
+
client: opts.client,
|
|
3003
|
+
conversationId: opts.conversationId
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
// src/commands/ask.ts
|
|
3008
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3009
|
+
import {
|
|
3010
|
+
MiotHarnessApiError as MiotHarnessApiError2,
|
|
3011
|
+
createMiotHarnessClient
|
|
3012
|
+
} from "@microboxlabs/miot-harness-client";
|
|
3013
|
+
async function runAsk(opts) {
|
|
3014
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
3015
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
3016
|
+
const color = {
|
|
3017
|
+
noColor: opts.noColor ?? Boolean(process.env.NO_COLOR),
|
|
3018
|
+
isTTY: "isTTY" in stdout ? stdout.isTTY : false
|
|
3019
|
+
};
|
|
3020
|
+
const client = createMiotHarnessClient({
|
|
3021
|
+
baseUrl: opts.config.harnessBaseUrl,
|
|
3022
|
+
token: opts.config.token
|
|
3023
|
+
});
|
|
3024
|
+
const req = {
|
|
3025
|
+
message: opts.message,
|
|
3026
|
+
tenant_id: opts.config.tenantId,
|
|
3027
|
+
user_id: opts.config.userId,
|
|
3028
|
+
mode: opts.config.mode,
|
|
3029
|
+
conversation_id: opts.conversationId ?? randomUUID3(),
|
|
3030
|
+
...opts.config.debug ? { debug: true } : {}
|
|
3031
|
+
};
|
|
3032
|
+
stdout.write(
|
|
3033
|
+
`${dim(`miot-chat \u2192 ${opts.config.harnessBaseUrl} (${opts.config.mode} / ${opts.config.tenantId})`, color)}
|
|
3034
|
+
`
|
|
3035
|
+
);
|
|
3036
|
+
let state = initialState(color);
|
|
3037
|
+
let terminal = null;
|
|
3038
|
+
let failureMessage = "";
|
|
3039
|
+
let runId = "";
|
|
3040
|
+
const events = [];
|
|
3041
|
+
try {
|
|
3042
|
+
const { run_id } = await client.runs.create(req);
|
|
3043
|
+
runId = run_id;
|
|
3044
|
+
for await (const event of client.runs.stream(run_id)) {
|
|
3045
|
+
events.push(event);
|
|
3046
|
+
const r = renderEvent(state, event);
|
|
3047
|
+
state = r.state;
|
|
3048
|
+
if (r.output.length > 0) stdout.write(r.output);
|
|
3049
|
+
if (event.type === "run.completed") {
|
|
3050
|
+
terminal = "completed";
|
|
3051
|
+
break;
|
|
3052
|
+
}
|
|
3053
|
+
if (event.type === "run.failed") {
|
|
3054
|
+
terminal = "failed";
|
|
3055
|
+
failureMessage = event.message;
|
|
3056
|
+
break;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
} catch (e) {
|
|
3060
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3061
|
+
stderr.write(`${red(`error: ${msg}`, color)}
|
|
3062
|
+
`);
|
|
3063
|
+
return e instanceof MiotHarnessApiError2 ? 1 : 2;
|
|
3064
|
+
}
|
|
3065
|
+
if (terminal === "completed") {
|
|
3066
|
+
let answer = state.pendingAnswer;
|
|
3067
|
+
try {
|
|
3068
|
+
const record = await client.runs.get(runId);
|
|
3069
|
+
answer = record.answer;
|
|
3070
|
+
} catch {
|
|
3071
|
+
}
|
|
3072
|
+
const finalRender = renderAuthoritativeAnswer(state, answer);
|
|
3073
|
+
stdout.write(finalRender.output);
|
|
3074
|
+
return 0;
|
|
3075
|
+
}
|
|
3076
|
+
if (terminal === "failed") {
|
|
3077
|
+
const failureRender = renderRunFailure(state, failureMessage);
|
|
3078
|
+
stdout.write(failureRender.output);
|
|
3079
|
+
return 1;
|
|
3080
|
+
}
|
|
3081
|
+
stderr.write(
|
|
3082
|
+
`${red("stream ended without a terminal event", color)}
|
|
3083
|
+
`
|
|
3084
|
+
);
|
|
3085
|
+
return 2;
|
|
3086
|
+
}
|
|
3087
|
+
export {
|
|
3088
|
+
DEFAULT_CONFIG,
|
|
3089
|
+
getConfigDir,
|
|
3090
|
+
getConfigPath,
|
|
3091
|
+
readConfig,
|
|
3092
|
+
resolveConfig,
|
|
3093
|
+
runAsk,
|
|
3094
|
+
runMiotChat,
|
|
3095
|
+
shouldUseTui,
|
|
3096
|
+
writeConfig
|
|
3097
|
+
};
|