praxis-agent 0.2.0 → 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 +18 -4
  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 +68 -8
  7. package/dist/cli/interactive.d.ts.map +1 -1
  8. package/dist/cli/interactive.js +2617 -206
  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 +142 -3
  14. package/dist/cli/tui/claude-style.d.ts.map +1 -1
  15. package/dist/cli/tui/claude-style.js +377 -16
  16. package/dist/cli/tui/claude-style.js.map +1 -1
  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 +23 -2
  66. package/dist/cli.d.ts.map +1 -1
  67. package/dist/cli.js +45 -5
  68. package/dist/cli.js.map +1 -1
  69. package/dist/compatibility/claude/compaction.d.ts +8 -0
  70. package/dist/compatibility/claude/compaction.d.ts.map +1 -1
  71. package/dist/compatibility/claude/compaction.js +45 -8
  72. package/dist/compatibility/claude/compaction.js.map +1 -1
  73. package/dist/compatibility/claude/fork.d.ts.map +1 -1
  74. package/dist/compatibility/claude/fork.js +24 -6
  75. package/dist/compatibility/claude/fork.js.map +1 -1
  76. package/dist/compatibility/claude/history.d.ts.map +1 -1
  77. package/dist/compatibility/claude/history.js +41 -1
  78. package/dist/compatibility/claude/history.js.map +1 -1
  79. package/dist/compatibility/claude/projection.d.ts +29 -1
  80. package/dist/compatibility/claude/projection.d.ts.map +1 -1
  81. package/dist/compatibility/claude/projection.js +132 -2
  82. package/dist/compatibility/claude/projection.js.map +1 -1
  83. package/dist/compatibility/claude/schema.d.ts.map +1 -1
  84. package/dist/compatibility/claude/schema.js +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 +1 -1
@@ -1,8 +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';
5
- import { Composer, DialogFrame, SessionPicker, Transcript, WelcomePanel, useTerminalWidth, } from './tui/claude-style.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
+ }
6
99
  function questionAnswer(question, input) {
7
100
  const answer = input.trim();
8
101
  if (!answer)
@@ -30,19 +123,91 @@ function questionAnswer(question, input) {
30
123
  })
31
124
  .join(', ');
32
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
+ }
33
140
  function describeTool(call, sensitiveValues) {
34
141
  const name = redactSensitiveText(call.name, sensitiveValues);
35
142
  return `${name} ${describeToolInput(call, sensitiveValues)}`;
36
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
+ }
37
151
  function describeToolInput(call, sensitiveValues) {
38
152
  const detail = redactSensitiveText(JSON.stringify(call.input), sensitiveValues);
39
153
  return detail.length > 160 ? `${detail.slice(0, 157)}...` : detail;
40
154
  }
41
- export function InteractiveApp({ factory, initialSessions, initialPrompt, signal, onCancel, onTurnChange, onCleanup, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, }) {
42
- const { exit } = useApp();
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
+ }));
172
+ }
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();
43
190
  const width = useTerminalWidth(terminalWidth);
191
+ const keybindingsRoot = useMemo(() => resolve(keybindingsConfigRoot ??
192
+ process.env.CLAUDE_CONFIG_DIR ??
193
+ resolve(homedir(), '.claude')), [keybindingsConfigRoot]);
44
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]);
45
204
  const choices = useMemo(() => allowNewSession ? [null, ...initialSessions] : initialSessions, [allowNewSession, initialSessions]);
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]);
46
211
  const [selectingSession, setSelectingSession] = useState(initialSessions.length > 0 &&
47
212
  resume?.sessionId === undefined &&
48
213
  (!allowNewSession || resume?.requireSession === true));
@@ -52,14 +217,63 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
52
217
  const [pendingFork, setPendingFork] = useState(resume?.forkSession === true);
53
218
  const [input, setInput] = useState('');
54
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);
55
251
  const [busy, setBusy] = useState(false);
252
+ const [compactProgress, setCompactProgress] = useState(0);
56
253
  const initialPromptRef = useRef(initialPrompt?.trim() ?? '');
57
254
  const [initialPromptPending, setInitialPromptPending] = useState(initialPromptRef.current.length > 0);
58
255
  const [status, setStatus] = useState('ready');
59
256
  const [activeText, setActiveText] = useState('');
60
257
  const [activeThinking, setActiveThinking] = useState('');
258
+ const [thinkingExpanded, setThinkingExpanded] = useState(false);
61
259
  const [usage, setUsage] = useState();
62
- const [history, setHistory] = 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);
63
277
  const [permission, setPermission] = useState(null);
64
278
  const permissionRef = useRef(null);
65
279
  const [elicitation, setElicitation] = useState(null);
@@ -74,6 +288,121 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
74
288
  onCleanupRef.current = onCleanup;
75
289
  const scheduledWaitRef = useRef(null);
76
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]);
77
406
  useEffect(() => {
78
407
  if (!signal)
79
408
  return;
@@ -91,10 +420,13 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
91
420
  return () => signal.removeEventListener('abort', cancel);
92
421
  }, [exit, signal]);
93
422
  useEffect(() => () => {
423
+ componentMountedRef.current = false;
94
424
  permissionRef.current?.resolve(false);
95
425
  elicitationRef.current?.resolve({ action: 'cancel' });
96
426
  questionRef.current?.resolve(null);
97
427
  planApprovalRef.current?.resolve(false);
428
+ if (exitConfirmationTimerRef.current)
429
+ clearTimeout(exitConfirmationTimerRef.current);
98
430
  scheduledWaitRef.current?.abort();
99
431
  turnControllerRef.current?.abort();
100
432
  const closing = serviceRef.current?.close?.() ?? Promise.resolve();
@@ -104,16 +436,275 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
104
436
  void closing.catch(() => undefined);
105
437
  }, []);
106
438
  const append = (line) => setHistory((current) => [...current, line]);
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);
473
+ }
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,
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;
651
+ }
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;
659
+ }
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]);
107
696
  const handleEvent = (event) => {
108
697
  switch (event.type) {
109
698
  case 'text-delta':
110
699
  setActiveText((current) => current + event.delta);
111
700
  break;
112
701
  case 'thinking-start':
113
- setActiveThinking(event.block.type === 'thinking' ? event.block.thinking : '');
702
+ setActiveThinking(event.block.type === 'thinking'
703
+ ? redactSensitiveText(event.block.thinking, sensitiveValues)
704
+ : '');
114
705
  break;
115
706
  case 'thinking-delta':
116
- setActiveThinking((current) => current + event.delta);
707
+ setActiveThinking((current) => current + redactSensitiveText(event.delta, sensitiveValues));
117
708
  break;
118
709
  case 'thinking-signature-delta':
119
710
  // Signatures authenticate a thinking block for provider replay; they are
@@ -123,7 +714,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
123
714
  append({
124
715
  kind: 'thinking',
125
716
  text: event.block.type === 'thinking'
126
- ? event.block.thinking
717
+ ? redactSensitiveText(event.block.thinking, sensitiveValues)
127
718
  : activeThinking,
128
719
  });
129
720
  setActiveThinking('');
@@ -138,13 +729,20 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
138
729
  setUsage(event.usage);
139
730
  break;
140
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));
141
735
  append({
142
736
  kind: 'tool',
143
- call: event.call,
737
+ call: redactToolCall(event.call, sensitiveValues),
144
738
  detail: describeToolInput(event.call, sensitiveValues),
145
739
  });
146
740
  break;
147
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
+ }
148
746
  append({
149
747
  kind: event.behavior === 'deny' ? 'warning' : 'notice',
150
748
  text: event.behavior === 'ask'
@@ -160,6 +758,26 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
160
758
  isError: event.isError,
161
759
  });
162
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;
163
781
  case 'tool-progress':
164
782
  setStatus(`${event.toolName} · ${event.elapsedTimeSeconds}s`);
165
783
  break;
@@ -281,8 +899,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
281
899
  return;
282
900
  }
283
901
  signal?.addEventListener('abort', abort, { once: true });
284
- inputRef.current = '';
285
- setInput('');
902
+ clearComposerInput();
286
903
  questionRef.current = pending;
287
904
  setQuestion(pending);
288
905
  });
@@ -310,129 +927,780 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
310
927
  planApprovalRef.current = pending;
311
928
  setPlanApproval(pending);
312
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
+ };
313
957
  const service = async () => {
314
958
  if (serviceRef.current)
315
959
  return serviceRef.current;
316
- const pending = serviceCreationRef.current ??
317
- factory.createService({
318
- eventSink: handleEvent,
319
- requireProvider: true,
320
- approveRecovery,
321
- approveTool,
322
- onElicitation: requestElicitation,
323
- askUser,
324
- approvePlan,
325
- ...(signal ? { signal } : {}),
326
- });
327
- serviceCreationRef.current = pending;
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
+ }
328
988
  try {
329
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
+ }
330
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);
331
1009
  return created;
332
1010
  }
333
1011
  finally {
334
- serviceCreationRef.current = undefined;
1012
+ if (serviceCreationRef.current === pending) {
1013
+ serviceCreationRef.current = undefined;
1014
+ serviceCreationEpochRef.current = null;
1015
+ }
335
1016
  }
336
1017
  };
337
- const submit = async (prompt) => {
338
- scheduledWaitRef.current?.abort();
339
- const turnController = new AbortController();
340
- turnControllerRef.current?.abort();
341
- turnControllerRef.current = turnController;
342
- const turnSignal = signal
343
- ? AbortSignal.any([signal, turnController.signal])
344
- : turnController.signal;
345
- setBusy(true);
346
- setStatus('assembling-context');
347
- setActiveText('');
348
- setActiveThinking('');
349
- append({ kind: 'user', text: prompt });
350
- let commands;
351
- try {
352
- commands = await service();
353
- let activeSessionId = sessionId;
354
- if (activeSessionId && pendingFork) {
355
- const fork = await commands.fork(activeSessionId, resume?.forkSessionId);
356
- activeSessionId = fork.sessionId;
357
- setSessionId(activeSessionId);
358
- setPendingFork(false);
359
- }
360
- const result = activeSessionId
361
- ? await commands.resume(activeSessionId, prompt, turnSignal)
362
- : await commands.run(prompt, turnSignal);
363
- setSessionId(result.sessionId);
364
- setUsage(result.usage);
365
- append({ kind: 'assistant', text: result.text });
366
- setActiveText('');
367
- setActiveThinking('');
368
- setStatus('ready');
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;
369
1025
  }
370
- catch (error) {
371
- if (turnController.signal.aborted && !signal?.aborted) {
372
- append({ kind: 'notice', text: 'Interrupted by user.' });
373
- setStatus('cancelled');
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
+ }
374
1033
  }
375
- else {
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();
376
1083
  append({
377
- kind: 'warning',
378
- text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
1084
+ kind: 'notice',
1085
+ text: `Permission mode set to ${mode} for this session.`,
379
1086
  });
380
- setStatus('failed');
381
1087
  }
382
- }
383
- finally {
384
- if (!factory.scheduledPrompts && commands) {
385
- try {
386
- await commands.close?.();
387
- }
388
- catch (error) {
389
- append({
390
- kind: 'warning',
391
- text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
392
- });
393
- }
394
- finally {
395
- if (serviceRef.current === commands)
396
- serviceRef.current = null;
397
- }
1088
+ catch (error) {
1089
+ warn(error);
398
1090
  }
399
- if (turnControllerRef.current === turnController)
400
- turnControllerRef.current = null;
401
- setBusy(false);
402
- }
1091
+ finally {
1092
+ setBusy(false);
1093
+ setStatus('ready');
1094
+ }
1095
+ })();
1096
+ onTurnChange?.(change);
1097
+ void change.finally(() => onTurnChange?.(null));
403
1098
  };
404
- useEffect(() => {
405
- if (!initialPromptPending || selectingSession || busy)
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
+ });
406
1153
  return;
407
- const prompt = initialPromptRef.current;
408
- if (!prompt) {
409
- setInitialPromptPending(false);
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' });
410
1166
  return;
411
1167
  }
412
- initialPromptRef.current = '';
413
- const turn = submit(prompt);
414
- onTurnChange?.(turn);
415
- void turn.then(() => {
416
- setInitialPromptPending(false);
417
- onTurnChange?.(null);
418
- }, () => {
419
- setInitialPromptPending(false);
420
- onTurnChange?.(null);
421
- });
422
- }, [busy, initialPromptPending, selectingSession]);
423
- useEffect(() => {
424
- if (!factory.scheduledPrompts ||
425
- initialPromptPending ||
426
- selectingSession ||
427
- busy ||
428
- permission) {
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);
429
1186
  return;
430
1187
  }
431
- const controller = new AbortController();
432
- scheduledWaitRef.current?.abort();
433
- scheduledWaitRef.current = controller;
434
- const waitSignal = signal
435
- ? AbortSignal.any([signal, controller.signal])
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;
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));
1559
+ };
1560
+ const submit = async (prompt, shellCommand, images = []) => {
1561
+ const turnNumber = turnNumberRef.current + 1;
1562
+ turnNumberRef.current = turnNumber;
1563
+ turnMutatedFilesRef.current = false;
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;
1572
+ setBusy(true);
1573
+ setCommandPaletteOpen(false);
1574
+ setStatus('assembling-context');
1575
+ setActiveText('');
1576
+ setActiveThinking('');
1577
+ if (shellCommand === undefined)
1578
+ append({ kind: 'user', text: prompt });
1579
+ else
1580
+ turnMutatedFilesRef.current = true;
1581
+ let commands;
1582
+ try {
1583
+ commands = await service();
1584
+ let activeSessionId = sessionId;
1585
+ const startedNewSession = activeSessionId === null;
1586
+ if (activeSessionId && pendingFork) {
1587
+ const fork = await commands.fork(activeSessionId, resume?.forkSessionId);
1588
+ activeSessionId = fork.sessionId;
1589
+ setSessionId(activeSessionId);
1590
+ setPendingFork(false);
1591
+ }
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
+ })();
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);
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
+ }
1632
+ setActiveText('');
1633
+ setActiveThinking('');
1634
+ setStatus('ready');
1635
+ }
1636
+ catch (error) {
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 {
1652
+ if (!factory.scheduledPrompts && commands) {
1653
+ try {
1654
+ await commands.close?.();
1655
+ }
1656
+ catch (error) {
1657
+ append({
1658
+ kind: 'warning',
1659
+ text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
1660
+ });
1661
+ }
1662
+ finally {
1663
+ if (serviceRef.current === commands)
1664
+ serviceRef.current = null;
1665
+ }
1666
+ }
1667
+ if (turnControllerRef.current === turnController)
1668
+ turnControllerRef.current = null;
1669
+ setBusy(false);
1670
+ }
1671
+ };
1672
+ useEffect(() => {
1673
+ if (!initialPromptPending || selectingSession || busy)
1674
+ return;
1675
+ const prompt = initialPromptRef.current;
1676
+ if (!prompt) {
1677
+ setInitialPromptPending(false);
1678
+ return;
1679
+ }
1680
+ initialPromptRef.current = '';
1681
+ const turn = submit(prompt);
1682
+ onTurnChange?.(turn);
1683
+ void turn.then(() => {
1684
+ setInitialPromptPending(false);
1685
+ onTurnChange?.(null);
1686
+ }, () => {
1687
+ setInitialPromptPending(false);
1688
+ onTurnChange?.(null);
1689
+ });
1690
+ }, [busy, initialPromptPending, selectingSession]);
1691
+ useEffect(() => {
1692
+ if (!factory.scheduledPrompts ||
1693
+ initialPromptPending ||
1694
+ selectingSession ||
1695
+ busy ||
1696
+ permission) {
1697
+ return;
1698
+ }
1699
+ const controller = new AbortController();
1700
+ scheduledWaitRef.current?.abort();
1701
+ scheduledWaitRef.current = controller;
1702
+ const waitSignal = signal
1703
+ ? AbortSignal.any([signal, controller.signal])
436
1704
  : controller.signal;
437
1705
  void service()
438
1706
  .then((commands) => commands.nextScheduledPrompt?.(waitSignal) ?? null)
@@ -454,35 +1722,134 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
454
1722
  return () => controller.abort();
455
1723
  }, [busy, initialPromptPending, permission, selectingSession, sessionId]);
456
1724
  useInput((value, key) => {
457
- if (key.ctrl && value.toLowerCase() === 'c') {
458
- permissionRef.current?.resolve(false);
459
- elicitationRef.current?.resolve({ action: 'cancel' });
460
- questionRef.current?.resolve(null);
461
- planApprovalRef.current?.resolve(false);
462
- onCancel?.();
463
- exit();
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() };
464
1836
  return;
465
1837
  }
1838
+ const isKeybinding = (action) => keybindingAction === action;
466
1839
  if (permission) {
467
- if (value.toLowerCase() === 'y' || value === '1') {
1840
+ if (lower === 'y' || value === '1') {
468
1841
  permission.resolve(true);
469
1842
  }
470
- else if (value.toLowerCase() === 'n' ||
471
- value === '2' ||
472
- key.return ||
473
- key.escape) {
1843
+ else if (lower === 'n' || value === '2' || key.return || key.escape) {
474
1844
  permission.resolve(false);
475
1845
  }
476
1846
  return;
477
1847
  }
478
1848
  if (planApproval) {
479
- if (value.toLowerCase() === 'y' || value === '1') {
1849
+ if (lower === 'y' || value === '1') {
480
1850
  planApproval.resolve(true);
481
1851
  }
482
- else if (value.toLowerCase() === 'n' ||
483
- value === '2' ||
484
- key.return ||
485
- key.escape) {
1852
+ else if (lower === 'n' || value === '2' || key.return || key.escape) {
486
1853
  planApproval.resolve(false);
487
1854
  }
488
1855
  return;
@@ -498,8 +1865,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
498
1865
  throw new Error('Question state is invalid.');
499
1866
  const answer = questionAnswer(current, inputRef.current.trim());
500
1867
  const answers = { ...question.answers, [current.question]: answer };
501
- inputRef.current = '';
502
- setInput('');
1868
+ clearComposerInput();
503
1869
  if (question.index === question.questions.length - 1) {
504
1870
  question.resolve({ answers });
505
1871
  }
@@ -516,13 +1882,8 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
516
1882
  });
517
1883
  }
518
1884
  }
519
- else if (key.backspace || key.delete) {
520
- inputRef.current = inputRef.current.slice(0, -1);
521
- setInput(inputRef.current);
522
- }
523
- else if (!key.ctrl && !key.meta && value) {
524
- inputRef.current += value;
525
- setInput(inputRef.current);
1885
+ else {
1886
+ editComposer();
526
1887
  }
527
1888
  return;
528
1889
  }
@@ -532,8 +1893,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
532
1893
  }
533
1894
  else if (key.return) {
534
1895
  const answer = inputRef.current.trim();
535
- inputRef.current = '';
536
- setInput('');
1896
+ clearComposerInput();
537
1897
  if (!answer || answer.toLowerCase() === 'decline') {
538
1898
  elicitation.resolve({ action: 'decline' });
539
1899
  }
@@ -550,10 +1910,9 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
550
1910
  typeof content !== 'object' ||
551
1911
  Array.isArray(content))
552
1912
  throw new Error('elicitation content must be a JSON object');
553
- const elicitationContent = content;
554
1913
  elicitation.resolve({
555
1914
  action: 'accept',
556
- content: elicitationContent,
1915
+ content: content,
557
1916
  });
558
1917
  }
559
1918
  catch {
@@ -564,23 +1923,21 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
564
1923
  }
565
1924
  }
566
1925
  }
567
- else if (key.backspace || key.delete) {
568
- inputRef.current = inputRef.current.slice(0, -1);
569
- setInput(inputRef.current);
570
- }
571
- else if (!key.ctrl && !key.meta && value) {
572
- inputRef.current += value;
573
- setInput(inputRef.current);
1926
+ else {
1927
+ editComposer();
574
1928
  }
575
1929
  return;
576
1930
  }
577
1931
  if (selectingSession) {
578
- if (key.escape) {
579
- if (allowNewSession) {
1932
+ if (key.escape || value === '\u001B') {
1933
+ if (pickerIncludesNewSessionRef.current && allowNewSession) {
580
1934
  setSessionId(null);
581
1935
  setPendingFork(false);
582
1936
  setSelectingSession(false);
583
1937
  }
1938
+ else if (allowNewSession) {
1939
+ setSelectingSession(false);
1940
+ }
584
1941
  else {
585
1942
  onCancel?.();
586
1943
  exit();
@@ -591,100 +1948,1130 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, signal
591
1948
  setSelectedIndex(selectedIndexRef.current);
592
1949
  }
593
1950
  else if (key.downArrow) {
594
- selectedIndexRef.current = Math.min(choices.length - 1, selectedIndexRef.current + 1);
1951
+ selectedIndexRef.current = Math.min(Math.max(0, filteredPickerChoices.length - 1), selectedIndexRef.current + 1);
595
1952
  setSelectedIndex(selectedIndexRef.current);
596
1953
  }
597
1954
  else if (key.return) {
598
- const selected = choices[selectedIndexRef.current];
599
- 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);
600
1962
  if (!selected)
601
1963
  setPendingFork(false);
602
1964
  setSelectingSession(false);
603
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
+ }
604
1978
  return;
605
1979
  }
606
- if (busy) {
607
- if (key.escape || value === '\u001B')
608
- turnControllerRef.current?.abort();
1980
+ if (externalEditorRequest !== null || keybindingsEditing)
1981
+ return;
1982
+ if (clipboardPendingRef.current > 0) {
1983
+ if (isKeybinding('chat:imagePaste'))
1984
+ pasteClipboard();
609
1985
  return;
610
1986
  }
611
- if (key.return) {
612
- if (key.shift) {
613
- inputRef.current += '\n';
614
- setInput(inputRef.current);
615
- return;
616
- }
617
- const prompt = inputRef.current.trim();
618
- inputRef.current = '';
619
- setInput('');
620
- if (!prompt)
621
- return;
622
- if (prompt === '/exit') {
623
- exit();
624
- }
625
- else if (prompt === '/help' || prompt === '?') {
626
- append({
627
- kind: 'notice',
628
- text: '/new start a session · /sessions resume · /workflows list workflows · /exit quit',
629
- });
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 });
2006
+ return;
2007
+ }
2008
+ if (!busy && isKeybinding('chat:stash')) {
2009
+ if (inputRef.current.length > 0) {
2010
+ stashedInputRef.current = inputRef.current;
2011
+ clearComposerInput();
630
2012
  }
631
- else if (prompt === '/new') {
632
- setSessionId(null);
633
- setPendingFork(false);
634
- append({ kind: 'notice', text: 'Started a new session.' });
2013
+ else if (stashedInputRef.current.length > 0) {
2014
+ const stashed = stashedInputRef.current;
2015
+ stashedInputRef.current = '';
2016
+ updateComposerInput(stashed);
635
2017
  }
636
- else if (prompt === '/sessions') {
2018
+ 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
+ }
2783
+ const prompt = inputRef.current.trim();
2784
+ const images = promptImages(prompt);
2785
+ clearComposerInput();
2786
+ undoStackRef.current = [];
2787
+ composerImagesRef.current.clear();
2788
+ if (!prompt || prompt === '!')
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);
2793
+ if (prompt === '/exit') {
2794
+ exit();
2795
+ }
2796
+ else if (prompt === '/help' || prompt === '?') {
2797
+ updateMenu({ kind: 'help', tabIndex: 0, selectedIndex: 0 });
2798
+ }
2799
+ else if (prompt === '/new') {
2800
+ setSessionId(null);
2801
+ setPendingFork(false);
2802
+ append({ kind: 'notice', text: 'Started a new session.' });
2803
+ }
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('');
637
2884
  selectedIndexRef.current = 0;
638
2885
  setSelectedIndex(0);
639
2886
  setSelectingSession(true);
640
2887
  }
641
- else if (prompt === '/workflows') {
642
- const turn = (async () => {
2888
+ else if (prompt === '/diff') {
2889
+ const inspection = (async () => {
643
2890
  setBusy(true);
2891
+ setStatus('loading diff');
644
2892
  try {
645
- const workflows = (await service()).workflows?.() ?? [];
646
- append({
647
- kind: 'notice',
648
- text: workflows.length === 0
649
- ? 'No workflows.'
650
- : workflows
651
- .map((workflow) => `${String(workflow.task_id)} [${String(workflow.status)}] ${String(workflow.summary)}`)
652
- .join('\n'),
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,
653
2904
  });
654
2905
  }
655
2906
  catch (error) {
656
- append({
657
- kind: 'warning',
658
- text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
659
- });
2907
+ warn(error);
660
2908
  }
661
2909
  finally {
662
2910
  setBusy(false);
2911
+ setStatus('ready');
663
2912
  }
664
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);
665
2992
  onTurnChange?.(turn);
666
- void turn.finally(() => onTurnChange?.(null));
2993
+ void turn.then(() => onTurnChange?.(null), () => onTurnChange?.(null));
667
2994
  }
668
2995
  else {
669
- const turn = submit(prompt);
2996
+ const turn = submit(prompt, undefined, images);
670
2997
  onTurnChange?.(turn);
671
2998
  void turn.then(() => onTurnChange?.(null), () => onTurnChange?.(null));
672
2999
  }
3000
+ return;
3001
+ }
3002
+ if (isKeybinding('history:previous')) {
3003
+ restorePromptHistory('previous');
3004
+ return;
673
3005
  }
674
- else if (key.backspace || key.delete) {
675
- inputRef.current = inputRef.current.slice(0, -1);
676
- setInput(inputRef.current);
3006
+ if (isKeybinding('history:next')) {
3007
+ restorePromptHistory('next');
3008
+ return;
677
3009
  }
678
- else if (!key.ctrl && !key.meta && value) {
679
- inputRef.current += value;
680
- setInput(inputRef.current);
3010
+ if (isKeybinding('chat:clearInput')) {
3011
+ clearComposerInput();
3012
+ return;
681
3013
  }
3014
+ editComposer();
682
3015
  });
683
- return (_jsx(Box, { flexDirection: "column", children: selectingSession ? (_jsx(SessionPicker, { sessions: choices, selectedIndex: selectedIndex, screenReader: axScreenReader })) : (_jsxs(_Fragment, { children: [!axScreenReader && history.length === 0 && !sessionId ? (_jsx(WelcomePanel, { display: display, width: width })) : null, sessionId ? (_jsxs(Text, { dimColor: true, children: ["Session ", sessionId.slice(0, 8)] })) : null, _jsx(Transcript, { items: history, activeText: activeText, activeThinking: activeThinking, screenReader: axScreenReader }), permission ? (_jsxs(DialogFrame, { title: permission.kind === 'recovery'
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'
684
3017
  ? `Retry interrupted ${permission.call.name}?`
685
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
686
3019
  ? 'Enter comma-separated option numbers or custom text · Esc cancels'
687
- : 'Enter one option number or custom text · Esc cancels' })] })) : elicitation ? (_jsxs(DialogFrame, { title: `MCP elicitation (${elicitation.request.serverName})`, screenReader: axScreenReader, children: [_jsx(Text, { children: elicitation.request.message }), elicitation.request.url ? (_jsx(Text, { children: elicitation.request.url })) : null, elicitation.request.requestedSchema ? (_jsx(Text, { dimColor: true, children: JSON.stringify(elicitation.request.requestedSchema) })) : null, _jsxs(Text, { children: ["\u203A ", input] }), _jsx(Text, { dimColor: true, children: "Enter JSON object to accept \u00B7 Esc to cancel" })] })) : (_jsx(Composer, { input: input, busy: busy, status: status, display: display, ...(usage === undefined ? {} : { usage }), width: width, screenReader: axScreenReader }))] })) }));
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 }) })] }))] })) }));
688
3075
  }
689
3076
  export async function runInteractive(options) {
690
3077
  const controller = new AbortController();
@@ -697,11 +3084,15 @@ export async function runInteractive(options) {
697
3084
  signal,
698
3085
  });
699
3086
  let initialSessions;
3087
+ let initialSlashCommands;
3088
+ let initialAgents;
700
3089
  try {
701
3090
  const sessions = await listing.sessions();
702
3091
  initialSessions = options.sessionFilter
703
3092
  ? sessions.filter(options.sessionFilter)
704
3093
  : sessions;
3094
+ initialSlashCommands = listing.slashCommands?.() ?? [];
3095
+ initialAgents = listing.agentDefinitions?.() ?? [];
705
3096
  if (options.requireSession && initialSessions.length === 0) {
706
3097
  throw new Error(options.missingSessionMessage ??
707
3098
  'No conversation linked to a pull request in this project');
@@ -716,7 +3107,6 @@ export async function runInteractive(options) {
716
3107
  }
717
3108
  throw error;
718
3109
  }
719
- await listing.close?.();
720
3110
  const canonicalResumeSession = options.resume?.sessionId === undefined
721
3111
  ? undefined
722
3112
  : initialSessions.find((session) => session.sessionId.toLowerCase() ===
@@ -724,15 +3114,36 @@ export async function runInteractive(options) {
724
3114
  const resume = canonicalResumeSession
725
3115
  ? { ...options.resume, sessionId: canonicalResumeSession.sessionId }
726
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?.();
727
3134
  let activeTurn = null;
728
3135
  let cleanup = null;
729
- 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
730
3137
  ? {}
731
3138
  : { initialPrompt: options.initialPrompt }), signal: signal, onCancel: () => controller.abort(), onTurnChange: (turn) => {
732
3139
  activeTurn = turn;
733
3140
  }, onCleanup: (closing) => {
734
3141
  cleanup = closing;
735
- }, allowNewSession: !options.requireSession, ...(resume === undefined ? {} : { resume }), ...(options.display === undefined ? {} : { display: options.display }), ...(options.axScreenReader ? { axScreenReader: true } : {}) }), {
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
+ : {}) }), {
736
3147
  exitOnCtrlC: false,
737
3148
  incrementalRendering: !options.axScreenReader,
738
3149
  interactive: true,