@researchcomputer/pista 0.1.2
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 +110 -0
- package/dist/agent-events.js +119 -0
- package/dist/agent-events.test.js +67 -0
- package/dist/app.js +608 -0
- package/dist/commands.js +673 -0
- package/dist/commands.test.js +36 -0
- package/dist/components/assistant-message-render.js +52 -0
- package/dist/components/assistant-message-render.test.js +9 -0
- package/dist/components/assistant-message.js +9 -0
- package/dist/components/editor.js +65 -0
- package/dist/components/editor.test.js +40 -0
- package/dist/components/frame.js +22 -0
- package/dist/components/header.js +63 -0
- package/dist/components/header.test.js +12 -0
- package/dist/components/log-row.js +28 -0
- package/dist/components/picker.js +10 -0
- package/dist/components/prompt.js +8 -0
- package/dist/components/scrollbar.js +15 -0
- package/dist/components/slash-command-menu.js +9 -0
- package/dist/config.js +168 -0
- package/dist/config.test.js +45 -0
- package/dist/index.js +26 -0
- package/dist/model.js +17 -0
- package/dist/transcript.js +21 -0
- package/dist/transcript.test.js +33 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +219 -0
- package/dist/utils.test.js +97 -0
- package/package.json +34 -0
package/dist/app.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
6
|
+
import { createCodingAgent, } from '@researchcomputer/agents-sdk';
|
|
7
|
+
import { resolveModel, describeSelection } from './model.js';
|
|
8
|
+
import { handleAgentEvent } from './agent-events.js';
|
|
9
|
+
import { getSlashCommandSuggestions, handleSlashCommand } from './commands.js';
|
|
10
|
+
import { Frame } from './components/frame.js';
|
|
11
|
+
import { Scrollbar } from './components/scrollbar.js';
|
|
12
|
+
import { Header } from './components/header.js';
|
|
13
|
+
import { LogRow } from './components/log-row.js';
|
|
14
|
+
import { PickerPanel } from './components/picker.js';
|
|
15
|
+
import { PromptPanel } from './components/prompt.js';
|
|
16
|
+
import { SlashCommandMenu } from './components/slash-command-menu.js';
|
|
17
|
+
import { AssistantMessage } from './components/assistant-message.js';
|
|
18
|
+
import { Editor, editBuffer } from './components/editor.js';
|
|
19
|
+
import { getTranscriptOffsetForEntry, getTranscriptWindow, stepTranscriptOffset } from './transcript.js';
|
|
20
|
+
import { errorMessage, stringifyPreview, getStoredApiKey, saveApiKey, getApiKeyPath, getCliMemoryDir, getCliSessionsDir, loadStoredPreferences, mergeStoredPreferences, saveStoredPreferences, } from './utils.js';
|
|
21
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
22
|
+
function Spinner({ frame }) {
|
|
23
|
+
return _jsx(Text, { color: "cyanBright", children: SPINNER_FRAMES[frame % SPINNER_FRAMES.length] });
|
|
24
|
+
}
|
|
25
|
+
export function App({ initialConfig }) {
|
|
26
|
+
const { exit } = useApp();
|
|
27
|
+
const agentRef = useRef(null);
|
|
28
|
+
const agentNameRef = useRef(initialConfig.agentName);
|
|
29
|
+
const unsubscribeRef = useRef(null);
|
|
30
|
+
const buildTokenRef = useRef(0);
|
|
31
|
+
const pendingRequestRef = useRef(null);
|
|
32
|
+
const permissionMemoryRef = useRef(new Map());
|
|
33
|
+
const sessionIdRef = useRef(initialConfig.sessionId);
|
|
34
|
+
const liveAssistantRef = useRef('');
|
|
35
|
+
const liveAssistantFlushTimerRef = useRef(null);
|
|
36
|
+
const mountedRef = useRef(true);
|
|
37
|
+
const runningRef = useRef(false);
|
|
38
|
+
const isRebuildingRef = useRef(false);
|
|
39
|
+
const [config, setConfig] = useState(initialConfig);
|
|
40
|
+
const [logs, setLogs] = useState([]);
|
|
41
|
+
const [composer, setComposer] = useState({ value: '', cursor: 0 });
|
|
42
|
+
const [composerHistory, setComposerHistory] = useState([]);
|
|
43
|
+
const [composerHistoryIndex, setComposerHistoryIndex] = useState(null);
|
|
44
|
+
const [composerDraftBeforeHistory, setComposerDraftBeforeHistory] = useState(null);
|
|
45
|
+
const [slashMenuSelectedIndex, setSlashMenuSelectedIndex] = useState(0);
|
|
46
|
+
const [liveAssistant, setLiveAssistant] = useState('');
|
|
47
|
+
const [status, setStatus] = useState('Idle');
|
|
48
|
+
const [running, setRunning] = useState(false);
|
|
49
|
+
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
50
|
+
const [pendingTools, setPendingTools] = useState([]);
|
|
51
|
+
const [logScrollOffset, setLogScrollOffset] = useState(0);
|
|
52
|
+
const [promptState, setPromptState] = useState(null);
|
|
53
|
+
const [pickerState, setPickerState] = useState(null);
|
|
54
|
+
const [isRebuilding, setIsRebuilding] = useState(false);
|
|
55
|
+
const currentInputMode = promptState ? 'prompt' : pickerState ? 'picker' : 'composer';
|
|
56
|
+
const terminalRows = process.stdout.rows || 24;
|
|
57
|
+
useEffect(() => { agentNameRef.current = config.agentName; }, [config.agentName]);
|
|
58
|
+
useEffect(() => { sessionIdRef.current = config.sessionId; }, [config.sessionId]);
|
|
59
|
+
useEffect(() => { runningRef.current = running; }, [running]);
|
|
60
|
+
useEffect(() => { isRebuildingRef.current = isRebuilding; }, [isRebuilding]);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!running) {
|
|
63
|
+
setSpinnerFrame(0);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const timer = setInterval(() => {
|
|
67
|
+
setSpinnerFrame((current) => (current + 1) % SPINNER_FRAMES.length);
|
|
68
|
+
}, 100);
|
|
69
|
+
return () => clearInterval(timer);
|
|
70
|
+
}, [running]);
|
|
71
|
+
const appendLog = useCallback((kind, title, body) => {
|
|
72
|
+
setLogs((current) => [
|
|
73
|
+
...current,
|
|
74
|
+
{ id: randomUUID(), kind, title, body, timestamp: Date.now() },
|
|
75
|
+
]);
|
|
76
|
+
}, []);
|
|
77
|
+
const flushLiveAssistant = useCallback(() => {
|
|
78
|
+
if (liveAssistantFlushTimerRef.current) {
|
|
79
|
+
clearTimeout(liveAssistantFlushTimerRef.current);
|
|
80
|
+
liveAssistantFlushTimerRef.current = null;
|
|
81
|
+
}
|
|
82
|
+
if (!mountedRef.current)
|
|
83
|
+
return;
|
|
84
|
+
const next = liveAssistantRef.current;
|
|
85
|
+
setLiveAssistant((current) => (current === next ? current : next));
|
|
86
|
+
}, []);
|
|
87
|
+
const scheduleLiveAssistantFlush = useCallback(() => {
|
|
88
|
+
if (liveAssistantFlushTimerRef.current)
|
|
89
|
+
return;
|
|
90
|
+
liveAssistantFlushTimerRef.current = setTimeout(() => {
|
|
91
|
+
liveAssistantFlushTimerRef.current = null;
|
|
92
|
+
if (!mountedRef.current)
|
|
93
|
+
return;
|
|
94
|
+
const next = liveAssistantRef.current;
|
|
95
|
+
setLiveAssistant((current) => (current === next ? current : next));
|
|
96
|
+
}, 33);
|
|
97
|
+
}, []);
|
|
98
|
+
const appendLiveAssistantDelta = useCallback((delta) => {
|
|
99
|
+
if (!delta)
|
|
100
|
+
return;
|
|
101
|
+
liveAssistantRef.current += delta;
|
|
102
|
+
scheduleLiveAssistantFlush();
|
|
103
|
+
}, [scheduleLiveAssistantFlush]);
|
|
104
|
+
const clearLiveAssistant = useCallback(() => {
|
|
105
|
+
if (liveAssistantFlushTimerRef.current) {
|
|
106
|
+
clearTimeout(liveAssistantFlushTimerRef.current);
|
|
107
|
+
liveAssistantFlushTimerRef.current = null;
|
|
108
|
+
}
|
|
109
|
+
liveAssistantRef.current = '';
|
|
110
|
+
setLiveAssistant((current) => (current ? '' : current));
|
|
111
|
+
}, []);
|
|
112
|
+
const rememberComposerDraft = useCallback((value) => {
|
|
113
|
+
setComposerHistory((current) => {
|
|
114
|
+
const trimmed = value.trim();
|
|
115
|
+
if (!trimmed)
|
|
116
|
+
return current;
|
|
117
|
+
const deduped = current.filter((entry) => entry !== value);
|
|
118
|
+
return [...deduped, value].slice(-50);
|
|
119
|
+
});
|
|
120
|
+
}, []);
|
|
121
|
+
const visibleLogRowsFor = useCallback((prompt, picker, live, isRunning, slashMenuCount) => {
|
|
122
|
+
const slashMenuRows = slashMenuCount > 0 ? Math.min(slashMenuCount, 6) * 2 + 3 : 0;
|
|
123
|
+
const reservedRows = 8 + (prompt ? 4 : 0) + (picker ? Math.min(picker.items.length, 6) + 3 : 0) + (live || isRunning ? 3 : 0) + slashMenuRows;
|
|
124
|
+
return Math.max(4, terminalRows - reservedRows);
|
|
125
|
+
}, [terminalRows]);
|
|
126
|
+
const jumpTranscriptTo = useCallback((target) => {
|
|
127
|
+
if (target === 'latest') {
|
|
128
|
+
setLogScrollOffset(0);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const targetIndex = [...logs].reverse().findIndex((entry) => entry.kind === target);
|
|
132
|
+
if (targetIndex === -1) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const actualIndex = logs.length - 1 - targetIndex;
|
|
136
|
+
const visibleLogRows = visibleLogRowsFor(promptState, pickerState, liveAssistant, running, 0);
|
|
137
|
+
setLogScrollOffset(getTranscriptOffsetForEntry(logs.length, visibleLogRows, actualIndex));
|
|
138
|
+
return true;
|
|
139
|
+
}, [liveAssistant, logs, pickerState, promptState, running, visibleLogRowsFor]);
|
|
140
|
+
const openPrompt = useCallback((request) => {
|
|
141
|
+
return new Promise((resolvePrompt) => {
|
|
142
|
+
pendingRequestRef.current = { type: 'prompt', resolve: resolvePrompt };
|
|
143
|
+
const value = request.initialValue ?? '';
|
|
144
|
+
setPromptState({
|
|
145
|
+
title: request.title,
|
|
146
|
+
description: request.description,
|
|
147
|
+
placeholder: request.placeholder,
|
|
148
|
+
secret: request.secret,
|
|
149
|
+
value,
|
|
150
|
+
cursor: value.length,
|
|
151
|
+
});
|
|
152
|
+
setStatus('Waiting for input');
|
|
153
|
+
});
|
|
154
|
+
}, []);
|
|
155
|
+
const openPicker = useCallback((request) => {
|
|
156
|
+
return new Promise((resolvePicker) => {
|
|
157
|
+
const selectedIndex = request.selectedId
|
|
158
|
+
? Math.max(0, request.items.findIndex((item) => item.id === request.selectedId))
|
|
159
|
+
: 0;
|
|
160
|
+
pendingRequestRef.current = { type: 'picker', resolve: resolvePicker };
|
|
161
|
+
setPickerState({
|
|
162
|
+
title: request.title,
|
|
163
|
+
description: request.description,
|
|
164
|
+
items: request.items,
|
|
165
|
+
emptyMessage: request.emptyMessage,
|
|
166
|
+
selectedIndex,
|
|
167
|
+
});
|
|
168
|
+
setStatus('Choose an option');
|
|
169
|
+
});
|
|
170
|
+
}, []);
|
|
171
|
+
const closeRequest = useCallback((value) => {
|
|
172
|
+
const pending = pendingRequestRef.current;
|
|
173
|
+
pendingRequestRef.current = null;
|
|
174
|
+
setPromptState(null);
|
|
175
|
+
setPickerState(null);
|
|
176
|
+
setStatus(runningRef.current ? 'Running' : isRebuildingRef.current ? 'Rebuilding agent' : 'Idle');
|
|
177
|
+
pending?.resolve(value);
|
|
178
|
+
}, []);
|
|
179
|
+
const rebuildAgent = useCallback(async (nextConfig, note) => {
|
|
180
|
+
const buildToken = ++buildTokenRef.current;
|
|
181
|
+
if (nextConfig.sessionId !== sessionIdRef.current) {
|
|
182
|
+
permissionMemoryRef.current.clear();
|
|
183
|
+
}
|
|
184
|
+
setIsRebuilding(true);
|
|
185
|
+
setStatus('Rebuilding agent');
|
|
186
|
+
setRunning(false);
|
|
187
|
+
setPendingTools([]);
|
|
188
|
+
clearLiveAssistant();
|
|
189
|
+
unsubscribeRef.current?.();
|
|
190
|
+
unsubscribeRef.current = null;
|
|
191
|
+
const previousAgent = agentRef.current;
|
|
192
|
+
agentRef.current = null;
|
|
193
|
+
if (previousAgent) {
|
|
194
|
+
await previousAgent.dispose();
|
|
195
|
+
}
|
|
196
|
+
setConfig(nextConfig);
|
|
197
|
+
try {
|
|
198
|
+
saveStoredPreferences(mergeStoredPreferences(loadStoredPreferences(), { selection: nextConfig.selection }));
|
|
199
|
+
saveStoredPreferences(mergeStoredPreferences(loadStoredPreferences(nextConfig.cwd), { selection: nextConfig.selection }), nextConfig.cwd);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
appendLog('error', 'Preferences not saved', errorMessage(error));
|
|
203
|
+
}
|
|
204
|
+
const selectedModel = resolveModel(nextConfig.selection, nextConfig.thinkingLevel);
|
|
205
|
+
try {
|
|
206
|
+
const codingAgent = await createCodingAgent({
|
|
207
|
+
model: selectedModel,
|
|
208
|
+
cwd: nextConfig.cwd,
|
|
209
|
+
sessionId: nextConfig.sessionId,
|
|
210
|
+
sessionDir: getCliSessionsDir(),
|
|
211
|
+
memoryDir: getCliMemoryDir(),
|
|
212
|
+
permissionMode: nextConfig.permissionMode,
|
|
213
|
+
thinkingLevel: nextConfig.thinkingLevel,
|
|
214
|
+
skills: nextConfig.skills,
|
|
215
|
+
getApiKey: async () => {
|
|
216
|
+
const storedKey = getStoredApiKey();
|
|
217
|
+
if (storedKey)
|
|
218
|
+
return storedKey;
|
|
219
|
+
const promptedKey = await openPrompt({
|
|
220
|
+
title: 'API Key Required',
|
|
221
|
+
description: `Please enter your API key for ${nextConfig.selection.endpoint}. It will be saved to ${getApiKeyPath()}.`,
|
|
222
|
+
placeholder: 'Paste your API key here',
|
|
223
|
+
secret: true,
|
|
224
|
+
});
|
|
225
|
+
if (promptedKey) {
|
|
226
|
+
saveApiKey(promptedKey);
|
|
227
|
+
appendLog('system', 'API Key Saved', `Saved to ${getApiKeyPath()}`);
|
|
228
|
+
return promptedKey;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
},
|
|
232
|
+
onPermissionAsk: async (toolName, args) => {
|
|
233
|
+
const rememberedDecision = permissionMemoryRef.current.get(toolName);
|
|
234
|
+
if (rememberedDecision) {
|
|
235
|
+
appendLog(rememberedDecision === 'allow' ? 'system' : 'error', rememberedDecision === 'allow' ? 'Permission auto-approved' : 'Permission auto-denied', `${toolName} ${rememberedDecision === 'allow' ? 'allowed' : 'blocked'} by session memory`);
|
|
236
|
+
return rememberedDecision === 'allow';
|
|
237
|
+
}
|
|
238
|
+
const answer = await openPicker({
|
|
239
|
+
title: `Permission required · ${toolName}`,
|
|
240
|
+
description: [
|
|
241
|
+
'The agent wants to run this tool with the following arguments:',
|
|
242
|
+
'Remembered choices apply to this tool name for the rest of the current session.',
|
|
243
|
+
stringifyPreview(args, 320),
|
|
244
|
+
].join('\n'),
|
|
245
|
+
items: [
|
|
246
|
+
{ id: 'allow', label: 'Allow once', description: 'Run this tool call and continue the current turn.' },
|
|
247
|
+
{ id: 'allow-session', label: 'Always allow this session', description: 'Auto-approve future calls to this tool name in the current session.' },
|
|
248
|
+
{ id: 'deny', label: 'Deny', description: 'Block this tool call and let the agent recover.' },
|
|
249
|
+
{ id: 'deny-session', label: 'Always deny this session', description: 'Auto-deny future calls to this tool name in the current session.' },
|
|
250
|
+
],
|
|
251
|
+
selectedId: 'allow',
|
|
252
|
+
});
|
|
253
|
+
if (answer === 'allow-session') {
|
|
254
|
+
permissionMemoryRef.current.set(toolName, 'allow');
|
|
255
|
+
}
|
|
256
|
+
else if (answer === 'deny-session') {
|
|
257
|
+
permissionMemoryRef.current.set(toolName, 'deny');
|
|
258
|
+
}
|
|
259
|
+
const allowed = answer === 'allow' || answer === 'allow-session';
|
|
260
|
+
appendLog(allowed ? 'system' : 'error', allowed ? 'Permission granted' : 'Permission denied', `${toolName} ${allowed ? 'allowed' : 'blocked'}${answer?.endsWith('session') ? ' for the rest of this session' : ''}`);
|
|
261
|
+
return allowed;
|
|
262
|
+
},
|
|
263
|
+
onQuestion: async (question) => {
|
|
264
|
+
const answer = await openPrompt({
|
|
265
|
+
title: 'Agent question',
|
|
266
|
+
description: question,
|
|
267
|
+
placeholder: 'Type your answer',
|
|
268
|
+
});
|
|
269
|
+
appendLog('system', 'User response sent to agent', answer ?? '');
|
|
270
|
+
return answer ?? '';
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
if (!mountedRef.current || buildToken !== buildTokenRef.current) {
|
|
274
|
+
await codingAgent.dispose();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
agentRef.current = codingAgent;
|
|
278
|
+
unsubscribeRef.current = codingAgent.agent.subscribe((event) => {
|
|
279
|
+
handleAgentEvent(event, {
|
|
280
|
+
getAgentName: () => agentNameRef.current,
|
|
281
|
+
getLiveAssistantText: () => {
|
|
282
|
+
flushLiveAssistant();
|
|
283
|
+
return liveAssistantRef.current;
|
|
284
|
+
},
|
|
285
|
+
appendLog,
|
|
286
|
+
appendLiveAssistantDelta,
|
|
287
|
+
clearLiveAssistant,
|
|
288
|
+
setPendingTools,
|
|
289
|
+
setRunning,
|
|
290
|
+
setStatus,
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
appendLog('system', 'Agent ready', note);
|
|
294
|
+
setStatus('Idle');
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
appendLog('error', 'Agent init failed', errorMessage(error));
|
|
298
|
+
setStatus('Error');
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
if (buildToken === buildTokenRef.current) {
|
|
302
|
+
setIsRebuilding(false);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}, [appendLog, openPicker, openPrompt]);
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
void rebuildAgent(initialConfig, [
|
|
308
|
+
`Agent ${initialConfig.agentName}`,
|
|
309
|
+
`Session ${initialConfig.sessionId}`,
|
|
310
|
+
`CWD ${initialConfig.cwd}`,
|
|
311
|
+
`Model ${describeSelection(initialConfig.selection)}`,
|
|
312
|
+
'Type /help for commands.',
|
|
313
|
+
].join('\n'));
|
|
314
|
+
return () => {
|
|
315
|
+
mountedRef.current = false;
|
|
316
|
+
unsubscribeRef.current?.();
|
|
317
|
+
unsubscribeRef.current = null;
|
|
318
|
+
if (liveAssistantFlushTimerRef.current) {
|
|
319
|
+
clearTimeout(liveAssistantFlushTimerRef.current);
|
|
320
|
+
liveAssistantFlushTimerRef.current = null;
|
|
321
|
+
}
|
|
322
|
+
if (agentRef.current) {
|
|
323
|
+
void agentRef.current.dispose();
|
|
324
|
+
agentRef.current = null;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}, [clearLiveAssistant, initialConfig, rebuildAgent]);
|
|
328
|
+
const commandContext = useMemo(() => ({
|
|
329
|
+
config,
|
|
330
|
+
running,
|
|
331
|
+
appendLog,
|
|
332
|
+
openPrompt,
|
|
333
|
+
openPicker,
|
|
334
|
+
rebuildAgent,
|
|
335
|
+
setConfig: (updater) => setConfig(updater),
|
|
336
|
+
resetAgent: () => {
|
|
337
|
+
agentRef.current?.agent.reset();
|
|
338
|
+
clearLiveAssistant();
|
|
339
|
+
setPendingTools([]);
|
|
340
|
+
},
|
|
341
|
+
abortAgent: () => {
|
|
342
|
+
agentRef.current?.agent.abort();
|
|
343
|
+
},
|
|
344
|
+
cancelPendingRequest: () => {
|
|
345
|
+
if (pendingRequestRef.current) {
|
|
346
|
+
closeRequest(null);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
clearLogs: () => {
|
|
350
|
+
setLogs([]);
|
|
351
|
+
clearLiveAssistant();
|
|
352
|
+
setLogScrollOffset(0);
|
|
353
|
+
},
|
|
354
|
+
getCostSummary: () => {
|
|
355
|
+
const agent = agentRef.current;
|
|
356
|
+
if (!agent)
|
|
357
|
+
return null;
|
|
358
|
+
const total = agent.costTracker.total();
|
|
359
|
+
return { tokens: total.tokens, cost: total.cost };
|
|
360
|
+
},
|
|
361
|
+
jumpTranscript: (target) => jumpTranscriptTo(target),
|
|
362
|
+
promptAgent: async (message) => {
|
|
363
|
+
const agent = agentRef.current;
|
|
364
|
+
if (!agent) {
|
|
365
|
+
appendLog('error', 'Agent unavailable', 'The agent is not ready yet.');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
appendLog('user', 'You', message);
|
|
369
|
+
setStatus('Running');
|
|
370
|
+
try {
|
|
371
|
+
await agent.prompt(message);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
appendLog('error', 'Agent error', errorMessage(error));
|
|
375
|
+
setStatus('Error');
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
exit,
|
|
379
|
+
}), [appendLog, clearLiveAssistant, closeRequest, config, exit, jumpTranscriptTo, openPicker, openPrompt, rebuildAgent, running]);
|
|
380
|
+
const submitComposer = useCallback(async () => {
|
|
381
|
+
const trimmed = composer.value.trim();
|
|
382
|
+
if (!trimmed)
|
|
383
|
+
return;
|
|
384
|
+
rememberComposerDraft(composer.value);
|
|
385
|
+
setComposer({ value: '', cursor: 0 });
|
|
386
|
+
setComposerHistoryIndex(null);
|
|
387
|
+
setComposerDraftBeforeHistory(null);
|
|
388
|
+
setLogScrollOffset(0);
|
|
389
|
+
if (trimmed.startsWith('/')) {
|
|
390
|
+
await handleSlashCommand(trimmed, commandContext);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (isRebuilding) {
|
|
394
|
+
appendLog('error', 'Agent unavailable', 'Wait for the current agent rebuild to finish.');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const agent = agentRef.current;
|
|
398
|
+
if (!agent) {
|
|
399
|
+
appendLog('error', 'Agent unavailable', 'The agent is not ready yet.');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (running) {
|
|
403
|
+
appendLog('error', 'Agent busy', 'Use /abort or wait for the current run to finish.');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
appendLog('user', 'You', trimmed);
|
|
407
|
+
setStatus('Running');
|
|
408
|
+
try {
|
|
409
|
+
await agent.prompt(trimmed);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
appendLog('error', 'Agent error', errorMessage(error));
|
|
413
|
+
setStatus('Error');
|
|
414
|
+
}
|
|
415
|
+
}, [appendLog, commandContext, composer.value, isRebuilding, rememberComposerDraft, running]);
|
|
416
|
+
const slashCommandItems = useMemo(() => getSlashCommandSuggestions(composer.value, running), [composer.value, running]);
|
|
417
|
+
const slashMenuVisible = currentInputMode === 'composer' && slashCommandItems.length > 0;
|
|
418
|
+
const selectedSlashCommand = slashMenuVisible ? slashCommandItems[Math.min(slashMenuSelectedIndex, slashCommandItems.length - 1)] : null;
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
setSlashMenuSelectedIndex((current) => {
|
|
421
|
+
if (slashCommandItems.length === 0)
|
|
422
|
+
return 0;
|
|
423
|
+
return Math.min(current, slashCommandItems.length - 1);
|
|
424
|
+
});
|
|
425
|
+
}, [slashCommandItems]);
|
|
426
|
+
const applySlashCommandSelection = useCallback((insertText) => {
|
|
427
|
+
setComposer({ value: insertText, cursor: insertText.length });
|
|
428
|
+
setComposerHistoryIndex(null);
|
|
429
|
+
setComposerDraftBeforeHistory(null);
|
|
430
|
+
}, []);
|
|
431
|
+
useInput((input, key) => {
|
|
432
|
+
if (key.ctrl && input === 'c') {
|
|
433
|
+
if (running && agentRef.current) {
|
|
434
|
+
if (pendingRequestRef.current) {
|
|
435
|
+
closeRequest(null);
|
|
436
|
+
}
|
|
437
|
+
agentRef.current.agent.abort();
|
|
438
|
+
appendLog('system', 'Abort requested', 'Waiting for the current run to stop.');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
exit();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (currentInputMode === 'picker' && pickerState) {
|
|
445
|
+
if (key.escape) {
|
|
446
|
+
closeRequest(null);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (key.upArrow) {
|
|
450
|
+
setPickerState((current) => {
|
|
451
|
+
if (!current || current.items.length === 0)
|
|
452
|
+
return current;
|
|
453
|
+
return {
|
|
454
|
+
...current,
|
|
455
|
+
selectedIndex: (current.selectedIndex - 1 + current.items.length) % current.items.length,
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (key.downArrow) {
|
|
461
|
+
setPickerState((current) => {
|
|
462
|
+
if (!current || current.items.length === 0)
|
|
463
|
+
return current;
|
|
464
|
+
return {
|
|
465
|
+
...current,
|
|
466
|
+
selectedIndex: (current.selectedIndex + 1) % current.items.length,
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (key.return) {
|
|
472
|
+
const item = pickerState.items[pickerState.selectedIndex];
|
|
473
|
+
closeRequest(item?.id ?? null);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (currentInputMode === 'prompt' && promptState) {
|
|
478
|
+
if (key.escape) {
|
|
479
|
+
closeRequest(null);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (key.return) {
|
|
483
|
+
closeRequest(promptState.value);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (key.upArrow || key.downArrow || key.tab) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
setPromptState((current) => current ? editBuffer(current, input, { ...key, escape: key.escape }) : current);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (key.escape) {
|
|
493
|
+
setComposer({ value: '', cursor: 0 });
|
|
494
|
+
setComposerHistoryIndex(null);
|
|
495
|
+
setComposerDraftBeforeHistory(null);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const visibleLogRows = visibleLogRowsFor(promptState, pickerState, liveAssistant, running, slashMenuVisible ? slashCommandItems.length : 0);
|
|
499
|
+
const scrollStep = Math.max(1, visibleLogRows - 2);
|
|
500
|
+
if (key.pageUp) {
|
|
501
|
+
setLogScrollOffset((current) => stepTranscriptOffset(current, scrollStep, logs.length, visibleLogRows));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (key.pageDown) {
|
|
505
|
+
setLogScrollOffset((current) => stepTranscriptOffset(current, -scrollStep, logs.length, visibleLogRows));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (slashMenuVisible && key.upArrow) {
|
|
509
|
+
setSlashMenuSelectedIndex((current) => {
|
|
510
|
+
if (slashCommandItems.length === 0)
|
|
511
|
+
return 0;
|
|
512
|
+
return (current - 1 + slashCommandItems.length) % slashCommandItems.length;
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (slashMenuVisible && key.downArrow) {
|
|
517
|
+
setSlashMenuSelectedIndex((current) => {
|
|
518
|
+
if (slashCommandItems.length === 0)
|
|
519
|
+
return 0;
|
|
520
|
+
return (current + 1) % slashCommandItems.length;
|
|
521
|
+
});
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (slashMenuVisible && key.tab && selectedSlashCommand) {
|
|
525
|
+
applySlashCommandSelection(selectedSlashCommand.insertText);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (key.ctrl && input === 'j') {
|
|
529
|
+
setComposer((current) => {
|
|
530
|
+
const nextValue = `${current.value.slice(0, current.cursor)}\n${current.value.slice(current.cursor)}`;
|
|
531
|
+
return { value: nextValue, cursor: current.cursor + 1 };
|
|
532
|
+
});
|
|
533
|
+
setComposerHistoryIndex(null);
|
|
534
|
+
setComposerDraftBeforeHistory(null);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (key.return && key.shift) {
|
|
538
|
+
setComposer((current) => {
|
|
539
|
+
const nextValue = `${current.value.slice(0, current.cursor)}\n${current.value.slice(current.cursor)}`;
|
|
540
|
+
return { value: nextValue, cursor: current.cursor + 1 };
|
|
541
|
+
});
|
|
542
|
+
setComposerHistoryIndex(null);
|
|
543
|
+
setComposerDraftBeforeHistory(null);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (key.upArrow) {
|
|
547
|
+
if (composerHistory.length === 0)
|
|
548
|
+
return;
|
|
549
|
+
setComposerHistoryIndex((current) => {
|
|
550
|
+
const nextIndex = current === null ? composerHistory.length - 1 : Math.max(0, current - 1);
|
|
551
|
+
setComposerDraftBeforeHistory((draft) => draft ?? composer);
|
|
552
|
+
const nextValue = composerHistory[nextIndex] ?? '';
|
|
553
|
+
setComposer({ value: nextValue, cursor: nextValue.length });
|
|
554
|
+
return nextIndex;
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (key.downArrow) {
|
|
559
|
+
if (composerHistory.length === 0 || composerHistoryIndex === null)
|
|
560
|
+
return;
|
|
561
|
+
if (composerHistoryIndex >= composerHistory.length - 1) {
|
|
562
|
+
setComposer(composerDraftBeforeHistory ?? { value: '', cursor: 0 });
|
|
563
|
+
setComposerHistoryIndex(null);
|
|
564
|
+
setComposerDraftBeforeHistory(null);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const nextIndex = composerHistoryIndex + 1;
|
|
568
|
+
const nextValue = composerHistory[nextIndex] ?? '';
|
|
569
|
+
setComposer({ value: nextValue, cursor: nextValue.length });
|
|
570
|
+
setComposerHistoryIndex(nextIndex);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (key.return) {
|
|
574
|
+
if (slashMenuVisible && selectedSlashCommand) {
|
|
575
|
+
const trimmed = composer.value.trim();
|
|
576
|
+
const hasArgs = /\s/.test(trimmed.slice(1));
|
|
577
|
+
const needsCompletion = !hasArgs && (trimmed !== selectedSlashCommand.commandText ||
|
|
578
|
+
selectedSlashCommand.insertText.endsWith(' '));
|
|
579
|
+
if (needsCompletion) {
|
|
580
|
+
applySlashCommandSelection(selectedSlashCommand.insertText);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
void submitComposer();
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (key.tab) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
setComposer((current) => {
|
|
591
|
+
const next = editBuffer(current, input, { ...key, escape: key.escape });
|
|
592
|
+
if (next.value !== current.value || next.cursor !== current.cursor) {
|
|
593
|
+
setComposerHistoryIndex(null);
|
|
594
|
+
setComposerDraftBeforeHistory(null);
|
|
595
|
+
}
|
|
596
|
+
return next;
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
const visibleLogRows = visibleLogRowsFor(promptState, pickerState, liveAssistant, running, slashMenuVisible ? slashCommandItems.length : 0);
|
|
600
|
+
const transcriptWindow = getTranscriptWindow(logs.length, visibleLogRows, logScrollOffset);
|
|
601
|
+
const renderedLogs = logs.slice(transcriptWindow.start, transcriptWindow.end);
|
|
602
|
+
const hasContentAbove = transcriptWindow.start > 0;
|
|
603
|
+
const hasContentBelow = transcriptWindow.offset > 0;
|
|
604
|
+
const rangeText = logs.length > 0 ? `${transcriptWindow.start + 1}-${transcriptWindow.end} of ${logs.length}` : '';
|
|
605
|
+
const transcriptTopRight = [rangeText, hasContentAbove ? '▲' : ''].filter(Boolean).join(' ') || undefined;
|
|
606
|
+
const transcriptBottomRight = hasContentBelow ? `▼ ${transcriptWindow.offset} newer` : undefined;
|
|
607
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 0, minHeight: terminalRows, flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Header, { agentName: config.agentName, cwd: config.cwd, sessionId: config.sessionId, model: config.selection.model, apiStyle: config.selection.apiStyle, endpoint: config.selection.endpoint, permissionMode: config.permissionMode, thinkingLevel: config.thinkingLevel, status: status, pendingTools: pendingTools }) }), _jsx(Box, { flexGrow: 1, flexDirection: "column", marginTop: 1, minHeight: 0, children: _jsx(Frame, { borderColor: "blue", topLeft: "Transcript", topRight: transcriptTopRight, bottomRight: transcriptBottomRight, children: _jsxs(Box, { flexDirection: "row", flexGrow: 1, minHeight: 0, children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [renderedLogs.length === 0 && !liveAssistant && !running ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No messages yet." }), _jsx(Text, { dimColor: true, children: "Try asking for a repo change, or use /help to inspect commands." }), _jsx(Text, { dimColor: true, children: "Useful setup commands: /model, /endpoint, /permissions, /thinking, /session new" })] })) : null, renderedLogs.map((entry) => (_jsx(LogRow, { entry: entry }, entry.id))), liveAssistant ? (_jsxs(Box, { flexDirection: "column", marginBottom: 1, flexShrink: 0, children: [_jsxs(Text, { color: "cyanBright", bold: true, children: ["\u25CF ", config.agentName] }), _jsx(Box, { paddingLeft: 2, children: _jsx(AssistantMessage, { content: liveAssistant, showSpinner: running, spinner: _jsx(Spinner, { frame: spinnerFrame }) }) })] })) : null, !liveAssistant && running ? (_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "cyanBright", bold: true, children: "\u25CF " }), _jsx(Text, { italic: true, dimColor: true, children: "Thinking " }), _jsx(Spinner, { frame: spinnerFrame })] })) : null] }), logs.length > visibleLogRows ? (_jsx(Scrollbar, { trackHeight: visibleLogRows, totalEntries: logs.length, visibleEntries: visibleLogRows, offset: logScrollOffset })) : null] }) }) }), _jsxs(Box, { flexShrink: 0, flexDirection: "column", marginTop: 1, children: [pickerState ? (_jsx(PickerPanel, { pickerState: pickerState })) : null, promptState ? (_jsx(PromptPanel, { promptState: promptState })) : null, !promptState && !pickerState && slashMenuVisible ? (_jsx(SlashCommandMenu, { items: slashCommandItems, selectedIndex: Math.min(slashMenuSelectedIndex, Math.max(0, slashCommandItems.length - 1)) })) : null, !promptState && !pickerState ? (_jsx(Frame, { borderColor: running ? 'yellow' : 'cyan', bottomLeft: slashMenuVisible ? 'Enter send · ⇧Enter newline · Tab complete · /help' : 'Enter send · ⇧Enter newline · /help', children: _jsxs(Box, { children: [_jsx(Text, { color: "greenBright", bold: true, children: '> ' }), _jsx(Editor, { value: composer.value, cursor: composer.cursor, placeholder: `Ask ${config.agentName} or type /help` })] }) })) : null, _jsxs(Box, { paddingX: 1, justifyContent: "space-between", marginBottom: 0, children: [_jsx(Text, { dimColor: true, children: running ? 'Ctrl+C abort' : 'Ctrl+C exit' }), _jsx(Box, { children: running ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { frame: spinnerFrame }), _jsxs(Text, { color: "yellowBright", children: [" ", status] })] })) : (_jsx(Text, { color: "greenBright", children: "\u25CF ready" })) })] })] })] }));
|
|
608
|
+
}
|