s-app-scoder-agent 0.7.0 → 0.8.1
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 +5 -4
- package/dist/agent.js +16 -22
- package/dist/guardrails.js +14 -0
- package/dist/index.js +8 -0
- package/dist/prompt.js +25 -19
- package/dist/server.js +4 -1
- package/dist/tui/app.js +7 -0
- package/dist/tui/chat-views.js +3 -2
- package/dist/tui/chrome.js +7 -7
- package/dist/tui/sidebar.js +4 -6
- package/dist/tui/state.js +16 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -272,7 +272,7 @@ The autonomous loop is wrapped by a configurable rule engine (`src/guardrails.ts
|
|
|
272
272
|
| `chat-done` | Ends conversational turns (marker-based) without a build |
|
|
273
273
|
| `completion-claim` | Never trusts "the task is complete" — forces an independent build verification |
|
|
274
274
|
| `text-only-streak` | Ends the run after 2 consecutive tool-less steps with no file changes (the "chào" failure mode) |
|
|
275
|
-
| `continue-nudge` | Nudges plain-text answers back to work |
|
|
275
|
+
| `continue-nudge` | Nudges plain-text answers back to work (no build demand when no files changed — suggests the `<<CHAT_DONE>>` exit instead) |
|
|
276
276
|
| `doom-loop` | Intervenes after 3× identical consecutive tool-call batches |
|
|
277
277
|
| `build-stop` | Ends the run when the exact build command succeeds; re-verifies on failure |
|
|
278
278
|
| `history-trim` | Char-budget history trimming with whole-turn drops + old-tool-result pruning |
|
|
@@ -371,9 +371,10 @@ scoder tui "fix the bug" # auto-submit an initial task
|
|
|
371
371
|
Features:
|
|
372
372
|
|
|
373
373
|
- **Alternate screen buffer** (vim/htop style) — the app owns the full screen: the header stays pinned at the top, input + status bar at the bottom, the chat pane scrolls internally, and your terminal content is restored on exit. Terminals without alt-screen support (`TERM=dumb`, non-TTY) automatically render inline instead.
|
|
374
|
-
- **Build Rail** (pinned top bar) — brand, build lamp (
|
|
375
|
-
- **
|
|
376
|
-
- **
|
|
374
|
+
- **Build Rail** (pinned top bar, single row) — brand, build lamp (`✓ ok` / `✗ fail` / `no build yet`), permission mode, and a center region that shows either the phase breadcrumb (`plan › explore › edit › build › verify`) or the latest guardrail notice spanning the full available width (e.g. `⚠ trả lời text chưa dùng tool — nhắc tiếp tục`)
|
|
375
|
+
- **Sidebar (≥92 cols)** — SESSION (model + workdir, no permission line), METRICS (step fuel gauge, tokens, tok/s, ctx input/limit, cost, elapsed, guardrail count), PHASES, BUILD, MCP, PLAN (live checklist at the bottom; resets on each new user message)
|
|
376
|
+
- **Chat pane** — your messages with a full colored border + `you ▸` label, streaming assistant text with a live block cursor (`▋`), and every tool call with live stdout/stderr preview (`✔/✘` status, spinner while running, wall-clock duration per tool), prefixed with a flight-recorder step number (`03▸`)
|
|
377
|
+
- **Header row 2** — (no longer present; phase breadcrumb + metrics moved into the sidebar / status bar to avoid duplication)
|
|
377
378
|
- **Diff cards** (`✎`) — every file write/edit renders as a unified diff (`+` green / `-` red / `@@` cyan hunk headers); the same diff also flows out of the HTTP SSE stream
|
|
378
379
|
- **Status bar** — model, token usage (input/output/cached), scroll position, keybind hints
|
|
379
380
|
- **Slash commands** — `/help`, `/clear`, `/cost`, `/model`, `/skills`, `/skill`, `/mcp`, `/review`, `/permission`, `/exit` (identical to the REPL)
|
package/dist/agent.js
CHANGED
|
@@ -176,6 +176,10 @@ export class Agent {
|
|
|
176
176
|
catch (err) {
|
|
177
177
|
return { ok: false, output: `ERROR: subagent failed: ${err.message}` };
|
|
178
178
|
}
|
|
179
|
+
finally {
|
|
180
|
+
// Child agents are ephemeral: close their MCP connections (if any).
|
|
181
|
+
child.dispose();
|
|
182
|
+
}
|
|
179
183
|
};
|
|
180
184
|
this.ctx.taskRunner = runner;
|
|
181
185
|
}
|
|
@@ -626,7 +630,12 @@ export class Agent {
|
|
|
626
630
|
this.tools = [...allToolSpecs(), ...mcpSpecs];
|
|
627
631
|
}
|
|
628
632
|
}
|
|
629
|
-
/**
|
|
633
|
+
/**
|
|
634
|
+
* Disconnect MCP servers when the agent is disposed — connections are kept
|
|
635
|
+
* alive for the whole agent/session lifetime (one "connected" line, no
|
|
636
|
+
* re-spawn per message). Ephemeral owners (server runs, one-shot CLI) call
|
|
637
|
+
* dispose() when their agent is done.
|
|
638
|
+
*/
|
|
630
639
|
shutdownMcp() {
|
|
631
640
|
this.mcp?.killAll();
|
|
632
641
|
this.mcp = null;
|
|
@@ -689,13 +698,8 @@ export class Agent {
|
|
|
689
698
|
buildCmd,
|
|
690
699
|
permission: this.config.permissionMode,
|
|
691
700
|
});
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
return await this.runLoop(0, buildCmd, null);
|
|
695
|
-
}
|
|
696
|
-
finally {
|
|
697
|
-
this.shutdownMcp();
|
|
698
|
-
}
|
|
701
|
+
await this.connectMcp();
|
|
702
|
+
return this.runLoop(0, buildCmd, null);
|
|
699
703
|
}
|
|
700
704
|
/**
|
|
701
705
|
* One conversational turn in an ongoing session (used by the REPL):
|
|
@@ -742,13 +746,8 @@ export class Agent {
|
|
|
742
746
|
this.messages.push({ role: "user", content });
|
|
743
747
|
this.pendingContext = [];
|
|
744
748
|
this.messages.push({ role: "user", content: task });
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
return await this.runLoop(0, buildCmd, null);
|
|
748
|
-
}
|
|
749
|
-
finally {
|
|
750
|
-
this.shutdownMcp();
|
|
751
|
-
}
|
|
749
|
+
await this.connectMcp();
|
|
750
|
+
return this.runLoop(0, buildCmd, null);
|
|
752
751
|
}
|
|
753
752
|
/**
|
|
754
753
|
* Resume a previous session: restore messages/step count and continue the loop.
|
|
@@ -782,13 +781,8 @@ export class Agent {
|
|
|
782
781
|
: null;
|
|
783
782
|
// v2: opening a resumed run stamps a fresh run.started (event log continuity).
|
|
784
783
|
this.events.append({ type: "run.started", task: this.currentTask });
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
return await this.runLoop(session.steps ?? 0, buildCmd, lastBuild);
|
|
788
|
-
}
|
|
789
|
-
finally {
|
|
790
|
-
this.shutdownMcp();
|
|
791
|
-
}
|
|
784
|
+
await this.connectMcp();
|
|
785
|
+
return this.runLoop(session.steps ?? 0, buildCmd, lastBuild);
|
|
792
786
|
}
|
|
793
787
|
/**
|
|
794
788
|
* The shared autonomous loop:
|
package/dist/guardrails.js
CHANGED
|
@@ -167,6 +167,20 @@ const continueNudgeRule = { name: "continue-nudge",
|
|
|
167
167
|
return null;
|
|
168
168
|
if (claimsCompletion(ctx.text))
|
|
169
169
|
return null; // completion-claim rule phụ trách
|
|
170
|
+
// No file changed yet → never demand a build for what may be a pure Q&A or
|
|
171
|
+
// an explanation-only answer. Give the model the explicit conversational
|
|
172
|
+
// exit instead; text-only-streak still ends the run if it keeps stalling.
|
|
173
|
+
if (!ctx.filesModified) {
|
|
174
|
+
return {
|
|
175
|
+
action: {
|
|
176
|
+
kind: "inject",
|
|
177
|
+
reminder: `Continue working on the task. Use tools only if code changes are needed. ` +
|
|
178
|
+
`If the user only asked a question and no code change is needed, do NOT run the build — ` +
|
|
179
|
+
`end your reply with ${CHAT_DONE_MARKER} on its own last line instead.`,
|
|
180
|
+
},
|
|
181
|
+
reason: "plain text answer without tool calls (no files modified)",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
170
184
|
return {
|
|
171
185
|
action: {
|
|
172
186
|
kind: "inject",
|
package/dist/index.js
CHANGED
|
@@ -220,6 +220,9 @@ async function runOnce(cfg, task) {
|
|
|
220
220
|
ui.error(err.message);
|
|
221
221
|
return 2;
|
|
222
222
|
}
|
|
223
|
+
finally {
|
|
224
|
+
agent.dispose();
|
|
225
|
+
}
|
|
223
226
|
}
|
|
224
227
|
/**
|
|
225
228
|
* `scoder init` — first-run wizard: asks for the essentials, writes
|
|
@@ -323,11 +326,13 @@ async function repl(cfg) {
|
|
|
323
326
|
config: cfg,
|
|
324
327
|
replaceAgent: (next) => {
|
|
325
328
|
agent.abort();
|
|
329
|
+
agent.dispose();
|
|
326
330
|
agent = next ?? new Agent(cfg);
|
|
327
331
|
first = true;
|
|
328
332
|
},
|
|
329
333
|
resumeAndRun: async (s) => {
|
|
330
334
|
agent.abort();
|
|
335
|
+
agent.dispose();
|
|
331
336
|
agent = new Agent(cfg);
|
|
332
337
|
first = false;
|
|
333
338
|
await agent.resume(s);
|
|
@@ -357,6 +362,7 @@ async function repl(cfg) {
|
|
|
357
362
|
ui.error(err.message);
|
|
358
363
|
}
|
|
359
364
|
}
|
|
365
|
+
agent.dispose();
|
|
360
366
|
ui.info("bye");
|
|
361
367
|
process.exit(0);
|
|
362
368
|
}
|
|
@@ -419,9 +425,11 @@ async function main() {
|
|
|
419
425
|
});
|
|
420
426
|
try {
|
|
421
427
|
const result = await agent.resume(s, args.task ?? undefined);
|
|
428
|
+
agent.dispose();
|
|
422
429
|
process.exit(result.success ? 0 : 1);
|
|
423
430
|
}
|
|
424
431
|
catch (err) {
|
|
432
|
+
agent.dispose();
|
|
425
433
|
ui.error(err.message);
|
|
426
434
|
process.exit(2);
|
|
427
435
|
}
|
package/dist/prompt.js
CHANGED
|
@@ -3,18 +3,14 @@
|
|
|
3
3
|
* - versioning rõ ràng (PROMPT_VERSION được ghi vào session JSONL khi chạy),
|
|
4
4
|
* - test được độc lập (không cần instantiate Agent).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* - Safety & Boundaries: explicit prohibitions on git hooks, CI configs,
|
|
13
|
-
* dependencies, infrastructure configs, and destructive commands
|
|
14
|
-
* - Tool selection: concrete descriptions with explicit usage rules
|
|
15
|
-
* - Skills/MCP: passive listing, never auto-load
|
|
6
|
+
* V4 — conversational exits + no wasted builds:
|
|
7
|
+
* - restore the <<CHAT_DONE>> marker rule (V3 dropped it, so the chat-done
|
|
8
|
+
* guardrail could never fire and every Q&A turn was nudged into a build)
|
|
9
|
+
* - build only after file edits; never run the build when nothing changed
|
|
10
|
+
* - V3 baseline: stricter scope rules, clarification-before-action, safety
|
|
11
|
+
* boundaries, MCP/skills as passive listings only
|
|
16
12
|
*/
|
|
17
|
-
export const PROMPT_VERSION =
|
|
13
|
+
export const PROMPT_VERSION = 4;
|
|
18
14
|
export const SYSTEM_PROMPT = `You are Scoder, an autonomous coding agent running in a terminal.
|
|
19
15
|
You complete coding tasks end-to-end: exploring the code, editing files, and
|
|
20
16
|
verifying your work with the build command. You act; you don't just advise.
|
|
@@ -30,9 +26,9 @@ verifying your work with the build command. You act; you don't just advise.
|
|
|
30
26
|
3. EDIT: Make small, focused changes. Match the file's existing style
|
|
31
27
|
(indentation, quote style, naming conventions) — do NOT reformat unrelated
|
|
32
28
|
parts. Never refactor unrelated code, never add speculative features.
|
|
33
|
-
4. VERIFY:
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
4. VERIFY: If you changed files, run the build command. Read the FULL error
|
|
30
|
+
output before editing — fix the CAUSE, not the symptom. Errors can be
|
|
31
|
+
misleading (e.g. 'X not found' might be caused by a missing import upstream).
|
|
36
32
|
5. REPORT: Your final message states what changed, why, and anything left
|
|
37
33
|
unresolved. Be honest: if something didn't work, say exactly what failed.
|
|
38
34
|
|
|
@@ -46,7 +42,8 @@ verifying your work with the build command. You act; you don't just advise.
|
|
|
46
42
|
- **write_file**: Create or overwrite. Parent dirs created automatically. Use for
|
|
47
43
|
new files or when write is cleaner than targeted edits.
|
|
48
44
|
- **run_command**: Run shell command. Captures stdout + stderr. THIS IS THE BUILD
|
|
49
|
-
COMMAND —
|
|
45
|
+
COMMAND — run it only when you changed files; a coding task ends with
|
|
46
|
+
run_command(<buildCmd>).
|
|
50
47
|
- **write_todos**: Plan + live progress for multi-step work.
|
|
51
48
|
- **web_fetch**: Exact URLs only (it cannot search). Use for library/API docs,
|
|
52
49
|
versions, changelogs when the answer isn't in the working directory.
|
|
@@ -74,11 +71,20 @@ verifying your work with the build command. You act; you don't just advise.
|
|
|
74
71
|
- Improve code quality outside the task scope
|
|
75
72
|
2. If the request is ambiguous or the scope is unclear: ask ONE clarifying question
|
|
76
73
|
before taking action. Do NOT guess and proceed — clarify first.
|
|
77
|
-
3. When you believe the task is done
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
3. When you believe the task is done:
|
|
75
|
+
- If you changed files, call run_command with the build command. The coding
|
|
76
|
+
loop ends only when the build succeeds.
|
|
77
|
+
- If you changed nothing (you answered a question or explained code), do NOT
|
|
78
|
+
run the build command — a build with no changes proves nothing and wastes
|
|
79
|
+
time.
|
|
80
|
+
4. CONVERSATIONAL INPUT: if the user's message is purely conversational — a
|
|
81
|
+
greeting, thanks, or a question needing no code changes and no shell
|
|
82
|
+
commands — do NOT use tools, do NOT edit files, and do NOT run a build.
|
|
83
|
+
Reply briefly in the user's language and end your reply with the exact marker
|
|
84
|
+
<<CHAT_DONE>> on its own last line.
|
|
85
|
+
5. If run_command is denied by permission policy, find a safer alternative or
|
|
80
86
|
report the limitation clearly. Do not retry the same denied command.
|
|
81
|
-
|
|
87
|
+
6. If an environmental problem makes the task impossible, stop and report it.
|
|
82
88
|
|
|
83
89
|
## Quality bar
|
|
84
90
|
|
package/dist/server.js
CHANGED
|
@@ -83,7 +83,10 @@ export function startServer(baseConfig, opts = {}) {
|
|
|
83
83
|
record.finishedAt = new Date().toISOString();
|
|
84
84
|
for (const sub of record.subscribers)
|
|
85
85
|
sub();
|
|
86
|
-
})
|
|
86
|
+
})
|
|
87
|
+
// MCP connections live for the agent lifetime — each server run owns an
|
|
88
|
+
// ephemeral agent, so dispose it here (never leak child processes).
|
|
89
|
+
.finally(() => agent.dispose());
|
|
87
90
|
};
|
|
88
91
|
const handleEventsSSE = (req, res, record) => {
|
|
89
92
|
res.writeHead(200, {
|
package/dist/tui/app.js
CHANGED
|
@@ -61,6 +61,12 @@ export function TuiApp({ config, initialTask }) {
|
|
|
61
61
|
const historyRef = useRef([]);
|
|
62
62
|
const runningRef = useRef(false);
|
|
63
63
|
const agentRef = useRef(null);
|
|
64
|
+
// MCP connections live for the session (one connect, no per-message re-spawn).
|
|
65
|
+
// Dispose on unmount so child server processes never outlive the TUI.
|
|
66
|
+
useEffect(() => () => {
|
|
67
|
+
agentRef.current?.dispose();
|
|
68
|
+
agentRef.current = null;
|
|
69
|
+
}, []);
|
|
64
70
|
// ── Prompt-gate review flow (/review) ──────────────────────────────────────
|
|
65
71
|
// State machine relaying the universal-prompt-gate-mcp text-fallback flow:
|
|
66
72
|
// call → pending_level_selection → user picks level → pending_user_selection
|
|
@@ -214,6 +220,7 @@ export function TuiApp({ config, initialTask }) {
|
|
|
214
220
|
isRunning: () => runningRef.current,
|
|
215
221
|
replaceAgent: (next) => {
|
|
216
222
|
agentRef.current?.abort();
|
|
223
|
+
agentRef.current?.dispose();
|
|
217
224
|
agentRef.current = next;
|
|
218
225
|
if (!next) {
|
|
219
226
|
runningRef.current = false;
|
package/dist/tui/chat-views.js
CHANGED
|
@@ -114,8 +114,9 @@ function TodoItemView() {
|
|
|
114
114
|
export function ChatItemView({ item }) {
|
|
115
115
|
switch (item.kind) {
|
|
116
116
|
case "user":
|
|
117
|
-
//
|
|
118
|
-
|
|
117
|
+
// User messages stand out with a full colored border + bold label so they're
|
|
118
|
+
// immediately visible at a glance, distinct from the agent's ◆ markdown.
|
|
119
|
+
return (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsxs(Box, { borderStyle: "single", borderColor: theme.user, paddingX: 1, paddingTop: 0, flexDirection: "column", children: [_jsx(Text, { color: theme.user, bold: true, children: "you \u25B8" }), _jsx(Text, { bold: true, children: item.text })] }) }));
|
|
119
120
|
case "assistant": {
|
|
120
121
|
// Streaming: dim with pulsing cursor; finalized: markdown with agent mark.
|
|
121
122
|
if (item.streaming || !item.text) {
|
package/dist/tui/chrome.js
CHANGED
|
@@ -23,7 +23,7 @@ export function gaugeColor(step, maxSteps) {
|
|
|
23
23
|
/**
|
|
24
24
|
* BuildRail — polished top bar (SINGLE row).
|
|
25
25
|
*
|
|
26
|
-
* Layout: [⚑ scoder] ── [phase breadcrumb / guardrail warning] ── [perm] [build]
|
|
26
|
+
* Layout: [⚑ scoder] ── [phase breadcrumb / guardrail warning] ── [perm] [build]
|
|
27
27
|
*
|
|
28
28
|
* Terminal ≥ SIDEBAR_MIN_COLS: sidebar handles METRICS + PHASES.
|
|
29
29
|
* Terminal < SIDEBAR_MIN_COLS: phase breadcrumb + metrics move into this row.
|
|
@@ -37,15 +37,15 @@ export function BuildRail({ state, config }) {
|
|
|
37
37
|
const lampColor = state.buildLamp === "ok" ? theme.success : state.buildLamp === "fail" ? theme.error : theme.dim;
|
|
38
38
|
const lampLabel = state.buildLamp === "ok" ? "✓" : state.buildLamp === "fail" ? "✗" : "○";
|
|
39
39
|
const lampText = state.buildLamp === "ok" ? "ok" : state.buildLamp === "fail" ? "fail" : "—";
|
|
40
|
-
//
|
|
41
|
-
const stepLabel = state.step > 0
|
|
42
|
-
? `${state.step}${state.maxSteps > 0 ? `/${state.maxSteps}` : ""}`
|
|
43
|
-
: "idle";
|
|
44
|
-
// Guardrail: shown inline if it fired (small warning before the phase breadcrumb)
|
|
40
|
+
// Guardrail: shown inline if it fired (small warning before the phase breadcrumb).
|
|
45
41
|
const hasGuardrail = Boolean(state.lastGuardrail);
|
|
42
|
+
// Guardrail text gets the whole center region — no fixed 40-char cap that
|
|
43
|
+
// truncates while the header still has empty space. Reserve room for the
|
|
44
|
+
// brand on the left and permission/build lamps on the right.
|
|
45
|
+
const centerBudget = Math.max(24, (columns ?? 100) - 34);
|
|
46
46
|
// Phase breadcrumb or narrow-terminal metrics (right of the center)
|
|
47
47
|
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,
|
|
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
49
|
}
|
|
50
50
|
/** Breadcrumb tiến trình: plan › explore › edit › build › verify. */
|
|
51
51
|
const PHASE_SEQUENCE = ["plan", "explore", "edit", "build", "verify"];
|
package/dist/tui/sidebar.js
CHANGED
|
@@ -15,11 +15,9 @@ function SectionLine({ label, value, color }) {
|
|
|
15
15
|
}
|
|
16
16
|
// ── Session ──────────────────────────────────────────────────────────────────
|
|
17
17
|
function SessionSection({ state, config }) {
|
|
18
|
-
const perm = state.permission || config.permissionMode;
|
|
19
|
-
const permColor = perm === "readonly" ? theme.warn : perm === "ask" ? theme.accent : theme.dim;
|
|
20
18
|
const model = state.model || config.model || "—";
|
|
21
19
|
const dir = config.workdir.split("/").filter(Boolean).pop() ?? config.workdir;
|
|
22
|
-
return (_jsxs(Section, { title: "SESSION", children: [_jsx(SectionLine, { label: "model", value: fit(model, INNER - 9) }), _jsx(SectionLine, { label: "
|
|
20
|
+
return (_jsxs(Section, { title: "SESSION", children: [_jsx(SectionLine, { label: "model", value: fit(model, INNER - 9) }), _jsx(SectionLine, { label: "dir", value: fit(dir, INNER - 9) })] }));
|
|
23
21
|
}
|
|
24
22
|
function PlanRow({ t, width }) {
|
|
25
23
|
if (t.status === "completed") {
|
|
@@ -51,7 +49,7 @@ function MetricsSection({ state }) {
|
|
|
51
49
|
? Math.round(state.usage.outputTokens / (elapsedMs / 1000))
|
|
52
50
|
: null;
|
|
53
51
|
// Live estimate based on accumulated streaming chars (chars / ~4 ≈ tokens).
|
|
54
|
-
const liveTokPerSec = state.streamingOutputChars > 0 && elapsedMs != null && elapsedMs >
|
|
52
|
+
const liveTokPerSec = state.streamingOutputChars > 0 && elapsedMs != null && elapsedMs > 250
|
|
55
53
|
? Math.round((state.streamingOutputChars / 4) / (elapsedMs / 1000))
|
|
56
54
|
: null;
|
|
57
55
|
const win = contextWindowFor(state.provider, state.model);
|
|
@@ -61,7 +59,7 @@ function MetricsSection({ state }) {
|
|
|
61
59
|
const ctxDisplay = ctxPct != null
|
|
62
60
|
? `${ctxFmt(state.usage.inputTokens)}/${ctxFmt(win)} (${ctxPct}%)`
|
|
63
61
|
: null;
|
|
64
|
-
return (_jsx(Section, { title: "METRICS", children: !hasUsage && elapsedSec == null ? (_jsx(Text, { dimColor: true, children: " starting\u2026" })) : (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [gauge && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { dimColor: true, children: " steps " }), _jsx(Text, { color: gaugeColor(state.step, state.maxSteps), children: gauge }), _jsxs(Text, { dimColor: true, children: [" ", state.step, "/", state.maxSteps] })] })), hasUsage && (_jsx(SectionLine, { label: "toks", value: `${fmt(state.usage.inputTokens)} → ${fmt(state.usage.outputTokens)}` })), tokPerSec != null && (_jsx(SectionLine, { label: "speed", value: `${fmt(tokPerSec)} tok/s`, color: theme.accent })), liveTokPerSec != null && tokPerSec == null && (_jsx(SectionLine, { label: "speed", value: `~${fmt(liveTokPerSec)} tok/s`, color: theme.accent })), ctxDisplay != null && _jsx(SectionLine, { label: "ctx", value: ctxDisplay }), cost > 0 && _jsx(SectionLine, { label: "cost", value: `$${cost < 0.01 ? cost.toFixed(4) : cost.toFixed(2)}` }), elapsedSec != null && (_jsx(SectionLine, { label: "elapsed", value: `${Math.floor(elapsedSec / 60)}:${String(elapsedSec % 60).padStart(2, "0")}` })), state.usage.cachedTokens > 0 && (_jsx(SectionLine, { label: "cached", value: fmt(state.usage.cachedTokens) })), state.guardrailCount > 0 && (_jsx(SectionLine, { label: "guard", value: String(state.guardrailCount), color: theme.warn }))] })) }));
|
|
62
|
+
return (_jsx(Section, { title: "METRICS", children: !hasUsage && elapsedSec == null ? (_jsx(Text, { dimColor: true, children: " starting\u2026" })) : (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [gauge && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { dimColor: true, children: " steps " }), _jsx(Text, { color: gaugeColor(state.step, state.maxSteps), children: gauge }), _jsxs(Text, { dimColor: true, children: [" ", state.step, "/", state.maxSteps] })] })), hasUsage && (_jsx(SectionLine, { label: "toks", value: `${fmt(state.usage.inputTokens)} → ${fmt(state.usage.outputTokens)}` })), !hasUsage && state.streamingOutputChars > 0 && (_jsx(SectionLine, { label: "toks", value: `~${fmt(Math.round(state.streamingOutputChars / 4))} (streaming)`, color: theme.accent })), tokPerSec != null && (_jsx(SectionLine, { label: "speed", value: `${fmt(tokPerSec)} tok/s`, color: theme.accent })), liveTokPerSec != null && tokPerSec == null && (_jsx(SectionLine, { label: "speed", value: `~${fmt(liveTokPerSec)} tok/s`, color: theme.accent })), ctxDisplay != null && _jsx(SectionLine, { label: "ctx", value: ctxDisplay }), cost > 0 && _jsx(SectionLine, { label: "cost", value: `$${cost < 0.01 ? cost.toFixed(4) : cost.toFixed(2)}` }), elapsedSec != null && (_jsx(SectionLine, { label: "elapsed", value: `${Math.floor(elapsedSec / 60)}:${String(elapsedSec % 60).padStart(2, "0")}` })), state.usage.cachedTokens > 0 && (_jsx(SectionLine, { label: "cached", value: fmt(state.usage.cachedTokens) })), state.guardrailCount > 0 && (_jsx(SectionLine, { label: "guard", value: String(state.guardrailCount), color: theme.warn }))] })) }));
|
|
65
63
|
}
|
|
66
64
|
// ── Phases (plan › explore › edit › build › verify — vertical) ───────────────
|
|
67
65
|
const PHASES = ["plan", "explore", "edit", "build", "verify"];
|
|
@@ -97,5 +95,5 @@ function McpSection({ state, config }) {
|
|
|
97
95
|
// ── Root ─────────────────────────────────────────────────────────────────────
|
|
98
96
|
/** Sidebar phải — mọi trạng thái sống của run, cập nhật theo AgentEvent. */
|
|
99
97
|
export function Sidebar({ state, config }) {
|
|
100
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.border, width: SIDEBAR_WIDTH, paddingX: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(SessionSection, { state: state, config: config }), _jsx(
|
|
98
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.border, width: SIDEBAR_WIDTH, paddingX: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(SessionSection, { state: state, config: config }), _jsx(McpSection, { state: state, config: config }), _jsx(MetricsSection, { state: state }), _jsx(PlanSection, { state: state }), _jsx(PhaseSection, { state: state }), _jsx(BuildSection, { state: state, config: config })] }));
|
|
101
99
|
}
|
package/dist/tui/state.js
CHANGED
|
@@ -69,12 +69,24 @@ export function visibleWindow(items, viewportRows, margin = 50) {
|
|
|
69
69
|
/**
|
|
70
70
|
* Plan mới nhất cho sidebar — todo items được append mỗi lần update nhưng
|
|
71
71
|
* sidebar chỉ cần snapshot hiện tại (last-wins), null khi chưa có plan.
|
|
72
|
+
*
|
|
73
|
+
* Reset realtime: mỗi lần user gửi message mới, plan reset — chỉ hiện plan
|
|
74
|
+
* được viết SAU user message cuối cùng. Tránh stale plan từ task trước hiện
|
|
75
|
+
* trong task mới.
|
|
72
76
|
*/
|
|
73
77
|
export function latestPlan(items) {
|
|
78
|
+
// Tìm user message cuối cùng — plan chỉ reset tại đây.
|
|
79
|
+
let lastUserIdx = -1;
|
|
74
80
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
if (items[i].kind === "user") {
|
|
82
|
+
lastUserIdx = i;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Chỉ tìm todo SAU user cuối.
|
|
87
|
+
for (let i = items.length - 1; i > lastUserIdx; i--) {
|
|
88
|
+
if (items[i].kind === "todo")
|
|
89
|
+
return items[i];
|
|
78
90
|
}
|
|
79
91
|
return null;
|
|
80
92
|
}
|
|
@@ -120,13 +132,11 @@ function applyGuardrail(state, ev) {
|
|
|
120
132
|
"chat-done": "lượt chat thuần — kết thúc không cần build",
|
|
121
133
|
"completion-claim": "agent báo hoàn thành — buộc chạy build xác minh",
|
|
122
134
|
"text-only-streak": "agent chỉ nói không làm — dừng run",
|
|
123
|
-
"continue-nudge": "
|
|
135
|
+
"continue-nudge": "trả lời text chưa dùng tool — nhắc tiếp tục",
|
|
124
136
|
"doom-loop": "lặp lẫn lộn cùng tool call — yêu cầu đổi hướng",
|
|
125
137
|
"build-stop": "build pass — kết thúc run",
|
|
126
138
|
};
|
|
127
139
|
state.lastGuardrail = HUMAN[ev.rule] ?? `${ev.rule}: ${ev.reason}`;
|
|
128
|
-
if (state.lastGuardrail.length > 72)
|
|
129
|
-
state.lastGuardrail = state.lastGuardrail.slice(0, 69) + "…";
|
|
130
140
|
}
|
|
131
141
|
/**
|
|
132
142
|
* Fold one AgentEvent into the state (mutates `state`, returns it for
|
package/package.json
CHANGED