@pentoshi/clai 2.0.26 → 2.0.28
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 +1 -1
- package/dist/agent/context-manager.d.ts +15 -2
- package/dist/agent/context-manager.js +41 -70
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/events.d.ts +1 -1
- package/dist/agent/runner.d.ts +38 -1
- package/dist/agent/runner.js +719 -135
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/session-title.d.ts +27 -0
- package/dist/agent/session-title.js +109 -0
- package/dist/agent/session-title.js.map +1 -0
- package/dist/commands/update.js +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/http.js +20 -6
- package/dist/llm/http.js.map +1 -1
- package/dist/modes/agent.d.ts +1 -0
- package/dist/modes/agent.js.map +1 -1
- package/dist/modes/ask.d.ts +29 -0
- package/dist/modes/ask.js +188 -23
- package/dist/modes/ask.js.map +1 -1
- package/dist/prompts/index.d.ts +2 -2
- package/dist/prompts/index.js +32 -9
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.js +104 -3
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.js +66 -7
- package/dist/safety/classifier.js.map +1 -1
- package/dist/store/history.js +68 -48
- package/dist/store/history.js.map +1 -1
- package/dist/tools/fs.d.ts +4 -0
- package/dist/tools/fs.js +40 -2
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/registry.d.ts +8 -0
- package/dist/tools/registry.js +3 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tui/App.js +242 -33
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/ConfirmModal.js +14 -2
- package/dist/tui/components/ConfirmModal.js.map +1 -1
- package/dist/tui/components/PickerPanel.d.ts +2 -1
- package/dist/tui/components/PickerPanel.js +73 -23
- package/dist/tui/components/PickerPanel.js.map +1 -1
- package/dist/tui/confirm.d.ts +1 -1
- package/dist/tui/confirm.js +8 -0
- package/dist/tui/confirm.js.map +1 -1
- package/dist/tui/hooks/useAgentRunner.d.ts +27 -2
- package/dist/tui/hooks/useAgentRunner.js +149 -18
- package/dist/tui/hooks/useAgentRunner.js.map +1 -1
- package/dist/tui/render-lines.js +28 -1
- package/dist/tui/render-lines.js.map +1 -1
- package/dist/tui/state.d.ts +1 -1
- package/dist/ui/markdown.js +63 -30
- package/dist/ui/markdown.js.map +1 -1
- package/package.json +3 -2
package/dist/tui/App.js
CHANGED
|
@@ -16,6 +16,7 @@ import { modelSupportsThinking, modelSupportsVision, } from "../llm/capabilities
|
|
|
16
16
|
import { getConfig, getProviderModel, setDefaultMode, setDefaultProvider, setProviderModel, setThinking, updateConfig, } from "../store/config.js";
|
|
17
17
|
import { estimateMessagesTokens } from "../agent/context-manager.js";
|
|
18
18
|
import { clearAllHistory, getSession, listSessions, saveSession, upsertSession, } from "../store/history.js";
|
|
19
|
+
import { generateSessionTitle } from "../agent/session-title.js";
|
|
19
20
|
import { safeCwd } from "../os/cwd.js";
|
|
20
21
|
import { resolve } from "node:path";
|
|
21
22
|
import { existsSync } from "node:fs";
|
|
@@ -41,6 +42,18 @@ import { addScopeTargets, clearScope, loadScope, saveScope, } from "../store/sco
|
|
|
41
42
|
import { formatKeyStatus } from "./format-keys.js";
|
|
42
43
|
import { shouldStoreInPromptHistory } from "./input-history.js";
|
|
43
44
|
import { DISABLE_MOUSE_REPORTING, ENABLE_MOUSE_REPORTING, isMouseReport, mouseWheelDirection, stripMouseReports, } from "./mouse.js";
|
|
45
|
+
function truncateMiddle(text, maxWidth) {
|
|
46
|
+
if (maxWidth <= 0)
|
|
47
|
+
return "";
|
|
48
|
+
if (text.length <= maxWidth)
|
|
49
|
+
return text;
|
|
50
|
+
if (maxWidth <= 1)
|
|
51
|
+
return "…";
|
|
52
|
+
const keep = maxWidth - 1;
|
|
53
|
+
const head = Math.ceil(keep * 0.6);
|
|
54
|
+
const tail = Math.floor(keep * 0.4);
|
|
55
|
+
return `${text.slice(0, head)}…${text.slice(Math.max(0, text.length - tail))}`;
|
|
56
|
+
}
|
|
44
57
|
function wrapPlainString(text, width) {
|
|
45
58
|
if (text.length === 0) {
|
|
46
59
|
return [{ lineText: "", startIdx: 0, endIdx: 0 }];
|
|
@@ -51,7 +64,11 @@ function wrapPlainString(text, width) {
|
|
|
51
64
|
for (let p = 0; p < paragraphs.length; p++) {
|
|
52
65
|
const para = paragraphs[p];
|
|
53
66
|
if (para.length === 0) {
|
|
54
|
-
lines.push({
|
|
67
|
+
lines.push({
|
|
68
|
+
lineText: "",
|
|
69
|
+
startIdx: currentOffset,
|
|
70
|
+
endIdx: currentOffset,
|
|
71
|
+
});
|
|
55
72
|
}
|
|
56
73
|
else {
|
|
57
74
|
let idx = 0;
|
|
@@ -86,6 +103,37 @@ const IMPLEMENT_PROMPT = "I approve the plan. Execute it now in STRICT ORDER. Ta
|
|
|
86
103
|
"Do NOT call web.search — you already know everything needed. " +
|
|
87
104
|
"Run real commands (installs, servers, verification) — do not claim anything ran without a successful tool call.";
|
|
88
105
|
const MAX_FILE_SUGGESTIONS = 6;
|
|
106
|
+
/** Compact "time ago" label for history rows, e.g. "3m", "5h", "2d". */
|
|
107
|
+
function relativeTime(iso) {
|
|
108
|
+
const then = Date.parse(iso);
|
|
109
|
+
if (Number.isNaN(then))
|
|
110
|
+
return "";
|
|
111
|
+
const diffMs = Date.now() - then;
|
|
112
|
+
if (diffMs < 0)
|
|
113
|
+
return "just now";
|
|
114
|
+
const sec = Math.floor(diffMs / 1000);
|
|
115
|
+
if (sec < 60)
|
|
116
|
+
return "just now";
|
|
117
|
+
const min = Math.floor(sec / 60);
|
|
118
|
+
if (min < 60)
|
|
119
|
+
return `${min}m ago`;
|
|
120
|
+
const hr = Math.floor(min / 60);
|
|
121
|
+
if (hr < 24)
|
|
122
|
+
return `${hr}h ago`;
|
|
123
|
+
const day = Math.floor(hr / 24);
|
|
124
|
+
if (day < 30)
|
|
125
|
+
return `${day}d ago`;
|
|
126
|
+
const mon = Math.floor(day / 30);
|
|
127
|
+
if (mon < 12)
|
|
128
|
+
return `${mon}mo ago`;
|
|
129
|
+
return `${Math.floor(mon / 12)}y ago`;
|
|
130
|
+
}
|
|
131
|
+
/** Last path segment of a cwd, for a short location hint in history rows. */
|
|
132
|
+
function shortCwd(cwd) {
|
|
133
|
+
const trimmed = cwd.replace(/[\\/]+$/, "");
|
|
134
|
+
const base = trimmed.split(/[\\/]/).pop() ?? trimmed;
|
|
135
|
+
return base || trimmed;
|
|
136
|
+
}
|
|
89
137
|
export function App({ version, initialMode, provider: initialProvider, initialModel, noHistory = false, }) {
|
|
90
138
|
const { exit } = useApp();
|
|
91
139
|
const { stdin } = useStdin();
|
|
@@ -113,6 +161,14 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
113
161
|
const autosaveTimer = useRef(undefined);
|
|
114
162
|
const latestItems = useRef(state.items);
|
|
115
163
|
latestItems.current = state.items;
|
|
164
|
+
// Tracks model-generated session titles. `titledAtUserCount` records the
|
|
165
|
+
// number of user turns the current title was generated from, so we can
|
|
166
|
+
// refresh the title as the conversation grows past it without regenerating
|
|
167
|
+
// on every single turn. Reset whenever a fresh session starts.
|
|
168
|
+
const titleRef = useRef({
|
|
169
|
+
titledAtUserCount: 0,
|
|
170
|
+
inFlight: false,
|
|
171
|
+
});
|
|
116
172
|
const compactAbortRef = useRef(undefined);
|
|
117
173
|
const cancelCompaction = useCallback(() => {
|
|
118
174
|
if (compactAbortRef.current) {
|
|
@@ -152,6 +208,11 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
152
208
|
confirm: confirmController.port,
|
|
153
209
|
getContext: useCallback(() => ctxRef.current, []),
|
|
154
210
|
requestSecret,
|
|
211
|
+
onSwitchToAgent: useCallback(() => {
|
|
212
|
+
setMode("agent");
|
|
213
|
+
setDefaultMode("agent");
|
|
214
|
+
dispatch({ type: "notice", level: "info", text: "mode → agent" });
|
|
215
|
+
}, []),
|
|
155
216
|
});
|
|
156
217
|
const exitTui = useCallback(() => {
|
|
157
218
|
void (async () => {
|
|
@@ -162,6 +223,46 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
162
223
|
exit();
|
|
163
224
|
})();
|
|
164
225
|
}, [exit, noHistory, runner, state.items]);
|
|
226
|
+
// Generate (and later refresh) a concise, model-written title for the
|
|
227
|
+
// active session. Runs only when a real exchange exists, throttles by user
|
|
228
|
+
// turn count, and guards against overlapping requests and session resets.
|
|
229
|
+
const maybeUpdateTitle = useCallback((sessionId) => {
|
|
230
|
+
if (noHistory || getConfig().privateMode)
|
|
231
|
+
return;
|
|
232
|
+
if (titleRef.current.inFlight)
|
|
233
|
+
return;
|
|
234
|
+
const messages = runner.getMessages();
|
|
235
|
+
const userCount = messages.filter((m) => m.role === "user").length;
|
|
236
|
+
const hasAssistant = messages.some((m) => m.role === "assistant" && m.content.trim().length > 0);
|
|
237
|
+
if (userCount === 0 || !hasAssistant)
|
|
238
|
+
return;
|
|
239
|
+
const titledAt = titleRef.current.titledAtUserCount;
|
|
240
|
+
// Title on the first completed exchange, then refresh once two more
|
|
241
|
+
// user turns have accumulated so the name tracks the evolving topic.
|
|
242
|
+
const shouldGenerate = titledAt === 0 || userCount - titledAt >= 2;
|
|
243
|
+
if (!shouldGenerate)
|
|
244
|
+
return;
|
|
245
|
+
titleRef.current.inFlight = true;
|
|
246
|
+
const targetCount = userCount;
|
|
247
|
+
const ctx = ctxRef.current;
|
|
248
|
+
void generateSessionTitle(messages, {
|
|
249
|
+
provider: ctx.provider,
|
|
250
|
+
model: ctx.model,
|
|
251
|
+
})
|
|
252
|
+
.then((title) => {
|
|
253
|
+
titleRef.current.inFlight = false;
|
|
254
|
+
if (!title)
|
|
255
|
+
return;
|
|
256
|
+
// Discard if the user started a new session while we were waiting.
|
|
257
|
+
if (runner.getSession().sessionId !== sessionId)
|
|
258
|
+
return;
|
|
259
|
+
titleRef.current.titledAtUserCount = targetCount;
|
|
260
|
+
void upsertSession(sessionId, runner.getMessages(), title, latestItems.current).catch(() => undefined);
|
|
261
|
+
})
|
|
262
|
+
.catch(() => {
|
|
263
|
+
titleRef.current.inFlight = false;
|
|
264
|
+
});
|
|
265
|
+
}, [noHistory, runner]);
|
|
165
266
|
useEffect(() => {
|
|
166
267
|
if (noHistory || getConfig().privateMode)
|
|
167
268
|
return;
|
|
@@ -171,22 +272,27 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
171
272
|
clearTimeout(autosaveTimer.current);
|
|
172
273
|
autosaveTimer.current = setTimeout(() => {
|
|
173
274
|
const messages = runner.getMessages();
|
|
174
|
-
|
|
275
|
+
// Only persist sessions that contain an actual conversation. This keeps
|
|
276
|
+
// `/new` with no input, or a cleared-then-exited session (notices only,
|
|
277
|
+
// no user turns), out of the history list.
|
|
278
|
+
if (!messages.some((m) => m.role === "user"))
|
|
175
279
|
return;
|
|
176
|
-
|
|
280
|
+
const sessionId = runner.getSession().sessionId;
|
|
281
|
+
void upsertSession(sessionId, messages, undefined, state.items).catch(() => undefined);
|
|
282
|
+
maybeUpdateTitle(sessionId);
|
|
177
283
|
}, 250);
|
|
178
284
|
return () => {
|
|
179
285
|
if (autosaveTimer.current)
|
|
180
286
|
clearTimeout(autosaveTimer.current);
|
|
181
287
|
};
|
|
182
|
-
}, [noHistory, runner, state.items]);
|
|
288
|
+
}, [noHistory, runner, state.items, maybeUpdateTitle]);
|
|
183
289
|
useEffect(() => () => {
|
|
184
290
|
if (autosaveTimer.current)
|
|
185
291
|
clearTimeout(autosaveTimer.current);
|
|
186
292
|
const messages = runner.getMessages();
|
|
187
293
|
if (!noHistory &&
|
|
188
294
|
!getConfig().privateMode &&
|
|
189
|
-
|
|
295
|
+
messages.some((m) => m.role === "user")) {
|
|
190
296
|
void upsertSession(runner.getSession().sessionId, messages, undefined, latestItems.current).catch(() => undefined);
|
|
191
297
|
}
|
|
192
298
|
}, [noHistory, runner]);
|
|
@@ -206,13 +312,20 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
206
312
|
}, [provider, model, startTurn]);
|
|
207
313
|
useEffect(() => {
|
|
208
314
|
if (!state.status.running &&
|
|
315
|
+
!state.pendingConfirm &&
|
|
209
316
|
state.queued.length > 0 &&
|
|
210
317
|
!runner.isRunning()) {
|
|
211
318
|
const next = state.queued[0];
|
|
212
319
|
dispatch({ type: "dequeue" });
|
|
213
320
|
beginTurn(next);
|
|
214
321
|
}
|
|
215
|
-
}, [
|
|
322
|
+
}, [
|
|
323
|
+
state.status.running,
|
|
324
|
+
state.pendingConfirm,
|
|
325
|
+
state.queued,
|
|
326
|
+
runner,
|
|
327
|
+
beginTurn,
|
|
328
|
+
]);
|
|
216
329
|
const lastToolOutput = useCallback(() => {
|
|
217
330
|
for (let i = state.items.length - 1; i >= 0; i -= 1) {
|
|
218
331
|
const item = state.items[i];
|
|
@@ -257,6 +370,52 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
257
370
|
});
|
|
258
371
|
void startTurn("/implement", IMPLEMENT_PROMPT);
|
|
259
372
|
}, [runner, startTurn]);
|
|
373
|
+
// After a turn ends, if the agent just created (or revised) a plan that is
|
|
374
|
+
// still a draft awaiting approval, prompt the user with a yes/no modal to
|
|
375
|
+
// implement it now or discard it. /implement and /discard remain available.
|
|
376
|
+
const maybePromptPlanApproval = useCallback(async () => {
|
|
377
|
+
const session = runner.getSession();
|
|
378
|
+
if (session.planApproved.value)
|
|
379
|
+
return;
|
|
380
|
+
if (confirmResolver.current)
|
|
381
|
+
return; // a confirm is already on screen
|
|
382
|
+
const plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
383
|
+
if (!plan || plan.status !== "draft" || plan.tasks.length === 0)
|
|
384
|
+
return;
|
|
385
|
+
const ok = await new Promise((res) => {
|
|
386
|
+
confirmResolver.current = res;
|
|
387
|
+
dispatch({
|
|
388
|
+
type: "event",
|
|
389
|
+
event: {
|
|
390
|
+
type: "confirm-request",
|
|
391
|
+
id: "plan",
|
|
392
|
+
kind: "plan",
|
|
393
|
+
prompt: `Implement this plan now? "${plan.goal}" — ${plan.tasks.length} task(s). (Y to implement · N to discard)`,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
if (ok) {
|
|
398
|
+
await runImplement();
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
await deletePlan(session.sessionId).catch(() => undefined);
|
|
402
|
+
session.planApproved.value = false;
|
|
403
|
+
dispatch({
|
|
404
|
+
type: "notice",
|
|
405
|
+
level: "info",
|
|
406
|
+
text: `plan discarded · ${plan.goal}`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}, [runner, runImplement]);
|
|
410
|
+
// Detect a turn finishing (running true → false) and offer plan approval.
|
|
411
|
+
const prevRunningRef = useRef(false);
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
const wasRunning = prevRunningRef.current;
|
|
414
|
+
prevRunningRef.current = state.status.running;
|
|
415
|
+
if (wasRunning && !state.status.running) {
|
|
416
|
+
void maybePromptPlanApproval();
|
|
417
|
+
}
|
|
418
|
+
}, [state.status.running, maybePromptPlanApproval]);
|
|
260
419
|
const chooseModel = useCallback(async () => {
|
|
261
420
|
const providerImpl = getProvider(provider);
|
|
262
421
|
let models = [];
|
|
@@ -467,6 +626,7 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
467
626
|
if (!noHistory && !getConfig().privateMode && messages.length > 0)
|
|
468
627
|
await saveSession(messages, undefined, state.items).catch(() => undefined);
|
|
469
628
|
runner.reset();
|
|
629
|
+
titleRef.current = { titledAtUserCount: 0, inFlight: false };
|
|
470
630
|
dispatch({ type: "reset" });
|
|
471
631
|
info("fresh session started");
|
|
472
632
|
})();
|
|
@@ -474,6 +634,7 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
474
634
|
case "/clean":
|
|
475
635
|
cancelCompaction();
|
|
476
636
|
runner.reset();
|
|
637
|
+
titleRef.current = { titledAtUserCount: 0, inFlight: false };
|
|
477
638
|
dispatch({ type: "reset" });
|
|
478
639
|
info("fresh session started");
|
|
479
640
|
return true;
|
|
@@ -667,24 +828,26 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
667
828
|
.then((result) => {
|
|
668
829
|
if (ac.signal.aborted)
|
|
669
830
|
return;
|
|
670
|
-
if (result.
|
|
671
|
-
info("nothing to compact yet — more
|
|
672
|
-
|
|
673
|
-
else {
|
|
674
|
-
const memoMsg = result.messages.find((m) => m.role === "system" &&
|
|
675
|
-
(m.content.startsWith("Session memory") ||
|
|
676
|
-
m.content.startsWith("Earlier turns"))) ?? result.messages.find((m, i) => i > 0 && m.role === "system");
|
|
677
|
-
const summary = !result.summarized && fullSession.trim()
|
|
678
|
-
? `Earlier turns in this session, summarized to fit the context budget. Full local transcript shown for review; the model received a compact fallback memory.\n\n${fullSession}`
|
|
679
|
-
: memoMsg
|
|
680
|
-
? memoMsg.content
|
|
681
|
-
: "Compacted context";
|
|
682
|
-
dispatch({ type: "compacted", summary, keepRecent: 2 });
|
|
683
|
-
info(`compacted ${result.before} → ${result.after} messages · ~${result.beforeTokens.toLocaleString()} → ~${result.afterTokens.toLocaleString()} tokens${result.summarized ? "" : " · local fallback"}`);
|
|
831
|
+
if (!result.summarized || result.after === result.before) {
|
|
832
|
+
info("nothing to compact yet — more conversation is needed");
|
|
833
|
+
return;
|
|
684
834
|
}
|
|
835
|
+
const memoMsg = result.messages.find((m) => m.role === "system" &&
|
|
836
|
+
m.content.startsWith("Session memory")) ??
|
|
837
|
+
result.messages.find((m, i) => i > 0 && m.role === "system");
|
|
838
|
+
const summary = memoMsg ? memoMsg.content : "Compacted context";
|
|
839
|
+
dispatch({ type: "compacted", summary, keepRecent: 2 });
|
|
840
|
+
const freed = Math.max(0, result.beforeTokens - result.afterTokens);
|
|
841
|
+
const pct = result.beforeTokens > 0
|
|
842
|
+
? Math.round((freed / result.beforeTokens) * 100)
|
|
843
|
+
: 0;
|
|
844
|
+
info(`context compacted — earlier turns summarized into a memory · ` +
|
|
845
|
+
`freed ~${freed.toLocaleString()} tokens (${pct}% smaller, ` +
|
|
846
|
+
`~${result.beforeTokens.toLocaleString()} → ~${result.afterTokens.toLocaleString()} est.)`);
|
|
685
847
|
})
|
|
686
848
|
.catch((error) => {
|
|
687
|
-
if (ac.signal.aborted ||
|
|
849
|
+
if (ac.signal.aborted ||
|
|
850
|
+
(error instanceof Error && error.name === "AbortError")) {
|
|
688
851
|
info("compaction cancelled");
|
|
689
852
|
}
|
|
690
853
|
else {
|
|
@@ -721,22 +884,37 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
721
884
|
setOverlay({
|
|
722
885
|
kind: "picker",
|
|
723
886
|
title: "Session history",
|
|
887
|
+
twoLine: true,
|
|
724
888
|
options: [
|
|
725
889
|
...(currentMessages.length
|
|
726
890
|
? [
|
|
727
891
|
{
|
|
728
892
|
value: "__current__",
|
|
729
893
|
label: "Current session",
|
|
730
|
-
description:
|
|
894
|
+
description: `active now · ${currentMessages.length} messages`,
|
|
731
895
|
active: true,
|
|
732
896
|
},
|
|
733
897
|
]
|
|
734
898
|
: []),
|
|
735
|
-
...sessions.map((session) =>
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
899
|
+
...sessions.map((session) => {
|
|
900
|
+
const date = session.updatedAt ?? session.createdAt;
|
|
901
|
+
const when = relativeTime(date);
|
|
902
|
+
const stamp = date.slice(0, 16).replace("T", " ");
|
|
903
|
+
const count = session.transcript?.length ?? session.messages.length;
|
|
904
|
+
const where = shortCwd(session.cwd);
|
|
905
|
+
const meta = [
|
|
906
|
+
when ? `${when} · ${stamp}` : stamp,
|
|
907
|
+
`${count} msgs`,
|
|
908
|
+
where,
|
|
909
|
+
]
|
|
910
|
+
.filter(Boolean)
|
|
911
|
+
.join(" · ");
|
|
912
|
+
return {
|
|
913
|
+
value: session.id,
|
|
914
|
+
label: session.name ?? session.id,
|
|
915
|
+
description: meta,
|
|
916
|
+
};
|
|
917
|
+
}),
|
|
740
918
|
],
|
|
741
919
|
onSelect: (id) => {
|
|
742
920
|
void (async () => {
|
|
@@ -751,7 +929,13 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
751
929
|
return;
|
|
752
930
|
}
|
|
753
931
|
cancelCompaction();
|
|
754
|
-
runner.setMessages(session.messages);
|
|
932
|
+
runner.setMessages(session.messages, session.id);
|
|
933
|
+
// Keep the resumed session's existing title; only refresh
|
|
934
|
+
// once the user adds new turns on top of it.
|
|
935
|
+
titleRef.current = {
|
|
936
|
+
titledAtUserCount: session.messages.filter((m) => m.role === "user").length,
|
|
937
|
+
inFlight: false,
|
|
938
|
+
};
|
|
755
939
|
setOverlay({ kind: "none" });
|
|
756
940
|
dispatch({
|
|
757
941
|
type: "load-history",
|
|
@@ -951,7 +1135,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
951
1135
|
return {
|
|
952
1136
|
provider: id,
|
|
953
1137
|
configured: keyless || Boolean(secret.value),
|
|
954
|
-
maskedKey: secret.value
|
|
1138
|
+
maskedKey: secret.value
|
|
1139
|
+
? maskSecret(secret.value)
|
|
1140
|
+
: undefined,
|
|
955
1141
|
};
|
|
956
1142
|
}));
|
|
957
1143
|
setOverlay({
|
|
@@ -1181,7 +1367,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
1181
1367
|
return {
|
|
1182
1368
|
provider: id,
|
|
1183
1369
|
configured: keyless || Boolean(secret.value),
|
|
1184
|
-
maskedKey: secret.value
|
|
1370
|
+
maskedKey: secret.value
|
|
1371
|
+
? maskSecret(secret.value)
|
|
1372
|
+
: undefined,
|
|
1185
1373
|
};
|
|
1186
1374
|
}));
|
|
1187
1375
|
setOverlay({
|
|
@@ -1474,7 +1662,9 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
1474
1662
|
useInput((ch, key) => {
|
|
1475
1663
|
if (modalActive)
|
|
1476
1664
|
return; // overlay/modal owns input
|
|
1477
|
-
const cleanedChunk = stripMouseReports(ch)
|
|
1665
|
+
const cleanedChunk = stripMouseReports(ch)
|
|
1666
|
+
.replace(/\r\n/g, "\n")
|
|
1667
|
+
.replace(/\r/g, "\n");
|
|
1478
1668
|
if (isMouseReport(ch) && cleanedChunk.length === 0)
|
|
1479
1669
|
return;
|
|
1480
1670
|
// Global shortcuts
|
|
@@ -1694,7 +1884,26 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
1694
1884
|
const before = input.slice(0, cursor);
|
|
1695
1885
|
const at = input.slice(cursor, cursor + 1) || " ";
|
|
1696
1886
|
const after = input.slice(cursor + 1);
|
|
1697
|
-
|
|
1887
|
+
const footerInnerWidth = Math.max(20, cols - 6); // outer paddingX(2) + footer paddingX(1)
|
|
1888
|
+
const runningBadgeWidth = compacting
|
|
1889
|
+
? " COMPACTING ".length
|
|
1890
|
+
: " RUNNING ".length;
|
|
1891
|
+
const cancelBadgeWidth = " ESC CANCEL ".length;
|
|
1892
|
+
const queuedBadgeWidth = state.queued.length > 0 ? ` ${state.queued.length} QUEUED `.length + 1 : 0;
|
|
1893
|
+
const scrollBadgeWidth = (maxOffset > offset ? ` ▲ ${maxOffset - offset} `.length + 1 : 0) +
|
|
1894
|
+
(offset > 0 ? ` ▼ ${offset} `.length + 1 : 0);
|
|
1895
|
+
const spinnerWidth = 3; // spinner plus following space
|
|
1896
|
+
const fixedRunningFooterWidth = runningBadgeWidth +
|
|
1897
|
+
1 +
|
|
1898
|
+
spinnerWidth +
|
|
1899
|
+
1 +
|
|
1900
|
+
cancelBadgeWidth +
|
|
1901
|
+
queuedBadgeWidth +
|
|
1902
|
+
scrollBadgeWidth;
|
|
1903
|
+
const runningActivityWidth = Math.max(12, footerInnerWidth - fixedRunningFooterWidth);
|
|
1904
|
+
const rawRunningActivity = `${compacting ? "compacting conversation" : state.status.activity || "working"}${state.status.step > 0 ? ` (step ${state.status.step})` : ""} · ${elapsed}s`;
|
|
1905
|
+
const runningActivity = truncateMiddle(rawRunningActivity.replace(/\s+/g, " ").trim(), runningActivityWidth);
|
|
1906
|
+
return (_jsxs(Box, { flexDirection: "column", width: cols, height: usableRows, paddingX: 2, children: [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, searchDescription: overlay.searchDescription, twoLine: overlay.twoLine, 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))) })), _jsx(Box, { height: gapH }), slashMenuOpen && !modalActive
|
|
1698
1907
|
? visibleSlashSuggestions.map((cmd, i) => {
|
|
1699
1908
|
const absoluteIndex = slashWindowStart + i;
|
|
1700
1909
|
return (_jsxs(Text, { wrap: "truncate-end", backgroundColor: absoluteIndex === selected
|
|
@@ -1738,6 +1947,6 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
|
|
|
1738
1947
|
const atSliced = slicedInput.slice(slicedCursor, slicedCursor + 1) || " ";
|
|
1739
1948
|
const afterSliced = slicedInput.slice(slicedCursor + 1);
|
|
1740
1949
|
return (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "#F8FAFC", children: beforeSliced }), _jsx(Text, { backgroundColor: "#22D3EE", color: "#020617", bold: true, children: atSliced }), _jsx(Text, { color: "#F8FAFC", children: afterSliced })] }));
|
|
1741
|
-
})())] }), !secretRequest && !state.pendingConfirm ? (_jsxs(Box, { paddingX: 1, justifyContent: "center", children: [state.status.running || compacting ? (_jsxs(_Fragment, { children: [_jsxs(Text, { backgroundColor: "#B45309", color: "#FFFFFF", bold: true, children: [" ", compacting ? "COMPACTING" : "RUNNING", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "magenta", children: [spinner, " "] }),
|
|
1950
|
+
})())] }), !secretRequest && !state.pendingConfirm ? (_jsxs(Box, { paddingX: 1, justifyContent: "center", children: [state.status.running || compacting ? (_jsxs(_Fragment, { children: [_jsxs(Text, { backgroundColor: "#B45309", color: "#FFFFFF", bold: true, children: [" ", compacting ? "COMPACTING" : "RUNNING", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "magenta", children: [spinner, " "] }), _jsx(Box, { width: runningActivityWidth, flexShrink: 1, children: _jsx(Text, { color: "yellow", wrap: "truncate-end", children: runningActivity }) }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "ESC CANCEL", " "] }), state.queued.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ${state.queued.length} QUEUED ` })] })) : null] })) : (_jsxs(_Fragment, { 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"} ` })] })), maxOffset > offset ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ▲ ${maxOffset - offset} ` })] })) : null, offset > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ▼ ${offset} ` })] })) : null] })) : null] }));
|
|
1742
1951
|
}
|
|
1743
1952
|
//# sourceMappingURL=App.js.map
|