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