@researchcomputer/pista 0.1.2 → 0.1.3
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 +4 -0
- package/dist/agent-events.js +2 -6
- package/dist/agent-events.test.js +3 -0
- package/dist/app.js +142 -144
- package/dist/commands.js +200 -104
- package/dist/commands.test.js +37 -0
- package/dist/components/assistant-message-render.test.js +12 -2
- package/dist/components/assistant-message.js +1 -1
- package/dist/components/composer-panel.js +11 -0
- package/dist/components/editor.js +4 -2
- package/dist/components/editor.test.js +10 -2
- package/dist/components/frame.js +4 -5
- package/dist/components/header.js +3 -3
- package/dist/components/log-row.js +2 -2
- package/dist/components/scrollbar.js +1 -1
- package/dist/components/status-bar.js +7 -0
- package/dist/hooks/use-composer.js +89 -0
- package/dist/hooks/use-composer.test.js +24 -0
- package/dist/hooks/use-live-assistant.js +64 -0
- package/dist/hooks/use-live-assistant.test.js +30 -0
- package/dist/hooks/use-terminal-size.js +21 -0
- package/dist/transcript.test.js +22 -0
- package/dist/utils.js +24 -6
- package/dist/utils.test.js +53 -5
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Pista
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./docs/assets/logo.png" alt="Pista" style="max-width: 128px;" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
3
7
|
A full-featured terminal coding agent powered by [`@researchcomputer/agents-sdk`](https://www.npmjs.com/package/@researchcomputer/agents-sdk). Pista provides a rich Ink-based TUI with:
|
|
4
8
|
|
|
5
9
|
- Live assistant streaming
|
package/dist/agent-events.js
CHANGED
|
@@ -63,16 +63,12 @@ export function handleAgentEvent(event, controls) {
|
|
|
63
63
|
function summarizeAssistantMessage(message) {
|
|
64
64
|
if (message.role !== 'assistant')
|
|
65
65
|
return '';
|
|
66
|
-
const text = message.content
|
|
67
|
-
.map((item) => (item.type === 'text' ? item.text : ''))
|
|
68
|
-
.join('');
|
|
66
|
+
const text = message.content.map((item) => (item.type === 'text' ? item.text : '')).join('');
|
|
69
67
|
if (text.trim())
|
|
70
68
|
return text.trim();
|
|
71
69
|
if (message.errorMessage)
|
|
72
70
|
return `[${message.stopReason}] ${message.errorMessage}`;
|
|
73
|
-
const toolNames = message.content
|
|
74
|
-
.map((item) => (item.type === 'toolCall' ? item.name : ''))
|
|
75
|
-
.filter(Boolean);
|
|
71
|
+
const toolNames = message.content.map((item) => (item.type === 'toolCall' ? item.name : '')).filter(Boolean);
|
|
76
72
|
return toolNames.length > 0 ? `[requested tools: ${toolNames.join(', ')}]` : '';
|
|
77
73
|
}
|
|
78
74
|
function summarizeToolResult(result) {
|
|
@@ -22,6 +22,7 @@ test('text deltas are forwarded through the buffered live assistant path', () =>
|
|
|
22
22
|
type: 'text_delta',
|
|
23
23
|
delta: 'hello',
|
|
24
24
|
},
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
26
|
}, createControls({
|
|
26
27
|
appendLiveAssistantDelta: (delta) => {
|
|
27
28
|
deltas.push(delta);
|
|
@@ -38,6 +39,7 @@ test('assistant message end logs the final text and clears the live buffer', ()
|
|
|
38
39
|
role: 'assistant',
|
|
39
40
|
content: [{ type: 'text', text: 'final answer' }],
|
|
40
41
|
},
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
43
|
}, createControls({
|
|
42
44
|
appendLog: (kind, title, body) => {
|
|
43
45
|
logs.push({ kind, title, body });
|
|
@@ -57,6 +59,7 @@ test('assistant message end falls back to buffered live text when needed', () =>
|
|
|
57
59
|
role: 'assistant',
|
|
58
60
|
content: [],
|
|
59
61
|
},
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
63
|
}, createControls({
|
|
61
64
|
getLiveAssistantText: () => 'buffered answer',
|
|
62
65
|
appendLog: (kind, title, body) => {
|
package/dist/app.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
-
import process from 'node:process';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
2
|
import { randomUUID } from 'node:crypto';
|
|
4
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
4
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
6
|
-
import {
|
|
5
|
+
import { useTerminalSize } from './hooks/use-terminal-size.js';
|
|
6
|
+
import { useComposer } from './hooks/use-composer.js';
|
|
7
|
+
import { useLiveAssistant } from './hooks/use-live-assistant.js';
|
|
8
|
+
import { createCodingAgent } from '@researchcomputer/agents-sdk';
|
|
7
9
|
import { resolveModel, describeSelection } from './model.js';
|
|
8
10
|
import { handleAgentEvent } from './agent-events.js';
|
|
9
11
|
import { getSlashCommandSuggestions, handleSlashCommand } from './commands.js';
|
|
@@ -13,9 +15,11 @@ import { Header } from './components/header.js';
|
|
|
13
15
|
import { LogRow } from './components/log-row.js';
|
|
14
16
|
import { PickerPanel } from './components/picker.js';
|
|
15
17
|
import { PromptPanel } from './components/prompt.js';
|
|
16
|
-
|
|
18
|
+
// SlashCommandMenu is rendered via ComposerPanel
|
|
19
|
+
import { StatusBar } from './components/status-bar.js';
|
|
20
|
+
import { ComposerPanel } from './components/composer-panel.js';
|
|
17
21
|
import { AssistantMessage } from './components/assistant-message.js';
|
|
18
|
-
import {
|
|
22
|
+
import { editBuffer } from './components/editor.js';
|
|
19
23
|
import { getTranscriptOffsetForEntry, getTranscriptWindow, stepTranscriptOffset } from './transcript.js';
|
|
20
24
|
import { errorMessage, stringifyPreview, getStoredApiKey, saveApiKey, getApiKeyPath, getCliMemoryDir, getCliSessionsDir, loadStoredPreferences, mergeStoredPreferences, saveStoredPreferences, } from './utils.js';
|
|
21
25
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -31,19 +35,18 @@ export function App({ initialConfig }) {
|
|
|
31
35
|
const pendingRequestRef = useRef(null);
|
|
32
36
|
const permissionMemoryRef = useRef(new Map());
|
|
33
37
|
const sessionIdRef = useRef(initialConfig.sessionId);
|
|
34
|
-
const liveAssistantRef = useRef('');
|
|
35
|
-
const liveAssistantFlushTimerRef = useRef(null);
|
|
36
38
|
const mountedRef = useRef(true);
|
|
37
39
|
const runningRef = useRef(false);
|
|
38
40
|
const isRebuildingRef = useRef(false);
|
|
41
|
+
const btwSnapshotRef = useRef(null);
|
|
42
|
+
const btwLogCountRef = useRef(0);
|
|
43
|
+
const btwOneShotRef = useRef(false);
|
|
39
44
|
const [config, setConfig] = useState(initialConfig);
|
|
40
45
|
const [logs, setLogs] = useState([]);
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const [composerDraftBeforeHistory, setComposerDraftBeforeHistory] = useState(null);
|
|
46
|
+
const composerHook = useComposer();
|
|
47
|
+
const { composer, setComposer } = composerHook;
|
|
48
|
+
const liveAssistant = useLiveAssistant();
|
|
45
49
|
const [slashMenuSelectedIndex, setSlashMenuSelectedIndex] = useState(0);
|
|
46
|
-
const [liveAssistant, setLiveAssistant] = useState('');
|
|
47
50
|
const [status, setStatus] = useState('Idle');
|
|
48
51
|
const [running, setRunning] = useState(false);
|
|
49
52
|
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
@@ -52,12 +55,21 @@ export function App({ initialConfig }) {
|
|
|
52
55
|
const [promptState, setPromptState] = useState(null);
|
|
53
56
|
const [pickerState, setPickerState] = useState(null);
|
|
54
57
|
const [isRebuilding, setIsRebuilding] = useState(false);
|
|
58
|
+
const [btwActive, setBtwActive] = useState(false);
|
|
55
59
|
const currentInputMode = promptState ? 'prompt' : pickerState ? 'picker' : 'composer';
|
|
56
|
-
const
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
useEffect(() => {
|
|
60
|
+
const { columns: termWidth, rows: terminalRows } = useTerminalSize();
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
agentNameRef.current = config.agentName;
|
|
63
|
+
}, [config.agentName]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
sessionIdRef.current = config.sessionId;
|
|
66
|
+
}, [config.sessionId]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
runningRef.current = running;
|
|
69
|
+
}, [running]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
isRebuildingRef.current = isRebuilding;
|
|
72
|
+
}, [isRebuilding]);
|
|
61
73
|
useEffect(() => {
|
|
62
74
|
if (!running) {
|
|
63
75
|
setSpinnerFrame(0);
|
|
@@ -65,62 +77,19 @@ export function App({ initialConfig }) {
|
|
|
65
77
|
}
|
|
66
78
|
const timer = setInterval(() => {
|
|
67
79
|
setSpinnerFrame((current) => (current + 1) % SPINNER_FRAMES.length);
|
|
68
|
-
},
|
|
80
|
+
}, 150);
|
|
69
81
|
return () => clearInterval(timer);
|
|
70
82
|
}, [running]);
|
|
71
83
|
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
|
-
});
|
|
84
|
+
setLogs((current) => [...current, { id: randomUUID(), kind, title, body, timestamp: Date.now() }]);
|
|
120
85
|
}, []);
|
|
121
86
|
const visibleLogRowsFor = useCallback((prompt, picker, live, isRunning, slashMenuCount) => {
|
|
122
87
|
const slashMenuRows = slashMenuCount > 0 ? Math.min(slashMenuCount, 6) * 2 + 3 : 0;
|
|
123
|
-
const reservedRows = 8 +
|
|
88
|
+
const reservedRows = 8 +
|
|
89
|
+
(prompt ? 4 : 0) +
|
|
90
|
+
(picker ? Math.min(picker.items.length, 6) + 3 : 0) +
|
|
91
|
+
(live || isRunning ? 3 : 0) +
|
|
92
|
+
slashMenuRows;
|
|
124
93
|
return Math.max(4, terminalRows - reservedRows);
|
|
125
94
|
}, [terminalRows]);
|
|
126
95
|
const jumpTranscriptTo = useCallback((target) => {
|
|
@@ -133,10 +102,10 @@ export function App({ initialConfig }) {
|
|
|
133
102
|
return false;
|
|
134
103
|
}
|
|
135
104
|
const actualIndex = logs.length - 1 - targetIndex;
|
|
136
|
-
const visibleLogRows = visibleLogRowsFor(promptState, pickerState, liveAssistant, running, 0);
|
|
105
|
+
const visibleLogRows = visibleLogRowsFor(promptState, pickerState, liveAssistant.text, running, 0);
|
|
137
106
|
setLogScrollOffset(getTranscriptOffsetForEntry(logs.length, visibleLogRows, actualIndex));
|
|
138
107
|
return true;
|
|
139
|
-
}, [liveAssistant, logs, pickerState, promptState, running, visibleLogRowsFor]);
|
|
108
|
+
}, [liveAssistant.text, logs, pickerState, promptState, running, visibleLogRowsFor]);
|
|
140
109
|
const openPrompt = useCallback((request) => {
|
|
141
110
|
return new Promise((resolvePrompt) => {
|
|
142
111
|
pendingRequestRef.current = { type: 'prompt', resolve: resolvePrompt };
|
|
@@ -176,7 +145,45 @@ export function App({ initialConfig }) {
|
|
|
176
145
|
setStatus(runningRef.current ? 'Running' : isRebuildingRef.current ? 'Rebuilding agent' : 'Idle');
|
|
177
146
|
pending?.resolve(value);
|
|
178
147
|
}, []);
|
|
148
|
+
const enterBtw = useCallback((oneShot) => {
|
|
149
|
+
const agent = agentRef.current;
|
|
150
|
+
if (!agent) {
|
|
151
|
+
appendLog('error', 'Agent unavailable', 'The agent is not ready yet.');
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (btwSnapshotRef.current) {
|
|
155
|
+
appendLog('system', 'Already in btw mode', 'Press Esc to exit first.');
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
btwSnapshotRef.current = agent.snapshot();
|
|
159
|
+
btwLogCountRef.current = logs.length;
|
|
160
|
+
btwOneShotRef.current = oneShot;
|
|
161
|
+
setBtwActive(true);
|
|
162
|
+
appendLog('system', 'btw mode', oneShot ? 'One-shot btw — will revert after response.' : 'Entered btw mode. Press Esc to exit and revert.');
|
|
163
|
+
return true;
|
|
164
|
+
}, [appendLog, logs.length]);
|
|
165
|
+
const exitBtw = useCallback(() => {
|
|
166
|
+
const agent = agentRef.current;
|
|
167
|
+
const snapshot = btwSnapshotRef.current;
|
|
168
|
+
if (!agent || !snapshot)
|
|
169
|
+
return;
|
|
170
|
+
agent.restore(snapshot);
|
|
171
|
+
btwSnapshotRef.current = null;
|
|
172
|
+
btwOneShotRef.current = false;
|
|
173
|
+
setBtwActive(false);
|
|
174
|
+
liveAssistant.clear();
|
|
175
|
+
setPendingTools([]);
|
|
176
|
+
setLogs((current) => current.slice(0, btwLogCountRef.current));
|
|
177
|
+
setLogScrollOffset(0);
|
|
178
|
+
setStatus(runningRef.current ? 'Running' : 'Idle');
|
|
179
|
+
}, [liveAssistant]);
|
|
179
180
|
const rebuildAgent = useCallback(async (nextConfig, note) => {
|
|
181
|
+
if (btwSnapshotRef.current) {
|
|
182
|
+
btwSnapshotRef.current = null;
|
|
183
|
+
btwOneShotRef.current = false;
|
|
184
|
+
setBtwActive(false);
|
|
185
|
+
setLogs((current) => current.slice(0, btwLogCountRef.current));
|
|
186
|
+
}
|
|
180
187
|
const buildToken = ++buildTokenRef.current;
|
|
181
188
|
if (nextConfig.sessionId !== sessionIdRef.current) {
|
|
182
189
|
permissionMemoryRef.current.clear();
|
|
@@ -185,7 +192,7 @@ export function App({ initialConfig }) {
|
|
|
185
192
|
setStatus('Rebuilding agent');
|
|
186
193
|
setRunning(false);
|
|
187
194
|
setPendingTools([]);
|
|
188
|
-
|
|
195
|
+
liveAssistant.clear();
|
|
189
196
|
unsubscribeRef.current?.();
|
|
190
197
|
unsubscribeRef.current = null;
|
|
191
198
|
const previousAgent = agentRef.current;
|
|
@@ -244,9 +251,17 @@ export function App({ initialConfig }) {
|
|
|
244
251
|
].join('\n'),
|
|
245
252
|
items: [
|
|
246
253
|
{ id: 'allow', label: 'Allow once', description: 'Run this tool call and continue the current turn.' },
|
|
247
|
-
{
|
|
254
|
+
{
|
|
255
|
+
id: 'allow-session',
|
|
256
|
+
label: 'Always allow this session',
|
|
257
|
+
description: 'Auto-approve future calls to this tool name in the current session.',
|
|
258
|
+
},
|
|
248
259
|
{ id: 'deny', label: 'Deny', description: 'Block this tool call and let the agent recover.' },
|
|
249
|
-
{
|
|
260
|
+
{
|
|
261
|
+
id: 'deny-session',
|
|
262
|
+
label: 'Always deny this session',
|
|
263
|
+
description: 'Auto-deny future calls to this tool name in the current session.',
|
|
264
|
+
},
|
|
250
265
|
],
|
|
251
266
|
selectedId: 'allow',
|
|
252
267
|
});
|
|
@@ -278,17 +293,18 @@ export function App({ initialConfig }) {
|
|
|
278
293
|
unsubscribeRef.current = codingAgent.agent.subscribe((event) => {
|
|
279
294
|
handleAgentEvent(event, {
|
|
280
295
|
getAgentName: () => agentNameRef.current,
|
|
281
|
-
getLiveAssistantText: () =>
|
|
282
|
-
flushLiveAssistant();
|
|
283
|
-
return liveAssistantRef.current;
|
|
284
|
-
},
|
|
296
|
+
getLiveAssistantText: () => liveAssistant.getText(),
|
|
285
297
|
appendLog,
|
|
286
|
-
appendLiveAssistantDelta,
|
|
287
|
-
clearLiveAssistant,
|
|
298
|
+
appendLiveAssistantDelta: liveAssistant.appendDelta,
|
|
299
|
+
clearLiveAssistant: liveAssistant.clear,
|
|
288
300
|
setPendingTools,
|
|
289
301
|
setRunning,
|
|
290
302
|
setStatus,
|
|
291
303
|
});
|
|
304
|
+
// Auto-exit btw mode after one-shot completes
|
|
305
|
+
if (event.type === 'agent_end' && btwSnapshotRef.current && btwOneShotRef.current) {
|
|
306
|
+
setTimeout(() => exitBtw(), 0);
|
|
307
|
+
}
|
|
292
308
|
});
|
|
293
309
|
appendLog('system', 'Agent ready', note);
|
|
294
310
|
setStatus('Idle');
|
|
@@ -302,7 +318,7 @@ export function App({ initialConfig }) {
|
|
|
302
318
|
setIsRebuilding(false);
|
|
303
319
|
}
|
|
304
320
|
}
|
|
305
|
-
}, [appendLog, openPicker, openPrompt]);
|
|
321
|
+
}, [appendLog, exitBtw, liveAssistant, openPicker, openPrompt]);
|
|
306
322
|
useEffect(() => {
|
|
307
323
|
void rebuildAgent(initialConfig, [
|
|
308
324
|
`Agent ${initialConfig.agentName}`,
|
|
@@ -315,16 +331,13 @@ export function App({ initialConfig }) {
|
|
|
315
331
|
mountedRef.current = false;
|
|
316
332
|
unsubscribeRef.current?.();
|
|
317
333
|
unsubscribeRef.current = null;
|
|
318
|
-
|
|
319
|
-
clearTimeout(liveAssistantFlushTimerRef.current);
|
|
320
|
-
liveAssistantFlushTimerRef.current = null;
|
|
321
|
-
}
|
|
334
|
+
liveAssistant.cleanup();
|
|
322
335
|
if (agentRef.current) {
|
|
323
336
|
void agentRef.current.dispose();
|
|
324
337
|
agentRef.current = null;
|
|
325
338
|
}
|
|
326
339
|
};
|
|
327
|
-
}, [
|
|
340
|
+
}, [initialConfig, liveAssistant, rebuildAgent]);
|
|
328
341
|
const commandContext = useMemo(() => ({
|
|
329
342
|
config,
|
|
330
343
|
running,
|
|
@@ -334,8 +347,14 @@ export function App({ initialConfig }) {
|
|
|
334
347
|
rebuildAgent,
|
|
335
348
|
setConfig: (updater) => setConfig(updater),
|
|
336
349
|
resetAgent: () => {
|
|
350
|
+
if (btwSnapshotRef.current) {
|
|
351
|
+
btwSnapshotRef.current = null;
|
|
352
|
+
btwOneShotRef.current = false;
|
|
353
|
+
setBtwActive(false);
|
|
354
|
+
setLogs((current) => current.slice(0, btwLogCountRef.current));
|
|
355
|
+
}
|
|
337
356
|
agentRef.current?.agent.reset();
|
|
338
|
-
|
|
357
|
+
liveAssistant.clear();
|
|
339
358
|
setPendingTools([]);
|
|
340
359
|
},
|
|
341
360
|
abortAgent: () => {
|
|
@@ -348,7 +367,7 @@ export function App({ initialConfig }) {
|
|
|
348
367
|
},
|
|
349
368
|
clearLogs: () => {
|
|
350
369
|
setLogs([]);
|
|
351
|
-
|
|
370
|
+
liveAssistant.clear();
|
|
352
371
|
setLogScrollOffset(0);
|
|
353
372
|
},
|
|
354
373
|
getCostSummary: () => {
|
|
@@ -359,6 +378,8 @@ export function App({ initialConfig }) {
|
|
|
359
378
|
return { tokens: total.tokens, cost: total.cost };
|
|
360
379
|
},
|
|
361
380
|
jumpTranscript: (target) => jumpTranscriptTo(target),
|
|
381
|
+
enterBtw,
|
|
382
|
+
isBtwActive: () => btwSnapshotRef.current !== null,
|
|
362
383
|
promptAgent: async (message) => {
|
|
363
384
|
const agent = agentRef.current;
|
|
364
385
|
if (!agent) {
|
|
@@ -376,15 +397,25 @@ export function App({ initialConfig }) {
|
|
|
376
397
|
}
|
|
377
398
|
},
|
|
378
399
|
exit,
|
|
379
|
-
}), [
|
|
400
|
+
}), [
|
|
401
|
+
appendLog,
|
|
402
|
+
enterBtw,
|
|
403
|
+
liveAssistant,
|
|
404
|
+
closeRequest,
|
|
405
|
+
config,
|
|
406
|
+
exit,
|
|
407
|
+
jumpTranscriptTo,
|
|
408
|
+
openPicker,
|
|
409
|
+
openPrompt,
|
|
410
|
+
rebuildAgent,
|
|
411
|
+
running,
|
|
412
|
+
]);
|
|
380
413
|
const submitComposer = useCallback(async () => {
|
|
381
414
|
const trimmed = composer.value.trim();
|
|
382
415
|
if (!trimmed)
|
|
383
416
|
return;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
setComposerHistoryIndex(null);
|
|
387
|
-
setComposerDraftBeforeHistory(null);
|
|
417
|
+
composerHook.remember(composer.value);
|
|
418
|
+
composerHook.clear();
|
|
388
419
|
setLogScrollOffset(0);
|
|
389
420
|
if (trimmed.startsWith('/')) {
|
|
390
421
|
await handleSlashCommand(trimmed, commandContext);
|
|
@@ -412,10 +443,12 @@ export function App({ initialConfig }) {
|
|
|
412
443
|
appendLog('error', 'Agent error', errorMessage(error));
|
|
413
444
|
setStatus('Error');
|
|
414
445
|
}
|
|
415
|
-
}, [appendLog, commandContext, composer.value,
|
|
446
|
+
}, [appendLog, commandContext, composer.value, composerHook, isRebuilding, running]);
|
|
416
447
|
const slashCommandItems = useMemo(() => getSlashCommandSuggestions(composer.value, running), [composer.value, running]);
|
|
417
448
|
const slashMenuVisible = currentInputMode === 'composer' && slashCommandItems.length > 0;
|
|
418
|
-
const selectedSlashCommand = slashMenuVisible
|
|
449
|
+
const selectedSlashCommand = slashMenuVisible
|
|
450
|
+
? slashCommandItems[Math.min(slashMenuSelectedIndex, slashCommandItems.length - 1)]
|
|
451
|
+
: null;
|
|
419
452
|
useEffect(() => {
|
|
420
453
|
setSlashMenuSelectedIndex((current) => {
|
|
421
454
|
if (slashCommandItems.length === 0)
|
|
@@ -423,11 +456,6 @@ export function App({ initialConfig }) {
|
|
|
423
456
|
return Math.min(current, slashCommandItems.length - 1);
|
|
424
457
|
});
|
|
425
458
|
}, [slashCommandItems]);
|
|
426
|
-
const applySlashCommandSelection = useCallback((insertText) => {
|
|
427
|
-
setComposer({ value: insertText, cursor: insertText.length });
|
|
428
|
-
setComposerHistoryIndex(null);
|
|
429
|
-
setComposerDraftBeforeHistory(null);
|
|
430
|
-
}, []);
|
|
431
459
|
useInput((input, key) => {
|
|
432
460
|
if (key.ctrl && input === 'c') {
|
|
433
461
|
if (running && agentRef.current) {
|
|
@@ -486,16 +514,17 @@ export function App({ initialConfig }) {
|
|
|
486
514
|
if (key.upArrow || key.downArrow || key.tab) {
|
|
487
515
|
return;
|
|
488
516
|
}
|
|
489
|
-
setPromptState((current) => current ? editBuffer(current, input, { ...key, escape: key.escape }) : current);
|
|
517
|
+
setPromptState((current) => (current ? editBuffer(current, input, { ...key, escape: key.escape }) : current));
|
|
490
518
|
return;
|
|
491
519
|
}
|
|
492
520
|
if (key.escape) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
521
|
+
if (btwActive) {
|
|
522
|
+
exitBtw();
|
|
523
|
+
}
|
|
524
|
+
composerHook.clear();
|
|
496
525
|
return;
|
|
497
526
|
}
|
|
498
|
-
const visibleLogRows = visibleLogRowsFor(promptState, pickerState, liveAssistant, running, slashMenuVisible ? slashCommandItems.length : 0);
|
|
527
|
+
const visibleLogRows = visibleLogRowsFor(promptState, pickerState, liveAssistant.text, running, slashMenuVisible ? slashCommandItems.length : 0);
|
|
499
528
|
const scrollStep = Math.max(1, visibleLogRows - 2);
|
|
500
529
|
if (key.pageUp) {
|
|
501
530
|
setLogScrollOffset((current) => stepTranscriptOffset(current, scrollStep, logs.length, visibleLogRows));
|
|
@@ -522,62 +551,32 @@ export function App({ initialConfig }) {
|
|
|
522
551
|
return;
|
|
523
552
|
}
|
|
524
553
|
if (slashMenuVisible && key.tab && selectedSlashCommand) {
|
|
525
|
-
|
|
554
|
+
composerHook.applySelection(selectedSlashCommand.insertText);
|
|
526
555
|
return;
|
|
527
556
|
}
|
|
528
557
|
if (key.ctrl && input === 'j') {
|
|
529
|
-
|
|
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);
|
|
558
|
+
composerHook.insertNewline();
|
|
535
559
|
return;
|
|
536
560
|
}
|
|
537
561
|
if (key.return && key.shift) {
|
|
538
|
-
|
|
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);
|
|
562
|
+
composerHook.insertNewline();
|
|
544
563
|
return;
|
|
545
564
|
}
|
|
546
565
|
if (key.upArrow) {
|
|
547
|
-
|
|
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
|
-
});
|
|
566
|
+
composerHook.navigateHistory('up');
|
|
556
567
|
return;
|
|
557
568
|
}
|
|
558
569
|
if (key.downArrow) {
|
|
559
|
-
|
|
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);
|
|
570
|
+
composerHook.navigateHistory('down');
|
|
571
571
|
return;
|
|
572
572
|
}
|
|
573
573
|
if (key.return) {
|
|
574
574
|
if (slashMenuVisible && selectedSlashCommand) {
|
|
575
575
|
const trimmed = composer.value.trim();
|
|
576
576
|
const hasArgs = /\s/.test(trimmed.slice(1));
|
|
577
|
-
const needsCompletion = !hasArgs && (trimmed !== selectedSlashCommand.commandText ||
|
|
578
|
-
selectedSlashCommand.insertText.endsWith(' '));
|
|
577
|
+
const needsCompletion = !hasArgs && (trimmed !== selectedSlashCommand.commandText || selectedSlashCommand.insertText.endsWith(' '));
|
|
579
578
|
if (needsCompletion) {
|
|
580
|
-
|
|
579
|
+
composerHook.applySelection(selectedSlashCommand.insertText);
|
|
581
580
|
return;
|
|
582
581
|
}
|
|
583
582
|
}
|
|
@@ -590,13 +589,12 @@ export function App({ initialConfig }) {
|
|
|
590
589
|
setComposer((current) => {
|
|
591
590
|
const next = editBuffer(current, input, { ...key, escape: key.escape });
|
|
592
591
|
if (next.value !== current.value || next.cursor !== current.cursor) {
|
|
593
|
-
|
|
594
|
-
setComposerDraftBeforeHistory(null);
|
|
592
|
+
composerHook.resetHistoryNavigation();
|
|
595
593
|
}
|
|
596
594
|
return next;
|
|
597
595
|
});
|
|
598
596
|
});
|
|
599
|
-
const visibleLogRows = visibleLogRowsFor(promptState, pickerState, liveAssistant, running, slashMenuVisible ? slashCommandItems.length : 0);
|
|
597
|
+
const visibleLogRows = visibleLogRowsFor(promptState, pickerState, liveAssistant.text, running, slashMenuVisible ? slashCommandItems.length : 0);
|
|
600
598
|
const transcriptWindow = getTranscriptWindow(logs.length, visibleLogRows, logScrollOffset);
|
|
601
599
|
const renderedLogs = logs.slice(transcriptWindow.start, transcriptWindow.end);
|
|
602
600
|
const hasContentAbove = transcriptWindow.start > 0;
|
|
@@ -604,5 +602,5 @@ export function App({ initialConfig }) {
|
|
|
604
602
|
const rangeText = logs.length > 0 ? `${transcriptWindow.start + 1}-${transcriptWindow.end} of ${logs.length}` : '';
|
|
605
603
|
const transcriptTopRight = [rangeText, hasContentAbove ? '▲' : ''].filter(Boolean).join(' ') || undefined;
|
|
606
604
|
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:
|
|
605
|
+
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, termWidth: termWidth }) }), _jsx(Box, { flexGrow: 1, flexDirection: "column", marginTop: 1, minHeight: 0, children: _jsx(Frame, { borderColor: btwActive ? 'magenta' : 'blue', topLeft: btwActive ? 'Transcript (btw)' : 'Transcript', topRight: transcriptTopRight, bottomRight: transcriptBottomRight, termWidth: termWidth, children: _jsxs(Box, { flexDirection: "row", flexGrow: 1, minHeight: 0, children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [renderedLogs.length === 0 && !liveAssistant.text && !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, termWidth: termWidth }, entry.id))), liveAssistant.text ? (_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.text, showSpinner: running, spinner: _jsx(Spinner, { frame: spinnerFrame }) }) })] })) : null, !liveAssistant.text && running ? (_jsxs(Box, { flexShrink: 0, children: [_jsxs(Text, { color: "cyanBright", bold: true, children: ["\u25CF", ' '] }), _jsxs(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 ? (_jsx(ComposerPanel, { value: composerHook.composer.value, cursor: composerHook.composer.cursor, agentName: config.agentName, running: running, btwActive: btwActive, slashMenuVisible: slashMenuVisible, slashCommandItems: slashCommandItems, slashMenuSelectedIndex: slashMenuSelectedIndex, termWidth: termWidth })) : null, _jsx(StatusBar, { running: running, status: status, spinnerFrame: spinnerFrame, btwActive: btwActive })] })] }));
|
|
608
606
|
}
|