@pentoshi/clai 2.0.5 → 2.0.8

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/tui/App.js CHANGED
@@ -1,24 +1,29 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text, useApp, useInput, useStdin } from "ink";
3
- import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState, } from "react";
4
4
  import { providerIds } from "../types.js";
5
5
  import { assertProvider } from "../llm/provider.js";
6
6
  import { getProvider } from "../llm/router.js";
7
- import { envValue, getProviderSecret, getSearchProviderKey, listProviderStatuses, maskSecret, setProviderSecret } from "../store/keys.js";
8
- import { searchProviderIds } from "../tools/web/types.js";
9
- import { modelSupportsThinking, modelSupportsVision } from "../llm/capabilities.js";
7
+ import { envValue, getProviderSecret, getSearchProviderKey, listProviderStatuses, maskSecret, setProviderSecret, setSecret, } from "../store/keys.js";
8
+ import { setActiveSearchProvider } from "../store/config.js";
9
+ import { searchProviderIds, } from "../tools/web/types.js";
10
+ import { assertSearchProvider, searchProviders, } from "../tools/web/providers/provider.js";
11
+ import "../tools/web/providers/duckduckgo.js";
12
+ import "../tools/web/providers/brave.js";
13
+ import "../tools/web/providers/tavily.js";
14
+ import { modelSupportsThinking, modelSupportsVision, } from "../llm/capabilities.js";
10
15
  import { getConfig, getProviderModel, setDefaultMode, setDefaultProvider, setProviderModel, setThinking, updateConfig, } from "../store/config.js";
11
16
  import { estimateMessagesTokens } from "../agent/context-manager.js";
12
- import { clearAllHistory, getSession, listSessions, saveSession } from "../store/history.js";
17
+ import { clearAllHistory, getSession, listSessions, saveSession, upsertSession, } from "../store/history.js";
13
18
  import { safeCwd } from "../os/cwd.js";
14
19
  import { resolve } from "node:path";
15
20
  import { existsSync } from "node:fs";
16
21
  import { readFile } from "node:fs/promises";
17
- import { expandMentions, findFileSuggestions, getMentionQuery, loadImageAttachments } from "../ui/mentions.js";
22
+ import { expandMentions, findFileSuggestions, getMentionQuery, loadImageAttachments, } from "../ui/mentions.js";
18
23
  import { deletePlan, loadPlan, savePlan } from "../store/plan.js";
19
24
  import { renderPlanDocument } from "../ui/plan-pane.js";
20
- import { getSlashCommandSuggestions, isKnownSlashCommand, knownModels, slashCommands } from "../repl.js";
21
- import { initialState, reducer, serializeTranscriptForCompaction } from "./state.js";
25
+ import { getSlashCommandSuggestions, isKnownSlashCommand, knownModels, slashCommands, } from "../repl.js";
26
+ import { initialState, reducer, serializeTranscriptForCompaction, } from "./state.js";
22
27
  import { renderTranscriptLines } from "./render-lines.js";
23
28
  import { createTuiConfirmPort } from "./confirm.js";
24
29
  import { useAgentRunner } from "./hooks/useAgentRunner.js";
@@ -31,10 +36,10 @@ import { JobsPanel } from "./components/JobsPanel.js";
31
36
  import { PickerPanel } from "./components/PickerPanel.js";
32
37
  import { SecretInputPanel } from "./components/SecretInputPanel.js";
33
38
  import { clearArtifacts, clearAuditLogs } from "../store/logs.js";
34
- import { addScopeTargets, clearScope, loadScope, saveScope } from "../store/scope.js";
39
+ import { addScopeTargets, clearScope, loadScope, saveScope, } from "../store/scope.js";
35
40
  import { formatKeyStatus } from "./format-keys.js";
36
41
  import { shouldStoreInPromptHistory } from "./input-history.js";
37
- import { isMouseReport, mouseWheelDirection, stripMouseReports } from "./mouse.js";
42
+ import { DISABLE_MOUSE_REPORTING, ENABLE_MOUSE_REPORTING, isMouseReport, mouseWheelDirection, stripMouseReports, } from "./mouse.js";
38
43
  const IMPLEMENT_PROMPT = "I approve the plan. Execute it now in STRICT ORDER. Task 1 (explore) is ALREADY COMPLETE from the planning phase — " +
39
44
  "do NOT re-list or re-read the directory. Start with the FIRST pending task that still needs implementation work. " +
40
45
  "For each task: call task.update {taskId, state:'in_progress'} → do the real work → VERIFY it succeeded → " +
@@ -44,7 +49,7 @@ const IMPLEMENT_PROMPT = "I approve the plan. Execute it now in STRICT ORDER. Ta
44
49
  "Do NOT call web.search — you already know everything needed. " +
45
50
  "Run real commands (installs, servers, verification) — do not claim anything ran without a successful tool call.";
46
51
  const MAX_FILE_SUGGESTIONS = 6;
47
- export function App({ version, initialMode, provider: initialProvider, initialModel, noHistory = false }) {
52
+ export function App({ version, initialMode, provider: initialProvider, initialModel, noHistory = false, }) {
48
53
  const { exit } = useApp();
49
54
  const { stdin } = useStdin();
50
55
  const { columns: cols, rows } = useTerminalSize();
@@ -60,19 +65,34 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
60
65
  const secretResolver = useRef();
61
66
  const [scroll, setScroll] = useState(0); // lines scrolled up from bottom
62
67
  const [compacting, setCompacting] = useState(false);
68
+ const [mouseMode, setMouseMode] = useState(true);
63
69
  const history = useRef([]);
64
70
  const historyIdx = useRef(-1);
65
71
  const historyDraft = useRef("");
66
72
  const lastCtrlC = useRef(0);
67
73
  const jobs = useJobs(overlay.kind === "jobs");
68
74
  const spinner = useSpinner(state.status.running);
75
+ const autosaveTimer = useRef(undefined);
76
+ const latestItems = useRef(state.items);
77
+ latestItems.current = state.items;
78
+ useEffect(() => {
79
+ if (!process.stdout.isTTY)
80
+ return;
81
+ process.stdout.write(mouseMode ? ENABLE_MOUSE_REPORTING : DISABLE_MOUSE_REPORTING);
82
+ return () => {
83
+ process.stdout.write(DISABLE_MOUSE_REPORTING);
84
+ };
85
+ }, [mouseMode]);
69
86
  // ── Confirm port → in-app modal ────────────────────────────────────────────
70
87
  const confirmController = useMemo(() => createTuiConfirmPort(), []);
71
88
  const confirmResolver = useRef(undefined);
72
89
  useEffect(() => {
73
90
  confirmController.setHandler((req) => new Promise((res) => {
74
91
  confirmResolver.current = res;
75
- dispatch({ type: "event", event: { type: "confirm-request", id: "c", ...req } });
92
+ dispatch({
93
+ type: "event",
94
+ event: { type: "confirm-request", id: "c", ...req },
95
+ });
76
96
  }));
77
97
  }, [confirmController]);
78
98
  const ctxRef = useRef({ mode, provider, model });
@@ -91,11 +111,39 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
91
111
  void (async () => {
92
112
  const messages = runner.getMessages();
93
113
  if (!noHistory && !getConfig().privateMode && messages.length > 0) {
94
- await saveSession(messages, undefined, state.items).catch(() => undefined);
114
+ await upsertSession(runner.getSession().sessionId, messages, undefined, state.items).catch(() => undefined);
95
115
  }
96
116
  exit();
97
117
  })();
98
118
  }, [exit, noHistory, runner, state.items]);
119
+ useEffect(() => {
120
+ if (noHistory || getConfig().privateMode)
121
+ return;
122
+ if (state.items.length === 0)
123
+ return;
124
+ if (autosaveTimer.current)
125
+ clearTimeout(autosaveTimer.current);
126
+ autosaveTimer.current = setTimeout(() => {
127
+ const messages = runner.getMessages();
128
+ if (messages.length === 0 && state.items.length === 0)
129
+ return;
130
+ void upsertSession(runner.getSession().sessionId, messages, undefined, state.items).catch(() => undefined);
131
+ }, 250);
132
+ return () => {
133
+ if (autosaveTimer.current)
134
+ clearTimeout(autosaveTimer.current);
135
+ };
136
+ }, [noHistory, runner, state.items]);
137
+ useEffect(() => () => {
138
+ if (autosaveTimer.current)
139
+ clearTimeout(autosaveTimer.current);
140
+ const messages = runner.getMessages();
141
+ if (!noHistory &&
142
+ !getConfig().privateMode &&
143
+ (messages.length > 0 || latestItems.current.length > 0)) {
144
+ void upsertSession(runner.getSession().sessionId, messages, undefined, latestItems.current).catch(() => undefined);
145
+ }
146
+ }, [noHistory, runner]);
99
147
  const startTurn = useCallback(async (display, modelInput, images) => {
100
148
  setScroll(0);
101
149
  dispatch({ type: "submit", text: display });
@@ -105,11 +153,15 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
105
153
  const vision = modelSupportsVision(provider, model);
106
154
  const expansion = expandMentions(rawText, safeCwd(), vision);
107
155
  const images = vision ? loadImageAttachments(rawText, safeCwd()) : [];
108
- const modelInput = expansion.contextBlock ? `${rawText}\n\n${expansion.contextBlock}` : rawText;
156
+ const modelInput = expansion.contextBlock
157
+ ? `${rawText}\n\n${expansion.contextBlock}`
158
+ : rawText;
109
159
  void startTurn(rawText, modelInput, images);
110
160
  }, [provider, model, startTurn]);
111
161
  useEffect(() => {
112
- if (!state.status.running && state.queued.length > 0 && !runner.isRunning()) {
162
+ if (!state.status.running &&
163
+ state.queued.length > 0 &&
164
+ !runner.isRunning()) {
113
165
  const next = state.queued[0];
114
166
  dispatch({ type: "dequeue" });
115
167
  beginTurn(next);
@@ -134,17 +186,29 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
134
186
  const session = runner.getSession();
135
187
  const plan = await loadPlan(session.sessionId).catch(() => undefined);
136
188
  if (!plan) {
137
- dispatch({ type: "notice", level: "info", text: "no plan to implement — ask clai to plan a multi-step task first" });
189
+ dispatch({
190
+ type: "notice",
191
+ level: "info",
192
+ text: "no plan to implement — ask clai to plan a multi-step task first",
193
+ });
138
194
  return;
139
195
  }
140
196
  if (plan.tasks.every((t) => t.state === "done")) {
141
- dispatch({ type: "notice", level: "info", text: "this plan is already complete ✓" });
197
+ dispatch({
198
+ type: "notice",
199
+ level: "info",
200
+ text: "this plan is already complete ✓",
201
+ });
142
202
  return;
143
203
  }
144
204
  plan.status = "approved";
145
205
  await savePlan(plan).catch(() => undefined);
146
206
  session.planApproved.value = true;
147
- dispatch({ type: "notice", level: "info", text: "✦ plan approved — executing it now" });
207
+ dispatch({
208
+ type: "notice",
209
+ level: "info",
210
+ text: "✦ plan approved — executing it now",
211
+ });
148
212
  void startTurn("/implement", IMPLEMENT_PROMPT);
149
213
  }, [runner, startTurn]);
150
214
  const chooseModel = useCallback(async () => {
@@ -173,7 +237,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
173
237
  setOverlay({
174
238
  kind: "picker",
175
239
  title: `Models · ${provider}`,
176
- options: models.map((value) => ({ value, label: value, active: value === model })),
240
+ options: models.map((value) => ({
241
+ value,
242
+ label: value,
243
+ active: value === model,
244
+ })),
177
245
  onSelect: (value) => {
178
246
  setModel(value);
179
247
  setProviderModel(provider, value);
@@ -183,18 +251,28 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
183
251
  });
184
252
  }, [model, provider]);
185
253
  const activateProvider = useCallback(async (next) => {
186
- const configured = next === "ollama" || Boolean(envValue(next)) || Boolean((await getProviderSecret(next)).value);
254
+ const configured = next === "ollama" ||
255
+ Boolean(envValue(next)) ||
256
+ Boolean((await getProviderSecret(next)).value);
187
257
  if (!configured) {
188
258
  const key = await requestSecret({
189
259
  title: `${next} API key`,
190
260
  prompt: `No API key is configured for ${next}. Enter it now to activate this provider.`,
191
261
  });
192
262
  if (!key) {
193
- dispatch({ type: "notice", level: "warn", text: `provider unchanged · ${next} needs an API key` });
263
+ dispatch({
264
+ type: "notice",
265
+ level: "warn",
266
+ text: `provider unchanged · ${next} needs an API key`,
267
+ });
194
268
  return;
195
269
  }
196
270
  if (!getProvider(next).validateKey(key)) {
197
- dispatch({ type: "notice", level: "warn", text: `invalid API key format for ${next}` });
271
+ dispatch({
272
+ type: "notice",
273
+ level: "warn",
274
+ text: `invalid API key format for ${next}`,
275
+ });
198
276
  return;
199
277
  }
200
278
  await setProviderSecret(next, key);
@@ -204,7 +282,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
204
282
  setProvider(next);
205
283
  setModel(nextModel);
206
284
  setOverlay({ kind: "none" });
207
- dispatch({ type: "notice", level: "info", text: `provider → ${next} · model → ${nextModel}` });
285
+ dispatch({
286
+ type: "notice",
287
+ level: "info",
288
+ text: `provider → ${next} · model → ${nextModel}`,
289
+ });
208
290
  }, [requestSecret]);
209
291
  const chooseProvider = useCallback(() => {
210
292
  setOverlay({
@@ -222,6 +304,54 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
222
304
  },
223
305
  });
224
306
  }, [activateProvider, provider]);
307
+ const activateSearchProvider = useCallback(async (next) => {
308
+ const adapter = searchProviders[next];
309
+ if (adapter?.needsApiKey) {
310
+ const current = await getSearchProviderKey(next);
311
+ if (!current.value) {
312
+ const key = await requestSecret({
313
+ title: `${next} search API key`,
314
+ prompt: `No API key is configured for ${adapter.displayName}. Enter it now to use this search provider.`,
315
+ });
316
+ if (!key) {
317
+ dispatch({
318
+ type: "notice",
319
+ level: "warn",
320
+ text: `search provider unchanged · ${next} needs an API key`,
321
+ });
322
+ return;
323
+ }
324
+ await setSecret("search", next, key);
325
+ }
326
+ }
327
+ setActiveSearchProvider(next);
328
+ setOverlay({ kind: "none" });
329
+ dispatch({
330
+ type: "notice",
331
+ level: "info",
332
+ text: `search provider → ${next}`,
333
+ });
334
+ }, [requestSecret]);
335
+ const chooseSearchProvider = useCallback(() => {
336
+ const active = getConfig().activeSearchProvider;
337
+ setOverlay({
338
+ kind: "picker",
339
+ title: "Search providers",
340
+ options: searchProviderIds.map((id) => {
341
+ const adapter = searchProviders[id];
342
+ return {
343
+ value: id,
344
+ label: id === active ? `${id} · active` : id,
345
+ description: adapter?.needsApiKey
346
+ ? `${adapter.displayName} · API key required`
347
+ : `${adapter?.displayName ?? id} · keyless`,
348
+ };
349
+ }),
350
+ onSelect: (value) => {
351
+ void activateSearchProvider(assertSearchProvider(value));
352
+ },
353
+ });
354
+ }, [activateSearchProvider]);
225
355
  const setReasoning = useCallback((value) => {
226
356
  if (value === "off" || value === "none")
227
357
  setThinking({ enabled: false });
@@ -230,7 +360,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
230
360
  dispatch({
231
361
  type: "notice",
232
362
  level: "info",
233
- text: value === "off" || value === "none" ? "thinking → off" : `thinking → ${value}`,
363
+ text: value === "off" || value === "none"
364
+ ? "thinking → off"
365
+ : `thinking → ${value}`,
234
366
  });
235
367
  }, []);
236
368
  const chooseReasoning = useCallback(() => {
@@ -310,7 +442,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
310
442
  if (!plan)
311
443
  info("no active plan yet");
312
444
  else
313
- setOverlay({ kind: "pager", title: "Plan", body: renderPlanDocument(plan) });
445
+ setOverlay({
446
+ kind: "pager",
447
+ title: "Plan",
448
+ body: renderPlanDocument(plan),
449
+ });
314
450
  })();
315
451
  return true;
316
452
  case "/jobs":
@@ -318,11 +454,22 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
318
454
  return true;
319
455
  case "/output": {
320
456
  const outputs = state.items.filter((item) => item.kind === "tool" && Boolean(item.output));
457
+ if (!arg) {
458
+ if (outputs.length)
459
+ dispatch({ type: "toggle-output" });
460
+ else
461
+ info("no tool output yet");
462
+ return true;
463
+ }
321
464
  if (arg === "list" || arg === "ls") {
322
- info(outputs.length ? outputs.map((item) => `${item.id} · ${item.name}`).join("\n") : "no tool output yet");
465
+ info(outputs.length
466
+ ? outputs.map((item) => `${item.id} · ${item.name}`).join("\n")
467
+ : "no tool output yet");
323
468
  return true;
324
469
  }
325
- const selectedOutput = arg && arg !== "last" ? outputs.find((item) => item.id === arg) : lastToolOutput();
470
+ const selectedOutput = arg && arg !== "last"
471
+ ? outputs.find((item) => item.id === arg)
472
+ : lastToolOutput();
326
473
  if (!selectedOutput)
327
474
  info(arg ? `no tool output: ${arg}` : "no tool output yet");
328
475
  else
@@ -337,7 +484,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
337
484
  {
338
485
  const options = knownModels[provider] ?? [];
339
486
  const index = Number.parseInt(arg, 10);
340
- const nextModel = Number.isInteger(index) && index >= 1 && index <= options.length ? options[index - 1] : arg;
487
+ const nextModel = Number.isInteger(index) && index >= 1 && index <= options.length
488
+ ? options[index - 1]
489
+ : arg;
341
490
  setModel(nextModel);
342
491
  setProviderModel(provider, nextModel);
343
492
  info(`model → ${nextModel}`);
@@ -357,6 +506,39 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
357
506
  warn(`unknown provider: ${arg}`);
358
507
  }
359
508
  return true;
509
+ case "/search":
510
+ case "/search-provider":
511
+ if (!arg || arg === "list" || arg === "ls") {
512
+ chooseSearchProvider();
513
+ return true;
514
+ }
515
+ try {
516
+ const next = assertSearchProvider(arg);
517
+ void activateSearchProvider(next);
518
+ }
519
+ catch {
520
+ warn(`unknown search provider: ${arg}`);
521
+ }
522
+ return true;
523
+ case "/mouse": {
524
+ const normalized = arg.toLowerCase();
525
+ if (!normalized) {
526
+ info(`mouse=${mouseMode ? "on" : "off"} · on: touchpad scrolls chat; off: native select/copy`);
527
+ return true;
528
+ }
529
+ if (/^(on|true|1|enable)$/i.test(normalized)) {
530
+ setMouseMode(true);
531
+ info("mouse=on · touchpad scrolls chat; use Option/Shift selection if your terminal requires it");
532
+ return true;
533
+ }
534
+ if (/^(off|false|0|disable)$/i.test(normalized)) {
535
+ setMouseMode(false);
536
+ info("mouse=off · native select/copy restored; use PageUp/PageDown/Ctrl+U/Ctrl+D or j/k to scroll chat");
537
+ return true;
538
+ }
539
+ warn("usage: /mouse [on|off]");
540
+ return true;
541
+ }
360
542
  case "/variants":
361
543
  case "/reasoning": {
362
544
  if (!arg) {
@@ -400,7 +582,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
400
582
  case "/allow":
401
583
  if (!arg || arg === "list" || arg === "ls") {
402
584
  const list = [...runner.getSession().allow];
403
- info(list.length ? `allowed: ${list.join(", ")}` : "no session allowances");
585
+ info(list.length
586
+ ? `allowed: ${list.join(", ")}`
587
+ : "no session allowances");
404
588
  return true;
405
589
  }
406
590
  runner.getSession().allow.add(arg);
@@ -424,14 +608,17 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
424
608
  setCompacting(true);
425
609
  info("compacting conversation…");
426
610
  const fullSession = serializeTranscriptForCompaction(state.items);
427
- void runner.compact(fullSession).then((result) => {
611
+ void runner
612
+ .compact(fullSession)
613
+ .then((result) => {
428
614
  if (result.after === result.before) {
429
615
  info("nothing to compact yet — more than 8 recent messages are required");
430
616
  }
431
617
  else {
432
618
  info(`compacted ${result.before} → ${result.after} messages · ~${result.beforeTokens.toLocaleString()} → ~${result.afterTokens.toLocaleString()} tokens${result.summarized ? "" : " · local fallback"}`);
433
619
  }
434
- }).catch((error) => warn(`compaction failed: ${error instanceof Error ? error.message : String(error)}`))
620
+ })
621
+ .catch((error) => warn(`compaction failed: ${error instanceof Error ? error.message : String(error)}`))
435
622
  .finally(() => setCompacting(false));
436
623
  return true;
437
624
  }
@@ -458,12 +645,16 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
458
645
  kind: "picker",
459
646
  title: "Session history",
460
647
  options: [
461
- ...(currentMessages.length ? [{
462
- value: "__current__",
463
- label: "Current session",
464
- description: `${currentMessages.length} messages · active now`,
465
- active: true,
466
- }] : []),
648
+ ...(currentMessages.length
649
+ ? [
650
+ {
651
+ value: "__current__",
652
+ label: "Current session",
653
+ description: `${currentMessages.length} messages · active now`,
654
+ active: true,
655
+ },
656
+ ]
657
+ : []),
467
658
  ...sessions.map((session) => ({
468
659
  value: session.id,
469
660
  label: session.name ?? session.id,
@@ -484,7 +675,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
484
675
  }
485
676
  runner.setMessages(session.messages);
486
677
  setOverlay({ kind: "none" });
487
- dispatch({ type: "load-history", messages: session.messages, transcript: session.transcript });
678
+ dispatch({
679
+ type: "load-history",
680
+ messages: session.messages,
681
+ transcript: session.transcript,
682
+ });
488
683
  setScroll(0);
489
684
  info(`session resumed · ${session.transcript?.length ?? session.messages.length} items`);
490
685
  })();
@@ -518,11 +713,16 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
518
713
  }
519
714
  if (sub === "show" || sub === "list" || sub === "ls") {
520
715
  const scope = await loadScope();
521
- info(scope ? `scope: ${scope.name ?? "unnamed"} · ${scope.authorizedTargets.join(", ")}` : "no engagement scope configured");
716
+ info(scope
717
+ ? `scope: ${scope.name ?? "unnamed"} · ${scope.authorizedTargets.join(", ")}`
718
+ : "no engagement scope configured");
522
719
  return;
523
720
  }
524
721
  if (sub === "add") {
525
- const targets = parts.join(" ").split(/[\s,]+/).filter(Boolean);
722
+ const targets = parts
723
+ .join(" ")
724
+ .split(/[\s,]+/)
725
+ .filter(Boolean);
526
726
  if (!targets.length) {
527
727
  warn("usage: /scope add <target1,target2>");
528
728
  return;
@@ -532,12 +732,18 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
532
732
  return;
533
733
  }
534
734
  if (sub === "new" || sub === "set") {
535
- const targets = parts.join(" ").split(/[\s,]+/).filter(Boolean);
735
+ const targets = parts
736
+ .join(" ")
737
+ .split(/[\s,]+/)
738
+ .filter(Boolean);
536
739
  if (!targets.length) {
537
740
  warn("usage: /scope new <target1,target2>");
538
741
  return;
539
742
  }
540
- await saveScope({ authorizedTargets: targets, createdAt: new Date().toISOString() });
743
+ await saveScope({
744
+ authorizedTargets: targets,
745
+ createdAt: new Date().toISOString(),
746
+ });
541
747
  info(`scope created · ${targets.join(", ")}`);
542
748
  return;
543
749
  }
@@ -577,7 +783,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
577
783
  return;
578
784
  }
579
785
  if (sub === "clear-all") {
580
- const [historyResult, logResult, artifactResult] = await Promise.all([clearAllHistory(), clearAuditLogs(), clearArtifacts()]);
786
+ const [historyResult, logResult, artifactResult] = await Promise.all([
787
+ clearAllHistory(),
788
+ clearAuditLogs(),
789
+ clearArtifacts(),
790
+ ]);
581
791
  info(`cleared history (${historyResult.detail || "ok"}), logs (${logResult.removed}), artifacts (${artifactResult.removed})`);
582
792
  return;
583
793
  }
@@ -616,10 +826,16 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
616
826
  active: id === activeSearch,
617
827
  configured: keyless || Boolean(secret.value),
618
828
  source: keyless ? "keyless" : secret.source,
619
- maskedKey: secret.value ? maskSecret(secret.value) : undefined,
829
+ maskedKey: secret.value
830
+ ? maskSecret(secret.value)
831
+ : undefined,
620
832
  };
621
833
  }));
622
- setOverlay({ kind: "pager", title: "Credential status", body: formatKeyStatus(llm, search) });
834
+ setOverlay({
835
+ kind: "pager",
836
+ title: "Credential status",
837
+ body: formatKeyStatus(llm, search),
838
+ });
623
839
  })().catch((error) => warn(`could not read keys: ${error instanceof Error ? error.message : String(error)}`));
624
840
  return true;
625
841
  case "/set":
@@ -649,9 +865,28 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
649
865
  case "/quit":
650
866
  exitTui();
651
867
  return true;
652
- default: return false;
868
+ default:
869
+ return false;
653
870
  }
654
- }, [activateProvider, chooseModel, chooseProvider, chooseReasoning, compacting, exitTui, noHistory, provider, runner, runImplement, lastToolOutput, openToolOutput, setReasoning, state.items]);
871
+ }, [
872
+ activateProvider,
873
+ activateSearchProvider,
874
+ chooseModel,
875
+ chooseProvider,
876
+ chooseReasoning,
877
+ chooseSearchProvider,
878
+ compacting,
879
+ exitTui,
880
+ mouseMode,
881
+ noHistory,
882
+ provider,
883
+ runner,
884
+ runImplement,
885
+ lastToolOutput,
886
+ openToolOutput,
887
+ setReasoning,
888
+ state.items,
889
+ ]);
655
890
  const submitText = useCallback((text) => {
656
891
  const trimmed = text.trim();
657
892
  if (!trimmed)
@@ -666,7 +901,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
666
901
  // token is an actual clai command; file paths remain normal prompts.
667
902
  if (trimmed.startsWith("/") && isKnownSlashCommand(trimmed)) {
668
903
  if (!handleLocalSlash(trimmed))
669
- dispatch({ type: "notice", level: "warn", text: `unknown command: ${trimmed}` });
904
+ dispatch({
905
+ type: "notice",
906
+ level: "warn",
907
+ text: `unknown command: ${trimmed}`,
908
+ });
670
909
  return;
671
910
  }
672
911
  if (shouldStoreInPromptHistory(trimmed))
@@ -707,7 +946,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
707
946
  const statusH = state.pendingConfirm ? 6 : secretRequest ? 7 : 1;
708
947
  const composerH = 3;
709
948
  const maxMenuRows = Math.max(3, usableRows - headerH - statusH - composerH - 3);
710
- const menuH = slashMenuOpen ? Math.min(suggestions.length, maxMenuRows) : fileMenuOpen ? fileSuggestions.length : 0;
949
+ const menuH = slashMenuOpen
950
+ ? Math.min(suggestions.length, maxMenuRows)
951
+ : fileMenuOpen
952
+ ? fileSuggestions.length
953
+ : 0;
711
954
  const viewportH = Math.max(3, usableRows - headerH - statusH - composerH - menuH);
712
955
  const slashWindowStart = slashMenuOpen
713
956
  ? Math.min(Math.max(0, selected - Math.floor(menuH / 2)), Math.max(0, suggestions.length - menuH))
@@ -741,7 +984,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
741
984
  setScroll((value) => Math.max(0, value - 3));
742
985
  };
743
986
  stdin.on("data", onData);
744
- return () => { stdin.off("data", onData); };
987
+ return () => {
988
+ stdin.off("data", onData);
989
+ };
745
990
  }, [stdin, modalActive, maxOffset]);
746
991
  // ── Key handling ────────────────────────────────────────────────────────────
747
992
  useInput((ch, key) => {
@@ -756,9 +1001,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
756
1001
  return;
757
1002
  }
758
1003
  if (key.ctrl && ch === "o") {
759
- const last = lastToolOutput();
760
- if (last)
761
- void openToolOutput(last);
1004
+ const hasOutput = state.items.some((item) => item.kind === "tool" && Boolean(item.output));
1005
+ if (hasOutput)
1006
+ dispatch({ type: "toggle-output" });
762
1007
  else
763
1008
  dispatch({ type: "notice", level: "info", text: "no tool output yet" });
764
1009
  return;
@@ -767,9 +1012,17 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
767
1012
  void (async () => {
768
1013
  const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
769
1014
  if (plan)
770
- setOverlay({ kind: "pager", title: "Plan", body: renderPlanDocument(plan) });
1015
+ setOverlay({
1016
+ kind: "pager",
1017
+ title: "Plan",
1018
+ body: renderPlanDocument(plan),
1019
+ });
771
1020
  else
772
- dispatch({ type: "notice", level: "info", text: "no active plan yet" });
1021
+ dispatch({
1022
+ type: "notice",
1023
+ level: "info",
1024
+ text: "no active plan yet",
1025
+ });
773
1026
  })();
774
1027
  return;
775
1028
  }
@@ -803,7 +1056,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
803
1056
  return;
804
1057
  }
805
1058
  if (key.escape) {
806
- if (runner.isRunning())
1059
+ if (state.outputExpanded)
1060
+ dispatch({ type: "toggle-output" });
1061
+ else if (runner.isRunning())
807
1062
  runner.abort();
808
1063
  else if (input) {
809
1064
  setInput("");
@@ -870,7 +1125,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
870
1125
  return;
871
1126
  if (historyIdx.current < 0)
872
1127
  historyDraft.current = input;
873
- const idx = historyIdx.current < 0 ? history.current.length - 1 : Math.max(0, historyIdx.current - 1);
1128
+ const idx = historyIdx.current < 0
1129
+ ? history.current.length - 1
1130
+ : Math.max(0, historyIdx.current - 1);
874
1131
  historyIdx.current = idx;
875
1132
  const v = history.current[idx] ?? "";
876
1133
  setInput(v);
@@ -930,17 +1187,31 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
930
1187
  const before = input.slice(0, cursor);
931
1188
  const at = input.slice(cursor, cursor + 1) || " ";
932
1189
  const after = input.slice(cursor + 1);
933
- return (_jsxs(Box, { flexDirection: "column", width: cols, height: usableRows, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", marginX: 2, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { backgroundColor: "#2563EB", color: "#FFFFFF", bold: true, children: " \u25C6 clai " }), _jsxs(Text, { color: "#94A3B8", children: [" v", version] })] }), _jsxs(Text, { children: [_jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ${mode.toUpperCase()} ` }), _jsx(Text, { color: "#94A3B8", children: " MODE" })] })] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "green", children: provider }), _jsx(Text, { dimColor: true, children: " / " }), _jsx(Text, { color: "cyan", children: model }), _jsx(Text, { dimColor: true, children: ` · ${safeCwd()}` }), offset > 0 ? _jsx(Text, { color: "yellow", children: ` · ▲ ${offset}` }) : null] })] }), overlay.kind === "pager" ? (_jsx(Pager, { title: overlay.title, body: overlay.body, height: viewportH, onClose: closeOverlay })) : overlay.kind === "jobs" ? (_jsx(JobsPanel, { jobs: jobs, onClose: closeOverlay })) : overlay.kind === "picker" ? (_jsx(PickerPanel, { title: overlay.title, options: overlay.options, height: viewportH, onSelect: overlay.onSelect, onClose: closeOverlay })) : (_jsx(Box, { flexDirection: "column", height: viewportH, children: visible.map((line, i) => (_jsx(Text, { wrap: "truncate-end", children: line === "" ? " " : line }, i))) })), slashMenuOpen && !modalActive
1190
+ return (_jsxs(Box, { flexDirection: "column", width: cols, height: usableRows, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", marginX: 2, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsxs(Text, { backgroundColor: "#2563EB", color: "#FFFFFF", bold: true, children: [" ", "\u25C6 clai", " "] }), _jsxs(Text, { color: "#94A3B8", children: [" v", version] })] }), _jsxs(Text, { children: [_jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ${mode.toUpperCase()} ` }), _jsx(Text, { color: "#94A3B8", children: " MODE" })] })] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "green", children: provider }), _jsx(Text, { dimColor: true, children: " / " }), _jsx(Text, { color: "cyan", children: model }), _jsx(Text, { dimColor: true, children: ` · ${safeCwd()}` }), offset > 0 ? _jsx(Text, { color: "yellow", children: ` · ▲ ${offset}` }) : null] })] }), overlay.kind === "pager" ? (_jsx(Pager, { title: overlay.title, body: overlay.body, height: viewportH, onClose: closeOverlay })) : overlay.kind === "jobs" ? (_jsx(JobsPanel, { jobs: jobs, onClose: closeOverlay })) : overlay.kind === "picker" ? (_jsx(PickerPanel, { title: overlay.title, options: overlay.options, height: viewportH, onSelect: overlay.onSelect, onClose: closeOverlay })) : (_jsx(Box, { flexDirection: "column", height: viewportH, children: visible.map((line, i) => (_jsx(Text, { wrap: "wrap", children: line === "" ? " " : line }, i))) })), slashMenuOpen && !modalActive
934
1191
  ? visibleSlashSuggestions.map((cmd, i) => {
935
1192
  const absoluteIndex = slashWindowStart + i;
936
- return (_jsxs(Text, { wrap: "truncate-end", backgroundColor: absoluteIndex === selected ? "#2563EB" : absoluteIndex % 2 === 0 ? "#1E293B" : "#0F172A", children: [_jsxs(Text, { color: absoluteIndex === selected ? "#FFFFFF" : "#67E8F9", bold: true, children: [absoluteIndex === selected ? " ❯ " : " ", cmd.command.padEnd(14)] }), cmd.usage ? _jsxs(Text, { color: absoluteIndex === selected ? "#FFFFFF" : "#CBD5E1", children: [cmd.usage, " "] }) : null, _jsxs(Text, { color: "#F8FAFC", children: [" ", cmd.description] }), i === visibleSlashSuggestions.length - 1 && slashWindowStart + menuH < suggestions.length
937
- ? _jsx(Text, { dimColor: true, children: ` · ${suggestions.length - slashWindowStart - menuH} more ↓` })
938
- : null] }, cmd.command));
1193
+ return (_jsxs(Text, { wrap: "truncate-end", backgroundColor: absoluteIndex === selected
1194
+ ? "#2563EB"
1195
+ : absoluteIndex % 2 === 0
1196
+ ? "#1E293B"
1197
+ : "#0F172A", children: [_jsxs(Text, { color: absoluteIndex === selected ? "#FFFFFF" : "#67E8F9", bold: true, children: [absoluteIndex === selected ? " ❯ " : " ", cmd.command.padEnd(14)] }), cmd.usage ? (_jsxs(Text, { color: absoluteIndex === selected ? "#FFFFFF" : "#CBD5E1", children: [cmd.usage, " "] })) : null, _jsxs(Text, { color: "#F8FAFC", children: [" ", cmd.description] }), i === visibleSlashSuggestions.length - 1 &&
1198
+ slashWindowStart + menuH < suggestions.length ? (_jsx(Text, { dimColor: true, children: ` · ${suggestions.length - slashWindowStart - menuH} more ↓` })) : null] }, cmd.command));
939
1199
  })
940
1200
  : null, fileMenuOpen && !modalActive
941
- ? fileSuggestions.map((file, i) => (_jsxs(Text, { wrap: "truncate-end", backgroundColor: i === selected ? "#2563EB" : i % 2 === 0 ? "#1E293B" : "#0F172A", children: [_jsxs(Text, { color: i === selected ? "#FFFFFF" : file.isDir ? "#67E8F9" : "#F8FAFC", bold: i === selected, children: [i === selected ? "❯ " : " ", file.isDir ? "▸ " : "· ", file.value] }), _jsx(Text, { dimColor: true, children: file.isDir ? " directory" : " attach file" })] }, file.value)))
942
- : null, secretRequest ? (_jsx(SecretInputPanel, { title: secretRequest.title, prompt: secretRequest.prompt, onSubmit: (value) => answerSecret(value), onCancel: () => answerSecret(undefined) })) : state.pendingConfirm ? (_jsx(ConfirmModal, { confirm: state.pendingConfirm, onAnswer: answerConfirm })) : (_jsx(Box, { children: compacting ? (_jsxs(Text, { children: [_jsxs(Text, { color: "magenta", children: [spinner, " "] }), _jsx(Text, { color: "yellow", children: "compacting conversation\u2026" })] })) : state.status.running ? (_jsxs(Text, { children: [_jsxs(Text, { color: "magenta", children: [spinner, " "] }), _jsx(Text, { color: "yellow", children: state.status.activity || "working" }), state.status.step > 0 ? _jsx(Text, { dimColor: true, children: ` · step ${state.status.step}` }) : null, _jsx(Text, { dimColor: true, children: ` · ${elapsed}s · esc to cancel` }), state.queued.length > 0 ? _jsx(Text, { dimColor: true, children: ` · ${state.queued.length} queued` }) : null] })) : null })), _jsxs(Box, { borderStyle: "round", borderColor: state.pendingConfirm || secretRequest ? "yellow" : state.status.running ? "yellow" : "magenta", paddingX: 1, children: [_jsx(Text, { color: state.status.running ? "yellow" : "magenta", bold: true, children: state.pendingConfirm || secretRequest ? "! " : "❯ " }), secretRequest ? (_jsx(Text, { bold: true, children: "Input locked \u00B7 complete the secure input above" })) : state.pendingConfirm ? (_jsx(Text, { bold: true, children: "Input locked \u00B7 answer the confirmation above with Y or N" })) : input.length === 0 ? (_jsx(Text, { dimColor: true, children: state.status.running
1201
+ ? fileSuggestions.map((file, i) => (_jsxs(Text, { wrap: "truncate-end", backgroundColor: i === selected ? "#2563EB" : i % 2 === 0 ? "#1E293B" : "#0F172A", children: [_jsxs(Text, { color: i === selected
1202
+ ? "#FFFFFF"
1203
+ : file.isDir
1204
+ ? "#67E8F9"
1205
+ : "#F8FAFC", bold: i === selected, children: [i === selected ? "❯ " : " ", file.isDir ? "▸ " : "· ", file.value] }), _jsx(Text, { dimColor: true, children: file.isDir ? " directory" : " attach file" })] }, file.value)))
1206
+ : null, secretRequest ? (_jsx(SecretInputPanel, { title: secretRequest.title, prompt: secretRequest.prompt, onSubmit: (value) => answerSecret(value), onCancel: () => answerSecret(undefined) })) : state.pendingConfirm ? (_jsx(ConfirmModal, { confirm: state.pendingConfirm, onAnswer: answerConfirm })) : (_jsx(Box, { children: compacting ? (_jsxs(Text, { children: [_jsxs(Text, { color: "magenta", children: [spinner, " "] }), _jsx(Text, { color: "yellow", children: "compacting conversation\u2026" })] })) : state.status.running ? (_jsxs(Text, { children: [_jsxs(Text, { color: "magenta", children: [spinner, " "] }), _jsx(Text, { color: "yellow", children: state.status.activity || "working" }), state.status.step > 0 ? (_jsx(Text, { dimColor: true, children: ` · step ${state.status.step}` })) : null, _jsx(Text, { dimColor: true, children: ` · ${elapsed}s · esc to cancel` }), state.queued.length > 0 ? (_jsx(Text, { dimColor: true, children: ` · ${state.queued.length} queued` })) : null] })) : null })), _jsxs(Box, { borderStyle: "round", borderColor: state.pendingConfirm || secretRequest
1207
+ ? "yellow"
1208
+ : state.status.running
1209
+ ? "yellow"
1210
+ : "cyan", paddingX: 1, children: [_jsx(Text, { color: state.status.running ? "yellow" : "cyan", bold: true, children: state.pendingConfirm || secretRequest ? "! " : "❯ " }), secretRequest ? (_jsx(Text, { bold: true, children: "Input locked \u00B7 complete the secure input above" })) : state.pendingConfirm ? (_jsx(Text, { bold: true, children: "Input locked \u00B7 answer the confirmation above with Y or N" })) : input.length === 0 ? (_jsx(Text, { dimColor: true, children: state.status.running
943
1211
  ? "type to queue a message…"
944
- : "ask anything · / for commands · @file to attach · esc to cancel" })) : (_jsxs(Text, { wrap: "truncate-start", children: [before, _jsx(Text, { inverse: true, children: at }), after] }))] }), !secretRequest && !state.pendingConfirm && !state.status.running && !compacting ? (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { backgroundColor: "#166534", color: "#FFFFFF", bold: true, children: " READY " }), state.queued.length > 0 ? _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ${state.queued.length} QUEUED ` }) : null, _jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: " / COMMANDS " }), _jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: " CTRL+T THINKING " }), _jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: " CTRL+O OUTPUT " })] })) : null] }));
1212
+ : "ask anything · / for commands · @file to attach · esc to cancel" })) : (_jsxs(Text, { wrap: "truncate-start", children: [_jsx(Text, { color: "#F8FAFC", children: before }), _jsx(Text, { backgroundColor: "#22D3EE", color: "#020617", bold: true, children: at }), _jsx(Text, { color: "#F8FAFC", children: after })] }))] }), !secretRequest &&
1213
+ !state.pendingConfirm &&
1214
+ !state.status.running &&
1215
+ !compacting ? (_jsxs(Box, { paddingX: 1, justifyContent: "center", children: [_jsxs(Text, { backgroundColor: "#166534", color: "#FFFFFF", bold: true, children: [" ", "READY", " "] }), state.queued.length > 0 ? (_jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ${state.queued.length} QUEUED ` })) : null, _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "/ COMMANDS", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "CTRL+T THINKING", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "CTRL+O OUTPUT", " "] }), _jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: mouseMode ? "#155E75" : "#334155", color: "#F8FAFC", children: ` MOUSE ${mouseMode ? "ON" : "OFF"} ` })] })) : null] }));
945
1216
  }
946
1217
  //# sourceMappingURL=App.js.map