indusagi-coding-agent 0.1.47 → 0.1.49

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 (86) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/command-line/session-picker.js +1 -1
  3. package/dist/command-line/session-picker.js.map +1 -1
  4. package/dist/interfaces/react-ink/adapters/command-router.d.ts +2 -0
  5. package/dist/interfaces/react-ink/adapters/command-router.d.ts.map +1 -1
  6. package/dist/interfaces/react-ink/adapters/command-router.js +47 -19
  7. package/dist/interfaces/react-ink/adapters/command-router.js.map +1 -1
  8. package/dist/interfaces/react-ink/adapters/session-events.d.ts.map +1 -1
  9. package/dist/interfaces/react-ink/adapters/session-events.js +9 -2
  10. package/dist/interfaces/react-ink/adapters/session-events.js.map +1 -1
  11. package/dist/interfaces/react-ink/adapters/session-history.d.ts +8 -2
  12. package/dist/interfaces/react-ink/adapters/session-history.d.ts.map +1 -1
  13. package/dist/interfaces/react-ink/adapters/session-history.js +19 -5
  14. package/dist/interfaces/react-ink/adapters/session-history.js.map +1 -1
  15. package/dist/interfaces/react-ink/adapters/tool-state.d.ts.map +1 -1
  16. package/dist/interfaces/react-ink/adapters/tool-state.js +1 -1
  17. package/dist/interfaces/react-ink/adapters/tool-state.js.map +1 -1
  18. package/dist/interfaces/react-ink/components/AppShell.d.ts +3 -2
  19. package/dist/interfaces/react-ink/components/AppShell.d.ts.map +1 -1
  20. package/dist/interfaces/react-ink/components/AppShell.js +914 -48
  21. package/dist/interfaces/react-ink/components/AppShell.js.map +1 -1
  22. package/dist/interfaces/react-ink/components/Header.d.ts +3 -2
  23. package/dist/interfaces/react-ink/components/Header.d.ts.map +1 -1
  24. package/dist/interfaces/react-ink/components/Header.js +5 -3
  25. package/dist/interfaces/react-ink/components/Header.js.map +1 -1
  26. package/dist/interfaces/react-ink/components/PromptInput.d.ts +15 -1
  27. package/dist/interfaces/react-ink/components/PromptInput.d.ts.map +1 -1
  28. package/dist/interfaces/react-ink/components/PromptInput.js +100 -12
  29. package/dist/interfaces/react-ink/components/PromptInput.js.map +1 -1
  30. package/dist/interfaces/react-ink/components/StartupDiagnosticsBlock.d.ts +9 -0
  31. package/dist/interfaces/react-ink/components/StartupDiagnosticsBlock.d.ts.map +1 -0
  32. package/dist/interfaces/react-ink/components/StartupDiagnosticsBlock.js +14 -0
  33. package/dist/interfaces/react-ink/components/StartupDiagnosticsBlock.js.map +1 -0
  34. package/dist/interfaces/react-ink/components/extensions/ExtensionComponentHost.d.ts +30 -0
  35. package/dist/interfaces/react-ink/components/extensions/ExtensionComponentHost.d.ts.map +1 -0
  36. package/dist/interfaces/react-ink/components/extensions/ExtensionComponentHost.js +106 -0
  37. package/dist/interfaces/react-ink/components/extensions/ExtensionComponentHost.js.map +1 -0
  38. package/dist/interfaces/react-ink/components/extensions/ExtensionDialogs.d.ts +20 -0
  39. package/dist/interfaces/react-ink/components/extensions/ExtensionDialogs.d.ts.map +1 -0
  40. package/dist/interfaces/react-ink/components/extensions/ExtensionDialogs.js +144 -0
  41. package/dist/interfaces/react-ink/components/extensions/ExtensionDialogs.js.map +1 -0
  42. package/dist/interfaces/react-ink/hooks/use-agent-session.d.ts +2 -2
  43. package/dist/interfaces/react-ink/hooks/use-agent-session.d.ts.map +1 -1
  44. package/dist/interfaces/react-ink/hooks/use-agent-session.js +6 -1
  45. package/dist/interfaces/react-ink/hooks/use-agent-session.js.map +1 -1
  46. package/dist/interfaces/react-ink/hooks/use-app-keybindings.d.ts +0 -2
  47. package/dist/interfaces/react-ink/hooks/use-app-keybindings.d.ts.map +1 -1
  48. package/dist/interfaces/react-ink/hooks/use-app-keybindings.js +2 -39
  49. package/dist/interfaces/react-ink/hooks/use-app-keybindings.js.map +1 -1
  50. package/dist/interfaces/react-ink/hooks/use-prompt-submit.d.ts +2 -0
  51. package/dist/interfaces/react-ink/hooks/use-prompt-submit.d.ts.map +1 -1
  52. package/dist/interfaces/react-ink/hooks/use-prompt-submit.js +14 -1
  53. package/dist/interfaces/react-ink/hooks/use-prompt-submit.js.map +1 -1
  54. package/dist/interfaces/react-ink/index.d.ts +1 -5
  55. package/dist/interfaces/react-ink/index.d.ts.map +1 -1
  56. package/dist/interfaces/react-ink/index.js +1 -5
  57. package/dist/interfaces/react-ink/index.js.map +1 -1
  58. package/dist/interfaces/react-ink/interactive-mode.d.ts +1 -0
  59. package/dist/interfaces/react-ink/interactive-mode.d.ts.map +1 -1
  60. package/dist/interfaces/react-ink/interactive-mode.js +8 -1
  61. package/dist/interfaces/react-ink/interactive-mode.js.map +1 -1
  62. package/dist/interfaces/react-ink/render-root.d.ts +3 -2
  63. package/dist/interfaces/react-ink/render-root.d.ts.map +1 -1
  64. package/dist/interfaces/react-ink/render-root.js +2 -2
  65. package/dist/interfaces/react-ink/render-root.js.map +1 -1
  66. package/dist/interfaces/react-ink/theme-adapter.d.ts +2 -8
  67. package/dist/interfaces/react-ink/theme-adapter.d.ts.map +1 -1
  68. package/dist/interfaces/react-ink/theme-adapter.js +2 -41
  69. package/dist/interfaces/react-ink/theme-adapter.js.map +1 -1
  70. package/dist/interfaces/react-ink/types.d.ts +4 -93
  71. package/dist/interfaces/react-ink/types.d.ts.map +1 -1
  72. package/dist/interfaces/react-ink/utils/key-data.d.ts +22 -0
  73. package/dist/interfaces/react-ink/utils/key-data.d.ts.map +1 -0
  74. package/dist/interfaces/react-ink/utils/key-data.js +123 -0
  75. package/dist/interfaces/react-ink/utils/key-data.js.map +1 -0
  76. package/dist/interfaces/react-ink/utils/session-actions.d.ts +2 -0
  77. package/dist/interfaces/react-ink/utils/session-actions.d.ts.map +1 -1
  78. package/dist/interfaces/react-ink/utils/session-actions.js +10 -0
  79. package/dist/interfaces/react-ink/utils/session-actions.js.map +1 -1
  80. package/dist/interfaces/react-ink/utils/startup-diagnostics.d.ts +7 -0
  81. package/dist/interfaces/react-ink/utils/startup-diagnostics.d.ts.map +1 -0
  82. package/dist/interfaces/react-ink/utils/startup-diagnostics.js +111 -0
  83. package/dist/interfaces/react-ink/utils/startup-diagnostics.js.map +1 -0
  84. package/dist/interfaces/theme/dark.json +1 -1
  85. package/dist/interfaces/theme/light.json +1 -1
  86. package/package.json +4 -4
@@ -1,33 +1,49 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { spawnSync } from "node:child_process";
3
+ import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
2
6
  import { useEffect, useMemo, useReducer, useRef, useState } from "react";
3
7
  import { Box, Text, useApp } from "ink";
4
- import { getAuthPath } from "../../../config.js";
8
+ import { matchesKey } from "indusagi/tui";
9
+ import { Footer, LoginDialog, MessageList, ModelDialog, OAuthDialog, ScopedModelsDialog, SessionDialog, SettingsDialog, StatusLine, TaskPanel, ThemeDialog, TreeDialog, UserMessageDialog, } from "indusagi/react-ink";
10
+ import { APP_NAME, VERSION, getAuthPath } from "../../../config.js";
11
+ import { extensionForImageMimeType, readClipboardImage } from "../../../helpers/clipboard-image.js";
5
12
  import { openAuthUrl } from "../../../helpers/open-auth-url.js";
13
+ import { KeybindingsManager } from "../../../runtime/keybindings.js";
6
14
  import { resolveModelScope } from "../../../runtime/model-resolver.js";
7
- import { getAvailableThemes, setTheme } from "../../theme/theme.js";
8
- import { flattenSessionTree, listSessions, listUserMessages } from "../adapters/session-history.js";
15
+ import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getThemeByName, setTheme, setThemeInstance, theme as extensionTheme, Theme, } from "../../theme/theme.js";
16
+ import { flattenSessionTree, listAllSessions, listCurrentSessions, listUserMessages } from "../adapters/session-history.js";
9
17
  import { uiReducer } from "../state/reducer.js";
10
18
  import { createUiStore } from "../state/store.js";
11
19
  import { createThemeAdapter } from "../theme-adapter.js";
20
+ import { deleteSessionAtPath, renameSessionAtPath } from "../utils/session-actions.js";
12
21
  import { Header } from "./Header.js";
13
- import { MessageList } from "./MessageList.js";
14
22
  import { PromptInput } from "./PromptInput.js";
15
- import { StatusLine } from "./StatusLine.js";
16
- import { TaskPanel } from "./TaskPanel.js";
17
- import { Footer } from "./Footer.js";
18
23
  import { useAgentSession } from "../hooks/use-agent-session.js";
19
24
  import { useAppKeybindings } from "../hooks/use-app-keybindings.js";
20
25
  import { useFooterData } from "../hooks/use-footer-data.js";
21
26
  import { usePromptSubmit } from "../hooks/use-prompt-submit.js";
22
- import { ModelDialog } from "./dialogs/ModelDialog.js";
23
- import { ThemeDialog } from "./dialogs/ThemeDialog.js";
24
- import { SessionDialog } from "./dialogs/SessionDialog.js";
25
- import { TreeDialog } from "./dialogs/TreeDialog.js";
26
- import { UserMessageDialog } from "./dialogs/UserMessageDialog.js";
27
- import { LoginDialog } from "./dialogs/LoginDialog.js";
28
- import { OAuthDialog } from "./dialogs/OAuthDialog.js";
29
- import { SettingsDialog } from "./dialogs/SettingsDialog.js";
30
- import { ScopedModelsDialog } from "./dialogs/ScopedModelsDialog.js";
27
+ import { createExtensionTuiBridge, createHostedExtensionComponent, createHostedExtensionComponentFromParts, ExtensionComponentHost, } from "./extensions/ExtensionComponentHost.js";
28
+ import { ExtensionSelectDialog, ExtensionTextDialog } from "./extensions/ExtensionDialogs.js";
29
+ function truncateWidgetLines(lines, maxLines = 10) {
30
+ if (lines.length <= maxLines) {
31
+ return lines;
32
+ }
33
+ return [...lines.slice(0, maxLines), `... ${lines.length - maxLines} more line(s)`];
34
+ }
35
+ function renderTextLines(lines, prefix) {
36
+ if (lines.length === 0) {
37
+ return null;
38
+ }
39
+ return (_jsx(Box, { flexDirection: "column", children: lines.map((line, index) => (_jsx(Text, { children: line.length > 0 ? line : " " }, `${prefix}:${index}`))) }));
40
+ }
41
+ function writeTerminalTitle(title) {
42
+ if (!process.stdout.isTTY) {
43
+ return;
44
+ }
45
+ process.stdout.write(`\u001B]0;${title}\u0007`);
46
+ }
31
47
  function buildSettingsItems(session, state, onRefresh, setError) {
32
48
  const booleanValues = ["enabled", "disabled"];
33
49
  const visibilityValues = ["visible", "hidden"];
@@ -204,15 +220,33 @@ function buildSettingsItems(session, state, onRefresh, setError) {
204
220
  },
205
221
  ];
206
222
  }
207
- export function AppShell({ context, footerDataProvider, initialImages, initialMessage, initialMessages, startupChangelog, startupNotices, verbose, }) {
223
+ export function AppShell({ context, footerDataProvider, initialImages, initialMessage, initialMessages, startupChangelog, startupDiagnostics, startupNotices, verbose, }) {
208
224
  const { exit } = useApp();
225
+ const keybindings = useMemo(() => KeybindingsManager.create(), []);
209
226
  const [state, dispatch] = useReducer(uiReducer, context.session, createUiStore);
210
227
  const overlayRef = useRef(state.overlay);
211
228
  const oauthResolverRef = useRef(null);
212
229
  const startupRanRef = useRef(false);
213
230
  const displayBlockIdRef = useRef(0);
214
231
  const [terminalRows, setTerminalRows] = useState(process.stdout.rows ?? 40);
232
+ const [terminalColumns, setTerminalColumns] = useState(process.stdout.columns ?? 80);
215
233
  const [displayBlocks, setDisplayBlocks] = useState([]);
234
+ const [extensionHeader, setExtensionHeader] = useState(null);
235
+ const [extensionFooter, setExtensionFooter] = useState(null);
236
+ const [extensionWidgetsAbove, setExtensionWidgetsAbove] = useState(() => new Map());
237
+ const [extensionWidgetsBelow, setExtensionWidgetsBelow] = useState(() => new Map());
238
+ const [extensionOverlayState, setExtensionOverlayState] = useState({ kind: "none" });
239
+ const [extensionEditor, setExtensionEditor] = useState(null);
240
+ const [extensionWorkingMessage, setExtensionWorkingMessage] = useState();
241
+ const [toolOutputExpanded, setToolOutputExpanded] = useState(false);
242
+ const [compactionQueuedMessages, setCompactionQueuedMessages] = useState([]);
243
+ const [currentSessions, setCurrentSessions] = useState([]);
244
+ const [layoutEpoch, setLayoutEpoch] = useState(0);
245
+ const [startupDismissed, setStartupDismissed] = useState(context.session.messages.length > 0);
246
+ const extensionOverlayRef = useRef({ kind: "none" });
247
+ const extensionUiContextRef = useRef(undefined);
248
+ const inputRef = useRef(state.input);
249
+ const customTerminalTitleRef = useRef(null);
216
250
  const setStatus = (text, kind = "info") => {
217
251
  dispatch({ type: "set-status", status: { text, kind } });
218
252
  };
@@ -232,6 +266,7 @@ export function AppShell({ context, footerDataProvider, initialImages, initialMe
232
266
  const setInput = (value, cursorOffset) => {
233
267
  dispatch({ type: "set-input", value, cursorOffset });
234
268
  };
269
+ inputRef.current = state.input;
235
270
  const appendDisplayBlock = (block) => {
236
271
  const timestamp = Date.now();
237
272
  displayBlockIdRef.current += 1;
@@ -244,26 +279,289 @@ export function AppShell({ context, footerDataProvider, initialImages, initialMe
244
279
  },
245
280
  ]);
246
281
  };
247
- const resetSessionView = () => {
248
- setDisplayBlocks([]);
249
- dispatch({ type: "set-tool-executions", toolExecutions: {} });
250
- refresh();
251
- };
252
282
  const handleExit = () => {
253
283
  context.requestExit();
254
284
  exit();
255
285
  };
286
+ const isExtensionCommand = (text) => {
287
+ if (!text.startsWith("/")) {
288
+ return false;
289
+ }
290
+ const extensionRunner = context.session.extensionRunner;
291
+ if (!extensionRunner) {
292
+ return false;
293
+ }
294
+ const spaceIndex = text.indexOf(" ");
295
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
296
+ return Boolean(extensionRunner.getCommand(commandName));
297
+ };
298
+ const flushCompactionQueueRef = useRef(null);
256
299
  const { snapshot, toolExecutions, refresh } = useAgentSession(context.session, (status) => {
257
300
  dispatch({ type: "set-status", status });
301
+ }, (event) => {
302
+ if (event.type === "session_compact") {
303
+ void flushCompactionQueueRef.current?.();
304
+ }
305
+ if (event.type === "auto_compaction_end" && !event.aborted && event.result) {
306
+ void flushCompactionQueueRef.current?.();
307
+ }
258
308
  });
259
309
  useEffect(() => {
260
310
  dispatch({ type: "set-tool-executions", toolExecutions });
261
311
  }, [toolExecutions]);
312
+ const refreshSessionView = () => {
313
+ dispatch({ type: "set-tool-executions", toolExecutions: {} });
314
+ refresh();
315
+ };
316
+ const resetSessionView = () => {
317
+ setDisplayBlocks([]);
318
+ refreshSessionView();
319
+ };
320
+ const triggerFooterRefresh = () => {
321
+ dispatch({ type: "footer-tick" });
322
+ };
323
+ const pendingMessages = [
324
+ ...context.session.getSteeringMessages().map((text, index) => ({
325
+ id: `session-steer-${index}-${text}`,
326
+ mode: "steer",
327
+ text,
328
+ source: "session",
329
+ })),
330
+ ...context.session.getFollowUpMessages().map((text, index) => ({
331
+ id: `session-follow-${index}-${text}`,
332
+ mode: "followUp",
333
+ text,
334
+ source: "session",
335
+ })),
336
+ ...compactionQueuedMessages.map((message) => ({
337
+ id: message.id,
338
+ mode: message.mode,
339
+ text: message.text,
340
+ source: "compaction",
341
+ })),
342
+ ];
343
+ const restoreQueuedMessagesToPrompt = (options) => {
344
+ const { steering, followUp } = context.session.clearQueue();
345
+ const queuedMessages = [
346
+ ...steering,
347
+ ...followUp,
348
+ ...compactionQueuedMessages.map((message) => message.text),
349
+ ];
350
+ setCompactionQueuedMessages([]);
351
+ if (queuedMessages.length === 0) {
352
+ if (options?.abort) {
353
+ void context.session.abort();
354
+ }
355
+ return 0;
356
+ }
357
+ const combinedText = [...queuedMessages, options?.currentText ?? state.input]
358
+ .filter((value) => value.trim().length > 0)
359
+ .join("\n\n");
360
+ setInput(combinedText);
361
+ if (options?.abort) {
362
+ void context.session.abort();
363
+ }
364
+ return queuedMessages.length;
365
+ };
366
+ const queueCompactionMessage = (text, mode) => {
367
+ setCompactionQueuedMessages((current) => [
368
+ ...current,
369
+ {
370
+ id: `${mode}:${Date.now()}:${current.length}`,
371
+ text,
372
+ mode,
373
+ },
374
+ ]);
375
+ };
376
+ const flushCompactionQueue = async () => {
377
+ if (compactionQueuedMessages.length === 0) {
378
+ return;
379
+ }
380
+ const queuedMessages = [...compactionQueuedMessages];
381
+ setCompactionQueuedMessages([]);
382
+ const restoreQueue = (error) => {
383
+ setCompactionQueuedMessages(queuedMessages);
384
+ setError(`Failed to send queued message${queuedMessages.length === 1 ? "" : "s"}: ${error instanceof Error ? error.message : String(error)}`);
385
+ };
386
+ try {
387
+ const firstPromptIndex = queuedMessages.findIndex((message) => !isExtensionCommand(message.text));
388
+ if (firstPromptIndex === -1) {
389
+ for (const message of queuedMessages) {
390
+ await context.session.prompt(message.text);
391
+ }
392
+ return;
393
+ }
394
+ for (const message of queuedMessages.slice(0, firstPromptIndex)) {
395
+ await context.session.prompt(message.text);
396
+ }
397
+ const firstPrompt = queuedMessages[firstPromptIndex];
398
+ const rest = queuedMessages.slice(firstPromptIndex + 1);
399
+ await context.session.prompt(firstPrompt.text);
400
+ for (const message of rest) {
401
+ if (isExtensionCommand(message.text)) {
402
+ await context.session.prompt(message.text);
403
+ continue;
404
+ }
405
+ await context.session.prompt(message.text, {
406
+ streamingBehavior: message.mode === "followUp" ? "followUp" : "steer",
407
+ });
408
+ }
409
+ setStatus(`Flushed ${queuedMessages.length} queued message${queuedMessages.length === 1 ? "" : "s"} after compaction.`, "success");
410
+ }
411
+ catch (error) {
412
+ restoreQueue(error);
413
+ }
414
+ };
415
+ flushCompactionQueueRef.current = flushCompactionQueue;
416
+ const cycleModel = async (direction) => {
417
+ try {
418
+ const result = await context.session.cycleModel(direction);
419
+ if (!result) {
420
+ setStatus(context.session.scopedModels.length > 0 ? "Only one model in scope." : "Only one model available.", "warning");
421
+ return;
422
+ }
423
+ refresh();
424
+ setStatus(`Switched to ${result.model.provider}/${result.model.id}.`, "success");
425
+ }
426
+ catch (error) {
427
+ setError(error instanceof Error ? error.message : String(error));
428
+ }
429
+ };
430
+ const toggleThinkingVisibility = () => {
431
+ const nextShowThinking = !state.showThinking;
432
+ context.session.settingsManager.setHideThinkingBlock(!nextShowThinking);
433
+ dispatch({ type: "set-show-thinking", value: nextShowThinking });
434
+ setStatus(`Thinking blocks ${nextShowThinking ? "visible" : "hidden"}.`, "success");
435
+ };
436
+ const toggleToolOutputExpansion = () => {
437
+ setToolOutputExpanded((current) => {
438
+ const next = !current;
439
+ setStatus(`Tool outputs ${next ? "expanded" : "collapsed"}.`, "success");
440
+ return next;
441
+ });
442
+ };
443
+ const openExternalEditor = async () => {
444
+ const editorCommand = process.env.VISUAL || process.env.EDITOR;
445
+ if (!editorCommand) {
446
+ setStatus("Set $VISUAL or $EDITOR to use the external editor shortcut.", "warning");
447
+ return;
448
+ }
449
+ const tempFile = join(tmpdir(), `indusagi-editor-${Date.now()}.md`);
450
+ const currentText = state.input;
451
+ try {
452
+ writeFileSync(tempFile, currentText, "utf-8");
453
+ process.stdout.write("\u001B[?25h");
454
+ const [editor, ...editorArgs] = editorCommand.split(" ").filter(Boolean);
455
+ const result = spawnSync(editor, [...editorArgs, tempFile], {
456
+ stdio: "inherit",
457
+ });
458
+ if (result.status === 0) {
459
+ const nextText = readFileSync(tempFile, "utf-8").replace(/\n$/, "");
460
+ setInput(nextText, nextText.length);
461
+ setStatus("Updated prompt from external editor.", "success");
462
+ }
463
+ }
464
+ catch (error) {
465
+ setError(`External editor failed: ${error instanceof Error ? error.message : String(error)}`);
466
+ }
467
+ finally {
468
+ try {
469
+ unlinkSync(tempFile);
470
+ }
471
+ catch { }
472
+ process.stdout.write("\u001B[?25l");
473
+ }
474
+ };
475
+ const dequeueQueuedMessages = () => {
476
+ const restored = restoreQueuedMessagesToPrompt();
477
+ if (restored === 0) {
478
+ setStatus("No queued messages to restore.", "warning");
479
+ return;
480
+ }
481
+ setStatus(`Restored ${restored} queued message${restored === 1 ? "" : "s"} to the prompt.`, "success");
482
+ };
483
+ const pasteClipboardImage = async () => {
484
+ try {
485
+ const image = await readClipboardImage();
486
+ if (!image) {
487
+ setStatus("No image was found in the clipboard.", "warning");
488
+ return;
489
+ }
490
+ const extension = extensionForImageMimeType(image.mimeType) ?? "png";
491
+ const filePath = join(tmpdir(), `indusagi-clipboard-${crypto.randomUUID()}.${extension}`);
492
+ writeFileSync(filePath, Buffer.from(image.bytes));
493
+ const nextValue = state.input.length > 0 && !/\s$/.test(state.input)
494
+ ? `${state.input} ${filePath}`
495
+ : `${state.input}${filePath}`;
496
+ setInput(nextValue, nextValue.length);
497
+ setStatus("Inserted clipboard image path into the prompt.", "success");
498
+ }
499
+ catch (error) {
500
+ setStatus(`Unable to paste clipboard image: ${error instanceof Error ? error.message : String(error)}`, "warning");
501
+ }
502
+ };
503
+ const suspendTerminal = () => {
504
+ if (process.platform === "win32") {
505
+ setStatus("Suspend is not available on Windows terminals.", "warning");
506
+ return;
507
+ }
508
+ process.once("SIGCONT", () => {
509
+ refresh();
510
+ triggerFooterRefresh();
511
+ setStatus("Resumed terminal session.", "success");
512
+ });
513
+ process.stdout.write("\u001B[?25h");
514
+ process.kill(0, "SIGTSTP");
515
+ };
516
+ const restoreDefaultTerminalTitle = () => {
517
+ customTerminalTitleRef.current = null;
518
+ const sessionName = context.session.sessionManager.getSessionName();
519
+ writeTerminalTitle(sessionName ? `${APP_NAME} ${VERSION} - ${sessionName}` : `${APP_NAME} ${VERSION}`);
520
+ };
521
+ const cancelExtensionOverlay = () => {
522
+ const current = extensionOverlayRef.current;
523
+ switch (current.kind) {
524
+ case "select":
525
+ current.resolve(undefined);
526
+ break;
527
+ case "confirm":
528
+ current.resolve(false);
529
+ break;
530
+ case "input":
531
+ case "editor":
532
+ current.resolve(undefined);
533
+ break;
534
+ case "custom":
535
+ current.cancel();
536
+ break;
537
+ case "none":
538
+ break;
539
+ }
540
+ setExtensionOverlayState({ kind: "none" });
541
+ };
542
+ const resetExtensionUi = () => {
543
+ cancelExtensionOverlay();
544
+ setExtensionEditor(null);
545
+ setExtensionHeader(null);
546
+ setExtensionFooter(null);
547
+ setExtensionWidgetsAbove(new Map());
548
+ setExtensionWidgetsBelow(new Map());
549
+ setExtensionWorkingMessage(undefined);
550
+ footerDataProvider.clearExtensionStatuses();
551
+ triggerFooterRefresh();
552
+ restoreDefaultTerminalTitle();
553
+ };
262
554
  useEffect(() => {
263
555
  overlayRef.current = state.overlay;
264
556
  }, [state.overlay]);
265
557
  useEffect(() => {
266
- const onResize = () => setTerminalRows(process.stdout.rows ?? 40);
558
+ extensionOverlayRef.current = extensionOverlayState;
559
+ }, [extensionOverlayState]);
560
+ useEffect(() => {
561
+ const onResize = () => {
562
+ setTerminalRows(process.stdout.rows ?? 40);
563
+ setTerminalColumns(process.stdout.columns ?? 80);
564
+ };
267
565
  process.stdout.on("resize", onResize);
268
566
  return () => {
269
567
  process.stdout.off("resize", onResize);
@@ -271,6 +569,8 @@ export function AppShell({ context, footerDataProvider, initialImages, initialMe
271
569
  }, []);
272
570
  const footerData = useFooterData(footerDataProvider, state.footerTick);
273
571
  const theme = createThemeAdapter(state.themeName);
572
+ const showStartupDiagnostics = !startupDismissed;
573
+ const previousStartupDiagnosticsRef = useRef(showStartupDiagnostics);
274
574
  function handleOAuthOverlayChange(partial) {
275
575
  const current = overlayRef.current;
276
576
  if (current.kind !== "oauth")
@@ -339,10 +639,492 @@ export function AppShell({ context, footerDataProvider, initialImages, initialMe
339
639
  appendDisplayBlock,
340
640
  startOAuthLogin,
341
641
  resetSessionView,
642
+ refreshSessionView,
643
+ onBeforeReload: resetExtensionUi,
342
644
  onExit: handleExit,
343
645
  },
344
646
  onAfterSubmit: refresh,
647
+ onQueueDuringCompaction: queueCompactionMessage,
648
+ onShouldRunDuringCompaction: isExtensionCommand,
345
649
  });
650
+ const submitPromptRef = useRef(submitPrompt);
651
+ const submitPromptWithStartupDismiss = async (input, mode = "followUp") => {
652
+ if (!startupDismissed && input.trim().length > 0) {
653
+ setStartupDismissed(true);
654
+ }
655
+ await submitPrompt(input, mode);
656
+ };
657
+ submitPromptRef.current = submitPromptWithStartupDismiss;
658
+ const handleExtensionShortcutKeyData = async (data) => {
659
+ const extensionRunner = context.session.extensionRunner;
660
+ if (!extensionRunner) {
661
+ return false;
662
+ }
663
+ const shortcuts = extensionRunner.getShortcuts(keybindings.getEffectiveConfig());
664
+ if (shortcuts.size === 0) {
665
+ return false;
666
+ }
667
+ const shortcutContext = {
668
+ ui: extensionUiContextRef.current ?? {
669
+ select: async () => undefined,
670
+ confirm: async () => false,
671
+ input: async () => undefined,
672
+ notify: () => { },
673
+ setStatus: () => { },
674
+ setWorkingMessage: () => { },
675
+ setWidget: () => { },
676
+ setFooter: () => { },
677
+ setHeader: () => { },
678
+ setTitle: () => { },
679
+ custom: async () => undefined,
680
+ setEditorText: () => { },
681
+ getEditorText: () => inputRef.current,
682
+ editor: async () => undefined,
683
+ setEditorComponent: () => { },
684
+ get theme() {
685
+ return extensionTheme;
686
+ },
687
+ getAllThemes: () => [],
688
+ getTheme: () => undefined,
689
+ setTheme: () => ({ success: false, error: "Extension UI is not ready yet." }),
690
+ },
691
+ hasUI: true,
692
+ cwd: process.cwd(),
693
+ sessionManager: context.session.sessionManager,
694
+ modelRegistry: context.session.modelRegistry,
695
+ model: context.session.model,
696
+ isIdle: () => !context.session.isStreaming,
697
+ abort: () => {
698
+ void context.session.abort();
699
+ },
700
+ hasPendingMessages: () => context.session.pendingMessageCount > 0,
701
+ shutdown: handleExit,
702
+ getContextUsage: () => context.session.getContextUsage(),
703
+ compact: (options) => {
704
+ void (async () => {
705
+ try {
706
+ const result = await context.session.compact(options?.customInstructions);
707
+ refreshSessionView();
708
+ options?.onComplete?.(result);
709
+ }
710
+ catch (error) {
711
+ const resolvedError = error instanceof Error ? error : new Error(String(error));
712
+ options?.onError?.(resolvedError);
713
+ }
714
+ })();
715
+ },
716
+ };
717
+ for (const [shortcutKey, shortcut] of shortcuts) {
718
+ if (!matchesKey(data, shortcutKey)) {
719
+ continue;
720
+ }
721
+ try {
722
+ await Promise.resolve(shortcut.handler(shortcutContext));
723
+ }
724
+ catch (error) {
725
+ setError(`Shortcut handler error: ${error instanceof Error ? error.message : String(error)}`);
726
+ }
727
+ return true;
728
+ }
729
+ return false;
730
+ };
731
+ useEffect(() => {
732
+ if (customTerminalTitleRef.current) {
733
+ return;
734
+ }
735
+ restoreDefaultTerminalTitle();
736
+ }, [snapshot.sessionName]);
737
+ useEffect(() => {
738
+ if (extensionOverlayState.kind === "none") {
739
+ return;
740
+ }
741
+ const timeoutAt = extensionOverlayState.kind === "select" ||
742
+ extensionOverlayState.kind === "confirm" ||
743
+ extensionOverlayState.kind === "input" ||
744
+ extensionOverlayState.kind === "editor"
745
+ ? extensionOverlayState.timeoutAt
746
+ : undefined;
747
+ if (!timeoutAt) {
748
+ return;
749
+ }
750
+ const remaining = timeoutAt - Date.now();
751
+ if (remaining <= 0) {
752
+ cancelExtensionOverlay();
753
+ return;
754
+ }
755
+ const timer = setTimeout(() => {
756
+ cancelExtensionOverlay();
757
+ }, remaining);
758
+ return () => {
759
+ clearTimeout(timer);
760
+ };
761
+ }, [extensionOverlayState]);
762
+ useEffect(() => {
763
+ if (!extensionEditor) {
764
+ return;
765
+ }
766
+ if (extensionEditor.component.getText() !== state.input) {
767
+ extensionEditor.component.setText(state.input);
768
+ extensionEditor.bridge.requestRender?.();
769
+ }
770
+ }, [extensionEditor, state.input]);
771
+ useEffect(() => {
772
+ const buildExtensionUiContext = () => ({
773
+ select: (title, options, opts) => new Promise((resolve) => {
774
+ if (opts?.signal?.aborted) {
775
+ resolve(undefined);
776
+ return;
777
+ }
778
+ const finalize = (() => {
779
+ let settled = false;
780
+ return (value) => {
781
+ if (settled) {
782
+ return;
783
+ }
784
+ settled = true;
785
+ opts?.signal?.removeEventListener("abort", onAbort);
786
+ setExtensionOverlayState({ kind: "none" });
787
+ resolve(value);
788
+ };
789
+ })();
790
+ const onAbort = () => {
791
+ finalize(undefined);
792
+ };
793
+ cancelExtensionOverlay();
794
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
795
+ setExtensionOverlayState({
796
+ kind: "select",
797
+ title,
798
+ options,
799
+ timeoutAt: opts?.timeout ? Date.now() + opts.timeout : undefined,
800
+ resolve: finalize,
801
+ });
802
+ }),
803
+ confirm: (title, message, opts) => new Promise((resolve) => {
804
+ if (opts?.signal?.aborted) {
805
+ resolve(false);
806
+ return;
807
+ }
808
+ const finalize = (() => {
809
+ let settled = false;
810
+ return (value) => {
811
+ if (settled) {
812
+ return;
813
+ }
814
+ settled = true;
815
+ opts?.signal?.removeEventListener("abort", onAbort);
816
+ setExtensionOverlayState({ kind: "none" });
817
+ resolve(value);
818
+ };
819
+ })();
820
+ const onAbort = () => {
821
+ finalize(false);
822
+ };
823
+ cancelExtensionOverlay();
824
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
825
+ setExtensionOverlayState({
826
+ kind: "confirm",
827
+ title,
828
+ message,
829
+ timeoutAt: opts?.timeout ? Date.now() + opts.timeout : undefined,
830
+ resolve: finalize,
831
+ });
832
+ }),
833
+ input: (title, placeholder, opts) => new Promise((resolve) => {
834
+ if (opts?.signal?.aborted) {
835
+ resolve(undefined);
836
+ return;
837
+ }
838
+ const finalize = (() => {
839
+ let settled = false;
840
+ return (value) => {
841
+ if (settled) {
842
+ return;
843
+ }
844
+ settled = true;
845
+ opts?.signal?.removeEventListener("abort", onAbort);
846
+ setExtensionOverlayState({ kind: "none" });
847
+ resolve(value);
848
+ };
849
+ })();
850
+ const onAbort = () => {
851
+ finalize(undefined);
852
+ };
853
+ cancelExtensionOverlay();
854
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
855
+ setExtensionOverlayState({
856
+ kind: "input",
857
+ title,
858
+ placeholder,
859
+ timeoutAt: opts?.timeout ? Date.now() + opts.timeout : undefined,
860
+ resolve: finalize,
861
+ });
862
+ }),
863
+ notify: (message, type) => {
864
+ if (type === "error") {
865
+ setError(message);
866
+ return;
867
+ }
868
+ if (type === "warning") {
869
+ setStatus(message, "warning");
870
+ return;
871
+ }
872
+ setStatus(message, "info");
873
+ },
874
+ setStatus: (key, text) => {
875
+ footerDataProvider.setExtensionStatus(key, text);
876
+ triggerFooterRefresh();
877
+ },
878
+ setWorkingMessage: (message) => {
879
+ setExtensionWorkingMessage(message);
880
+ },
881
+ setWidget: (key, content, options) => {
882
+ const placement = options?.placement ?? "aboveEditor";
883
+ const updateMap = placement === "belowEditor" ? setExtensionWidgetsBelow : setExtensionWidgetsAbove;
884
+ const clearOtherMap = placement === "belowEditor" ? setExtensionWidgetsAbove : setExtensionWidgetsBelow;
885
+ clearOtherMap((current) => {
886
+ if (!current.has(key)) {
887
+ return current;
888
+ }
889
+ const next = new Map(current);
890
+ next.delete(key);
891
+ return next;
892
+ });
893
+ updateMap((current) => {
894
+ const next = new Map(current);
895
+ if (content === undefined) {
896
+ next.delete(key);
897
+ return next;
898
+ }
899
+ if (Array.isArray(content)) {
900
+ next.set(key, { kind: "lines", lines: truncateWidgetLines(content) });
901
+ return next;
902
+ }
903
+ try {
904
+ next.set(key, {
905
+ kind: "component",
906
+ hosted: createHostedExtensionComponent((tui) => content(tui, extensionTheme)),
907
+ });
908
+ }
909
+ catch (error) {
910
+ setError(`Failed to render extension widget "${key}": ${error instanceof Error ? error.message : String(error)}`);
911
+ }
912
+ return next;
913
+ });
914
+ },
915
+ setFooter: (factory) => {
916
+ if (!factory) {
917
+ setExtensionFooter(null);
918
+ return;
919
+ }
920
+ try {
921
+ setExtensionFooter(createHostedExtensionComponent((tui) => factory(tui, extensionTheme, footerDataProvider)));
922
+ }
923
+ catch (error) {
924
+ setError(`Failed to render extension footer: ${error instanceof Error ? error.message : String(error)}`);
925
+ }
926
+ },
927
+ setHeader: (factory) => {
928
+ if (!factory) {
929
+ setExtensionHeader(null);
930
+ return;
931
+ }
932
+ try {
933
+ setExtensionHeader(createHostedExtensionComponent((tui) => factory(tui, extensionTheme)));
934
+ }
935
+ catch (error) {
936
+ setError(`Failed to render extension header: ${error instanceof Error ? error.message : String(error)}`);
937
+ }
938
+ },
939
+ setTitle: (title) => {
940
+ customTerminalTitleRef.current = title;
941
+ writeTerminalTitle(title);
942
+ },
943
+ custom: async (factory, options) => new Promise((resolve, reject) => {
944
+ let overlayHandleHidden = false;
945
+ const finish = (() => {
946
+ let settled = false;
947
+ return (value) => {
948
+ if (settled) {
949
+ return;
950
+ }
951
+ settled = true;
952
+ setExtensionOverlayState({ kind: "none" });
953
+ resolve(value);
954
+ };
955
+ })();
956
+ const cancel = () => {
957
+ setExtensionOverlayState({ kind: "none" });
958
+ resolve(undefined);
959
+ };
960
+ cancelExtensionOverlay();
961
+ const bridge = createExtensionTuiBridge();
962
+ Promise.resolve(factory(bridge, extensionTheme, keybindings, finish))
963
+ .then((component) => {
964
+ const hosted = createHostedExtensionComponentFromParts(component, bridge);
965
+ const handle = {
966
+ hide: () => {
967
+ overlayHandleHidden = true;
968
+ setExtensionOverlayState((current) => current.kind === "custom"
969
+ ? { ...current, hidden: true }
970
+ : current);
971
+ },
972
+ setHidden: (hidden) => {
973
+ overlayHandleHidden = hidden;
974
+ setExtensionOverlayState((current) => current.kind === "custom"
975
+ ? { ...current, hidden }
976
+ : current);
977
+ },
978
+ isHidden: () => overlayHandleHidden,
979
+ };
980
+ options?.onHandle?.(handle);
981
+ setExtensionOverlayState({
982
+ kind: "custom",
983
+ hosted,
984
+ mode: options?.overlay === true ? "overlay" : "inline",
985
+ hidden: false,
986
+ cancel: () => {
987
+ cancel();
988
+ },
989
+ });
990
+ })
991
+ .catch((error) => {
992
+ setExtensionOverlayState({ kind: "none" });
993
+ reject(error);
994
+ });
995
+ }),
996
+ setEditorText: (text) => {
997
+ setInput(text);
998
+ },
999
+ getEditorText: () => inputRef.current,
1000
+ editor: (title, prefill) => new Promise((resolve) => {
1001
+ const finalize = (() => {
1002
+ let settled = false;
1003
+ return (value) => {
1004
+ if (settled) {
1005
+ return;
1006
+ }
1007
+ settled = true;
1008
+ setExtensionOverlayState({ kind: "none" });
1009
+ resolve(value);
1010
+ };
1011
+ })();
1012
+ cancelExtensionOverlay();
1013
+ setExtensionOverlayState({
1014
+ kind: "editor",
1015
+ title,
1016
+ prefill,
1017
+ resolve: finalize,
1018
+ });
1019
+ }),
1020
+ setEditorComponent: (factory) => {
1021
+ if (!factory) {
1022
+ setExtensionEditor(null);
1023
+ return;
1024
+ }
1025
+ try {
1026
+ const hosted = createHostedExtensionComponent((tui) => {
1027
+ const editor = factory(tui, getEditorTheme(), keybindings);
1028
+ editor.onSubmit = (text) => {
1029
+ void submitPromptRef.current(text, "steer");
1030
+ };
1031
+ editor.onChange = (text) => {
1032
+ dispatch({ type: "set-input", value: text, cursorOffset: text.length });
1033
+ };
1034
+ editor.setText(inputRef.current);
1035
+ return editor;
1036
+ });
1037
+ setExtensionEditor(hosted);
1038
+ }
1039
+ catch (error) {
1040
+ setError(`Failed to enable extension editor: ${error instanceof Error ? error.message : String(error)}`);
1041
+ }
1042
+ },
1043
+ get theme() {
1044
+ return extensionTheme;
1045
+ },
1046
+ getAllThemes: () => getAvailableThemesWithPaths(),
1047
+ getTheme: (name) => getThemeByName(name),
1048
+ setTheme: (themeOrName) => {
1049
+ if (themeOrName instanceof Theme) {
1050
+ setThemeInstance(themeOrName);
1051
+ if (themeOrName.name) {
1052
+ context.session.settingsManager.setTheme(themeOrName.name);
1053
+ dispatch({ type: "set-theme-name", themeName: themeOrName.name });
1054
+ }
1055
+ triggerFooterRefresh();
1056
+ refresh();
1057
+ return { success: true };
1058
+ }
1059
+ const result = setTheme(themeOrName, true);
1060
+ if (result.success) {
1061
+ context.session.settingsManager.setTheme(themeOrName);
1062
+ dispatch({ type: "set-theme-name", themeName: themeOrName });
1063
+ triggerFooterRefresh();
1064
+ refresh();
1065
+ }
1066
+ return result;
1067
+ },
1068
+ });
1069
+ const uiContext = buildExtensionUiContext();
1070
+ extensionUiContextRef.current = uiContext;
1071
+ void context.session.bindExtensions({
1072
+ uiContext,
1073
+ commandContextActions: {
1074
+ waitForIdle: () => context.session.agent.waitForIdle(),
1075
+ newSession: async (options) => {
1076
+ const started = await context.session.newSession({ parentSession: options?.parentSession });
1077
+ if (!started) {
1078
+ return { cancelled: true };
1079
+ }
1080
+ if (options?.setup) {
1081
+ await options.setup(context.session.sessionManager);
1082
+ }
1083
+ resetSessionView();
1084
+ setInput("");
1085
+ setStatus("New session started.", "success");
1086
+ return { cancelled: false };
1087
+ },
1088
+ fork: async (entryId) => {
1089
+ const result = await context.session.fork(entryId);
1090
+ if (result.cancelled) {
1091
+ return { cancelled: true };
1092
+ }
1093
+ resetSessionView();
1094
+ setInput(result.selectedText);
1095
+ setStatus("Forked to new session.", "success");
1096
+ return { cancelled: false };
1097
+ },
1098
+ navigateTree: async (targetId, options) => {
1099
+ const result = await context.session.navigateTree(targetId, {
1100
+ summarize: options?.summarize,
1101
+ customInstructions: options?.customInstructions,
1102
+ replaceInstructions: options?.replaceInstructions,
1103
+ label: options?.label,
1104
+ });
1105
+ if (result.cancelled) {
1106
+ return { cancelled: true };
1107
+ }
1108
+ resetSessionView();
1109
+ if (result.editorText) {
1110
+ setInput(result.editorText);
1111
+ }
1112
+ setStatus("Navigated to selected point.", "success");
1113
+ return { cancelled: false };
1114
+ },
1115
+ },
1116
+ shutdownHandler: handleExit,
1117
+ onError: (error) => {
1118
+ setError(`Extension error in ${error.extensionPath}: ${error.error}`);
1119
+ },
1120
+ onHookError: (error) => {
1121
+ setError(`Hook error in ${error.hookPath}: ${error.error}`);
1122
+ },
1123
+ });
1124
+ return () => {
1125
+ resetExtensionUi();
1126
+ };
1127
+ }, [context.session, footerDataProvider, keybindings]);
346
1128
  useEffect(() => {
347
1129
  if (startupRanRef.current)
348
1130
  return;
@@ -370,35 +1152,48 @@ export function AppShell({ context, footerDataProvider, initialImages, initialMe
370
1152
  }
371
1153
  })();
372
1154
  }, [context.session, initialImages, initialMessage, initialMessages, refresh]);
1155
+ const extensionUiActive = Boolean(extensionEditor) ||
1156
+ (extensionOverlayState.kind !== "none" &&
1157
+ !(extensionOverlayState.kind === "custom" && extensionOverlayState.hidden));
373
1158
  useAppKeybindings({
374
1159
  session: context.session,
375
- overlayActive: state.overlay.kind !== "none",
1160
+ overlayActive: state.overlay.kind !== "none" || extensionUiActive,
376
1161
  onExit: handleExit,
377
- onRefresh: refresh,
378
1162
  onOpenSettings: () => setOverlay({ kind: "settings" }),
379
1163
  onOpenResume: () => {
380
- void (async () => {
381
- setOverlay({ kind: "session", sessions: [], loading: true });
382
- try {
383
- const sessions = await listSessions(context.session);
384
- setOverlay({ kind: "session", sessions, loading: false });
385
- }
386
- catch (error) {
387
- setOverlay({
388
- kind: "session",
389
- sessions: [],
390
- loading: false,
391
- error: error instanceof Error ? error.message : String(error),
392
- });
393
- }
394
- })();
1164
+ setOverlay({ kind: "session" });
395
1165
  },
396
- onOpenTree: () => setOverlay({ kind: "tree", items: flattenSessionTree(context.session) }),
397
1166
  onStatus: setStatus,
398
1167
  });
1168
+ useEffect(() => {
1169
+ if (state.overlay.kind !== "session") {
1170
+ return;
1171
+ }
1172
+ let cancelled = false;
1173
+ void listCurrentSessions(context.session)
1174
+ .then((sessions) => {
1175
+ if (!cancelled) {
1176
+ setCurrentSessions(sessions);
1177
+ }
1178
+ })
1179
+ .catch((error) => {
1180
+ if (!cancelled) {
1181
+ setError(error instanceof Error ? error.message : String(error));
1182
+ }
1183
+ });
1184
+ return () => {
1185
+ cancelled = true;
1186
+ };
1187
+ }, [context.session, state.overlay.kind]);
399
1188
  const settingsItems = useMemo(() => buildSettingsItems(context.session, state, () => dispatch({ type: "footer-tick" }), setError), [context.session, state]);
400
1189
  const maxItems = Math.max(10, terminalRows - (state.overlay.kind === "none" ? 14 : 24));
1190
+ const effectiveStatus = state.status ??
1191
+ (extensionWorkingMessage &&
1192
+ (snapshot.isStreaming || snapshot.isCompacting || snapshot.isBashRunning || snapshot.pendingToolCallCount > 0)
1193
+ ? { kind: "busy", text: extensionWorkingMessage }
1194
+ : undefined);
401
1195
  let overlay = null;
1196
+ let extensionOverlay = null;
402
1197
  switch (state.overlay.kind) {
403
1198
  case "settings":
404
1199
  overlay = (_jsx(SettingsDialog, { items: settingsItems, onClose: closeOverlay }));
@@ -459,7 +1254,33 @@ export function AppShell({ context, footerDataProvider, initialImages, initialMe
459
1254
  } }));
460
1255
  break;
461
1256
  case "session":
462
- overlay = (_jsx(SessionDialog, { error: state.overlay.error, loading: state.overlay.loading, onClose: closeOverlay, onSelect: async (item) => {
1257
+ overlay = (_jsx(SessionDialog, { onClose: closeOverlay, currentSessionPath: context.session.sessionFile, onDeleteSession: async (item) => {
1258
+ try {
1259
+ if (item.path === context.session.sessionFile) {
1260
+ setStatus("Cannot delete the current active session from the picker.", "warning");
1261
+ return;
1262
+ }
1263
+ await deleteSessionAtPath(item.path);
1264
+ setCurrentSessions((sessions) => sessions.filter((session) => session.path !== item.path));
1265
+ setStatus("Deleted session.", "success");
1266
+ }
1267
+ catch (error) {
1268
+ setError(error instanceof Error ? error.message : String(error));
1269
+ }
1270
+ }, onLoadAllSessions: (onProgress) => listAllSessions(onProgress), onRenameSession: async (item, name) => {
1271
+ try {
1272
+ const resolvedName = await renameSessionAtPath(item.path, name);
1273
+ setCurrentSessions((sessions) => sessions.map((session) => session.path === item.path
1274
+ ? { ...session, name: resolvedName, modified: new Date(), lastModified: Date.now() }
1275
+ : session));
1276
+ if (item.path === context.session.sessionFile)
1277
+ refresh();
1278
+ setStatus(`Renamed session to ${resolvedName}.`, "success");
1279
+ }
1280
+ catch (error) {
1281
+ setError(error instanceof Error ? error.message : String(error));
1282
+ }
1283
+ }, onSelect: async (item) => {
463
1284
  try {
464
1285
  await context.session.switchSession(item.path);
465
1286
  closeOverlay();
@@ -469,7 +1290,7 @@ export function AppShell({ context, footerDataProvider, initialImages, initialMe
469
1290
  catch (error) {
470
1291
  setError(error instanceof Error ? error.message : String(error));
471
1292
  }
472
- }, sessions: state.overlay.sessions }));
1293
+ }, currentSessions: currentSessions }));
473
1294
  break;
474
1295
  case "tree":
475
1296
  overlay = (_jsx(TreeDialog, { items: state.overlay.items, onClose: closeOverlay, onSelect: async (item) => {
@@ -568,6 +1389,32 @@ export function AppShell({ context, footerDataProvider, initialImages, initialMe
568
1389
  case "none":
569
1390
  break;
570
1391
  }
1392
+ switch (extensionOverlayState.kind) {
1393
+ case "select":
1394
+ extensionOverlay = (_jsx(ExtensionSelectDialog, { onClose: () => extensionOverlayState.resolve(undefined), onSelect: (value) => extensionOverlayState.resolve(value), options: extensionOverlayState.options, timeoutAt: extensionOverlayState.timeoutAt, title: extensionOverlayState.title }));
1395
+ break;
1396
+ case "confirm":
1397
+ extensionOverlay = (_jsx(ExtensionSelectDialog, { onClose: () => extensionOverlayState.resolve(false), onSelect: (value) => extensionOverlayState.resolve(value === "Yes"), options: ["Yes", "No"], timeoutAt: extensionOverlayState.timeoutAt, title: `${extensionOverlayState.title}\n${extensionOverlayState.message}` }));
1398
+ break;
1399
+ case "input":
1400
+ extensionOverlay = (_jsx(ExtensionTextDialog, { onClose: () => extensionOverlayState.resolve(undefined), onSubmit: (value) => extensionOverlayState.resolve(value), placeholder: extensionOverlayState.placeholder, timeoutAt: extensionOverlayState.timeoutAt, title: extensionOverlayState.title }));
1401
+ break;
1402
+ case "editor":
1403
+ extensionOverlay = (_jsx(ExtensionTextDialog, { multiline: true, onClose: () => extensionOverlayState.resolve(undefined), onSubmit: (value) => extensionOverlayState.resolve(value), prefill: extensionOverlayState.prefill, timeoutAt: extensionOverlayState.timeoutAt, title: extensionOverlayState.title }));
1404
+ break;
1405
+ case "custom":
1406
+ if (extensionOverlayState.mode === "overlay" && !extensionOverlayState.hidden) {
1407
+ extensionOverlay = (_jsx(ExtensionComponentHost, { active: true, hosted: extensionOverlayState.hosted, width: terminalColumns }));
1408
+ }
1409
+ break;
1410
+ case "none":
1411
+ break;
1412
+ }
1413
+ useEffect(() => {
1414
+ if (!startupDismissed && snapshot.messages.length > 0) {
1415
+ setStartupDismissed(true);
1416
+ }
1417
+ }, [snapshot.messages.length, startupDismissed]);
571
1418
  useEffect(() => {
572
1419
  const nextShowThinking = !context.session.settingsManager.getHideThinkingBlock();
573
1420
  const nextShowImages = context.session.settingsManager.getShowImages();
@@ -582,18 +1429,37 @@ export function AppShell({ context, footerDataProvider, initialImages, initialMe
582
1429
  dispatch({ type: "set-theme-name", themeName: nextThemeName });
583
1430
  }
584
1431
  }, [context.session, state.footerTick, state.showImages, state.showThinking, state.themeName]);
585
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { collapseChangelog: context.session.settingsManager.getCollapseChangelog(), sessionName: snapshot.sessionName, showBranding: Boolean(verbose) || !context.session.settingsManager.getQuietStartup(), startupChangelog: startupChangelog, startupNotices: startupNotices, theme: theme }), _jsx(MessageList, { displayBlocks: displayBlocks, maxItems: maxItems, messages: snapshot.messages, showImages: state.showImages, showThinking: state.showThinking, theme: theme }), _jsx(TaskPanel, { snapshot: snapshot, theme: theme, toolExecutions: state.toolExecutions }), _jsx(StatusLine, { snapshot: snapshot, status: state.status, theme: theme }), _jsx(PromptInput, { cursorOffset: state.cursorOffset, disabled: state.overlay.kind !== "none", dispatch: dispatch, input: state.input, onCancelActiveWork: async (kind) => {
1432
+ useEffect(() => {
1433
+ if (previousStartupDiagnosticsRef.current && !showStartupDiagnostics && process.stdout.isTTY) {
1434
+ process.stdout.write("\u001B[2J\u001B[3J\u001B[H");
1435
+ setLayoutEpoch((current) => current + 1);
1436
+ }
1437
+ previousStartupDiagnosticsRef.current = showStartupDiagnostics;
1438
+ }, [showStartupDiagnostics]);
1439
+ return (_jsxs(Box, { flexDirection: "column", children: [extensionHeader ? (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(ExtensionComponentHost, { hosted: extensionHeader, width: terminalColumns }) })) : (_jsx(Header, { collapseChangelog: context.session.settingsManager.getCollapseChangelog(), sessionName: snapshot.sessionName, showBranding: Boolean(verbose) || !context.session.settingsManager.getQuietStartup(), startupChangelog: startupChangelog, startupDiagnostics: showStartupDiagnostics ? startupDiagnostics : undefined, startupNotices: startupNotices, theme: theme })), _jsx(MessageList, { displayBlocks: displayBlocks, expandToolOutputs: toolOutputExpanded, maxItems: maxItems, messages: snapshot.messages, showImages: state.showImages, showThinking: state.showThinking, theme: theme }), _jsx(TaskPanel, { expandToolOutputs: toolOutputExpanded, pendingMessages: pendingMessages, snapshot: snapshot, theme: theme, toolExecutions: state.toolExecutions }), _jsx(StatusLine, { snapshot: snapshot, status: effectiveStatus, theme: theme }), Array.from(extensionWidgetsAbove.entries()).map(([key, widget]) => widget.kind === "lines" ? (_jsx(Box, { marginTop: 1, children: renderTextLines(widget.lines, `widget-above:${key}`) }, `widget-above:${key}`)) : (_jsx(Box, { marginTop: 1, children: _jsx(ExtensionComponentHost, { hosted: widget.hosted, maxLines: 10, width: terminalColumns }) }, `widget-above:${key}`))), extensionEditor ? (_jsx(Box, { marginTop: 1, children: _jsx(ExtensionComponentHost, { active: true, hosted: extensionEditor, onKeyData: handleExtensionShortcutKeyData, width: terminalColumns }) })) : extensionOverlayState.kind === "custom" &&
1440
+ extensionOverlayState.mode === "inline" &&
1441
+ !extensionOverlayState.hidden ? (_jsx(Box, { marginTop: 1, children: _jsx(ExtensionComponentHost, { active: true, hosted: extensionOverlayState.hosted, width: terminalColumns }) })) : (_jsx(PromptInput, { cursorOffset: state.cursorOffset, disabled: state.overlay.kind !== "none" || extensionUiActive, dispatch: dispatch, input: state.input, keybindings: keybindings, onCancelActiveWork: async (kind) => {
586
1442
  if (kind === "bash") {
587
1443
  context.session.abortBash();
588
1444
  setStatus("Cancelled bash command.", "warning");
589
1445
  return;
590
1446
  }
591
- await context.session.abort();
1447
+ if (context.session.isCompacting) {
1448
+ context.session.abortCompaction();
1449
+ setStatus("Cancelling compaction...", "warning");
1450
+ return;
1451
+ }
1452
+ const restored = restoreQueuedMessagesToPrompt({ abort: true });
592
1453
  refresh();
593
- setStatus("Cancelled active work.", "warning");
594
- }, onOpenFork: () => setOverlay({ kind: "userMessage", items: listUserMessages(context.session) }), onOpenTree: () => setOverlay({ kind: "tree", items: flattenSessionTree(context.session) }), onSubmit: (value) => submitPrompt(value ?? state.input, "followUp"), onSubmitSteer: (value) => submitPrompt(value ?? state.input, "steer"), onThinkingLevelChange: (level) => {
1454
+ if (restored > 0) {
1455
+ setStatus(`Cancelled active work and restored ${restored} queued message${restored === 1 ? "" : "s"}.`, "warning");
1456
+ }
1457
+ else {
1458
+ setStatus("Cancelled active work.", "warning");
1459
+ }
1460
+ }, onCycleModel: cycleModel, onDequeue: dequeueQueuedMessages, onExit: handleExit, onExternalEditor: openExternalEditor, onKeyData: handleExtensionShortcutKeyData, onOpenModelSelector: () => setOverlay({ kind: "model" }), onOpenFork: () => setOverlay({ kind: "userMessage", items: listUserMessages(context.session) }), onOpenTree: () => setOverlay({ kind: "tree", items: flattenSessionTree(context.session) }), onPasteClipboardImage: pasteClipboardImage, onStatus: setStatus, onSubmit: (value) => submitPromptWithStartupDismiss(value ?? state.input, "steer"), onSubmitFollowUp: (value) => submitPromptWithStartupDismiss(value ?? state.input, "followUp"), onSubmitSteer: (value) => submitPromptWithStartupDismiss(value ?? state.input, "steer"), onSuspend: suspendTerminal, onThinkingLevelChange: (level) => {
595
1461
  refresh();
596
1462
  setStatus(`Thinking level: ${level}`, "success");
597
- }, session: context.session, theme: theme }), _jsx(Footer, { availableProviderCount: footerData.availableProviderCount, branch: footerData.branch, snapshot: snapshot, theme: theme }), overlay, state.overlay.kind === "none" ? null : _jsx(Text, { children: theme.muted("Dialogs pause typing in the main prompt until they close.") })] }));
1463
+ }, onToggleThinkingVisibility: toggleThinkingVisibility, onToggleToolExpansion: toggleToolOutputExpansion, session: context.session, theme: theme })), Array.from(extensionWidgetsBelow.entries()).map(([key, widget]) => widget.kind === "lines" ? (_jsx(Box, { marginTop: 1, children: renderTextLines(widget.lines, `widget-below:${key}`) }, `widget-below:${key}`)) : (_jsx(Box, { marginTop: 1, children: _jsx(ExtensionComponentHost, { hosted: widget.hosted, maxLines: 10, width: terminalColumns }) }, `widget-below:${key}`))), extensionFooter ? (_jsx(ExtensionComponentHost, { hosted: extensionFooter, width: terminalColumns })) : (_jsx(Footer, { availableProviderCount: footerData.availableProviderCount, branch: footerData.branch, extensionStatuses: footerData.extensionStatuses, snapshot: snapshot, theme: theme })), overlay, extensionOverlay, state.overlay.kind === "none" && extensionOverlayState.kind === "none" ? null : (_jsx(Text, { children: theme.muted("Dialogs pause typing in the main prompt until they close.") }))] }, layoutEpoch));
598
1464
  }
599
1465
  //# sourceMappingURL=AppShell.js.map