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