@pentoshi/clai 2.0.6 → 2.0.9
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/agent/events.d.ts +1 -1
- package/dist/commands/providers.d.ts +2 -0
- package/dist/commands/providers.js +60 -3
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/llm/openai.js +1 -1
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/provider.js +6 -1
- package/dist/llm/provider.js.map +1 -1
- package/dist/repl.js +5 -11
- package/dist/repl.js.map +1 -1
- package/dist/tui/App.d.ts +1 -1
- package/dist/tui/App.js +570 -72
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/ConfirmModal.js +20 -5
- package/dist/tui/components/ConfirmModal.js.map +1 -1
- package/dist/tui/render-lines.js +8 -2
- package/dist/tui/render-lines.js.map +1 -1
- package/dist/tui/state.d.ts +1 -1
- package/package.json +1 -1
package/dist/tui/App.js
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
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
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState, } from "react";
|
|
4
5
|
import { providerIds } from "../types.js";
|
|
5
6
|
import { assertProvider } from "../llm/provider.js";
|
|
6
7
|
import { getProvider } from "../llm/router.js";
|
|
7
|
-
import { envValue, getProviderSecret, getSearchProviderKey, listProviderStatuses, maskSecret, setProviderSecret, setSecret } from "../store/keys.js";
|
|
8
|
+
import { envValue, getProviderSecret, getSearchProviderKey, listProviderStatuses, maskSecret, setProviderSecret, setSecret, unsetProviderSecret, } from "../store/keys.js";
|
|
8
9
|
import { setActiveSearchProvider } from "../store/config.js";
|
|
9
|
-
import { searchProviderIds } from "../tools/web/types.js";
|
|
10
|
-
import { assertSearchProvider, searchProviders } from "../tools/web/providers/provider.js";
|
|
10
|
+
import { searchProviderIds, } from "../tools/web/types.js";
|
|
11
|
+
import { assertSearchProvider, searchProviders, } from "../tools/web/providers/provider.js";
|
|
11
12
|
import "../tools/web/providers/duckduckgo.js";
|
|
12
13
|
import "../tools/web/providers/brave.js";
|
|
13
14
|
import "../tools/web/providers/tavily.js";
|
|
14
|
-
import { modelSupportsThinking, modelSupportsVision } from "../llm/capabilities.js";
|
|
15
|
+
import { modelSupportsThinking, modelSupportsVision, } from "../llm/capabilities.js";
|
|
15
16
|
import { getConfig, getProviderModel, setDefaultMode, setDefaultProvider, setProviderModel, setThinking, updateConfig, } from "../store/config.js";
|
|
16
17
|
import { estimateMessagesTokens } from "../agent/context-manager.js";
|
|
17
|
-
import { clearAllHistory, getSession, listSessions, saveSession, upsertSession } from "../store/history.js";
|
|
18
|
+
import { clearAllHistory, getSession, listSessions, saveSession, upsertSession, } from "../store/history.js";
|
|
18
19
|
import { safeCwd } from "../os/cwd.js";
|
|
19
20
|
import { resolve } from "node:path";
|
|
20
21
|
import { existsSync } from "node:fs";
|
|
21
22
|
import { readFile } from "node:fs/promises";
|
|
22
|
-
import { expandMentions, findFileSuggestions, getMentionQuery, loadImageAttachments } from "../ui/mentions.js";
|
|
23
|
+
import { expandMentions, findFileSuggestions, getMentionQuery, loadImageAttachments, } from "../ui/mentions.js";
|
|
23
24
|
import { deletePlan, loadPlan, savePlan } from "../store/plan.js";
|
|
24
25
|
import { renderPlanDocument } from "../ui/plan-pane.js";
|
|
25
|
-
import { getSlashCommandSuggestions, isKnownSlashCommand, knownModels, slashCommands } from "../repl.js";
|
|
26
|
-
import { initialState, reducer, serializeTranscriptForCompaction } from "./state.js";
|
|
26
|
+
import { getSlashCommandSuggestions, isKnownSlashCommand, knownModels, slashCommands, } from "../repl.js";
|
|
27
|
+
import { initialState, reducer, serializeTranscriptForCompaction, } from "./state.js";
|
|
27
28
|
import { renderTranscriptLines } from "./render-lines.js";
|
|
28
29
|
import { createTuiConfirmPort } from "./confirm.js";
|
|
29
30
|
import { useAgentRunner } from "./hooks/useAgentRunner.js";
|
|
@@ -36,10 +37,10 @@ import { JobsPanel } from "./components/JobsPanel.js";
|
|
|
36
37
|
import { PickerPanel } from "./components/PickerPanel.js";
|
|
37
38
|
import { SecretInputPanel } from "./components/SecretInputPanel.js";
|
|
38
39
|
import { clearArtifacts, clearAuditLogs } from "../store/logs.js";
|
|
39
|
-
import { addScopeTargets, clearScope, loadScope, saveScope } from "../store/scope.js";
|
|
40
|
+
import { addScopeTargets, clearScope, loadScope, saveScope, } from "../store/scope.js";
|
|
40
41
|
import { formatKeyStatus } from "./format-keys.js";
|
|
41
42
|
import { shouldStoreInPromptHistory } from "./input-history.js";
|
|
42
|
-
import { DISABLE_MOUSE_REPORTING, ENABLE_MOUSE_REPORTING, isMouseReport, mouseWheelDirection, stripMouseReports } from "./mouse.js";
|
|
43
|
+
import { DISABLE_MOUSE_REPORTING, ENABLE_MOUSE_REPORTING, isMouseReport, mouseWheelDirection, stripMouseReports, } from "./mouse.js";
|
|
43
44
|
const IMPLEMENT_PROMPT = "I approve the plan. Execute it now in STRICT ORDER. Task 1 (explore) is ALREADY COMPLETE from the planning phase — " +
|
|
44
45
|
"do NOT re-list or re-read the directory. Start with the FIRST pending task that still needs implementation work. " +
|
|
45
46
|
"For each task: call task.update {taskId, state:'in_progress'} → do the real work → VERIFY it succeeded → " +
|
|
@@ -49,7 +50,7 @@ const IMPLEMENT_PROMPT = "I approve the plan. Execute it now in STRICT ORDER. Ta
|
|
|
49
50
|
"Do NOT call web.search — you already know everything needed. " +
|
|
50
51
|
"Run real commands (installs, servers, verification) — do not claim anything ran without a successful tool call.";
|
|
51
52
|
const MAX_FILE_SUGGESTIONS = 6;
|
|
52
|
-
export function App({ version, initialMode, provider: initialProvider, initialModel, noHistory = false }) {
|
|
53
|
+
export function App({ version, initialMode, provider: initialProvider, initialModel, noHistory = false, }) {
|
|
53
54
|
const { exit } = useApp();
|
|
54
55
|
const { stdin } = useStdin();
|
|
55
56
|
const { columns: cols, rows } = useTerminalSize();
|
|
@@ -89,7 +90,10 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
89
90
|
useEffect(() => {
|
|
90
91
|
confirmController.setHandler((req) => new Promise((res) => {
|
|
91
92
|
confirmResolver.current = res;
|
|
92
|
-
dispatch({
|
|
93
|
+
dispatch({
|
|
94
|
+
type: "event",
|
|
95
|
+
event: { type: "confirm-request", id: "c", ...req },
|
|
96
|
+
});
|
|
93
97
|
}));
|
|
94
98
|
}, [confirmController]);
|
|
95
99
|
const ctxRef = useRef({ mode, provider, model });
|
|
@@ -135,7 +139,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
135
139
|
if (autosaveTimer.current)
|
|
136
140
|
clearTimeout(autosaveTimer.current);
|
|
137
141
|
const messages = runner.getMessages();
|
|
138
|
-
if (!noHistory &&
|
|
142
|
+
if (!noHistory &&
|
|
143
|
+
!getConfig().privateMode &&
|
|
144
|
+
(messages.length > 0 || latestItems.current.length > 0)) {
|
|
139
145
|
void upsertSession(runner.getSession().sessionId, messages, undefined, latestItems.current).catch(() => undefined);
|
|
140
146
|
}
|
|
141
147
|
}, [noHistory, runner]);
|
|
@@ -148,11 +154,15 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
148
154
|
const vision = modelSupportsVision(provider, model);
|
|
149
155
|
const expansion = expandMentions(rawText, safeCwd(), vision);
|
|
150
156
|
const images = vision ? loadImageAttachments(rawText, safeCwd()) : [];
|
|
151
|
-
const modelInput = expansion.contextBlock
|
|
157
|
+
const modelInput = expansion.contextBlock
|
|
158
|
+
? `${rawText}\n\n${expansion.contextBlock}`
|
|
159
|
+
: rawText;
|
|
152
160
|
void startTurn(rawText, modelInput, images);
|
|
153
161
|
}, [provider, model, startTurn]);
|
|
154
162
|
useEffect(() => {
|
|
155
|
-
if (!state.status.running &&
|
|
163
|
+
if (!state.status.running &&
|
|
164
|
+
state.queued.length > 0 &&
|
|
165
|
+
!runner.isRunning()) {
|
|
156
166
|
const next = state.queued[0];
|
|
157
167
|
dispatch({ type: "dequeue" });
|
|
158
168
|
beginTurn(next);
|
|
@@ -177,17 +187,29 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
177
187
|
const session = runner.getSession();
|
|
178
188
|
const plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
179
189
|
if (!plan) {
|
|
180
|
-
dispatch({
|
|
190
|
+
dispatch({
|
|
191
|
+
type: "notice",
|
|
192
|
+
level: "info",
|
|
193
|
+
text: "no plan to implement — ask clai to plan a multi-step task first",
|
|
194
|
+
});
|
|
181
195
|
return;
|
|
182
196
|
}
|
|
183
197
|
if (plan.tasks.every((t) => t.state === "done")) {
|
|
184
|
-
dispatch({
|
|
198
|
+
dispatch({
|
|
199
|
+
type: "notice",
|
|
200
|
+
level: "info",
|
|
201
|
+
text: "this plan is already complete ✓",
|
|
202
|
+
});
|
|
185
203
|
return;
|
|
186
204
|
}
|
|
187
205
|
plan.status = "approved";
|
|
188
206
|
await savePlan(plan).catch(() => undefined);
|
|
189
207
|
session.planApproved.value = true;
|
|
190
|
-
dispatch({
|
|
208
|
+
dispatch({
|
|
209
|
+
type: "notice",
|
|
210
|
+
level: "info",
|
|
211
|
+
text: "✦ plan approved — executing it now",
|
|
212
|
+
});
|
|
191
213
|
void startTurn("/implement", IMPLEMENT_PROMPT);
|
|
192
214
|
}, [runner, startTurn]);
|
|
193
215
|
const chooseModel = useCallback(async () => {
|
|
@@ -216,7 +238,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
216
238
|
setOverlay({
|
|
217
239
|
kind: "picker",
|
|
218
240
|
title: `Models · ${provider}`,
|
|
219
|
-
options: models.map((value) => ({
|
|
241
|
+
options: models.map((value) => ({
|
|
242
|
+
value,
|
|
243
|
+
label: value,
|
|
244
|
+
active: value === model,
|
|
245
|
+
})),
|
|
220
246
|
onSelect: (value) => {
|
|
221
247
|
setModel(value);
|
|
222
248
|
setProviderModel(provider, value);
|
|
@@ -226,18 +252,28 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
226
252
|
});
|
|
227
253
|
}, [model, provider]);
|
|
228
254
|
const activateProvider = useCallback(async (next) => {
|
|
229
|
-
const configured = next === "ollama" ||
|
|
255
|
+
const configured = next === "ollama" ||
|
|
256
|
+
Boolean(envValue(next)) ||
|
|
257
|
+
Boolean((await getProviderSecret(next)).value);
|
|
230
258
|
if (!configured) {
|
|
231
259
|
const key = await requestSecret({
|
|
232
260
|
title: `${next} API key`,
|
|
233
261
|
prompt: `No API key is configured for ${next}. Enter it now to activate this provider.`,
|
|
234
262
|
});
|
|
235
263
|
if (!key) {
|
|
236
|
-
dispatch({
|
|
264
|
+
dispatch({
|
|
265
|
+
type: "notice",
|
|
266
|
+
level: "warn",
|
|
267
|
+
text: `provider unchanged · ${next} needs an API key`,
|
|
268
|
+
});
|
|
237
269
|
return;
|
|
238
270
|
}
|
|
239
271
|
if (!getProvider(next).validateKey(key)) {
|
|
240
|
-
dispatch({
|
|
272
|
+
dispatch({
|
|
273
|
+
type: "notice",
|
|
274
|
+
level: "warn",
|
|
275
|
+
text: `invalid API key format for ${next}`,
|
|
276
|
+
});
|
|
241
277
|
return;
|
|
242
278
|
}
|
|
243
279
|
await setProviderSecret(next, key);
|
|
@@ -247,7 +283,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
247
283
|
setProvider(next);
|
|
248
284
|
setModel(nextModel);
|
|
249
285
|
setOverlay({ kind: "none" });
|
|
250
|
-
dispatch({
|
|
286
|
+
dispatch({
|
|
287
|
+
type: "notice",
|
|
288
|
+
level: "info",
|
|
289
|
+
text: `provider → ${next} · model → ${nextModel}`,
|
|
290
|
+
});
|
|
251
291
|
}, [requestSecret]);
|
|
252
292
|
const chooseProvider = useCallback(() => {
|
|
253
293
|
setOverlay({
|
|
@@ -275,7 +315,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
275
315
|
prompt: `No API key is configured for ${adapter.displayName}. Enter it now to use this search provider.`,
|
|
276
316
|
});
|
|
277
317
|
if (!key) {
|
|
278
|
-
dispatch({
|
|
318
|
+
dispatch({
|
|
319
|
+
type: "notice",
|
|
320
|
+
level: "warn",
|
|
321
|
+
text: `search provider unchanged · ${next} needs an API key`,
|
|
322
|
+
});
|
|
279
323
|
return;
|
|
280
324
|
}
|
|
281
325
|
await setSecret("search", next, key);
|
|
@@ -283,7 +327,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
283
327
|
}
|
|
284
328
|
setActiveSearchProvider(next);
|
|
285
329
|
setOverlay({ kind: "none" });
|
|
286
|
-
dispatch({
|
|
330
|
+
dispatch({
|
|
331
|
+
type: "notice",
|
|
332
|
+
level: "info",
|
|
333
|
+
text: `search provider → ${next}`,
|
|
334
|
+
});
|
|
287
335
|
}, [requestSecret]);
|
|
288
336
|
const chooseSearchProvider = useCallback(() => {
|
|
289
337
|
const active = getConfig().activeSearchProvider;
|
|
@@ -300,7 +348,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
300
348
|
: `${adapter?.displayName ?? id} · keyless`,
|
|
301
349
|
};
|
|
302
350
|
}),
|
|
303
|
-
onSelect: (value) => {
|
|
351
|
+
onSelect: (value) => {
|
|
352
|
+
void activateSearchProvider(assertSearchProvider(value));
|
|
353
|
+
},
|
|
304
354
|
});
|
|
305
355
|
}, [activateSearchProvider]);
|
|
306
356
|
const setReasoning = useCallback((value) => {
|
|
@@ -311,7 +361,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
311
361
|
dispatch({
|
|
312
362
|
type: "notice",
|
|
313
363
|
level: "info",
|
|
314
|
-
text: value === "off" || value === "none"
|
|
364
|
+
text: value === "off" || value === "none"
|
|
365
|
+
? "thinking → off"
|
|
366
|
+
: `thinking → ${value}`,
|
|
315
367
|
});
|
|
316
368
|
}, []);
|
|
317
369
|
const chooseReasoning = useCallback(() => {
|
|
@@ -391,7 +443,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
391
443
|
if (!plan)
|
|
392
444
|
info("no active plan yet");
|
|
393
445
|
else
|
|
394
|
-
setOverlay({
|
|
446
|
+
setOverlay({
|
|
447
|
+
kind: "pager",
|
|
448
|
+
title: "Plan",
|
|
449
|
+
body: renderPlanDocument(plan),
|
|
450
|
+
});
|
|
395
451
|
})();
|
|
396
452
|
return true;
|
|
397
453
|
case "/jobs":
|
|
@@ -399,11 +455,22 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
399
455
|
return true;
|
|
400
456
|
case "/output": {
|
|
401
457
|
const outputs = state.items.filter((item) => item.kind === "tool" && Boolean(item.output));
|
|
458
|
+
if (!arg) {
|
|
459
|
+
if (outputs.length)
|
|
460
|
+
dispatch({ type: "toggle-output" });
|
|
461
|
+
else
|
|
462
|
+
info("no tool output yet");
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
402
465
|
if (arg === "list" || arg === "ls") {
|
|
403
|
-
info(outputs.length
|
|
466
|
+
info(outputs.length
|
|
467
|
+
? outputs.map((item) => `${item.id} · ${item.name}`).join("\n")
|
|
468
|
+
: "no tool output yet");
|
|
404
469
|
return true;
|
|
405
470
|
}
|
|
406
|
-
const selectedOutput = arg && arg !== "last"
|
|
471
|
+
const selectedOutput = arg && arg !== "last"
|
|
472
|
+
? outputs.find((item) => item.id === arg)
|
|
473
|
+
: lastToolOutput();
|
|
407
474
|
if (!selectedOutput)
|
|
408
475
|
info(arg ? `no tool output: ${arg}` : "no tool output yet");
|
|
409
476
|
else
|
|
@@ -418,7 +485,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
418
485
|
{
|
|
419
486
|
const options = knownModels[provider] ?? [];
|
|
420
487
|
const index = Number.parseInt(arg, 10);
|
|
421
|
-
const nextModel = Number.isInteger(index) && index >= 1 && index <= options.length
|
|
488
|
+
const nextModel = Number.isInteger(index) && index >= 1 && index <= options.length
|
|
489
|
+
? options[index - 1]
|
|
490
|
+
: arg;
|
|
422
491
|
setModel(nextModel);
|
|
423
492
|
setProviderModel(provider, nextModel);
|
|
424
493
|
info(`model → ${nextModel}`);
|
|
@@ -514,7 +583,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
514
583
|
case "/allow":
|
|
515
584
|
if (!arg || arg === "list" || arg === "ls") {
|
|
516
585
|
const list = [...runner.getSession().allow];
|
|
517
|
-
info(list.length
|
|
586
|
+
info(list.length
|
|
587
|
+
? `allowed: ${list.join(", ")}`
|
|
588
|
+
: "no session allowances");
|
|
518
589
|
return true;
|
|
519
590
|
}
|
|
520
591
|
runner.getSession().allow.add(arg);
|
|
@@ -538,14 +609,17 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
538
609
|
setCompacting(true);
|
|
539
610
|
info("compacting conversation…");
|
|
540
611
|
const fullSession = serializeTranscriptForCompaction(state.items);
|
|
541
|
-
void runner
|
|
612
|
+
void runner
|
|
613
|
+
.compact(fullSession)
|
|
614
|
+
.then((result) => {
|
|
542
615
|
if (result.after === result.before) {
|
|
543
616
|
info("nothing to compact yet — more than 8 recent messages are required");
|
|
544
617
|
}
|
|
545
618
|
else {
|
|
546
619
|
info(`compacted ${result.before} → ${result.after} messages · ~${result.beforeTokens.toLocaleString()} → ~${result.afterTokens.toLocaleString()} tokens${result.summarized ? "" : " · local fallback"}`);
|
|
547
620
|
}
|
|
548
|
-
})
|
|
621
|
+
})
|
|
622
|
+
.catch((error) => warn(`compaction failed: ${error instanceof Error ? error.message : String(error)}`))
|
|
549
623
|
.finally(() => setCompacting(false));
|
|
550
624
|
return true;
|
|
551
625
|
}
|
|
@@ -572,12 +646,16 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
572
646
|
kind: "picker",
|
|
573
647
|
title: "Session history",
|
|
574
648
|
options: [
|
|
575
|
-
...(currentMessages.length
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
649
|
+
...(currentMessages.length
|
|
650
|
+
? [
|
|
651
|
+
{
|
|
652
|
+
value: "__current__",
|
|
653
|
+
label: "Current session",
|
|
654
|
+
description: `${currentMessages.length} messages · active now`,
|
|
655
|
+
active: true,
|
|
656
|
+
},
|
|
657
|
+
]
|
|
658
|
+
: []),
|
|
581
659
|
...sessions.map((session) => ({
|
|
582
660
|
value: session.id,
|
|
583
661
|
label: session.name ?? session.id,
|
|
@@ -598,7 +676,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
598
676
|
}
|
|
599
677
|
runner.setMessages(session.messages);
|
|
600
678
|
setOverlay({ kind: "none" });
|
|
601
|
-
dispatch({
|
|
679
|
+
dispatch({
|
|
680
|
+
type: "load-history",
|
|
681
|
+
messages: session.messages,
|
|
682
|
+
transcript: session.transcript,
|
|
683
|
+
});
|
|
602
684
|
setScroll(0);
|
|
603
685
|
info(`session resumed · ${session.transcript?.length ?? session.messages.length} items`);
|
|
604
686
|
})();
|
|
@@ -632,11 +714,16 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
632
714
|
}
|
|
633
715
|
if (sub === "show" || sub === "list" || sub === "ls") {
|
|
634
716
|
const scope = await loadScope();
|
|
635
|
-
info(scope
|
|
717
|
+
info(scope
|
|
718
|
+
? `scope: ${scope.name ?? "unnamed"} · ${scope.authorizedTargets.join(", ")}`
|
|
719
|
+
: "no engagement scope configured");
|
|
636
720
|
return;
|
|
637
721
|
}
|
|
638
722
|
if (sub === "add") {
|
|
639
|
-
const targets = parts
|
|
723
|
+
const targets = parts
|
|
724
|
+
.join(" ")
|
|
725
|
+
.split(/[\s,]+/)
|
|
726
|
+
.filter(Boolean);
|
|
640
727
|
if (!targets.length) {
|
|
641
728
|
warn("usage: /scope add <target1,target2>");
|
|
642
729
|
return;
|
|
@@ -646,12 +733,18 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
646
733
|
return;
|
|
647
734
|
}
|
|
648
735
|
if (sub === "new" || sub === "set") {
|
|
649
|
-
const targets = parts
|
|
736
|
+
const targets = parts
|
|
737
|
+
.join(" ")
|
|
738
|
+
.split(/[\s,]+/)
|
|
739
|
+
.filter(Boolean);
|
|
650
740
|
if (!targets.length) {
|
|
651
741
|
warn("usage: /scope new <target1,target2>");
|
|
652
742
|
return;
|
|
653
743
|
}
|
|
654
|
-
await saveScope({
|
|
744
|
+
await saveScope({
|
|
745
|
+
authorizedTargets: targets,
|
|
746
|
+
createdAt: new Date().toISOString(),
|
|
747
|
+
});
|
|
655
748
|
info(`scope created · ${targets.join(", ")}`);
|
|
656
749
|
return;
|
|
657
750
|
}
|
|
@@ -691,7 +784,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
691
784
|
return;
|
|
692
785
|
}
|
|
693
786
|
if (sub === "clear-all") {
|
|
694
|
-
const [historyResult, logResult, artifactResult] = await Promise.all([
|
|
787
|
+
const [historyResult, logResult, artifactResult] = await Promise.all([
|
|
788
|
+
clearAllHistory(),
|
|
789
|
+
clearAuditLogs(),
|
|
790
|
+
clearArtifacts(),
|
|
791
|
+
]);
|
|
695
792
|
info(`cleared history (${historyResult.detail || "ok"}), logs (${logResult.removed}), artifacts (${artifactResult.removed})`);
|
|
696
793
|
return;
|
|
697
794
|
}
|
|
@@ -730,16 +827,360 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
730
827
|
active: id === activeSearch,
|
|
731
828
|
configured: keyless || Boolean(secret.value),
|
|
732
829
|
source: keyless ? "keyless" : secret.source,
|
|
733
|
-
maskedKey: secret.value
|
|
830
|
+
maskedKey: secret.value
|
|
831
|
+
? maskSecret(secret.value)
|
|
832
|
+
: undefined,
|
|
734
833
|
};
|
|
735
834
|
}));
|
|
736
|
-
setOverlay({
|
|
835
|
+
setOverlay({
|
|
836
|
+
kind: "pager",
|
|
837
|
+
title: "Credential status",
|
|
838
|
+
body: formatKeyStatus(llm, search),
|
|
839
|
+
});
|
|
737
840
|
})().catch((error) => warn(`could not read keys: ${error instanceof Error ? error.message : String(error)}`));
|
|
738
841
|
return true;
|
|
739
|
-
case "/set":
|
|
740
|
-
|
|
842
|
+
case "/set": {
|
|
843
|
+
const parts = arg.split(/\s+/).filter(Boolean);
|
|
844
|
+
const providerVal = parts[0];
|
|
845
|
+
const keyVal = parts[1];
|
|
846
|
+
void (async () => {
|
|
847
|
+
if (!providerVal) {
|
|
848
|
+
const llm = await listProviderStatuses(provider);
|
|
849
|
+
const activeSearch = getConfig().activeSearchProvider;
|
|
850
|
+
const search = await Promise.all(searchProviderIds.map(async (id) => {
|
|
851
|
+
const secret = await getSearchProviderKey(id);
|
|
852
|
+
const keyless = id === "duckduckgo";
|
|
853
|
+
return {
|
|
854
|
+
provider: id,
|
|
855
|
+
configured: keyless || Boolean(secret.value),
|
|
856
|
+
maskedKey: secret.value ? maskSecret(secret.value) : undefined,
|
|
857
|
+
};
|
|
858
|
+
}));
|
|
859
|
+
setOverlay({
|
|
860
|
+
kind: "picker",
|
|
861
|
+
title: "Set API key for provider",
|
|
862
|
+
options: [
|
|
863
|
+
...llm.map((status) => ({
|
|
864
|
+
value: `llm:${status.provider}`,
|
|
865
|
+
label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ key set") : chalk.red("✗ no key")}${status.active ? " (active)" : ""}`,
|
|
866
|
+
description: status.model,
|
|
867
|
+
})),
|
|
868
|
+
...search.map((status) => ({
|
|
869
|
+
value: `search:${status.provider}`,
|
|
870
|
+
label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ key set") : chalk.red("✗ no key")}`,
|
|
871
|
+
description: `Search provider${status.provider === activeSearch ? " (active)" : ""}`,
|
|
872
|
+
})),
|
|
873
|
+
],
|
|
874
|
+
onSelect: (val) => {
|
|
875
|
+
setOverlay({ kind: "none" });
|
|
876
|
+
void (async () => {
|
|
877
|
+
const isSearch = val.startsWith("search:");
|
|
878
|
+
const id = val.split(":")[1];
|
|
879
|
+
if (isSearch) {
|
|
880
|
+
const next = id;
|
|
881
|
+
if (next === "duckduckgo") {
|
|
882
|
+
info("duckduckgo is keyless and requires no setup");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const secret = await getSearchProviderKey(next);
|
|
886
|
+
if (secret.value) {
|
|
887
|
+
const reset = await new Promise((resolveConfirm) => {
|
|
888
|
+
confirmResolver.current = resolveConfirm;
|
|
889
|
+
dispatch({
|
|
890
|
+
type: "event",
|
|
891
|
+
event: {
|
|
892
|
+
type: "confirm-request",
|
|
893
|
+
id: "c",
|
|
894
|
+
kind: "reset",
|
|
895
|
+
prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
if (!reset) {
|
|
900
|
+
info("cancelled");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const key = await requestSecret({
|
|
905
|
+
title: `${next} API key`,
|
|
906
|
+
prompt: `Enter API key for ${next}:`,
|
|
907
|
+
});
|
|
908
|
+
if (!key) {
|
|
909
|
+
info("cancelled");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
await setSecret("search", next, key.trim());
|
|
913
|
+
info(`saved ${next} ${maskSecret(key.trim())}`);
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
const next = id;
|
|
917
|
+
if (next === "ollama") {
|
|
918
|
+
const key = await requestSecret({
|
|
919
|
+
title: "Ollama host URL",
|
|
920
|
+
prompt: `Enter host URL for Ollama:`,
|
|
921
|
+
});
|
|
922
|
+
if (!key) {
|
|
923
|
+
info("cancelled");
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
updateConfig({ ollamaHost: key.trim() });
|
|
927
|
+
info(`saved ollama host → ${key.trim()}`);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const secret = await getProviderSecret(next);
|
|
931
|
+
if (secret.value) {
|
|
932
|
+
const reset = await new Promise((resolveConfirm) => {
|
|
933
|
+
confirmResolver.current = resolveConfirm;
|
|
934
|
+
dispatch({
|
|
935
|
+
type: "event",
|
|
936
|
+
event: {
|
|
937
|
+
type: "confirm-request",
|
|
938
|
+
id: "c",
|
|
939
|
+
kind: "reset",
|
|
940
|
+
prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
if (!reset) {
|
|
945
|
+
info("cancelled");
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
const key = await requestSecret({
|
|
950
|
+
title: `${next} API key`,
|
|
951
|
+
prompt: `Enter API key for ${next}:`,
|
|
952
|
+
});
|
|
953
|
+
if (!key) {
|
|
954
|
+
info("cancelled");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (!getProvider(next).validateKey(key.trim())) {
|
|
958
|
+
warn(`invalid API key format for ${next}`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
await setProviderSecret(next, key.trim());
|
|
962
|
+
info(`saved ${next} ${maskSecret(key.trim())}`);
|
|
963
|
+
}
|
|
964
|
+
})();
|
|
965
|
+
},
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
try {
|
|
970
|
+
const isSearch = ["brave", "tavily", "duckduckgo"].includes(providerVal);
|
|
971
|
+
if (isSearch) {
|
|
972
|
+
const next = providerVal;
|
|
973
|
+
if (next === "duckduckgo") {
|
|
974
|
+
info("duckduckgo is keyless and requires no setup");
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
let key = keyVal;
|
|
978
|
+
if (!key) {
|
|
979
|
+
const secret = await getSearchProviderKey(next);
|
|
980
|
+
if (secret.value) {
|
|
981
|
+
const reset = await new Promise((resolveConfirm) => {
|
|
982
|
+
confirmResolver.current = resolveConfirm;
|
|
983
|
+
dispatch({
|
|
984
|
+
type: "event",
|
|
985
|
+
event: {
|
|
986
|
+
type: "confirm-request",
|
|
987
|
+
id: "c",
|
|
988
|
+
kind: "reset",
|
|
989
|
+
prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
|
|
990
|
+
},
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
if (!reset) {
|
|
994
|
+
info("cancelled");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
key = await requestSecret({
|
|
999
|
+
title: `${next} API key`,
|
|
1000
|
+
prompt: `Enter API key for ${next}:`,
|
|
1001
|
+
});
|
|
1002
|
+
if (!key) {
|
|
1003
|
+
info("cancelled");
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
await setSecret("search", next, key.trim());
|
|
1008
|
+
info(`saved ${next} ${maskSecret(key.trim())}`);
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
const next = assertProvider(providerVal);
|
|
1012
|
+
if (next === "ollama") {
|
|
1013
|
+
let key = keyVal;
|
|
1014
|
+
if (!key) {
|
|
1015
|
+
key = await requestSecret({
|
|
1016
|
+
title: "Ollama host URL",
|
|
1017
|
+
prompt: `Enter host URL for Ollama:`,
|
|
1018
|
+
});
|
|
1019
|
+
if (!key) {
|
|
1020
|
+
info("cancelled");
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
updateConfig({ ollamaHost: key.trim() });
|
|
1025
|
+
info(`saved ollama host → ${key.trim()}`);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
let key = keyVal;
|
|
1029
|
+
if (!key) {
|
|
1030
|
+
const secret = await getProviderSecret(next);
|
|
1031
|
+
if (secret.value) {
|
|
1032
|
+
const reset = await new Promise((resolveConfirm) => {
|
|
1033
|
+
confirmResolver.current = resolveConfirm;
|
|
1034
|
+
dispatch({
|
|
1035
|
+
type: "event",
|
|
1036
|
+
event: {
|
|
1037
|
+
type: "confirm-request",
|
|
1038
|
+
id: "c",
|
|
1039
|
+
kind: "reset",
|
|
1040
|
+
prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
if (!reset) {
|
|
1045
|
+
info("cancelled");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
key = await requestSecret({
|
|
1050
|
+
title: `${next} API key`,
|
|
1051
|
+
prompt: `Enter API key for ${next}:`,
|
|
1052
|
+
});
|
|
1053
|
+
if (!key) {
|
|
1054
|
+
info("cancelled");
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (!getProvider(next).validateKey(key.trim())) {
|
|
1059
|
+
warn(`invalid API key format for ${next}`);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
await setProviderSecret(next, key.trim());
|
|
1063
|
+
info(`saved ${next} ${maskSecret(key.trim())}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
catch (e) {
|
|
1067
|
+
warn(e.message);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
})();
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
case "/unset": {
|
|
1074
|
+
const parts = arg.split(/\s+/).filter(Boolean);
|
|
1075
|
+
const providerVal = parts[0];
|
|
1076
|
+
void (async () => {
|
|
1077
|
+
if (!providerVal) {
|
|
1078
|
+
const llm = await listProviderStatuses(provider);
|
|
1079
|
+
const activeSearch = getConfig().activeSearchProvider;
|
|
1080
|
+
const search = await Promise.all(searchProviderIds.map(async (id) => {
|
|
1081
|
+
const secret = await getSearchProviderKey(id);
|
|
1082
|
+
const keyless = id === "duckduckgo";
|
|
1083
|
+
return {
|
|
1084
|
+
provider: id,
|
|
1085
|
+
configured: keyless || Boolean(secret.value),
|
|
1086
|
+
maskedKey: secret.value ? maskSecret(secret.value) : undefined,
|
|
1087
|
+
};
|
|
1088
|
+
}));
|
|
1089
|
+
setOverlay({
|
|
1090
|
+
kind: "picker",
|
|
1091
|
+
title: "Unset API key for provider",
|
|
1092
|
+
options: [
|
|
1093
|
+
...llm.map((status) => ({
|
|
1094
|
+
value: `llm:${status.provider}`,
|
|
1095
|
+
label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ ") + (status.maskedKey ?? "key set") : chalk.red("✗ no key")}${status.active ? " (active)" : ""}`,
|
|
1096
|
+
description: status.model,
|
|
1097
|
+
})),
|
|
1098
|
+
...search.map((status) => ({
|
|
1099
|
+
value: `search:${status.provider}`,
|
|
1100
|
+
label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ ") + (status.maskedKey ?? "keyless") : chalk.red("✗ no key")}`,
|
|
1101
|
+
description: `Search provider${status.provider === activeSearch ? " (active)" : ""}`,
|
|
1102
|
+
})),
|
|
1103
|
+
],
|
|
1104
|
+
onSelect: (val) => {
|
|
1105
|
+
setOverlay({ kind: "none" });
|
|
1106
|
+
void (async () => {
|
|
1107
|
+
const isSearch = val.startsWith("search:");
|
|
1108
|
+
const id = val.split(":")[1];
|
|
1109
|
+
if (isSearch) {
|
|
1110
|
+
const next = id;
|
|
1111
|
+
if (next === "duckduckgo") {
|
|
1112
|
+
info("duckduckgo requires no credentials and cannot be unset");
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const secret = await getSearchProviderKey(next);
|
|
1116
|
+
if (!secret.value) {
|
|
1117
|
+
warn(`${next} has no key to unset`);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const { unsetSearchProviderKey } = await import("../commands/search-providers.js");
|
|
1121
|
+
await unsetSearchProviderKey(next);
|
|
1122
|
+
info(`unset ${next}`);
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
const next = id;
|
|
1126
|
+
if (next === "ollama") {
|
|
1127
|
+
info("ollama does not store an API key");
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const secret = await getProviderSecret(next);
|
|
1131
|
+
if (!secret.value) {
|
|
1132
|
+
warn(`${next} has no key to unset`);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
await unsetProviderSecret(next);
|
|
1136
|
+
info(`unset ${next}`);
|
|
1137
|
+
}
|
|
1138
|
+
})();
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
try {
|
|
1144
|
+
const isSearch = ["brave", "tavily", "duckduckgo"].includes(providerVal);
|
|
1145
|
+
if (isSearch) {
|
|
1146
|
+
const next = providerVal;
|
|
1147
|
+
if (next === "duckduckgo") {
|
|
1148
|
+
info("duckduckgo requires no credentials and cannot be unset");
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const secret = await getSearchProviderKey(next);
|
|
1152
|
+
if (!secret.value) {
|
|
1153
|
+
warn(`${next} has no key to unset`);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const { unsetSearchProviderKey } = await import("../commands/search-providers.js");
|
|
1157
|
+
await unsetSearchProviderKey(next);
|
|
1158
|
+
info(`unset ${next}`);
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
const next = assertProvider(providerVal);
|
|
1162
|
+
if (next === "ollama") {
|
|
1163
|
+
info("ollama does not store an API key");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const secret = await getProviderSecret(next);
|
|
1167
|
+
if (!secret.value) {
|
|
1168
|
+
warn(`${next} has no key to unset`);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
await unsetProviderSecret(next);
|
|
1172
|
+
info(`unset ${next}`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
catch (e) {
|
|
1176
|
+
warn(e.message);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
})();
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
741
1182
|
case "/update":
|
|
742
|
-
info(`${cmd} manages
|
|
1183
|
+
info(`${cmd} manages updates; use the equivalent \`clai update\` command outside the TUI`);
|
|
743
1184
|
return true;
|
|
744
1185
|
case "/help":
|
|
745
1186
|
setOverlay({
|
|
@@ -763,9 +1204,30 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
763
1204
|
case "/quit":
|
|
764
1205
|
exitTui();
|
|
765
1206
|
return true;
|
|
766
|
-
default:
|
|
1207
|
+
default:
|
|
1208
|
+
return false;
|
|
767
1209
|
}
|
|
768
|
-
}, [
|
|
1210
|
+
}, [
|
|
1211
|
+
activateProvider,
|
|
1212
|
+
activateSearchProvider,
|
|
1213
|
+
chooseModel,
|
|
1214
|
+
chooseProvider,
|
|
1215
|
+
chooseReasoning,
|
|
1216
|
+
chooseSearchProvider,
|
|
1217
|
+
compacting,
|
|
1218
|
+
exitTui,
|
|
1219
|
+
mouseMode,
|
|
1220
|
+
noHistory,
|
|
1221
|
+
provider,
|
|
1222
|
+
runner,
|
|
1223
|
+
runImplement,
|
|
1224
|
+
lastToolOutput,
|
|
1225
|
+
openToolOutput,
|
|
1226
|
+
setReasoning,
|
|
1227
|
+
state.items,
|
|
1228
|
+
requestSecret,
|
|
1229
|
+
setOverlay,
|
|
1230
|
+
]);
|
|
769
1231
|
const submitText = useCallback((text) => {
|
|
770
1232
|
const trimmed = text.trim();
|
|
771
1233
|
if (!trimmed)
|
|
@@ -780,7 +1242,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
780
1242
|
// token is an actual clai command; file paths remain normal prompts.
|
|
781
1243
|
if (trimmed.startsWith("/") && isKnownSlashCommand(trimmed)) {
|
|
782
1244
|
if (!handleLocalSlash(trimmed))
|
|
783
|
-
dispatch({
|
|
1245
|
+
dispatch({
|
|
1246
|
+
type: "notice",
|
|
1247
|
+
level: "warn",
|
|
1248
|
+
text: `unknown command: ${trimmed}`,
|
|
1249
|
+
});
|
|
784
1250
|
return;
|
|
785
1251
|
}
|
|
786
1252
|
if (shouldStoreInPromptHistory(trimmed))
|
|
@@ -821,7 +1287,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
821
1287
|
const statusH = state.pendingConfirm ? 6 : secretRequest ? 7 : 1;
|
|
822
1288
|
const composerH = 3;
|
|
823
1289
|
const maxMenuRows = Math.max(3, usableRows - headerH - statusH - composerH - 3);
|
|
824
|
-
const menuH = slashMenuOpen
|
|
1290
|
+
const menuH = slashMenuOpen
|
|
1291
|
+
? Math.min(suggestions.length, maxMenuRows)
|
|
1292
|
+
: fileMenuOpen
|
|
1293
|
+
? fileSuggestions.length
|
|
1294
|
+
: 0;
|
|
825
1295
|
const viewportH = Math.max(3, usableRows - headerH - statusH - composerH - menuH);
|
|
826
1296
|
const slashWindowStart = slashMenuOpen
|
|
827
1297
|
? Math.min(Math.max(0, selected - Math.floor(menuH / 2)), Math.max(0, suggestions.length - menuH))
|
|
@@ -855,7 +1325,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
855
1325
|
setScroll((value) => Math.max(0, value - 3));
|
|
856
1326
|
};
|
|
857
1327
|
stdin.on("data", onData);
|
|
858
|
-
return () => {
|
|
1328
|
+
return () => {
|
|
1329
|
+
stdin.off("data", onData);
|
|
1330
|
+
};
|
|
859
1331
|
}, [stdin, modalActive, maxOffset]);
|
|
860
1332
|
// ── Key handling ────────────────────────────────────────────────────────────
|
|
861
1333
|
useInput((ch, key) => {
|
|
@@ -870,9 +1342,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
870
1342
|
return;
|
|
871
1343
|
}
|
|
872
1344
|
if (key.ctrl && ch === "o") {
|
|
873
|
-
const
|
|
874
|
-
if (
|
|
875
|
-
|
|
1345
|
+
const hasOutput = state.items.some((item) => item.kind === "tool" && Boolean(item.output));
|
|
1346
|
+
if (hasOutput)
|
|
1347
|
+
dispatch({ type: "toggle-output" });
|
|
876
1348
|
else
|
|
877
1349
|
dispatch({ type: "notice", level: "info", text: "no tool output yet" });
|
|
878
1350
|
return;
|
|
@@ -881,9 +1353,17 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
881
1353
|
void (async () => {
|
|
882
1354
|
const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
|
|
883
1355
|
if (plan)
|
|
884
|
-
setOverlay({
|
|
1356
|
+
setOverlay({
|
|
1357
|
+
kind: "pager",
|
|
1358
|
+
title: "Plan",
|
|
1359
|
+
body: renderPlanDocument(plan),
|
|
1360
|
+
});
|
|
885
1361
|
else
|
|
886
|
-
dispatch({
|
|
1362
|
+
dispatch({
|
|
1363
|
+
type: "notice",
|
|
1364
|
+
level: "info",
|
|
1365
|
+
text: "no active plan yet",
|
|
1366
|
+
});
|
|
887
1367
|
})();
|
|
888
1368
|
return;
|
|
889
1369
|
}
|
|
@@ -917,7 +1397,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
917
1397
|
return;
|
|
918
1398
|
}
|
|
919
1399
|
if (key.escape) {
|
|
920
|
-
if (
|
|
1400
|
+
if (state.outputExpanded)
|
|
1401
|
+
dispatch({ type: "toggle-output" });
|
|
1402
|
+
else if (runner.isRunning())
|
|
921
1403
|
runner.abort();
|
|
922
1404
|
else if (input) {
|
|
923
1405
|
setInput("");
|
|
@@ -984,7 +1466,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
984
1466
|
return;
|
|
985
1467
|
if (historyIdx.current < 0)
|
|
986
1468
|
historyDraft.current = input;
|
|
987
|
-
const idx = historyIdx.current < 0
|
|
1469
|
+
const idx = historyIdx.current < 0
|
|
1470
|
+
? history.current.length - 1
|
|
1471
|
+
: Math.max(0, historyIdx.current - 1);
|
|
988
1472
|
historyIdx.current = idx;
|
|
989
1473
|
const v = history.current[idx] ?? "";
|
|
990
1474
|
setInput(v);
|
|
@@ -1044,17 +1528,31 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
1044
1528
|
const before = input.slice(0, cursor);
|
|
1045
1529
|
const at = input.slice(cursor, cursor + 1) || " ";
|
|
1046
1530
|
const after = input.slice(cursor + 1);
|
|
1047
|
-
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: [
|
|
1531
|
+
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
|
|
1048
1532
|
? visibleSlashSuggestions.map((cmd, i) => {
|
|
1049
1533
|
const absoluteIndex = slashWindowStart + i;
|
|
1050
|
-
return (_jsxs(Text, { wrap: "truncate-end", backgroundColor: absoluteIndex === selected
|
|
1051
|
-
|
|
1052
|
-
|
|
1534
|
+
return (_jsxs(Text, { wrap: "truncate-end", backgroundColor: absoluteIndex === selected
|
|
1535
|
+
? "#2563EB"
|
|
1536
|
+
: absoluteIndex % 2 === 0
|
|
1537
|
+
? "#1E293B"
|
|
1538
|
+
: "#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 &&
|
|
1539
|
+
slashWindowStart + menuH < suggestions.length ? (_jsx(Text, { dimColor: true, children: ` · ${suggestions.length - slashWindowStart - menuH} more ↓` })) : null] }, cmd.command));
|
|
1053
1540
|
})
|
|
1054
1541
|
: null, fileMenuOpen && !modalActive
|
|
1055
|
-
? fileSuggestions.map((file, i) => (_jsxs(Text, { wrap: "truncate-end", backgroundColor: i === selected ? "#2563EB" : i % 2 === 0 ? "#1E293B" : "#0F172A", children: [_jsxs(Text, { color: i === selected
|
|
1056
|
-
|
|
1542
|
+
? fileSuggestions.map((file, i) => (_jsxs(Text, { wrap: "truncate-end", backgroundColor: i === selected ? "#2563EB" : i % 2 === 0 ? "#1E293B" : "#0F172A", children: [_jsxs(Text, { color: i === selected
|
|
1543
|
+
? "#FFFFFF"
|
|
1544
|
+
: file.isDir
|
|
1545
|
+
? "#67E8F9"
|
|
1546
|
+
: "#F8FAFC", bold: i === selected, children: [i === selected ? "❯ " : " ", file.isDir ? "▸ " : "· ", file.value] }), _jsx(Text, { dimColor: true, children: file.isDir ? " directory" : " attach file" })] }, file.value)))
|
|
1547
|
+
: 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
|
|
1548
|
+
? "yellow"
|
|
1549
|
+
: state.status.running
|
|
1550
|
+
? "yellow"
|
|
1551
|
+
: "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
|
|
1057
1552
|
? "type to queue a message…"
|
|
1058
|
-
: "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 &&
|
|
1553
|
+
: "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 &&
|
|
1554
|
+
!state.pendingConfirm &&
|
|
1555
|
+
!state.status.running &&
|
|
1556
|
+
!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] }));
|
|
1059
1557
|
}
|
|
1060
1558
|
//# sourceMappingURL=App.js.map
|