@wangzhizhi/remi 0.0.1-alpha
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 +9 -0
- package/dist/doctor.js +108 -0
- package/dist/git.js +41 -0
- package/dist/help.js +27 -0
- package/dist/i18n.js +422 -0
- package/dist/index.js +97 -0
- package/dist/initPrompt.js +17 -0
- package/dist/model.js +116 -0
- package/dist/modelSelection.js +34 -0
- package/dist/permissionDisplay.js +46 -0
- package/dist/permissions.js +206 -0
- package/dist/repl.js +346 -0
- package/dist/resume.js +3 -0
- package/dist/setup.js +62 -0
- package/dist/statusline.js +59 -0
- package/dist/style.js +48 -0
- package/dist/syntaxTheme.js +39 -0
- package/dist/tui/RemiApp.js +1756 -0
- package/dist/tui/commands.js +427 -0
- package/dist/tui/index.js +42 -0
- package/dist/tui/renderers/Header.js +28 -0
- package/dist/tui/renderers/MessageList.js +1176 -0
- package/dist/tui/renderers/PromptBox.js +118 -0
- package/dist/tui/renderers/StatusLine.js +124 -0
- package/dist/tui/renderers/WorkingIndicator.js +70 -0
- package/dist/tui/slashCommandHighlight.js +8 -0
- package/dist/tui/theme.js +13 -0
- package/dist/tui/types.js +1 -0
- package/dist/usage.js +66 -0
- package/dist/version.js +5 -0
- package/node_modules/@remi/compact/dist/index.js +389 -0
- package/node_modules/@remi/compact/package.json +8 -0
- package/node_modules/@remi/config/dist/index.js +426 -0
- package/node_modules/@remi/config/package.json +8 -0
- package/node_modules/@remi/core/dist/contextBuilder.js +344 -0
- package/node_modules/@remi/core/dist/directoryOverview.js +359 -0
- package/node_modules/@remi/core/dist/index.js +2843 -0
- package/node_modules/@remi/core/dist/projectInstructions.js +123 -0
- package/node_modules/@remi/core/dist/responseStyles.js +98 -0
- package/node_modules/@remi/core/package.json +8 -0
- package/node_modules/@remi/llm/dist/index.js +804 -0
- package/node_modules/@remi/llm/package.json +8 -0
- package/node_modules/@remi/memory/dist/index.js +312 -0
- package/node_modules/@remi/memory/package.json +8 -0
- package/node_modules/@remi/permissions/dist/index.js +90 -0
- package/node_modules/@remi/permissions/package.json +8 -0
- package/node_modules/@remi/sessions/dist/index.js +370 -0
- package/node_modules/@remi/sessions/package.json +8 -0
- package/node_modules/@remi/skills/dist/index.js +273 -0
- package/node_modules/@remi/skills/package.json +8 -0
- package/node_modules/@remi/terminal-markdown/dist/index.js +1412 -0
- package/node_modules/@remi/terminal-markdown/package.json +8 -0
- package/node_modules/@remi/tools/dist/index.js +3875 -0
- package/node_modules/@remi/tools/package.json +8 -0
- package/package.json +48 -0
|
@@ -0,0 +1,1756 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { Box, Text, useApp, useInput, useWindowSize } from 'ink';
|
|
6
|
+
import { estimateSessionContextTrace, runChatTurn } from '@remi/core';
|
|
7
|
+
import { compactSession } from '@remi/compact';
|
|
8
|
+
import { loadRemiConfig, normalizePermissionProfile } from '@remi/config';
|
|
9
|
+
import { createModelRouter } from '@remi/llm';
|
|
10
|
+
import { loadSkillIndex } from '@remi/skills';
|
|
11
|
+
import { appendProjectInputHistory, createNewSession, readProjectInputHistory, readSessionEvents, readSessionPermissionRules, readSessionUserInputs, resolveActiveSessionId, writeSessionPermissionRules, } from '@remi/sessions';
|
|
12
|
+
import { createSkillsListPanelMessage, executeSlashCommand, listSlashCommands, saveProjectDisabledSkills } from './commands.js';
|
|
13
|
+
import { Header } from './renderers/Header.js';
|
|
14
|
+
import { MessageList, PlanMessage } from './renderers/MessageList.js';
|
|
15
|
+
import { PromptBox } from './renderers/PromptBox.js';
|
|
16
|
+
import { StatusLine } from './renderers/StatusLine.js';
|
|
17
|
+
import { loadGitStatus } from '../git.js';
|
|
18
|
+
import { languageLabel, resolveConfiguredLanguage, saveProjectLanguage, t } from '../i18n.js';
|
|
19
|
+
import { addSessionPermissionRules, formatPermissionProfileChanged, formatPermissionRulesScope, resolveConfiguredPermissionProfile, saveProjectPermissionRule, saveProjectPermissionProfile, } from '../permissions.js';
|
|
20
|
+
import { formatResponseStyleChanged, resolveConfiguredResponseStyle, saveProjectResponseStyle } from '../style.js';
|
|
21
|
+
import { formatSyntaxThemeChanged, resolveConfiguredSyntaxTheme, saveProjectSyntaxTheme } from '../syntaxTheme.js';
|
|
22
|
+
import { defaultStatusLineItems, getStatusLineCatalog, resolveConfiguredStatusLineItems, saveProjectStatusLineItems } from '../statusline.js';
|
|
23
|
+
import { addTokenUsage, contextRemainingPercentFromUsage, createEmptyTokenUsage, formatTokenUsage, sumSessionTokenUsage } from '../usage.js';
|
|
24
|
+
import { formatPermissionFileAction } from '../permissionDisplay.js';
|
|
25
|
+
import { saveProjectMainModel } from '../modelSelection.js';
|
|
26
|
+
import { remiDarkTheme } from './theme.js';
|
|
27
|
+
const assistantStreamFlushIntervalMs = 24;
|
|
28
|
+
const assistantStreamImmediateFlushChars = 32;
|
|
29
|
+
function isPrintableInput(input) {
|
|
30
|
+
return input.length > 0 && !/[\u0000-\u001f\u007f]/.test(input);
|
|
31
|
+
}
|
|
32
|
+
function estimateInitialContextRemainingPercent(cwd, sessionId) {
|
|
33
|
+
try {
|
|
34
|
+
return estimateSessionContextTrace(sessionId ? { cwd, sessionId } : { cwd })?.remainingPercent;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const exitResponseDelayMs = 20;
|
|
41
|
+
const compactProgressDelayMs = 20;
|
|
42
|
+
const queuedPromptAbortReason = 'queued-prompt-dispatch';
|
|
43
|
+
function abortSignalReasonMessage(signal) {
|
|
44
|
+
const reason = signal.reason;
|
|
45
|
+
if (reason instanceof Error) {
|
|
46
|
+
return reason.message;
|
|
47
|
+
}
|
|
48
|
+
return typeof reason === 'string' ? reason : undefined;
|
|
49
|
+
}
|
|
50
|
+
function isQueuedPromptDispatchAbort(signal, message) {
|
|
51
|
+
return signal.aborted && (message === queuedPromptAbortReason || abortSignalReasonMessage(signal) === queuedPromptAbortReason);
|
|
52
|
+
}
|
|
53
|
+
function isSlashCommandInput(input) {
|
|
54
|
+
const normalized = input.trim();
|
|
55
|
+
return normalized.startsWith('/') || ['help', 'exit', 'quit'].includes(normalized);
|
|
56
|
+
}
|
|
57
|
+
function buildQueuedPromptDispatch(previousUserInput, queuedText) {
|
|
58
|
+
if (isSlashCommandInput(queuedText)) {
|
|
59
|
+
return { visibleText: queuedText, modelInput: queuedText };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
visibleText: queuedText,
|
|
63
|
+
modelInput: [
|
|
64
|
+
'A user submitted additional guidance while the previous request was running.',
|
|
65
|
+
'Treat the queued guidance as a continuation or confirmation for the same objective, not as an isolated new task.',
|
|
66
|
+
'',
|
|
67
|
+
'Previous user request:',
|
|
68
|
+
previousUserInput,
|
|
69
|
+
'',
|
|
70
|
+
'Queued guidance:',
|
|
71
|
+
queuedText,
|
|
72
|
+
].join('\n'),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function visibleMessagesWithoutPlanUpdates(messages) {
|
|
76
|
+
return messages.filter(message => message.kind !== 'plan-update');
|
|
77
|
+
}
|
|
78
|
+
function latestPlanItemsFromMessages(messages) {
|
|
79
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
80
|
+
const message = messages[index];
|
|
81
|
+
if (message?.kind === 'plan-update') {
|
|
82
|
+
return message.items;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
function trimOneWord(value) {
|
|
88
|
+
return value.trimEnd().replace(/\s*\S+$/, '');
|
|
89
|
+
}
|
|
90
|
+
function inputLength(value) {
|
|
91
|
+
return Array.from(value).length;
|
|
92
|
+
}
|
|
93
|
+
function loadInputHistory(cwd, sessionId) {
|
|
94
|
+
return mergeInputHistory(readProjectInputHistory(cwd), readSessionUserInputs(cwd, sessionId));
|
|
95
|
+
}
|
|
96
|
+
function mergeInputHistory(...sources) {
|
|
97
|
+
return sources.reduce((history, source) => {
|
|
98
|
+
return source.reduce((current, value) => appendInputHistoryEntry(current, value), history);
|
|
99
|
+
}, []);
|
|
100
|
+
}
|
|
101
|
+
function appendInputHistoryEntry(history, value) {
|
|
102
|
+
const normalized = value.trim();
|
|
103
|
+
if (normalized.length === 0 || history[history.length - 1] === normalized) {
|
|
104
|
+
return history;
|
|
105
|
+
}
|
|
106
|
+
return [...history, normalized].slice(-200);
|
|
107
|
+
}
|
|
108
|
+
function clampInputCursor(cursor, value) {
|
|
109
|
+
return Math.max(0, Math.min(inputLength(value), cursor));
|
|
110
|
+
}
|
|
111
|
+
function insertInputText(value, cursor, insertedText) {
|
|
112
|
+
const chars = Array.from(value);
|
|
113
|
+
const insertedChars = Array.from(insertedText);
|
|
114
|
+
const safeCursor = Math.max(0, Math.min(chars.length, cursor));
|
|
115
|
+
chars.splice(safeCursor, 0, ...insertedChars);
|
|
116
|
+
return { value: chars.join(''), cursor: safeCursor + insertedChars.length };
|
|
117
|
+
}
|
|
118
|
+
function deleteInputBeforeCursor(value, cursor) {
|
|
119
|
+
const chars = Array.from(value);
|
|
120
|
+
const safeCursor = Math.max(0, Math.min(chars.length, cursor));
|
|
121
|
+
if (safeCursor === 0) {
|
|
122
|
+
return { value, cursor: safeCursor };
|
|
123
|
+
}
|
|
124
|
+
chars.splice(safeCursor - 1, 1);
|
|
125
|
+
return { value: chars.join(''), cursor: safeCursor - 1 };
|
|
126
|
+
}
|
|
127
|
+
function deleteInputAtCursor(value, cursor) {
|
|
128
|
+
const chars = Array.from(value);
|
|
129
|
+
const safeCursor = Math.max(0, Math.min(chars.length, cursor));
|
|
130
|
+
if (safeCursor >= chars.length) {
|
|
131
|
+
return { value, cursor: safeCursor };
|
|
132
|
+
}
|
|
133
|
+
chars.splice(safeCursor, 1);
|
|
134
|
+
return { value: chars.join(''), cursor: safeCursor };
|
|
135
|
+
}
|
|
136
|
+
function trimOneWordBeforeCursor(value, cursor) {
|
|
137
|
+
const chars = Array.from(value);
|
|
138
|
+
const safeCursor = Math.max(0, Math.min(chars.length, cursor));
|
|
139
|
+
const before = chars.slice(0, safeCursor).join('');
|
|
140
|
+
const after = chars.slice(safeCursor).join('');
|
|
141
|
+
const trimmedBefore = trimOneWord(before);
|
|
142
|
+
return { value: `${trimmedBefore}${after}`, cursor: inputLength(trimmedBefore) };
|
|
143
|
+
}
|
|
144
|
+
export function RemiApp({ cwd = process.cwd(), chatRunner = runChatTurn, initialMessages = [], initialSessionId, onTokenUsage, onSessionId, width, }) {
|
|
145
|
+
const { exit } = useApp();
|
|
146
|
+
const { columns } = useWindowSize();
|
|
147
|
+
const activeAbortController = useRef(undefined);
|
|
148
|
+
const initialResolvedSessionId = useRef(undefined);
|
|
149
|
+
initialResolvedSessionId.current ??= initialSessionId ?? resolveActiveSessionId(cwd);
|
|
150
|
+
const initialUsageTotals = useRef(undefined);
|
|
151
|
+
initialUsageTotals.current ??= sumSessionTokenUsage(readSessionEvents(cwd, initialResolvedSessionId.current ?? resolveActiveSessionId(cwd)));
|
|
152
|
+
const initialVisibleMessages = useRef(undefined);
|
|
153
|
+
initialVisibleMessages.current ??= visibleMessagesWithoutPlanUpdates(initialMessages);
|
|
154
|
+
const initialPlanItems = useRef(undefined);
|
|
155
|
+
initialPlanItems.current ??= latestPlanItemsFromMessages(initialMessages);
|
|
156
|
+
const initialContextRemainingPercent = useRef(undefined);
|
|
157
|
+
initialContextRemainingPercent.current ??= estimateInitialContextRemainingPercent(cwd, initialResolvedSessionId.current);
|
|
158
|
+
const [input, setInput] = useState('');
|
|
159
|
+
const [inputCursor, setInputCursor] = useState(0);
|
|
160
|
+
const [sessionId, setSessionId] = useState(() => initialResolvedSessionId.current ?? resolveActiveSessionId(cwd));
|
|
161
|
+
const [messages, setMessages] = useState(initialVisibleMessages.current ?? []);
|
|
162
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
163
|
+
const [workingStartedAt, setWorkingStartedAt] = useState(undefined);
|
|
164
|
+
const [progressLabelOverride, setProgressLabelOverride] = useState(undefined);
|
|
165
|
+
const [selectedCommandIndex, setSelectedCommandIndex] = useState(0);
|
|
166
|
+
const [mainModelAlias, setMainModelAlias] = useState(undefined);
|
|
167
|
+
const [language, setLanguage] = useState(() => resolveConfiguredLanguage(cwd));
|
|
168
|
+
const [responseStyle, setResponseStyle] = useState(() => resolveConfiguredResponseStyle(cwd));
|
|
169
|
+
const [syntaxTheme, setSyntaxTheme] = useState(() => resolveConfiguredSyntaxTheme(cwd));
|
|
170
|
+
const [permissionProfile, setPermissionProfile] = useState(() => resolveConfiguredPermissionProfile(cwd));
|
|
171
|
+
const [fixedPlanItems, setFixedPlanItems] = useState(initialPlanItems.current);
|
|
172
|
+
const [queuedPrompts, setQueuedPrompts] = useState([]);
|
|
173
|
+
const queuedPromptsRef = useRef([]);
|
|
174
|
+
const queuedPromptIdRef = useRef(0);
|
|
175
|
+
const [pendingQueuedDispatch, setPendingQueuedDispatch] = useState(undefined);
|
|
176
|
+
const [statusLineItems, setStatusLineItems] = useState(() => resolveConfiguredStatusLineItems(cwd));
|
|
177
|
+
const [contextRemainingPercent, setContextRemainingPercent] = useState(initialContextRemainingPercent.current);
|
|
178
|
+
const [usageTotals, setUsageTotals] = useState(() => initialUsageTotals.current ?? createEmptyTokenUsage());
|
|
179
|
+
const [inputHistory, setInputHistory] = useState(() => loadInputHistory(cwd, initialResolvedSessionId.current ?? resolveActiveSessionId(cwd)));
|
|
180
|
+
const [historyCursor, setHistoryCursor] = useState(undefined);
|
|
181
|
+
const [historyDraft, setHistoryDraft] = useState('');
|
|
182
|
+
const initialSessionPermissionRules = useRef(undefined);
|
|
183
|
+
initialSessionPermissionRules.current ??= readSessionPermissionRules(cwd, sessionId);
|
|
184
|
+
const sessionPermissionRules = useRef(initialSessionPermissionRules.current);
|
|
185
|
+
const pendingPermission = useRef(undefined);
|
|
186
|
+
const isStatusLinePanelOpen = useMemo(() => {
|
|
187
|
+
const panelIndex = findLastStatusLinePanelIndex(messages);
|
|
188
|
+
return panelIndex === findLastPickerPanelIndex(messages) && messages[panelIndex]?.kind === 'statusline-panel';
|
|
189
|
+
}, [messages]);
|
|
190
|
+
const isSyntaxThemePanelOpen = useMemo(() => {
|
|
191
|
+
const panelIndex = findLastSyntaxThemePanelIndex(messages);
|
|
192
|
+
return panelIndex === findLastPickerPanelIndex(messages) && messages[panelIndex]?.kind === 'syntax-theme-panel';
|
|
193
|
+
}, [messages]);
|
|
194
|
+
const isPermissionRequestOpen = useMemo(() => {
|
|
195
|
+
const panelIndex = findLastPermissionRequestIndex(messages);
|
|
196
|
+
return panelIndex === findLastPickerPanelIndex(messages) && messages[panelIndex]?.kind === 'permission-request';
|
|
197
|
+
}, [messages]);
|
|
198
|
+
const isSkillsPanelOpen = useMemo(() => {
|
|
199
|
+
const panelIndex = findLastSkillsPanelIndex(messages);
|
|
200
|
+
return panelIndex === findLastPickerPanelIndex(messages) && messages[panelIndex]?.kind === 'skills-panel';
|
|
201
|
+
}, [messages]);
|
|
202
|
+
const bottomPanelIndex = useMemo(() => findActiveBottomPanelIndex(messages), [messages]);
|
|
203
|
+
const bottomPanelMessage = bottomPanelIndex >= 0 ? messages[bottomPanelIndex] : undefined;
|
|
204
|
+
const mainMessages = useMemo(() => (bottomPanelIndex >= 0 ? messages.filter((_, index) => index !== bottomPanelIndex) : messages), [bottomPanelIndex, messages]);
|
|
205
|
+
const previewStatusLineItems = useMemo(() => {
|
|
206
|
+
const panelIndex = findLastStatusLinePanelIndex(messages);
|
|
207
|
+
const panel = messages[panelIndex];
|
|
208
|
+
if (panelIndex === findLastPickerPanelIndex(messages) && panel?.kind === 'statusline-panel') {
|
|
209
|
+
return statusLineItemsFromPanel(panel);
|
|
210
|
+
}
|
|
211
|
+
return statusLineItems;
|
|
212
|
+
}, [messages, statusLineItems]);
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
onSessionId?.(sessionId);
|
|
215
|
+
}, [onSessionId, sessionId]);
|
|
216
|
+
const status = useMemo(() => {
|
|
217
|
+
try {
|
|
218
|
+
const loaded = loadRemiConfig({ cwd });
|
|
219
|
+
const router = createModelRouter(loaded.config);
|
|
220
|
+
if (mainModelAlias) {
|
|
221
|
+
router.switchRoleModel('main', mainModelAlias);
|
|
222
|
+
}
|
|
223
|
+
const resolved = router.resolve('main');
|
|
224
|
+
const git = loadGitStatus(cwd);
|
|
225
|
+
const resolvedPermissionProfile = permissionProfile ?? normalizePermissionProfile(loaded.config.permissions);
|
|
226
|
+
const permissionMode = loaded.config.permissions?.mode ?? (resolvedPermissionProfile === 'full-access' ? 'bypass' : 'auto');
|
|
227
|
+
return {
|
|
228
|
+
cwd,
|
|
229
|
+
profile: resolved.profile,
|
|
230
|
+
model: resolved.displayName ?? resolved.alias,
|
|
231
|
+
role: resolved.role,
|
|
232
|
+
sessionId,
|
|
233
|
+
permissionMode,
|
|
234
|
+
permissionProfile: resolvedPermissionProfile,
|
|
235
|
+
...(contextRemainingPercent !== undefined ? { contextRemainingPercent } : {}),
|
|
236
|
+
...(git ? { git } : {}),
|
|
237
|
+
statusLineItems: previewStatusLineItems,
|
|
238
|
+
usage: usageTotals,
|
|
239
|
+
runState: isSubmitting ? (workingStartedAt === undefined ? 'Working' : 'Thinking') : 'Ready',
|
|
240
|
+
language,
|
|
241
|
+
mcpCount: 0,
|
|
242
|
+
skillCount: loadEnabledSkillCount(cwd, loaded.config.skills?.disabled),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return {
|
|
247
|
+
cwd,
|
|
248
|
+
profile: 'unset',
|
|
249
|
+
model: 'unconfigured',
|
|
250
|
+
role: 'main',
|
|
251
|
+
sessionId,
|
|
252
|
+
permissionMode: 'readonly',
|
|
253
|
+
permissionProfile,
|
|
254
|
+
...(contextRemainingPercent !== undefined ? { contextRemainingPercent } : {}),
|
|
255
|
+
statusLineItems: previewStatusLineItems,
|
|
256
|
+
usage: usageTotals,
|
|
257
|
+
runState: isSubmitting ? (workingStartedAt === undefined ? 'Working' : 'Thinking') : 'Ready',
|
|
258
|
+
language,
|
|
259
|
+
mcpCount: 0,
|
|
260
|
+
skillCount: 0,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}, [contextRemainingPercent, cwd, isSubmitting, language, mainModelAlias, permissionProfile, previewStatusLineItems, sessionId, usageTotals, workingStartedAt]);
|
|
264
|
+
const commandHints = useMemo(() => listSlashCommands(language).map(command => ({
|
|
265
|
+
name: command.name,
|
|
266
|
+
description: command.description,
|
|
267
|
+
})), [language]);
|
|
268
|
+
const activeCommandHints = historyCursor === undefined ? commandHints : [];
|
|
269
|
+
const prompt = {
|
|
270
|
+
value: input,
|
|
271
|
+
cursorPosition: clampInputCursor(inputCursor, input),
|
|
272
|
+
mode: 'prompt',
|
|
273
|
+
placeholder: isSubmitting ? t(language, 'prompt.queue.placeholder') : t(language, 'prompt.placeholder'),
|
|
274
|
+
queuedCommands: [],
|
|
275
|
+
commandHints: activeCommandHints,
|
|
276
|
+
selectedCommandIndex: normalizeIndex(selectedCommandIndex, visibleCommandHintsForInput(input, activeCommandHints).length),
|
|
277
|
+
};
|
|
278
|
+
const viewportWidth = width ?? columns;
|
|
279
|
+
const appendMessage = useCallback((message) => {
|
|
280
|
+
setMessages(current => {
|
|
281
|
+
const base = message.kind === 'model-panel' ||
|
|
282
|
+
message.kind === 'style-panel' ||
|
|
283
|
+
message.kind === 'syntax-theme-panel' ||
|
|
284
|
+
message.kind === 'statusline-panel' ||
|
|
285
|
+
message.kind === 'language-panel' ||
|
|
286
|
+
message.kind === 'permission-profile-panel' ||
|
|
287
|
+
message.kind === 'skills-panel'
|
|
288
|
+
? current.filter(existing => existing.kind !== 'model-panel' &&
|
|
289
|
+
existing.kind !== 'style-panel' &&
|
|
290
|
+
existing.kind !== 'syntax-theme-panel' &&
|
|
291
|
+
existing.kind !== 'statusline-panel' &&
|
|
292
|
+
existing.kind !== 'language-panel' &&
|
|
293
|
+
existing.kind !== 'permission-profile-panel' &&
|
|
294
|
+
existing.kind !== 'skills-panel')
|
|
295
|
+
: current;
|
|
296
|
+
return [...base, message];
|
|
297
|
+
});
|
|
298
|
+
}, []);
|
|
299
|
+
const updateQueuedPrompts = useCallback((updater) => {
|
|
300
|
+
const next = updater(queuedPromptsRef.current);
|
|
301
|
+
queuedPromptsRef.current = next;
|
|
302
|
+
setQueuedPrompts(next);
|
|
303
|
+
}, []);
|
|
304
|
+
const enqueuePrompt = useCallback((text) => {
|
|
305
|
+
const normalized = text.trim();
|
|
306
|
+
if (normalized.length === 0) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
queuedPromptIdRef.current += 1;
|
|
310
|
+
const prompt = { id: queuedPromptIdRef.current, text: normalized };
|
|
311
|
+
updateQueuedPrompts(current => [...current, prompt]);
|
|
312
|
+
return true;
|
|
313
|
+
}, [updateQueuedPrompts]);
|
|
314
|
+
const consumeQueuedPromptText = useCallback(() => {
|
|
315
|
+
const prompts = queuedPromptsRef.current;
|
|
316
|
+
if (prompts.length === 0) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
updateQueuedPrompts(() => []);
|
|
320
|
+
return prompts.map(prompt => prompt.text).join('\n\n');
|
|
321
|
+
}, [updateQueuedPrompts]);
|
|
322
|
+
const queueCurrentInput = useCallback(() => {
|
|
323
|
+
const queued = enqueuePrompt(input);
|
|
324
|
+
if (!queued) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
setInput('');
|
|
328
|
+
setInputCursor(0);
|
|
329
|
+
setSelectedCommandIndex(0);
|
|
330
|
+
setHistoryCursor(undefined);
|
|
331
|
+
setHistoryDraft('');
|
|
332
|
+
return true;
|
|
333
|
+
}, [enqueuePrompt, input]);
|
|
334
|
+
const requestExit = useCallback((message, userInput) => {
|
|
335
|
+
setMessages(current => {
|
|
336
|
+
const next = [...finalizeStreamingMessages(current)];
|
|
337
|
+
if (userInput) {
|
|
338
|
+
next.push({ kind: 'user', text: userInput });
|
|
339
|
+
}
|
|
340
|
+
if (message) {
|
|
341
|
+
next.push(message);
|
|
342
|
+
}
|
|
343
|
+
return next;
|
|
344
|
+
});
|
|
345
|
+
setTimeout(() => exit(), exitResponseDelayMs);
|
|
346
|
+
}, [exit]);
|
|
347
|
+
const requestToolPermission = useCallback(async (request, decision) => {
|
|
348
|
+
const requestId = `permission-${request.toolName}-${Date.now()}`;
|
|
349
|
+
const message = permissionRequestMessage(requestId, request, decision, language);
|
|
350
|
+
setWorkingStartedAt(undefined);
|
|
351
|
+
setMessages(current => [...finalizeStreamingMessages(current), message]);
|
|
352
|
+
return await new Promise(resolve => {
|
|
353
|
+
pendingPermission.current = { decision, resolve };
|
|
354
|
+
});
|
|
355
|
+
}, [language]);
|
|
356
|
+
const submit = useCallback(async (inputOverride) => {
|
|
357
|
+
if (isSubmitting) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const value = (typeof inputOverride === 'object' ? inputOverride.visibleText : (inputOverride ?? input)).trim();
|
|
361
|
+
const overrideModelInput = typeof inputOverride === 'object' ? inputOverride.modelInput.trim() : undefined;
|
|
362
|
+
setInput('');
|
|
363
|
+
setInputCursor(0);
|
|
364
|
+
setSelectedCommandIndex(0);
|
|
365
|
+
setHistoryCursor(undefined);
|
|
366
|
+
setHistoryDraft('');
|
|
367
|
+
if (value.length === 0) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
appendProjectInputHistory(cwd, value, sessionId);
|
|
371
|
+
setInputHistory(current => appendInputHistoryEntry(current, value));
|
|
372
|
+
let chatInput = overrideModelInput ?? value;
|
|
373
|
+
const visibleUserInput = value;
|
|
374
|
+
if (isSlashCommandInput(value)) {
|
|
375
|
+
const context = mainModelAlias
|
|
376
|
+
? { cwd, sessionId, mainModelAlias, responseStyle, syntaxTheme, statusLineItems, language, permissionProfile }
|
|
377
|
+
: { cwd, sessionId, responseStyle, syntaxTheme, statusLineItems, language, permissionProfile };
|
|
378
|
+
const result = executeSlashCommand(value, context);
|
|
379
|
+
if (result.kind === 'exit') {
|
|
380
|
+
requestExit(undefined, value);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (result.kind === 'new-session') {
|
|
384
|
+
const previousUsage = formatTokenUsage(usageTotals, language);
|
|
385
|
+
const nextSessionId = createNewSession(cwd);
|
|
386
|
+
setSessionId(nextSessionId);
|
|
387
|
+
sessionPermissionRules.current = readSessionPermissionRules(cwd, nextSessionId);
|
|
388
|
+
setUsageTotals(createEmptyTokenUsage());
|
|
389
|
+
setContextRemainingPercent(estimateInitialContextRemainingPercent(cwd, nextSessionId));
|
|
390
|
+
setFixedPlanItems(undefined);
|
|
391
|
+
updateQueuedPrompts(() => []);
|
|
392
|
+
setMessages([{ kind: 'user', text: value }, { kind: 'session-switch', previousUsage, sessionId: nextSessionId, label: t(language, 'session.new') }]);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (result.kind === 'prompt') {
|
|
396
|
+
chatInput = result.prompt;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
appendMessage({ kind: 'user', text: value });
|
|
400
|
+
if (result.kind === 'style') {
|
|
401
|
+
setResponseStyle(result.styleId);
|
|
402
|
+
saveProjectResponseStyle(cwd, result.styleId);
|
|
403
|
+
appendMessage({ kind: 'system', level: 'info', text: formatResponseStyleChanged(result.styleId, true, language) });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (result.kind === 'syntax-theme') {
|
|
407
|
+
setSyntaxTheme(result.themeId);
|
|
408
|
+
saveProjectSyntaxTheme(cwd, result.themeId);
|
|
409
|
+
appendMessage({ kind: 'system', level: 'info', text: formatSyntaxThemeChanged(result.themeId, true, language) });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (result.kind === 'language') {
|
|
413
|
+
setLanguage(result.language);
|
|
414
|
+
saveProjectLanguage(cwd, result.language);
|
|
415
|
+
appendMessage({ kind: 'system', level: 'info', text: t(result.language, 'language.saved', { label: languageLabel(result.language) }) });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (result.kind === 'permission-profile') {
|
|
419
|
+
setPermissionProfile(result.profile);
|
|
420
|
+
saveProjectPermissionProfile(cwd, result.profile);
|
|
421
|
+
appendMessage(result.message);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (result.kind === 'compact') {
|
|
425
|
+
if (!sessionId) {
|
|
426
|
+
appendMessage({ kind: 'system', level: 'warn', text: t(language, 'compact.noActive') });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
setIsSubmitting(true);
|
|
430
|
+
setWorkingStartedAt(Date.now());
|
|
431
|
+
setProgressLabelOverride(t(language, 'compact.progress'));
|
|
432
|
+
try {
|
|
433
|
+
await new Promise(resolve => setTimeout(resolve, compactProgressDelayMs));
|
|
434
|
+
const compactResult = compactSession(cwd, sessionId);
|
|
435
|
+
appendMessage({
|
|
436
|
+
kind: 'compact-boundary',
|
|
437
|
+
summary: t(language, 'compact.done', {
|
|
438
|
+
count: compactResult.sourceEventCount,
|
|
439
|
+
tokens: compactResult.estimatedTokens,
|
|
440
|
+
}),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
appendMessage({
|
|
445
|
+
kind: 'system',
|
|
446
|
+
level: 'error',
|
|
447
|
+
text: error instanceof Error ? error.message : String(error),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
setIsSubmitting(false);
|
|
452
|
+
setWorkingStartedAt(undefined);
|
|
453
|
+
setProgressLabelOverride(undefined);
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
appendMessage(result.message);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const taskStartedAt = Date.now();
|
|
462
|
+
setIsSubmitting(true);
|
|
463
|
+
setWorkingStartedAt(taskStartedAt);
|
|
464
|
+
setProgressLabelOverride(undefined);
|
|
465
|
+
setMessages(current => [...current, { kind: 'user', text: visibleUserInput }]);
|
|
466
|
+
const abortController = new AbortController();
|
|
467
|
+
activeAbortController.current = abortController;
|
|
468
|
+
let pendingAssistantText = '';
|
|
469
|
+
let assistantFlushTimer;
|
|
470
|
+
let lastAssistantFlushAt = 0;
|
|
471
|
+
const flushAssistantText = () => {
|
|
472
|
+
if (assistantFlushTimer) {
|
|
473
|
+
clearTimeout(assistantFlushTimer);
|
|
474
|
+
assistantFlushTimer = undefined;
|
|
475
|
+
}
|
|
476
|
+
if (pendingAssistantText.length === 0) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const text = pendingAssistantText;
|
|
480
|
+
pendingAssistantText = '';
|
|
481
|
+
lastAssistantFlushAt = Date.now();
|
|
482
|
+
setMessages(current => appendAssistantDelta(current, text));
|
|
483
|
+
};
|
|
484
|
+
const scheduleAssistantFlush = (delayMs = assistantStreamFlushIntervalMs) => {
|
|
485
|
+
if (assistantFlushTimer) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
assistantFlushTimer = setTimeout(() => {
|
|
489
|
+
assistantFlushTimer = undefined;
|
|
490
|
+
flushAssistantText();
|
|
491
|
+
}, delayMs);
|
|
492
|
+
};
|
|
493
|
+
const queueAssistantText = (text) => {
|
|
494
|
+
pendingAssistantText += text;
|
|
495
|
+
const now = Date.now();
|
|
496
|
+
const elapsedSinceFlush = lastAssistantFlushAt === 0 ? Infinity : now - lastAssistantFlushAt;
|
|
497
|
+
if (lastAssistantFlushAt === 0 ||
|
|
498
|
+
elapsedSinceFlush >= assistantStreamFlushIntervalMs ||
|
|
499
|
+
pendingAssistantText.length >= assistantStreamImmediateFlushChars ||
|
|
500
|
+
text.includes('\n')) {
|
|
501
|
+
flushAssistantText();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
scheduleAssistantFlush(Math.max(1, assistantStreamFlushIntervalMs - elapsedSinceFlush));
|
|
505
|
+
};
|
|
506
|
+
let sawUsageEvent = false;
|
|
507
|
+
let activeContextWindowTokens;
|
|
508
|
+
try {
|
|
509
|
+
const chatConfig = {
|
|
510
|
+
cwd,
|
|
511
|
+
sessionId,
|
|
512
|
+
responseStyle,
|
|
513
|
+
permissionProfile,
|
|
514
|
+
requestToolPermission,
|
|
515
|
+
permissionRules: () => sessionPermissionRules.current,
|
|
516
|
+
signal: abortController.signal,
|
|
517
|
+
...(mainModelAlias ? { modelOverrides: { main: mainModelAlias } } : {}),
|
|
518
|
+
};
|
|
519
|
+
for await (const event of chatRunner(chatInput, chatConfig)) {
|
|
520
|
+
if (event.type === 'start') {
|
|
521
|
+
setSessionId(event.sessionId);
|
|
522
|
+
activeContextWindowTokens = event.contextTrace?.contextWindowTokens;
|
|
523
|
+
if (event.contextTrace?.remainingPercent !== undefined) {
|
|
524
|
+
setContextRemainingPercent(event.contextTrace.remainingPercent);
|
|
525
|
+
}
|
|
526
|
+
setMessages(current => [
|
|
527
|
+
...current,
|
|
528
|
+
{ kind: 'assistant', text: '', streaming: true },
|
|
529
|
+
]);
|
|
530
|
+
}
|
|
531
|
+
else if (event.type === 'compact_start') {
|
|
532
|
+
setWorkingStartedAt(Date.now());
|
|
533
|
+
setProgressLabelOverride(t(language, 'compact.progress'));
|
|
534
|
+
}
|
|
535
|
+
else if (event.type === 'compact') {
|
|
536
|
+
setMessages(current => [
|
|
537
|
+
...finalizeStreamingMessages(current),
|
|
538
|
+
{
|
|
539
|
+
kind: 'compact-boundary',
|
|
540
|
+
summary: t(language, 'compact.done', {
|
|
541
|
+
count: event.sourceEventCount,
|
|
542
|
+
tokens: event.estimatedTokens,
|
|
543
|
+
}),
|
|
544
|
+
},
|
|
545
|
+
]);
|
|
546
|
+
}
|
|
547
|
+
else if (event.type === 'compact_end') {
|
|
548
|
+
setProgressLabelOverride(undefined);
|
|
549
|
+
}
|
|
550
|
+
else if (event.type === 'skill_use') {
|
|
551
|
+
setMessages(current => [
|
|
552
|
+
...finalizeStreamingMessages(current),
|
|
553
|
+
{
|
|
554
|
+
kind: 'skill-use',
|
|
555
|
+
name: event.name,
|
|
556
|
+
displayName: event.displayName,
|
|
557
|
+
source: event.source,
|
|
558
|
+
filePath: event.filePath,
|
|
559
|
+
},
|
|
560
|
+
]);
|
|
561
|
+
}
|
|
562
|
+
else if (event.type === 'delta') {
|
|
563
|
+
setWorkingStartedAt(undefined);
|
|
564
|
+
queueAssistantText(event.text);
|
|
565
|
+
}
|
|
566
|
+
else if (event.type === 'tool_call') {
|
|
567
|
+
setWorkingStartedAt(undefined);
|
|
568
|
+
flushAssistantText();
|
|
569
|
+
setMessages(current => [
|
|
570
|
+
...finalizeStreamingMessages(current),
|
|
571
|
+
{ kind: 'tool-call', callId: event.callId, toolName: event.toolName, summary: event.summary, state: 'running', startedAt: Date.now() },
|
|
572
|
+
]);
|
|
573
|
+
}
|
|
574
|
+
else if (event.type === 'tool_result') {
|
|
575
|
+
setWorkingStartedAt(Date.now());
|
|
576
|
+
flushAssistantText();
|
|
577
|
+
if (event.recoverable) {
|
|
578
|
+
setMessages(current => removeMatchingToolCall(finalizeStreamingMessages(current), event.callId));
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
setMessages(current => replaceToolCallWithResult(finalizeStreamingMessages(current), {
|
|
582
|
+
kind: 'tool-result',
|
|
583
|
+
callId: event.callId,
|
|
584
|
+
toolName: event.toolName,
|
|
585
|
+
summary: event.summary,
|
|
586
|
+
...(event.detail ? { detail: event.detail } : {}),
|
|
587
|
+
state: event.ok ? 'done' : 'failed',
|
|
588
|
+
}));
|
|
589
|
+
if (queuedPromptsRef.current.length > 0 && !abortController.signal.aborted) {
|
|
590
|
+
abortController.abort(new Error(queuedPromptAbortReason));
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else if (event.type === 'plan_update') {
|
|
595
|
+
setWorkingStartedAt(Date.now());
|
|
596
|
+
flushAssistantText();
|
|
597
|
+
setFixedPlanItems(event.items.length > 0 ? event.items : undefined);
|
|
598
|
+
setMessages(current => removeMatchingToolCall(finalizeStreamingMessages(current), event.callId));
|
|
599
|
+
}
|
|
600
|
+
else if (event.type === 'usage') {
|
|
601
|
+
sawUsageEvent = true;
|
|
602
|
+
setUsageTotals(current => addTokenUsage(current, event.usage));
|
|
603
|
+
const remaining = contextRemainingPercentFromUsage(event.usage, activeContextWindowTokens);
|
|
604
|
+
if (remaining !== undefined) {
|
|
605
|
+
setContextRemainingPercent(remaining);
|
|
606
|
+
}
|
|
607
|
+
onTokenUsage?.(event.usage);
|
|
608
|
+
}
|
|
609
|
+
else if (event.type === 'done') {
|
|
610
|
+
setWorkingStartedAt(undefined);
|
|
611
|
+
flushAssistantText();
|
|
612
|
+
setFixedPlanItems(current => (isCompletedPlan(current) ? undefined : current));
|
|
613
|
+
if (event.usage && !sawUsageEvent) {
|
|
614
|
+
const usage = event.usage;
|
|
615
|
+
setUsageTotals(current => addTokenUsage(current, usage));
|
|
616
|
+
const remaining = contextRemainingPercentFromUsage(usage, activeContextWindowTokens);
|
|
617
|
+
if (remaining !== undefined) {
|
|
618
|
+
setContextRemainingPercent(remaining);
|
|
619
|
+
}
|
|
620
|
+
onTokenUsage?.(usage);
|
|
621
|
+
}
|
|
622
|
+
setMessages(current => [
|
|
623
|
+
...current.map(message => (message.kind === 'assistant' ? { ...message, streaming: false } : message)),
|
|
624
|
+
taskSummaryMessage(taskStartedAt, language),
|
|
625
|
+
]);
|
|
626
|
+
}
|
|
627
|
+
else if (event.type === 'error') {
|
|
628
|
+
setWorkingStartedAt(undefined);
|
|
629
|
+
flushAssistantText();
|
|
630
|
+
const wasInterrupted = abortController.signal.aborted || event.message === 'Interrupted by user';
|
|
631
|
+
if (!isQueuedPromptDispatchAbort(abortController.signal, event.message) && wasInterrupted) {
|
|
632
|
+
setFixedPlanItems(undefined);
|
|
633
|
+
}
|
|
634
|
+
setMessages(current => {
|
|
635
|
+
const finalizedMessages = finalizeStreamingMessages(current);
|
|
636
|
+
if (isQueuedPromptDispatchAbort(abortController.signal, event.message)) {
|
|
637
|
+
return finalizedMessages;
|
|
638
|
+
}
|
|
639
|
+
const terminalMessage = wasInterrupted
|
|
640
|
+
? interruptedMessage(language)
|
|
641
|
+
: { kind: 'system', level: 'error', text: event.message };
|
|
642
|
+
return [...finalizedMessages, terminalMessage, taskSummaryMessage(taskStartedAt, language)];
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
649
|
+
flushAssistantText();
|
|
650
|
+
const wasInterrupted = abortController.signal.aborted || message === 'Interrupted by user';
|
|
651
|
+
if (!isQueuedPromptDispatchAbort(abortController.signal, message) && wasInterrupted) {
|
|
652
|
+
setFixedPlanItems(undefined);
|
|
653
|
+
}
|
|
654
|
+
setMessages(current => {
|
|
655
|
+
const finalizedMessages = finalizeStreamingMessages(current);
|
|
656
|
+
if (isQueuedPromptDispatchAbort(abortController.signal, message)) {
|
|
657
|
+
return finalizedMessages;
|
|
658
|
+
}
|
|
659
|
+
const terminalMessage = wasInterrupted
|
|
660
|
+
? interruptedMessage(language)
|
|
661
|
+
: { kind: 'system', level: 'error', text: message };
|
|
662
|
+
return [...finalizedMessages, terminalMessage, taskSummaryMessage(taskStartedAt, language)];
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
flushAssistantText();
|
|
667
|
+
if (activeAbortController.current === abortController) {
|
|
668
|
+
activeAbortController.current = undefined;
|
|
669
|
+
}
|
|
670
|
+
setIsSubmitting(false);
|
|
671
|
+
setWorkingStartedAt(undefined);
|
|
672
|
+
const queuedText = consumeQueuedPromptText();
|
|
673
|
+
if (queuedText) {
|
|
674
|
+
setPendingQueuedDispatch(buildQueuedPromptDispatch(visibleUserInput, queuedText));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}, [
|
|
678
|
+
appendMessage,
|
|
679
|
+
chatRunner,
|
|
680
|
+
cwd,
|
|
681
|
+
exit,
|
|
682
|
+
input,
|
|
683
|
+
isSubmitting,
|
|
684
|
+
language,
|
|
685
|
+
mainModelAlias,
|
|
686
|
+
onTokenUsage,
|
|
687
|
+
permissionProfile,
|
|
688
|
+
consumeQueuedPromptText,
|
|
689
|
+
requestToolPermission,
|
|
690
|
+
requestExit,
|
|
691
|
+
responseStyle,
|
|
692
|
+
sessionId,
|
|
693
|
+
statusLineItems,
|
|
694
|
+
syntaxTheme,
|
|
695
|
+
updateQueuedPrompts,
|
|
696
|
+
usageTotals,
|
|
697
|
+
]);
|
|
698
|
+
useEffect(() => {
|
|
699
|
+
if (!pendingQueuedDispatch || isSubmitting) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const queuedInput = pendingQueuedDispatch;
|
|
703
|
+
setPendingQueuedDispatch(undefined);
|
|
704
|
+
void submit(queuedInput);
|
|
705
|
+
}, [isSubmitting, pendingQueuedDispatch, submit]);
|
|
706
|
+
const updateModelPanelSelection = useCallback((delta) => {
|
|
707
|
+
if (!hasActivePickerPanel(messages, input, 'model-panel', panel => panel.models)) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
setMessages(current => updatePickerPanelSelectionInMessages(current, 'model-panel', delta, panel => panel.models));
|
|
711
|
+
return true;
|
|
712
|
+
}, [input, messages]);
|
|
713
|
+
const updateStylePanelSelection = useCallback((delta) => {
|
|
714
|
+
if (!hasActivePickerPanel(messages, input, 'style-panel', panel => panel.styles)) {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
setMessages(current => updatePickerPanelSelectionInMessages(current, 'style-panel', delta, panel => panel.styles));
|
|
718
|
+
return true;
|
|
719
|
+
}, [input, messages]);
|
|
720
|
+
const updateSyntaxThemePanelSelection = useCallback((delta) => {
|
|
721
|
+
if (!hasActivePickerPanel(messages, input, 'syntax-theme-panel', visibleSyntaxThemePanelItems)) {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
setMessages(current => updatePickerPanelSelectionInMessages(current, 'syntax-theme-panel', delta, visibleSyntaxThemePanelItems));
|
|
725
|
+
return true;
|
|
726
|
+
}, [input, messages]);
|
|
727
|
+
const updateLanguagePanelSelection = useCallback((delta) => {
|
|
728
|
+
if (!hasActivePickerPanel(messages, input, 'language-panel', panel => panel.languages)) {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
setMessages(current => updatePickerPanelSelectionInMessages(current, 'language-panel', delta, panel => panel.languages));
|
|
732
|
+
return true;
|
|
733
|
+
}, [input, messages]);
|
|
734
|
+
const updatePermissionProfilePanelSelection = useCallback((delta) => {
|
|
735
|
+
if (!hasActivePickerPanel(messages, input, 'permission-profile-panel', panel => panel.profiles)) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
setMessages(current => updatePickerPanelSelectionInMessages(current, 'permission-profile-panel', delta, panel => panel.profiles));
|
|
739
|
+
return true;
|
|
740
|
+
}, [input, messages]);
|
|
741
|
+
const updatePermissionRequestSelection = useCallback((delta) => {
|
|
742
|
+
if (!hasActivePickerPanel(messages, '', 'permission-request', panel => panel.options, { requireEmptyInput: false })) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
setMessages(current => updatePickerPanelSelectionInMessages(current, 'permission-request', delta, panel => panel.options));
|
|
746
|
+
return true;
|
|
747
|
+
}, [messages]);
|
|
748
|
+
const updateStatusLinePanelSelection = useCallback((delta) => {
|
|
749
|
+
if (!hasActivePickerPanel(messages, input, 'statusline-panel', visibleStatusLinePanelItems)) {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
setMessages(current => updatePickerPanelSelectionInMessages(current, 'statusline-panel', delta, visibleStatusLinePanelItems));
|
|
753
|
+
return true;
|
|
754
|
+
}, [input, messages]);
|
|
755
|
+
const updateSkillsPanelSelection = useCallback((delta) => {
|
|
756
|
+
if (!hasActivePickerPanel(messages, input, 'skills-panel', skillsPanelItems)) {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
setMessages(current => updatePickerPanelSelectionInMessages(current, 'skills-panel', delta, skillsPanelItems));
|
|
760
|
+
return true;
|
|
761
|
+
}, [input, messages]);
|
|
762
|
+
const navigateInputHistory = useCallback((delta) => {
|
|
763
|
+
if (inputHistory.length === 0) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
if (delta < 0) {
|
|
767
|
+
const nextCursor = historyCursor === undefined ? inputHistory.length - 1 : Math.max(0, historyCursor - 1);
|
|
768
|
+
if (historyCursor === undefined) {
|
|
769
|
+
setHistoryDraft(input);
|
|
770
|
+
}
|
|
771
|
+
const nextInput = inputHistory[nextCursor] ?? '';
|
|
772
|
+
setHistoryCursor(nextCursor);
|
|
773
|
+
setInput(nextInput);
|
|
774
|
+
setInputCursor(inputLength(nextInput));
|
|
775
|
+
setSelectedCommandIndex(0);
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
if (historyCursor === undefined) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
const nextCursor = historyCursor + 1;
|
|
782
|
+
if (nextCursor >= inputHistory.length) {
|
|
783
|
+
setHistoryCursor(undefined);
|
|
784
|
+
setInput(historyDraft);
|
|
785
|
+
setInputCursor(inputLength(historyDraft));
|
|
786
|
+
setHistoryDraft('');
|
|
787
|
+
setSelectedCommandIndex(0);
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
const nextInput = inputHistory[nextCursor] ?? '';
|
|
791
|
+
setHistoryCursor(nextCursor);
|
|
792
|
+
setInput(nextInput);
|
|
793
|
+
setInputCursor(inputLength(nextInput));
|
|
794
|
+
setSelectedCommandIndex(0);
|
|
795
|
+
return true;
|
|
796
|
+
}, [historyCursor, historyDraft, input, inputHistory]);
|
|
797
|
+
const resetInputHistoryNavigation = useCallback(() => {
|
|
798
|
+
setHistoryCursor(undefined);
|
|
799
|
+
setHistoryDraft('');
|
|
800
|
+
}, []);
|
|
801
|
+
const selectModelFromPanel = useCallback(() => {
|
|
802
|
+
const selection = selectedPickerPanelItem(messages, input, 'model-panel', panel => panel.models);
|
|
803
|
+
const selectedModel = selection?.item;
|
|
804
|
+
if (!selectedModel) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
setMainModelAlias(selectedModel.alias);
|
|
808
|
+
saveProjectMainModel(cwd, selectedModel.alias);
|
|
809
|
+
setMessages(current => {
|
|
810
|
+
const currentPanelIndex = findLastModelPanelIndex(current);
|
|
811
|
+
const currentPanel = current[currentPanelIndex];
|
|
812
|
+
if (currentPanel?.kind !== 'model-panel') {
|
|
813
|
+
return current;
|
|
814
|
+
}
|
|
815
|
+
const next = [...current];
|
|
816
|
+
next.splice(currentPanelIndex, 1);
|
|
817
|
+
next.push({
|
|
818
|
+
kind: 'system',
|
|
819
|
+
level: 'info',
|
|
820
|
+
text: t(language, 'model.selected', { label: selectedModel.displayName }),
|
|
821
|
+
});
|
|
822
|
+
return next;
|
|
823
|
+
});
|
|
824
|
+
return true;
|
|
825
|
+
}, [cwd, input, language, messages]);
|
|
826
|
+
const selectStyleFromPanel = useCallback(() => {
|
|
827
|
+
const selection = selectedPickerPanelItem(messages, input, 'style-panel', panel => panel.styles);
|
|
828
|
+
const selectedStyle = selection?.item;
|
|
829
|
+
if (!selectedStyle) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
const styleId = selectedStyle.id;
|
|
833
|
+
setResponseStyle(styleId);
|
|
834
|
+
saveProjectResponseStyle(cwd, styleId);
|
|
835
|
+
setMessages(current => {
|
|
836
|
+
const currentPanelIndex = findLastStylePanelIndex(current);
|
|
837
|
+
const currentPanel = current[currentPanelIndex];
|
|
838
|
+
if (currentPanel?.kind !== 'style-panel') {
|
|
839
|
+
return current;
|
|
840
|
+
}
|
|
841
|
+
const next = [...current];
|
|
842
|
+
next.splice(currentPanelIndex, 1);
|
|
843
|
+
next.push({ kind: 'system', level: 'info', text: formatResponseStyleChanged(styleId, true, language) });
|
|
844
|
+
return next;
|
|
845
|
+
});
|
|
846
|
+
return true;
|
|
847
|
+
}, [cwd, input, language, messages]);
|
|
848
|
+
const selectSyntaxThemeFromPanel = useCallback(() => {
|
|
849
|
+
const selection = selectedPickerPanelItem(messages, input, 'syntax-theme-panel', visibleSyntaxThemePanelItems);
|
|
850
|
+
const selectedTheme = selection?.item;
|
|
851
|
+
if (!selectedTheme) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
setSyntaxTheme(selectedTheme.id);
|
|
855
|
+
saveProjectSyntaxTheme(cwd, selectedTheme.id);
|
|
856
|
+
setMessages(current => {
|
|
857
|
+
const currentPanelIndex = findLastSyntaxThemePanelIndex(current);
|
|
858
|
+
const currentPanel = current[currentPanelIndex];
|
|
859
|
+
if (currentPanel?.kind !== 'syntax-theme-panel') {
|
|
860
|
+
return current;
|
|
861
|
+
}
|
|
862
|
+
const next = [...current];
|
|
863
|
+
next.splice(currentPanelIndex, 1);
|
|
864
|
+
next.push({ kind: 'system', level: 'info', text: formatSyntaxThemeChanged(selectedTheme.id, true, language) });
|
|
865
|
+
return next;
|
|
866
|
+
});
|
|
867
|
+
return true;
|
|
868
|
+
}, [cwd, input, language, messages]);
|
|
869
|
+
const selectLanguageFromPanel = useCallback(() => {
|
|
870
|
+
const selection = selectedPickerPanelItem(messages, input, 'language-panel', panel => panel.languages);
|
|
871
|
+
const selectedLanguage = selection?.item;
|
|
872
|
+
if (!selectedLanguage) {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
setLanguage(selectedLanguage.code);
|
|
876
|
+
saveProjectLanguage(cwd, selectedLanguage.code);
|
|
877
|
+
setMessages(current => {
|
|
878
|
+
const currentPanelIndex = findLastLanguagePanelIndex(current);
|
|
879
|
+
const currentPanel = current[currentPanelIndex];
|
|
880
|
+
if (currentPanel?.kind !== 'language-panel') {
|
|
881
|
+
return current;
|
|
882
|
+
}
|
|
883
|
+
const next = [...current];
|
|
884
|
+
next.splice(currentPanelIndex, 1);
|
|
885
|
+
next.push({
|
|
886
|
+
kind: 'system',
|
|
887
|
+
level: 'info',
|
|
888
|
+
text: t(selectedLanguage.code, 'language.saved', { label: languageLabel(selectedLanguage.code) }),
|
|
889
|
+
});
|
|
890
|
+
return next;
|
|
891
|
+
});
|
|
892
|
+
return true;
|
|
893
|
+
}, [cwd, input, messages]);
|
|
894
|
+
const selectPermissionProfileFromPanel = useCallback(() => {
|
|
895
|
+
const selection = selectedPickerPanelItem(messages, input, 'permission-profile-panel', panel => panel.profiles);
|
|
896
|
+
const selectedProfile = selection?.item;
|
|
897
|
+
if (!selectedProfile) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
setPermissionProfile(selectedProfile.id);
|
|
901
|
+
saveProjectPermissionProfile(cwd, selectedProfile.id);
|
|
902
|
+
setMessages(current => {
|
|
903
|
+
const currentPanelIndex = findLastPermissionProfilePanelIndex(current);
|
|
904
|
+
const currentPanel = current[currentPanelIndex];
|
|
905
|
+
if (currentPanel?.kind !== 'permission-profile-panel') {
|
|
906
|
+
return current;
|
|
907
|
+
}
|
|
908
|
+
const next = [...current];
|
|
909
|
+
next.splice(currentPanelIndex, 1);
|
|
910
|
+
next.push({ kind: 'system', level: 'info', text: formatPermissionProfileChanged(selectedProfile.id, language) });
|
|
911
|
+
return next;
|
|
912
|
+
});
|
|
913
|
+
return true;
|
|
914
|
+
}, [cwd, input, language, messages]);
|
|
915
|
+
const selectSkillsPanelAction = useCallback(() => {
|
|
916
|
+
const panelIndex = findLastSkillsPanelIndex(messages);
|
|
917
|
+
const panel = messages[panelIndex];
|
|
918
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || input.trim().length > 0 || panel?.kind !== 'skills-panel') {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
if (panel.mode === 'actions') {
|
|
922
|
+
const selectedAction = panel.actions[normalizeIndex(panel.selectedIndex ?? 0, panel.actions.length)];
|
|
923
|
+
if (!selectedAction) {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
setMessages(current => {
|
|
927
|
+
const currentPanelIndex = findLastSkillsPanelIndex(current);
|
|
928
|
+
const next = [...current];
|
|
929
|
+
if (next[currentPanelIndex]?.kind === 'skills-panel') {
|
|
930
|
+
next[currentPanelIndex] = createSkillsListPanelMessage(cwd, language, selectedAction.id === 'toggle' ? 'toggle' : 'list');
|
|
931
|
+
}
|
|
932
|
+
return next;
|
|
933
|
+
});
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
if (panel.mode === 'toggle') {
|
|
937
|
+
const disabled = panel.skills.filter(skill => !skill.enabled).map(skill => skill.name);
|
|
938
|
+
saveProjectDisabledSkills(cwd, disabled);
|
|
939
|
+
setMessages(current => {
|
|
940
|
+
const currentPanelIndex = findLastSkillsPanelIndex(current);
|
|
941
|
+
const next = [...current];
|
|
942
|
+
if (next[currentPanelIndex]?.kind === 'skills-panel') {
|
|
943
|
+
next.splice(currentPanelIndex, 1);
|
|
944
|
+
}
|
|
945
|
+
next.push({
|
|
946
|
+
kind: 'system',
|
|
947
|
+
level: 'info',
|
|
948
|
+
text: t(language, 'skills.saved', { enabled: panel.skills.length - disabled.length, disabled: disabled.length }),
|
|
949
|
+
});
|
|
950
|
+
return next;
|
|
951
|
+
});
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
setMessages(current => {
|
|
955
|
+
const currentPanelIndex = findLastSkillsPanelIndex(current);
|
|
956
|
+
const next = [...current];
|
|
957
|
+
if (next[currentPanelIndex]?.kind === 'skills-panel') {
|
|
958
|
+
next.splice(currentPanelIndex, 1);
|
|
959
|
+
}
|
|
960
|
+
next.push({ kind: 'system', level: 'info', text: t(language, 'skills.closed') });
|
|
961
|
+
return next;
|
|
962
|
+
});
|
|
963
|
+
return true;
|
|
964
|
+
}, [cwd, input, language, messages]);
|
|
965
|
+
const toggleSkillsPanelItem = useCallback(() => {
|
|
966
|
+
const panelIndex = findLastSkillsPanelIndex(messages);
|
|
967
|
+
const panel = messages[panelIndex];
|
|
968
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || input.trim().length > 0 || panel?.kind !== 'skills-panel' || panel.mode !== 'toggle') {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
const selectedSkill = panel.skills[normalizeIndex(panel.selectedIndex ?? 0, panel.skills.length)];
|
|
972
|
+
if (!selectedSkill) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
setMessages(current => {
|
|
976
|
+
const currentPanelIndex = findLastSkillsPanelIndex(current);
|
|
977
|
+
const currentPanel = current[currentPanelIndex];
|
|
978
|
+
if (currentPanel?.kind !== 'skills-panel' || currentPanel.mode !== 'toggle') {
|
|
979
|
+
return current;
|
|
980
|
+
}
|
|
981
|
+
const next = [...current];
|
|
982
|
+
next[currentPanelIndex] = {
|
|
983
|
+
...currentPanel,
|
|
984
|
+
skills: currentPanel.skills.map(skill => (skill.name === selectedSkill.name ? { ...skill, enabled: !skill.enabled } : skill)),
|
|
985
|
+
};
|
|
986
|
+
return next;
|
|
987
|
+
});
|
|
988
|
+
return true;
|
|
989
|
+
}, [input, messages]);
|
|
990
|
+
const closeSkillsPanel = useCallback(() => {
|
|
991
|
+
const panelIndex = findLastSkillsPanelIndex(messages);
|
|
992
|
+
const panel = messages[panelIndex];
|
|
993
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || input.trim().length > 0 || panel?.kind !== 'skills-panel') {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
setMessages(current => {
|
|
997
|
+
const currentPanelIndex = findLastSkillsPanelIndex(current);
|
|
998
|
+
const next = [...current];
|
|
999
|
+
if (next[currentPanelIndex]?.kind === 'skills-panel') {
|
|
1000
|
+
next.splice(currentPanelIndex, 1);
|
|
1001
|
+
}
|
|
1002
|
+
next.push({ kind: 'system', level: 'info', text: t(language, 'skills.closed') });
|
|
1003
|
+
return next;
|
|
1004
|
+
});
|
|
1005
|
+
return true;
|
|
1006
|
+
}, [input, language, messages]);
|
|
1007
|
+
const resolvePermissionRequest = useCallback((optionId) => {
|
|
1008
|
+
const panelIndex = findLastPermissionRequestIndex(messages);
|
|
1009
|
+
const panel = messages[panelIndex];
|
|
1010
|
+
const pending = pendingPermission.current;
|
|
1011
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || panel?.kind !== 'permission-request' || !pending) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
const option = optionId ? panel.options.find(candidate => candidate.id === optionId) : panel.options[normalizeIndex(panel.selectedIndex ?? 0, panel.options.length)];
|
|
1015
|
+
if (!option) {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
pendingPermission.current = undefined;
|
|
1019
|
+
if (option.id === 'allow-session' && option.rules?.length) {
|
|
1020
|
+
sessionPermissionRules.current = addSessionPermissionRules(sessionPermissionRules.current, cwd, option.rules);
|
|
1021
|
+
writeSessionPermissionRules(cwd, sessionId, sessionPermissionRules.current);
|
|
1022
|
+
}
|
|
1023
|
+
else if (option.id === 'allow-session' && option.prefixRule) {
|
|
1024
|
+
sessionPermissionRules.current = addSessionPermissionRules(sessionPermissionRules.current, cwd, [
|
|
1025
|
+
{ kind: 'shell-prefix', prefix: option.prefixRule },
|
|
1026
|
+
]);
|
|
1027
|
+
writeSessionPermissionRules(cwd, sessionId, sessionPermissionRules.current);
|
|
1028
|
+
}
|
|
1029
|
+
else if (option.id === 'allow-persist' && option.rules?.length) {
|
|
1030
|
+
for (const rule of option.rules) {
|
|
1031
|
+
saveProjectPermissionRule(cwd, rule);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
else if (option.id === 'allow-persist' && option.prefixRule) {
|
|
1035
|
+
saveProjectPermissionRule(cwd, option.prefixRule);
|
|
1036
|
+
}
|
|
1037
|
+
pending.resolve(option.id === 'deny'
|
|
1038
|
+
? { status: 'deny', reason: 'User denied the permission request.', requirements: pending.decision.requirements }
|
|
1039
|
+
: {
|
|
1040
|
+
status: 'allow',
|
|
1041
|
+
reason: option.id === 'allow-session'
|
|
1042
|
+
? 'User approved and allowed this rule for the current session.'
|
|
1043
|
+
: option.id === 'allow-persist'
|
|
1044
|
+
? 'User approved and saved a persistent permission rule.'
|
|
1045
|
+
: 'User approved this tool call once.',
|
|
1046
|
+
requirements: pending.decision.requirements,
|
|
1047
|
+
});
|
|
1048
|
+
setMessages(current => {
|
|
1049
|
+
const currentPanelIndex = findLastPermissionRequestIndex(current);
|
|
1050
|
+
const next = [...current];
|
|
1051
|
+
if (currentPanelIndex >= 0 && next[currentPanelIndex]?.kind === 'permission-request') {
|
|
1052
|
+
next.splice(currentPanelIndex, 1);
|
|
1053
|
+
}
|
|
1054
|
+
if (option.id === 'allow-session' || option.id === 'allow-persist') {
|
|
1055
|
+
const rules = option.rules?.length ? option.rules : option.prefixRule ? [{ kind: 'shell-prefix', prefix: option.prefixRule }] : [];
|
|
1056
|
+
if (rules.length > 0) {
|
|
1057
|
+
next.push({
|
|
1058
|
+
kind: 'system',
|
|
1059
|
+
level: 'info',
|
|
1060
|
+
text: t(language, option.id === 'allow-session' ? 'permission.savedRules' : 'permission.savedPersistentRules', {
|
|
1061
|
+
rule: formatPermissionRulesScope(rules, language),
|
|
1062
|
+
}),
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return next;
|
|
1067
|
+
});
|
|
1068
|
+
return true;
|
|
1069
|
+
}, [cwd, language, messages, sessionId]);
|
|
1070
|
+
const closeStatusLinePanel = useCallback(() => {
|
|
1071
|
+
const panelIndex = findLastStatusLinePanelIndex(messages);
|
|
1072
|
+
const panel = messages[panelIndex];
|
|
1073
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || input.trim().length > 0 || panel?.kind !== 'statusline-panel') {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
setMessages(current => {
|
|
1077
|
+
const currentPanelIndex = findLastStatusLinePanelIndex(current);
|
|
1078
|
+
const currentPanel = current[currentPanelIndex];
|
|
1079
|
+
if (currentPanel?.kind !== 'statusline-panel') {
|
|
1080
|
+
return current;
|
|
1081
|
+
}
|
|
1082
|
+
const next = [...current];
|
|
1083
|
+
next.splice(currentPanelIndex, 1);
|
|
1084
|
+
next.push({ kind: 'system', level: 'info', text: t(language, 'statusline.closed') });
|
|
1085
|
+
return next;
|
|
1086
|
+
});
|
|
1087
|
+
return true;
|
|
1088
|
+
}, [input, language, messages]);
|
|
1089
|
+
const closeSyntaxThemePanel = useCallback(() => {
|
|
1090
|
+
const panelIndex = findLastSyntaxThemePanelIndex(messages);
|
|
1091
|
+
const panel = messages[panelIndex];
|
|
1092
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || input.trim().length > 0 || panel?.kind !== 'syntax-theme-panel') {
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
setMessages(current => {
|
|
1096
|
+
const currentPanelIndex = findLastSyntaxThemePanelIndex(current);
|
|
1097
|
+
const currentPanel = current[currentPanelIndex];
|
|
1098
|
+
if (currentPanel?.kind !== 'syntax-theme-panel') {
|
|
1099
|
+
return current;
|
|
1100
|
+
}
|
|
1101
|
+
const next = [...current];
|
|
1102
|
+
next.splice(currentPanelIndex, 1);
|
|
1103
|
+
next.push({ kind: 'system', level: 'info', text: t(language, 'theme.closed') });
|
|
1104
|
+
return next;
|
|
1105
|
+
});
|
|
1106
|
+
return true;
|
|
1107
|
+
}, [input, language, messages]);
|
|
1108
|
+
const updateSyntaxThemePanelQuery = useCallback((change) => {
|
|
1109
|
+
const panelIndex = findLastSyntaxThemePanelIndex(messages);
|
|
1110
|
+
const panel = messages[panelIndex];
|
|
1111
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || input.trim().length > 0 || panel?.kind !== 'syntax-theme-panel') {
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
setMessages(current => {
|
|
1115
|
+
const currentPanelIndex = findLastSyntaxThemePanelIndex(current);
|
|
1116
|
+
const currentPanel = current[currentPanelIndex];
|
|
1117
|
+
if (currentPanel?.kind !== 'syntax-theme-panel') {
|
|
1118
|
+
return current;
|
|
1119
|
+
}
|
|
1120
|
+
const query = change === 'backspace' ? currentPanel.query.slice(0, -1) : `${currentPanel.query}${change}`;
|
|
1121
|
+
const next = [...current];
|
|
1122
|
+
next[currentPanelIndex] = { ...currentPanel, query, selectedIndex: 0 };
|
|
1123
|
+
return next;
|
|
1124
|
+
});
|
|
1125
|
+
return true;
|
|
1126
|
+
}, [input, messages]);
|
|
1127
|
+
const confirmStatusLinePanel = useCallback(() => {
|
|
1128
|
+
const panelIndex = findLastStatusLinePanelIndex(messages);
|
|
1129
|
+
const panel = messages[panelIndex];
|
|
1130
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || input.trim().length > 0 || panel?.kind !== 'statusline-panel') {
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
const enabledItems = panel.items.filter(item => item.enabled).map(item => item.id);
|
|
1134
|
+
const nextItems = enabledItems.length > 0 ? enabledItems : defaultStatusLineItems;
|
|
1135
|
+
const savedText = t(language, 'statusline.saved', { items: formatStatusLineItemLabels(nextItems, language) });
|
|
1136
|
+
setStatusLineItems(nextItems);
|
|
1137
|
+
saveProjectStatusLineItems(cwd, nextItems);
|
|
1138
|
+
setMessages(current => {
|
|
1139
|
+
const currentPanelIndex = findLastStatusLinePanelIndex(current);
|
|
1140
|
+
const currentPanel = current[currentPanelIndex];
|
|
1141
|
+
if (currentPanel?.kind !== 'statusline-panel') {
|
|
1142
|
+
return current;
|
|
1143
|
+
}
|
|
1144
|
+
const next = [...current];
|
|
1145
|
+
next.splice(currentPanelIndex, 1);
|
|
1146
|
+
next.push({ kind: 'system', level: 'info', text: savedText });
|
|
1147
|
+
return next;
|
|
1148
|
+
});
|
|
1149
|
+
return true;
|
|
1150
|
+
}, [cwd, input, language, messages]);
|
|
1151
|
+
const toggleStatusLinePanelItem = useCallback(() => {
|
|
1152
|
+
const panelIndex = findLastStatusLinePanelIndex(messages);
|
|
1153
|
+
const panel = messages[panelIndex];
|
|
1154
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || input.trim().length > 0 || panel?.kind !== 'statusline-panel') {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
const selectedItem = visibleStatusLinePanelItems(panel)[normalizeIndex(panel.selectedIndex ?? 0, visibleStatusLinePanelItems(panel).length)];
|
|
1158
|
+
if (!selectedItem) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
setMessages(current => {
|
|
1162
|
+
const currentPanelIndex = findLastStatusLinePanelIndex(current);
|
|
1163
|
+
const currentPanel = current[currentPanelIndex];
|
|
1164
|
+
if (currentPanel?.kind !== 'statusline-panel') {
|
|
1165
|
+
return current;
|
|
1166
|
+
}
|
|
1167
|
+
const next = [...current];
|
|
1168
|
+
next[currentPanelIndex] = {
|
|
1169
|
+
...currentPanel,
|
|
1170
|
+
items: currentPanel.items.map(item => (item.id === selectedItem.id ? { ...item, enabled: !item.enabled } : item)),
|
|
1171
|
+
};
|
|
1172
|
+
return next;
|
|
1173
|
+
});
|
|
1174
|
+
return true;
|
|
1175
|
+
}, [input, messages]);
|
|
1176
|
+
const updateStatusLinePanelQuery = useCallback((change) => {
|
|
1177
|
+
const panelIndex = findLastStatusLinePanelIndex(messages);
|
|
1178
|
+
const panel = messages[panelIndex];
|
|
1179
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || input.trim().length > 0 || panel?.kind !== 'statusline-panel') {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
setMessages(current => {
|
|
1183
|
+
const currentPanelIndex = findLastStatusLinePanelIndex(current);
|
|
1184
|
+
const currentPanel = current[currentPanelIndex];
|
|
1185
|
+
if (currentPanel?.kind !== 'statusline-panel') {
|
|
1186
|
+
return current;
|
|
1187
|
+
}
|
|
1188
|
+
const query = change === 'backspace' ? currentPanel.query.slice(0, -1) : `${currentPanel.query}${change}`;
|
|
1189
|
+
const next = [...current];
|
|
1190
|
+
next[currentPanelIndex] = { ...currentPanel, query, selectedIndex: 0 };
|
|
1191
|
+
return next;
|
|
1192
|
+
});
|
|
1193
|
+
return true;
|
|
1194
|
+
}, [input, messages]);
|
|
1195
|
+
useInput((rawInput, key) => {
|
|
1196
|
+
const commandHintsForInput = visibleCommandHintsForInput(input, activeCommandHints);
|
|
1197
|
+
if (key.ctrl && rawInput === 'c') {
|
|
1198
|
+
if (input.length > 0) {
|
|
1199
|
+
setInput('');
|
|
1200
|
+
setInputCursor(0);
|
|
1201
|
+
setSelectedCommandIndex(0);
|
|
1202
|
+
resetInputHistoryNavigation();
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (isSubmitting) {
|
|
1206
|
+
if (pendingPermission.current) {
|
|
1207
|
+
resolvePermissionRequest('deny');
|
|
1208
|
+
}
|
|
1209
|
+
activeAbortController.current?.abort(new Error('Interrupted by user'));
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
requestExit();
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (key.escape && resolvePermissionRequest('deny')) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (key.upArrow && updatePermissionRequestSelection(-1)) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (key.downArrow && updatePermissionRequestSelection(1)) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (key.return && resolvePermissionRequest()) {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (key.escape && isSubmitting) {
|
|
1228
|
+
const queuedCurrentInput = queueCurrentInput();
|
|
1229
|
+
if (queuedCurrentInput || queuedPromptsRef.current.length > 0) {
|
|
1230
|
+
activeAbortController.current?.abort(new Error(queuedPromptAbortReason));
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
activeAbortController.current?.abort(new Error('Interrupted by user'));
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (key.escape && closeStatusLinePanel()) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (key.escape && closeSyntaxThemePanel()) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (key.escape && closeSkillsPanel()) {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (key.upArrow && updateStatusLinePanelSelection(-1)) {
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (key.downArrow && updateStatusLinePanelSelection(1)) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (key.upArrow && updateSyntaxThemePanelSelection(-1)) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (key.downArrow && updateSyntaxThemePanelSelection(1)) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (key.upArrow && updateSkillsPanelSelection(-1)) {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (key.downArrow && updateSkillsPanelSelection(1)) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (rawInput === ' ' && toggleStatusLinePanelItem()) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (rawInput === ' ' && toggleSkillsPanelItem()) {
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (key.return && confirmStatusLinePanel()) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (key.return && selectSyntaxThemeFromPanel()) {
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if ((key.backspace || key.delete) && updateStatusLinePanelQuery('backspace')) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if ((key.backspace || key.delete) && updateSyntaxThemePanelQuery('backspace')) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (isPrintableInput(rawInput) && updateStatusLinePanelQuery(rawInput)) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (isPrintableInput(rawInput) && updateSyntaxThemePanelQuery(rawInput)) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (key.ctrl && rawInput === 'u') {
|
|
1288
|
+
setInput('');
|
|
1289
|
+
setInputCursor(0);
|
|
1290
|
+
setSelectedCommandIndex(0);
|
|
1291
|
+
resetInputHistoryNavigation();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (key.ctrl && rawInput === 'w') {
|
|
1295
|
+
const next = trimOneWordBeforeCursor(input, inputCursor);
|
|
1296
|
+
setInput(next.value);
|
|
1297
|
+
setInputCursor(next.cursor);
|
|
1298
|
+
resetInputHistoryNavigation();
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (key.leftArrow) {
|
|
1302
|
+
setInputCursor(current => Math.max(0, current - 1));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (key.rightArrow) {
|
|
1306
|
+
setInputCursor(current => Math.min(inputLength(input), current + 1));
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (key.upArrow && commandHintsForInput.length > 0) {
|
|
1310
|
+
setSelectedCommandIndex(current => normalizeIndex(current - 1, commandHintsForInput.length));
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (key.downArrow && commandHintsForInput.length > 0) {
|
|
1314
|
+
setSelectedCommandIndex(current => normalizeIndex(current + 1, commandHintsForInput.length));
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (key.upArrow && updateModelPanelSelection(-1)) {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (key.downArrow && updateModelPanelSelection(1)) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (key.upArrow && updateStylePanelSelection(-1)) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if (key.downArrow && updateStylePanelSelection(1)) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (key.upArrow && updateLanguagePanelSelection(-1)) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (key.downArrow && updateLanguagePanelSelection(1)) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (key.upArrow && updatePermissionProfilePanelSelection(-1)) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (key.downArrow && updatePermissionProfilePanelSelection(1)) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (key.upArrow && navigateInputHistory(-1)) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (key.downArrow && navigateInputHistory(1)) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (key.tab || rawInput === '\t') {
|
|
1348
|
+
const selectedCommand = selectedCommandForInput(input, activeCommandHints, selectedCommandIndex);
|
|
1349
|
+
if (selectedCommand) {
|
|
1350
|
+
const nextInput = `${selectedCommand.name} `;
|
|
1351
|
+
setInput(nextInput);
|
|
1352
|
+
setInputCursor(inputLength(nextInput));
|
|
1353
|
+
setSelectedCommandIndex(0);
|
|
1354
|
+
}
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (key.return) {
|
|
1358
|
+
if (isSubmitting && queueCurrentInput()) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const selectedCommand = selectedCommandForInput(input, activeCommandHints, selectedCommandIndex);
|
|
1362
|
+
if (selectedCommand) {
|
|
1363
|
+
void submit(selectedCommand.name);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (selectModelFromPanel()) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
if (selectStyleFromPanel()) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (selectSyntaxThemeFromPanel()) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (selectLanguageFromPanel()) {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (selectPermissionProfileFromPanel()) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (selectSkillsPanelAction()) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
void submit();
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (key.backspace) {
|
|
1388
|
+
const next = deleteInputBeforeCursor(input, inputCursor);
|
|
1389
|
+
setInput(next.value);
|
|
1390
|
+
setInputCursor(next.cursor);
|
|
1391
|
+
setSelectedCommandIndex(0);
|
|
1392
|
+
resetInputHistoryNavigation();
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (key.delete) {
|
|
1396
|
+
const next = deleteInputAtCursor(input, inputCursor);
|
|
1397
|
+
setInput(next.value);
|
|
1398
|
+
setInputCursor(next.cursor);
|
|
1399
|
+
setSelectedCommandIndex(0);
|
|
1400
|
+
resetInputHistoryNavigation();
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (isPrintableInput(rawInput)) {
|
|
1404
|
+
if (rawInput === '$' && input.length === 0 && !isSubmitting) {
|
|
1405
|
+
appendMessage(createSkillsListPanelMessage(cwd, language, 'list'));
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const next = insertInputText(input, inputCursor, rawInput);
|
|
1409
|
+
setInput(next.value);
|
|
1410
|
+
setInputCursor(next.cursor);
|
|
1411
|
+
setSelectedCommandIndex(0);
|
|
1412
|
+
resetInputHistoryNavigation();
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { status: status, width: viewportWidth }), _jsx(MessageList, { messages: mainMessages, width: viewportWidth, emptyText: fixedPlanItems && fixedPlanItems.length > 0 ? '' : t(language, 'messages.empty'), progress: isSubmitting && workingStartedAt !== undefined
|
|
1416
|
+
? { startedAt: workingStartedAt, label: progressLabelOverride ?? t(language, 'thinking.label'), interruptText: t(language, 'working.interrupt') }
|
|
1417
|
+
: undefined, syntaxTheme: syntaxTheme, language: language, slashCommandHints: commandHints }), queuedPrompts.length > 0 ? _jsx(QueuedPromptList, { prompts: queuedPrompts, language: language }) : null, fixedPlanItems && fixedPlanItems.length > 0 ? (_jsx(PlanMessage, { items: fixedPlanItems, marginTop: mainMessages.length > 0 ? 1 : 0, width: viewportWidth })) : null, bottomPanelMessage ? (_jsx(Box, { marginTop: fixedPlanItems && fixedPlanItems.length > 0 ? 1 : mainMessages.length > 0 ? 1 : 0, children: _jsx(MessageList, { messages: [bottomPanelMessage], width: viewportWidth, emptyText: "", syntaxTheme: syntaxTheme, language: language, slashCommandHints: commandHints }) })) : null, isStatusLinePanelOpen || isSyntaxThemePanelOpen || isPermissionRequestOpen || isSkillsPanelOpen ? null : (_jsx(Box, { marginTop: fixedPlanItems && fixedPlanItems.length > 0 ? 1 : 0, children: _jsx(PromptBox, { input: prompt, width: viewportWidth }) })), _jsx(StatusLine, { status: status, width: viewportWidth })] }));
|
|
1418
|
+
}
|
|
1419
|
+
function visibleCommandHintsForInput(input, commands) {
|
|
1420
|
+
if (!input.startsWith('/') || /\s/.test(input)) {
|
|
1421
|
+
return [];
|
|
1422
|
+
}
|
|
1423
|
+
const query = input.trim();
|
|
1424
|
+
return commands.filter(command => query === '/' || command.name.startsWith(query));
|
|
1425
|
+
}
|
|
1426
|
+
function loadEnabledSkillCount(cwd, disabled) {
|
|
1427
|
+
return loadSkillIndex(disabled ? { cwd, disabled } : { cwd }).skills.filter(skill => skill.enabled).length;
|
|
1428
|
+
}
|
|
1429
|
+
function selectedCommandForInput(input, commands, selectedIndex) {
|
|
1430
|
+
const commandsForInput = visibleCommandHintsForInput(input, commands);
|
|
1431
|
+
if (commandsForInput.length === 0) {
|
|
1432
|
+
return undefined;
|
|
1433
|
+
}
|
|
1434
|
+
return commandsForInput[normalizeIndex(selectedIndex, commandsForInput.length)];
|
|
1435
|
+
}
|
|
1436
|
+
function QueuedPromptList({ prompts, language }) {
|
|
1437
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: remiDarkTheme.text, children: ["\u2022 ", t(language, 'queue.title')] }), _jsxs(Text, { color: remiDarkTheme.muted, children: [" (", t(language, 'queue.hint'), ")"] })] }), prompts.map(prompt => (_jsxs(Box, { children: [_jsx(Text, { color: remiDarkTheme.muted, children: " \u21B3 " }), _jsx(Text, { color: remiDarkTheme.muted, children: prompt.text })] }, prompt.id)))] }));
|
|
1438
|
+
}
|
|
1439
|
+
function normalizeIndex(index, length) {
|
|
1440
|
+
if (length <= 0) {
|
|
1441
|
+
return 0;
|
|
1442
|
+
}
|
|
1443
|
+
return ((index % length) + length) % length;
|
|
1444
|
+
}
|
|
1445
|
+
function hasActivePickerPanel(messages, input, kind, getItems, options = {}) {
|
|
1446
|
+
if ((options.requireEmptyInput ?? true) && input.trim().length > 0) {
|
|
1447
|
+
return false;
|
|
1448
|
+
}
|
|
1449
|
+
const panelIndex = findLastPanelIndexByKind(messages, kind);
|
|
1450
|
+
const panel = messages[panelIndex];
|
|
1451
|
+
return panelIndex === findLastPickerPanelIndex(messages) && panel?.kind === kind && getItems(panel).length > 0;
|
|
1452
|
+
}
|
|
1453
|
+
function selectedPickerPanelItem(messages, input, kind, getItems, options = {}) {
|
|
1454
|
+
if (!hasActivePickerPanel(messages, input, kind, getItems, options)) {
|
|
1455
|
+
return undefined;
|
|
1456
|
+
}
|
|
1457
|
+
const panelIndex = findLastPanelIndexByKind(messages, kind);
|
|
1458
|
+
const panel = messages[panelIndex];
|
|
1459
|
+
const items = getItems(panel);
|
|
1460
|
+
const index = normalizeIndex(panel.selectedIndex ?? 0, items.length);
|
|
1461
|
+
const item = items[index];
|
|
1462
|
+
return item === undefined ? undefined : { panel, item, index };
|
|
1463
|
+
}
|
|
1464
|
+
function updatePickerPanelSelectionInMessages(messages, kind, delta, getItems) {
|
|
1465
|
+
const panelIndex = findLastPanelIndexByKind(messages, kind);
|
|
1466
|
+
const panel = messages[panelIndex];
|
|
1467
|
+
if (panelIndex !== findLastPickerPanelIndex(messages) || panel?.kind !== kind) {
|
|
1468
|
+
return messages;
|
|
1469
|
+
}
|
|
1470
|
+
const items = getItems(panel);
|
|
1471
|
+
if (items.length === 0) {
|
|
1472
|
+
return messages;
|
|
1473
|
+
}
|
|
1474
|
+
const next = [...messages];
|
|
1475
|
+
next[panelIndex] = {
|
|
1476
|
+
...panel,
|
|
1477
|
+
selectedIndex: normalizeIndex((panel.selectedIndex ?? 0) + delta, items.length),
|
|
1478
|
+
};
|
|
1479
|
+
return next;
|
|
1480
|
+
}
|
|
1481
|
+
function findLastPanelIndexByKind(messages, kind) {
|
|
1482
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1483
|
+
if (messages[index]?.kind === kind) {
|
|
1484
|
+
return index;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return -1;
|
|
1488
|
+
}
|
|
1489
|
+
function findActiveBottomPanelIndex(messages) {
|
|
1490
|
+
const panelIndex = findLastPickerPanelIndex(messages);
|
|
1491
|
+
const message = messages[panelIndex];
|
|
1492
|
+
if (message?.kind === 'permission-request' ||
|
|
1493
|
+
message?.kind === 'statusline-panel' ||
|
|
1494
|
+
message?.kind === 'syntax-theme-panel' ||
|
|
1495
|
+
message?.kind === 'skills-panel') {
|
|
1496
|
+
return panelIndex;
|
|
1497
|
+
}
|
|
1498
|
+
return -1;
|
|
1499
|
+
}
|
|
1500
|
+
function isCompletedPlan(items) {
|
|
1501
|
+
return Boolean(items && items.length > 0 && items.every(item => item.status === 'completed'));
|
|
1502
|
+
}
|
|
1503
|
+
function appendAssistantDelta(messages, text) {
|
|
1504
|
+
const next = [...messages];
|
|
1505
|
+
const index = next.length - 1;
|
|
1506
|
+
const currentMessage = next[index];
|
|
1507
|
+
if (currentMessage?.kind === 'assistant') {
|
|
1508
|
+
next[index] = {
|
|
1509
|
+
...currentMessage,
|
|
1510
|
+
text: currentMessage.text + text,
|
|
1511
|
+
streaming: true,
|
|
1512
|
+
};
|
|
1513
|
+
return next;
|
|
1514
|
+
}
|
|
1515
|
+
return [...next, { kind: 'assistant', text, streaming: true }];
|
|
1516
|
+
}
|
|
1517
|
+
function finalizeStreamingMessages(messages) {
|
|
1518
|
+
return messages
|
|
1519
|
+
.filter(message => !(message.kind === 'assistant' && message.text.length === 0))
|
|
1520
|
+
.map(message => (message.kind === 'assistant' ? { ...message, streaming: false } : message));
|
|
1521
|
+
}
|
|
1522
|
+
function removeMatchingToolCall(messages, callId) {
|
|
1523
|
+
const index = findLastMatchingToolCallIndex(messages, callId);
|
|
1524
|
+
if (index < 0) {
|
|
1525
|
+
return messages;
|
|
1526
|
+
}
|
|
1527
|
+
const next = [...messages];
|
|
1528
|
+
next.splice(index, 1);
|
|
1529
|
+
return next;
|
|
1530
|
+
}
|
|
1531
|
+
function replaceToolCallWithResult(messages, result) {
|
|
1532
|
+
const index = findLastMatchingToolCallIndex(messages, result.callId);
|
|
1533
|
+
if (index < 0) {
|
|
1534
|
+
return [...messages, result];
|
|
1535
|
+
}
|
|
1536
|
+
const current = messages[index];
|
|
1537
|
+
const next = [...messages];
|
|
1538
|
+
next[index] = {
|
|
1539
|
+
...result,
|
|
1540
|
+
summary: result.summary || (current?.kind === 'tool-call' ? current.summary : ''),
|
|
1541
|
+
...(current?.kind === 'tool-call' ? { inputSummary: current.summary } : {}),
|
|
1542
|
+
...(result.detail ? { detail: result.detail } : {}),
|
|
1543
|
+
};
|
|
1544
|
+
return next;
|
|
1545
|
+
}
|
|
1546
|
+
function findLastMatchingToolCallIndex(messages, callId) {
|
|
1547
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1548
|
+
const message = messages[index];
|
|
1549
|
+
if (message?.kind !== 'tool-call') {
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
if (!callId || message.callId === callId) {
|
|
1553
|
+
return index;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return -1;
|
|
1557
|
+
}
|
|
1558
|
+
function taskSummaryMessage(startedAt, language) {
|
|
1559
|
+
return { kind: 'task-summary', durationMs: Date.now() - startedAt, label: t(language, 'task.worked', { duration: '' }).trim() };
|
|
1560
|
+
}
|
|
1561
|
+
function interruptedMessage(language) {
|
|
1562
|
+
return { kind: 'interrupted', text: t(language, 'interrupt.message') };
|
|
1563
|
+
}
|
|
1564
|
+
function permissionRequestMessage(requestId, request, decision, language) {
|
|
1565
|
+
const fileAction = formatPermissionFileAction(request, language);
|
|
1566
|
+
const options = [
|
|
1567
|
+
{
|
|
1568
|
+
id: 'allow-once',
|
|
1569
|
+
label: t(language, 'permission.request.once'),
|
|
1570
|
+
},
|
|
1571
|
+
];
|
|
1572
|
+
const explicitSuggestedRules = request.suggestedRules?.length
|
|
1573
|
+
? request.suggestedRules
|
|
1574
|
+
: request.command?.suggestedRules?.length
|
|
1575
|
+
? request.command.suggestedRules
|
|
1576
|
+
: request.command?.suggestedRule
|
|
1577
|
+
? [request.command.suggestedRule]
|
|
1578
|
+
: request.command?.suggestedPrefix
|
|
1579
|
+
? [{ kind: 'shell-prefix', prefix: request.command.suggestedPrefix }]
|
|
1580
|
+
: [];
|
|
1581
|
+
const fallbackSuggestedRules = explicitSuggestedRules.length > 0 ? [] : fallbackFilesystemPermissionRules(request);
|
|
1582
|
+
const suggestedRules = explicitSuggestedRules.length > 0 ? explicitSuggestedRules : fallbackSuggestedRules;
|
|
1583
|
+
const canOfferRule = ((request.canPersist ?? request.command?.canPersist ?? false) || fallbackSuggestedRules.length > 0) && suggestedRules.length > 0;
|
|
1584
|
+
const filesystemRulesOnly = suggestedRules.length > 0 && suggestedRules.every(rule => rule.kind === 'filesystem-write-root');
|
|
1585
|
+
const shellRulesOnly = suggestedRules.length > 0 && suggestedRules.every(rule => rule.kind === 'shell-prefix');
|
|
1586
|
+
if (canOfferRule && filesystemRulesOnly) {
|
|
1587
|
+
const ruleLabel = formatPermissionRulesScope(suggestedRules, language);
|
|
1588
|
+
options.push({
|
|
1589
|
+
id: 'allow-session',
|
|
1590
|
+
label: t(language, 'permission.request.sessionRule', { rule: ruleLabel }),
|
|
1591
|
+
rules: suggestedRules,
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
else if (canOfferRule && shellRulesOnly) {
|
|
1595
|
+
const ruleLabel = formatPermissionRulesScope(suggestedRules, language);
|
|
1596
|
+
const firstShellRule = suggestedRules.find(rule => rule.kind === 'shell-prefix');
|
|
1597
|
+
const prefixRule = request.command?.suggestedPrefix ?? firstShellRule?.prefix;
|
|
1598
|
+
options.push({
|
|
1599
|
+
id: 'allow-persist',
|
|
1600
|
+
label: t(language, 'permission.request.persistRule', { rule: ruleLabel }),
|
|
1601
|
+
rules: suggestedRules,
|
|
1602
|
+
...(prefixRule ? { prefixRule } : {}),
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
options.push({
|
|
1606
|
+
id: 'deny',
|
|
1607
|
+
label: t(language, 'permission.request.deny'),
|
|
1608
|
+
});
|
|
1609
|
+
return {
|
|
1610
|
+
kind: 'permission-request',
|
|
1611
|
+
requestId,
|
|
1612
|
+
title: t(language, fileAction ? 'permission.request.title.action' : 'permission.request.title'),
|
|
1613
|
+
reason: t(language, 'permission.request.reason', { reason: decision.reason }),
|
|
1614
|
+
toolName: request.toolName,
|
|
1615
|
+
summary: request.inputSummary,
|
|
1616
|
+
...(request.targetPath ? { targetPath: request.targetPath } : {}),
|
|
1617
|
+
...(fileAction ? { action: fileAction.action, actionDetail: fileAction.detail } : {}),
|
|
1618
|
+
...(request.command?.commandLine ? { command: request.command.commandLine } : {}),
|
|
1619
|
+
...(request.command?.capability
|
|
1620
|
+
? {
|
|
1621
|
+
capability: {
|
|
1622
|
+
id: request.command.capability.id,
|
|
1623
|
+
risk: request.command.capability.risk,
|
|
1624
|
+
...(request.command.capability.platform ? { platform: request.command.capability.platform } : {}),
|
|
1625
|
+
...(request.command.capability.target ? { target: request.command.capability.target } : {}),
|
|
1626
|
+
...(request.command.capability.packageManager ? { packageManager: request.command.capability.packageManager } : {}),
|
|
1627
|
+
...(request.command.capability.requiresAdmin !== undefined ? { requiresAdmin: request.command.capability.requiresAdmin } : {}),
|
|
1628
|
+
},
|
|
1629
|
+
}
|
|
1630
|
+
: {}),
|
|
1631
|
+
selectedIndex: 0,
|
|
1632
|
+
options,
|
|
1633
|
+
helpText: t(language, 'permission.request.help'),
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
function fallbackFilesystemPermissionRules(request) {
|
|
1637
|
+
const operations = permissionFilesystemOperationsForTool(request.toolName);
|
|
1638
|
+
const targetPath = request.targetPath ?? parsePermissionRequestPath(request.inputSummary);
|
|
1639
|
+
if (operations.length === 0 || !targetPath) {
|
|
1640
|
+
return [];
|
|
1641
|
+
}
|
|
1642
|
+
const root = fallbackFilesystemPermissionRoot(targetPath, request.cwd, request.toolName);
|
|
1643
|
+
return root ? [{ kind: 'filesystem-write-root', root, operations }] : [];
|
|
1644
|
+
}
|
|
1645
|
+
function permissionFilesystemOperationForTool(toolName) {
|
|
1646
|
+
if (toolName === 'create_directory') {
|
|
1647
|
+
return 'create';
|
|
1648
|
+
}
|
|
1649
|
+
if (toolName === 'write_file') {
|
|
1650
|
+
return 'write';
|
|
1651
|
+
}
|
|
1652
|
+
if (toolName === 'edit_file') {
|
|
1653
|
+
return 'edit';
|
|
1654
|
+
}
|
|
1655
|
+
if (toolName === 'delete_file') {
|
|
1656
|
+
return 'delete';
|
|
1657
|
+
}
|
|
1658
|
+
if (toolName === 'delete_directory') {
|
|
1659
|
+
return 'delete';
|
|
1660
|
+
}
|
|
1661
|
+
return undefined;
|
|
1662
|
+
}
|
|
1663
|
+
function permissionFilesystemOperationsForTool(toolName) {
|
|
1664
|
+
const operation = permissionFilesystemOperationForTool(toolName);
|
|
1665
|
+
if (!operation) {
|
|
1666
|
+
return [];
|
|
1667
|
+
}
|
|
1668
|
+
if (operation === 'write' || operation === 'edit') {
|
|
1669
|
+
return ['write', 'edit'];
|
|
1670
|
+
}
|
|
1671
|
+
return [operation];
|
|
1672
|
+
}
|
|
1673
|
+
function fallbackFilesystemPermissionRoot(targetPath, cwd, toolName) {
|
|
1674
|
+
const absolutePath = isAbsolute(targetPath) ? resolve(targetPath) : resolve(cwd, targetPath);
|
|
1675
|
+
const resolvedCwd = resolve(cwd);
|
|
1676
|
+
if (isPathInsideOrEqual(absolutePath, resolvedCwd)) {
|
|
1677
|
+
return resolvedCwd;
|
|
1678
|
+
}
|
|
1679
|
+
const desktop = resolve(homedir(), 'Desktop');
|
|
1680
|
+
if (isPathInsideOrEqual(absolutePath, desktop)) {
|
|
1681
|
+
const rel = relative(desktop, absolutePath);
|
|
1682
|
+
const [project] = rel.split(sep).filter(Boolean);
|
|
1683
|
+
if (project) {
|
|
1684
|
+
return resolve(desktop, project);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return dirname(absolutePath);
|
|
1688
|
+
}
|
|
1689
|
+
function isPathInsideOrEqual(target, root) {
|
|
1690
|
+
const rel = relative(root, target);
|
|
1691
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
1692
|
+
}
|
|
1693
|
+
function parsePermissionRequestPath(inputSummary) {
|
|
1694
|
+
try {
|
|
1695
|
+
const parsed = JSON.parse(inputSummary);
|
|
1696
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1697
|
+
return undefined;
|
|
1698
|
+
}
|
|
1699
|
+
const path = parsed['path'];
|
|
1700
|
+
return typeof path === 'string' && path.trim().length > 0 ? path : undefined;
|
|
1701
|
+
}
|
|
1702
|
+
catch {
|
|
1703
|
+
return undefined;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function visibleStatusLinePanelItems(message) {
|
|
1707
|
+
const query = message.query.trim().toLowerCase();
|
|
1708
|
+
if (!query) {
|
|
1709
|
+
return message.items;
|
|
1710
|
+
}
|
|
1711
|
+
return message.items.filter(item => `${item.label} ${item.description}`.toLowerCase().includes(query));
|
|
1712
|
+
}
|
|
1713
|
+
function statusLineItemsFromPanel(message) {
|
|
1714
|
+
return message.items.filter(item => item.enabled).map(item => item.id);
|
|
1715
|
+
}
|
|
1716
|
+
function visibleSyntaxThemePanelItems(message) {
|
|
1717
|
+
const query = message.query.trim().toLowerCase();
|
|
1718
|
+
if (!query) {
|
|
1719
|
+
return message.themes;
|
|
1720
|
+
}
|
|
1721
|
+
return message.themes.filter(theme => `${theme.id} ${theme.label} ${theme.description}`.toLowerCase().includes(query));
|
|
1722
|
+
}
|
|
1723
|
+
function skillsPanelItems(message) {
|
|
1724
|
+
return message.mode === 'actions' ? message.actions : message.skills;
|
|
1725
|
+
}
|
|
1726
|
+
function formatStatusLineItemLabels(items, language) {
|
|
1727
|
+
const labels = new Map(getStatusLineCatalog(language).map(item => [item.id, item.label]));
|
|
1728
|
+
return items.map(item => labels.get(item) ?? item).join(', ');
|
|
1729
|
+
}
|
|
1730
|
+
function findLastModelPanelIndex(messages) {
|
|
1731
|
+
return findLastPanelIndexByKind(messages, 'model-panel');
|
|
1732
|
+
}
|
|
1733
|
+
function findLastStylePanelIndex(messages) {
|
|
1734
|
+
return findLastPanelIndexByKind(messages, 'style-panel');
|
|
1735
|
+
}
|
|
1736
|
+
function findLastSyntaxThemePanelIndex(messages) {
|
|
1737
|
+
return findLastPanelIndexByKind(messages, 'syntax-theme-panel');
|
|
1738
|
+
}
|
|
1739
|
+
function findLastLanguagePanelIndex(messages) {
|
|
1740
|
+
return findLastPanelIndexByKind(messages, 'language-panel');
|
|
1741
|
+
}
|
|
1742
|
+
function findLastPermissionProfilePanelIndex(messages) {
|
|
1743
|
+
return findLastPanelIndexByKind(messages, 'permission-profile-panel');
|
|
1744
|
+
}
|
|
1745
|
+
function findLastPermissionRequestIndex(messages) {
|
|
1746
|
+
return findLastPanelIndexByKind(messages, 'permission-request');
|
|
1747
|
+
}
|
|
1748
|
+
function findLastStatusLinePanelIndex(messages) {
|
|
1749
|
+
return findLastPanelIndexByKind(messages, 'statusline-panel');
|
|
1750
|
+
}
|
|
1751
|
+
function findLastSkillsPanelIndex(messages) {
|
|
1752
|
+
return findLastPanelIndexByKind(messages, 'skills-panel');
|
|
1753
|
+
}
|
|
1754
|
+
function findLastPickerPanelIndex(messages) {
|
|
1755
|
+
return Math.max(findLastModelPanelIndex(messages), findLastStylePanelIndex(messages), findLastSyntaxThemePanelIndex(messages), findLastLanguagePanelIndex(messages), findLastPermissionProfilePanelIndex(messages), findLastPermissionRequestIndex(messages), findLastStatusLinePanelIndex(messages), findLastSkillsPanelIndex(messages));
|
|
1756
|
+
}
|