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