@pentoshi/clai 2.0.26 → 2.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/context-manager.d.ts +5 -2
  3. package/dist/agent/context-manager.js +26 -69
  4. package/dist/agent/context-manager.js.map +1 -1
  5. package/dist/agent/events.d.ts +1 -1
  6. package/dist/agent/runner.d.ts +21 -1
  7. package/dist/agent/runner.js +686 -135
  8. package/dist/agent/runner.js.map +1 -1
  9. package/dist/agent/session-title.d.ts +27 -0
  10. package/dist/agent/session-title.js +109 -0
  11. package/dist/agent/session-title.js.map +1 -0
  12. package/dist/commands/update.js +1 -1
  13. package/dist/index.js +5 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/llm/http.js +20 -6
  16. package/dist/llm/http.js.map +1 -1
  17. package/dist/modes/ask.d.ts +29 -0
  18. package/dist/modes/ask.js +188 -23
  19. package/dist/modes/ask.js.map +1 -1
  20. package/dist/prompts/index.d.ts +2 -2
  21. package/dist/prompts/index.js +32 -9
  22. package/dist/prompts/index.js.map +1 -1
  23. package/dist/repl.js +104 -3
  24. package/dist/repl.js.map +1 -1
  25. package/dist/safety/classifier.js +66 -7
  26. package/dist/safety/classifier.js.map +1 -1
  27. package/dist/store/history.js +68 -48
  28. package/dist/store/history.js.map +1 -1
  29. package/dist/tools/fs.d.ts +4 -0
  30. package/dist/tools/fs.js +40 -2
  31. package/dist/tools/fs.js.map +1 -1
  32. package/dist/tools/registry.d.ts +8 -0
  33. package/dist/tools/registry.js +3 -1
  34. package/dist/tools/registry.js.map +1 -1
  35. package/dist/tui/App.js +242 -33
  36. package/dist/tui/App.js.map +1 -1
  37. package/dist/tui/components/ConfirmModal.js +14 -2
  38. package/dist/tui/components/ConfirmModal.js.map +1 -1
  39. package/dist/tui/components/PickerPanel.d.ts +2 -1
  40. package/dist/tui/components/PickerPanel.js +73 -23
  41. package/dist/tui/components/PickerPanel.js.map +1 -1
  42. package/dist/tui/confirm.d.ts +1 -1
  43. package/dist/tui/confirm.js +8 -0
  44. package/dist/tui/confirm.js.map +1 -1
  45. package/dist/tui/hooks/useAgentRunner.d.ts +27 -2
  46. package/dist/tui/hooks/useAgentRunner.js +132 -17
  47. package/dist/tui/hooks/useAgentRunner.js.map +1 -1
  48. package/dist/tui/render-lines.js +28 -1
  49. package/dist/tui/render-lines.js.map +1 -1
  50. package/dist/tui/state.d.ts +1 -1
  51. package/dist/ui/markdown.js +63 -30
  52. package/dist/ui/markdown.js.map +1 -1
  53. 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({ lineText: "", startIdx: currentOffset, endIdx: currentOffset });
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
- if (messages.length === 0 && state.items.length === 0)
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
- void upsertSession(runner.getSession().sessionId, messages, undefined, state.items).catch(() => undefined);
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
- (messages.length > 0 || latestItems.current.length > 0)) {
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
- }, [state.status.running, state.queued, runner, beginTurn]);
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.after === result.before && !result.summarized) {
671
- info("nothing to compact yet — more messages are required");
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 || (error instanceof Error && error.name === "AbortError")) {
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: `${currentMessages.length} messages · active now`,
894
+ description: `active now · ${currentMessages.length} messages`,
731
895
  active: true,
732
896
  },
733
897
  ]
734
898
  : []),
735
- ...sessions.map((session) => ({
736
- value: session.id,
737
- label: session.name ?? session.id,
738
- description: `${session.createdAt.slice(0, 16).replace("T", " ")} · ${session.transcript?.length ?? session.messages.length} items`,
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 ? maskSecret(secret.value) : undefined,
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 ? maskSecret(secret.value) : undefined,
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).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
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
- 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, 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
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, " "] }), _jsxs(Text, { color: "yellow", children: [compacting ? "compacting conversation" : (state.status.activity || "working"), state.status.step > 0 ? ` (step ${state.status.step})` : "", ` · ${elapsed}s`] }), _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] }));
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