s-app-scoder-agent 0.8.21 → 0.9.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 +31 -5
- package/dist/agent.js +662 -65
- package/dist/agents-guide.js +201 -0
- package/dist/agents.js +76 -3
- package/dist/compaction.js +12 -4
- package/dist/config.js +16 -2
- package/dist/context.js +15 -3
- package/dist/diagnostics.js +188 -0
- package/dist/index.js +277 -6
- package/dist/mcp-import.js +204 -0
- package/dist/mcp.js +553 -54
- package/dist/memory.js +140 -0
- package/dist/permissions.js +18 -3
- package/dist/prompt.js +204 -84
- package/dist/repo-map.js +177 -0
- package/dist/sandbox.js +6 -5
- package/dist/session.js +29 -8
- package/dist/skills.js +211 -62
- package/dist/snapshot.js +5 -0
- package/dist/steering.js +4 -0
- package/dist/tools.js +374 -57
- package/dist/tui/app.js +79 -28
- package/dist/tui/ask-user-modal.js +25 -0
- package/dist/tui/chat-views.js +15 -8
- package/dist/tui/chrome.js +2 -1
- package/dist/tui/command-registry.js +284 -9
- package/dist/tui/commands.js +224 -37
- package/dist/tui/markdown.js +170 -12
- package/dist/tui/sidebar.js +36 -13
- package/dist/tui/state.js +101 -2
- package/dist/tui/theme.js +4 -0
- package/dist/tui/transcript.js +145 -0
- package/dist/watch.js +8 -0
- package/dist/webfetch.js +5 -2
- package/package.json +1 -1
package/dist/tui/app.js
CHANGED
|
@@ -18,8 +18,8 @@ import { setQuiet } from "../ui.js";
|
|
|
18
18
|
import { discoverSkills } from "../skills.js";
|
|
19
19
|
import { discoverAgents } from "../agents.js";
|
|
20
20
|
import { resolveSessionId } from "../session.js";
|
|
21
|
-
import { applyEvent, initialState, markAborted } from "./state.js";
|
|
22
|
-
import { parseSlashCommand } from "./commands.js";
|
|
21
|
+
import { applyEvent, initialState, markAborted, estimateItemRows } from "./state.js";
|
|
22
|
+
import { parseSlashCommand, SLASH_COMMANDS, formatSlashHelp } from "./commands.js";
|
|
23
23
|
import { dispatchCommand } from "./command-registry.js";
|
|
24
24
|
import { ChatItemView } from "./chat-views.js"; // T2
|
|
25
25
|
import { BuildRail, StatusBar, SIDEBAR_MIN_COLS } from "./chrome.js"; // T2
|
|
@@ -33,26 +33,31 @@ import { PermissionModal } from "./permission-modal.js"; // T2
|
|
|
33
33
|
import { Sidebar } from "./sidebar.js"; // sidebar opencode
|
|
34
34
|
import { getVersion } from "../version.js";
|
|
35
35
|
export function TuiApp({ config, initialTask }) {
|
|
36
|
+
// v2: the effective config can be hot-patched by slash commands (/agent,
|
|
37
|
+
// /permission) — the prop is the initial value, cfg is the live one.
|
|
38
|
+
const [cfg, setCfg] = useState(config);
|
|
36
39
|
const { exit } = useApp();
|
|
37
40
|
// T-09: theme resolve at mount (.scoder/theme.json > builtin). Proxy-based
|
|
38
41
|
// theme reads re-render via a bump.
|
|
39
42
|
const [, bump] = useState(0);
|
|
40
43
|
const bumpFrame = useCallback(() => bump((n) => n + 1), []);
|
|
41
44
|
useEffect(() => {
|
|
42
|
-
setActiveTheme(getTheme(
|
|
45
|
+
setActiveTheme(getTheme(cfg.workdir));
|
|
43
46
|
bumpFrame();
|
|
44
|
-
}, [
|
|
47
|
+
}, [cfg.workdir, bumpFrame]);
|
|
45
48
|
const [state, setState] = useState(initialState);
|
|
46
49
|
const [input, setInput] = useState(initialTask ?? "");
|
|
47
50
|
/** Rows scrolled up from the bottom; 0 = follow the newest content. */
|
|
48
51
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
49
|
-
const [permMode, setPermMode] = useState(
|
|
52
|
+
const [permMode, setPermMode] = useState(cfg.permissionMode);
|
|
50
53
|
// T-03: active permission prompt (modal) — the run blocks until answered.
|
|
51
54
|
const [permPrompt, setPermPrompt] = useState(null);
|
|
52
55
|
const permPromptRef = useRef(permPrompt);
|
|
53
56
|
permPromptRef.current = permPrompt;
|
|
54
57
|
// Session picker modal state (open via Ctrl+S)
|
|
55
58
|
const [sessionPickerOpen, setSessionPickerOpen] = useState(false);
|
|
59
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
60
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
56
61
|
const inputRef = useRef(initialTask ?? "");
|
|
57
62
|
const writeInput = useCallback((v) => {
|
|
58
63
|
inputRef.current = v;
|
|
@@ -113,14 +118,14 @@ export function TuiApp({ config, initialTask }) {
|
|
|
113
118
|
process.stdout.write(MOUSE_DISABLE);
|
|
114
119
|
};
|
|
115
120
|
}, [stdin, setRawMode, isRawModeSupported, wheelScroll]);
|
|
116
|
-
const skills = useMemo(() => discoverSkills(
|
|
121
|
+
const skills = useMemo(() => discoverSkills(cfg.workdir), [cfg.workdir]);
|
|
117
122
|
const skillNames = useMemo(() => skills.map((s) => s.name), [skills]);
|
|
118
123
|
// T-02: completion sources (agents + files) computed once per mount.
|
|
119
124
|
const atSources = useMemo(() => ({
|
|
120
125
|
skills: skillNames,
|
|
121
|
-
agents: discoverAgents(
|
|
122
|
-
files: fileIndex(
|
|
123
|
-
}), [
|
|
126
|
+
agents: discoverAgents(cfg.workdir).filter((a) => a.mode !== "subagent" || a.name === "explore").map((a) => a.name),
|
|
127
|
+
files: fileIndex(cfg.workdir),
|
|
128
|
+
}), [cfg.workdir, skillNames]);
|
|
124
129
|
// Mirror of state for the submit path (isFirst, /cost, /model) and the
|
|
125
130
|
// abort handler — kept in sync synchronously by update() below, so it is
|
|
126
131
|
// exact even between renders (event bursts land between them).
|
|
@@ -156,6 +161,17 @@ export function TuiApp({ config, initialTask }) {
|
|
|
156
161
|
return { ...next, items: [...next.items] };
|
|
157
162
|
});
|
|
158
163
|
}, [update]);
|
|
164
|
+
// Realtime tick: re-render at least once per second while a run is active so
|
|
165
|
+
// sidebar metrics (elapsed time, ctx pressure, tok/s during/after a stream)
|
|
166
|
+
// update live instead of only on the next event/command boundary.
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
const id = setInterval(() => {
|
|
169
|
+
if (!runningRef.current)
|
|
170
|
+
return;
|
|
171
|
+
update((s) => ({ ...s }));
|
|
172
|
+
}, 1000);
|
|
173
|
+
return () => clearInterval(id);
|
|
174
|
+
}, [update]);
|
|
159
175
|
const queueEvent = useCallback((ev) => {
|
|
160
176
|
// T-03: permission modal must appear instantly (not batched) — the run
|
|
161
177
|
// blocks on the answer.
|
|
@@ -181,7 +197,7 @@ export function TuiApp({ config, initialTask }) {
|
|
|
181
197
|
pushNotice("agent is running — wait or Esc first", "warn");
|
|
182
198
|
return;
|
|
183
199
|
}
|
|
184
|
-
const s = resolveSessionId(
|
|
200
|
+
const s = resolveSessionId(cfg.workdir, session.id);
|
|
185
201
|
if (!s) {
|
|
186
202
|
pushNotice(`session not found: ${session.id}`, "error");
|
|
187
203
|
return;
|
|
@@ -195,9 +211,9 @@ export function TuiApp({ config, initialTask }) {
|
|
|
195
211
|
? { kind: "user", text: typeof m.content === "string" ? m.content : "" }
|
|
196
212
|
: { kind: "assistant", text: typeof m.content === "string" ? m.content : "", streaming: false }),
|
|
197
213
|
step: s.steps ?? 0,
|
|
198
|
-
maxSteps: s.maxSteps ??
|
|
199
|
-
model: s.model ||
|
|
200
|
-
provider: s.provider ||
|
|
214
|
+
maxSteps: s.maxSteps ?? cfg.maxSteps,
|
|
215
|
+
model: s.model || cfg.model,
|
|
216
|
+
provider: s.provider || cfg.provider,
|
|
201
217
|
status: "idle",
|
|
202
218
|
summary: null,
|
|
203
219
|
}));
|
|
@@ -212,11 +228,12 @@ export function TuiApp({ config, initialTask }) {
|
|
|
212
228
|
runningRef.current = false;
|
|
213
229
|
pushNotice(`resume failed: ${err.message}`, "error");
|
|
214
230
|
}
|
|
215
|
-
}, [
|
|
231
|
+
}, [cfg, createAgent, pushNotice, update]);
|
|
216
232
|
/** T1: CommandHost cho registry dùng chung với REPL. */
|
|
217
233
|
const tuiHost = useCallback(() => ({
|
|
218
234
|
agent: () => agentRef.current,
|
|
219
|
-
config,
|
|
235
|
+
config: cfg,
|
|
236
|
+
updateConfig: (patch) => setCfg((c) => ({ ...c, ...patch })),
|
|
220
237
|
isRunning: () => runningRef.current,
|
|
221
238
|
replaceAgent: (next) => {
|
|
222
239
|
agentRef.current?.abort();
|
|
@@ -228,7 +245,7 @@ export function TuiApp({ config, initialTask }) {
|
|
|
228
245
|
eventQueue.current = [];
|
|
229
246
|
update(() => initialState());
|
|
230
247
|
}
|
|
231
|
-
setPermMode(
|
|
248
|
+
setPermMode(cfg.permissionMode);
|
|
232
249
|
},
|
|
233
250
|
resumeAndRun: async (s) => {
|
|
234
251
|
const agent = createAgent();
|
|
@@ -249,9 +266,24 @@ export function TuiApp({ config, initialTask }) {
|
|
|
249
266
|
runningRef.current = false;
|
|
250
267
|
pushNotice(`resume failed: ${err.message}`, "error");
|
|
251
268
|
}
|
|
252
|
-
return { success: true, steps: 0, buildCmd:
|
|
269
|
+
return { success: true, steps: 0, buildCmd: cfg.buildCmd, summary: "resumed" };
|
|
253
270
|
},
|
|
254
|
-
|
|
271
|
+
/** [Phase 7] Transcript items — used by /export and /search. */
|
|
272
|
+
transcriptItems: () => stateRef.current.items,
|
|
273
|
+
/** [Phase 7] Scroll to a transcript item by index (best-effort row estimation). */
|
|
274
|
+
selectTranscriptItem: (index) => {
|
|
275
|
+
const items = stateRef.current.items;
|
|
276
|
+
if (index < 0 || index >= items.length)
|
|
277
|
+
return;
|
|
278
|
+
// target: show item at index at the TOP of the viewport.
|
|
279
|
+
// Ink contentOffsetY shifts content upward by that many rows.
|
|
280
|
+
let above = 0;
|
|
281
|
+
for (let i = 0; i < index; i++)
|
|
282
|
+
above += estimateItemRows(items[i]);
|
|
283
|
+
const maxScroll = Math.max(0, content.height - viewport.height);
|
|
284
|
+
setScrollOffset(Math.max(0, maxScroll - above));
|
|
285
|
+
},
|
|
286
|
+
}), [cfg, update, pushNotice, createAgent, viewport, content]);
|
|
255
287
|
/** One round of the /review state machine: call the gate, render the result. */
|
|
256
288
|
const reviewStep = useCallback(async (tool, args) => {
|
|
257
289
|
const agent = agentRef.current ?? createAgent();
|
|
@@ -310,7 +342,7 @@ export function TuiApp({ config, initialTask }) {
|
|
|
310
342
|
writeInput("");
|
|
311
343
|
}, [closeStreamingItems, writeInput]);
|
|
312
344
|
const submit = useCallback(async (text) => {
|
|
313
|
-
|
|
345
|
+
let task = text.trim();
|
|
314
346
|
if (!task)
|
|
315
347
|
return;
|
|
316
348
|
// Prompt-gate review in progress — numeric replies drive the flow.
|
|
@@ -336,17 +368,21 @@ export function TuiApp({ config, initialTask }) {
|
|
|
336
368
|
pushNotice("usage: /review <prompt> — rewrites it, you pick original/rewritten/update/cancel", "warn");
|
|
337
369
|
return;
|
|
338
370
|
}
|
|
339
|
-
|
|
340
|
-
|
|
371
|
+
// v2: no hardcoded server/language — any MCP server exposing a
|
|
372
|
+
// review_and_select_prompt tool wins; language is auto-detected by
|
|
373
|
+
// the gate from the prompt itself.
|
|
374
|
+
const servers = Object.keys(cfg.mcpServers ?? {});
|
|
375
|
+
const gateServer = servers.find((s) => s === "prompt-gate") ?? servers.find((s) => /gate/i.test(s));
|
|
376
|
+
if (!gateServer) {
|
|
377
|
+
pushNotice("no MCP prompt-gate server configured — add one exposing review_and_select_prompt to ~/.scoder/config.json", "error");
|
|
341
378
|
return;
|
|
342
379
|
}
|
|
343
380
|
// Context grounded in this session: last user item + current workdir.
|
|
344
381
|
const lastUser = [...stateRef.current.items].reverse().find((i) => i.kind === "user");
|
|
345
|
-
const context = `User is in scoder TUI at ${
|
|
346
|
-
void reviewStep(
|
|
382
|
+
const context = `User is in scoder TUI at ${cfg.workdir}${lastUser ? `; earlier they asked: "${lastUser.text.slice(0, 120)}"` : ""}.`;
|
|
383
|
+
void reviewStep(`mcp__${gateServer}__review_and_select_prompt`, {
|
|
347
384
|
prompt: slash.arg,
|
|
348
385
|
context,
|
|
349
|
-
language: "vietnamese",
|
|
350
386
|
});
|
|
351
387
|
return;
|
|
352
388
|
}
|
|
@@ -453,7 +489,12 @@ export function TuiApp({ config, initialTask }) {
|
|
|
453
489
|
agentRef.current?.abort();
|
|
454
490
|
exit();
|
|
455
491
|
}
|
|
456
|
-
|
|
492
|
+
if (handled && outcome?.task) {
|
|
493
|
+
task = outcome.task;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
457
498
|
}
|
|
458
499
|
if (runningRef.current) {
|
|
459
500
|
// Steer: user input while the agent runs — deliver at next step
|
|
@@ -485,12 +526,12 @@ export function TuiApp({ config, initialTask }) {
|
|
|
485
526
|
update((s) => applyEvent({ ...s, items: [...s.items] }, { type: "finish", success: false, steps: s.step, summary: `Run failed: ${err.message}` }));
|
|
486
527
|
update((s) => ({ ...s, items: [...s.items, { kind: "notice", text: err.message, level: "error" }] }));
|
|
487
528
|
}
|
|
488
|
-
}, [
|
|
529
|
+
}, [cfg, pushNotice, writeInput, exit, update, queueEvent, skills, skillNames, permMode, reviewAnswer, reviewStep, tuiHost, preSubmit]);
|
|
489
530
|
/** Create (or return) the session agent — permission mode always current. */
|
|
490
531
|
function createAgent() {
|
|
491
532
|
if (agentRef.current)
|
|
492
533
|
return agentRef.current;
|
|
493
|
-
const agent = new Agent({ ...
|
|
534
|
+
const agent = new Agent({ ...cfg, permissionMode: permMode });
|
|
494
535
|
// Events go through the batching queue (see FLUSH_MS above): applyEvent
|
|
495
536
|
// still receives the latest state because update() keeps stateRef in
|
|
496
537
|
// sync synchronously — tool results can never miss their items.
|
|
@@ -526,6 +567,16 @@ export function TuiApp({ config, initialTask }) {
|
|
|
526
567
|
}
|
|
527
568
|
return;
|
|
528
569
|
}
|
|
570
|
+
// ── Ctrl+P: command palette ──
|
|
571
|
+
if (key.ctrl && _input === "p") {
|
|
572
|
+
setPaletteOpen((o) => !o);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// ── ?: help overlay (when input empty) ──
|
|
576
|
+
if (_input === "?" && !inputRef.current.trim()) {
|
|
577
|
+
setHelpOpen((o) => !o);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
529
580
|
// T-10: ctrl+t toggles the thinking stream visibility.
|
|
530
581
|
if (key.ctrl && _input === "t") {
|
|
531
582
|
update((s) => ({ ...s, thinkingVisible: !s.thinkingVisible }));
|
|
@@ -598,7 +649,7 @@ export function TuiApp({ config, initialTask }) {
|
|
|
598
649
|
// flexShrink={0}: pinned chrome must never be squeezed when the chat
|
|
599
650
|
// content overflows — Ink defaults every box to flexShrink:1, which
|
|
600
651
|
// shrank the input bar to zero rows once the log outgrew the terminal.
|
|
601
|
-
_jsxs(Box, { flexDirection: "column", height: rows || 24, flexShrink: 0, children: [_jsx(BuildRail, { state: state, config:
|
|
652
|
+
_jsxs(Box, { flexDirection: "column", height: rows || 24, flexShrink: 0, children: [_jsx(BuildRail, { state: state, config: cfg }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, minHeight: 0, children: [_jsx(Box, { ref: viewportRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflow: "hidden", contentOffsetY: scrollTop, children: _jsxs(Box, { ref: contentRef, flexDirection: "column", paddingX: 1, flexShrink: 0, children: [state.items.length === 0 ? (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: [_jsx(Text, { bold: true, color: theme.brand, children: "\u2691 SCoder" }), _jsx(Text, { dimColor: true, children: `autonomous coding agent · v${getVersion()}` }), _jsx(Text, {}), _jsx(Text, { dimColor: true, children: "type a task to start \u2014 agent plans, edits, and builds" }), _jsx(Text, {}), _jsxs(Text, { dimColor: true, children: ["\u23CE send \u00B7 ", _jsx(Text, { bold: true, children: "ctrl+s" }), " sessions \u00B7 tab complete \u00B7 /help \u00B7 /skills"] })] })) : (state.items.map((item, i) => _jsx(ChatItemView, { item: item, width: (columns ?? 100) - (showSidebar ? 42 : 4) }, i))), lastSummary && (_jsx(Box, { marginTop: 1, children: state.status === "success" ? (_jsxs(Text, { color: "green", bold: true, children: ["\u2714 ", lastSummary] })) : state.status === "aborted" ? (_jsxs(Text, { color: "yellow", bold: true, children: ["\u23F9 ", lastSummary] })) : (_jsxs(Text, { color: "red", bold: true, children: ["\u2718 ", lastSummary] })) }))] }) }), showSidebar && _jsx(Sidebar, { state: state, config: cfg })] }), sessionPickerOpen && (_jsx(SessionPicker, { workdir: cfg.workdir, onSelect: handleSessionSelect, onClose: () => setSessionPickerOpen(false) })), paletteOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "Command palette (Ctrl+P to close)" }), SLASH_COMMANDS.slice(0, 40).map((c) => (_jsxs(Text, { children: [c.cmd.padEnd(12), " ", c.desc] }, c.cmd)))] })), helpOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.dim, paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: theme.brand, bold: true, children: "Help (?)" }), _jsx(Text, { children: formatSlashHelp("scoder tui") })] })), permPrompt && _jsx(PermissionModal, { prompt: permPrompt }), _jsx(Box, { paddingX: 1, paddingTop: 1, paddingBottom: 1, flexShrink: 0, alignSelf: "stretch", children: _jsx(Input, { valueRef: inputRef, value: input, onChange: writeInput, onSubmit: (v) => void submit(v), historyRef: historyRef, skillNames: skillNames, agentRef: agentRef, config: cfg, atSources: atSources, modalRef: permPromptRef }) }), _jsx(StatusBar, { state: state, scroll: scrollOffset > 0 ? `↑ ${Math.min(scrollOffset, maxScroll)}/${maxScroll}` : null })] }));
|
|
602
653
|
}
|
|
603
654
|
/** Mount the TUI (called from the `scoder tui` subcommand). */
|
|
604
655
|
export async function runTui(config, initialTask) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { theme } from "./theme.js";
|
|
5
|
+
/** Modal for ask_user tool — single/multi choice + free text. */
|
|
6
|
+
export function AskUserModal({ question, options, multiple, onAnswer, onCancel }) {
|
|
7
|
+
const [input, setInput] = useState("");
|
|
8
|
+
useInput((ch, key) => {
|
|
9
|
+
if (key.escape) {
|
|
10
|
+
onCancel();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (key.return) {
|
|
14
|
+
onAnswer(input.trim() || (options?.[0] ?? ""));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (key.backspace) {
|
|
18
|
+
setInput((v) => v.slice(0, -1));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (ch && !key.ctrl && !key.meta)
|
|
22
|
+
setInput((v) => v + ch);
|
|
23
|
+
});
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, paddingY: 1, children: [_jsxs(Text, { color: theme.accent, bold: true, children: ["\u2753 ", question] }), options?.length ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o, i) => _jsxs(Text, { children: [i + 1, ". ", o] }, i)) })) : null, _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: [multiple ? "comma-separated choices" : "type answer", ": "] }), _jsx(Text, { children: input }), _jsx(Text, { color: theme.accent, children: "\u258C" })] }), _jsx(Text, { dimColor: true, children: "Enter submit \u00B7 Esc cancel" })] }));
|
|
25
|
+
}
|
package/dist/tui/chat-views.js
CHANGED
|
@@ -53,8 +53,9 @@ function previewLines(output) {
|
|
|
53
53
|
.filter((l) => !/^--- (stdout|stderr) ---$/.test(l.trim()));
|
|
54
54
|
return lines.slice(-OUTPUT_PREVIEW_LINES);
|
|
55
55
|
}
|
|
56
|
-
function ToolItemView({ item }) {
|
|
56
|
+
function ToolItemView({ item, width }) {
|
|
57
57
|
const running = item.ok === null;
|
|
58
|
+
const outputWidth = Math.max(24, (width ?? 100) - 6);
|
|
58
59
|
// One meaningful arg per tool type keeps the line scannable.
|
|
59
60
|
const keyArg = typeof item.args.command === "string"
|
|
60
61
|
? fit(item.args.command, 60)
|
|
@@ -75,7 +76,7 @@ function ToolItemView({ item }) {
|
|
|
75
76
|
? `${(elapsedMs / 1000).toFixed(0)}s`
|
|
76
77
|
: `${(elapsedMs / 1000).toFixed(1)}s`
|
|
77
78
|
: null;
|
|
78
|
-
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [String(item.step).padStart(2, "0"), "\u25B8 "] }), _jsxs(Text, { color: theme.accent, children: [toolIcon(item.name), " "] }), _jsx(Text, { color: item.ok === false ? theme.error : theme.accent, bold: true, children: item.name }), keyArg && _jsxs(Text, { dimColor: true, children: [" ", keyArg] }), running && _jsx(Text, { dimColor: true, children: " \u00B7 running" }), !running && elapsed && _jsxs(Text, { dimColor: true, children: [" \u00B7 ", elapsed] })] }), preview.map((line, i) => (_jsxs(Text, { dimColor: true, children: [" └ ", line.length >
|
|
79
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [String(item.step).padStart(2, "0"), "\u25B8 "] }), _jsxs(Text, { color: theme.accent, children: [toolIcon(item.name), " "] }), _jsx(Text, { color: item.ok === false ? theme.error : theme.accent, bold: true, children: item.name }), keyArg && _jsxs(Text, { dimColor: true, children: [" ", keyArg] }), running && _jsx(Text, { dimColor: true, children: " \u00B7 running" }), !running && elapsed && _jsxs(Text, { dimColor: true, children: [" \u00B7 ", elapsed] })] }), preview.map((line, i) => (_jsxs(Text, { dimColor: true, children: [" └ ", line.length > outputWidth ? line.slice(0, outputWidth - 1) + "…" : line] }, i))), halfblocks && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [halfblocks.map((r, i) => (_jsx(Text, { children: r.text }, i))), _jsx(Text, { dimColor: true, children: " \u2514 image preview" })] })), item.image && !halfblocks && (_jsxs(Text, { dimColor: true, children: [" \u2514 image \u2014 ", item.output] }))] }));
|
|
79
80
|
}
|
|
80
81
|
/** One colored line of a unified diff (+ green, - red, @@ cyan, header bold). */
|
|
81
82
|
export function DiffLine({ line }) {
|
|
@@ -95,13 +96,14 @@ const MAX_DIFF_RENDER_LINES = 20;
|
|
|
95
96
|
* Header row: [step] ✎ [+/− stats] [path]
|
|
96
97
|
* Body: up to MAX_DIFF_RENDER_LINES of colored diff lines.
|
|
97
98
|
*/
|
|
98
|
-
function DiffItemView({ item }) {
|
|
99
|
+
function DiffItemView({ item, width }) {
|
|
99
100
|
const lines = item.text.length > 0 ? item.text.split("\n") : [];
|
|
100
101
|
const shown = lines.slice(0, MAX_DIFF_RENDER_LINES);
|
|
101
102
|
const hidden = lines.length - shown.length;
|
|
103
|
+
const diffWidth = Math.max(24, (width ?? 100) - 6);
|
|
102
104
|
const addedColor = item.added > 0 ? theme.success : undefined;
|
|
103
105
|
const removedColor = item.removed > 0 ? theme.error : undefined;
|
|
104
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginTop: 1, marginLeft: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [String(item.step).padStart(2, "0"), "\u25B8 "] }), _jsx(Text, { color: theme.brand, bold: true, children: "\u270E" }), _jsx(Text, { children: " " }), _jsxs(Text, { color: addedColor, children: ["+", item.added] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: removedColor, children: ["-", item.removed] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: theme.accent, children: item.path }), item.truncated && _jsx(Text, { dimColor: true, children: " \u00B7 diff truncated" })] }), shown.map((line, i) => (_jsx(DiffLine, { line: line.length >
|
|
106
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginTop: 1, marginLeft: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [String(item.step).padStart(2, "0"), "\u25B8 "] }), _jsx(Text, { color: theme.brand, bold: true, children: "\u270E" }), _jsx(Text, { children: " " }), _jsxs(Text, { color: addedColor, children: ["+", item.added] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: removedColor, children: ["-", item.removed] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: theme.accent, children: item.path }), item.truncated && _jsx(Text, { dimColor: true, children: " \u00B7 diff truncated" })] }), shown.map((line, i) => (_jsx(DiffLine, { line: line.length > diffWidth ? line.slice(0, diffWidth - 1) + "…" : line }, i))), hidden > 0 && (_jsxs(Text, { dimColor: true, children: [" \u2026 +", hidden, " more diff lines"] }))] }));
|
|
105
107
|
}
|
|
106
108
|
/**
|
|
107
109
|
* Plan KHÔNG còn render trong chat — checklist live hiển thị ở Sidebar bên
|
|
@@ -111,7 +113,7 @@ function DiffItemView({ item }) {
|
|
|
111
113
|
function TodoItemView() {
|
|
112
114
|
return null;
|
|
113
115
|
}
|
|
114
|
-
export function ChatItemView({ item }) {
|
|
116
|
+
export function ChatItemView({ item, width }) {
|
|
115
117
|
switch (item.kind) {
|
|
116
118
|
case "user":
|
|
117
119
|
// User messages stand out with a full colored border + bold label so they're
|
|
@@ -122,15 +124,15 @@ export function ChatItemView({ item }) {
|
|
|
122
124
|
if (item.streaming || !item.text) {
|
|
123
125
|
return (_jsxs(Box, { marginTop: item.text ? 1 : 0, paddingLeft: 2, children: [_jsx(AgentMark, {}), _jsx(Text, { dimColor: item.streaming, children: item.text || (item.streaming ? "" : "(no text)") }), item.streaming && _jsx(Text, { color: theme.accent, bold: true, children: "\u258B" })] }));
|
|
124
126
|
}
|
|
125
|
-
return (_jsxs(Box, { marginTop: 1, paddingLeft: 2, children: [_jsx(AgentMark, {}), _jsx(Markdown, { text: item.text })] }));
|
|
127
|
+
return (_jsxs(Box, { marginTop: 1, paddingLeft: 2, children: [_jsx(AgentMark, {}), _jsx(Markdown, { text: item.text, width: width })] }));
|
|
126
128
|
}
|
|
127
129
|
case "thinking":
|
|
128
130
|
// T-10: reasoning stream — dim color, ⊙ prefix, collapse to last 4 lines.
|
|
129
131
|
return (_jsx(Box, { paddingLeft: 1, marginTop: 1, children: _jsxs(Text, { color: theme.thinking, italic: true, children: [_jsx(Text, { dimColor: true, children: "\u2299" }), " ", item.text.split("\n").slice(-4).join("\n ")] }) }));
|
|
130
132
|
case "tool":
|
|
131
|
-
return _jsx(ToolItemView, { item: item });
|
|
133
|
+
return _jsx(ToolItemView, { item: item, width: width });
|
|
132
134
|
case "diff":
|
|
133
|
-
return _jsx(DiffItemView, { item: item });
|
|
135
|
+
return _jsx(DiffItemView, { item: item, width: width });
|
|
134
136
|
case "todo":
|
|
135
137
|
return _jsx(TodoItemView, {});
|
|
136
138
|
case "build":
|
|
@@ -138,6 +140,11 @@ export function ChatItemView({ item }) {
|
|
|
138
140
|
return (_jsx(Box, { paddingLeft: 1, marginTop: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: item.success
|
|
139
141
|
? _jsx(Text, { color: theme.success, bold: true, children: "\u2713 build passed" })
|
|
140
142
|
: _jsx(Text, { color: theme.error, bold: true, children: "\u2717 build FAILED" }) }), _jsxs(Text, { dimColor: true, children: [" ", item.command] })] }) }));
|
|
143
|
+
case "compaction": {
|
|
144
|
+
// Divider marking an LLM summarization of older history.
|
|
145
|
+
const fmt = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n));
|
|
146
|
+
return (_jsx(Box, { paddingLeft: 1, marginTop: 1, children: _jsxs(Text, { dimColor: true, italic: true, children: ["\u2500\u2500 ", item.manual ? "compacted (manual)" : "compacted", ": ", fmt(item.tokensBefore), " \u2192 ", fmt(item.tokensAfter), " tokens \u2500\u2500\u2500"] }) }));
|
|
147
|
+
}
|
|
141
148
|
case "notice":
|
|
142
149
|
return (_jsx(Box, { paddingLeft: 2, marginTop: 1, children: _jsxs(Text, { color: item.level === "error" ? theme.error :
|
|
143
150
|
item.level === "warn" ? theme.warn :
|
package/dist/tui/chrome.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { fit, theme } from "./theme.js";
|
|
4
4
|
import { Spinner } from "./chat-views.js";
|
|
5
|
+
import { getVersion } from "../version.js";
|
|
5
6
|
/** Fuel gauge is scaled to this many cells regardless of maxSteps. */
|
|
6
7
|
export const GAUGE_CELLS = 12;
|
|
7
8
|
/** Sidebar phải: tổng rộng (kể cả border + padding) — pattern opencode. */
|
|
@@ -45,7 +46,7 @@ export function BuildRail({ state, config }) {
|
|
|
45
46
|
const centerBudget = Math.max(24, (columns ?? 100) - 34);
|
|
46
47
|
// Phase breadcrumb or narrow-terminal metrics (right of the center)
|
|
47
48
|
const showNarrowMetrics = !wide && (state.status === "running" || state.phases.length > 0);
|
|
48
|
-
return (_jsxs(Box, { borderStyle: "bold", borderColor: theme.border, paddingX: 1, justifyContent: "space-between", alignItems: "center", flexShrink: 0, children: [_jsxs(Text, { bold: true, color: theme.brand, children: ["\u2691 ", _jsx(Text, { bold: true, children: "scoder" })] }), _jsx(Box, { flexGrow: 1, justifyContent: "center", children: hasGuardrail ? (_jsxs(Text, { color: theme.warn, bold: true, children: ["\u26A0 ", fit(state.lastGuardrail, centerBudget)] })) : state.status === "running" ? (showNarrowMetrics ? (_jsx(PhaseBreadcrumb, { phases: state.phases })) : (_jsx(Text, { dimColor: true, children: "plan \u203A explore \u203A edit \u203A build \u203A verify" }))) : (_jsx(Text, { dimColor: true, children: "ready" })) }), _jsxs(Text, { children: [_jsxs(Text, { color: permColor, children: ["[", perm, "]"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: lampColor, children: lampLabel }), _jsxs(Text, { dimColor: true, children: [" ", lampText] })] })] }));
|
|
49
|
+
return (_jsxs(Box, { borderStyle: "bold", borderColor: theme.border, paddingX: 1, justifyContent: "space-between", alignItems: "center", flexShrink: 0, children: [_jsxs(Text, { bold: true, color: theme.brand, children: ["\u2691 ", _jsx(Text, { bold: true, children: "scoder" }), _jsxs(Text, { dimColor: true, children: [" (v", getVersion(), ")"] })] }), _jsx(Box, { flexGrow: 1, justifyContent: "center", children: hasGuardrail ? (_jsxs(Text, { color: theme.warn, bold: true, children: ["\u26A0 ", fit(state.lastGuardrail, centerBudget)] })) : state.status === "running" ? (showNarrowMetrics ? (_jsx(PhaseBreadcrumb, { phases: state.phases })) : (_jsx(Text, { dimColor: true, children: "plan \u203A explore \u203A edit \u203A build \u203A verify" }))) : (_jsx(Text, { dimColor: true, children: "ready" })) }), _jsxs(Text, { children: [_jsxs(Text, { color: permColor, children: ["[", perm, "]"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: lampColor, children: lampLabel }), _jsxs(Text, { dimColor: true, children: [" ", lampText] })] })] }));
|
|
49
50
|
}
|
|
50
51
|
/** Breadcrumb tiến trình: plan › explore › edit › build › verify. */
|
|
51
52
|
const PHASE_SEQUENCE = ["plan", "explore", "edit", "build", "verify"];
|