praxis-agent 0.2.0 → 0.4.0

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 (110) hide show
  1. package/README.md +19 -4
  2. package/dist/application/session-service.d.ts +55 -0
  3. package/dist/application/session-service.d.ts.map +1 -1
  4. package/dist/application/session-service.js +939 -39
  5. package/dist/application/session-service.js.map +1 -1
  6. package/dist/cli/interactive.d.ts +72 -8
  7. package/dist/cli/interactive.d.ts.map +1 -1
  8. package/dist/cli/interactive.js +2809 -192
  9. package/dist/cli/interactive.js.map +1 -1
  10. package/dist/cli/protocol.d.ts +1 -0
  11. package/dist/cli/protocol.d.ts.map +1 -1
  12. package/dist/cli/protocol.js.map +1 -1
  13. package/dist/cli/tui/claude-style.d.ts +157 -3
  14. package/dist/cli/tui/claude-style.d.ts.map +1 -1
  15. package/dist/cli/tui/claude-style.js +403 -16
  16. package/dist/cli/tui/claude-style.js.map +1 -1
  17. package/dist/cli/tui/clipboard.d.ts +27 -0
  18. package/dist/cli/tui/clipboard.d.ts.map +1 -0
  19. package/dist/cli/tui/clipboard.js +165 -0
  20. package/dist/cli/tui/clipboard.js.map +1 -0
  21. package/dist/cli/tui/composer-editor.d.ts +19 -0
  22. package/dist/cli/tui/composer-editor.d.ts.map +1 -0
  23. package/dist/cli/tui/composer-editor.js +99 -0
  24. package/dist/cli/tui/composer-editor.js.map +1 -0
  25. package/dist/cli/tui/composer-images.d.ts +7 -0
  26. package/dist/cli/tui/composer-images.d.ts.map +1 -0
  27. package/dist/cli/tui/composer-images.js +48 -0
  28. package/dist/cli/tui/composer-images.js.map +1 -0
  29. package/dist/cli/tui/conversation-export.d.ts +6 -0
  30. package/dist/cli/tui/conversation-export.d.ts.map +1 -0
  31. package/dist/cli/tui/conversation-export.js +76 -0
  32. package/dist/cli/tui/conversation-export.js.map +1 -0
  33. package/dist/cli/tui/external-editor.d.ts +21 -0
  34. package/dist/cli/tui/external-editor.d.ts.map +1 -0
  35. package/dist/cli/tui/external-editor.js +144 -0
  36. package/dist/cli/tui/external-editor.js.map +1 -0
  37. package/dist/cli/tui/file-picker.d.ts +33 -0
  38. package/dist/cli/tui/file-picker.d.ts.map +1 -0
  39. package/dist/cli/tui/file-picker.js +117 -0
  40. package/dist/cli/tui/file-picker.js.map +1 -0
  41. package/dist/cli/tui/git-diff.d.ts +14 -0
  42. package/dist/cli/tui/git-diff.d.ts.map +1 -0
  43. package/dist/cli/tui/git-diff.js +55 -0
  44. package/dist/cli/tui/git-diff.js.map +1 -0
  45. package/dist/cli/tui/keybindings.d.ts +14 -0
  46. package/dist/cli/tui/keybindings.d.ts.map +1 -0
  47. package/dist/cli/tui/keybindings.js +430 -0
  48. package/dist/cli/tui/keybindings.js.map +1 -0
  49. package/dist/cli/tui/permission-settings.d.ts +18 -0
  50. package/dist/cli/tui/permission-settings.d.ts.map +1 -0
  51. package/dist/cli/tui/permission-settings.js +109 -0
  52. package/dist/cli/tui/permission-settings.js.map +1 -0
  53. package/dist/cli/tui/slash-commands.d.ts +11 -0
  54. package/dist/cli/tui/slash-commands.d.ts.map +1 -0
  55. package/dist/cli/tui/slash-commands.js +188 -0
  56. package/dist/cli/tui/slash-commands.js.map +1 -0
  57. package/dist/cli/tui/terminal-suspend.d.ts +5 -0
  58. package/dist/cli/tui/terminal-suspend.d.ts.map +1 -0
  59. package/dist/cli/tui/terminal-suspend.js +9 -0
  60. package/dist/cli/tui/terminal-suspend.js.map +1 -0
  61. package/dist/cli/tui/workspace-directories.d.ts +3 -0
  62. package/dist/cli/tui/workspace-directories.d.ts.map +1 -0
  63. package/dist/cli/tui/workspace-directories.js +55 -0
  64. package/dist/cli/tui/workspace-directories.js.map +1 -0
  65. package/dist/cli.d.ts +26 -2
  66. package/dist/cli.d.ts.map +1 -1
  67. package/dist/cli.js +48 -5
  68. package/dist/cli.js.map +1 -1
  69. package/dist/compatibility/claude/compaction.d.ts +8 -0
  70. package/dist/compatibility/claude/compaction.d.ts.map +1 -1
  71. package/dist/compatibility/claude/compaction.js +45 -8
  72. package/dist/compatibility/claude/compaction.js.map +1 -1
  73. package/dist/compatibility/claude/fork.d.ts.map +1 -1
  74. package/dist/compatibility/claude/fork.js +24 -6
  75. package/dist/compatibility/claude/fork.js.map +1 -1
  76. package/dist/compatibility/claude/history.d.ts.map +1 -1
  77. package/dist/compatibility/claude/history.js +41 -1
  78. package/dist/compatibility/claude/history.js.map +1 -1
  79. package/dist/compatibility/claude/projection.d.ts +29 -1
  80. package/dist/compatibility/claude/projection.d.ts.map +1 -1
  81. package/dist/compatibility/claude/projection.js +132 -2
  82. package/dist/compatibility/claude/projection.js.map +1 -1
  83. package/dist/compatibility/claude/schema.d.ts.map +1 -1
  84. package/dist/compatibility/claude/schema.js +45 -8
  85. package/dist/compatibility/claude/schema.js.map +1 -1
  86. package/dist/compatibility/claude/translation.d.ts +7 -0
  87. package/dist/compatibility/claude/translation.d.ts.map +1 -1
  88. package/dist/compatibility/claude/translation.js +20 -0
  89. package/dist/compatibility/claude/translation.js.map +1 -1
  90. package/dist/core/runtime.d.ts +19 -0
  91. package/dist/core/runtime.d.ts.map +1 -1
  92. package/dist/core/runtime.js +30 -11
  93. package/dist/core/runtime.js.map +1 -1
  94. package/dist/extensions/claude-extensions.d.ts +12 -0
  95. package/dist/extensions/claude-extensions.d.ts.map +1 -1
  96. package/dist/extensions/claude-extensions.js +49 -0
  97. package/dist/extensions/claude-extensions.js.map +1 -1
  98. package/dist/hooks/claude-hook-tools.js +2 -2
  99. package/dist/hooks/claude-hook-tools.js.map +1 -1
  100. package/dist/persistence/claude-transcript-store.d.ts.map +1 -1
  101. package/dist/persistence/claude-transcript-store.js +12 -5
  102. package/dist/persistence/claude-transcript-store.js.map +1 -1
  103. package/dist/tools/claude-interactive-tools.d.ts +1 -0
  104. package/dist/tools/claude-interactive-tools.d.ts.map +1 -1
  105. package/dist/tools/claude-interactive-tools.js +12 -0
  106. package/dist/tools/claude-interactive-tools.js.map +1 -1
  107. package/dist/tools/local-tools.d.ts.map +1 -1
  108. package/dist/tools/local-tools.js +11 -0
  109. package/dist/tools/local-tools.js.map +1 -1
  110. package/package.json +1 -1
@@ -1,8 +1,102 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { homedir } from 'node:os';
3
+ import { resolve } from 'node:path';
2
4
  import { useEffect, useMemo, useRef, useState } from 'react';
3
5
  import { Box, Text, render, useApp, useInput } from 'ink';
6
+ import { AgentRunCancelledError } from '../core/runtime.js';
4
7
  import { redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
5
- import { Composer, DialogFrame, SessionPicker, Transcript, WelcomePanel, useTerminalWidth, } from './tui/claude-style.js';
8
+ import { CommandPalette, BtwPanel, Composer, DiffDashboard, DialogFrame, ExternalEditorWait, HelpMenu, ListDashboard, MentionPicker, PermissionDashboard, SelectionMenu, SessionPicker, StatusDashboard, Transcript, WelcomePanel, useTerminalWidth, } from './tui/claude-style.js';
9
+ import { loadGitDiff, visiblePatchLines, } from './tui/git-diff.js';
10
+ import { addTuiPermissionRule, loadTuiPermissionRules, removeTuiPermissionRule, } from './tui/permission-settings.js';
11
+ import { filterTuiSlashCommands, mergeTuiSlashCommands, slashCommandQuery, } from './tui/slash-commands.js';
12
+ import { createComposerEditor, deleteComposerToEnd, deleteComposerToStart, deleteComposerWordBackward, insertComposerText, moveComposerCursorByWord, } from './tui/composer-editor.js';
13
+ import { applyMentionReference, fileReferenceAtCursor, filterTuiMentionEntries, loadTuiFileEntries, } from './tui/file-picker.js';
14
+ import { editTuiPrompt, openTuiEditorFile, } from './tui/external-editor.js';
15
+ import { suspendTuiProcess } from './tui/terminal-suspend.js';
16
+ import { readTuiClipboard, writeTuiClipboard, writeTuiOsc52Clipboard, } from './tui/clipboard.js';
17
+ import { composerImageIds, deleteComposerImageBackward, deleteComposerImageForward, insertComposerImageMarker, moveComposerCursorAcrossImages, } from './tui/composer-images.js';
18
+ import { defaultTuiKeybindings, ensureTuiKeybindingsFile, hasTuiKeybindingPrefix, loadTuiKeybindings, resolveTuiKeybinding, tuiKeyChord, } from './tui/keybindings.js';
19
+ import { completeTuiWorkspaceDirectory, resolveTuiWorkspaceDirectory, } from './tui/workspace-directories.js';
20
+ import { conversationExportPath, conversationExportText, defaultConversationExportFilename, writeConversationExport, } from './tui/conversation-export.js';
21
+ const EMPTY_SLASH_COMMANDS = [];
22
+ const EMPTY_AGENTS = [];
23
+ const EFFORT_OPTIONS = ['low', 'medium', 'high', 'xhigh', 'max'];
24
+ const PERMISSION_OPTIONS = [
25
+ {
26
+ mode: 'default',
27
+ label: 'Default',
28
+ description: 'Ask before changes that need approval.',
29
+ },
30
+ {
31
+ mode: 'acceptEdits',
32
+ label: 'Accept edits',
33
+ description: 'Allow file edits while keeping command approval.',
34
+ },
35
+ {
36
+ mode: 'auto',
37
+ label: 'Auto',
38
+ description: 'Classify safe tool calls before asking.',
39
+ },
40
+ {
41
+ mode: 'manual',
42
+ label: 'Manual',
43
+ description: 'Ask for every tool call that is not explicitly allowed.',
44
+ },
45
+ {
46
+ mode: 'dontAsk',
47
+ label: "Don't ask",
48
+ description: 'Deny tool calls that require confirmation.',
49
+ },
50
+ {
51
+ mode: 'plan',
52
+ label: 'Plan mode',
53
+ description: 'Restrict changes to the plan file until approval.',
54
+ },
55
+ ];
56
+ function rewindActions(point) {
57
+ const shared = [
58
+ { action: 'summarize-from', label: 'Summarize from here' },
59
+ { action: 'summarize-to', label: 'Summarize up to here' },
60
+ { action: 'cancel', label: 'Never mind' },
61
+ ];
62
+ return point.fileRestoreAvailable && point.fileChanges.length > 0
63
+ ? [
64
+ {
65
+ action: 'code-and-conversation',
66
+ label: 'Restore code and conversation',
67
+ },
68
+ { action: 'conversation', label: 'Restore conversation' },
69
+ { action: 'code', label: 'Restore code' },
70
+ ...shared,
71
+ ]
72
+ : [{ action: 'conversation', label: 'Restore conversation' }, ...shared];
73
+ }
74
+ function rewindPointWindow(points, selectedIndex, size = 6) {
75
+ if (points.length <= size)
76
+ return { start: 0, end: points.length };
77
+ if (selectedIndex >= points.length) {
78
+ return { start: points.length - size, end: points.length };
79
+ }
80
+ const start = Math.max(0, Math.min(points.length - size, selectedIndex - Math.floor(size / 2)));
81
+ return { start, end: start + size };
82
+ }
83
+ function permissionMode(value) {
84
+ return [
85
+ 'acceptEdits',
86
+ 'auto',
87
+ 'bypassPermissions',
88
+ 'manual',
89
+ 'dontAsk',
90
+ 'plan',
91
+ ].includes(value ?? '')
92
+ ? value
93
+ : 'default';
94
+ }
95
+ function effort(value) {
96
+ return EFFORT_OPTIONS.includes(value)
97
+ ? value
98
+ : 'high';
99
+ }
6
100
  function questionAnswer(question, input) {
7
101
  const answer = input.trim();
8
102
  if (!answer)
@@ -30,19 +124,91 @@ function questionAnswer(question, input) {
30
124
  })
31
125
  .join(', ');
32
126
  }
127
+ function permissionDeleteTitle(behavior) {
128
+ return `Delete ${behavior === 'allow' ? 'allowed' : behavior === 'deny' ? 'denied' : 'ask'} tool?`;
129
+ }
130
+ function permissionScopeLabel(scope) {
131
+ return scope === 'local'
132
+ ? 'From project local settings'
133
+ : scope === 'project'
134
+ ? 'From project settings'
135
+ : 'From user settings';
136
+ }
137
+ function permissionRuleDescription(rule) {
138
+ const bashPrefix = /^Bash\((.*?)(?::\*| \*)\)$/u.exec(rule)?.[1]?.trim();
139
+ return bashPrefix ? `Any Bash command starting with ${bashPrefix}` : undefined;
140
+ }
33
141
  function describeTool(call, sensitiveValues) {
34
142
  const name = redactSensitiveText(call.name, sensitiveValues);
35
143
  return `${name} ${describeToolInput(call, sensitiveValues)}`;
36
144
  }
145
+ function redactToolCall(call, sensitiveValues) {
146
+ return {
147
+ ...call,
148
+ name: redactSensitiveText(call.name, sensitiveValues),
149
+ input: JSON.parse(redactSensitiveText(JSON.stringify(call.input), sensitiveValues)),
150
+ };
151
+ }
37
152
  function describeToolInput(call, sensitiveValues) {
38
153
  const detail = redactSensitiveText(JSON.stringify(call.input), sensitiveValues);
39
154
  return detail.length > 160 ? `${detail.slice(0, 157)}...` : detail;
40
155
  }
41
- export function InteractiveApp({ factory, initialSessions, initialPrompt, signal, onCancel, onTurnChange, onCleanup, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, }) {
42
- const { exit } = useApp();
156
+ function filterSessionChoices(sessions, search) {
157
+ const query = search.trim().toLowerCase();
158
+ if (!query)
159
+ return sessions;
160
+ return sessions.filter((session) => {
161
+ if (!session)
162
+ return false;
163
+ return [session.sessionId, session.name, session.lastPrompt].some((value) => value?.toLowerCase().includes(query));
164
+ });
165
+ }
166
+ function estimatedCommandTokens(command) {
167
+ return Math.max(1, Math.ceil(`/${command.name} ${command.description}`.length / 4));
168
+ }
169
+ function workflowRows(workflows) {
170
+ return workflows.map((workflow) => ({
171
+ label: `${String(workflow.task_id ?? workflow.id ?? 'task')} [${String(workflow.status ?? 'unknown')}] ${String(workflow.summary ?? workflow.description ?? '')}`.trim(),
172
+ }));
173
+ }
174
+ function ordinal(value) {
175
+ const remainder100 = value % 100;
176
+ if (remainder100 >= 11 && remainder100 <= 13)
177
+ return `${value}th`;
178
+ switch (value % 10) {
179
+ case 1:
180
+ return `${value}st`;
181
+ case 2:
182
+ return `${value}nd`;
183
+ case 3:
184
+ return `${value}rd`;
185
+ default:
186
+ return `${value}th`;
187
+ }
188
+ }
189
+ export function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory = [], signal, onCancel, onTurnChange, onCleanup, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, }) {
190
+ const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
43
191
  const width = useTerminalWidth(terminalWidth);
192
+ const keybindingsRoot = useMemo(() => resolve(keybindingsConfigRoot ??
193
+ process.env.CLAUDE_CONFIG_DIR ??
194
+ resolve(homedir(), '.claude')), [keybindingsConfigRoot]);
44
195
  const sensitiveValues = useMemo(() => sensitiveEnvironmentValues(process.env), []);
196
+ const [runtimeCwd, setRuntimeCwd] = useState(display.cwd);
197
+ const runtimeCwdRef = useRef(display.cwd);
198
+ const loadDiffSnapshot = useMemo(() => diffLoader ?? (() => loadGitDiff(runtimeCwd)), [diffLoader, runtimeCwd]);
199
+ const loadFiles = useMemo(() => fileLoader ?? (() => loadTuiFileEntries(runtimeCwd)), [fileLoader, runtimeCwd]);
200
+ const permissionStore = useMemo(() => permissionRuleStore ?? {
201
+ load: () => loadTuiPermissionRules(runtimeCwd),
202
+ add: (input) => addTuiPermissionRule({ cwd: runtimeCwd, ...input }),
203
+ remove: removeTuiPermissionRule,
204
+ }, [permissionRuleStore, runtimeCwd]);
45
205
  const choices = useMemo(() => allowNewSession ? [null, ...initialSessions] : initialSessions, [allowNewSession, initialSessions]);
206
+ const [pickerIncludesNewSession, setPickerIncludesNewSession] = useState(allowNewSession);
207
+ const pickerIncludesNewSessionRef = useRef(allowNewSession);
208
+ const pickerChoices = pickerIncludesNewSession ? choices : initialSessions;
209
+ const [sessionSearch, setSessionSearch] = useState('');
210
+ const sessionSearchRef = useRef('');
211
+ const filteredPickerChoices = useMemo(() => filterSessionChoices(pickerChoices, sessionSearch), [pickerChoices, sessionSearch]);
46
212
  const [selectingSession, setSelectingSession] = useState(initialSessions.length > 0 &&
47
213
  resume?.sessionId === undefined &&
48
214
  (!allowNewSession || resume?.requireSession === true));
@@ -52,14 +218,69 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
52
218
  const [pendingFork, setPendingFork] = useState(resume?.forkSession === true);
53
219
  const [input, setInput] = useState('');
54
220
  const inputRef = useRef('');
221
+ const [inputCursor, setInputCursor] = useState(0);
222
+ const inputCursorRef = useRef(0);
223
+ const inputHistoryRef = useRef([]);
224
+ const inputHistoryIndexRef = useRef(null);
225
+ const inputHistoryDraftRef = useRef('');
226
+ const undoStackRef = useRef([]);
227
+ const composerImagesRef = useRef(new Map());
228
+ const nextImageIdRef = useRef(1);
229
+ const clipboardQueueRef = useRef(Promise.resolve());
230
+ const clipboardPendingRef = useRef(0);
231
+ const componentMountedRef = useRef(true);
232
+ const [clipboardBusy, setClipboardBusy] = useState(false);
233
+ const stashedInputRef = useRef('');
234
+ const lastEscapeAtRef = useRef(0);
235
+ const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
236
+ const [commandSelection, setCommandSelection] = useState(0);
237
+ const [filePickerOpen, setFilePickerOpen] = useState(false);
238
+ const [fileSelection, setFileSelection] = useState(0);
239
+ const [fileEntries, setFileEntries] = useState(null);
240
+ const [shortcutsVisible, setShortcutsVisible] = useState(false);
241
+ const [externalEditorRequest, setExternalEditorRequest] = useState(null);
242
+ const [keybindingsEditing, setKeybindingsEditing] = useState(false);
243
+ const [keybindings, setKeybindings] = useState(defaultTuiKeybindings);
244
+ const keySequenceRef = useRef(null);
245
+ const [processSuspendRequested, setProcessSuspendRequested] = useState(false);
246
+ const processSuspendRequestedRef = useRef(false);
247
+ const [editorFooterMessage, setEditorFooterMessage] = useState();
248
+ const [availableSlashCommands, setAvailableSlashCommands] = useState(slashCommands);
249
+ const [availableAgents, setAvailableAgents] = useState(agents);
250
+ const [btwHistory, setBtwHistory] = useState([]);
251
+ const btwHistoryRef = useRef([]);
252
+ const btwIdRef = useRef(0);
253
+ const btwControllerRef = useRef(null);
254
+ const [btwCopied, setBtwCopied] = useState(false);
255
+ const btwCopiedTimerRef = useRef(null);
256
+ const [menu, setMenu] = useState(null);
257
+ const menuRef = useRef(null);
55
258
  const [busy, setBusy] = useState(false);
259
+ const [compactProgress, setCompactProgress] = useState(0);
56
260
  const initialPromptRef = useRef(initialPrompt?.trim() ?? '');
57
261
  const [initialPromptPending, setInitialPromptPending] = useState(initialPromptRef.current.length > 0);
58
262
  const [status, setStatus] = useState('ready');
59
263
  const [activeText, setActiveText] = useState('');
60
264
  const [activeThinking, setActiveThinking] = useState('');
265
+ const [thinkingExpanded, setThinkingExpanded] = useState(false);
61
266
  const [usage, setUsage] = useState();
62
- const [history, setHistory] = useState([]);
267
+ const [costUsd, setCostUsd] = useState();
268
+ const [contextWindowTokens, setContextWindowTokens] = useState(display.contextWindowTokens);
269
+ const [history, setHistory] = useState([...initialHistory]);
270
+ const sessionLoadRef = useRef(0);
271
+ const [turnDiffs, setTurnDiffs] = useState([]);
272
+ const turnNumberRef = useRef(0);
273
+ const turnMutatedFilesRef = useRef(false);
274
+ const permissionCallsRef = useRef(new Map());
275
+ const [recentDenied, setRecentDenied] = useState([]);
276
+ const [exitConfirmation, setExitConfirmation] = useState(false);
277
+ const exitConfirmationTimerRef = useRef(null);
278
+ const [runtimePreferences, setRuntimePreferences] = useState(() => ({
279
+ effort: effort(display.effort),
280
+ permissionMode: permissionMode(display.permissionMode),
281
+ additionalDirectories: [...new Set(additionalDirectories)],
282
+ }));
283
+ const runtimePreferencesRef = useRef(runtimePreferences);
63
284
  const [permission, setPermission] = useState(null);
64
285
  const permissionRef = useRef(null);
65
286
  const [elicitation, setElicitation] = useState(null);
@@ -74,6 +295,121 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
74
295
  onCleanupRef.current = onCleanup;
75
296
  const scheduledWaitRef = useRef(null);
76
297
  const turnControllerRef = useRef(null);
298
+ const serviceRetirementRef = useRef(null);
299
+ const serviceEpochRef = useRef(0);
300
+ const serviceCreationEpochRef = useRef(null);
301
+ const allSlashCommands = useMemo(() => mergeTuiSlashCommands(availableSlashCommands), [availableSlashCommands]);
302
+ const builtinSlashCommands = useMemo(() => allSlashCommands.filter((command) => command.source === 'builtin'), [allSlashCommands]);
303
+ const customSlashCommands = useMemo(() => allSlashCommands.filter((command) => command.source !== 'builtin'), [allSlashCommands]);
304
+ const commandQuery = commandPaletteOpen ? slashCommandQuery(input) : null;
305
+ const fileReference = filePickerOpen
306
+ ? fileReferenceAtCursor(input, inputCursor)
307
+ : null;
308
+ const shellMode = input.startsWith('!');
309
+ const matchingSlashCommands = useMemo(() => commandQuery === null
310
+ ? []
311
+ : filterTuiSlashCommands(allSlashCommands, commandQuery), [allSlashCommands, commandQuery]);
312
+ const commandPaletteVisible = !busy &&
313
+ !permission &&
314
+ !planApproval &&
315
+ !question &&
316
+ !elicitation &&
317
+ !selectingSession &&
318
+ commandQuery !== null;
319
+ const matchingMentionEntries = useMemo(() => fileEntries === null || fileReference === null
320
+ ? []
321
+ : filterTuiMentionEntries(fileEntries, availableAgents, fileReference.query), [availableAgents, fileEntries, fileReference?.query]);
322
+ const filePickerVisible = !busy &&
323
+ !permission &&
324
+ !planApproval &&
325
+ !question &&
326
+ !elicitation &&
327
+ !selectingSession &&
328
+ filePickerOpen &&
329
+ !shellMode &&
330
+ fileReference !== null;
331
+ const selectedFileIndex = Math.min(fileSelection, Math.max(0, matchingMentionEntries.length - 1));
332
+ const hasThinking = activeThinking.length > 0 ||
333
+ history.some((item) => item.kind === 'thinking');
334
+ const hasDetailedTranscript = hasThinking ||
335
+ history.some((item) => item.kind === 'tool' ||
336
+ item.kind === 'shell' ||
337
+ item.kind === 'compact');
338
+ const selectedSlashCommandIndex = Math.min(commandSelection, Math.max(0, matchingSlashCommands.length - 1));
339
+ const runtimeDisplay = {
340
+ ...display,
341
+ cwd: runtimeCwd,
342
+ ...(runtimePreferences.model === undefined
343
+ ? {}
344
+ : { model: runtimePreferences.model }),
345
+ effort: runtimePreferences.effort,
346
+ permissionMode: runtimePreferences.permissionMode,
347
+ ...(contextWindowTokens === undefined ? {} : { contextWindowTokens }),
348
+ };
349
+ const workspaceDirectories = [
350
+ runtimeCwd,
351
+ ...runtimePreferences.additionalDirectories.filter((path) => path !== runtimeCwd),
352
+ ].map((path) => ({ path, original: path === runtimeCwd }));
353
+ const permissionOptions = useMemo(() => [
354
+ ...PERMISSION_OPTIONS,
355
+ ...(allowDangerouslySkipPermissions
356
+ ? [
357
+ {
358
+ mode: 'bypassPermissions',
359
+ label: 'Bypass permissions',
360
+ description: 'Allow tool calls except explicit deny rules for this session.',
361
+ },
362
+ ]
363
+ : []),
364
+ ], [allowDangerouslySkipPermissions]);
365
+ const modelOptions = useMemo(() => [
366
+ {
367
+ label: runtimeDisplay.model
368
+ ? `Current: ${runtimeDisplay.model}`
369
+ : 'Current: provider default',
370
+ description: 'Keep the model selected for this interactive session.',
371
+ selected: true,
372
+ },
373
+ {
374
+ label: 'Use invocation default',
375
+ description: 'Use the model supplied by the original CLI invocation.',
376
+ },
377
+ {
378
+ label: 'Enter a model ID…',
379
+ description: 'Use any model identifier supported by the configured provider.',
380
+ },
381
+ ], [runtimeDisplay.model]);
382
+ useEffect(() => {
383
+ setAvailableSlashCommands(slashCommands);
384
+ }, [slashCommands]);
385
+ useEffect(() => {
386
+ setAvailableAgents(agents);
387
+ }, [agents]);
388
+ useEffect(() => {
389
+ if (!commandPaletteOpen)
390
+ return;
391
+ const currentCommands = serviceRef.current?.slashCommands?.();
392
+ if (currentCommands)
393
+ setAvailableSlashCommands(currentCommands);
394
+ }, [commandPaletteOpen]);
395
+ useEffect(() => {
396
+ setFileEntries(null);
397
+ }, [loadFiles]);
398
+ useEffect(() => {
399
+ if (!filePickerVisible || fileEntries !== null)
400
+ return;
401
+ let cancelled = false;
402
+ void loadFiles().then((entries) => {
403
+ if (!cancelled)
404
+ setFileEntries(entries);
405
+ }, () => {
406
+ if (!cancelled)
407
+ setFileEntries([]);
408
+ });
409
+ return () => {
410
+ cancelled = true;
411
+ };
412
+ }, [fileEntries, filePickerVisible, loadFiles]);
77
413
  useEffect(() => {
78
414
  if (!signal)
79
415
  return;
@@ -91,12 +427,18 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
91
427
  return () => signal.removeEventListener('abort', cancel);
92
428
  }, [exit, signal]);
93
429
  useEffect(() => () => {
430
+ componentMountedRef.current = false;
94
431
  permissionRef.current?.resolve(false);
95
432
  elicitationRef.current?.resolve({ action: 'cancel' });
96
433
  questionRef.current?.resolve(null);
97
434
  planApprovalRef.current?.resolve(false);
435
+ if (exitConfirmationTimerRef.current)
436
+ clearTimeout(exitConfirmationTimerRef.current);
437
+ if (btwCopiedTimerRef.current)
438
+ clearTimeout(btwCopiedTimerRef.current);
98
439
  scheduledWaitRef.current?.abort();
99
440
  turnControllerRef.current?.abort();
441
+ btwControllerRef.current?.abort();
100
442
  const closing = serviceRef.current?.close?.() ?? Promise.resolve();
101
443
  if (onCleanupRef.current)
102
444
  onCleanupRef.current(closing);
@@ -104,16 +446,280 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
104
446
  void closing.catch(() => undefined);
105
447
  }, []);
106
448
  const append = (line) => setHistory((current) => [...current, line]);
449
+ useEffect(() => {
450
+ let cancelled = false;
451
+ void keybindingsLoader(keybindingsRoot).then((loaded) => {
452
+ if (!cancelled)
453
+ setKeybindings(loaded);
454
+ }, (error) => {
455
+ if (!cancelled)
456
+ append({
457
+ kind: 'warning',
458
+ text: error instanceof Error ? error.message : String(error),
459
+ });
460
+ });
461
+ return () => {
462
+ cancelled = true;
463
+ };
464
+ }, [keybindingsRoot, keybindingsLoader]);
465
+ const updateComposerInput = (next, cursor) => {
466
+ const editor = createComposerEditor(next, cursor);
467
+ inputRef.current = editor.text;
468
+ inputCursorRef.current = editor.cursor;
469
+ setInput(editor.text);
470
+ setInputCursor(editor.cursor);
471
+ setShortcutsVisible(false);
472
+ setCommandPaletteOpen(slashCommandQuery(editor.text) !== null);
473
+ setCommandSelection(0);
474
+ setFilePickerOpen(fileReferenceAtCursor(editor.text, editor.cursor) !== null);
475
+ setFileSelection(0);
476
+ };
477
+ const updateComposerEditor = (editor, recordUndo = true) => {
478
+ if (recordUndo && editor.text !== inputRef.current) {
479
+ undoStackRef.current = [
480
+ ...undoStackRef.current,
481
+ createComposerEditor(inputRef.current, inputCursorRef.current),
482
+ ].slice(-100);
483
+ }
484
+ updateComposerInput(editor.text, editor.cursor);
485
+ };
486
+ const clearComposerInput = () => updateComposerInput('');
487
+ const promptImages = (prompt) => {
488
+ const seen = new Set();
489
+ return composerImageIds(prompt).flatMap((id) => {
490
+ if (seen.has(id))
491
+ return [];
492
+ seen.add(id);
493
+ const image = composerImagesRef.current.get(id);
494
+ return image ? [image] : [];
495
+ });
496
+ };
497
+ const pasteClipboard = () => {
498
+ clipboardPendingRef.current += 1;
499
+ setClipboardBusy(true);
500
+ const paste = clipboardQueueRef.current.then(async () => {
501
+ if (!componentMountedRef.current)
502
+ return;
503
+ const content = await clipboardReader();
504
+ if (!componentMountedRef.current)
505
+ return;
506
+ if (content.kind === 'text') {
507
+ updateComposerEditor(insertComposerText(createComposerEditor(inputRef.current, inputCursorRef.current), content.text));
508
+ }
509
+ else if (content.kind === 'image') {
510
+ const id = nextImageIdRef.current;
511
+ nextImageIdRef.current += 1;
512
+ composerImagesRef.current.set(id, content.image);
513
+ updateComposerEditor(insertComposerImageMarker(createComposerEditor(inputRef.current, inputCursorRef.current), id));
514
+ }
515
+ });
516
+ clipboardQueueRef.current = paste.catch(() => undefined);
517
+ void paste
518
+ .catch((error) => {
519
+ if (!componentMountedRef.current)
520
+ return;
521
+ setEditorFooterMessage({
522
+ text: error instanceof Error ? error.message : String(error),
523
+ isError: true,
524
+ });
525
+ })
526
+ .finally(() => {
527
+ clipboardPendingRef.current -= 1;
528
+ if (componentMountedRef.current && clipboardPendingRef.current === 0)
529
+ setClipboardBusy(false);
530
+ });
531
+ };
532
+ useEffect(() => {
533
+ if (externalEditorRequest === null)
534
+ return;
535
+ const editing = (async () => {
536
+ try {
537
+ await waitUntilRenderFlush();
538
+ let result;
539
+ await suspendTerminal(async () => {
540
+ result = await externalEditor(externalEditorRequest.prompt, {
541
+ cwd: runtimeCwdRef.current,
542
+ ...(signal === undefined ? {} : { signal }),
543
+ });
544
+ });
545
+ if (!result)
546
+ throw new Error('External editor returned no content');
547
+ updateComposerEditor(createComposerEditor(result.content));
548
+ setEditorFooterMessage({
549
+ text: `ctrl+g to edit in ${result.editorName}`,
550
+ isError: false,
551
+ });
552
+ }
553
+ catch (error) {
554
+ setEditorFooterMessage({
555
+ text: error instanceof Error ? error.message : String(error),
556
+ isError: true,
557
+ });
558
+ }
559
+ finally {
560
+ setExternalEditorRequest(null);
561
+ }
562
+ })();
563
+ onTurnChange?.(editing);
564
+ void editing.finally(() => onTurnChange?.(null));
565
+ }, [externalEditorRequest]);
566
+ useEffect(() => {
567
+ if (!keybindingsEditing)
568
+ return;
569
+ const editing = (async () => {
570
+ try {
571
+ const file = await keybindingsFile(keybindingsRoot);
572
+ await waitUntilRenderFlush();
573
+ await suspendTerminal(async () => {
574
+ await keybindingsEditor(file.path, {
575
+ cwd: runtimeCwdRef.current,
576
+ ...(signal === undefined ? {} : { signal }),
577
+ });
578
+ });
579
+ append({
580
+ kind: 'local-result',
581
+ text: file.created
582
+ ? `Created ${file.path} with template. Opened in your editor.`
583
+ : `Opened ${file.path} in your editor.`,
584
+ });
585
+ try {
586
+ setKeybindings(await keybindingsLoader(keybindingsRoot));
587
+ }
588
+ catch (error) {
589
+ append({
590
+ kind: 'warning',
591
+ text: error instanceof Error ? error.message : String(error),
592
+ });
593
+ }
594
+ }
595
+ catch (error) {
596
+ append({
597
+ kind: 'warning',
598
+ text: error instanceof Error ? error.message : String(error),
599
+ });
600
+ }
601
+ finally {
602
+ setKeybindingsEditing(false);
603
+ }
604
+ })();
605
+ onTurnChange?.(editing);
606
+ void editing.finally(() => onTurnChange?.(null));
607
+ }, [keybindingsEditing]);
608
+ useEffect(() => {
609
+ if (!processSuspendRequested)
610
+ return;
611
+ void (async () => {
612
+ try {
613
+ await waitUntilRenderFlush();
614
+ await suspendTerminal(suspendProcess);
615
+ }
616
+ catch (error) {
617
+ append({
618
+ kind: 'warning',
619
+ text: error instanceof Error ? error.message : String(error),
620
+ });
621
+ }
622
+ finally {
623
+ processSuspendRequestedRef.current = false;
624
+ setProcessSuspendRequested(false);
625
+ }
626
+ })();
627
+ }, [processSuspendRequested]);
628
+ const updateMenu = (next) => {
629
+ menuRef.current = next;
630
+ setMenu(next);
631
+ };
632
+ const updateBtwHistory = (updater) => {
633
+ const next = updater(btwHistoryRef.current);
634
+ btwHistoryRef.current = next;
635
+ setBtwHistory(next);
636
+ };
637
+ useEffect(() => {
638
+ if (menu?.kind !== 'compact-progress')
639
+ return;
640
+ const startedAt = Date.now();
641
+ const timer = setInterval(() => {
642
+ setCompactProgress(Math.min(99, Math.max(1, Math.floor((Date.now() - startedAt) / 250))));
643
+ }, 250);
644
+ return () => clearInterval(timer);
645
+ }, [menu?.kind]);
646
+ const appendPromptHistory = (prompt) => {
647
+ if (!prompt)
648
+ return;
649
+ const history = inputHistoryRef.current.filter((item) => item !== prompt);
650
+ inputHistoryRef.current = [prompt, ...history].slice(0, 100);
651
+ inputHistoryIndexRef.current = null;
652
+ inputHistoryDraftRef.current = '';
653
+ };
654
+ const restorePromptHistory = (direction) => {
655
+ const history = inputHistoryRef.current;
656
+ if (history.length === 0)
657
+ return;
658
+ const currentIndex = inputHistoryIndexRef.current;
659
+ if (direction === 'previous') {
660
+ if (currentIndex === null)
661
+ inputHistoryDraftRef.current = inputRef.current;
662
+ const nextIndex = Math.min(history.length - 1, (currentIndex ?? -1) + 1);
663
+ inputHistoryIndexRef.current = nextIndex;
664
+ updateComposerInput(history[nextIndex] ?? '');
665
+ return;
666
+ }
667
+ if (currentIndex === null)
668
+ return;
669
+ const nextIndex = currentIndex - 1;
670
+ if (nextIndex < 0) {
671
+ inputHistoryIndexRef.current = null;
672
+ updateComposerInput(inputHistoryDraftRef.current);
673
+ return;
674
+ }
675
+ inputHistoryIndexRef.current = nextIndex;
676
+ updateComposerInput(history[nextIndex] ?? '');
677
+ };
678
+ const dismissExitConfirmation = () => {
679
+ if (!exitConfirmation)
680
+ return;
681
+ if (exitConfirmationTimerRef.current)
682
+ clearTimeout(exitConfirmationTimerRef.current);
683
+ exitConfirmationTimerRef.current = null;
684
+ setExitConfirmation(false);
685
+ };
686
+ const armExitConfirmation = () => {
687
+ if (exitConfirmation) {
688
+ permissionRef.current?.resolve(false);
689
+ elicitationRef.current?.resolve({ action: 'cancel' });
690
+ questionRef.current?.resolve(null);
691
+ planApprovalRef.current?.resolve(false);
692
+ onCancel?.();
693
+ exit();
694
+ return;
695
+ }
696
+ clearComposerInput();
697
+ setExitConfirmation(true);
698
+ if (exitConfirmationTimerRef.current)
699
+ clearTimeout(exitConfirmationTimerRef.current);
700
+ exitConfirmationTimerRef.current = setTimeout(() => {
701
+ exitConfirmationTimerRef.current = null;
702
+ setExitConfirmation(false);
703
+ }, 1_500);
704
+ };
705
+ useEffect(() => {
706
+ setCommandSelection((current) => Math.min(current, Math.max(0, matchingSlashCommands.length - 1)));
707
+ }, [matchingSlashCommands.length]);
708
+ useEffect(() => {
709
+ setFileSelection((current) => Math.min(current, Math.max(0, matchingMentionEntries.length - 1)));
710
+ }, [matchingMentionEntries.length]);
107
711
  const handleEvent = (event) => {
108
712
  switch (event.type) {
109
713
  case 'text-delta':
110
714
  setActiveText((current) => current + event.delta);
111
715
  break;
112
716
  case 'thinking-start':
113
- setActiveThinking(event.block.type === 'thinking' ? event.block.thinking : '');
717
+ setActiveThinking(event.block.type === 'thinking'
718
+ ? redactSensitiveText(event.block.thinking, sensitiveValues)
719
+ : '');
114
720
  break;
115
721
  case 'thinking-delta':
116
- setActiveThinking((current) => current + event.delta);
722
+ setActiveThinking((current) => current + redactSensitiveText(event.delta, sensitiveValues));
117
723
  break;
118
724
  case 'thinking-signature-delta':
119
725
  // Signatures authenticate a thinking block for provider replay; they are
@@ -123,7 +729,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
123
729
  append({
124
730
  kind: 'thinking',
125
731
  text: event.block.type === 'thinking'
126
- ? event.block.thinking
732
+ ? redactSensitiveText(event.block.thinking, sensitiveValues)
127
733
  : activeThinking,
128
734
  });
129
735
  setActiveThinking('');
@@ -138,13 +744,20 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
138
744
  setUsage(event.usage);
139
745
  break;
140
746
  case 'tool-call':
747
+ if (['Edit', 'Write', 'NotebookEdit'].includes(event.call.name))
748
+ turnMutatedFilesRef.current = true;
749
+ permissionCallsRef.current.set(event.call.id, describeTool(event.call, sensitiveValues));
141
750
  append({
142
751
  kind: 'tool',
143
- call: event.call,
752
+ call: redactToolCall(event.call, sensitiveValues),
144
753
  detail: describeToolInput(event.call, sensitiveValues),
145
754
  });
146
755
  break;
147
756
  case 'permission-decision':
757
+ if (event.behavior === 'deny') {
758
+ const denied = permissionCallsRef.current.get(event.callId) ?? event.callId;
759
+ setRecentDenied((current) => [denied, ...current.filter((item) => item !== denied)].slice(0, 20));
760
+ }
148
761
  append({
149
762
  kind: event.behavior === 'deny' ? 'warning' : 'notice',
150
763
  text: event.behavior === 'ask'
@@ -160,6 +773,26 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
160
773
  isError: event.isError,
161
774
  });
162
775
  break;
776
+ case 'shell-command':
777
+ append({
778
+ kind: 'shell',
779
+ callId: event.callId,
780
+ command: redactSensitiveText(event.command, sensitiveValues),
781
+ });
782
+ break;
783
+ case 'shell-result':
784
+ append({
785
+ kind: 'shell-result',
786
+ callId: event.callId,
787
+ stdout: redactSensitiveText(event.stdout, sensitiveValues),
788
+ stderr: redactSensitiveText(event.stderr, sensitiveValues),
789
+ isError: event.isError,
790
+ });
791
+ break;
792
+ case 'shell-cancelled':
793
+ setHistory((current) => current.filter((item) => !((item.kind === 'shell' || item.kind === 'shell-result') &&
794
+ item.callId === event.callId)));
795
+ break;
163
796
  case 'tool-progress':
164
797
  setStatus(`${event.toolName} · ${event.elapsedTimeSeconds}s`);
165
798
  break;
@@ -281,8 +914,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
281
914
  return;
282
915
  }
283
916
  signal?.addEventListener('abort', abort, { once: true });
284
- inputRef.current = '';
285
- setInput('');
917
+ clearComposerInput();
286
918
  questionRef.current = pending;
287
919
  setQuestion(pending);
288
920
  });
@@ -310,128 +942,899 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
310
942
  planApprovalRef.current = pending;
311
943
  setPlanApproval(pending);
312
944
  });
945
+ const warn = (error) => append({
946
+ kind: 'warning',
947
+ text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
948
+ });
949
+ const retireService = () => {
950
+ serviceEpochRef.current += 1;
951
+ scheduledWaitRef.current?.abort();
952
+ const current = serviceRef.current;
953
+ serviceRef.current = null;
954
+ if (!current?.close)
955
+ return Promise.resolve();
956
+ const closing = current.close().catch((error) => {
957
+ warn(error);
958
+ });
959
+ serviceRetirementRef.current = closing;
960
+ void closing.finally(() => {
961
+ if (serviceRetirementRef.current === closing)
962
+ serviceRetirementRef.current = null;
963
+ });
964
+ return closing;
965
+ };
966
+ const updateRuntimePreferences = (update) => {
967
+ const next = update(runtimePreferencesRef.current);
968
+ runtimePreferencesRef.current = next;
969
+ setRuntimePreferences(next);
970
+ return next;
971
+ };
313
972
  const service = async () => {
314
973
  if (serviceRef.current)
315
974
  return serviceRef.current;
316
- const pending = serviceCreationRef.current ??
317
- factory.createService({
318
- eventSink: handleEvent,
319
- requireProvider: true,
320
- approveRecovery,
321
- approveTool,
322
- onElicitation: requestElicitation,
323
- askUser,
324
- approvePlan,
325
- ...(signal ? { signal } : {}),
326
- });
327
- serviceCreationRef.current = pending;
975
+ let pending = serviceCreationRef.current;
976
+ let epoch = serviceCreationEpochRef.current;
977
+ if (!pending) {
978
+ epoch = serviceEpochRef.current;
979
+ const preferences = runtimePreferencesRef.current;
980
+ pending = (async () => {
981
+ await serviceRetirementRef.current;
982
+ return factory.createService({
983
+ eventSink: handleEvent,
984
+ requireProvider: true,
985
+ approveRecovery,
986
+ approveTool,
987
+ onElicitation: requestElicitation,
988
+ askUser,
989
+ approvePlan,
990
+ ...(preferences.model === undefined
991
+ ? {}
992
+ : { model: preferences.model }),
993
+ effort: preferences.effort,
994
+ permissionMode: preferences.permissionMode,
995
+ additionalDirectories: preferences.additionalDirectories,
996
+ cwd: runtimeCwdRef.current,
997
+ ...(signal ? { signal } : {}),
998
+ });
999
+ })();
1000
+ serviceCreationRef.current = pending;
1001
+ serviceCreationEpochRef.current = epoch;
1002
+ }
328
1003
  try {
329
1004
  const created = await pending;
1005
+ if (epoch !== serviceEpochRef.current) {
1006
+ try {
1007
+ await created.close?.();
1008
+ }
1009
+ catch (error) {
1010
+ warn(error);
1011
+ }
1012
+ if (serviceCreationRef.current === pending) {
1013
+ serviceCreationRef.current = undefined;
1014
+ serviceCreationEpochRef.current = null;
1015
+ }
1016
+ return service();
1017
+ }
330
1018
  serviceRef.current = created;
1019
+ setAvailableSlashCommands(created.slashCommands?.() ?? slashCommands);
1020
+ setAvailableAgents(created.agentDefinitions?.() ?? agents);
1021
+ const runtimeInfo = created.runtimeInfo?.();
1022
+ if (runtimeInfo?.contextWindowTokens !== undefined)
1023
+ setContextWindowTokens(runtimeInfo.contextWindowTokens);
331
1024
  return created;
332
1025
  }
333
1026
  finally {
334
- serviceCreationRef.current = undefined;
1027
+ if (serviceCreationRef.current === pending) {
1028
+ serviceCreationRef.current = undefined;
1029
+ serviceCreationEpochRef.current = null;
1030
+ }
335
1031
  }
336
1032
  };
337
- const submit = async (prompt) => {
338
- scheduledWaitRef.current?.abort();
339
- const turnController = new AbortController();
340
- turnControllerRef.current?.abort();
341
- turnControllerRef.current = turnController;
342
- const turnSignal = signal
343
- ? AbortSignal.any([signal, turnController.signal])
344
- : turnController.signal;
345
- setBusy(true);
346
- setStatus('assembling-context');
347
- setActiveText('');
348
- setActiveThinking('');
349
- append({ kind: 'user', text: prompt });
350
- let commands;
351
- try {
352
- commands = await service();
353
- let activeSessionId = sessionId;
354
- if (activeSessionId && pendingFork) {
355
- const fork = await commands.fork(activeSessionId, resume?.forkSessionId);
356
- activeSessionId = fork.sessionId;
357
- setSessionId(activeSessionId);
358
- setPendingFork(false);
359
- }
360
- const result = activeSessionId
361
- ? await commands.resume(activeSessionId, prompt, turnSignal)
362
- : await commands.run(prompt, turnSignal);
363
- setSessionId(result.sessionId);
364
- setUsage(result.usage);
365
- append({ kind: 'assistant', text: result.text });
366
- setActiveText('');
367
- setActiveThinking('');
368
- setStatus('ready');
1033
+ const openSession = (nextSessionId) => {
1034
+ setSessionId(nextSessionId);
1035
+ const loadId = sessionLoadRef.current + 1;
1036
+ sessionLoadRef.current = loadId;
1037
+ if (nextSessionId === null) {
1038
+ setHistory([]);
1039
+ return;
369
1040
  }
370
- catch (error) {
371
- if (turnController.signal.aborted && !signal?.aborted) {
372
- append({ kind: 'notice', text: 'Interrupted by user.' });
373
- setStatus('cancelled');
1041
+ const loading = (async () => {
1042
+ try {
1043
+ const commands = await service();
1044
+ const transcript = await commands.transcript?.(nextSessionId);
1045
+ if (sessionLoadRef.current === loadId) {
1046
+ setHistory(transcript ? [...transcript] : []);
1047
+ }
374
1048
  }
375
- else {
1049
+ catch (error) {
1050
+ if (sessionLoadRef.current === loadId)
1051
+ warn(error);
1052
+ }
1053
+ })();
1054
+ onTurnChange?.(loading);
1055
+ void loading.finally(() => onTurnChange?.(null));
1056
+ };
1057
+ const changeModel = (model) => {
1058
+ updateRuntimePreferences((current) => {
1059
+ if (model !== undefined)
1060
+ return { ...current, model };
1061
+ const withoutModel = { ...current };
1062
+ delete withoutModel.model;
1063
+ return withoutModel;
1064
+ });
1065
+ void retireService();
1066
+ append({
1067
+ kind: 'notice',
1068
+ text: model
1069
+ ? `Model set to ${model} for this session.`
1070
+ : 'Model reset to the invocation default for this session.',
1071
+ });
1072
+ };
1073
+ const changeEffort = (nextEffort) => {
1074
+ updateRuntimePreferences((current) => ({ ...current, effort: nextEffort }));
1075
+ void retireService();
1076
+ append({
1077
+ kind: 'notice',
1078
+ text: `Effort set to ${nextEffort} for this session.`,
1079
+ });
1080
+ };
1081
+ const changePermissionMode = (mode) => {
1082
+ updateRuntimePreferences((current) => ({
1083
+ ...current,
1084
+ permissionMode: mode,
1085
+ }));
1086
+ const change = (async () => {
1087
+ setBusy(true);
1088
+ setStatus('updating permission mode');
1089
+ try {
1090
+ if (sessionId) {
1091
+ const commands = await service();
1092
+ if (!commands.setPermissionMode) {
1093
+ throw new Error('This interactive service cannot persist permission mode changes.');
1094
+ }
1095
+ await commands.setPermissionMode(sessionId, mode);
1096
+ }
1097
+ await retireService();
376
1098
  append({
377
- kind: 'warning',
378
- text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
1099
+ kind: 'notice',
1100
+ text: `Permission mode set to ${mode} for this session.`,
379
1101
  });
380
- setStatus('failed');
381
1102
  }
382
- }
383
- finally {
384
- if (!factory.scheduledPrompts && commands) {
385
- try {
386
- await commands.close?.();
387
- }
388
- catch (error) {
389
- append({
390
- kind: 'warning',
391
- text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
392
- });
393
- }
394
- finally {
395
- if (serviceRef.current === commands)
396
- serviceRef.current = null;
397
- }
1103
+ catch (error) {
1104
+ warn(error);
398
1105
  }
399
- if (turnControllerRef.current === turnController)
400
- turnControllerRef.current = null;
401
- setBusy(false);
402
- }
1106
+ finally {
1107
+ setBusy(false);
1108
+ setStatus('ready');
1109
+ }
1110
+ })();
1111
+ onTurnChange?.(change);
1112
+ void change.finally(() => onTurnChange?.(null));
403
1113
  };
404
- useEffect(() => {
405
- if (!initialPromptPending || selectingSession || busy)
1114
+ const openTasks = () => {
1115
+ const loading = (async () => {
1116
+ setBusy(true);
1117
+ setStatus('loading tasks');
1118
+ try {
1119
+ const rows = workflowRows((await service()).workflows?.() ?? []);
1120
+ updateMenu({
1121
+ kind: 'list',
1122
+ title: 'Background',
1123
+ rows,
1124
+ emptyText: 'No tasks currently running',
1125
+ selectedIndex: 0,
1126
+ });
1127
+ }
1128
+ catch (error) {
1129
+ warn(error);
1130
+ }
1131
+ finally {
1132
+ setBusy(false);
1133
+ setStatus('ready');
1134
+ }
1135
+ })();
1136
+ onTurnChange?.(loading);
1137
+ void loading.finally(() => onTurnChange?.(null));
1138
+ };
1139
+ const openWorkspaceDirectoryInput = () => {
1140
+ const loading = (async () => {
1141
+ setBusy(true);
1142
+ try {
1143
+ updateMenu({
1144
+ kind: 'workspace-directory-input',
1145
+ rules: await permissionStore.load(),
1146
+ returnToPermissions: false,
1147
+ });
1148
+ }
1149
+ catch (error) {
1150
+ warn(error);
1151
+ }
1152
+ finally {
1153
+ setBusy(false);
1154
+ }
1155
+ })();
1156
+ onTurnChange?.(loading);
1157
+ void loading.finally(() => onTurnChange?.(null));
1158
+ };
1159
+ const copyResponse = (position) => {
1160
+ const response = [...history]
1161
+ .reverse()
1162
+ .filter((item) => item.kind === 'assistant')[position - 1];
1163
+ if (!response) {
1164
+ append({
1165
+ kind: 'warning',
1166
+ text: `No ${position === 1 ? '' : `${ordinal(position)}-latest `}response to copy.`,
1167
+ });
406
1168
  return;
407
- const prompt = initialPromptRef.current;
408
- if (!prompt) {
409
- setInitialPromptPending(false);
1169
+ }
1170
+ const copying = clipboardWriter(response.text).then(() => append({
1171
+ kind: 'local-result',
1172
+ text: `Copied ${position === 1 ? 'last' : `${ordinal(position)}-latest`} response to clipboard.`,
1173
+ }), (error) => warn(error));
1174
+ onTurnChange?.(copying);
1175
+ void copying.finally(() => onTurnChange?.(null));
1176
+ };
1177
+ const exportConversation = (method) => {
1178
+ if (method === 'file') {
1179
+ updateComposerInput(defaultConversationExportFilename());
1180
+ updateMenu({ kind: 'export-filename' });
410
1181
  return;
411
1182
  }
412
- initialPromptRef.current = '';
413
- const turn = submit(prompt);
414
- onTurnChange?.(turn);
415
- void turn.then(() => {
416
- setInitialPromptPending(false);
417
- onTurnChange?.(null);
418
- }, () => {
419
- setInitialPromptPending(false);
420
- onTurnChange?.(null);
421
- });
422
- }, [busy, initialPromptPending, selectingSession]);
423
- useEffect(() => {
424
- if (!factory.scheduledPrompts ||
425
- initialPromptPending ||
426
- selectingSession ||
427
- busy ||
428
- permission) {
1183
+ const text = conversationExportText(runtimeDisplay, history);
1184
+ const copying = clipboardWriter(text).then(() => {
1185
+ updateMenu(null);
1186
+ append({
1187
+ kind: 'local-result',
1188
+ text: 'Conversation copied to clipboard',
1189
+ });
1190
+ }, (error) => warn(error));
1191
+ onTurnChange?.(copying);
1192
+ void copying.finally(() => onTurnChange?.(null));
1193
+ };
1194
+ const saveConversation = (filename) => {
1195
+ let path;
1196
+ try {
1197
+ path = conversationExportPath(runtimeCwdRef.current, filename);
1198
+ }
1199
+ catch (error) {
1200
+ warn(error);
429
1201
  return;
430
1202
  }
431
- const controller = new AbortController();
432
- scheduledWaitRef.current?.abort();
433
- scheduledWaitRef.current = controller;
434
- const waitSignal = signal
1203
+ const text = conversationExportText(runtimeDisplay, history);
1204
+ const saving = exportWriter(path, text).then(() => {
1205
+ clearComposerInput();
1206
+ updateMenu(null);
1207
+ append({
1208
+ kind: 'local-result',
1209
+ text: `Conversation exported to: ${path}`,
1210
+ });
1211
+ }, (error) => warn(error));
1212
+ onTurnChange?.(saving);
1213
+ void saving.finally(() => onTurnChange?.(null));
1214
+ };
1215
+ const reloadExtensions = (kind) => {
1216
+ const loading = (async () => {
1217
+ setBusy(true);
1218
+ setStatus(`reloading ${kind}`);
1219
+ try {
1220
+ await retireService();
1221
+ await service();
1222
+ append({
1223
+ kind: 'local-result',
1224
+ text: `${kind === 'plugins' ? 'Plugin changes activated' : 'Skills reloaded'} for this session.`,
1225
+ });
1226
+ }
1227
+ catch (error) {
1228
+ warn(error);
1229
+ }
1230
+ finally {
1231
+ setBusy(false);
1232
+ setStatus('ready');
1233
+ }
1234
+ })();
1235
+ onTurnChange?.(loading);
1236
+ void loading.finally(() => onTurnChange?.(null));
1237
+ };
1238
+ const openMcpServers = () => {
1239
+ const loading = (async () => {
1240
+ setBusy(true);
1241
+ setStatus('loading MCP servers');
1242
+ try {
1243
+ const servers = (await service()).runtimeInfo?.().mcpServers ?? [];
1244
+ updateMenu({
1245
+ kind: 'list',
1246
+ title: 'MCP servers',
1247
+ rows: servers.map((server) => ({
1248
+ label: server.name,
1249
+ description: server.status,
1250
+ })),
1251
+ emptyText: 'No MCP servers configured',
1252
+ selectedIndex: 0,
1253
+ });
1254
+ }
1255
+ catch (error) {
1256
+ warn(error);
1257
+ }
1258
+ finally {
1259
+ setBusy(false);
1260
+ setStatus('ready');
1261
+ }
1262
+ })();
1263
+ onTurnChange?.(loading);
1264
+ void loading.finally(() => onTurnChange?.(null));
1265
+ };
1266
+ const changeWorkingDirectory = (requestedCwd) => {
1267
+ const changing = (async () => {
1268
+ setBusy(true);
1269
+ setStatus('changing directory');
1270
+ try {
1271
+ const commands = await service();
1272
+ if (!commands.changeCwd) {
1273
+ throw new Error('This interactive service cannot change working directories.');
1274
+ }
1275
+ const cwd = await commands.changeCwd(sessionId ?? undefined, requestedCwd);
1276
+ runtimeCwdRef.current = cwd;
1277
+ setRuntimeCwd(cwd);
1278
+ await retireService();
1279
+ append({ kind: 'local-result', text: `Moved to ${cwd}` });
1280
+ }
1281
+ catch (error) {
1282
+ warn(error);
1283
+ }
1284
+ finally {
1285
+ setBusy(false);
1286
+ setStatus('ready');
1287
+ }
1288
+ })();
1289
+ onTurnChange?.(changing);
1290
+ void changing.finally(() => onTurnChange?.(null));
1291
+ };
1292
+ const showCdUsage = () => {
1293
+ const showing = (async () => {
1294
+ try {
1295
+ if (sessionId)
1296
+ await (await service()).recordCdUsage?.(sessionId);
1297
+ append({ kind: 'local-result', text: 'Usage: /cd <path>' });
1298
+ }
1299
+ catch (error) {
1300
+ warn(error);
1301
+ }
1302
+ })();
1303
+ onTurnChange?.(showing);
1304
+ void showing.finally(() => onTurnChange?.(null));
1305
+ };
1306
+ const showBtwUsage = () => {
1307
+ appendPromptHistory('/btw');
1308
+ const showing = (async () => {
1309
+ try {
1310
+ const activeSessionId = await (await service()).recordBtwUsage?.(sessionId ?? undefined, runtimePreferencesRef.current.permissionMode);
1311
+ if (activeSessionId)
1312
+ setSessionId(activeSessionId);
1313
+ append({ kind: 'user', text: '/btw' });
1314
+ append({ kind: 'local-result', text: 'Usage: /btw <your question>' });
1315
+ }
1316
+ catch (error) {
1317
+ warn(error);
1318
+ }
1319
+ })();
1320
+ onTurnChange?.(showing);
1321
+ void showing.finally(() => onTurnChange?.(null));
1322
+ };
1323
+ const askSideQuestion = (question) => {
1324
+ const id = btwIdRef.current + 1;
1325
+ btwIdRef.current = id;
1326
+ appendPromptHistory(`/btw ${question}`);
1327
+ const entry = {
1328
+ id,
1329
+ question,
1330
+ answer: '',
1331
+ status: 'answering',
1332
+ };
1333
+ updateBtwHistory((entries) => [...entries, entry]);
1334
+ updateMenu({
1335
+ kind: 'btw',
1336
+ selectedIndex: btwHistoryRef.current.length - 1,
1337
+ scrollOffset: 0,
1338
+ });
1339
+ setBtwCopied(false);
1340
+ const controller = new AbortController();
1341
+ btwControllerRef.current?.abort();
1342
+ btwControllerRef.current = controller;
1343
+ const sideSignal = signal
1344
+ ? AbortSignal.any([signal, controller.signal])
1345
+ : controller.signal;
1346
+ const answering = (async () => {
1347
+ try {
1348
+ const commands = await service();
1349
+ if (!commands.answerSideQuestion) {
1350
+ throw new Error('Side questions are unavailable.');
1351
+ }
1352
+ const result = await commands.answerSideQuestion(sessionId ?? undefined, question, sideSignal, (delta) => updateBtwHistory((entries) => entries.map((item) => item.id === id
1353
+ ? { ...item, answer: item.answer + delta }
1354
+ : item)), runtimePreferencesRef.current.permissionMode);
1355
+ if (result.sessionId)
1356
+ setSessionId(result.sessionId);
1357
+ updateBtwHistory((entries) => entries.map((item) => item.id === id ? { ...item, status: 'complete' } : item));
1358
+ if (sideSignal.aborted)
1359
+ throw new AgentRunCancelledError();
1360
+ setUsage((current) => ({
1361
+ inputTokens: (current?.inputTokens ?? 0) + result.usage.inputTokens,
1362
+ outputTokens: (current?.outputTokens ?? 0) + result.usage.outputTokens,
1363
+ cacheReadInputTokens: (current?.cacheReadInputTokens ?? 0) +
1364
+ (result.usage.cacheReadInputTokens ?? 0),
1365
+ cacheCreationInputTokens: (current?.cacheCreationInputTokens ?? 0) +
1366
+ (result.usage.cacheCreationInputTokens ?? 0),
1367
+ }));
1368
+ if (result.costUsd !== undefined) {
1369
+ const sideCostUsd = result.costUsd;
1370
+ setCostUsd((current) => (current ?? 0) + sideCostUsd);
1371
+ }
1372
+ }
1373
+ catch (error) {
1374
+ updateBtwHistory((entries) => entries.map((item) => item.id === id
1375
+ ? {
1376
+ ...item,
1377
+ status: 'error',
1378
+ error: sideSignal.aborted
1379
+ ? 'Cancelled'
1380
+ : error instanceof Error
1381
+ ? error.message
1382
+ : String(error),
1383
+ }
1384
+ : item));
1385
+ }
1386
+ finally {
1387
+ if (btwControllerRef.current === controller) {
1388
+ btwControllerRef.current = null;
1389
+ }
1390
+ }
1391
+ })();
1392
+ onTurnChange?.(answering);
1393
+ void answering.finally(() => onTurnChange?.(null));
1394
+ };
1395
+ const forkSideQuestion = (entry) => {
1396
+ updateBtwHistory((entries) => entries.map((item) => item.id === entry.id ? { ...item, status: 'forking' } : item));
1397
+ const forking = (async () => {
1398
+ try {
1399
+ if (!sessionId) {
1400
+ throw new Error('Start a conversation before forking a side question.');
1401
+ }
1402
+ const commands = await service();
1403
+ if (!commands.forkSideQuestion) {
1404
+ throw new Error('Side-question forking is unavailable.');
1405
+ }
1406
+ const result = await commands.forkSideQuestion(sessionId, entry.question);
1407
+ updateMenu(null);
1408
+ append({
1409
+ kind: 'local-result',
1410
+ text: `⑂ forked ${result.name} (${result.agentId.slice(-4)})`,
1411
+ });
1412
+ }
1413
+ catch (error) {
1414
+ updateBtwHistory((entries) => entries.map((item) => item.id === entry.id
1415
+ ? {
1416
+ ...item,
1417
+ status: 'error',
1418
+ error: error instanceof Error ? error.message : String(error),
1419
+ }
1420
+ : item));
1421
+ }
1422
+ })();
1423
+ onTurnChange?.(forking);
1424
+ void forking.finally(() => onTurnChange?.(null));
1425
+ };
1426
+ const renameSession = (requestedName) => {
1427
+ const renaming = (async () => {
1428
+ if (!sessionId) {
1429
+ append({ kind: 'warning', text: 'No active conversation to rename.' });
1430
+ return;
1431
+ }
1432
+ setBusy(true);
1433
+ setStatus('renaming session');
1434
+ try {
1435
+ const commands = await service();
1436
+ if (!commands.rename) {
1437
+ throw new Error('This interactive service cannot rename sessions.');
1438
+ }
1439
+ const name = requestedName?.trim() ||
1440
+ (await commands.sessionNameSuggestion?.(sessionId, signal));
1441
+ if (!name)
1442
+ throw new Error('Could not generate a session name');
1443
+ await commands.rename(sessionId, name);
1444
+ append({ kind: 'local-result', text: `Session renamed to: ${name}` });
1445
+ }
1446
+ catch (error) {
1447
+ warn(error);
1448
+ }
1449
+ finally {
1450
+ setBusy(false);
1451
+ setStatus('ready');
1452
+ }
1453
+ })();
1454
+ onTurnChange?.(renaming);
1455
+ void renaming.finally(() => onTurnChange?.(null));
1456
+ };
1457
+ const branchSession = () => {
1458
+ const branching = (async () => {
1459
+ if (!sessionId) {
1460
+ append({ kind: 'warning', text: 'No active conversation to branch.' });
1461
+ return;
1462
+ }
1463
+ setBusy(true);
1464
+ setStatus('branching conversation');
1465
+ try {
1466
+ const commands = await service();
1467
+ const originalSessionId = sessionId;
1468
+ const source = (await commands.sessions()).find((candidate) => candidate.sessionId === originalSessionId);
1469
+ const fork = await commands.fork(originalSessionId);
1470
+ const branchName = source?.name ? `${source.name} (Branch)` : undefined;
1471
+ if (branchName && commands.rename) {
1472
+ await commands.rename(fork.sessionId, branchName);
1473
+ }
1474
+ setSessionId(fork.sessionId);
1475
+ setPendingFork(false);
1476
+ append({
1477
+ kind: 'local-result',
1478
+ text: `Branched conversation. You are now in the new branch (session ${fork.sessionId}). Use /resume ${originalSessionId}${source?.name ? ` ("${source.name}")` : ''} to return to the original, or run praxis -r ${originalSessionId} in a new terminal.`,
1479
+ });
1480
+ }
1481
+ catch (error) {
1482
+ warn(error);
1483
+ }
1484
+ finally {
1485
+ setBusy(false);
1486
+ setStatus('ready');
1487
+ }
1488
+ })();
1489
+ onTurnChange?.(branching);
1490
+ void branching.finally(() => onTurnChange?.(null));
1491
+ };
1492
+ const compactSession = () => {
1493
+ const compacting = (async () => {
1494
+ if (!sessionId) {
1495
+ append({ kind: 'warning', text: 'No active conversation to compact.' });
1496
+ return;
1497
+ }
1498
+ const controller = new AbortController();
1499
+ turnControllerRef.current?.abort();
1500
+ turnControllerRef.current = controller;
1501
+ const compactSignal = signal
1502
+ ? AbortSignal.any([signal, controller.signal])
1503
+ : controller.signal;
1504
+ setBusy(true);
1505
+ setStatus('compacting conversation');
1506
+ setCompactProgress(0);
1507
+ updateMenu({ kind: 'compact-progress' });
1508
+ try {
1509
+ const commands = await service();
1510
+ if (!commands.compact) {
1511
+ throw new Error('This interactive service cannot compact sessions.');
1512
+ }
1513
+ const result = await commands.compact(sessionId, compactSignal);
1514
+ const projected = await commands.transcript?.(sessionId);
1515
+ setHistory([
1516
+ ...(projected ?? [{ kind: 'compact', summary: result.summary }]),
1517
+ { kind: 'user', text: '/compact' },
1518
+ {
1519
+ kind: 'local-result',
1520
+ text: 'Compacted (ctrl+o to see full summary)',
1521
+ },
1522
+ ]);
1523
+ setUsage((current) => ({
1524
+ inputTokens: (current?.inputTokens ?? 0) + result.usage.inputTokens,
1525
+ outputTokens: (current?.outputTokens ?? 0) + result.usage.outputTokens,
1526
+ cacheReadInputTokens: (current?.cacheReadInputTokens ?? 0) +
1527
+ (result.usage.cacheReadInputTokens ?? 0),
1528
+ cacheCreationInputTokens: (current?.cacheCreationInputTokens ?? 0) +
1529
+ (result.usage.cacheCreationInputTokens ?? 0),
1530
+ }));
1531
+ }
1532
+ catch (error) {
1533
+ warn(error);
1534
+ }
1535
+ finally {
1536
+ if (turnControllerRef.current === controller) {
1537
+ turnControllerRef.current = null;
1538
+ }
1539
+ updateMenu(null);
1540
+ setBusy(false);
1541
+ setStatus('ready');
1542
+ }
1543
+ })();
1544
+ onTurnChange?.(compacting);
1545
+ void compacting.finally(() => onTurnChange?.(null));
1546
+ };
1547
+ const openRewind = () => {
1548
+ const loading = (async () => {
1549
+ if (!sessionId) {
1550
+ append({ kind: 'warning', text: 'No active conversation to rewind.' });
1551
+ return;
1552
+ }
1553
+ setBusy(true);
1554
+ setStatus('loading rewind points');
1555
+ try {
1556
+ const commands = await service();
1557
+ if (!commands.rewindPoints) {
1558
+ throw new Error('This interactive service cannot inspect rewind points.');
1559
+ }
1560
+ const points = await commands.rewindPoints(sessionId);
1561
+ if (points.length === 0) {
1562
+ append({
1563
+ kind: 'warning',
1564
+ text: 'No conversation checkpoints found.',
1565
+ });
1566
+ return;
1567
+ }
1568
+ updateMenu({
1569
+ kind: 'rewind',
1570
+ points,
1571
+ selectedIndex: points.length,
1572
+ });
1573
+ }
1574
+ catch (error) {
1575
+ warn(error);
1576
+ }
1577
+ finally {
1578
+ setBusy(false);
1579
+ setStatus('ready');
1580
+ }
1581
+ })();
1582
+ onTurnChange?.(loading);
1583
+ void loading.finally(() => onTurnChange?.(null));
1584
+ };
1585
+ const applyRewind = (point, action, summarizationContext) => {
1586
+ if (action === 'cancel') {
1587
+ updateMenu(null);
1588
+ return;
1589
+ }
1590
+ const restoring = (async () => {
1591
+ if (!sessionId)
1592
+ return;
1593
+ setBusy(true);
1594
+ setStatus(action.startsWith('summarize') ? 'summarizing' : 'rewinding');
1595
+ try {
1596
+ const commands = await service();
1597
+ if (action === 'summarize-from' || action === 'summarize-to') {
1598
+ if (!commands.compact) {
1599
+ throw new Error('This interactive service cannot summarize sessions.');
1600
+ }
1601
+ const direction = action === 'summarize-from' ? 'from' : 'to';
1602
+ const controller = new AbortController();
1603
+ turnControllerRef.current?.abort();
1604
+ turnControllerRef.current = controller;
1605
+ const compactSignal = signal
1606
+ ? AbortSignal.any([signal, controller.signal])
1607
+ : controller.signal;
1608
+ setCompactProgress(0);
1609
+ updateMenu({ kind: 'compact-progress' });
1610
+ const result = await commands
1611
+ .compact(sessionId, compactSignal, {
1612
+ messageId: point.messageId,
1613
+ direction,
1614
+ ...(summarizationContext?.trim()
1615
+ ? { context: summarizationContext.trim() }
1616
+ : {}),
1617
+ })
1618
+ .finally(() => {
1619
+ if (turnControllerRef.current === controller) {
1620
+ turnControllerRef.current = null;
1621
+ }
1622
+ });
1623
+ const projected = await commands.transcript?.(sessionId);
1624
+ setHistory(projected ?? [{ kind: 'compact', summary: result.summary }]);
1625
+ if (action === 'summarize-from')
1626
+ updateComposerInput(point.prompt);
1627
+ append({ kind: 'assistant', text: 'Summarized conversation' });
1628
+ append({
1629
+ kind: 'local-result',
1630
+ text: `Summarized ${result.messagesSummarized ?? 0} messages ${action === 'summarize-from' ? 'from' : 'up to'} this point\n (ctrl+o to expand history)`,
1631
+ });
1632
+ setUsage((current) => ({
1633
+ inputTokens: (current?.inputTokens ?? 0) + result.usage.inputTokens,
1634
+ outputTokens: (current?.outputTokens ?? 0) + result.usage.outputTokens,
1635
+ cacheReadInputTokens: (current?.cacheReadInputTokens ?? 0) +
1636
+ (result.usage.cacheReadInputTokens ?? 0),
1637
+ cacheCreationInputTokens: (current?.cacheCreationInputTokens ?? 0) +
1638
+ (result.usage.cacheCreationInputTokens ?? 0),
1639
+ }));
1640
+ return;
1641
+ }
1642
+ const restoreFiles = action === 'code' || action === 'code-and-conversation';
1643
+ const restoreConversation = action === 'conversation' || action === 'code-and-conversation';
1644
+ let restoredConversation;
1645
+ if (restoreConversation) {
1646
+ if (point.branchMessageId) {
1647
+ const originalSessionId = sessionId;
1648
+ const source = (await commands.sessions()).find((candidate) => candidate.sessionId === originalSessionId);
1649
+ const fork = await commands.fork(originalSessionId, undefined, point.branchMessageId);
1650
+ if (source?.name && commands.rename) {
1651
+ await commands.rename(fork.sessionId, `${source.name} (Branch)`);
1652
+ }
1653
+ restoredConversation = {
1654
+ sessionId: fork.sessionId,
1655
+ history: (await commands.transcript?.(fork.sessionId)) ?? [],
1656
+ };
1657
+ }
1658
+ else {
1659
+ restoredConversation = { sessionId: '', history: [] };
1660
+ }
1661
+ }
1662
+ if (restoreFiles) {
1663
+ if (!commands.rewindFiles) {
1664
+ throw new Error('File rewinding is not enabled.');
1665
+ }
1666
+ await commands.rewindFiles(sessionId, point.messageId);
1667
+ }
1668
+ if (restoredConversation) {
1669
+ setSessionId(restoredConversation.sessionId || null);
1670
+ setPendingFork(false);
1671
+ setHistory(restoredConversation.history);
1672
+ updateComposerInput(point.prompt);
1673
+ }
1674
+ append({
1675
+ kind: 'local-result',
1676
+ text: action === 'code'
1677
+ ? 'Code restored. The conversation was unchanged.'
1678
+ : restoreFiles
1679
+ ? 'Code and conversation restored. Edit the message and submit to continue.'
1680
+ : 'Conversation restored. Edit the message and submit to continue.',
1681
+ });
1682
+ }
1683
+ catch (error) {
1684
+ warn(error);
1685
+ }
1686
+ finally {
1687
+ updateMenu(null);
1688
+ setBusy(false);
1689
+ setStatus('ready');
1690
+ }
1691
+ })();
1692
+ onTurnChange?.(restoring);
1693
+ void restoring.finally(() => onTurnChange?.(null));
1694
+ };
1695
+ const submit = async (prompt, shellCommand, images = []) => {
1696
+ const turnNumber = turnNumberRef.current + 1;
1697
+ turnNumberRef.current = turnNumber;
1698
+ turnMutatedFilesRef.current = false;
1699
+ scheduledWaitRef.current?.abort();
1700
+ appendPromptHistory(prompt);
1701
+ const turnController = new AbortController();
1702
+ turnControllerRef.current?.abort();
1703
+ turnControllerRef.current = turnController;
1704
+ const turnSignal = signal
1705
+ ? AbortSignal.any([signal, turnController.signal])
1706
+ : turnController.signal;
1707
+ setBusy(true);
1708
+ setCommandPaletteOpen(false);
1709
+ setStatus('assembling-context');
1710
+ setActiveText('');
1711
+ setActiveThinking('');
1712
+ if (shellCommand === undefined)
1713
+ append({ kind: 'user', text: prompt });
1714
+ else
1715
+ turnMutatedFilesRef.current = true;
1716
+ let commands;
1717
+ try {
1718
+ commands = await service();
1719
+ let activeSessionId = sessionId;
1720
+ const startedNewSession = activeSessionId === null;
1721
+ if (activeSessionId && pendingFork) {
1722
+ const fork = await commands.fork(activeSessionId, resume?.forkSessionId);
1723
+ activeSessionId = fork.sessionId;
1724
+ setSessionId(activeSessionId);
1725
+ setPendingFork(false);
1726
+ }
1727
+ const result = shellCommand === undefined
1728
+ ? activeSessionId
1729
+ ? await commands.resume(activeSessionId, prompt, turnSignal, undefined, images)
1730
+ : await commands.run(prompt, turnSignal, undefined, undefined, images)
1731
+ : activeSessionId
1732
+ ? commands.resumeShell
1733
+ ? await commands.resumeShell(activeSessionId, shellCommand, turnSignal)
1734
+ : (() => {
1735
+ throw new Error('Interactive shell mode is unavailable');
1736
+ })()
1737
+ : commands.runShell
1738
+ ? await commands.runShell(shellCommand, turnSignal)
1739
+ : (() => {
1740
+ throw new Error('Interactive shell mode is unavailable');
1741
+ })();
1742
+ setSessionId(result.sessionId);
1743
+ if (startedNewSession &&
1744
+ runtimePreferencesRef.current.permissionMode !== 'default') {
1745
+ try {
1746
+ await commands.setPermissionMode?.(result.sessionId, runtimePreferencesRef.current.permissionMode);
1747
+ }
1748
+ catch (error) {
1749
+ warn(error);
1750
+ }
1751
+ }
1752
+ setUsage(result.usage);
1753
+ setCostUsd(result.costUsd);
1754
+ append({ kind: 'assistant', text: result.text });
1755
+ if (turnMutatedFilesRef.current) {
1756
+ try {
1757
+ const snapshot = await loadDiffSnapshot();
1758
+ setTurnDiffs((current) => [
1759
+ ...current.filter((source) => source.label !== `T${turnNumber}`),
1760
+ { label: `T${turnNumber}`, snapshot },
1761
+ ]);
1762
+ }
1763
+ catch {
1764
+ // Diff snapshots are a local presentation aid and must not fail a turn.
1765
+ }
1766
+ }
1767
+ setActiveText('');
1768
+ setActiveThinking('');
1769
+ setStatus('ready');
1770
+ }
1771
+ catch (error) {
1772
+ if (turnController.signal.aborted && !signal?.aborted) {
1773
+ if (shellCommand !== undefined)
1774
+ updateComposerInput(`!${shellCommand}`);
1775
+ append({ kind: 'notice', text: 'Interrupted by user.' });
1776
+ setStatus('cancelled');
1777
+ }
1778
+ else {
1779
+ append({
1780
+ kind: 'warning',
1781
+ text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
1782
+ });
1783
+ setStatus('failed');
1784
+ }
1785
+ }
1786
+ finally {
1787
+ if (!factory.scheduledPrompts && commands) {
1788
+ try {
1789
+ await commands.close?.();
1790
+ }
1791
+ catch (error) {
1792
+ append({
1793
+ kind: 'warning',
1794
+ text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
1795
+ });
1796
+ }
1797
+ finally {
1798
+ if (serviceRef.current === commands)
1799
+ serviceRef.current = null;
1800
+ }
1801
+ }
1802
+ if (turnControllerRef.current === turnController)
1803
+ turnControllerRef.current = null;
1804
+ setBusy(false);
1805
+ }
1806
+ };
1807
+ useEffect(() => {
1808
+ if (!initialPromptPending || selectingSession || busy)
1809
+ return;
1810
+ const prompt = initialPromptRef.current;
1811
+ if (!prompt) {
1812
+ setInitialPromptPending(false);
1813
+ return;
1814
+ }
1815
+ initialPromptRef.current = '';
1816
+ const turn = submit(prompt);
1817
+ onTurnChange?.(turn);
1818
+ void turn.then(() => {
1819
+ setInitialPromptPending(false);
1820
+ onTurnChange?.(null);
1821
+ }, () => {
1822
+ setInitialPromptPending(false);
1823
+ onTurnChange?.(null);
1824
+ });
1825
+ }, [busy, initialPromptPending, selectingSession]);
1826
+ useEffect(() => {
1827
+ if (!factory.scheduledPrompts ||
1828
+ initialPromptPending ||
1829
+ selectingSession ||
1830
+ busy ||
1831
+ permission) {
1832
+ return;
1833
+ }
1834
+ const controller = new AbortController();
1835
+ scheduledWaitRef.current?.abort();
1836
+ scheduledWaitRef.current = controller;
1837
+ const waitSignal = signal
435
1838
  ? AbortSignal.any([signal, controller.signal])
436
1839
  : controller.signal;
437
1840
  void service()
@@ -454,35 +1857,134 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
454
1857
  return () => controller.abort();
455
1858
  }, [busy, initialPromptPending, permission, selectingSession, sessionId]);
456
1859
  useInput((value, key) => {
457
- if (key.ctrl && value.toLowerCase() === 'c') {
458
- permissionRef.current?.resolve(false);
459
- elicitationRef.current?.resolve({ action: 'cancel' });
460
- questionRef.current?.resolve(null);
461
- planApprovalRef.current?.resolve(false);
462
- onCancel?.();
463
- exit();
1860
+ const lower = value.toLowerCase();
1861
+ const controlKey = (letter) => (key.ctrl && lower === letter) ||
1862
+ value === String.fromCharCode(letter.charCodeAt(0) - 96);
1863
+ const printable = [...value].every((character) => {
1864
+ const codePoint = character.codePointAt(0) ?? 0;
1865
+ return codePoint >= 32 && codePoint !== 127;
1866
+ });
1867
+ const editor = () => createComposerEditor(inputRef.current, inputCursorRef.current);
1868
+ const editComposer = () => {
1869
+ if (key.leftArrow) {
1870
+ updateComposerEditor(key.meta
1871
+ ? moveComposerCursorByWord(editor(), 'backward')
1872
+ : moveComposerCursorAcrossImages(editor(), -1));
1873
+ return true;
1874
+ }
1875
+ if (key.rightArrow) {
1876
+ updateComposerEditor(key.meta
1877
+ ? moveComposerCursorByWord(editor(), 'forward')
1878
+ : moveComposerCursorAcrossImages(editor(), 1));
1879
+ return true;
1880
+ }
1881
+ if (controlKey('a')) {
1882
+ updateComposerEditor(createComposerEditor(inputRef.current, 0));
1883
+ return true;
1884
+ }
1885
+ if (controlKey('e')) {
1886
+ updateComposerEditor(createComposerEditor(inputRef.current));
1887
+ return true;
1888
+ }
1889
+ if (controlKey('b')) {
1890
+ updateComposerEditor(moveComposerCursorAcrossImages(editor(), -1));
1891
+ return true;
1892
+ }
1893
+ if (controlKey('f')) {
1894
+ updateComposerEditor(moveComposerCursorAcrossImages(editor(), 1));
1895
+ return true;
1896
+ }
1897
+ if (controlKey('w')) {
1898
+ updateComposerEditor(deleteComposerWordBackward(editor()));
1899
+ return true;
1900
+ }
1901
+ if (controlKey('u')) {
1902
+ updateComposerEditor(deleteComposerToStart(editor()));
1903
+ return true;
1904
+ }
1905
+ if (controlKey('k')) {
1906
+ updateComposerEditor(deleteComposerToEnd(editor()));
1907
+ return true;
1908
+ }
1909
+ if (key.backspace) {
1910
+ updateComposerEditor(deleteComposerImageBackward(editor()));
1911
+ return true;
1912
+ }
1913
+ if (key.delete) {
1914
+ updateComposerEditor(deleteComposerImageForward(editor()));
1915
+ return true;
1916
+ }
1917
+ if (!key.ctrl && !key.meta && value && printable) {
1918
+ updateComposerEditor(insertComposerText(editor(), value));
1919
+ return true;
1920
+ }
1921
+ return false;
1922
+ };
1923
+ if (controlKey('c')) {
1924
+ armExitConfirmation();
1925
+ return;
1926
+ }
1927
+ if (controlKey('z')) {
1928
+ if (!processSuspendRequestedRef.current) {
1929
+ processSuspendRequestedRef.current = true;
1930
+ setProcessSuspendRequested(true);
1931
+ }
1932
+ return;
1933
+ }
1934
+ dismissExitConfirmation();
1935
+ const keybindingContexts = menuRef.current
1936
+ ? menuRef.current.kind === 'diff'
1937
+ ? ['DiffDialog']
1938
+ : menuRef.current.kind === 'help'
1939
+ ? ['Help', 'Tabs']
1940
+ : menuRef.current.kind === 'permission-dashboard'
1941
+ ? ['Settings', 'Tabs']
1942
+ : menuRef.current.kind === 'model'
1943
+ ? ['ModelPicker', 'Select']
1944
+ : ['Select']
1945
+ : commandPaletteVisible || filePickerVisible
1946
+ ? ['Autocomplete', 'Chat']
1947
+ : permission || planApproval
1948
+ ? ['Confirmation']
1949
+ : selectingSession
1950
+ ? ['Select']
1951
+ : ['Chat'];
1952
+ const inputChord = tuiKeyChord(value, key);
1953
+ const pendingSequence = keySequenceRef.current;
1954
+ let keybindingAction;
1955
+ if (inputChord &&
1956
+ pendingSequence &&
1957
+ Date.now() - pendingSequence.at <= 1_000) {
1958
+ keybindingAction = resolveTuiKeybinding(keybindings, keybindingContexts, `${pendingSequence.chord} ${inputChord}`);
1959
+ keySequenceRef.current = null;
1960
+ }
1961
+ else {
1962
+ keySequenceRef.current = null;
1963
+ }
1964
+ if (!keybindingAction) {
1965
+ keybindingAction = resolveTuiKeybinding(keybindings, keybindingContexts, inputChord);
1966
+ }
1967
+ if (!keybindingAction &&
1968
+ inputChord &&
1969
+ hasTuiKeybindingPrefix(keybindings, keybindingContexts, inputChord)) {
1970
+ keySequenceRef.current = { chord: inputChord, at: Date.now() };
464
1971
  return;
465
1972
  }
1973
+ const isKeybinding = (action) => keybindingAction === action;
466
1974
  if (permission) {
467
- if (value.toLowerCase() === 'y' || value === '1') {
1975
+ if (lower === 'y' || value === '1') {
468
1976
  permission.resolve(true);
469
1977
  }
470
- else if (value.toLowerCase() === 'n' ||
471
- value === '2' ||
472
- key.return ||
473
- key.escape) {
1978
+ else if (lower === 'n' || value === '2' || key.return || key.escape) {
474
1979
  permission.resolve(false);
475
1980
  }
476
1981
  return;
477
1982
  }
478
1983
  if (planApproval) {
479
- if (value.toLowerCase() === 'y' || value === '1') {
1984
+ if (lower === 'y' || value === '1') {
480
1985
  planApproval.resolve(true);
481
1986
  }
482
- else if (value.toLowerCase() === 'n' ||
483
- value === '2' ||
484
- key.return ||
485
- key.escape) {
1987
+ else if (lower === 'n' || value === '2' || key.return || key.escape) {
486
1988
  planApproval.resolve(false);
487
1989
  }
488
1990
  return;
@@ -498,8 +2000,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
498
2000
  throw new Error('Question state is invalid.');
499
2001
  const answer = questionAnswer(current, inputRef.current.trim());
500
2002
  const answers = { ...question.answers, [current.question]: answer };
501
- inputRef.current = '';
502
- setInput('');
2003
+ clearComposerInput();
503
2004
  if (question.index === question.questions.length - 1) {
504
2005
  question.resolve({ answers });
505
2006
  }
@@ -516,13 +2017,8 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
516
2017
  });
517
2018
  }
518
2019
  }
519
- else if (key.backspace || key.delete) {
520
- inputRef.current = inputRef.current.slice(0, -1);
521
- setInput(inputRef.current);
522
- }
523
- else if (!key.ctrl && !key.meta && value) {
524
- inputRef.current += value;
525
- setInput(inputRef.current);
2020
+ else {
2021
+ editComposer();
526
2022
  }
527
2023
  return;
528
2024
  }
@@ -532,8 +2028,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
532
2028
  }
533
2029
  else if (key.return) {
534
2030
  const answer = inputRef.current.trim();
535
- inputRef.current = '';
536
- setInput('');
2031
+ clearComposerInput();
537
2032
  if (!answer || answer.toLowerCase() === 'decline') {
538
2033
  elicitation.resolve({ action: 'decline' });
539
2034
  }
@@ -550,10 +2045,9 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
550
2045
  typeof content !== 'object' ||
551
2046
  Array.isArray(content))
552
2047
  throw new Error('elicitation content must be a JSON object');
553
- const elicitationContent = content;
554
2048
  elicitation.resolve({
555
2049
  action: 'accept',
556
- content: elicitationContent,
2050
+ content: content,
557
2051
  });
558
2052
  }
559
2053
  catch {
@@ -564,23 +2058,21 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
564
2058
  }
565
2059
  }
566
2060
  }
567
- else if (key.backspace || key.delete) {
568
- inputRef.current = inputRef.current.slice(0, -1);
569
- setInput(inputRef.current);
570
- }
571
- else if (!key.ctrl && !key.meta && value) {
572
- inputRef.current += value;
573
- setInput(inputRef.current);
2061
+ else {
2062
+ editComposer();
574
2063
  }
575
2064
  return;
576
2065
  }
577
2066
  if (selectingSession) {
578
- if (key.escape) {
579
- if (allowNewSession) {
2067
+ if (key.escape || value === '\u001B') {
2068
+ if (pickerIncludesNewSessionRef.current && allowNewSession) {
580
2069
  setSessionId(null);
581
2070
  setPendingFork(false);
582
2071
  setSelectingSession(false);
583
2072
  }
2073
+ else if (allowNewSession) {
2074
+ setSelectingSession(false);
2075
+ }
584
2076
  else {
585
2077
  onCancel?.();
586
2078
  exit();
@@ -591,100 +2083,1201 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
591
2083
  setSelectedIndex(selectedIndexRef.current);
592
2084
  }
593
2085
  else if (key.downArrow) {
594
- selectedIndexRef.current = Math.min(choices.length - 1, selectedIndexRef.current + 1);
2086
+ selectedIndexRef.current = Math.min(Math.max(0, filteredPickerChoices.length - 1), selectedIndexRef.current + 1);
595
2087
  setSelectedIndex(selectedIndexRef.current);
596
2088
  }
597
2089
  else if (key.return) {
598
- const selected = choices[selectedIndexRef.current];
599
- setSessionId(selected?.sessionId ?? null);
2090
+ const currentPickerChoices = pickerIncludesNewSessionRef.current
2091
+ ? choices
2092
+ : initialSessions;
2093
+ const selected = filterSessionChoices(currentPickerChoices, sessionSearchRef.current)[selectedIndexRef.current];
2094
+ if (selected === undefined)
2095
+ return;
2096
+ openSession(selected?.sessionId ?? null);
600
2097
  if (!selected)
601
2098
  setPendingFork(false);
602
2099
  setSelectingSession(false);
603
2100
  }
2101
+ else if (key.backspace || key.delete) {
2102
+ sessionSearchRef.current = sessionSearchRef.current.slice(0, -1);
2103
+ setSessionSearch(sessionSearchRef.current);
2104
+ selectedIndexRef.current = 0;
2105
+ setSelectedIndex(0);
2106
+ }
2107
+ else if (!key.ctrl && !key.meta && value && printable) {
2108
+ sessionSearchRef.current += value;
2109
+ setSessionSearch(sessionSearchRef.current);
2110
+ selectedIndexRef.current = 0;
2111
+ setSelectedIndex(0);
2112
+ }
604
2113
  return;
605
2114
  }
606
- if (busy) {
607
- if (key.escape || value === '\u001B')
2115
+ if (externalEditorRequest !== null || keybindingsEditing)
2116
+ return;
2117
+ if (clipboardPendingRef.current > 0) {
2118
+ if (isKeybinding('chat:imagePaste'))
2119
+ pasteClipboard();
2120
+ return;
2121
+ }
2122
+ if (!busy &&
2123
+ !permission &&
2124
+ !planApproval &&
2125
+ !question &&
2126
+ !elicitation &&
2127
+ !menuRef.current &&
2128
+ isKeybinding('chat:externalEditor')) {
2129
+ setCommandPaletteOpen(false);
2130
+ setFilePickerOpen(false);
2131
+ setShortcutsVisible(false);
2132
+ setExternalEditorRequest({ prompt: inputRef.current });
2133
+ return;
2134
+ }
2135
+ if (!busy && isKeybinding('app:toggleTodos')) {
2136
+ openTasks();
2137
+ return;
2138
+ }
2139
+ if (!busy && isKeybinding('chat:modelPicker')) {
2140
+ updateMenu({ kind: 'model', selectedIndex: 0 });
2141
+ return;
2142
+ }
2143
+ if (!busy && isKeybinding('chat:stash')) {
2144
+ if (inputRef.current.length > 0) {
2145
+ stashedInputRef.current = inputRef.current;
2146
+ clearComposerInput();
2147
+ }
2148
+ else if (stashedInputRef.current.length > 0) {
2149
+ const stashed = stashedInputRef.current;
2150
+ stashedInputRef.current = '';
2151
+ updateComposerInput(stashed);
2152
+ }
2153
+ return;
2154
+ }
2155
+ const activeMenu = menuRef.current;
2156
+ if (activeMenu) {
2157
+ if (activeMenu.kind === 'btw') {
2158
+ const selected = btwHistoryRef.current[activeMenu.selectedIndex];
2159
+ if (key.escape || value === '\u001B') {
2160
+ btwControllerRef.current?.abort();
2161
+ btwControllerRef.current = null;
2162
+ updateMenu(null);
2163
+ return;
2164
+ }
2165
+ if (key.leftArrow || key.rightArrow) {
2166
+ updateMenu({
2167
+ ...activeMenu,
2168
+ selectedIndex: Math.max(0, Math.min(btwHistoryRef.current.length - 1, activeMenu.selectedIndex + (key.leftArrow ? -1 : 1))),
2169
+ scrollOffset: 0,
2170
+ });
2171
+ setBtwCopied(false);
2172
+ return;
2173
+ }
2174
+ if (key.upArrow || key.downArrow) {
2175
+ const maxOffset = Math.max(0, (selected?.answer.split('\n').length ?? 0) - 16);
2176
+ updateMenu({
2177
+ ...activeMenu,
2178
+ scrollOffset: Math.max(0, Math.min(maxOffset, activeMenu.scrollOffset + (key.upArrow ? -1 : 1))),
2179
+ });
2180
+ return;
2181
+ }
2182
+ if (lower === 'x' && btwHistoryRef.current.length > 1 && selected) {
2183
+ updateBtwHistory(() => [selected]);
2184
+ updateMenu({ kind: 'btw', selectedIndex: 0, scrollOffset: 0 });
2185
+ setBtwCopied(false);
2186
+ return;
2187
+ }
2188
+ if (lower === 'c' && selected?.status === 'complete') {
2189
+ const copying = sideQuestionClipboardWriter(selected.answer).then(() => {
2190
+ if (!componentMountedRef.current)
2191
+ return;
2192
+ setBtwCopied(true);
2193
+ if (btwCopiedTimerRef.current) {
2194
+ clearTimeout(btwCopiedTimerRef.current);
2195
+ }
2196
+ btwCopiedTimerRef.current = setTimeout(() => {
2197
+ if (componentMountedRef.current)
2198
+ setBtwCopied(false);
2199
+ btwCopiedTimerRef.current = null;
2200
+ }, 1500);
2201
+ btwCopiedTimerRef.current.unref?.();
2202
+ }, (error) => updateBtwHistory((entries) => entries.map((item) => item.id === selected.id
2203
+ ? {
2204
+ ...item,
2205
+ status: 'error',
2206
+ error: error instanceof Error
2207
+ ? error.message
2208
+ : String(error),
2209
+ }
2210
+ : item)));
2211
+ onTurnChange?.(copying);
2212
+ void copying.finally(() => onTurnChange?.(null));
2213
+ return;
2214
+ }
2215
+ if (lower === 'f' && selected?.status === 'complete') {
2216
+ forkSideQuestion(selected);
2217
+ }
2218
+ return;
2219
+ }
2220
+ if (activeMenu.kind === 'help') {
2221
+ if (key.escape) {
2222
+ updateMenu(null);
2223
+ return;
2224
+ }
2225
+ if (key.leftArrow || key.rightArrow) {
2226
+ updateMenu({
2227
+ kind: 'help',
2228
+ tabIndex: Math.max(0, Math.min(2, activeMenu.tabIndex + (key.leftArrow ? -1 : 1))),
2229
+ selectedIndex: 0,
2230
+ });
2231
+ return;
2232
+ }
2233
+ const commands = activeMenu.tabIndex === 1
2234
+ ? builtinSlashCommands
2235
+ : activeMenu.tabIndex === 2
2236
+ ? customSlashCommands
2237
+ : [];
2238
+ if (key.upArrow) {
2239
+ updateMenu({
2240
+ ...activeMenu,
2241
+ selectedIndex: Math.max(0, activeMenu.selectedIndex - 1),
2242
+ });
2243
+ }
2244
+ else if (key.downArrow) {
2245
+ updateMenu({
2246
+ ...activeMenu,
2247
+ selectedIndex: Math.min(Math.max(0, commands.length - 1), activeMenu.selectedIndex + 1),
2248
+ });
2249
+ }
2250
+ return;
2251
+ }
2252
+ if (activeMenu.kind === 'diff') {
2253
+ const snapshot = activeMenu.snapshots[activeMenu.sourceIndex]?.snapshot;
2254
+ const selectedFile = snapshot?.files[activeMenu.selectedIndex];
2255
+ if (key.escape || value === '\u001B') {
2256
+ updateMenu(activeMenu.viewingFile
2257
+ ? { ...activeMenu, viewingFile: false, scrollOffset: 0 }
2258
+ : null);
2259
+ return;
2260
+ }
2261
+ if (key.leftArrow || key.rightArrow) {
2262
+ updateMenu({
2263
+ ...activeMenu,
2264
+ sourceIndex: Math.max(0, Math.min(activeMenu.snapshots.length - 1, activeMenu.sourceIndex + (key.leftArrow ? -1 : 1))),
2265
+ selectedIndex: 0,
2266
+ viewingFile: false,
2267
+ scrollOffset: 0,
2268
+ });
2269
+ return;
2270
+ }
2271
+ if (activeMenu.viewingFile) {
2272
+ const maxOffset = Math.max(0, visiblePatchLines(selectedFile?.patch ?? '').length - 18);
2273
+ if (key.upArrow || key.downArrow) {
2274
+ updateMenu({
2275
+ ...activeMenu,
2276
+ scrollOffset: Math.max(0, Math.min(maxOffset, activeMenu.scrollOffset + (key.upArrow ? -1 : 1))),
2277
+ });
2278
+ }
2279
+ return;
2280
+ }
2281
+ if (key.upArrow || key.downArrow) {
2282
+ updateMenu({
2283
+ ...activeMenu,
2284
+ selectedIndex: Math.max(0, Math.min(Math.max(0, (snapshot?.files.length ?? 0) - 1), activeMenu.selectedIndex + (key.upArrow ? -1 : 1))),
2285
+ });
2286
+ }
2287
+ else if (key.return && selectedFile) {
2288
+ updateMenu({ ...activeMenu, viewingFile: true, scrollOffset: 0 });
2289
+ }
2290
+ return;
2291
+ }
2292
+ if (activeMenu.kind === 'permission-rule-input') {
2293
+ if (key.escape || value === '\u001B') {
2294
+ clearComposerInput();
2295
+ updateMenu({
2296
+ kind: 'permission-dashboard',
2297
+ tabIndex: 1,
2298
+ selectedIndex: -1,
2299
+ query: '',
2300
+ rules: activeMenu.rules,
2301
+ });
2302
+ }
2303
+ else if (key.return) {
2304
+ const rule = inputRef.current.trim();
2305
+ if (!rule) {
2306
+ append({
2307
+ kind: 'warning',
2308
+ text: 'Enter a permission rule or press Esc.',
2309
+ });
2310
+ }
2311
+ else {
2312
+ clearComposerInput();
2313
+ updateMenu({
2314
+ kind: 'permission-scope',
2315
+ behavior: activeMenu.behavior,
2316
+ rule,
2317
+ rules: activeMenu.rules,
2318
+ selectedIndex: 0,
2319
+ });
2320
+ }
2321
+ }
2322
+ else {
2323
+ editComposer();
2324
+ }
2325
+ return;
2326
+ }
2327
+ if (activeMenu.kind === 'workspace-directory-input') {
2328
+ if (key.escape || value === '\u001B') {
2329
+ clearComposerInput();
2330
+ if (activeMenu.returnToPermissions === false) {
2331
+ updateMenu(null);
2332
+ append({
2333
+ kind: 'local-result',
2334
+ text: 'Did not add a working directory.',
2335
+ });
2336
+ }
2337
+ else {
2338
+ updateMenu({
2339
+ kind: 'permission-dashboard',
2340
+ tabIndex: 1,
2341
+ selectedIndex: -1,
2342
+ query: '',
2343
+ rules: activeMenu.rules,
2344
+ });
2345
+ }
2346
+ }
2347
+ else if (key.tab) {
2348
+ const completion = (async () => {
2349
+ try {
2350
+ updateComposerInput(await workspaceDirectoryCompleter(inputRef.current, runtimeCwdRef.current));
2351
+ }
2352
+ catch (error) {
2353
+ warn(error);
2354
+ }
2355
+ })();
2356
+ onTurnChange?.(completion);
2357
+ void completion.finally(() => onTurnChange?.(null));
2358
+ }
2359
+ else if (key.return) {
2360
+ const addition = (async () => {
2361
+ setBusy(true);
2362
+ try {
2363
+ const path = await workspaceDirectoryResolver(inputRef.current, runtimeCwdRef.current);
2364
+ clearComposerInput();
2365
+ if (path !== runtimeCwdRef.current) {
2366
+ updateRuntimePreferences((current) => ({
2367
+ ...current,
2368
+ additionalDirectories: [
2369
+ ...new Set([...current.additionalDirectories, path]),
2370
+ ],
2371
+ }));
2372
+ await retireService();
2373
+ }
2374
+ const rules = await permissionStore.load();
2375
+ if (activeMenu.returnToPermissions === false) {
2376
+ updateMenu(null);
2377
+ append({
2378
+ kind: 'local-result',
2379
+ text: `Added ${path} as a working directory.`,
2380
+ });
2381
+ }
2382
+ else {
2383
+ updateMenu({
2384
+ kind: 'permission-dashboard',
2385
+ tabIndex: 1,
2386
+ selectedIndex: -1,
2387
+ query: '',
2388
+ rules,
2389
+ });
2390
+ }
2391
+ }
2392
+ catch (error) {
2393
+ warn(error);
2394
+ }
2395
+ finally {
2396
+ setBusy(false);
2397
+ }
2398
+ })();
2399
+ onTurnChange?.(addition);
2400
+ void addition.finally(() => onTurnChange?.(null));
2401
+ }
2402
+ else {
2403
+ editComposer();
2404
+ }
2405
+ return;
2406
+ }
2407
+ if (activeMenu.kind === 'workspace-directory-delete') {
2408
+ if (key.escape || value === '\u001B') {
2409
+ updateMenu({
2410
+ kind: 'permission-dashboard',
2411
+ tabIndex: 1,
2412
+ selectedIndex: -1,
2413
+ query: '',
2414
+ rules: activeMenu.rules,
2415
+ });
2416
+ return;
2417
+ }
2418
+ if (key.upArrow || key.downArrow) {
2419
+ updateMenu({
2420
+ ...activeMenu,
2421
+ selectedIndex: key.upArrow ? 0 : 1,
2422
+ });
2423
+ return;
2424
+ }
2425
+ const confirmed = (key.return && activeMenu.selectedIndex === 0) ||
2426
+ value.toLowerCase() === 'y' ||
2427
+ value === '1';
2428
+ const declined = (key.return && activeMenu.selectedIndex === 1) ||
2429
+ value.toLowerCase() === 'n' ||
2430
+ value === '2';
2431
+ if (!confirmed && !declined)
2432
+ return;
2433
+ if (confirmed) {
2434
+ updateRuntimePreferences((current) => ({
2435
+ ...current,
2436
+ additionalDirectories: current.additionalDirectories.filter((path) => path !== activeMenu.path),
2437
+ }));
2438
+ void retireService();
2439
+ }
2440
+ updateMenu({
2441
+ kind: 'permission-dashboard',
2442
+ tabIndex: 1,
2443
+ selectedIndex: -1,
2444
+ query: '',
2445
+ rules: activeMenu.rules,
2446
+ });
2447
+ return;
2448
+ }
2449
+ if (activeMenu.kind === 'permission-delete') {
2450
+ if (key.escape || value === '\u001B') {
2451
+ updateMenu({
2452
+ kind: 'permission-dashboard',
2453
+ tabIndex: 1,
2454
+ selectedIndex: -1,
2455
+ query: '',
2456
+ rules: activeMenu.rules,
2457
+ });
2458
+ return;
2459
+ }
2460
+ if (key.upArrow || key.downArrow) {
2461
+ updateMenu({
2462
+ ...activeMenu,
2463
+ selectedIndex: key.upArrow ? 0 : 1,
2464
+ });
2465
+ return;
2466
+ }
2467
+ const confirmed = (key.return && activeMenu.selectedIndex === 0) ||
2468
+ value.toLowerCase() === 'y' ||
2469
+ value === '1';
2470
+ const declined = (key.return && activeMenu.selectedIndex === 1) ||
2471
+ value.toLowerCase() === 'n' ||
2472
+ value === '2';
2473
+ if (!confirmed && !declined)
2474
+ return;
2475
+ if (declined) {
2476
+ updateMenu({
2477
+ kind: 'permission-dashboard',
2478
+ tabIndex: 1,
2479
+ selectedIndex: -1,
2480
+ query: '',
2481
+ rules: activeMenu.rules,
2482
+ });
2483
+ return;
2484
+ }
2485
+ const removal = (async () => {
2486
+ setBusy(true);
2487
+ try {
2488
+ if (!permissionStore.remove)
2489
+ throw new Error('Permission rule removal is unavailable.');
2490
+ await permissionStore.remove(activeMenu.rule);
2491
+ await retireService();
2492
+ const rules = await permissionStore.load();
2493
+ updateMenu({
2494
+ kind: 'permission-dashboard',
2495
+ tabIndex: 1,
2496
+ selectedIndex: -1,
2497
+ query: '',
2498
+ rules,
2499
+ });
2500
+ }
2501
+ catch (error) {
2502
+ warn(error);
2503
+ }
2504
+ finally {
2505
+ setBusy(false);
2506
+ }
2507
+ })();
2508
+ onTurnChange?.(removal);
2509
+ void removal.finally(() => onTurnChange?.(null));
2510
+ return;
2511
+ }
2512
+ if (activeMenu.kind === 'permission-scope') {
2513
+ if (key.escape || value === '\u001B') {
2514
+ updateMenu({
2515
+ kind: 'permission-dashboard',
2516
+ tabIndex: 1,
2517
+ selectedIndex: -1,
2518
+ query: '',
2519
+ rules: activeMenu.rules,
2520
+ });
2521
+ return;
2522
+ }
2523
+ if (key.upArrow || key.downArrow) {
2524
+ updateMenu({
2525
+ ...activeMenu,
2526
+ selectedIndex: Math.max(0, Math.min(2, activeMenu.selectedIndex + (key.upArrow ? -1 : 1))),
2527
+ });
2528
+ return;
2529
+ }
2530
+ if (key.return) {
2531
+ const scopes = ['local', 'project', 'user'];
2532
+ const scope = scopes[activeMenu.selectedIndex];
2533
+ if (!scope)
2534
+ return;
2535
+ const addition = (async () => {
2536
+ setBusy(true);
2537
+ try {
2538
+ await permissionStore.add({
2539
+ behavior: activeMenu.behavior,
2540
+ rule: activeMenu.rule,
2541
+ scope,
2542
+ });
2543
+ await retireService();
2544
+ const rules = await permissionStore.load();
2545
+ updateMenu({
2546
+ kind: 'permission-dashboard',
2547
+ tabIndex: 1,
2548
+ selectedIndex: -1,
2549
+ query: '',
2550
+ rules,
2551
+ });
2552
+ }
2553
+ catch (error) {
2554
+ warn(error);
2555
+ }
2556
+ finally {
2557
+ setBusy(false);
2558
+ }
2559
+ })();
2560
+ onTurnChange?.(addition);
2561
+ void addition.finally(() => onTurnChange?.(null));
2562
+ }
2563
+ return;
2564
+ }
2565
+ if (activeMenu.kind === 'permission-dashboard') {
2566
+ if (key.escape || value === '\u001B') {
2567
+ updateMenu(null);
2568
+ return;
2569
+ }
2570
+ if (key.leftArrow || key.rightArrow) {
2571
+ updateMenu({
2572
+ ...activeMenu,
2573
+ tabIndex: Math.max(0, Math.min(4, activeMenu.tabIndex + (key.leftArrow ? -1 : 1))),
2574
+ selectedIndex: -1,
2575
+ query: '',
2576
+ });
2577
+ return;
2578
+ }
2579
+ const behavior = ['allow', 'ask', 'deny'][activeMenu.tabIndex - 1];
2580
+ const query = activeMenu.query.toLowerCase();
2581
+ const matchingRules = behavior
2582
+ ? activeMenu.rules.filter((rule) => rule.behavior === behavior &&
2583
+ (!query ||
2584
+ rule.rule.toLowerCase().includes(query) ||
2585
+ rule.scope.includes(query)))
2586
+ : [];
2587
+ const rowCount = activeMenu.tabIndex === 0
2588
+ ? recentDenied.length
2589
+ : activeMenu.tabIndex === 4
2590
+ ? runtimePreferences.additionalDirectories.length + 1
2591
+ : matchingRules.length + 1;
2592
+ if (key.upArrow || key.downArrow) {
2593
+ if (rowCount === 0)
2594
+ return;
2595
+ const nextIndex = activeMenu.selectedIndex < 0
2596
+ ? key.downArrow
2597
+ ? 0
2598
+ : -1
2599
+ : activeMenu.selectedIndex + (key.upArrow ? -1 : 1);
2600
+ updateMenu({
2601
+ ...activeMenu,
2602
+ selectedIndex: Math.max(-1, Math.min(Math.max(0, rowCount - 1), nextIndex)),
2603
+ });
2604
+ }
2605
+ else if (key.backspace || key.delete) {
2606
+ updateMenu({
2607
+ ...activeMenu,
2608
+ query: activeMenu.query.slice(0, -1),
2609
+ selectedIndex: -1,
2610
+ });
2611
+ }
2612
+ else if (behavior && !key.ctrl && !key.meta && value && printable) {
2613
+ updateMenu({
2614
+ ...activeMenu,
2615
+ query: activeMenu.query + value,
2616
+ selectedIndex: -1,
2617
+ });
2618
+ }
2619
+ else if (key.return && behavior && activeMenu.selectedIndex === 0) {
2620
+ clearComposerInput();
2621
+ updateMenu({
2622
+ kind: 'permission-rule-input',
2623
+ behavior,
2624
+ rules: activeMenu.rules,
2625
+ });
2626
+ }
2627
+ else if (key.return && behavior && activeMenu.selectedIndex > 0) {
2628
+ const rule = matchingRules[activeMenu.selectedIndex - 1];
2629
+ if (rule)
2630
+ updateMenu({
2631
+ kind: 'permission-delete',
2632
+ rule,
2633
+ rules: activeMenu.rules,
2634
+ selectedIndex: 0,
2635
+ });
2636
+ }
2637
+ else if (key.return &&
2638
+ activeMenu.tabIndex === 4 &&
2639
+ activeMenu.selectedIndex >= 0) {
2640
+ const path = runtimePreferences.additionalDirectories[activeMenu.selectedIndex];
2641
+ if (path) {
2642
+ updateMenu({
2643
+ kind: 'workspace-directory-delete',
2644
+ path,
2645
+ rules: activeMenu.rules,
2646
+ selectedIndex: 0,
2647
+ });
2648
+ }
2649
+ else if (activeMenu.selectedIndex ===
2650
+ runtimePreferences.additionalDirectories.length) {
2651
+ clearComposerInput();
2652
+ updateMenu({
2653
+ kind: 'workspace-directory-input',
2654
+ rules: activeMenu.rules,
2655
+ });
2656
+ }
2657
+ }
2658
+ return;
2659
+ }
2660
+ if (activeMenu.kind === 'status') {
2661
+ if (key.escape || value === '\u001B') {
2662
+ updateMenu(null);
2663
+ }
2664
+ else if (key.leftArrow || key.rightArrow || key.tab) {
2665
+ const direction = key.leftArrow || (key.tab && key.shift) ? -1 : 1;
2666
+ updateMenu({
2667
+ kind: 'status',
2668
+ tabIndex: Math.max(0, Math.min(4, activeMenu.tabIndex + direction)),
2669
+ });
2670
+ }
2671
+ return;
2672
+ }
2673
+ if (activeMenu.kind === 'list') {
2674
+ if (key.escape || value === '\u001B') {
2675
+ updateMenu(null);
2676
+ }
2677
+ else if (key.upArrow || key.downArrow) {
2678
+ updateMenu({
2679
+ ...activeMenu,
2680
+ selectedIndex: Math.max(0, Math.min(Math.max(0, activeMenu.rows.length - 1), activeMenu.selectedIndex + (key.upArrow ? -1 : 1))),
2681
+ });
2682
+ }
2683
+ return;
2684
+ }
2685
+ if (activeMenu.kind === 'rewind') {
2686
+ if (key.escape || value === '\u001B') {
2687
+ updateMenu(null);
2688
+ }
2689
+ else if (key.upArrow || key.downArrow) {
2690
+ updateMenu({
2691
+ ...activeMenu,
2692
+ selectedIndex: Math.max(0, Math.min(activeMenu.points.length, activeMenu.selectedIndex + (key.upArrow ? -1 : 1))),
2693
+ });
2694
+ }
2695
+ else if (key.return) {
2696
+ const point = activeMenu.points[activeMenu.selectedIndex];
2697
+ if (!point) {
2698
+ updateMenu(null);
2699
+ }
2700
+ else {
2701
+ updateMenu({
2702
+ kind: 'rewind-confirm',
2703
+ points: activeMenu.points,
2704
+ point,
2705
+ selectedIndex: 0,
2706
+ });
2707
+ }
2708
+ }
2709
+ return;
2710
+ }
2711
+ if (activeMenu.kind === 'rewind-confirm') {
2712
+ const actions = rewindActions(activeMenu.point);
2713
+ if (key.escape || value === '\u001B') {
2714
+ updateMenu({
2715
+ kind: 'rewind',
2716
+ points: activeMenu.points,
2717
+ selectedIndex: Math.max(0, activeMenu.points.findIndex((point) => point.messageId === activeMenu.point.messageId)),
2718
+ });
2719
+ }
2720
+ else if (key.upArrow || key.downArrow) {
2721
+ updateMenu({
2722
+ ...activeMenu,
2723
+ selectedIndex: Math.max(0, Math.min(actions.length - 1, activeMenu.selectedIndex + (key.upArrow ? -1 : 1))),
2724
+ });
2725
+ }
2726
+ else if (key.return) {
2727
+ const selected = actions[activeMenu.selectedIndex];
2728
+ if (selected?.action === 'summarize-from' ||
2729
+ selected?.action === 'summarize-to') {
2730
+ clearComposerInput();
2731
+ updateMenu({
2732
+ kind: 'rewind-context',
2733
+ points: activeMenu.points,
2734
+ point: activeMenu.point,
2735
+ direction: selected.action === 'summarize-from' ? 'from' : 'to',
2736
+ });
2737
+ }
2738
+ else if (selected) {
2739
+ applyRewind(activeMenu.point, selected.action);
2740
+ }
2741
+ }
2742
+ return;
2743
+ }
2744
+ if (activeMenu.kind === 'rewind-context') {
2745
+ if (key.escape || value === '\u001B') {
2746
+ clearComposerInput();
2747
+ updateMenu({
2748
+ kind: 'rewind-confirm',
2749
+ points: activeMenu.points,
2750
+ point: activeMenu.point,
2751
+ selectedIndex: activeMenu.direction === 'from'
2752
+ ? rewindActions(activeMenu.point).findIndex((option) => option.action === 'summarize-from')
2753
+ : rewindActions(activeMenu.point).findIndex((option) => option.action === 'summarize-to'),
2754
+ });
2755
+ }
2756
+ else if (key.return) {
2757
+ const context = inputRef.current;
2758
+ clearComposerInput();
2759
+ applyRewind(activeMenu.point, activeMenu.direction === 'from' ? 'summarize-from' : 'summarize-to', context);
2760
+ }
2761
+ else {
2762
+ editComposer();
2763
+ }
2764
+ return;
2765
+ }
2766
+ if (activeMenu.kind === 'model-input') {
2767
+ if (key.escape) {
2768
+ clearComposerInput();
2769
+ updateMenu(null);
2770
+ }
2771
+ else if (key.return) {
2772
+ const model = inputRef.current.trim();
2773
+ if (!model) {
2774
+ append({ kind: 'warning', text: 'Enter a model ID or press Esc.' });
2775
+ }
2776
+ else {
2777
+ clearComposerInput();
2778
+ updateMenu(null);
2779
+ changeModel(model);
2780
+ }
2781
+ }
2782
+ else {
2783
+ editComposer();
2784
+ }
2785
+ return;
2786
+ }
2787
+ if (activeMenu.kind === 'export-filename') {
2788
+ if (key.escape || value === '\u001B') {
2789
+ clearComposerInput();
2790
+ updateMenu({ kind: 'export', selectedIndex: 1 });
2791
+ }
2792
+ else if (key.return) {
2793
+ const filename = inputRef.current.trim();
2794
+ if (!filename) {
2795
+ append({ kind: 'warning', text: 'Enter a filename or press Esc.' });
2796
+ }
2797
+ else {
2798
+ saveConversation(filename);
2799
+ }
2800
+ }
2801
+ else {
2802
+ editComposer();
2803
+ }
2804
+ return;
2805
+ }
2806
+ if (activeMenu.kind === 'export') {
2807
+ if (key.escape || value === '\u001B') {
2808
+ updateMenu(null);
2809
+ }
2810
+ else if (key.upArrow || key.downArrow) {
2811
+ updateMenu({ ...activeMenu, selectedIndex: key.upArrow ? 0 : 1 });
2812
+ }
2813
+ else if (value === '1' || value === '2') {
2814
+ updateMenu({ ...activeMenu, selectedIndex: Number(value) - 1 });
2815
+ }
2816
+ else if (key.return) {
2817
+ exportConversation(activeMenu.selectedIndex === 0 ? 'clipboard' : 'file');
2818
+ }
2819
+ return;
2820
+ }
2821
+ if (activeMenu.kind === 'compact-progress') {
2822
+ if (key.escape || value === '\u001B' || isKeybinding('chat:cancel')) {
2823
+ turnControllerRef.current?.abort();
2824
+ }
2825
+ return;
2826
+ }
2827
+ if (key.escape) {
2828
+ updateMenu(null);
2829
+ return;
2830
+ }
2831
+ const optionCount = activeMenu.kind === 'model'
2832
+ ? modelOptions.length
2833
+ : EFFORT_OPTIONS.length;
2834
+ const select = (selectedIndex) => updateMenu({ ...activeMenu, selectedIndex });
2835
+ if (key.upArrow) {
2836
+ select(Math.max(0, activeMenu.selectedIndex - 1));
2837
+ return;
2838
+ }
2839
+ if (key.downArrow) {
2840
+ select(Math.min(optionCount - 1, activeMenu.selectedIndex + 1));
2841
+ return;
2842
+ }
2843
+ if (/^[1-9]$/u.test(value)) {
2844
+ select(Math.min(optionCount - 1, Number(value) - 1));
2845
+ return;
2846
+ }
2847
+ if (activeMenu.kind === 'model' && (key.leftArrow || key.rightArrow)) {
2848
+ const currentEffort = runtimePreferencesRef.current.effort;
2849
+ const currentIndex = EFFORT_OPTIONS.indexOf(currentEffort);
2850
+ const nextIndex = Math.max(0, Math.min(EFFORT_OPTIONS.length - 1, currentIndex + (key.leftArrow ? -1 : 1)));
2851
+ const nextEffort = EFFORT_OPTIONS[nextIndex];
2852
+ if (nextEffort && nextEffort !== currentEffort)
2853
+ changeEffort(nextEffort);
2854
+ return;
2855
+ }
2856
+ if (!key.return && lower !== 's')
2857
+ return;
2858
+ if (activeMenu.kind === 'model') {
2859
+ if (activeMenu.selectedIndex === 1) {
2860
+ updateMenu(null);
2861
+ changeModel(undefined);
2862
+ }
2863
+ else if (activeMenu.selectedIndex === 2) {
2864
+ clearComposerInput();
2865
+ updateMenu({ kind: 'model-input' });
2866
+ }
2867
+ else {
2868
+ updateMenu(null);
2869
+ }
2870
+ }
2871
+ else {
2872
+ const selectedEffort = EFFORT_OPTIONS[activeMenu.selectedIndex];
2873
+ if (selectedEffort)
2874
+ changeEffort(selectedEffort);
2875
+ updateMenu(null);
2876
+ }
2877
+ return;
2878
+ }
2879
+ if (!busy && value === '?' && inputRef.current.length === 0) {
2880
+ setShortcutsVisible((current) => !current);
2881
+ return;
2882
+ }
2883
+ if (isKeybinding('app:toggleTranscript') && hasDetailedTranscript) {
2884
+ setThinkingExpanded((current) => !current);
2885
+ return;
2886
+ }
2887
+ if (busy) {
2888
+ if (isKeybinding('chat:cancel'))
608
2889
  turnControllerRef.current?.abort();
609
2890
  return;
610
2891
  }
611
- if (key.return) {
612
- if (key.shift) {
613
- inputRef.current += '\n';
614
- setInput(inputRef.current);
2892
+ if (isKeybinding('chat:imagePaste')) {
2893
+ pasteClipboard();
2894
+ return;
2895
+ }
2896
+ if (isKeybinding('chat:undo')) {
2897
+ const previous = undoStackRef.current.at(-1);
2898
+ if (previous) {
2899
+ undoStackRef.current = undoStackRef.current.slice(0, -1);
2900
+ updateComposerEditor(previous, false);
2901
+ }
2902
+ return;
2903
+ }
2904
+ if (filePickerVisible) {
2905
+ if (key.escape || value === '\u001B') {
2906
+ setFilePickerOpen(false);
2907
+ return;
2908
+ }
2909
+ if (key.upArrow) {
2910
+ setFileSelection((current) => Math.max(0, current - 1));
2911
+ return;
2912
+ }
2913
+ if (key.downArrow) {
2914
+ setFileSelection((current) => Math.min(matchingMentionEntries.length - 1, current + 1));
2915
+ return;
2916
+ }
2917
+ const selected = matchingMentionEntries[selectedFileIndex];
2918
+ if (selected && (key.tab || key.return) && fileReference) {
2919
+ updateComposerEditor(applyMentionReference(inputRef.current, inputCursorRef.current, fileReference, selected));
2920
+ setFilePickerOpen(false);
2921
+ return;
2922
+ }
2923
+ if (key.tab)
2924
+ return;
2925
+ }
2926
+ if (isKeybinding('chat:cancel')) {
2927
+ const now = Date.now();
2928
+ if (now - lastEscapeAtRef.current <= 500)
2929
+ clearComposerInput();
2930
+ lastEscapeAtRef.current = now;
2931
+ return;
2932
+ }
2933
+ if (isKeybinding('chat:cycleMode')) {
2934
+ const currentIndex = permissionOptions.findIndex((option) => option.mode === runtimePreferences.permissionMode);
2935
+ const next = permissionOptions[(currentIndex + 1) % permissionOptions.length];
2936
+ if (next)
2937
+ changePermissionMode(next.mode);
2938
+ return;
2939
+ }
2940
+ if (commandPaletteVisible) {
2941
+ if (key.escape) {
2942
+ setCommandPaletteOpen(false);
2943
+ return;
2944
+ }
2945
+ if (key.upArrow) {
2946
+ if (matchingSlashCommands.length > 0) {
2947
+ setCommandSelection((current) => Math.max(0, current - 1));
2948
+ }
2949
+ return;
2950
+ }
2951
+ if (key.downArrow) {
2952
+ if (matchingSlashCommands.length > 0) {
2953
+ setCommandSelection((current) => Math.min(matchingSlashCommands.length - 1, current + 1));
2954
+ }
2955
+ return;
2956
+ }
2957
+ const selectedCommand = matchingSlashCommands[selectedSlashCommandIndex];
2958
+ const exactSelectedCommand = selectedCommand !== undefined &&
2959
+ inputRef.current.toLocaleLowerCase() ===
2960
+ `/${selectedCommand.name}`.toLocaleLowerCase();
2961
+ if (selectedCommand &&
2962
+ (key.tab || (key.return && !exactSelectedCommand))) {
2963
+ updateComposerInput(`/${selectedCommand.name} `);
2964
+ setCommandPaletteOpen(false);
2965
+ return;
2966
+ }
2967
+ }
2968
+ const implicitShiftNewline = key.return && key.shift && keybindingAction === undefined;
2969
+ if (isKeybinding('chat:submit') ||
2970
+ isKeybinding('chat:newline') ||
2971
+ implicitShiftNewline) {
2972
+ if (isKeybinding('chat:newline') || implicitShiftNewline) {
2973
+ updateComposerEditor(insertComposerText(editor(), '\n'));
2974
+ return;
2975
+ }
2976
+ if (inputRef.current.endsWith('\\')) {
2977
+ const withoutContinuation = createComposerEditor(inputRef.current.slice(0, -1));
2978
+ updateComposerEditor(insertComposerText(withoutContinuation, '\n'));
615
2979
  return;
616
2980
  }
617
2981
  const prompt = inputRef.current.trim();
618
- inputRef.current = '';
619
- setInput('');
620
- if (!prompt)
2982
+ const images = promptImages(prompt);
2983
+ clearComposerInput();
2984
+ undoStackRef.current = [];
2985
+ composerImagesRef.current.clear();
2986
+ if (!prompt || prompt === '!')
621
2987
  return;
2988
+ const copyCommand = /^\/copy(?:\s+(\d+))?$/u.exec(prompt);
2989
+ const renameCommand = /^\/rename(?:\s+(.+))?$/u.exec(prompt);
2990
+ const cdCommand = /^\/cd(?:\s+(.+))?$/u.exec(prompt);
2991
+ const btwCommand = /^\/btw(?:\s+([\s\S]+))?$/u.exec(prompt);
622
2992
  if (prompt === '/exit') {
623
2993
  exit();
624
2994
  }
625
2995
  else if (prompt === '/help' || prompt === '?') {
626
- append({
627
- kind: 'notice',
628
- text: '/new start a session · /sessions resume · /workflows list workflows · /exit quit',
629
- });
2996
+ updateMenu({ kind: 'help', tabIndex: 0, selectedIndex: 0 });
630
2997
  }
631
2998
  else if (prompt === '/new') {
632
2999
  setSessionId(null);
633
3000
  setPendingFork(false);
634
3001
  append({ kind: 'notice', text: 'Started a new session.' });
635
3002
  }
636
- else if (prompt === '/sessions') {
3003
+ else if (prompt === '/clear') {
3004
+ setSessionId(null);
3005
+ setPendingFork(false);
3006
+ setHistory([]);
3007
+ setUsage(undefined);
3008
+ setCostUsd(undefined);
3009
+ setActiveText('');
3010
+ setActiveThinking('');
3011
+ setThinkingExpanded(false);
3012
+ setStatus('ready');
3013
+ inputHistoryRef.current = [];
3014
+ inputHistoryIndexRef.current = null;
3015
+ inputHistoryDraftRef.current = '';
3016
+ }
3017
+ else if (prompt === '/model') {
3018
+ updateMenu({ kind: 'model', selectedIndex: 0 });
3019
+ }
3020
+ else if (prompt === '/effort') {
3021
+ updateMenu({
3022
+ kind: 'effort',
3023
+ selectedIndex: EFFORT_OPTIONS.indexOf(runtimePreferences.effort),
3024
+ });
3025
+ }
3026
+ else if (prompt === '/keybindings') {
3027
+ setKeybindingsEditing(true);
3028
+ }
3029
+ else if (prompt === '/add-dir') {
3030
+ openWorkspaceDirectoryInput();
3031
+ }
3032
+ else if (btwCommand) {
3033
+ const sideQuestion = btwCommand[1]?.trim();
3034
+ if (sideQuestion)
3035
+ askSideQuestion(sideQuestion);
3036
+ else
3037
+ showBtwUsage();
3038
+ }
3039
+ else if (cdCommand) {
3040
+ append({ kind: 'user', text: prompt });
3041
+ const requestedCwd = cdCommand[1]?.trim();
3042
+ if (!requestedCwd) {
3043
+ showCdUsage();
3044
+ }
3045
+ else {
3046
+ changeWorkingDirectory(requestedCwd);
3047
+ }
3048
+ }
3049
+ else if (prompt === '/branch') {
3050
+ append({ kind: 'user', text: prompt });
3051
+ branchSession();
3052
+ }
3053
+ else if (prompt === '/compact') {
3054
+ compactSession();
3055
+ }
3056
+ else if (prompt === '/rewind') {
3057
+ openRewind();
3058
+ }
3059
+ else if (prompt === '/export') {
3060
+ append({ kind: 'user', text: prompt });
3061
+ updateMenu({ kind: 'export', selectedIndex: 0 });
3062
+ }
3063
+ else if (prompt === '/permissions') {
3064
+ const loading = (async () => {
3065
+ setBusy(true);
3066
+ try {
3067
+ updateMenu({
3068
+ kind: 'permission-dashboard',
3069
+ tabIndex: 1,
3070
+ selectedIndex: -1,
3071
+ query: '',
3072
+ rules: await permissionStore.load(),
3073
+ });
3074
+ }
3075
+ catch (error) {
3076
+ warn(error);
3077
+ }
3078
+ finally {
3079
+ setBusy(false);
3080
+ }
3081
+ })();
3082
+ onTurnChange?.(loading);
3083
+ void loading.finally(() => onTurnChange?.(null));
3084
+ }
3085
+ else if (prompt === '/resume' || prompt === '/sessions') {
3086
+ pickerIncludesNewSessionRef.current = false;
3087
+ setPickerIncludesNewSession(false);
3088
+ sessionSearchRef.current = '';
3089
+ setSessionSearch('');
637
3090
  selectedIndexRef.current = 0;
638
3091
  setSelectedIndex(0);
639
3092
  setSelectingSession(true);
640
3093
  }
641
- else if (prompt === '/workflows') {
642
- const turn = (async () => {
3094
+ else if (prompt === '/diff') {
3095
+ const inspection = (async () => {
643
3096
  setBusy(true);
3097
+ setStatus('loading diff');
644
3098
  try {
645
- const workflows = (await service()).workflows?.() ?? [];
646
- append({
647
- kind: 'notice',
648
- text: workflows.length === 0
649
- ? 'No workflows.'
650
- : workflows
651
- .map((workflow) => `${String(workflow.task_id)} [${String(workflow.status)}] ${String(workflow.summary)}`)
652
- .join('\n'),
3099
+ const current = await loadDiffSnapshot();
3100
+ updateMenu({
3101
+ kind: 'diff',
3102
+ snapshots: [
3103
+ { label: 'Current', snapshot: current },
3104
+ ...turnDiffs,
3105
+ ],
3106
+ sourceIndex: 0,
3107
+ selectedIndex: 0,
3108
+ viewingFile: false,
3109
+ scrollOffset: 0,
653
3110
  });
654
3111
  }
655
3112
  catch (error) {
656
- append({
657
- kind: 'warning',
658
- text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
659
- });
3113
+ warn(error);
660
3114
  }
661
3115
  finally {
662
3116
  setBusy(false);
3117
+ setStatus('ready');
663
3118
  }
664
3119
  })();
3120
+ onTurnChange?.(inspection);
3121
+ void inspection.finally(() => onTurnChange?.(null));
3122
+ }
3123
+ else if (prompt === '/context') {
3124
+ const skills = allSlashCommands
3125
+ .filter((command) => command.source === 'skill')
3126
+ .map((command) => ({
3127
+ name: command.name,
3128
+ tokens: estimatedCommandTokens(command),
3129
+ }));
3130
+ const measuredTokens = (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0);
3131
+ const skillTokens = skills.reduce((total, skill) => total + skill.tokens, 0);
3132
+ setHistory((current) => [
3133
+ ...current,
3134
+ { kind: 'user', text: '/context' },
3135
+ {
3136
+ kind: 'context',
3137
+ usedTokens: Math.max(measuredTokens, skillTokens),
3138
+ contextWindowTokens: runtimeDisplay.contextWindowTokens ?? 200_000,
3139
+ skills,
3140
+ },
3141
+ ]);
3142
+ }
3143
+ else if (prompt === '/status') {
3144
+ updateMenu({ kind: 'status', tabIndex: 1 });
3145
+ }
3146
+ else if (prompt === '/config') {
3147
+ updateMenu({ kind: 'status', tabIndex: 2 });
3148
+ }
3149
+ else if (prompt === '/usage') {
3150
+ updateMenu({ kind: 'status', tabIndex: 3 });
3151
+ }
3152
+ else if (prompt === '/skills' || prompt === '/skill') {
3153
+ updateMenu({
3154
+ kind: 'list',
3155
+ title: 'Skills',
3156
+ rows: allSlashCommands
3157
+ .filter((command) => command.source === 'skill')
3158
+ .map((command) => ({
3159
+ label: command.name,
3160
+ description: command.description,
3161
+ })),
3162
+ emptyText: 'No skills found\nCreate skills in .claude/skills/ or ~/.claude/skills/',
3163
+ selectedIndex: 0,
3164
+ });
3165
+ }
3166
+ else if (prompt === '/tasks' || prompt === '/workflows') {
3167
+ openTasks();
3168
+ }
3169
+ else if (prompt === '/mcp') {
3170
+ openMcpServers();
3171
+ }
3172
+ else if (prompt === '/reload-plugins') {
3173
+ reloadExtensions('plugins');
3174
+ }
3175
+ else if (prompt === '/reload-skills') {
3176
+ reloadExtensions('skills');
3177
+ }
3178
+ else if (copyCommand) {
3179
+ append({ kind: 'user', text: prompt });
3180
+ const ordinal = Number(copyCommand[1] ?? 1);
3181
+ if (!Number.isSafeInteger(ordinal) || ordinal < 1) {
3182
+ append({ kind: 'warning', text: 'Usage: /copy [positive number]' });
3183
+ }
3184
+ else {
3185
+ copyResponse(ordinal);
3186
+ }
3187
+ }
3188
+ else if (renameCommand) {
3189
+ append({ kind: 'user', text: prompt });
3190
+ renameSession(renameCommand[1]);
3191
+ }
3192
+ else if (prompt === '/plan') {
3193
+ changePermissionMode('plan');
3194
+ }
3195
+ else if (prompt.startsWith('!')) {
3196
+ const command = prompt.slice(1).trim();
3197
+ const turn = submit(`! ${command}`, command);
665
3198
  onTurnChange?.(turn);
666
- void turn.finally(() => onTurnChange?.(null));
3199
+ void turn.then(() => onTurnChange?.(null), () => onTurnChange?.(null));
667
3200
  }
668
3201
  else {
669
- const turn = submit(prompt);
3202
+ const turn = submit(prompt, undefined, images);
670
3203
  onTurnChange?.(turn);
671
3204
  void turn.then(() => onTurnChange?.(null), () => onTurnChange?.(null));
672
3205
  }
3206
+ return;
673
3207
  }
674
- else if (key.backspace || key.delete) {
675
- inputRef.current = inputRef.current.slice(0, -1);
676
- setInput(inputRef.current);
3208
+ if (isKeybinding('history:previous')) {
3209
+ restorePromptHistory('previous');
3210
+ return;
677
3211
  }
678
- else if (!key.ctrl && !key.meta && value) {
679
- inputRef.current += value;
680
- setInput(inputRef.current);
3212
+ if (isKeybinding('history:next')) {
3213
+ restorePromptHistory('next');
3214
+ return;
681
3215
  }
3216
+ if (isKeybinding('chat:clearInput')) {
3217
+ clearComposerInput();
3218
+ return;
3219
+ }
3220
+ editComposer();
682
3221
  });
683
- return (_jsx(Box, { flexDirection: "column", children: selectingSession ? (_jsx(SessionPicker, { sessions: choices, selectedIndex: selectedIndex, screenReader: axScreenReader })) : (_jsxs(_Fragment, { children: [!axScreenReader && history.length === 0 && !sessionId ? (_jsx(WelcomePanel, { display: display, width: width })) : null, sessionId ? (_jsxs(Text, { dimColor: true, children: ["Session ", sessionId.slice(0, 8)] })) : null, _jsx(Transcript, { items: history, activeText: activeText, activeThinking: activeThinking, screenReader: axScreenReader }), permission ? (_jsxs(DialogFrame, { title: permission.kind === 'recovery'
3222
+ return (_jsx(Box, { flexDirection: "column", children: selectingSession ? (_jsx(SessionPicker, { sessions: filteredPickerChoices, selectedIndex: selectedIndex, screenReader: axScreenReader, query: sessionSearch })) : (_jsxs(_Fragment, { children: [!axScreenReader && history.length === 0 && !sessionId ? (_jsx(WelcomePanel, { display: runtimeDisplay, width: width })) : null, sessionId ? (_jsxs(Text, { dimColor: true, children: ["Session ", sessionId.slice(0, 8)] })) : null, _jsx(Transcript, { items: history, activeText: activeText, activeThinking: activeThinking, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded, screenReader: axScreenReader }), externalEditorRequest !== null || keybindingsEditing ? (_jsx(ExternalEditorWait, { screenReader: axScreenReader })) : permission ? (_jsxs(DialogFrame, { title: permission.kind === 'recovery'
684
3223
  ? `Retry interrupted ${permission.call.name}?`
685
3224
  : `Allow ${permission.call.name}?`, screenReader: axScreenReader, children: [_jsx(Text, { bold: true, children: describeTool(permission.call, sensitiveValues) }), _jsx(Text, { children: "\u276F 1. Yes" }), _jsx(Text, { children: " 2. No" }), _jsx(Text, { dimColor: true, children: "Enter/Esc declines \u00B7 y/n quick response" })] })) : planApproval ? (_jsxs(DialogFrame, { title: "Approve this plan and begin implementation?", screenReader: axScreenReader, children: [_jsx(Text, { dimColor: true, children: planApproval.request.planPath }), planApproval.request.plan ? (_jsx(Box, { marginY: 1, children: _jsx(Text, { children: planApproval.request.plan }) })) : null, _jsx(Text, { children: "\u276F 1. Yes, implement the plan" }), _jsx(Text, { children: " 2. No, keep planning" })] })) : question ? (_jsxs(DialogFrame, { title: `${question.questions[question.index]?.header}: ${question.questions[question.index]?.question}`, screenReader: axScreenReader, children: [question.questions[question.index]?.options.map((option, index) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [index === 0 ? '❯ ' : ' ', index + 1, ". ", option.label, " \u2014 ", option.description] }), option.preview ? (_jsx(Text, { dimColor: true, children: option.preview })) : null] }, `${index}-${option.label}`))), _jsxs(Text, { children: ["\u203A ", input] }), _jsx(Text, { dimColor: true, children: question.questions[question.index]?.multiSelect
686
3225
  ? 'Enter comma-separated option numbers or custom text · Esc cancels'
687
- : 'Enter one option number or custom text · Esc cancels' })] })) : elicitation ? (_jsxs(DialogFrame, { title: `MCP elicitation (${elicitation.request.serverName})`, screenReader: axScreenReader, children: [_jsx(Text, { children: elicitation.request.message }), elicitation.request.url ? (_jsx(Text, { children: elicitation.request.url })) : null, elicitation.request.requestedSchema ? (_jsx(Text, { dimColor: true, children: JSON.stringify(elicitation.request.requestedSchema) })) : null, _jsxs(Text, { children: ["\u203A ", input] }), _jsx(Text, { dimColor: true, children: "Enter JSON object to accept \u00B7 Esc to cancel" })] })) : (_jsx(Composer, { input: input, busy: busy, status: status, display: display, ...(usage === undefined ? {} : { usage }), width: width, screenReader: axScreenReader }))] })) }));
3226
+ : 'Enter one option number or custom text · Esc cancels' })] })) : elicitation ? (_jsxs(DialogFrame, { title: `MCP elicitation (${elicitation.request.serverName})`, screenReader: axScreenReader, children: [_jsx(Text, { children: elicitation.request.message }), elicitation.request.url ? (_jsx(Text, { children: elicitation.request.url })) : null, elicitation.request.requestedSchema ? (_jsx(Text, { dimColor: true, children: JSON.stringify(elicitation.request.requestedSchema) })) : null, _jsxs(Text, { children: ["\u203A ", input] }), _jsx(Text, { dimColor: true, children: "Enter JSON object to accept \u00B7 Esc to cancel" })] })) : menu?.kind === 'model-input' ? (_jsxs(DialogFrame, { title: "Enter model ID", screenReader: axScreenReader, children: [_jsx(Text, { dimColor: true, children: "Enter a model ID supported by the configured provider." }), _jsxs(Text, { children: ["\u203A ", input] }), _jsx(Text, { dimColor: true, children: "Enter confirms \u00B7 Esc cancels" })] })) : menu?.kind === 'export-filename' ? (_jsxs(DialogFrame, { title: "Enter filename:", screenReader: axScreenReader, children: [_jsxs(Text, { children: ["> ", input] }), _jsx(Text, { dimColor: true, children: "Enter to save \u00B7 Esc to go back" })] })) : menu?.kind === 'compact-progress' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "\u273B Compacting conversation\u2026" }), _jsxs(Text, { children: ['▰'.repeat(Math.floor(compactProgress / 2)), '▱'.repeat(50 - Math.floor(compactProgress / 2)), ' ', compactProgress, "%"] })] })) : menu?.kind === 'btw' ? (_jsx(BtwPanel, { entries: btwHistory, selectedIndex: menu.selectedIndex, scrollOffset: menu.scrollOffset, copied: btwCopied, width: width, screenReader: axScreenReader })) : menu?.kind === 'rewind' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: " Rewind" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [' ', "Restore the code and/or conversation to the point before\u2026"] }), _jsx(Text, { children: " " }), (() => {
3227
+ const window = rewindPointWindow(menu.points, menu.selectedIndex);
3228
+ return (_jsxs(_Fragment, { children: [window.start > 0 ? (_jsxs(Text, { dimColor: true, children: [" \u2191 ", window.start, " more above"] })) : null, menu.points
3229
+ .slice(window.start, window.end)
3230
+ .map((point, offset) => {
3231
+ const index = window.start + offset;
3232
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { inverse: menu.selectedIndex === index, children: [menu.selectedIndex === index ? ' ❯ ' : ' ', point.prompt.replace(/\s+/gu, ' ').slice(0, 72)] }), _jsxs(Text, { dimColor: true, children: [' ', point.fileChanges.length > 0
3233
+ ? point.fileChanges
3234
+ .map((path) => path.startsWith(`${runtimeCwd}/`)
3235
+ ? path.slice(runtimeCwd.length + 1)
3236
+ : path)
3237
+ .join(', ')
3238
+ : point.fileRestoreAvailable
3239
+ ? 'No code changes'
3240
+ : '⚠ No code restore'] })] }, point.messageId));
3241
+ }), window.end < menu.points.length ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2193 ", menu.points.length - window.end, " more below"] })) : null] }));
3242
+ })(), _jsxs(Text, { inverse: menu.selectedIndex === menu.points.length, children: [menu.selectedIndex === menu.points.length ? ' ❯ ' : ' ', "(current)"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Enter to continue \u00B7 Esc to cancel" })] })) : menu?.kind === 'rewind-confirm' ? (_jsxs(DialogFrame, { title: "Confirm restore point", screenReader: axScreenReader, children: [_jsxs(Text, { children: ["\u2502 ", menu.point.prompt] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "The conversation will be forked." }), _jsxs(Text, { children: ["The code will", ' ', menu.point.fileChanges.length > 0
3243
+ ? `restore ${menu.point.fileChanges.join(', ')}.`
3244
+ : 'be unchanged.'] }), _jsx(Text, { children: " " }), rewindActions(menu.point).map((option, index) => (_jsxs(Text, { inverse: menu.selectedIndex === index, children: [menu.selectedIndex === index ? '❯ ' : ' ', index + 1, ". ", option.label] }, option.action))), _jsx(Text, { children: " " }), _jsx(Text, { color: "yellow", children: "\u26A0 Rewinding does not affect files edited manually or via bash." })] })) : menu?.kind === 'rewind-context' ? (_jsxs(DialogFrame, { title: `Summarize ${menu.direction === 'from' ? 'from' : 'up to'} here`, screenReader: axScreenReader, children: [_jsx(Text, { children: menu.direction === 'from'
3245
+ ? 'Messages after this point will be summarized.'
3246
+ : 'Messages up to this point will be summarized.' }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["add context (optional): ", input] }), _jsx(Text, { dimColor: true, children: "Enter to summarize \u00B7 Esc to go back" })] })) : menu?.kind === 'permission-rule-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Add ", menu.behavior, " permission rule"] }), _jsx(Text, { children: "Permission rules are a tool name, optionally followed by a specifier in parentheses." }), _jsx(Text, { children: "e.g., WebFetch or Bash(ls *)" }), _jsx(Text, { children: " " }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Enter permission rule…' }) }), _jsx(Text, { dimColor: true, children: "Enter to submit \u00B7 Esc to cancel" })] })) : menu?.kind === 'workspace-directory-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Add directory to workspace" }), _jsx(Text, { children: "Praxis Code will be able to read files in this directory and make edits when auto-accept edits is on." }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter the path to the directory:" }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Directory path…' }) }), _jsx(Text, { dimColor: true, children: "Tab to complete \u00B7 Enter to add \u00B7 Esc to cancel" })] })) : menu?.kind === 'permission-delete' ? (_jsxs(DialogFrame, { title: permissionDeleteTitle(menu.rule.behavior), screenReader: axScreenReader, children: [_jsx(Text, { children: menu.rule.rule }), permissionRuleDescription(menu.rule.rule) ? (_jsx(Text, { dimColor: true, children: permissionRuleDescription(menu.rule.rule) })) : null, _jsx(Text, { dimColor: true, children: permissionScopeLabel(menu.rule.scope) }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Are you sure you want to delete this permission rule?" }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: menu.selectedIndex === 0, children: [menu.selectedIndex === 0 ? '❯ ' : ' ', "1. Yes"] }), _jsxs(Text, { inverse: menu.selectedIndex === 1, children: [menu.selectedIndex === 1 ? '❯ ' : ' ', "2. No"] }), _jsx(Text, { dimColor: true, children: "Esc to cancel" })] })) : menu?.kind === 'workspace-directory-delete' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Remove directory from workspace?" }), _jsxs(Text, { children: [" ", menu.path] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Praxis Code will no longer have access to files in this directory." }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: menu.selectedIndex === 0, children: [menu.selectedIndex === 0 ? '❯ ' : ' ', "1. Yes"] }), _jsxs(Text, { inverse: menu.selectedIndex === 1, children: [menu.selectedIndex === 1 ? '❯ ' : ' ', "2. No"] }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to cancel" })] })) : menu ? (menu.kind === 'help' ? (_jsx(HelpMenu, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, builtinCommands: builtinSlashCommands, customCommands: customSlashCommands, width: width, screenReader: axScreenReader })) : menu.kind === 'diff' ? (_jsx(DiffDashboard, { snapshots: menu.snapshots, sourceIndex: menu.sourceIndex, selectedIndex: menu.selectedIndex, viewingFile: menu.viewingFile, scrollOffset: menu.scrollOffset, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-dashboard' ? (_jsx(PermissionDashboard, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, query: menu.query, rules: menu.rules, recentDenied: recentDenied, workspaceDirectories: workspaceDirectories, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-scope' ? (_jsx(SelectionMenu, { title: "Where should this rule be saved?", description: `${menu.rule}${permissionRuleDescription(menu.rule)
3247
+ ? ` · ${permissionRuleDescription(menu.rule)}`
3248
+ : ''}`, options: [
3249
+ {
3250
+ label: 'Project settings (local)',
3251
+ description: 'Saved in .claude/settings.local.json',
3252
+ },
3253
+ {
3254
+ label: 'Project settings',
3255
+ description: 'Checked in at .claude/settings.json',
3256
+ },
3257
+ {
3258
+ label: 'User settings',
3259
+ description: 'Saved in at ~/.claude/settings.json',
3260
+ },
3261
+ ], selectedIndex: menu.selectedIndex, footer: "Enter to confirm \u00B7 Esc to cancel", width: width, screenReader: axScreenReader })) : menu.kind === 'status' ? (_jsx(StatusDashboard, { tabIndex: menu.tabIndex, version: runtimeDisplay.version, sessionId: sessionId, display: runtimeDisplay, ...(usage === undefined ? {} : { usage }), ...(costUsd === undefined ? {} : { costUsd }), turnCount: turnNumberRef.current, toolCount: history.filter((item) => item.kind === 'tool').length, commandCount: allSlashCommands.length, detailedTranscript: thinkingExpanded, width: width, screenReader: axScreenReader })) : menu.kind === 'list' ? (_jsx(ListDashboard, { title: menu.title, rows: menu.rows, emptyText: menu.emptyText, selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'model' ? (_jsx(SelectionMenu, { title: "Select model", description: `Effort: ${runtimePreferences.effort}`, options: modelOptions, selectedIndex: menu.selectedIndex, footer: "\u2191/\u2193 select \u00B7 \u2190/\u2192 effort \u00B7 Enter applies to this session \u00B7 Esc cancels", width: width, screenReader: axScreenReader })) : menu.kind === 'effort' ? (_jsx(SelectionMenu, { title: "Select effort", description: "Controls how much reasoning effort the provider should use.", options: EFFORT_OPTIONS.map((option) => ({
3262
+ label: option,
3263
+ description: option === 'low'
3264
+ ? 'Fastest and least deliberative.'
3265
+ : option === 'max'
3266
+ ? 'Highest available reasoning effort.'
3267
+ : 'Use this effort for the next session turns.',
3268
+ selected: option === runtimePreferences.effort,
3269
+ })), selectedIndex: menu.selectedIndex, footer: "\u2191/\u2193 select \u00B7 Enter applies to this session \u00B7 Esc cancels", width: width, screenReader: axScreenReader })) : menu.kind === 'export' ? (_jsx(SelectionMenu, { title: "Export conversation", description: "Select export method", options: [
3270
+ {
3271
+ label: 'Copy to clipboard',
3272
+ description: 'Copy the conversation to your system clipboard',
3273
+ },
3274
+ {
3275
+ label: 'Save to file',
3276
+ description: 'Save the conversation to a file in the current directory',
3277
+ },
3278
+ ], selectedIndex: menu.selectedIndex, footer: "Esc to cancel", width: width, screenReader: axScreenReader })) : null) : (_jsxs(_Fragment, { children: [commandPaletteVisible ? (_jsx(CommandPalette, { commands: matchingSlashCommands, selectedIndex: selectedSlashCommandIndex, width: width, screenReader: axScreenReader })) : null, filePickerVisible ? (_jsx(MentionPicker, { entries: matchingMentionEntries, selectedIndex: selectedFileIndex, width: width, screenReader: axScreenReader })) : null, exitConfirmation ? (_jsx(Text, { color: "yellow", children: "Press Ctrl-C again to exit" })) : null, _jsx(Composer, { input: shellMode ? input.slice(1) : input, cursor: shellMode ? Math.max(0, inputCursor - 1) : inputCursor, shellMode: shellMode, busy: busy, clipboardBusy: clipboardBusy, status: status, display: runtimeDisplay, ...(usage === undefined ? {} : { usage }), ...(costUsd === undefined ? {} : { costUsd }), width: width, screenReader: axScreenReader, hasThinking: hasDetailedTranscript, thinkingExpanded: thinkingExpanded, shortcutsVisible: shortcutsVisible, ...(editorFooterMessage === undefined
3279
+ ? {}
3280
+ : { footerMessage: editorFooterMessage }) })] }))] })) }));
688
3281
  }
689
3282
  export async function runInteractive(options) {
690
3283
  const controller = new AbortController();
@@ -697,11 +3290,15 @@ export async function runInteractive(options) {
697
3290
  signal,
698
3291
  });
699
3292
  let initialSessions;
3293
+ let initialSlashCommands;
3294
+ let initialAgents;
700
3295
  try {
701
3296
  const sessions = await listing.sessions();
702
3297
  initialSessions = options.sessionFilter
703
3298
  ? sessions.filter(options.sessionFilter)
704
3299
  : sessions;
3300
+ initialSlashCommands = listing.slashCommands?.() ?? [];
3301
+ initialAgents = listing.agentDefinitions?.() ?? [];
705
3302
  if (options.requireSession && initialSessions.length === 0) {
706
3303
  throw new Error(options.missingSessionMessage ??
707
3304
  'No conversation linked to a pull request in this project');
@@ -716,7 +3313,6 @@ export async function runInteractive(options) {
716
3313
  }
717
3314
  throw error;
718
3315
  }
719
- await listing.close?.();
720
3316
  const canonicalResumeSession = options.resume?.sessionId === undefined
721
3317
  ? undefined
722
3318
  : initialSessions.find((session) => session.sessionId.toLowerCase() ===
@@ -724,15 +3320,36 @@ export async function runInteractive(options) {
724
3320
  const resume = canonicalResumeSession
725
3321
  ? { ...options.resume, sessionId: canonicalResumeSession.sessionId }
726
3322
  : options.resume;
3323
+ let initialHistory = [];
3324
+ try {
3325
+ initialHistory =
3326
+ resume?.sessionId === undefined || listing.transcript === undefined
3327
+ ? []
3328
+ : await listing.transcript(resume.sessionId);
3329
+ }
3330
+ catch (error) {
3331
+ try {
3332
+ await listing.close?.();
3333
+ }
3334
+ catch {
3335
+ // Preserve the transcript-loading failure as the primary error.
3336
+ }
3337
+ throw error;
3338
+ }
3339
+ await listing.close?.();
727
3340
  let activeTurn = null;
728
3341
  let cleanup = null;
729
- const instance = render(_jsx(InteractiveApp, { factory: options.factory, initialSessions: initialSessions, ...(options.initialPrompt === undefined
3342
+ const instance = render(_jsx(InteractiveApp, { factory: options.factory, initialSessions: initialSessions, slashCommands: initialSlashCommands, agents: initialAgents, initialHistory: initialHistory, ...(options.initialPrompt === undefined
730
3343
  ? {}
731
3344
  : { initialPrompt: options.initialPrompt }), signal: signal, onCancel: () => controller.abort(), onTurnChange: (turn) => {
732
3345
  activeTurn = turn;
733
3346
  }, onCleanup: (closing) => {
734
3347
  cleanup = closing;
735
- }, allowNewSession: !options.requireSession, ...(resume === undefined ? {} : { resume }), ...(options.display === undefined ? {} : { display: options.display }), ...(options.axScreenReader ? { axScreenReader: true } : {}) }), {
3348
+ }, allowNewSession: !options.requireSession, ...(options.additionalDirectories === undefined
3349
+ ? {}
3350
+ : { additionalDirectories: options.additionalDirectories }), ...(resume === undefined ? {} : { resume }), ...(options.display === undefined ? {} : { display: options.display }), ...(options.axScreenReader ? { axScreenReader: true } : {}), ...(options.allowDangerouslySkipPermissions
3351
+ ? { allowDangerouslySkipPermissions: true }
3352
+ : {}) }), {
736
3353
  exitOnCtrlC: false,
737
3354
  incrementalRendering: !options.axScreenReader,
738
3355
  interactive: true,