klyro 0.1.8 → 0.1.10
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/dist/tui/activity-line.d.ts +19 -0
- package/dist/tui/activity-line.js +30 -0
- package/dist/tui/app.d.ts +2 -2
- package/dist/tui/app.fullscreen.d.ts +25 -0
- package/dist/tui/app.fullscreen.js +97 -0
- package/dist/tui/app.inline.d.ts +29 -0
- package/dist/tui/app.inline.js +336 -0
- package/dist/tui/app.js +56 -260
- package/dist/tui/app.legacy.d.ts +25 -0
- package/dist/tui/app.legacy.js +337 -0
- package/dist/tui/app.test.js +10 -5
- package/dist/tui/banner.d.ts +28 -0
- package/dist/tui/banner.js +15 -0
- package/dist/tui/components/Header.d.ts +7 -0
- package/dist/tui/components/Header.js +5 -0
- package/dist/tui/input-box.d.ts +21 -0
- package/dist/tui/input-box.js +31 -0
- package/dist/tui/snapshot.test.js +16 -7
- package/dist/tui/state.d.ts +66 -0
- package/dist/tui/state.js +20 -0
- package/dist/tui/thinking-block.d.ts +12 -0
- package/dist/tui/thinking-block.js +11 -0
- package/dist/tui/tokens.d.ts +67 -0
- package/dist/tui/tokens.js +88 -0
- package/package.json +1 -1
package/dist/tui/app.js
CHANGED
|
@@ -1,77 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Klyro Full-Screen TUI — TUI_DESIGN.md §2, §24, §38 (Phase 1-4)
|
|
4
|
+
* Full viewport, conversation, input, status bar — professional, dense, terminal-native
|
|
5
5
|
*/
|
|
6
|
-
import { useState,
|
|
7
|
-
import { Box, Text, useInput } from 'ink';
|
|
8
|
-
import { StatusLine } from './status.js';
|
|
6
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
7
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
9
8
|
import { Transcript } from './transcript.js';
|
|
10
|
-
import {
|
|
11
|
-
import { ApprovalModal, TuiApprovalBridge } from './approval.js';
|
|
9
|
+
import { TuiApprovalBridge } from './approval.js';
|
|
12
10
|
import { PlanView } from './plan.js';
|
|
13
11
|
import { parse as parseSlash } from '../cli/slash/parser.js';
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import { spawnSync } from 'node:child_process';
|
|
18
|
-
let _itemCounter = 0;
|
|
19
|
-
function nextId(prefix) {
|
|
20
|
-
_itemCounter += 1;
|
|
21
|
-
return `${prefix}-${_itemCounter}`;
|
|
22
|
-
}
|
|
23
|
-
function getGitBranch(cwd) {
|
|
24
|
-
try {
|
|
25
|
-
const r = spawnSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf-8', timeout: 800, windowsHide: true });
|
|
26
|
-
if (r.status === 0 && r.stdout)
|
|
27
|
-
return r.stdout.trim().slice(0, 40);
|
|
28
|
-
}
|
|
29
|
-
catch { /* ignore */ }
|
|
30
|
-
return '';
|
|
31
|
-
}
|
|
32
|
-
function getHistoryPath() {
|
|
33
|
-
const home = os.homedir() || process.cwd();
|
|
34
|
-
return path.join(home, '.klyro', 'history');
|
|
35
|
-
}
|
|
36
|
-
function loadHistory(cwd) {
|
|
37
|
-
try {
|
|
38
|
-
const raw = fs.readFileSync(getHistoryPath(), 'utf-8');
|
|
39
|
-
const lines = raw.split('\n').filter(Boolean);
|
|
40
|
-
const out = [];
|
|
41
|
-
for (const line of lines) {
|
|
42
|
-
try {
|
|
43
|
-
const obj = JSON.parse(line);
|
|
44
|
-
if (obj.cwd === cwd && typeof obj.text === 'string')
|
|
45
|
-
out.push(obj.text);
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// legacy plain text per line
|
|
49
|
-
if (line.trim())
|
|
50
|
-
out.push(line.trim());
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return out.slice(-200);
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function appendHistory(cwd, text) {
|
|
60
|
-
try {
|
|
61
|
-
const p = getHistoryPath();
|
|
62
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
63
|
-
const entry = JSON.stringify({ cwd, text, ts: Date.now() });
|
|
64
|
-
fs.appendFileSync(p, entry + '\n', 'utf-8');
|
|
65
|
-
}
|
|
66
|
-
catch { /* ignore */ }
|
|
67
|
-
}
|
|
12
|
+
import { tokens } from './tokens.js';
|
|
13
|
+
let _id = 0;
|
|
14
|
+
function nextId(p) { _id++; return `${p}-${_id}`; }
|
|
68
15
|
export function App(props) {
|
|
16
|
+
const { stdout } = useStdout();
|
|
69
17
|
const [transcript, setTranscript] = useState(props.initialTranscript ?? []);
|
|
70
18
|
const [input, setInput] = useState('');
|
|
71
19
|
const [bridge] = useState(() => props.approvalBridge ?? new TuiApprovalBridge());
|
|
72
20
|
const [awaitingApproval, setAwaitingApproval] = useState(false);
|
|
73
21
|
const [plan, setPlan] = useState([]);
|
|
74
|
-
const [planExpanded, setPlanExpanded] = useState(false);
|
|
75
22
|
const [status, setStatus] = useState({
|
|
76
23
|
model: props.initialModel,
|
|
77
24
|
step: 0,
|
|
@@ -82,38 +29,41 @@ export function App(props) {
|
|
|
82
29
|
status: 'idle',
|
|
83
30
|
...props.initialStatus,
|
|
84
31
|
});
|
|
85
|
-
const [
|
|
86
|
-
const historyIndexRef = useRef(-1);
|
|
87
|
-
const lastCtrlCRef = useRef(0);
|
|
88
|
-
const [gitBranch, setGitBranch] = useState(() => getGitBranch(props.cwd));
|
|
32
|
+
const [elapsed, setElapsed] = useState(0);
|
|
89
33
|
const [queued, setQueued] = useState(null);
|
|
34
|
+
useEffect(() => bridge.subscribe((p) => setAwaitingApproval(p !== null)), [bridge]);
|
|
35
|
+
// 2.4 — send queued when idle
|
|
90
36
|
useEffect(() => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
37
|
+
if (queued && status.status !== 'running' && !awaitingApproval) {
|
|
38
|
+
const toSend = queued;
|
|
39
|
+
setQueued(null);
|
|
40
|
+
const item = { id: nextId('text'), kind: 'text', text: toSend, role: 'user' };
|
|
41
|
+
setTranscript((prev) => [...prev, item]);
|
|
42
|
+
const cmd = parseSlash(toSend.trim());
|
|
43
|
+
if (cmd.kind === 'prompt')
|
|
44
|
+
void props.onPrompt(cmd.text);
|
|
45
|
+
else
|
|
46
|
+
void props.onSlash(cmd);
|
|
47
|
+
}
|
|
48
|
+
}, [queued, status.status, awaitingApproval]);
|
|
94
49
|
useEffect(() => {
|
|
95
|
-
|
|
96
|
-
|
|
50
|
+
if (status.status !== 'running')
|
|
51
|
+
return;
|
|
52
|
+
const start = Date.now() - elapsed;
|
|
53
|
+
const t = setInterval(() => setElapsed(Date.now() - start), 1000);
|
|
54
|
+
return () => clearInterval(t);
|
|
55
|
+
}, [status.status, elapsed]);
|
|
97
56
|
const append = useCallback((item) => {
|
|
98
57
|
setTranscript((prev) => {
|
|
99
58
|
const last = prev[prev.length - 1];
|
|
100
|
-
if (last?.kind === 'text' &&
|
|
101
|
-
item.kind === 'text' &&
|
|
102
|
-
last.role === 'assistant' &&
|
|
103
|
-
item.role === 'assistant' &&
|
|
104
|
-
last.id === item.id) {
|
|
59
|
+
if (last?.kind === 'text' && item.kind === 'text' && last.role === 'assistant' && item.role === 'assistant' && last.id === item.id) {
|
|
105
60
|
return [...prev.slice(0, -1), { ...last, text: last.text + item.text }];
|
|
106
61
|
}
|
|
107
62
|
return [...prev, item];
|
|
108
63
|
});
|
|
109
64
|
}, []);
|
|
110
|
-
const updateStatus = useCallback((s) => {
|
|
111
|
-
|
|
112
|
-
}, []);
|
|
113
|
-
const updatePlan = useCallback((p) => {
|
|
114
|
-
setPlan(p);
|
|
115
|
-
setPlanExpanded(true);
|
|
116
|
-
}, []);
|
|
65
|
+
const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
|
|
66
|
+
const updatePlan = useCallback((p) => setPlan(p), []);
|
|
117
67
|
const onMountedRef = useRef(props.onMounted);
|
|
118
68
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
119
69
|
useEffect(() => {
|
|
@@ -127,37 +77,7 @@ export function App(props) {
|
|
|
127
77
|
delete globalThis.__klyroAppPlan;
|
|
128
78
|
};
|
|
129
79
|
}, [append, updateStatus, updatePlan]);
|
|
130
|
-
//
|
|
131
|
-
const [rawModeWarning, setRawModeWarning] = useState(null);
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
try {
|
|
134
|
-
const stdin = process.stdin;
|
|
135
|
-
if (stdin.isTTY && typeof stdin.setRawMode !== 'function') {
|
|
136
|
-
setRawModeWarning('Raw mode not available (mintty) — input may be limited');
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch { /* ignore */ }
|
|
140
|
-
}, []);
|
|
141
|
-
// Queue next message if typed during stream (2.4)
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
if (queued && status.status !== 'running' && !awaitingApproval) {
|
|
144
|
-
const toSend = queued;
|
|
145
|
-
setQueued(null);
|
|
146
|
-
const trimmed = toSend.trim();
|
|
147
|
-
if (!trimmed)
|
|
148
|
-
return;
|
|
149
|
-
append({ id: nextId('text'), kind: 'text', text: toSend, role: 'user' });
|
|
150
|
-
const cmd = parseSlash(trimmed);
|
|
151
|
-
if (cmd.kind === 'prompt')
|
|
152
|
-
void props.onPrompt(cmd.text);
|
|
153
|
-
else if (cmd.kind === 'plan') {
|
|
154
|
-
if (plan.length > 0)
|
|
155
|
-
setPlanExpanded((v) => !v);
|
|
156
|
-
}
|
|
157
|
-
else
|
|
158
|
-
void props.onSlash(cmd);
|
|
159
|
-
}
|
|
160
|
-
}, [queued, status.status, awaitingApproval, plan.length, append]);
|
|
80
|
+
// Single useInput owner — handles queued when running (2.4)
|
|
161
81
|
useInput((inputStr, key) => {
|
|
162
82
|
if (awaitingApproval)
|
|
163
83
|
return;
|
|
@@ -167,171 +87,47 @@ export function App(props) {
|
|
|
167
87
|
return;
|
|
168
88
|
}
|
|
169
89
|
if (key.return) {
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
90
|
+
const v = input.trim();
|
|
91
|
+
if (!v)
|
|
172
92
|
return;
|
|
173
|
-
setQueued(
|
|
93
|
+
setQueued(v);
|
|
174
94
|
setInput('');
|
|
175
|
-
|
|
95
|
+
setTranscript((prev) => [...prev, { id: nextId('text'), kind: 'text', text: `queued: ${v.slice(0, 80)}`, role: 'assistant' }]);
|
|
176
96
|
return;
|
|
177
97
|
}
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
// History navigation
|
|
185
|
-
if (key.upArrow) {
|
|
186
|
-
if (history.length === 0)
|
|
98
|
+
if (key.backspace || key.delete) {
|
|
99
|
+
setInput((v) => v.slice(0, -1));
|
|
187
100
|
return;
|
|
188
|
-
if (historyIndexRef.current === -1)
|
|
189
|
-
historyIndexRef.current = history.length - 1;
|
|
190
|
-
else if (historyIndexRef.current > 0)
|
|
191
|
-
historyIndexRef.current--;
|
|
192
|
-
setInput(history[historyIndexRef.current] ?? '');
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (key.downArrow) {
|
|
196
|
-
if (historyIndexRef.current === -1)
|
|
197
|
-
return;
|
|
198
|
-
historyIndexRef.current++;
|
|
199
|
-
if (historyIndexRef.current >= history.length) {
|
|
200
|
-
historyIndexRef.current = -1;
|
|
201
|
-
setInput('');
|
|
202
101
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
// Ctrl+R search — simple: cycle history
|
|
209
|
-
if (key.ctrl && inputStr === 'r') {
|
|
210
|
-
if (history.length === 0)
|
|
211
|
-
return;
|
|
212
|
-
const term = input.toLowerCase();
|
|
213
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
214
|
-
if (history[i].toLowerCase().includes(term)) {
|
|
215
|
-
setInput(history[i]);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
102
|
+
if (!key.ctrl && !key.meta) {
|
|
103
|
+
const norm = inputStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
104
|
+
setInput((v) => v + norm);
|
|
218
105
|
}
|
|
219
106
|
return;
|
|
220
107
|
}
|
|
221
108
|
if (key.return) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const isShiftEnter = key.shift === true;
|
|
225
|
-
if (isShiftEnter || input.endsWith('\\')) {
|
|
226
|
-
// Replace trailing \ with newline, or just add newline for Shift+Enter
|
|
227
|
-
if (input.endsWith('\\'))
|
|
228
|
-
setInput((v) => v.slice(0, -1) + '\n');
|
|
229
|
-
else
|
|
230
|
-
setInput((v) => v + '\n');
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const value = input;
|
|
234
|
-
// Preserve newlines for bracketed paste — don't trim inner newlines, only outer
|
|
235
|
-
const trimmedOuter = value.replace(/^\s+|\s+$/g, '');
|
|
236
|
-
if (!trimmedOuter) {
|
|
237
|
-
setInput('');
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
// 4.4 — handle @path and !cmd and # note without model call
|
|
241
|
-
if (trimmedOuter.startsWith('@')) {
|
|
242
|
-
const atPath = trimmedOuter.slice(1).trim().split(' ')[0] ?? '';
|
|
243
|
-
setInput('');
|
|
244
|
-
append({ id: nextId('text'), kind: 'text', text: `Attached @${atPath} (fuzzy completion stub)`, role: 'assistant' });
|
|
245
|
-
// Still send to model as context, but mark as @ reference
|
|
246
|
-
const atText = `Reference file: ${atPath}`;
|
|
247
|
-
void props.onPrompt(atText);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
if (trimmedOuter.startsWith('!')) {
|
|
251
|
-
const cmdText = trimmedOuter.slice(1).trim();
|
|
252
|
-
setInput('');
|
|
253
|
-
// Run shell without model call, attach output
|
|
254
|
-
import('../tools/shell/shell-exec.js').then(async ({ shellExecTool }) => {
|
|
255
|
-
const { builtinRegistry } = await import('../tools/registry.js');
|
|
256
|
-
const reg = builtinRegistry();
|
|
257
|
-
const r = await reg.execute('shell_exec', { command: cmdText }, { cwd: props.cwd, env: process.env, nonInteractive: true });
|
|
258
|
-
const out = r.ok ? JSON.stringify(r.value).slice(0, 500) : String(r.error.message);
|
|
259
|
-
append({ id: nextId('text'), kind: 'text', text: `!${cmdText}\n${out}`, role: 'assistant' });
|
|
260
|
-
});
|
|
109
|
+
const v = input.trim();
|
|
110
|
+
if (!v)
|
|
261
111
|
return;
|
|
262
|
-
}
|
|
263
|
-
if (trimmedOuter.startsWith('# ')) {
|
|
264
|
-
const note = trimmedOuter.slice(2).trim();
|
|
265
|
-
// Append to .klyro/memory/session-notes.md
|
|
266
|
-
import('node:fs/promises').then(async (fs) => {
|
|
267
|
-
const p = (await import('node:path')).join(props.cwd, '.klyro', 'memory', 'session-notes.md');
|
|
268
|
-
await fs.mkdir((await import('node:path')).dirname(p), { recursive: true });
|
|
269
|
-
await fs.appendFile(p, `- ${note}\n`, 'utf-8');
|
|
270
|
-
});
|
|
271
|
-
setInput('');
|
|
272
|
-
append({ id: nextId('text'), kind: 'text', text: `Note saved: ${note}`, role: 'assistant' });
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
// Check for Ctrl+C double at empty prompt handled below, but here handle submit
|
|
276
112
|
setInput('');
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
appendHistory(props.cwd, value);
|
|
282
|
-
return next.slice(-200);
|
|
283
|
-
});
|
|
284
|
-
append({ id: nextId('text'), kind: 'text', text: value, role: 'user' });
|
|
285
|
-
const cmd = parseSlash(trimmedOuter);
|
|
286
|
-
if (cmd.kind === 'prompt') {
|
|
113
|
+
const item = { id: nextId('text'), kind: 'text', text: v, role: 'user' };
|
|
114
|
+
setTranscript((prev) => [...prev, item]);
|
|
115
|
+
const cmd = parseSlash(v);
|
|
116
|
+
if (cmd.kind === 'prompt')
|
|
287
117
|
void props.onPrompt(cmd.text);
|
|
288
|
-
|
|
289
|
-
else if (cmd.kind === 'plan') {
|
|
290
|
-
if (plan.length > 0)
|
|
291
|
-
setPlanExpanded((v) => !v);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
118
|
+
else
|
|
294
119
|
void props.onSlash(cmd);
|
|
295
|
-
}
|
|
296
120
|
return;
|
|
297
121
|
}
|
|
298
122
|
if (key.backspace || key.delete) {
|
|
299
123
|
setInput((v) => v.slice(0, -1));
|
|
300
124
|
return;
|
|
301
125
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (input === '') {
|
|
305
|
-
const now = Date.now();
|
|
306
|
-
if (now - lastCtrlCRef.current < 1500) {
|
|
307
|
-
void props.onSlash({ kind: 'quit' });
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
lastCtrlCRef.current = now;
|
|
311
|
-
append({ id: nextId('text'), kind: 'text', text: '(press Ctrl+C again to exit)', role: 'assistant' });
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
// Single Ctrl+C cancels input
|
|
315
|
-
setInput('');
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (key.ctrl && inputStr === 'd') {
|
|
319
|
-
void props.onSlash({ kind: 'quit' });
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
if (key.ctrl && inputStr === 'l') {
|
|
323
|
-
// Clear — keep session but clear transcript marker
|
|
324
|
-
append({ id: nextId('text'), kind: 'text', text: '(cleared)', role: 'assistant' });
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
// Handle bracketed paste: inputStr may contain \r\n or multiple lines
|
|
328
|
-
if (!key.ctrl && !key.meta) {
|
|
329
|
-
// Preserve all characters including newlines from paste
|
|
330
|
-
// Normalize \r\n to \n
|
|
331
|
-
const normalized = inputStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
332
|
-
setInput((v) => v + normalized);
|
|
333
|
-
}
|
|
126
|
+
if (!key.ctrl && !key.meta)
|
|
127
|
+
setInput((v) => v + inputStr);
|
|
334
128
|
});
|
|
335
|
-
const
|
|
336
|
-
|
|
129
|
+
const width = stdout?.columns ?? 100;
|
|
130
|
+
const height = stdout?.rows ?? 30;
|
|
131
|
+
const isSmall = width < 80;
|
|
132
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height - 1, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: "KLYRO v0.1.10" }), _jsxs(Text, { color: tokens.ansi.muted, children: [status.model, " \u00B7 API Usage Billing \u00B7 step ", status.step, "/", status.maxSteps, " \u00B7 ", status.status, " \u00B7 repairs ", status.repairs] }), _jsx(Text, { color: tokens.ansi.muted, children: props.cwd })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingX: 1, paddingY: 1, children: [transcript.length === 0 ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: tokens.ansi.muted, children: "No conversation yet. Try \"fix the failing login test\"" }) })) : (transcript.map((item) => (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: item.kind === 'text' && item.role === 'user' ? (_jsxs(Text, { children: ["\u203A ", item.text] })) : item.kind === 'text' ? (_jsxs(Text, { children: [" ", item.text] })) : item.kind === 'tool' ? (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsxs(Text, { children: [item.name, " ", item.isError ? '✗' : '✓', " ", item.latencyMs ?? 0, "ms"] }), item.result ? _jsx(Text, { color: tokens.ansi.muted, children: String(item.result).slice(0, 200) }) : null] })) : item.kind === 'diff' ? (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: item.summary ?? 'Diff' }), item.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: tokens.ansi.info, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { color: l.kind === 'add' ? tokens.ansi.success : l.kind === 'remove' ? tokens.ansi.error : tokens.ansi.muted, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] })) : (_jsx(Transcript, { items: [item] })) }, item.id)))), status.status === 'running' ? (_jsxs(Box, { children: [_jsx(Text, { color: tokens.ansi.info, children: "\u2726 Thinking..." }), _jsxs(Text, { color: tokens.ansi.muted, children: [" \u00B7 ", Math.round(elapsed / 1000), "s"] })] })) : null, plan.length > 0 ? _jsx(PlanView, { steps: plan, expanded: false, onToggle: () => { } }) : null] }), _jsxs(Box, { borderStyle: "single", borderColor: tokens.ansi.accent, paddingX: 1, children: [_jsx(Text, { children: "\u203A " }), _jsxs(Text, { children: [input, "\u258F"] })] }), _jsxs(Box, { justifyContent: "space-between", paddingX: 1, borderStyle: "single", borderColor: tokens.ansi.border, children: [_jsxs(Text, { color: tokens.ansi.muted, children: [status.model, " \u00B7 ", status.usageInput + status.usageOutput, " tokens \u00B7 $", (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015).toFixed(2), " \u00B7 ", Math.round(elapsed / 1000), "s"] }), _jsx(Text, { color: tokens.ansi.muted, children: isSmall ? 'Ctrl+C interrupt' : 'Ctrl+C interrupt · Ctrl+O expand · ↑↓ scroll' })] })] }));
|
|
337
133
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink app root — status line + scrollable transcript + input box.
|
|
3
|
+
* 1.4: history per project, multiline, Ctrl+C double, slash registry, Windows handling
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { type StatusSnapshot } from './status.js';
|
|
7
|
+
import { type TranscriptItem } from './transcript.js';
|
|
8
|
+
import { TuiApprovalBridge } from './approval.js';
|
|
9
|
+
import type { PlanStep } from '../agent/runtime.js';
|
|
10
|
+
export interface AppProps {
|
|
11
|
+
initialModel: string;
|
|
12
|
+
maxSteps: number;
|
|
13
|
+
cwd: string;
|
|
14
|
+
onPrompt: (text: string) => void | Promise<void>;
|
|
15
|
+
onSlash: (cmd: import('../cli/slash/parser.js').SlashCommand) => void | Promise<void>;
|
|
16
|
+
initialTranscript?: TranscriptItem[];
|
|
17
|
+
initialStatus?: Partial<StatusSnapshot>;
|
|
18
|
+
approvalBridge?: TuiApprovalBridge;
|
|
19
|
+
onMounted?: (hooks: {
|
|
20
|
+
append: (i: TranscriptItem) => void;
|
|
21
|
+
updateStatus: (s: Partial<StatusSnapshot>) => void;
|
|
22
|
+
updatePlan: (p: PlanStep[]) => void;
|
|
23
|
+
}) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function App(props: AppProps): React.JSX.Element;
|