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