@pentoshi/clai 1.2.8 → 2.0.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.
Files changed (70) hide show
  1. package/README.md +46 -2
  2. package/dist/agent/context-manager.d.ts +14 -0
  3. package/dist/agent/context-manager.js +61 -0
  4. package/dist/agent/context-manager.js.map +1 -1
  5. package/dist/agent/runner.d.ts +6 -0
  6. package/dist/agent/runner.js +24 -3
  7. package/dist/agent/runner.js.map +1 -1
  8. package/dist/commands/update.js +1 -1
  9. package/dist/index.js +3 -17
  10. package/dist/index.js.map +1 -1
  11. package/dist/modes/agent.d.ts +4 -0
  12. package/dist/modes/agent.js.map +1 -1
  13. package/dist/repl.d.ts +5 -0
  14. package/dist/repl.js +8 -2
  15. package/dist/repl.js.map +1 -1
  16. package/dist/store/history.d.ts +7 -1
  17. package/dist/store/history.js +34 -3
  18. package/dist/store/history.js.map +1 -1
  19. package/dist/tools/registry.d.ts +4 -0
  20. package/dist/tools/registry.js +57 -1
  21. package/dist/tools/registry.js.map +1 -1
  22. package/dist/tools/shell.d.ts +2 -0
  23. package/dist/tools/shell.js +5 -1
  24. package/dist/tools/shell.js.map +1 -1
  25. package/dist/tui/App.d.ts +2 -1
  26. package/dist/tui/App.js +695 -100
  27. package/dist/tui/App.js.map +1 -1
  28. package/dist/tui/components/ConfirmModal.js +2 -2
  29. package/dist/tui/components/ConfirmModal.js.map +1 -1
  30. package/dist/tui/components/Pager.d.ts +6 -5
  31. package/dist/tui/components/Pager.js +17 -26
  32. package/dist/tui/components/Pager.js.map +1 -1
  33. package/dist/tui/components/PickerPanel.d.ts +13 -0
  34. package/dist/tui/components/PickerPanel.js +68 -0
  35. package/dist/tui/components/PickerPanel.js.map +1 -0
  36. package/dist/tui/components/SecretInputPanel.d.ts +6 -0
  37. package/dist/tui/components/SecretInputPanel.js +25 -0
  38. package/dist/tui/components/SecretInputPanel.js.map +1 -0
  39. package/dist/tui/default.d.ts +6 -0
  40. package/dist/tui/default.js +11 -0
  41. package/dist/tui/default.js.map +1 -0
  42. package/dist/tui/hooks/useAgentRunner.d.ts +13 -2
  43. package/dist/tui/hooks/useAgentRunner.js +48 -3
  44. package/dist/tui/hooks/useAgentRunner.js.map +1 -1
  45. package/dist/tui/hooks/useTerminalSize.d.ts +9 -0
  46. package/dist/tui/hooks/useTerminalSize.js +26 -0
  47. package/dist/tui/hooks/useTerminalSize.js.map +1 -0
  48. package/dist/tui/index.js +1 -0
  49. package/dist/tui/index.js.map +1 -1
  50. package/dist/tui/render-lines.d.ts +15 -0
  51. package/dist/tui/render-lines.js +165 -0
  52. package/dist/tui/render-lines.js.map +1 -0
  53. package/dist/tui/state.d.ts +12 -10
  54. package/dist/tui/state.js +83 -57
  55. package/dist/tui/state.js.map +1 -1
  56. package/dist/ui/markdown.js +56 -12
  57. package/dist/ui/markdown.js.map +1 -1
  58. package/package.json +1 -1
  59. package/dist/tui/components/Composer.d.ts +0 -6
  60. package/dist/tui/components/Composer.js +0 -122
  61. package/dist/tui/components/Composer.js.map +0 -1
  62. package/dist/tui/components/Header.d.ts +0 -7
  63. package/dist/tui/components/Header.js +0 -6
  64. package/dist/tui/components/Header.js.map +0 -1
  65. package/dist/tui/components/StatusLine.d.ts +0 -7
  66. package/dist/tui/components/StatusLine.js +0 -25
  67. package/dist/tui/components/StatusLine.js.map +0 -1
  68. package/dist/tui/components/items.d.ts +0 -5
  69. package/dist/tui/components/items.js +0 -57
  70. package/dist/tui/components/items.js.map +0 -1
package/dist/tui/App.js CHANGED
@@ -1,24 +1,36 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Static, useApp, useInput, useStdout } from "ink";
2
+ import { Box, Text, useApp, useInput } from "ink";
3
3
  import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
4
+ import { providerIds } from "../types.js";
4
5
  import { assertProvider } from "../llm/provider.js";
5
- import { modelSupportsVision } from "../llm/capabilities.js";
6
- import { getProviderModel, setDefaultMode, setProviderModel, } from "../store/config.js";
6
+ import { getProvider } from "../llm/router.js";
7
+ import { envValue, getProviderSecret, setProviderSecret } from "../store/keys.js";
8
+ import { modelSupportsThinking, modelSupportsVision } from "../llm/capabilities.js";
9
+ import { getConfig, getProviderModel, setDefaultMode, setDefaultProvider, setProviderModel, setThinking, updateConfig, } from "../store/config.js";
10
+ import { estimateMessagesTokens } from "../agent/context-manager.js";
11
+ import { clearAllHistory, getSession, listSessions, saveSession } from "../store/history.js";
7
12
  import { safeCwd } from "../os/cwd.js";
8
- import { expandMentions, loadImageAttachments } from "../ui/mentions.js";
9
- import { loadPlan, savePlan } from "../store/plan.js";
13
+ import { resolve } from "node:path";
14
+ import { existsSync } from "node:fs";
15
+ import { readFile } from "node:fs/promises";
16
+ import { expandMentions, findFileSuggestions, getMentionQuery, loadImageAttachments } from "../ui/mentions.js";
17
+ import { deletePlan, loadPlan, savePlan } from "../store/plan.js";
10
18
  import { renderPlanDocument } from "../ui/plan-pane.js";
11
- import { initialState, reducer, splitItems } from "./state.js";
19
+ import { getSlashCommandSuggestions, isKnownSlashCommand, knownModels, slashCommands } from "../repl.js";
20
+ import { initialState, reducer, serializeTranscriptForCompaction } from "./state.js";
21
+ import { renderTranscriptLines } from "./render-lines.js";
12
22
  import { createTuiConfirmPort } from "./confirm.js";
13
23
  import { useAgentRunner } from "./hooks/useAgentRunner.js";
14
24
  import { useJobs } from "./hooks/useJobs.js";
15
- import { Header } from "./components/Header.js";
16
- import { ItemView } from "./components/items.js";
17
- import { StatusLine } from "./components/StatusLine.js";
18
- import { Composer } from "./components/Composer.js";
25
+ import { useSpinner } from "./hooks/useSpinner.js";
26
+ import { useTerminalSize } from "./hooks/useTerminalSize.js";
19
27
  import { ConfirmModal } from "./components/ConfirmModal.js";
20
28
  import { Pager } from "./components/Pager.js";
21
29
  import { JobsPanel } from "./components/JobsPanel.js";
30
+ import { PickerPanel } from "./components/PickerPanel.js";
31
+ import { SecretInputPanel } from "./components/SecretInputPanel.js";
32
+ import { clearArtifacts, clearAuditLogs } from "../store/logs.js";
33
+ import { addScopeTargets, clearScope, loadScope, saveScope } from "../store/scope.js";
22
34
  const IMPLEMENT_PROMPT = "I approve the plan. Execute it now in STRICT ORDER. Task 1 (explore) is ALREADY COMPLETE from the planning phase — " +
23
35
  "do NOT re-list or re-read the directory. Start with the FIRST pending task that still needs implementation work. " +
24
36
  "For each task: call task.update {taskId, state:'in_progress'} → do the real work → VERIFY it succeeded → " +
@@ -27,47 +39,79 @@ const IMPLEMENT_PROMPT = "I approve the plan. Execute it now in STRICT ORDER. Ta
27
39
  "Build the project for real with fs.writeMany (create all files in as few calls as possible). " +
28
40
  "Do NOT call web.search — you already know everything needed. " +
29
41
  "Run real commands (installs, servers, verification) — do not claim anything ran without a successful tool call.";
30
- export function App({ version, initialMode, provider: initialProvider, initialModel }) {
42
+ const MAX_SUGGESTIONS = 6;
43
+ export function App({ version, initialMode, provider: initialProvider, initialModel, noHistory = false }) {
31
44
  const { exit } = useApp();
32
- const { stdout } = useStdout();
45
+ const { columns: cols, rows } = useTerminalSize();
33
46
  const [state, dispatch] = useReducer(reducer, undefined, initialState);
34
47
  const [mode, setMode] = useState(initialMode);
35
48
  const [provider, setProvider] = useState(initialProvider);
36
49
  const [model, setModel] = useState(initialModel);
37
50
  const [overlay, setOverlay] = useState({ kind: "none" });
51
+ const [input, setInput] = useState("");
52
+ const [cursor, setCursor] = useState(0);
53
+ const [selected, setSelected] = useState(0);
54
+ const [secretRequest, setSecretRequest] = useState();
55
+ const secretResolver = useRef();
56
+ const [scroll, setScroll] = useState(0); // lines scrolled up from bottom
57
+ const [compacting, setCompacting] = useState(false);
58
+ const history = useRef([]);
59
+ const historyIdx = useRef(-1);
60
+ const historyDraft = useRef("");
38
61
  const lastCtrlC = useRef(0);
39
62
  const jobs = useJobs(overlay.kind === "jobs");
40
- // Confirm port wired to an in-app modal.
63
+ const spinner = useSpinner(state.status.running);
64
+ useEffect(() => {
65
+ if (!process.stdout.isTTY)
66
+ return;
67
+ process.stdout.write("\x1b[?1049h");
68
+ process.stdout.write("\x1b[2J\x1b[H");
69
+ return () => {
70
+ process.stdout.write("\x1b[?1049l");
71
+ };
72
+ }, []);
73
+ // ── Confirm port → in-app modal ────────────────────────────────────────────
41
74
  const confirmController = useMemo(() => createTuiConfirmPort(), []);
42
75
  const confirmResolver = useRef(undefined);
43
76
  useEffect(() => {
44
- confirmController.setHandler((req) => new Promise((resolve) => {
45
- confirmResolver.current = resolve;
77
+ confirmController.setHandler((req) => new Promise((res) => {
78
+ confirmResolver.current = res;
46
79
  dispatch({ type: "event", event: { type: "confirm-request", id: "c", ...req } });
47
80
  }));
48
81
  }, [confirmController]);
49
82
  const ctxRef = useRef({ mode, provider, model });
50
83
  ctxRef.current = { mode, provider, model };
84
+ const requestSecret = useCallback((request) => new Promise((resolveSecret) => {
85
+ secretResolver.current = resolveSecret;
86
+ setSecretRequest(request);
87
+ }), []);
51
88
  const runner = useAgentRunner({
52
89
  dispatchEvent: (event) => dispatch({ type: "event", event }),
53
90
  confirm: confirmController.port,
54
91
  getContext: useCallback(() => ctxRef.current, []),
92
+ requestSecret,
55
93
  });
94
+ const exitTui = useCallback(() => {
95
+ void (async () => {
96
+ const messages = runner.getMessages();
97
+ if (!noHistory && !getConfig().privateMode && messages.length > 0) {
98
+ await saveSession(messages, undefined, state.items).catch(() => undefined);
99
+ }
100
+ exit();
101
+ })();
102
+ }, [exit, noHistory, runner, state.items]);
56
103
  const startTurn = useCallback(async (display, modelInput, images) => {
104
+ setScroll(0);
57
105
  dispatch({ type: "submit", text: display });
58
106
  await runner.run(modelInput, images && images.length > 0 ? { images } : undefined);
59
107
  }, [runner]);
60
- // Expand @-mentions/dropped paths, then run the turn.
61
108
  const beginTurn = useCallback((rawText) => {
62
109
  const vision = modelSupportsVision(provider, model);
63
110
  const expansion = expandMentions(rawText, safeCwd(), vision);
64
111
  const images = vision ? loadImageAttachments(rawText, safeCwd()) : [];
65
- const modelInput = expansion.contextBlock
66
- ? `${rawText}\n\n${expansion.contextBlock}`
67
- : rawText;
112
+ const modelInput = expansion.contextBlock ? `${rawText}\n\n${expansion.contextBlock}` : rawText;
68
113
  void startTurn(rawText, modelInput, images);
69
114
  }, [provider, model, startTurn]);
70
- // Flush the next queued message once the current turn finishes.
71
115
  useEffect(() => {
72
116
  if (!state.status.running && state.queued.length > 0 && !runner.isRunning()) {
73
117
  const next = state.queued[0];
@@ -75,15 +119,26 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
75
119
  beginTurn(next);
76
120
  }
77
121
  }, [state.status.running, state.queued, runner, beginTurn]);
122
+ const lastToolOutput = useCallback(() => {
123
+ for (let i = state.items.length - 1; i >= 0; i -= 1) {
124
+ const item = state.items[i];
125
+ if (item.kind === "tool" && item.output)
126
+ return item;
127
+ }
128
+ return undefined;
129
+ }, [state.items]);
130
+ const openToolOutput = useCallback(async (item) => {
131
+ let body = item.output;
132
+ if (item.artifactPath) {
133
+ body = await readFile(item.artifactPath, "utf8").catch(() => item.output);
134
+ }
135
+ setOverlay({ kind: "pager", title: `${item.name} · full output`, body });
136
+ }, []);
78
137
  const runImplement = useCallback(async () => {
79
138
  const session = runner.getSession();
80
139
  const plan = await loadPlan(session.sessionId).catch(() => undefined);
81
140
  if (!plan) {
82
- dispatch({
83
- type: "notice",
84
- level: "info",
85
- text: "no plan to implement — ask clai to plan a multi-step task first",
86
- });
141
+ dispatch({ type: "notice", level: "info", text: "no plan to implement — ask clai to plan a multi-step task first" });
87
142
  return;
88
143
  }
89
144
  if (plan.tasks.every((t) => t.state === "done")) {
@@ -96,178 +151,718 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
96
151
  dispatch({ type: "notice", level: "info", text: "✦ plan approved — executing it now" });
97
152
  void startTurn("/implement", IMPLEMENT_PROMPT);
98
153
  }, [runner, startTurn]);
99
- const lastToolOutput = useCallback(() => {
100
- for (let i = state.items.length - 1; i >= 0; i -= 1) {
101
- const item = state.items[i];
102
- if (item.kind === "tool" && item.output)
103
- return item;
154
+ const chooseModel = useCallback(() => {
155
+ const models = knownModels[provider] ?? [model];
156
+ setOverlay({
157
+ kind: "picker",
158
+ title: `Models · ${provider}`,
159
+ options: models.map((value) => ({ value, label: value, active: value === model })),
160
+ onSelect: (value) => {
161
+ setModel(value);
162
+ setProviderModel(provider, value);
163
+ setOverlay({ kind: "none" });
164
+ dispatch({ type: "notice", level: "info", text: `model → ${value}` });
165
+ },
166
+ });
167
+ }, [model, provider]);
168
+ const activateProvider = useCallback(async (next) => {
169
+ const configured = next === "ollama" || Boolean(envValue(next)) || Boolean((await getProviderSecret(next)).value);
170
+ if (!configured) {
171
+ const key = await requestSecret({
172
+ title: `${next} API key`,
173
+ prompt: `No API key is configured for ${next}. Enter it now to activate this provider.`,
174
+ });
175
+ if (!key) {
176
+ dispatch({ type: "notice", level: "warn", text: `provider unchanged · ${next} needs an API key` });
177
+ return;
178
+ }
179
+ if (!getProvider(next).validateKey(key)) {
180
+ dispatch({ type: "notice", level: "warn", text: `invalid API key format for ${next}` });
181
+ return;
182
+ }
183
+ await setProviderSecret(next, key);
104
184
  }
105
- return undefined;
106
- }, [state.items]);
185
+ const nextModel = getProviderModel(next);
186
+ setDefaultProvider(next);
187
+ setProvider(next);
188
+ setModel(nextModel);
189
+ setOverlay({ kind: "none" });
190
+ dispatch({ type: "notice", level: "info", text: `provider → ${next} · model → ${nextModel}` });
191
+ }, [requestSecret]);
192
+ const chooseProvider = useCallback(() => {
193
+ setOverlay({
194
+ kind: "picker",
195
+ title: "Providers",
196
+ options: providerIds.map((value) => ({
197
+ value,
198
+ label: value,
199
+ description: getProviderModel(value),
200
+ active: value === provider,
201
+ })),
202
+ onSelect: (value) => {
203
+ const next = assertProvider(value);
204
+ void activateProvider(next);
205
+ },
206
+ });
207
+ }, [activateProvider, provider]);
208
+ const setReasoning = useCallback((value) => {
209
+ if (value === "off" || value === "none")
210
+ setThinking({ enabled: false });
211
+ else
212
+ setThinking({ enabled: true, effort: value });
213
+ dispatch({
214
+ type: "notice",
215
+ level: "info",
216
+ text: value === "off" || value === "none" ? "thinking → off" : `thinking → ${value}`,
217
+ });
218
+ }, []);
219
+ const chooseReasoning = useCallback(() => {
220
+ const current = getConfig().thinking;
221
+ const descriptions = {
222
+ off: "disable reasoning",
223
+ minimal: "lowest latency",
224
+ low: "light reasoning",
225
+ medium: "balanced",
226
+ high: "deep reasoning",
227
+ xhigh: "maximum depth",
228
+ };
229
+ setOverlay({
230
+ kind: "picker",
231
+ title: `Reasoning · ${modelSupportsThinking(provider, model) ? "supported" : "model may ignore it"}`,
232
+ options: Object.entries(descriptions).map(([value, description]) => ({
233
+ value,
234
+ label: value,
235
+ description,
236
+ active: value === (current.enabled ? current.effort : "off"),
237
+ })),
238
+ onSelect: (value) => {
239
+ setReasoning(value);
240
+ setOverlay({ kind: "none" });
241
+ },
242
+ });
243
+ }, [model, provider, setReasoning]);
107
244
  const handleLocalSlash = useCallback((text) => {
108
245
  const [cmd, ...rest] = text.trim().split(/\s+/);
109
246
  const arg = rest.join(" ").trim();
247
+ const info = (t) => dispatch({ type: "notice", level: "info", text: t });
248
+ const warn = (t) => dispatch({ type: "notice", level: "warn", text: t });
110
249
  switch (cmd) {
111
250
  case "/ask":
112
251
  setMode("ask");
113
252
  setDefaultMode("ask");
114
- dispatch({ type: "notice", level: "info", text: "mode → ask" });
253
+ info("mode → ask");
115
254
  return true;
116
255
  case "/agent":
117
256
  setMode("agent");
118
257
  setDefaultMode("agent");
119
- dispatch({ type: "notice", level: "info", text: "mode → agent" });
258
+ info("mode → agent");
120
259
  return true;
121
260
  case "/clear":
261
+ runner.reset();
262
+ dispatch({ type: "reset" });
263
+ info("context cleared");
264
+ return true;
122
265
  case "/new":
266
+ void (async () => {
267
+ const messages = runner.getMessages();
268
+ if (!noHistory && !getConfig().privateMode && messages.length > 0)
269
+ await saveSession(messages, undefined, state.items).catch(() => undefined);
270
+ runner.reset();
271
+ dispatch({ type: "reset" });
272
+ info("fresh session started");
273
+ })();
274
+ return true;
275
+ case "/clean":
123
276
  runner.reset();
124
277
  dispatch({ type: "reset" });
278
+ info("fresh session started");
125
279
  return true;
126
280
  case "/think":
127
281
  case "/thinking":
128
282
  dispatch({ type: "toggle-thinking" });
129
283
  return true;
130
284
  case "/implement":
131
- if (runner.isRunning()) {
132
- dispatch({ type: "notice", level: "warn", text: "a turn is already running" });
133
- }
134
- else {
285
+ if (runner.isRunning())
286
+ warn("a turn is already running");
287
+ else
135
288
  void runImplement();
136
- }
137
289
  return true;
138
- case "/plan": {
290
+ case "/plan":
139
291
  void (async () => {
140
292
  const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
141
- if (!plan) {
142
- dispatch({ type: "notice", level: "info", text: "no active plan yet" });
143
- return;
144
- }
145
- setOverlay({ kind: "pager", title: "Plan", body: renderPlanDocument(plan) });
293
+ if (!plan)
294
+ info("no active plan yet");
295
+ else
296
+ setOverlay({ kind: "pager", title: "Plan", body: renderPlanDocument(plan) });
146
297
  })();
147
298
  return true;
148
- }
149
299
  case "/jobs":
150
300
  setOverlay({ kind: "jobs" });
151
301
  return true;
152
302
  case "/output": {
153
- const last = lastToolOutput();
154
- if (!last) {
155
- dispatch({ type: "notice", level: "info", text: "no tool output yet" });
156
- }
157
- else {
158
- setOverlay({ kind: "pager", title: `${last.name} output`, body: last.output });
303
+ const outputs = state.items.filter((item) => item.kind === "tool" && Boolean(item.output));
304
+ if (arg === "list" || arg === "ls") {
305
+ info(outputs.length ? outputs.map((item) => `${item.id} · ${item.name}`).join("\n") : "no tool output yet");
306
+ return true;
159
307
  }
308
+ const selectedOutput = arg && arg !== "last" ? outputs.find((item) => item.id === arg) : lastToolOutput();
309
+ if (!selectedOutput)
310
+ info(arg ? `no tool output: ${arg}` : "no tool output yet");
311
+ else
312
+ void openToolOutput(selectedOutput);
160
313
  return true;
161
314
  }
162
- case "/model": {
163
- if (!arg) {
164
- dispatch({ type: "notice", level: "info", text: "usage: /model <name>" });
315
+ case "/model":
316
+ if (!arg || arg === "list" || arg === "ls") {
317
+ chooseModel();
318
+ return true;
319
+ }
320
+ {
321
+ const options = knownModels[provider] ?? [];
322
+ const index = Number.parseInt(arg, 10);
323
+ const nextModel = Number.isInteger(index) && index >= 1 && index <= options.length ? options[index - 1] : arg;
324
+ setModel(nextModel);
325
+ setProviderModel(provider, nextModel);
326
+ info(`model → ${nextModel}`);
165
327
  return true;
166
328
  }
167
- setModel(arg);
168
- setProviderModel(provider, arg);
169
- dispatch({ type: "notice", level: "info", text: `model → ${arg}` });
170
- return true;
171
- }
172
329
  case "/provider":
173
- case "/use": {
330
+ case "/use":
174
331
  if (!arg) {
175
- dispatch({ type: "notice", level: "info", text: "usage: /provider <name>" });
332
+ chooseProvider();
176
333
  return true;
177
334
  }
178
335
  try {
179
336
  const next = assertProvider(arg);
180
- setProvider(next);
181
- const nextModel = getProviderModel(next);
182
- setModel(nextModel);
183
- dispatch({ type: "notice", level: "info", text: `provider → ${next} / ${nextModel}` });
337
+ void activateProvider(next);
184
338
  }
185
339
  catch {
186
- dispatch({ type: "notice", level: "warn", text: `unknown provider: ${arg}` });
340
+ warn(`unknown provider: ${arg}`);
341
+ }
342
+ return true;
343
+ case "/variants":
344
+ case "/reasoning": {
345
+ if (!arg) {
346
+ chooseReasoning();
347
+ return true;
348
+ }
349
+ const value = arg.toLowerCase();
350
+ if (/^(on|enable|true)$/.test(value)) {
351
+ setThinking({ enabled: true });
352
+ info(`thinking → ${getConfig().thinking.effort}`);
187
353
  }
354
+ else if (["off", "none", "disable", "false"].includes(value)) {
355
+ setReasoning("off");
356
+ }
357
+ else if (["minimal", "low", "medium", "high", "xhigh"].includes(value)) {
358
+ setReasoning(value);
359
+ }
360
+ else
361
+ warn("usage: /variants [on|off|minimal|low|medium|high|xhigh]");
188
362
  return true;
189
363
  }
364
+ case "/cwd": {
365
+ if (!arg) {
366
+ info(`cwd: ${safeCwd()}`);
367
+ return true;
368
+ }
369
+ const target = resolve(safeCwd(), arg);
370
+ if (!existsSync(target)) {
371
+ warn(`no such directory: ${target}`);
372
+ return true;
373
+ }
374
+ try {
375
+ process.chdir(target);
376
+ info(`cwd → ${target}`);
377
+ }
378
+ catch (e) {
379
+ warn(`could not chdir: ${e instanceof Error ? e.message : String(e)}`);
380
+ }
381
+ return true;
382
+ }
383
+ case "/allow":
384
+ if (!arg || arg === "list" || arg === "ls") {
385
+ const list = [...runner.getSession().allow];
386
+ info(list.length ? `allowed: ${list.join(", ")}` : "no session allowances");
387
+ return true;
388
+ }
389
+ runner.getSession().allow.add(arg);
390
+ info(`allowed for session: ${arg}`);
391
+ return true;
392
+ case "/disallow":
393
+ if (arg)
394
+ runner.getSession().allow.delete(arg);
395
+ info(arg ? `disallowed: ${arg}` : "usage: /disallow <tool>");
396
+ return true;
397
+ case "/context": {
398
+ const msgs = runner.getMessages();
399
+ info(`context: ${msgs.length} messages · ~${estimateMessagesTokens(msgs)} tokens`);
400
+ return true;
401
+ }
402
+ case "/compact": {
403
+ if (runner.isRunning() || compacting) {
404
+ warn("wait for the current operation to finish");
405
+ return true;
406
+ }
407
+ setCompacting(true);
408
+ info("compacting conversation…");
409
+ const fullSession = serializeTranscriptForCompaction(state.items);
410
+ void runner.compact(fullSession).then((result) => {
411
+ if (result.after === result.before) {
412
+ info("nothing to compact yet — more than 8 recent messages are required");
413
+ }
414
+ else {
415
+ info(`compacted ${result.before} → ${result.after} messages · ~${result.beforeTokens.toLocaleString()} → ~${result.afterTokens.toLocaleString()} tokens${result.summarized ? "" : " · local fallback"}`);
416
+ }
417
+ }).catch((error) => warn(`compaction failed: ${error instanceof Error ? error.message : String(error)}`))
418
+ .finally(() => setCompacting(false));
419
+ return true;
420
+ }
421
+ case "/save":
422
+ void (async () => {
423
+ const msgs = runner.getMessages();
424
+ if (msgs.length === 0) {
425
+ info("nothing to save yet");
426
+ return;
427
+ }
428
+ const rec = await saveSession(msgs, arg || undefined, state.items).catch(() => undefined);
429
+ info(rec ? `saved session ${rec.id}` : "save failed");
430
+ })();
431
+ return true;
432
+ case "/history":
433
+ void (async () => {
434
+ const sessions = await listSessions(50);
435
+ const currentMessages = runner.getMessages();
436
+ if (sessions.length === 0 && currentMessages.length === 0) {
437
+ info("no session history yet");
438
+ return;
439
+ }
440
+ setOverlay({
441
+ kind: "picker",
442
+ title: "Session history",
443
+ options: [
444
+ ...(currentMessages.length ? [{
445
+ value: "__current__",
446
+ label: "Current session",
447
+ description: `${currentMessages.length} messages · active now`,
448
+ active: true,
449
+ }] : []),
450
+ ...sessions.map((session) => ({
451
+ value: session.id,
452
+ label: session.name ?? session.id,
453
+ description: `${session.createdAt.slice(0, 16).replace("T", " ")} · ${session.transcript?.length ?? session.messages.length} items`,
454
+ })),
455
+ ],
456
+ onSelect: (id) => {
457
+ void (async () => {
458
+ if (id === "__current__") {
459
+ setOverlay({ kind: "none" });
460
+ info("showing current session");
461
+ return;
462
+ }
463
+ const session = await getSession(id);
464
+ if (!session) {
465
+ warn("session not found");
466
+ return;
467
+ }
468
+ runner.setMessages(session.messages);
469
+ setOverlay({ kind: "none" });
470
+ dispatch({ type: "load-history", messages: session.messages, transcript: session.transcript });
471
+ setScroll(0);
472
+ info(`session resumed · ${session.transcript?.length ?? session.messages.length} items`);
473
+ })();
474
+ },
475
+ });
476
+ })();
477
+ return true;
478
+ case "/reset":
479
+ void clearAllHistory().then((result) => info(`history cleared · ${result.detail || "ok"}`));
480
+ return true;
481
+ case "/discard":
482
+ void (async () => {
483
+ const session = runner.getSession();
484
+ const plan = await loadPlan(session.sessionId).catch(() => undefined);
485
+ if (!plan) {
486
+ info("no active plan to discard");
487
+ return;
488
+ }
489
+ await deletePlan(session.sessionId);
490
+ session.planApproved.value = false;
491
+ info(`plan discarded · ${plan.goal}`);
492
+ })();
493
+ return true;
494
+ case "/scope":
495
+ void (async () => {
496
+ const [sub = "show", ...parts] = arg.split(/\s+/).filter(Boolean);
497
+ if (["clear", "reset", "off"].includes(sub)) {
498
+ await clearScope();
499
+ info("engagement scope cleared");
500
+ return;
501
+ }
502
+ if (sub === "show" || sub === "list" || sub === "ls") {
503
+ const scope = await loadScope();
504
+ info(scope ? `scope: ${scope.name ?? "unnamed"} · ${scope.authorizedTargets.join(", ")}` : "no engagement scope configured");
505
+ return;
506
+ }
507
+ if (sub === "add") {
508
+ const targets = parts.join(" ").split(/[\s,]+/).filter(Boolean);
509
+ if (!targets.length) {
510
+ warn("usage: /scope add <target1,target2>");
511
+ return;
512
+ }
513
+ const scope = await addScopeTargets(targets);
514
+ info(`scope updated · ${scope.authorizedTargets.join(", ")}`);
515
+ return;
516
+ }
517
+ if (sub === "new" || sub === "set") {
518
+ const targets = parts.join(" ").split(/[\s,]+/).filter(Boolean);
519
+ if (!targets.length) {
520
+ warn("usage: /scope new <target1,target2>");
521
+ return;
522
+ }
523
+ await saveScope({ authorizedTargets: targets, createdAt: new Date().toISOString() });
524
+ info(`scope created · ${targets.join(", ")}`);
525
+ return;
526
+ }
527
+ warn("usage: /scope [show|clear|new <targets>|add <targets>]");
528
+ })().catch((error) => warn(error instanceof Error ? error.message : String(error)));
529
+ return true;
530
+ case "/privacy":
531
+ void (async () => {
532
+ const sub = (arg || "status").toLowerCase();
533
+ if (["on", "enable"].includes(sub)) {
534
+ updateConfig({ privateMode: true });
535
+ info("private mode → on");
536
+ return;
537
+ }
538
+ if (["off", "disable"].includes(sub)) {
539
+ updateConfig({ privateMode: false });
540
+ info("private mode → off");
541
+ return;
542
+ }
543
+ if (sub === "status") {
544
+ info(`private mode: ${getConfig().privateMode ? "on" : "off"}`);
545
+ return;
546
+ }
547
+ if (sub === "clear-history") {
548
+ const result = await clearAllHistory();
549
+ info(`history cleared · ${result.detail || "ok"}`);
550
+ return;
551
+ }
552
+ if (sub === "clear-logs") {
553
+ const result = await clearAuditLogs();
554
+ info(`audit logs cleared · ${result.removed} files`);
555
+ return;
556
+ }
557
+ if (sub === "clear-artifacts") {
558
+ const result = await clearArtifacts();
559
+ info(`artifacts cleared · ${result.removed} files`);
560
+ return;
561
+ }
562
+ if (sub === "clear-all") {
563
+ const [historyResult, logResult, artifactResult] = await Promise.all([clearAllHistory(), clearAuditLogs(), clearArtifacts()]);
564
+ info(`cleared history (${historyResult.detail || "ok"}), logs (${logResult.removed}), artifacts (${artifactResult.removed})`);
565
+ return;
566
+ }
567
+ warn("usage: /privacy [status|on|off|clear-history|clear-logs|clear-artifacts|clear-all]");
568
+ })();
569
+ return true;
570
+ case "/freeonly": {
571
+ const on = /^(on|true|1|enable)$/i.test(arg), off = /^(off|false|0|disable)$/i.test(arg);
572
+ if (!on && !off) {
573
+ info(`freeOnly=${getConfig().freeOnly}`);
574
+ return true;
575
+ }
576
+ updateConfig({ freeOnly: on });
577
+ info(`freeOnly=${on}`);
578
+ return true;
579
+ }
580
+ case "/fallback": {
581
+ const on = /^(on|true|1|enable)$/i.test(arg), off = /^(off|false|0|disable)$/i.test(arg);
582
+ if (!on && !off) {
583
+ info(`providerFallback=${getConfig().providerFallback}`);
584
+ return true;
585
+ }
586
+ updateConfig({ providerFallback: on });
587
+ info(`providerFallback=${on}`);
588
+ return true;
589
+ }
590
+ case "/set":
591
+ case "/unset":
592
+ case "/update":
593
+ case "/keys":
594
+ info(`${cmd} manages external credentials or updates; use the equivalent \`clai ${cmd.slice(1)}\` command outside the TUI`);
595
+ return true;
190
596
  case "/help":
191
- dispatch({
192
- type: "notice",
193
- level: "info",
194
- text: "commands: /ask /agent /model <name> /provider <name> /implement /plan /jobs /output /clear /think /exit · " +
195
- "Ctrl+T thinking · Ctrl+O output · Ctrl+P plan · Ctrl+J jobs · Esc cancel · Ctrl+C exit",
196
- });
597
+ setOverlay({ kind: "pager", title: "Commands", body: slashCommands.map((item) => `${item.command}${item.usage ? ` ${item.usage}` : ""}\n ${item.description}`).join("\n\n") });
197
598
  return true;
198
599
  case "/exit":
199
600
  case "/quit":
200
- exit();
601
+ exitTui();
201
602
  return true;
202
- default:
203
- return false;
603
+ default: return false;
204
604
  }
205
- }, [exit, provider, runner, runImplement, lastToolOutput]);
206
- const handleSubmit = useCallback((text) => {
207
- if (text.startsWith("/")) {
208
- if (handleLocalSlash(text))
209
- return;
210
- dispatch({ type: "notice", level: "warn", text: `unknown command: ${text}` });
605
+ }, [activateProvider, chooseModel, chooseProvider, chooseReasoning, compacting, exitTui, noHistory, provider, runner, runImplement, lastToolOutput, openToolOutput, setReasoning, state.items]);
606
+ const submitText = useCallback((text) => {
607
+ const trimmed = text.trim();
608
+ if (!trimmed)
211
609
  return;
212
- }
213
- if (runner.isRunning()) {
214
- dispatch({ type: "queue", text });
610
+ history.current.push(text);
611
+ historyIdx.current = -1;
612
+ historyDraft.current = "";
613
+ setInput("");
614
+ setCursor(0);
615
+ setSelected(0);
616
+ // A macOS/Linux drag-and-drop commonly starts with an absolute path.
617
+ // Only route a leading slash through the command handler when its first
618
+ // token is an actual clai command; file paths remain normal prompts.
619
+ if (trimmed.startsWith("/") && isKnownSlashCommand(trimmed)) {
620
+ if (!handleLocalSlash(trimmed))
621
+ dispatch({ type: "notice", level: "warn", text: `unknown command: ${trimmed}` });
215
622
  return;
216
623
  }
217
- beginTurn(text);
624
+ if (runner.isRunning())
625
+ dispatch({ type: "queue", text: trimmed });
626
+ else
627
+ beginTurn(trimmed);
218
628
  }, [handleLocalSlash, runner, beginTurn]);
219
629
  const answerConfirm = useCallback((ok) => {
220
630
  confirmResolver.current?.(ok);
221
631
  confirmResolver.current = undefined;
222
632
  dispatch({ type: "confirm-resolved" });
223
633
  }, []);
634
+ const answerSecret = useCallback((value) => {
635
+ secretResolver.current?.(value);
636
+ secretResolver.current = undefined;
637
+ setSecretRequest(undefined);
638
+ }, []);
639
+ // ── Layout math (keep the composer pinned to the bottom) ────────────────────
640
+ const suggestions = input.startsWith("/")
641
+ ? getSlashCommandSuggestions(input).slice(0, MAX_SUGGESTIONS)
642
+ : [];
643
+ const mention = getMentionQuery(input, cursor);
644
+ const fileSuggestions = mention
645
+ ? findFileSuggestions(mention.query, safeCwd(), MAX_SUGGESTIONS)
646
+ : [];
647
+ const slashMenuOpen = suggestions.length > 0;
648
+ const fileMenuOpen = !slashMenuOpen && Boolean(mention) && fileSuggestions.length > 0;
649
+ const menuOpen = slashMenuOpen || fileMenuOpen;
224
650
  const overlayOpen = overlay.kind !== "none";
225
- // Global keys: overlays, thinking toggle, abort, exit.
226
- useInput((input, key) => {
227
- if (state.pendingConfirm || overlayOpen)
228
- return; // modal/overlay owns input
229
- if (key.ctrl && input === "t") {
651
+ const modalActive = Boolean(state.pendingConfirm) || Boolean(secretRequest) || overlayOpen;
652
+ // Leave the terminal's final row unused. Painting through the last cell can
653
+ // trigger an implicit scroll in several terminals, which looks like a full
654
+ // screen flash on every keypress/spinner frame.
655
+ const usableRows = Math.max(8, rows - 1);
656
+ const headerH = 4;
657
+ const statusH = state.pendingConfirm ? 6 : secretRequest ? 7 : 1;
658
+ const composerH = 3;
659
+ const menuH = slashMenuOpen ? suggestions.length : fileMenuOpen ? fileSuggestions.length : 0;
660
+ const viewportH = Math.max(3, usableRows - headerH - statusH - composerH - menuH);
661
+ const transcriptLines = renderTranscriptLines(state, {
662
+ width: cols,
663
+ thinkingExpanded: state.thinkingExpanded,
664
+ outputExpanded: state.outputExpanded,
665
+ running: state.status.running,
666
+ });
667
+ const total = transcriptLines.length;
668
+ const maxOffset = Math.max(0, total - viewportH);
669
+ const offset = Math.min(scroll, maxOffset);
670
+ const end = total - offset;
671
+ const start = Math.max(0, end - viewportH);
672
+ let visible = transcriptLines.slice(start, end);
673
+ if (visible.length < viewportH) {
674
+ visible = [...Array(viewportH - visible.length).fill(""), ...visible];
675
+ }
676
+ // ── Key handling ────────────────────────────────────────────────────────────
677
+ useInput((ch, key) => {
678
+ if (modalActive)
679
+ return; // overlay/modal owns input
680
+ // Global shortcuts
681
+ if (key.ctrl && ch === "t") {
230
682
  dispatch({ type: "toggle-thinking" });
231
683
  return;
232
684
  }
233
- if (key.ctrl && input === "o") {
685
+ if (key.ctrl && ch === "o") {
234
686
  const last = lastToolOutput();
235
687
  if (last)
236
- setOverlay({ kind: "pager", title: `${last.name} output`, body: last.output });
688
+ void openToolOutput(last);
689
+ else
690
+ dispatch({ type: "notice", level: "info", text: "no tool output yet" });
237
691
  return;
238
692
  }
239
- if (key.ctrl && input === "p") {
693
+ if (key.ctrl && ch === "p") {
240
694
  void (async () => {
241
695
  const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
242
696
  if (plan)
243
697
  setOverlay({ kind: "pager", title: "Plan", body: renderPlanDocument(plan) });
698
+ else
699
+ dispatch({ type: "notice", level: "info", text: "no active plan yet" });
244
700
  })();
245
701
  return;
246
702
  }
247
- if (key.ctrl && input === "j") {
703
+ if (key.ctrl && ch === "j") {
248
704
  setOverlay({ kind: "jobs" });
249
705
  return;
250
706
  }
707
+ // Scrolling
708
+ if (key.pageUp) {
709
+ setScroll((s) => Math.min(maxOffset, s + viewportH));
710
+ return;
711
+ }
712
+ if (key.pageDown) {
713
+ setScroll((s) => Math.max(0, s - viewportH));
714
+ return;
715
+ }
716
+ if (key.ctrl && ch === "u") {
717
+ setScroll((s) => Math.min(maxOffset, s + Math.floor(viewportH / 2)));
718
+ return;
719
+ }
720
+ if (key.ctrl && ch === "d") {
721
+ setScroll((s) => Math.max(0, s - Math.floor(viewportH / 2)));
722
+ return;
723
+ }
724
+ if (!input && maxOffset > 0 && ch === "k") {
725
+ setScroll((s) => Math.min(maxOffset, s + 1));
726
+ return;
727
+ }
728
+ if (!input && maxOffset > 0 && ch === "j") {
729
+ setScroll((s) => Math.max(0, s - 1));
730
+ return;
731
+ }
251
732
  if (key.escape) {
252
733
  if (runner.isRunning())
253
734
  runner.abort();
735
+ else if (input) {
736
+ setInput("");
737
+ setCursor(0);
738
+ setSelected(0);
739
+ }
254
740
  return;
255
741
  }
256
- if (key.ctrl && input === "c") {
742
+ if (key.ctrl && ch === "c") {
257
743
  if (runner.isRunning()) {
258
744
  runner.abort();
259
745
  return;
260
746
  }
261
747
  const now = Date.now();
262
748
  if (now - lastCtrlC.current < 1500)
263
- exit();
749
+ exitTui();
264
750
  else
265
751
  lastCtrlC.current = now;
752
+ return;
753
+ }
754
+ // Slash menu navigation
755
+ if (menuOpen && (key.upArrow || key.downArrow)) {
756
+ setSelected((s) => {
757
+ const n = slashMenuOpen ? suggestions.length : fileSuggestions.length;
758
+ return key.upArrow ? (s - 1 + n) % n : (s + 1) % n;
759
+ });
760
+ return;
761
+ }
762
+ if (menuOpen && key.tab) {
763
+ if (slashMenuOpen) {
764
+ setInput(suggestions[selected].command + " ");
765
+ setCursor(suggestions[selected].command.length + 1);
766
+ }
767
+ else if (mention && fileSuggestions[selected]) {
768
+ const suggestion = fileSuggestions[selected];
769
+ const inserted = `@${suggestion.value}${suggestion.isDir ? "" : " "}`;
770
+ const next = input.slice(0, mention.start) + inserted + input.slice(cursor);
771
+ setInput(next);
772
+ setCursor(mention.start + inserted.length);
773
+ }
774
+ setSelected(0);
775
+ return;
776
+ }
777
+ if (key.return) {
778
+ if (slashMenuOpen) {
779
+ submitText(suggestions[selected].command);
780
+ return;
781
+ }
782
+ if (fileMenuOpen && mention && fileSuggestions[selected]) {
783
+ const suggestion = fileSuggestions[selected];
784
+ const inserted = `@${suggestion.value}${suggestion.isDir ? "" : " "}`;
785
+ const next = input.slice(0, mention.start) + inserted + input.slice(cursor);
786
+ setInput(next);
787
+ setCursor(mention.start + inserted.length);
788
+ setSelected(0);
789
+ return;
790
+ }
791
+ submitText(input);
792
+ return;
793
+ }
794
+ // History (when menu closed)
795
+ if (!menuOpen && key.upArrow) {
796
+ if (history.current.length === 0)
797
+ return;
798
+ if (historyIdx.current < 0)
799
+ historyDraft.current = input;
800
+ const idx = historyIdx.current < 0 ? history.current.length - 1 : Math.max(0, historyIdx.current - 1);
801
+ historyIdx.current = idx;
802
+ const v = history.current[idx] ?? "";
803
+ setInput(v);
804
+ setCursor(v.length);
805
+ return;
806
+ }
807
+ if (!menuOpen && key.downArrow) {
808
+ if (historyIdx.current < 0)
809
+ return;
810
+ const idx = historyIdx.current + 1;
811
+ if (idx >= history.current.length) {
812
+ historyIdx.current = -1;
813
+ const draft = historyDraft.current;
814
+ setInput(draft);
815
+ setCursor(draft.length);
816
+ return;
817
+ }
818
+ historyIdx.current = idx;
819
+ const v = history.current[idx] ?? "";
820
+ setInput(v);
821
+ setCursor(v.length);
822
+ return;
823
+ }
824
+ if (key.leftArrow) {
825
+ setCursor((c) => Math.max(0, c - 1));
826
+ return;
827
+ }
828
+ if (key.rightArrow) {
829
+ setCursor((c) => Math.min(input.length, c + 1));
830
+ return;
831
+ }
832
+ if (key.backspace || key.delete) {
833
+ if (cursor === 0)
834
+ return;
835
+ setInput(input.slice(0, cursor - 1) + input.slice(cursor));
836
+ setCursor(cursor - 1);
837
+ setSelected(0);
838
+ historyIdx.current = -1;
839
+ historyDraft.current = "";
840
+ return;
841
+ }
842
+ if (key.ctrl || key.meta)
843
+ return;
844
+ if (ch) {
845
+ setInput(input.slice(0, cursor) + ch + input.slice(cursor));
846
+ setCursor(cursor + ch.length);
847
+ setSelected(0);
848
+ historyIdx.current = -1;
849
+ historyDraft.current = "";
266
850
  }
267
851
  });
268
- const { committed, live } = splitItems(state.items);
269
- const cols = stdout?.columns ?? 80;
270
852
  const closeOverlay = useCallback(() => setOverlay({ kind: "none" }), []);
271
- return (_jsxs(Box, { flexDirection: "column", width: cols, children: [_jsx(Static, { items: committed, children: (item) => (_jsx(ItemView, { item: item, thinkingExpanded: state.thinkingExpanded }, item.id)) }), _jsxs(Box, { flexDirection: "column", children: [state.items.length === 0 ? (_jsx(Header, { version: version, provider: provider, model: model, mode: mode })) : null, live.map((item) => (_jsx(ItemView, { item: item, thinkingExpanded: state.thinkingExpanded }, item.id)))] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay.kind === "pager" ? (_jsx(Pager, { title: overlay.title, body: overlay.body, onClose: closeOverlay })) : overlay.kind === "jobs" ? (_jsx(JobsPanel, { jobs: jobs, onClose: closeOverlay })) : state.pendingConfirm ? (_jsx(ConfirmModal, { confirm: state.pendingConfirm, onAnswer: answerConfirm })) : (_jsx(StatusLine, { status: state.status, thinkingPreview: state.thinkingPreview, queued: state.queued.length })), _jsx(Composer, { busy: state.status.running, disabled: Boolean(state.pendingConfirm) || overlayOpen, onSubmit: handleSubmit })] })] }));
853
+ // ── Render ───────────────────────────────────────────────────────────────────
854
+ const elapsed = state.status.startedAt
855
+ ? Math.max(0, Math.floor((Date.now() - state.status.startedAt) / 1000))
856
+ : 0;
857
+ const before = input.slice(0, cursor);
858
+ const at = input.slice(cursor, cursor + 1) || " ";
859
+ const after = input.slice(cursor + 1);
860
+ 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, { color: "magenta", bold: true, children: "\u25C6 clai" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: mode }), _jsx(Text, { dimColor: true, 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
861
+ ? suggestions.map((cmd, i) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: i === selected ? "magenta" : "cyan", children: [i === selected ? "❯ " : " ", cmd.command] }), cmd.usage ? _jsxs(Text, { dimColor: true, children: [" ", cmd.usage] }) : null, _jsxs(Text, { dimColor: true, children: [" ", cmd.description] })] }, cmd.command)))
862
+ : null, fileMenuOpen && !modalActive
863
+ ? fileSuggestions.map((file, i) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: i === selected ? "magenta" : file.isDir ? "cyan" : "white", bold: i === selected, children: [i === selected ? "❯ " : " ", file.isDir ? "▸ " : "· ", file.value] }), _jsx(Text, { dimColor: true, children: file.isDir ? " directory" : " attach file" })] }, file.value)))
864
+ : 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] })) : (_jsxs(Text, { dimColor: true, children: ["ready", state.queued.length > 0 ? ` · ${state.queued.length} queued` : "", " · / commands · ctrl+t thinking · ctrl+o output"] })) })), _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
865
+ ? "type to queue a message…"
866
+ : "ask anything · / for commands · @file to attach · esc to cancel" })) : (_jsxs(Text, { wrap: "truncate-start", children: [before, _jsx(Text, { inverse: true, children: at }), after] }))] })] }));
272
867
  }
273
868
  //# sourceMappingURL=App.js.map