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.
- package/README.md +18 -4
- package/dist/application/session-service.d.ts +34 -0
- package/dist/application/session-service.d.ts.map +1 -1
- package/dist/application/session-service.js +646 -38
- package/dist/application/session-service.js.map +1 -1
- package/dist/cli/interactive.d.ts +68 -8
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +2617 -206
- package/dist/cli/interactive.js.map +1 -1
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.d.ts.map +1 -1
- package/dist/cli/protocol.js.map +1 -1
- package/dist/cli/tui/claude-style.d.ts +142 -3
- package/dist/cli/tui/claude-style.d.ts.map +1 -1
- package/dist/cli/tui/claude-style.js +377 -16
- package/dist/cli/tui/claude-style.js.map +1 -1
- package/dist/cli/tui/clipboard.d.ts +24 -0
- package/dist/cli/tui/clipboard.d.ts.map +1 -0
- package/dist/cli/tui/clipboard.js +153 -0
- package/dist/cli/tui/clipboard.js.map +1 -0
- package/dist/cli/tui/composer-editor.d.ts +19 -0
- package/dist/cli/tui/composer-editor.d.ts.map +1 -0
- package/dist/cli/tui/composer-editor.js +99 -0
- package/dist/cli/tui/composer-editor.js.map +1 -0
- package/dist/cli/tui/composer-images.d.ts +7 -0
- package/dist/cli/tui/composer-images.d.ts.map +1 -0
- package/dist/cli/tui/composer-images.js +48 -0
- package/dist/cli/tui/composer-images.js.map +1 -0
- package/dist/cli/tui/conversation-export.d.ts +6 -0
- package/dist/cli/tui/conversation-export.d.ts.map +1 -0
- package/dist/cli/tui/conversation-export.js +76 -0
- package/dist/cli/tui/conversation-export.js.map +1 -0
- package/dist/cli/tui/external-editor.d.ts +21 -0
- package/dist/cli/tui/external-editor.d.ts.map +1 -0
- package/dist/cli/tui/external-editor.js +144 -0
- package/dist/cli/tui/external-editor.js.map +1 -0
- package/dist/cli/tui/file-picker.d.ts +33 -0
- package/dist/cli/tui/file-picker.d.ts.map +1 -0
- package/dist/cli/tui/file-picker.js +117 -0
- package/dist/cli/tui/file-picker.js.map +1 -0
- package/dist/cli/tui/git-diff.d.ts +14 -0
- package/dist/cli/tui/git-diff.d.ts.map +1 -0
- package/dist/cli/tui/git-diff.js +55 -0
- package/dist/cli/tui/git-diff.js.map +1 -0
- package/dist/cli/tui/keybindings.d.ts +14 -0
- package/dist/cli/tui/keybindings.d.ts.map +1 -0
- package/dist/cli/tui/keybindings.js +430 -0
- package/dist/cli/tui/keybindings.js.map +1 -0
- package/dist/cli/tui/permission-settings.d.ts +18 -0
- package/dist/cli/tui/permission-settings.d.ts.map +1 -0
- package/dist/cli/tui/permission-settings.js +109 -0
- package/dist/cli/tui/permission-settings.js.map +1 -0
- package/dist/cli/tui/slash-commands.d.ts +11 -0
- package/dist/cli/tui/slash-commands.d.ts.map +1 -0
- package/dist/cli/tui/slash-commands.js +183 -0
- package/dist/cli/tui/slash-commands.js.map +1 -0
- package/dist/cli/tui/terminal-suspend.d.ts +5 -0
- package/dist/cli/tui/terminal-suspend.d.ts.map +1 -0
- package/dist/cli/tui/terminal-suspend.js +9 -0
- package/dist/cli/tui/terminal-suspend.js.map +1 -0
- package/dist/cli/tui/workspace-directories.d.ts +3 -0
- package/dist/cli/tui/workspace-directories.d.ts.map +1 -0
- package/dist/cli/tui/workspace-directories.js +55 -0
- package/dist/cli/tui/workspace-directories.js.map +1 -0
- package/dist/cli.d.ts +23 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +45 -5
- package/dist/cli.js.map +1 -1
- package/dist/compatibility/claude/compaction.d.ts +8 -0
- package/dist/compatibility/claude/compaction.d.ts.map +1 -1
- package/dist/compatibility/claude/compaction.js +45 -8
- package/dist/compatibility/claude/compaction.js.map +1 -1
- package/dist/compatibility/claude/fork.d.ts.map +1 -1
- package/dist/compatibility/claude/fork.js +24 -6
- package/dist/compatibility/claude/fork.js.map +1 -1
- package/dist/compatibility/claude/history.d.ts.map +1 -1
- package/dist/compatibility/claude/history.js +41 -1
- package/dist/compatibility/claude/history.js.map +1 -1
- package/dist/compatibility/claude/projection.d.ts +29 -1
- package/dist/compatibility/claude/projection.d.ts.map +1 -1
- package/dist/compatibility/claude/projection.js +132 -2
- package/dist/compatibility/claude/projection.js.map +1 -1
- package/dist/compatibility/claude/schema.d.ts.map +1 -1
- package/dist/compatibility/claude/schema.js +34 -7
- package/dist/compatibility/claude/schema.js.map +1 -1
- package/dist/compatibility/claude/translation.d.ts +7 -0
- package/dist/compatibility/claude/translation.d.ts.map +1 -1
- package/dist/compatibility/claude/translation.js +20 -0
- package/dist/compatibility/claude/translation.js.map +1 -1
- package/dist/core/runtime.d.ts +19 -0
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +30 -11
- package/dist/core/runtime.js.map +1 -1
- package/dist/extensions/claude-extensions.d.ts +12 -0
- package/dist/extensions/claude-extensions.d.ts.map +1 -1
- package/dist/extensions/claude-extensions.js +49 -0
- package/dist/extensions/claude-extensions.js.map +1 -1
- package/dist/hooks/claude-hook-tools.js +2 -2
- package/dist/hooks/claude-hook-tools.js.map +1 -1
- package/dist/persistence/claude-transcript-store.d.ts.map +1 -1
- package/dist/persistence/claude-transcript-store.js +4 -3
- package/dist/persistence/claude-transcript-store.js.map +1 -1
- package/dist/tools/claude-interactive-tools.d.ts +1 -0
- package/dist/tools/claude-interactive-tools.d.ts.map +1 -1
- package/dist/tools/claude-interactive-tools.js +12 -0
- package/dist/tools/claude-interactive-tools.js.map +1 -1
- package/dist/tools/local-tools.d.ts.map +1 -1
- package/dist/tools/local-tools.js +11 -0
- package/dist/tools/local-tools.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/interactive.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
const
|
|
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 [
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
1012
|
+
if (serviceCreationRef.current === pending) {
|
|
1013
|
+
serviceCreationRef.current = undefined;
|
|
1014
|
+
serviceCreationEpochRef.current = null;
|
|
1015
|
+
}
|
|
335
1016
|
}
|
|
336
1017
|
};
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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: '
|
|
378
|
-
text:
|
|
1084
|
+
kind: 'notice',
|
|
1085
|
+
text: `Permission mode set to ${mode} for this session.`,
|
|
379
1086
|
});
|
|
380
|
-
setStatus('failed');
|
|
381
1087
|
}
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
1091
|
+
finally {
|
|
1092
|
+
setBusy(false);
|
|
1093
|
+
setStatus('ready');
|
|
1094
|
+
}
|
|
1095
|
+
})();
|
|
1096
|
+
onTurnChange?.(change);
|
|
1097
|
+
void change.finally(() => onTurnChange?.(null));
|
|
403
1098
|
};
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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 (
|
|
1840
|
+
if (lower === 'y' || value === '1') {
|
|
468
1841
|
permission.resolve(true);
|
|
469
1842
|
}
|
|
470
|
-
else if (
|
|
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 (
|
|
1849
|
+
if (lower === 'y' || value === '1') {
|
|
480
1850
|
planApproval.resolve(true);
|
|
481
1851
|
}
|
|
482
|
-
else if (
|
|
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
|
-
|
|
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
|
|
520
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
568
|
-
|
|
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(
|
|
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
|
|
599
|
-
|
|
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 (
|
|
607
|
-
|
|
608
|
-
|
|
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 (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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 (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
2013
|
+
else if (stashedInputRef.current.length > 0) {
|
|
2014
|
+
const stashed = stashedInputRef.current;
|
|
2015
|
+
stashedInputRef.current = '';
|
|
2016
|
+
updateComposerInput(stashed);
|
|
635
2017
|
}
|
|
636
|
-
|
|
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 === '/
|
|
642
|
-
const
|
|
2888
|
+
else if (prompt === '/diff') {
|
|
2889
|
+
const inspection = (async () => {
|
|
643
2890
|
setBusy(true);
|
|
2891
|
+
setStatus('loading diff');
|
|
644
2892
|
try {
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
kind: '
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
3006
|
+
if (isKeybinding('history:next')) {
|
|
3007
|
+
restorePromptHistory('next');
|
|
3008
|
+
return;
|
|
677
3009
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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:
|
|
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(
|
|
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, ...(
|
|
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,
|