@pentoshi/clai 1.2.9 → 2.0.1

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