@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.
- package/README.md +46 -2
- package/dist/agent/context-manager.d.ts +14 -0
- package/dist/agent/context-manager.js +61 -0
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/runner.d.ts +6 -0
- package/dist/agent/runner.js +24 -3
- package/dist/agent/runner.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/index.js +3 -17
- package/dist/index.js.map +1 -1
- package/dist/modes/agent.d.ts +4 -0
- package/dist/modes/agent.js.map +1 -1
- package/dist/repl.d.ts +5 -0
- package/dist/repl.js +8 -2
- package/dist/repl.js.map +1 -1
- package/dist/store/history.d.ts +7 -1
- package/dist/store/history.js +34 -3
- package/dist/store/history.js.map +1 -1
- package/dist/tools/registry.d.ts +4 -0
- package/dist/tools/registry.js +57 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +2 -0
- package/dist/tools/shell.js +5 -1
- package/dist/tools/shell.js.map +1 -1
- package/dist/tui/App.d.ts +2 -1
- package/dist/tui/App.js +696 -100
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/ConfirmModal.js +2 -2
- package/dist/tui/components/ConfirmModal.js.map +1 -1
- package/dist/tui/components/Pager.d.ts +6 -5
- package/dist/tui/components/Pager.js +17 -26
- package/dist/tui/components/Pager.js.map +1 -1
- package/dist/tui/components/PickerPanel.d.ts +13 -0
- package/dist/tui/components/PickerPanel.js +68 -0
- package/dist/tui/components/PickerPanel.js.map +1 -0
- package/dist/tui/components/SecretInputPanel.d.ts +6 -0
- package/dist/tui/components/SecretInputPanel.js +25 -0
- package/dist/tui/components/SecretInputPanel.js.map +1 -0
- package/dist/tui/default.d.ts +6 -0
- package/dist/tui/default.js +11 -0
- package/dist/tui/default.js.map +1 -0
- package/dist/tui/hooks/useAgentRunner.d.ts +13 -2
- package/dist/tui/hooks/useAgentRunner.js +48 -3
- package/dist/tui/hooks/useAgentRunner.js.map +1 -1
- package/dist/tui/hooks/useTerminalSize.d.ts +9 -0
- package/dist/tui/hooks/useTerminalSize.js +26 -0
- package/dist/tui/hooks/useTerminalSize.js.map +1 -0
- package/dist/tui/index.js +23 -10
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/render-lines.d.ts +15 -0
- package/dist/tui/render-lines.js +165 -0
- package/dist/tui/render-lines.js.map +1 -0
- package/dist/tui/state.d.ts +12 -10
- package/dist/tui/state.js +83 -57
- package/dist/tui/state.js.map +1 -1
- package/package.json +1 -1
- package/dist/tui/components/Composer.d.ts +0 -6
- package/dist/tui/components/Composer.js +0 -122
- package/dist/tui/components/Composer.js.map +0 -1
- package/dist/tui/components/Header.d.ts +0 -7
- package/dist/tui/components/Header.js +0 -6
- package/dist/tui/components/Header.js.map +0 -1
- package/dist/tui/components/StatusLine.d.ts +0 -7
- package/dist/tui/components/StatusLine.js +0 -25
- package/dist/tui/components/StatusLine.js.map +0 -1
- package/dist/tui/components/items.d.ts +0 -5
- package/dist/tui/components/items.js +0 -57
- 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,
|
|
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 {
|
|
6
|
-
import {
|
|
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 {
|
|
9
|
-
import {
|
|
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 {
|
|
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 {
|
|
16
|
-
import {
|
|
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
|
-
|
|
42
|
+
const MAX_FILE_SUGGESTIONS = 6;
|
|
43
|
+
export function App({ version, initialMode, provider: initialProvider, initialModel, noHistory = false }) {
|
|
31
44
|
const { exit } = useApp();
|
|
32
|
-
const {
|
|
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
|
-
|
|
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((
|
|
45
|
-
confirmResolver.current =
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
244
|
+
info("mode → ask");
|
|
115
245
|
return true;
|
|
116
246
|
case "/agent":
|
|
117
247
|
setMode("agent");
|
|
118
248
|
setDefaultMode("agent");
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
154
|
-
if (
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
+
chooseProvider();
|
|
176
324
|
return true;
|
|
177
325
|
}
|
|
178
326
|
try {
|
|
179
327
|
const next = assertProvider(arg);
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
592
|
+
exitTui();
|
|
201
593
|
return true;
|
|
202
|
-
default:
|
|
203
|
-
return false;
|
|
594
|
+
default: return false;
|
|
204
595
|
}
|
|
205
|
-
}, [
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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 &&
|
|
681
|
+
if (key.ctrl && ch === "o") {
|
|
234
682
|
const last = lastToolOutput();
|
|
235
683
|
if (last)
|
|
236
|
-
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|