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