s-app-scoder-agent 0.6.0 → 0.7.0
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/.scoderrc.example.json +97 -0
- package/LICENSE +21 -21
- package/README.md +599 -468
- package/dist/agent.js +825 -175
- package/dist/agents.js +70 -0
- package/dist/compaction.js +114 -0
- package/dist/config.js +178 -12
- package/dist/context.js +119 -14
- package/dist/cost.js +56 -6
- package/dist/guardrails.js +409 -0
- package/dist/index.js +121 -76
- package/dist/json-schema.js +58 -0
- package/dist/llm.js +123 -9
- package/dist/mcp.js +397 -70
- package/dist/model-catalog.js +175 -0
- package/dist/permissions.js +328 -35
- package/dist/prompt-builder.js +47 -0
- package/dist/prompt.js +97 -0
- package/dist/provider-anthropic.js +113 -35
- package/dist/provider-registry.js +116 -0
- package/dist/retry.js +3 -2
- package/dist/sandbox.js +96 -0
- package/dist/server.js +28 -1
- package/dist/session.js +282 -11
- package/dist/shell-scanner.js +112 -0
- package/dist/skills.js +122 -0
- package/dist/snapshot.js +170 -0
- package/dist/steering.js +35 -0
- package/dist/tools.js +513 -19
- package/dist/tui/app.js +498 -198
- package/dist/tui/at-complete.js +93 -0
- package/dist/tui/chat-views.js +145 -0
- package/dist/tui/chrome.js +96 -0
- package/dist/tui/command-registry.js +189 -0
- package/dist/tui/commands.js +200 -2
- package/dist/tui/image.js +149 -0
- package/dist/tui/input.js +214 -0
- package/dist/tui/markdown.js +229 -0
- package/dist/tui/mouse.js +20 -0
- package/dist/tui/permission-modal.js +34 -0
- package/dist/tui/review.js +64 -0
- package/dist/tui/session-picker.js +68 -0
- package/dist/tui/sidebar.js +101 -0
- package/dist/tui/state.js +182 -6
- package/dist/tui/theme.js +90 -0
- package/dist/tui/vim.js +60 -0
- package/dist/watch.js +52 -0
- package/package.json +16 -8
- package/dist/tui-direct.mjs +0 -4
package/dist/tui/state.js
CHANGED
|
@@ -9,13 +9,75 @@ export function initialState() {
|
|
|
9
9
|
workdir: "",
|
|
10
10
|
buildCmd: "",
|
|
11
11
|
buildLamp: "none",
|
|
12
|
+
permission: "",
|
|
13
|
+
startedAt: null,
|
|
14
|
+
endedAt: null,
|
|
12
15
|
usage: { calls: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0 },
|
|
13
16
|
summary: null,
|
|
17
|
+
guardrailCount: 0,
|
|
18
|
+
lastGuardrail: null,
|
|
19
|
+
thinkingVisible: false,
|
|
20
|
+
phases: [],
|
|
21
|
+
mcpServers: {},
|
|
22
|
+
streamingOutputChars: 0,
|
|
14
23
|
};
|
|
15
24
|
}
|
|
16
25
|
const MAX_ITEMS = 400;
|
|
17
26
|
const MAX_OUTPUT_CHARS = 20_000;
|
|
18
27
|
const MAX_TEXT_CHARS = 40_000;
|
|
28
|
+
/**
|
|
29
|
+
* T4: ước lượng chiều cao (rows) của một ChatItem khi render — dùng cho
|
|
30
|
+
* virtualization (chỉ render window quanh viewport thay vì cap cứng).
|
|
31
|
+
* Over-estimate nhẹ an toàn hơn under-estimate (viewport luôn đủ phủ).
|
|
32
|
+
* ponytail: heuristic theo chars, không đo Ink thật — nâng cấp thành đo
|
|
33
|
+
* height thật (useBoxMetrics per item) khi heuristic lệch đáng kể.
|
|
34
|
+
*/
|
|
35
|
+
export function estimateItemRows(item) {
|
|
36
|
+
switch (item.kind) {
|
|
37
|
+
case "user":
|
|
38
|
+
return 2 + Math.ceil(item.text.length / 80);
|
|
39
|
+
case "assistant":
|
|
40
|
+
case "thinking":
|
|
41
|
+
return 1 + Math.ceil((item.text?.length ?? 0) / 80) + (item.text ? item.text.split("\n").length : 0);
|
|
42
|
+
case "tool":
|
|
43
|
+
return 1 + 2; // title + preview 2 lines
|
|
44
|
+
case "diff":
|
|
45
|
+
return 3 + Math.min(24, (item.text?.split("\n").length ?? 0)); // header + ≤24 lines + border
|
|
46
|
+
case "todo":
|
|
47
|
+
return 2 + item.items.length;
|
|
48
|
+
case "build":
|
|
49
|
+
return 1;
|
|
50
|
+
case "notice":
|
|
51
|
+
return 1 + Math.ceil(item.text.length / 2000);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* T4: chọn window item để render: lấy từ cuối lên tới khi tổng rows ước
|
|
56
|
+
* lượng ≥ viewportRows + margin (render dư phía trên để scroll mượt), nhưng
|
|
57
|
+
* giữ toàn bộ khi tổng nhỏ. Trả về slice của items (không mutate).
|
|
58
|
+
*/
|
|
59
|
+
export function visibleWindow(items, viewportRows, margin = 50) {
|
|
60
|
+
const budget = viewportRows + margin;
|
|
61
|
+
let acc = 0;
|
|
62
|
+
let start = items.length;
|
|
63
|
+
while (start > 0 && acc < budget) {
|
|
64
|
+
start--;
|
|
65
|
+
acc += estimateItemRows(items[start]);
|
|
66
|
+
}
|
|
67
|
+
return start === 0 ? items : items.slice(start);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Plan mới nhất cho sidebar — todo items được append mỗi lần update nhưng
|
|
71
|
+
* sidebar chỉ cần snapshot hiện tại (last-wins), null khi chưa có plan.
|
|
72
|
+
*/
|
|
73
|
+
export function latestPlan(items) {
|
|
74
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
75
|
+
const it = items[i];
|
|
76
|
+
if (it.kind === "todo")
|
|
77
|
+
return it;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
19
81
|
function appendItem(state, item) {
|
|
20
82
|
state.items.push(item);
|
|
21
83
|
if (state.items.length > MAX_ITEMS)
|
|
@@ -30,22 +92,71 @@ function capTail(text, max = MAX_OUTPUT_CHARS) {
|
|
|
30
92
|
return text;
|
|
31
93
|
return `…[truncated ${text.length - max} chars]\n${text.slice(text.length - max)}`;
|
|
32
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Map tên tool → phase của breadcrumb (plan→explore→edit→build→verify).
|
|
97
|
+
* Trả về null với tool không thuộc phase nào (mcp__*...).
|
|
98
|
+
*/
|
|
99
|
+
export function toolPhase(name) {
|
|
100
|
+
if (name === "write_todos")
|
|
101
|
+
return "plan";
|
|
102
|
+
if (name === "list_files" || name === "read_file" || name === "web_fetch" || name === "load_skill")
|
|
103
|
+
return "explore";
|
|
104
|
+
if (name === "write_file" || name === "edit_file")
|
|
105
|
+
return "edit";
|
|
106
|
+
if (name === "run_command")
|
|
107
|
+
return "build";
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/** Guardrail event: đếm + giữ decision gần nhất (ngắn, tự giải thích cho header). */
|
|
111
|
+
function applyGuardrail(state, ev) {
|
|
112
|
+
// history-trim là routine context management — đếm làm gì, hiện làm gì:
|
|
113
|
+
// header chỉ hiển thị những can thiệp THẬT sự đáng chú ý (inject/finish/verify).
|
|
114
|
+
if (ev.action === "trim")
|
|
115
|
+
return;
|
|
116
|
+
state.guardrailCount += 1;
|
|
117
|
+
// Thông điệp thân thiện theo rule — không dump raw "rule · reason" lên UI.
|
|
118
|
+
const HUMAN = {
|
|
119
|
+
"wrap-up": "sắp hết bước — yêu cầu agent chốt việc và chạy build",
|
|
120
|
+
"chat-done": "lượt chat thuần — kết thúc không cần build",
|
|
121
|
+
"completion-claim": "agent báo hoàn thành — buộc chạy build xác minh",
|
|
122
|
+
"text-only-streak": "agent chỉ nói không làm — dừng run",
|
|
123
|
+
"continue-nudge": "agent trả lời text mà không dùng tool — nhắc tiếp tục làm việc",
|
|
124
|
+
"doom-loop": "lặp lẫn lộn cùng tool call — yêu cầu đổi hướng",
|
|
125
|
+
"build-stop": "build pass — kết thúc run",
|
|
126
|
+
};
|
|
127
|
+
state.lastGuardrail = HUMAN[ev.rule] ?? `${ev.rule}: ${ev.reason}`;
|
|
128
|
+
if (state.lastGuardrail.length > 72)
|
|
129
|
+
state.lastGuardrail = state.lastGuardrail.slice(0, 69) + "…";
|
|
130
|
+
}
|
|
33
131
|
/**
|
|
34
132
|
* Fold one AgentEvent into the state (mutates `state`, returns it for
|
|
35
133
|
* reducer-style chaining). Unknown events are ignored.
|
|
36
134
|
*/
|
|
37
135
|
export function applyEvent(state, ev) {
|
|
38
136
|
switch (ev.type) {
|
|
137
|
+
case "permission_request": {
|
|
138
|
+
// T-03: surfaced via a dedicated React state hook in TuiApp (not the
|
|
139
|
+
// chat items — the modal is modal, not transcript).
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
39
142
|
case "run_start": {
|
|
40
143
|
state.model = ev.model;
|
|
41
144
|
state.provider = ev.provider;
|
|
42
145
|
state.workdir = ev.workdir;
|
|
43
146
|
state.buildCmd = ev.buildCmd;
|
|
44
147
|
state.buildLamp = "none";
|
|
148
|
+
state.permission = ev.permission;
|
|
149
|
+
state.startedAt = Date.now();
|
|
150
|
+
state.endedAt = null;
|
|
45
151
|
state.status = "running";
|
|
46
152
|
state.step = 0;
|
|
47
153
|
state.summary = null;
|
|
48
154
|
state.usage = { calls: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
|
|
155
|
+
// Reset tracking của run mới.
|
|
156
|
+
state.guardrailCount = 0;
|
|
157
|
+
state.lastGuardrail = null;
|
|
158
|
+
state.phases = [];
|
|
159
|
+
state.streamingOutputChars = 0;
|
|
49
160
|
// The TUI pushes the user's message itself when submitting; avoid a
|
|
50
161
|
// duplicate when run_start carries the same task.
|
|
51
162
|
const last = lastItem(state);
|
|
@@ -54,6 +165,22 @@ export function applyEvent(state, ev) {
|
|
|
54
165
|
}
|
|
55
166
|
break;
|
|
56
167
|
}
|
|
168
|
+
case "llm_reasoning": {
|
|
169
|
+
// T-10: thinking stream — chỉ append khi thinkingVisible; vẫn giữ trong
|
|
170
|
+
// reasoning buffer riêng để toggle bật lại thấy (đơn giản: drop khi off).
|
|
171
|
+
if (!state.thinkingVisible)
|
|
172
|
+
break;
|
|
173
|
+
const last = lastItem(state);
|
|
174
|
+
if (last && last.kind === "thinking" && last.streaming) {
|
|
175
|
+
last.text += ev.delta;
|
|
176
|
+
if (last.text.length > MAX_TEXT_CHARS)
|
|
177
|
+
last.text = last.text.slice(-MAX_TEXT_CHARS);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
appendItem(state, { kind: "thinking", text: ev.delta, streaming: true });
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
57
184
|
case "step": {
|
|
58
185
|
state.step = ev.step;
|
|
59
186
|
state.maxSteps = ev.maxSteps;
|
|
@@ -61,9 +188,12 @@ export function applyEvent(state, ev) {
|
|
|
61
188
|
const last = lastItem(state);
|
|
62
189
|
if (last && last.kind === "assistant" && last.streaming)
|
|
63
190
|
last.streaming = false;
|
|
191
|
+
if (last && last.kind === "thinking" && last.streaming)
|
|
192
|
+
last.streaming = false;
|
|
64
193
|
break;
|
|
65
194
|
}
|
|
66
195
|
case "llm_delta": {
|
|
196
|
+
state.streamingOutputChars += ev.delta.length;
|
|
67
197
|
const last = lastItem(state);
|
|
68
198
|
if (last && last.kind === "assistant" && last.streaming) {
|
|
69
199
|
last.text += ev.delta;
|
|
@@ -89,7 +219,20 @@ export function applyEvent(state, ev) {
|
|
|
89
219
|
break;
|
|
90
220
|
}
|
|
91
221
|
case "tool_call": {
|
|
92
|
-
appendItem(state, {
|
|
222
|
+
appendItem(state, {
|
|
223
|
+
kind: "tool",
|
|
224
|
+
name: ev.name,
|
|
225
|
+
args: ev.args,
|
|
226
|
+
callId: ev.id,
|
|
227
|
+
ok: null,
|
|
228
|
+
output: "",
|
|
229
|
+
step: ev.step,
|
|
230
|
+
startedAt: Date.now(),
|
|
231
|
+
});
|
|
232
|
+
// Phase tracking cho breadcrumb (không trùng phase liền trước).
|
|
233
|
+
const phase = toolPhase(ev.name);
|
|
234
|
+
if (phase && state.phases[state.phases.length - 1] !== phase)
|
|
235
|
+
state.phases.push(phase);
|
|
93
236
|
break;
|
|
94
237
|
}
|
|
95
238
|
case "tool_output": {
|
|
@@ -109,13 +252,20 @@ export function applyEvent(state, ev) {
|
|
|
109
252
|
break;
|
|
110
253
|
}
|
|
111
254
|
case "tool_result": {
|
|
255
|
+
// T6: correlate theo callId (parallel calls cùng tên vẫn đúng card);
|
|
256
|
+
// fallback theo name cho event cũ không id.
|
|
112
257
|
for (let i = state.items.length - 1; i >= 0; i--) {
|
|
113
258
|
const it = state.items[i];
|
|
114
|
-
|
|
259
|
+
const match = it.kind === "tool" && it.ok === null && (ev.id ? it.callId === ev.id : it.name === ev.name);
|
|
260
|
+
if (match) {
|
|
115
261
|
it.ok = ev.ok;
|
|
262
|
+
if (it.startedAt != null)
|
|
263
|
+
it.durationMs = Date.now() - it.startedAt;
|
|
116
264
|
// Prefer the final (complete) result over accumulated live chunks.
|
|
117
265
|
if (ev.output.length >= it.output.length)
|
|
118
266
|
it.output = ev.output;
|
|
267
|
+
if (ev.image)
|
|
268
|
+
it.image = ev.image;
|
|
119
269
|
break;
|
|
120
270
|
}
|
|
121
271
|
}
|
|
@@ -136,16 +286,41 @@ export function applyEvent(state, ev) {
|
|
|
136
286
|
}
|
|
137
287
|
case "build": {
|
|
138
288
|
state.buildLamp = ev.success ? "ok" : "fail";
|
|
289
|
+
if (state.phases[state.phases.length - 1] !== "verify")
|
|
290
|
+
state.phases.push("verify");
|
|
139
291
|
appendItem(state, { kind: "build", success: ev.success, command: ev.command });
|
|
140
292
|
break;
|
|
141
293
|
}
|
|
294
|
+
case "todo": {
|
|
295
|
+
// Plan updates render as a live checkbox list in the chat pane.
|
|
296
|
+
appendItem(state, { kind: "todo", items: ev.items.map((t) => ({ ...t })) });
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case "guardrail": {
|
|
300
|
+
applyGuardrail(state, ev);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case "mcp_status": {
|
|
304
|
+
// Sidebar: structured event (server+state) → cập nhật map, không spam chat.
|
|
305
|
+
// onStatus riêng vẫn đẩy notice text đầy đủ cho transcript.
|
|
306
|
+
if (ev.server && ev.state) {
|
|
307
|
+
state.mcpServers = { ...state.mcpServers, [ev.server]: ev.state };
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
appendItem(state, { kind: "notice", text: ev.line, level: ev.line.includes("unavailable") ? "warn" : "info" });
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
142
313
|
case "finish": {
|
|
143
314
|
state.status = ev.success ? "success" : /interrupted|aborted/i.test(ev.summary) ? "aborted" : "failed";
|
|
144
315
|
state.summary = ev.summary;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
316
|
+
state.endedAt = Date.now();
|
|
317
|
+
// Close ALL dangling streaming assistant messages — not just the last
|
|
318
|
+
// item: a notice/tool item appended after the assistant text would
|
|
319
|
+
// otherwise leave it stuck in raw streaming mode (invisible text).
|
|
320
|
+
for (const it of state.items) {
|
|
321
|
+
if (it.kind === "assistant" && it.streaming)
|
|
322
|
+
it.streaming = false;
|
|
323
|
+
}
|
|
149
324
|
break;
|
|
150
325
|
}
|
|
151
326
|
case "error": {
|
|
@@ -159,6 +334,7 @@ export function applyEvent(state, ev) {
|
|
|
159
334
|
export function markAborted(state) {
|
|
160
335
|
state.status = "aborted";
|
|
161
336
|
state.summary = "abort requested…";
|
|
337
|
+
state.endedAt = Date.now();
|
|
162
338
|
const last = lastItem(state);
|
|
163
339
|
if (last && last.kind === "assistant")
|
|
164
340
|
last.streaming = false;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const MISSION = {
|
|
6
|
+
brand: "magenta", accent: "cyan", success: "green", error: "red", warn: "yellow",
|
|
7
|
+
user: "blue", dim: "gray", border: "gray",
|
|
8
|
+
mermaid: "magentaBright", thinking: "gray", code: "green", table: "cyan",
|
|
9
|
+
};
|
|
10
|
+
const PAPER = {
|
|
11
|
+
brand: "blue", accent: "blue", success: "green", error: "red", warn: "yellow",
|
|
12
|
+
user: "black", dim: "gray", border: "gray",
|
|
13
|
+
mermaid: "magentaBright", thinking: "gray", code: "green", table: "cyan",
|
|
14
|
+
};
|
|
15
|
+
const MONO = {
|
|
16
|
+
brand: "white", accent: "white", success: "white", error: "white", warn: "white",
|
|
17
|
+
user: "white", dim: "gray", border: "gray",
|
|
18
|
+
mermaid: "whiteBright", thinking: "white", code: "whiteBright", table: "whiteBright",
|
|
19
|
+
};
|
|
20
|
+
const LIGHT = {
|
|
21
|
+
brand: "magentaBright", accent: "cyanBright", success: "greenBright", error: "redBright",
|
|
22
|
+
warn: "yellowBright", user: "blueBright", dim: "blackBright", border: "blackBright",
|
|
23
|
+
mermaid: "magentaBright", thinking: "gray", code: "greenBright", table: "cyanBright",
|
|
24
|
+
};
|
|
25
|
+
const HIGH_CONTRAST = {
|
|
26
|
+
brand: "whiteBright", accent: "cyanBright", success: "greenBright", error: "redBright",
|
|
27
|
+
warn: "yellowBright", user: "whiteBright", dim: "white", border: "whiteBright",
|
|
28
|
+
mermaid: "magentaBright", thinking: "whiteBright", code: "greenBright", table: "cyanBright",
|
|
29
|
+
};
|
|
30
|
+
const SOLARIZED = {
|
|
31
|
+
brand: "magentaBright", accent: "cyanBright", success: "greenBright", error: "redBright",
|
|
32
|
+
warn: "yellowBright", user: "blueBright", dim: "gray", border: "gray",
|
|
33
|
+
mermaid: "magentaBright", thinking: "gray", code: "greenBright", table: "cyanBright",
|
|
34
|
+
};
|
|
35
|
+
export const THEMES = {
|
|
36
|
+
mission: MISSION, paper: PAPER, mono: MONO,
|
|
37
|
+
light: LIGHT, "high-contrast": HIGH_CONTRAST, solarized: SOLARIZED,
|
|
38
|
+
};
|
|
39
|
+
export function defaultThemeName(env = process.env) {
|
|
40
|
+
if (env.TERM === "dumb" || !env.TERM)
|
|
41
|
+
return "mono";
|
|
42
|
+
if (env.COLOR_COUNT && Number(env.COLOR_COUNT) < 16)
|
|
43
|
+
return "mono";
|
|
44
|
+
return "mission";
|
|
45
|
+
}
|
|
46
|
+
export function getTheme(workdir) {
|
|
47
|
+
const p = join(workdir, ".scoder", "theme.json");
|
|
48
|
+
if (existsSync(p)) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
51
|
+
const base = raw.name ? THEMES[raw.name] ?? THEMES[defaultThemeName()] : THEMES[defaultThemeName()];
|
|
52
|
+
return { ...base, ...Object.fromEntries(Object.entries(raw).filter(([k]) => k in base)) };
|
|
53
|
+
}
|
|
54
|
+
catch { /* malformed → default */ }
|
|
55
|
+
}
|
|
56
|
+
return THEMES[defaultThemeName()];
|
|
57
|
+
}
|
|
58
|
+
let active = MISSION;
|
|
59
|
+
export function setActiveTheme(t) { active = t; }
|
|
60
|
+
export function useNamedTheme(name) {
|
|
61
|
+
if (name === "list")
|
|
62
|
+
return `themes: ${Object.keys(THEMES).join(", ")} — /theme <name>`;
|
|
63
|
+
const t = THEMES[name];
|
|
64
|
+
if (!t)
|
|
65
|
+
return `ERROR: unknown theme "${name}" — available: ${Object.keys(THEMES).join(", ")}`;
|
|
66
|
+
active = t;
|
|
67
|
+
return `theme → ${name}`;
|
|
68
|
+
}
|
|
69
|
+
export const theme = new Proxy({}, {
|
|
70
|
+
get(_t, prop) { return active[prop]; },
|
|
71
|
+
});
|
|
72
|
+
/** Icon per tool name — compact visual vocabulary for the flight recorder. */
|
|
73
|
+
const TOOL_ICONS = {
|
|
74
|
+
list_files: "🗂", read_file: "📖", write_file: "📝", edit_file: "✏️",
|
|
75
|
+
run_command: "⚡", web_fetch: "🌐", write_todos: "☑",
|
|
76
|
+
};
|
|
77
|
+
export function toolIcon(name) {
|
|
78
|
+
if (TOOL_ICONS[name])
|
|
79
|
+
return TOOL_ICONS[name];
|
|
80
|
+
if (name.startsWith("mcp__"))
|
|
81
|
+
return "🔌";
|
|
82
|
+
return "🔧";
|
|
83
|
+
}
|
|
84
|
+
export function fit(text, max) {
|
|
85
|
+
return text.length <= max ? text : text.slice(0, Math.max(0, max - 1)) + "…";
|
|
86
|
+
}
|
|
87
|
+
/** The agent's signature mark, used as an avatar on assistant output. */
|
|
88
|
+
export function AgentMark() {
|
|
89
|
+
return _jsx(Text, { color: theme.brand, bold: true, children: "\u25C6 " });
|
|
90
|
+
}
|
package/dist/tui/vim.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T5: vim-motion cho Input — mode normal/insert, word motions.
|
|
3
|
+
* Module thuần (test được); Input chỉ wire keymap.
|
|
4
|
+
*/
|
|
5
|
+
/** w — đầu từ kế tiếp (vim: skip word chars rồi skip whitespace). */
|
|
6
|
+
export function wordForward(s, i) {
|
|
7
|
+
while (i < s.length && /\S/.test(s[i]))
|
|
8
|
+
i++;
|
|
9
|
+
while (i < s.length && /\s/.test(s[i]))
|
|
10
|
+
i++;
|
|
11
|
+
return Math.min(i, s.length);
|
|
12
|
+
}
|
|
13
|
+
/** b — đầu từ trước. */
|
|
14
|
+
export function wordBack(s, i) {
|
|
15
|
+
i--;
|
|
16
|
+
while (i > 0 && /\s/.test(s[i]))
|
|
17
|
+
i--;
|
|
18
|
+
while (i > 0 && /\S/.test(s[i - 1]))
|
|
19
|
+
i--;
|
|
20
|
+
return Math.max(0, i);
|
|
21
|
+
}
|
|
22
|
+
/** e — cuối từ hiện tại/kế tiếp. */
|
|
23
|
+
export function wordEnd(s, i) {
|
|
24
|
+
i++;
|
|
25
|
+
while (i < s.length - 1 && /\s/.test(s[i]))
|
|
26
|
+
i++;
|
|
27
|
+
while (i < s.length - 1 && /\S/.test(s[i + 1]))
|
|
28
|
+
i++;
|
|
29
|
+
return Math.min(i, Math.max(0, s.length - 1));
|
|
30
|
+
}
|
|
31
|
+
/** Áp dụng một phím normal-mode. Trả về unhandled=true khi không phải motion. */
|
|
32
|
+
export function applyNormalKey(value, cursor, input) {
|
|
33
|
+
const ok = (v, c, mode = "normal") => ({ value: v, cursor: c, mode, unhandled: false });
|
|
34
|
+
switch (input) {
|
|
35
|
+
case "h":
|
|
36
|
+
return ok(value, Math.max(0, cursor - 1));
|
|
37
|
+
case "l":
|
|
38
|
+
return ok(value, Math.min(value.length, cursor + 1));
|
|
39
|
+
case "w":
|
|
40
|
+
return ok(value, wordForward(value, cursor));
|
|
41
|
+
case "b":
|
|
42
|
+
return ok(value, wordBack(value, cursor));
|
|
43
|
+
case "e":
|
|
44
|
+
return ok(value, wordEnd(value, cursor));
|
|
45
|
+
case "0":
|
|
46
|
+
return ok(value, 0);
|
|
47
|
+
case "$":
|
|
48
|
+
return ok(value, value.length);
|
|
49
|
+
case "x":
|
|
50
|
+
return cursor < value.length ? ok(value.slice(0, cursor) + value.slice(cursor + 1), cursor) : ok(value, cursor);
|
|
51
|
+
case "i":
|
|
52
|
+
return ok(value, cursor, "insert");
|
|
53
|
+
case "a":
|
|
54
|
+
return ok(value, Math.min(value.length, cursor + 1), "insert");
|
|
55
|
+
case "A":
|
|
56
|
+
return ok(value, value.length, "insert");
|
|
57
|
+
default:
|
|
58
|
+
return { value, cursor, mode: "normal", unhandled: true };
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M4: hot-reload — watch .scoderrc + .scoder/skills/ (fs.watch, debounce
|
|
3
|
+
* 500ms). onChange(kind) — Agent quyết định hành động (reload skills /
|
|
4
|
+
* restart MCP). Module thuần, không dependency.
|
|
5
|
+
*/
|
|
6
|
+
import { watch, existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
const DEBOUNCE_MS = 500;
|
|
9
|
+
/** Watch rc file + skills dir. Fire onChange(kind) debounced (giữa các burst). */
|
|
10
|
+
export function watchWorkdir(workdir, onChange) {
|
|
11
|
+
const watchers = [];
|
|
12
|
+
const timers = new Map();
|
|
13
|
+
const fire = (kind) => {
|
|
14
|
+
const t = timers.get(kind);
|
|
15
|
+
if (t)
|
|
16
|
+
clearTimeout(t);
|
|
17
|
+
timers.set(kind, setTimeout(() => {
|
|
18
|
+
timers.delete(kind);
|
|
19
|
+
onChange(kind);
|
|
20
|
+
}, DEBOUNCE_MS));
|
|
21
|
+
};
|
|
22
|
+
// .scoderrc (hoặc .scoderrc.json) — config changes.
|
|
23
|
+
for (const name of [".scoderrc.json", ".scoderrc"]) {
|
|
24
|
+
const p = join(workdir, name);
|
|
25
|
+
if (!existsSync(p))
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
watchers.push(watch(p, () => fire("config")));
|
|
29
|
+
}
|
|
30
|
+
catch { /* race — file removed between existsSync and watch */ }
|
|
31
|
+
}
|
|
32
|
+
// .scoder/skills/ — skill files added/removed/edited.
|
|
33
|
+
const skillsDir = join(workdir, ".scoder", "skills");
|
|
34
|
+
if (existsSync(skillsDir)) {
|
|
35
|
+
try {
|
|
36
|
+
watchers.push(watch(skillsDir, { recursive: true }, () => fire("skills")));
|
|
37
|
+
}
|
|
38
|
+
catch { /* platform không hỗ trợ recursive (hiếm trên macOS/Windows OK) */ }
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
close() {
|
|
42
|
+
for (const w of watchers) {
|
|
43
|
+
try {
|
|
44
|
+
w.close();
|
|
45
|
+
}
|
|
46
|
+
catch { /* already closed */ }
|
|
47
|
+
}
|
|
48
|
+
for (const t of timers.values())
|
|
49
|
+
clearTimeout(t);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s-app-scoder-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Autonomous coding agent CLI
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Autonomous coding agent CLI — writes, edits, and builds code autonomously until the project compiles without errors.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -10,17 +10,19 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"prepare": "npm run build",
|
|
13
|
-
"prepublishOnly": "npm run lint && npm run typecheck
|
|
13
|
+
"prepublishOnly": "npm run lint && npm run typecheck",
|
|
14
14
|
"start": "node dist/index.js",
|
|
15
15
|
"dev": "tsc --watch",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"lint": "eslint src/",
|
|
18
18
|
"coverage": "npm run build && node --test --experimental-test-coverage test/*.test.mjs",
|
|
19
|
+
"eval": "node test/run-eval.mjs",
|
|
19
20
|
"test": "npm run build && node --test test/*.test.mjs && node test/run-e2e.mjs && node test/run-mcp-test.mjs"
|
|
20
21
|
},
|
|
21
22
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
23
|
+
"node": ">=20"
|
|
23
24
|
},
|
|
25
|
+
"author": "nbhson",
|
|
24
26
|
"keywords": [
|
|
25
27
|
"cli",
|
|
26
28
|
"agent",
|
|
@@ -41,18 +43,24 @@
|
|
|
41
43
|
"files": [
|
|
42
44
|
"dist",
|
|
43
45
|
"README.md",
|
|
44
|
-
"LICENSE"
|
|
46
|
+
"LICENSE",
|
|
47
|
+
".scoderrc.example.json"
|
|
45
48
|
],
|
|
46
49
|
"devDependencies": {
|
|
47
50
|
"@eslint/js": "^10.0.1",
|
|
48
51
|
"@types/node": "^22.0.0",
|
|
49
|
-
"@types/react": "^
|
|
52
|
+
"@types/react": "^19.3.0",
|
|
50
53
|
"eslint": "^10.13.0",
|
|
51
54
|
"typescript": "^5.6.0",
|
|
52
55
|
"typescript-eslint": "^8.72.1"
|
|
53
56
|
},
|
|
54
57
|
"dependencies": {
|
|
55
|
-
"
|
|
56
|
-
"
|
|
58
|
+
"fuzzysort": "^3.1.0",
|
|
59
|
+
"gpt-tokenizer": "^4.1.0",
|
|
60
|
+
"ink": "^8.0.0",
|
|
61
|
+
"react": "^19.3.1",
|
|
62
|
+
"react-dom": "^19.3.1",
|
|
63
|
+
"tree-sitter": "^0.25.1",
|
|
64
|
+
"tree-sitter-bash": "^0.25.1"
|
|
57
65
|
}
|
|
58
66
|
}
|
package/dist/tui-direct.mjs
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { runTui } from "./tui/app.js";
|
|
2
|
-
console.log("calling runTui");
|
|
3
|
-
await runTui({ workdir: process.cwd(), model: "m", provider: "openai", apiKey: "k", permissionMode: "auto", baseUrl: "http://127.0.0.1:1", maxSteps: 5, stream: false }, null);
|
|
4
|
-
console.log("returned");
|