s-app-scoder-agent 0.8.20 → 0.9.2
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 +50 -6
- package/dist/agent.js +754 -79
- 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 +37 -11
- 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/command-registry.js +297 -9
- package/dist/tui/commands.js +224 -36
- 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/commands.js
CHANGED
|
@@ -3,28 +3,45 @@
|
|
|
3
3
|
* (src/tui/app.tsx). Pure parsing/formatting only — each frontend executes
|
|
4
4
|
* the command against its own context, so both stay in sync on the surface.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{
|
|
9
|
-
{
|
|
10
|
-
{
|
|
11
|
-
{
|
|
12
|
-
{
|
|
13
|
-
{
|
|
14
|
-
{
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
18
|
-
{
|
|
19
|
-
{
|
|
20
|
-
{
|
|
6
|
+
/** Built-in commands — the single catalog both help and completion read. */
|
|
7
|
+
export const BUILTIN_COMMANDS = [
|
|
8
|
+
{ name: "/help", aliases: [], description: "show this command list", usage: "/help", visibility: "public", source: "builtin" },
|
|
9
|
+
{ name: "/plan", aliases: [], description: "show the current plan — /plan done|start <id|n> to mark steps", usage: "/plan [done|start <id|n|text>]", visibility: "public", source: "builtin" },
|
|
10
|
+
{ name: "/agent", aliases: [], description: "list selectable agents — /agent <name> to switch (idle only)", usage: "/agent [name]", visibility: "public", source: "builtin" },
|
|
11
|
+
{ name: "/init", aliases: [], description: "create an AGENTS.md template in this project", usage: "/init", visibility: "public", source: "builtin" },
|
|
12
|
+
{ name: "/sessions", aliases: [], description: "list saved sessions — /sessions resume <id|last>", usage: "/sessions [resume <id|last>]", visibility: "public", source: "builtin" },
|
|
13
|
+
{ name: "/diff", aliases: [], description: "show diffs: /diff turn | session | branch", usage: "/diff [turn|session|branch]", visibility: "tui-only", source: "builtin" },
|
|
14
|
+
{ name: "/theme", aliases: [], description: "switch palette: /theme <name> — /theme list (mission|paper|mono|light|high-contrast|solarized)", usage: "/theme [name|list]", visibility: "tui-only", source: "builtin" },
|
|
15
|
+
{ name: "/clear", aliases: [], description: "clear the conversation and reset agent context", usage: "/clear", visibility: "public", source: "builtin" },
|
|
16
|
+
{ name: "/compact", aliases: [], description: "summarize older history now (manual compaction)", usage: "/compact", visibility: "public", source: "builtin" },
|
|
17
|
+
{ name: "/cost", aliases: [], description: "token usage of the current session", usage: "/cost", visibility: "public", source: "builtin" },
|
|
18
|
+
{ name: "/model", aliases: [], description: "show the active model & provider", usage: "/model [provider/model]", visibility: "public", source: "builtin" },
|
|
19
|
+
{ name: "/skills", aliases: [], description: "list discovered skills (SKILL.md)", usage: "/skills", visibility: "public", source: "builtin" },
|
|
20
|
+
{ name: "/skill", aliases: [], description: "load a skill into context: /skill <name>", usage: "/skill <name>", visibility: "public", source: "builtin" },
|
|
21
|
+
{ name: "/mcp", aliases: [], description: "MCP servers & tools: /mcp [server] [tool] [json|text]", usage: "/mcp [server [tool [json]]]", visibility: "public", source: "builtin" },
|
|
22
|
+
{ name: "/review", aliases: [], description: "prompt gate: rewrite + user approval — /review <prompt>", usage: "/review <prompt>", visibility: "tui-only", source: "builtin" },
|
|
23
|
+
{ name: "/permission", aliases: [], description: "view or switch permission mode: /permission <auto|ask|readonly>", usage: "/permission [auto|ask|readonly]", visibility: "public", source: "builtin" },
|
|
24
|
+
{ name: "/undo", aliases: [], description: "revert files: /undo [files|edit] — edit also trims the turn back for re-send", usage: "/undo [files|edit]", visibility: "tui-only", source: "builtin" },
|
|
25
|
+
{ name: "/commands", aliases: [], description: "list all commands (builtin + custom) — /commands <name> to inspect", usage: "/commands [name]", visibility: "public", source: "builtin" },
|
|
26
|
+
{ name: "/export", aliases: [], description: "export session transcript: /export [md|json]", usage: "/export [md|json]", visibility: "public", source: "builtin" },
|
|
27
|
+
{ name: "/search", aliases: [], description: "search transcript: /search <query>", usage: "/search <query>", visibility: "public", source: "builtin" },
|
|
28
|
+
{ name: "/exit", aliases: ["/quit"], description: "quit (aliases: /quit, exit, quit)", usage: "/exit", visibility: "public", source: "builtin" },
|
|
21
29
|
];
|
|
30
|
+
/** Back-compat view used by completion + /help — derived from BUILTIN_COMMANDS. */
|
|
31
|
+
export const SLASH_COMMANDS = BUILTIN_COMMANDS
|
|
32
|
+
.filter((c) => c.visibility !== "hidden")
|
|
33
|
+
.map((c) => ({ cmd: c.name, desc: c.description }));
|
|
34
|
+
/** Resolve a command name or alias → canonical CommandDef (case-insensitive). */
|
|
35
|
+
export function resolveCommandDef(name) {
|
|
36
|
+
const n = name.toLowerCase();
|
|
37
|
+
return BUILTIN_COMMANDS.find((c) => c.name === n || c.aliases.includes(n));
|
|
38
|
+
}
|
|
22
39
|
/** Parse "/cmd arg…"; returns null when the input is not a slash command. */
|
|
23
40
|
export function parseSlashCommand(input) {
|
|
24
|
-
const m = /^\/([a-zA-Z][a-zA-Z0-9]*)(?:\s+(
|
|
41
|
+
const m = /^\/([a-zA-Z][a-zA-Z0-9-]*)(?:\s+([\s\S]*\S))?\s*$/.exec(input.trim());
|
|
25
42
|
if (!m)
|
|
26
43
|
return null;
|
|
27
|
-
return { cmd: "/" + m[1].toLowerCase(), arg: m[2] ?? "" };
|
|
44
|
+
return { cmd: resolveCommandDef("/" + m[1].toLowerCase())?.name ?? "/" + m[1].toLowerCase(), arg: m[2] ?? "" };
|
|
28
45
|
}
|
|
29
46
|
/**
|
|
30
47
|
* Tab completion for a partial input starting with "/".
|
|
@@ -52,44 +69,215 @@ export function completeSlashCommand(input) {
|
|
|
52
69
|
}
|
|
53
70
|
return { completed: prefix + (matches.length === 1 ? " " : ""), candidates: matches };
|
|
54
71
|
}
|
|
72
|
+
import { readdirSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
73
|
+
import { homedir } from "node:os";
|
|
74
|
+
import { join, resolve, basename, extname } from "node:path";
|
|
55
75
|
import { suggest } from "./at-complete.js";
|
|
76
|
+
/** Substitute $ARGUMENTS, $1..$n, @file references in the body. */
|
|
77
|
+
function substituteTemplate(body, argStr) {
|
|
78
|
+
const args = argStr.trim().split(/\s+/).filter(Boolean);
|
|
79
|
+
let out = body;
|
|
80
|
+
out = out.replace(/\$ARGUMENTS/g, argStr.trim());
|
|
81
|
+
for (let i = 0; i < args.length; i++) {
|
|
82
|
+
out = out.replace(new RegExp(`\\$${i + 1}`, "g"), args[i]);
|
|
83
|
+
}
|
|
84
|
+
out = out.replace(/\$ARGUMENTS/g, argStr.trim()); // repeat in case $ARGUMENTS contains $1
|
|
85
|
+
// @file references — leave as-is (agent interprets them).
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** Discover custom commands from ~/.scoder/commands/*.md + .scoder/commands/*.md. */
|
|
89
|
+
export function discoverCustomCommands(workdir) {
|
|
90
|
+
const dirs = [resolve(workdir, ".scoder", "commands"), join(homedir(), ".scoder", "commands")].filter((d) => existsSync(d));
|
|
91
|
+
const results = [];
|
|
92
|
+
for (const dir of dirs) {
|
|
93
|
+
let files;
|
|
94
|
+
try {
|
|
95
|
+
files = readdirSync(dir);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const file of files) {
|
|
101
|
+
if (extname(file) !== ".md")
|
|
102
|
+
continue;
|
|
103
|
+
const filePath = join(dir, file);
|
|
104
|
+
let stat;
|
|
105
|
+
try {
|
|
106
|
+
stat = statSync(filePath);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!stat.isFile())
|
|
112
|
+
continue;
|
|
113
|
+
const content = readFileSync(filePath, "utf8");
|
|
114
|
+
const parsed = parseCustomCommandFile(basename(file, ".md"), filePath, content, dir === resolve(workdir, ".scoder", "commands") ? "project" : "global");
|
|
115
|
+
if (parsed)
|
|
116
|
+
results.push(parsed);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
/** Parse a custom command .md file. Returns null on error. */
|
|
122
|
+
function parseCustomCommandFile(defaultName, filePath, content, source) {
|
|
123
|
+
const frontMatch = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(content.trim());
|
|
124
|
+
if (!frontMatch) {
|
|
125
|
+
// No frontmatter: body is the entire file, name is filename.
|
|
126
|
+
return {
|
|
127
|
+
name: defaultName,
|
|
128
|
+
description: content.trim().slice(0, 80),
|
|
129
|
+
visibility: "public",
|
|
130
|
+
aliases: [],
|
|
131
|
+
body: content.trim(),
|
|
132
|
+
source,
|
|
133
|
+
filePath,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const frontRaw = frontMatch[1];
|
|
137
|
+
const body = frontMatch[2].trim();
|
|
138
|
+
// Parse YAML frontmatter manually (no external dependency).
|
|
139
|
+
const front = {};
|
|
140
|
+
for (const line of frontRaw.split("\n")) {
|
|
141
|
+
const m = /^(\w+):\s*(.*)$/.exec(line.trim());
|
|
142
|
+
if (m) {
|
|
143
|
+
const [, key, rawVal] = m;
|
|
144
|
+
front[key] = rawVal.startsWith('"') && rawVal.endsWith('"')
|
|
145
|
+
? rawVal.slice(1, -1)
|
|
146
|
+
: rawVal;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
name: String(front.name ?? defaultName),
|
|
151
|
+
description: String(front.description ?? body.split("\n")[0].slice(0, 80)),
|
|
152
|
+
visibility: front.visibility ?? "public",
|
|
153
|
+
aliases: parseFrontList(front.aliases),
|
|
154
|
+
exec: front.exec ?? "prompt",
|
|
155
|
+
agent: front.agent ? String(front.agent) : undefined,
|
|
156
|
+
model: front.model ? String(front.model) : undefined,
|
|
157
|
+
shell: front.shell ? String(front.shell) : undefined,
|
|
158
|
+
args: front.args
|
|
159
|
+
? (Array.isArray(front.args)
|
|
160
|
+
? front.args.map(String)
|
|
161
|
+
: String(front.args)
|
|
162
|
+
// YAML flow array "[a, b]" → strip brackets, split on comma.
|
|
163
|
+
.replace(/^\[|\]$/g, "")
|
|
164
|
+
.split(",")
|
|
165
|
+
.map((s) => s.trim())
|
|
166
|
+
.filter(Boolean))
|
|
167
|
+
: undefined,
|
|
168
|
+
body,
|
|
169
|
+
source,
|
|
170
|
+
filePath,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Parse the small YAML list subset used by command aliases/args metadata. */
|
|
174
|
+
function parseFrontList(value) {
|
|
175
|
+
if (Array.isArray(value))
|
|
176
|
+
return value.map(String).map((s) => s.trim()).filter(Boolean);
|
|
177
|
+
if (typeof value !== "string")
|
|
178
|
+
return [];
|
|
179
|
+
const raw = value.trim().replace(/^\[|\]$/g, "");
|
|
180
|
+
return raw.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
181
|
+
}
|
|
182
|
+
/** Substitute + execute a custom command; returns the prompt text to inject. */
|
|
183
|
+
export function runCustomCommand(cmd, argStr) {
|
|
184
|
+
return substituteTemplate(cmd.body, argStr);
|
|
185
|
+
}
|
|
56
186
|
/** Build providers from runtime context (called once per keystroke burst). */
|
|
57
187
|
export function completionProviders(sources) {
|
|
58
|
-
|
|
188
|
+
const limit = sources.limit ?? 8;
|
|
189
|
+
const skills = sources.skills ?? [];
|
|
190
|
+
const agents = sources.agents ?? [];
|
|
191
|
+
const files = sources.files ?? [];
|
|
192
|
+
const commands = sources.commands ?? SLASH_COMMANDS;
|
|
193
|
+
const out = [
|
|
59
194
|
{
|
|
60
195
|
id: "commands",
|
|
61
196
|
match: (t) => t.startsWith("/") && !t.slice(1).includes(" "),
|
|
62
|
-
candidates: (t) =>
|
|
63
|
-
|
|
197
|
+
candidates: (t) => {
|
|
198
|
+
const q = t.toLowerCase();
|
|
199
|
+
return commands
|
|
200
|
+
.filter((c) => c.cmd.toLowerCase().startsWith(q) || c.cmd.toLowerCase().includes(q.slice(1)))
|
|
201
|
+
.slice(0, limit)
|
|
202
|
+
.map((c) => ({ label: c.cmd, desc: c.desc, insert: c.cmd + " " }));
|
|
203
|
+
},
|
|
64
204
|
},
|
|
65
|
-
|
|
205
|
+
];
|
|
206
|
+
if (files.length > 0) {
|
|
207
|
+
out.push({
|
|
66
208
|
id: "files",
|
|
67
209
|
match: (t) => t.startsWith("@") && t.length > 1,
|
|
68
|
-
candidates: (t) => suggest(
|
|
69
|
-
}
|
|
70
|
-
|
|
210
|
+
candidates: (t) => suggest(files, t.slice(1), limit).map((f) => ({ label: f, insert: "@" + f + (f.endsWith("/") ? "" : " ") })),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (skills.length > 0) {
|
|
214
|
+
out.push({
|
|
71
215
|
id: "skills",
|
|
72
216
|
match: (t) => t.startsWith("@") && t.length > 1,
|
|
73
217
|
candidates: (t) => {
|
|
74
218
|
const q = t.slice(1).toLowerCase();
|
|
75
|
-
return
|
|
76
|
-
.filter((s) => s.toLowerCase().includes(q))
|
|
77
|
-
.slice(0, 4)
|
|
78
|
-
.map((s) => ({ label: `@${s}`, desc: "skill", insert: `@${s} ` }));
|
|
219
|
+
return skills.filter((s) => s.toLowerCase().includes(q)).slice(0, limit).map((s) => ({ label: `@${s}`, desc: "skill", insert: `@${s} ` }));
|
|
79
220
|
},
|
|
80
|
-
}
|
|
81
|
-
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (agents.length > 0) {
|
|
224
|
+
out.push({
|
|
82
225
|
id: "agents",
|
|
83
226
|
match: (t) => t.startsWith("@") && t.length > 1,
|
|
84
227
|
candidates: (t) => {
|
|
85
228
|
const q = t.slice(1).toLowerCase();
|
|
86
|
-
return
|
|
87
|
-
.filter((a) => a.toLowerCase().includes(q))
|
|
88
|
-
.slice(0, 4)
|
|
89
|
-
.map((a) => ({ label: `@${a}`, desc: "agent", insert: `@${a} ` }));
|
|
229
|
+
return agents.filter((a) => a.toLowerCase().includes(q)).slice(0, limit).map((a) => ({ label: `@${a}`, desc: "agent", insert: `@${a} ` }));
|
|
90
230
|
},
|
|
91
|
-
}
|
|
92
|
-
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (sources.mcpServers && sources.mcpServers.length > 0) {
|
|
234
|
+
out.push({
|
|
235
|
+
id: "mcp",
|
|
236
|
+
match: (t) => t.startsWith("@") && t.length > 1,
|
|
237
|
+
candidates: (t) => {
|
|
238
|
+
const q = t.slice(1).toLowerCase();
|
|
239
|
+
const res = [];
|
|
240
|
+
for (const s of sources.mcpServers ?? []) {
|
|
241
|
+
if (s.name.toLowerCase().includes(q))
|
|
242
|
+
res.push({ label: `@${s.name}`, desc: "mcp server", insert: `@${s.name} ` });
|
|
243
|
+
for (const tool of s.tools ?? []) {
|
|
244
|
+
if (tool.toLowerCase().includes(q))
|
|
245
|
+
res.push({ label: `@${tool}`, desc: `mcp:${s.name}`, insert: `@${tool} ` });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return res.slice(0, limit);
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (sources.sessions && sources.sessions.length > 0) {
|
|
253
|
+
out.push({
|
|
254
|
+
id: "sessions",
|
|
255
|
+
match: (t) => t.startsWith("@") && t.length > 1,
|
|
256
|
+
candidates: (t) => {
|
|
257
|
+
const q = t.slice(1).toLowerCase();
|
|
258
|
+
return (sources.sessions ?? []).filter((s) => s.toLowerCase().includes(q)).slice(0, limit).map((s) => ({ label: `@${s}`, desc: "session", insert: `@${s} ` }));
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
/** Fuzzy score helper (Completion v2 ranking). Higher = better. */
|
|
265
|
+
export function fuzzyScore(query, target) {
|
|
266
|
+
const q = query.toLowerCase();
|
|
267
|
+
const t = target.toLowerCase();
|
|
268
|
+
if (!q)
|
|
269
|
+
return 0;
|
|
270
|
+
if (t.startsWith(q))
|
|
271
|
+
return 1000 - (t.length - q.length);
|
|
272
|
+
const idx = t.indexOf(q);
|
|
273
|
+
if (idx >= 0)
|
|
274
|
+
return 500 - idx;
|
|
275
|
+
// subsequence
|
|
276
|
+
let qi = 0;
|
|
277
|
+
for (let i = 0; i < t.length && qi < q.length; i++)
|
|
278
|
+
if (t[i] === q[qi])
|
|
279
|
+
qi++;
|
|
280
|
+
return qi === q.length ? 100 : 0;
|
|
93
281
|
}
|
|
94
282
|
/**
|
|
95
283
|
* Ghost text: when the current input has exactly ONE candidate across all
|
package/dist/tui/markdown.js
CHANGED
|
@@ -1,14 +1,121 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { theme } from "./theme.js";
|
|
4
|
-
const
|
|
4
|
+
const DEFAULT_WIDTH = 100;
|
|
5
5
|
const MAX_CODE_LINES = 60;
|
|
6
|
-
function clip(line) {
|
|
7
|
-
|
|
6
|
+
function clip(line, width = DEFAULT_WIDTH) {
|
|
7
|
+
const safeWidth = Math.max(8, width);
|
|
8
|
+
return line.length > safeWidth ? line.slice(0, safeWidth - 1) + "…" : line;
|
|
9
|
+
}
|
|
10
|
+
function contentWidth(width) {
|
|
11
|
+
return Math.max(32, Math.floor(width ?? DEFAULT_WIDTH));
|
|
12
|
+
}
|
|
13
|
+
function scalar(value) {
|
|
14
|
+
if (value == null)
|
|
15
|
+
return "";
|
|
16
|
+
return typeof value === "string" ? value.trim() : String(value);
|
|
17
|
+
}
|
|
18
|
+
/** Parse JSON or the intentionally small line format accepted by chart fences. */
|
|
19
|
+
export function parseChartSpec(source, language = "chart") {
|
|
20
|
+
const raw = source.trim();
|
|
21
|
+
if (!raw)
|
|
22
|
+
return null;
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
parsed = null;
|
|
29
|
+
}
|
|
30
|
+
const object = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
31
|
+
? parsed
|
|
32
|
+
: {};
|
|
33
|
+
const typeRaw = scalar(object.type) || language.split(/[:\s]+/)[1] || "bar";
|
|
34
|
+
const type = typeRaw.toLowerCase();
|
|
35
|
+
const rows = Array.isArray(parsed)
|
|
36
|
+
? parsed
|
|
37
|
+
: Array.isArray(object.data) ? object.data : [];
|
|
38
|
+
const data = rows.flatMap((row, index) => {
|
|
39
|
+
if (Array.isArray(row)) {
|
|
40
|
+
const value = Number(row[1]);
|
|
41
|
+
return Number.isFinite(value) ? [{ label: scalar(row[0]) || String(index + 1), value }] : [];
|
|
42
|
+
}
|
|
43
|
+
if (row && typeof row === "object") {
|
|
44
|
+
const r = row;
|
|
45
|
+
const value = Number(r.value ?? r.y ?? r.amount);
|
|
46
|
+
return Number.isFinite(value) ? [{ label: scalar(r.label ?? r.name ?? r.x) || String(index + 1), value }] : [];
|
|
47
|
+
}
|
|
48
|
+
return [];
|
|
49
|
+
});
|
|
50
|
+
// Friendly fallback: `label | value` or `label, value` per line.
|
|
51
|
+
if (data.length === 0) {
|
|
52
|
+
for (const [index, line] of raw.split("\n").entries()) {
|
|
53
|
+
if (!line.trim() || /^(type|title|data)\s*:/i.test(line))
|
|
54
|
+
continue;
|
|
55
|
+
const parts = line.split(/\s*[|,]\s*/);
|
|
56
|
+
const value = Number(parts.at(-1));
|
|
57
|
+
if (parts.length >= 2 && Number.isFinite(value))
|
|
58
|
+
data.push({ label: parts.slice(0, -1).join(" ").trim() || String(index + 1), value });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (data.length === 0 || !["bar", "line", "pie"].includes(type))
|
|
62
|
+
return null;
|
|
63
|
+
const title = scalar(object.title);
|
|
64
|
+
return {
|
|
65
|
+
type: type,
|
|
66
|
+
...(title ? { title } : {}),
|
|
67
|
+
data: data.slice(0, 40),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Parse JSON or `time | title | detail | status` timeline rows. */
|
|
71
|
+
export function parseTimelineSpec(source) {
|
|
72
|
+
const raw = source.trim();
|
|
73
|
+
if (!raw)
|
|
74
|
+
return [];
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(raw);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
parsed = null;
|
|
81
|
+
}
|
|
82
|
+
const rows = Array.isArray(parsed)
|
|
83
|
+
? parsed
|
|
84
|
+
: parsed && typeof parsed === "object" && Array.isArray(parsed.events)
|
|
85
|
+
? parsed.events
|
|
86
|
+
: [];
|
|
87
|
+
const events = rows.flatMap((row) => {
|
|
88
|
+
if (!row || typeof row !== "object")
|
|
89
|
+
return [];
|
|
90
|
+
const r = row;
|
|
91
|
+
const title = scalar(r.title ?? r.name ?? r.event);
|
|
92
|
+
if (!title)
|
|
93
|
+
return [];
|
|
94
|
+
const status = scalar(r.status).toLowerCase();
|
|
95
|
+
return [{
|
|
96
|
+
time: scalar(r.time ?? r.date ?? r.timestamp),
|
|
97
|
+
title,
|
|
98
|
+
detail: scalar(r.detail ?? r.description ?? r.body) || undefined,
|
|
99
|
+
status: ["done", "active", "pending", "error"].includes(status) ? status : undefined,
|
|
100
|
+
}];
|
|
101
|
+
});
|
|
102
|
+
if (events.length > 0)
|
|
103
|
+
return events.slice(0, 40);
|
|
104
|
+
return raw.split("\n").flatMap((line) => {
|
|
105
|
+
const trimmed = line.replace(/^[-*]\s*/, "").trim();
|
|
106
|
+
if (!trimmed)
|
|
107
|
+
return [];
|
|
108
|
+
const parts = trimmed.split(/\s*\|\s*/);
|
|
109
|
+
if (parts.length < 2)
|
|
110
|
+
return [];
|
|
111
|
+
const status = parts[3]?.toLowerCase();
|
|
112
|
+
return [{ time: parts[0], title: parts[1], detail: parts[2] || undefined,
|
|
113
|
+
status: ["done", "active", "pending", "error"].includes(status) ? status : undefined }];
|
|
114
|
+
}).slice(0, 40);
|
|
8
115
|
}
|
|
9
116
|
// ── Syntax highlighting ───────────────────────────────────────────────────────
|
|
10
117
|
/** T-15: token-based syntax highlighter for TypeScript/JavaScript/JSON/shell. */
|
|
11
|
-
function highlightCode(code) {
|
|
118
|
+
function highlightCode(code, width = DEFAULT_WIDTH) {
|
|
12
119
|
const kw = new Set(["const", "let", "var", "function", "async", "await", "return",
|
|
13
120
|
"if", "else", "for", "while", "do", "switch", "case", "break", "continue",
|
|
14
121
|
"try", "catch", "finally", "throw", "class", "extends", "import", "export",
|
|
@@ -18,7 +125,9 @@ function highlightCode(code) {
|
|
|
18
125
|
const typeKw = new Set(["string", "number", "boolean", "any", "unknown", "never",
|
|
19
126
|
"null", "undefined", "void", "object", "true", "false", "Promise", "Array",
|
|
20
127
|
"Record", "Partial", "Required", "Omit", "Pick", "keyof", "infer"]);
|
|
21
|
-
|
|
128
|
+
const codeWidth = Math.max(8, width - 6);
|
|
129
|
+
return code.slice(0, MAX_CODE_LINES).map((sourceLine, i) => {
|
|
130
|
+
const line = clip(sourceLine, codeWidth);
|
|
22
131
|
const parts = [];
|
|
23
132
|
const re = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(\b0x[\da-fA-F]+|\d+\.?\d*(?:[eE][+-]?\d+)?\b)|(\w+)/g;
|
|
24
133
|
let last = 0;
|
|
@@ -105,6 +214,37 @@ function mermaidPreview(code) {
|
|
|
105
214
|
result.push("└─────────────────────────────────────────────────────┘");
|
|
106
215
|
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: result.map((l, i) => (_jsx(Text, { color: theme.mermaid, children: i === 0 || i === result.length - 1 ? _jsx(Text, { bold: true, children: l }) : l }, i))) }));
|
|
107
216
|
}
|
|
217
|
+
function chartPreview(spec, width) {
|
|
218
|
+
const chartWidth = Math.max(24, Math.min(72, width - 8));
|
|
219
|
+
const max = Math.max(...spec.data.map((p) => Math.abs(p.value)), 1);
|
|
220
|
+
const min = Math.min(...spec.data.map((p) => p.value), 0);
|
|
221
|
+
const range = Math.max(1, max - min);
|
|
222
|
+
const labelWidth = Math.min(14, Math.max(4, ...spec.data.map((p) => p.label.length)));
|
|
223
|
+
const plotWidth = Math.max(12, chartWidth - labelWidth - 10);
|
|
224
|
+
const title = spec.title ? ` ${spec.title} ` : ` ${spec.type} chart `;
|
|
225
|
+
const rows = spec.data.map((point, index) => {
|
|
226
|
+
const ratio = (point.value - min) / range;
|
|
227
|
+
const filled = Math.max(0, Math.min(plotWidth, Math.round(ratio * plotWidth)));
|
|
228
|
+
const bar = "█".repeat(filled) + "░".repeat(plotWidth - filled);
|
|
229
|
+
const marker = spec.type === "line" ? (index === 0 ? "╭" : index === spec.data.length - 1 ? "╰" : "│") : "▌";
|
|
230
|
+
return ` ${point.label.slice(0, labelWidth).padEnd(labelWidth)} ${marker}${bar} ${String(point.value)}`;
|
|
231
|
+
});
|
|
232
|
+
if (spec.type === "pie") {
|
|
233
|
+
const total = spec.data.reduce((sum, p) => sum + Math.max(0, p.value), 0) || 1;
|
|
234
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.table, bold: true, children: ["\u250C\u2500", title, "─".repeat(Math.max(0, chartWidth - title.length - 2)), "\u2510"] }), spec.data.map((p, i) => {
|
|
235
|
+
const pct = Math.round((Math.max(0, p.value) / total) * 100);
|
|
236
|
+
const pieBarWidth = Math.max(1, chartWidth - labelWidth - 9);
|
|
237
|
+
const pieBar = "█".repeat(Math.min(pieBarWidth, Math.max(1, Math.round((pct / 100) * pieBarWidth))));
|
|
238
|
+
return _jsxs(Text, { color: theme.code, children: ["\u2502 ", p.label.slice(0, labelWidth).padEnd(labelWidth), " ", String(pct).padStart(3), "% ", pieBar.padEnd(pieBarWidth, " ")] }, i);
|
|
239
|
+
}), _jsxs(Text, { color: theme.table, bold: true, children: ["\u2514", "─".repeat(chartWidth), "\u2518"] })] }));
|
|
240
|
+
}
|
|
241
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.table, bold: true, children: ["\u250C\u2500", title, "─".repeat(Math.max(0, chartWidth - title.length - 2)), "\u2510"] }), rows.map((row, i) => _jsxs(Text, { color: theme.code, children: ["\u2502", row] }, i)), _jsxs(Text, { color: theme.table, bold: true, children: ["\u2514", "─".repeat(chartWidth), "\u2518"] })] }));
|
|
242
|
+
}
|
|
243
|
+
function timelinePreview(events, width) {
|
|
244
|
+
const detailWidth = Math.max(16, width - 18);
|
|
245
|
+
const icon = (status) => status === "done" ? "✓" : status === "error" ? "✗" : status === "active" ? "◆" : "○";
|
|
246
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.table, bold: true, children: [" timeline \u00B7 ", events.length, " event", events.length === 1 ? "" : "s"] }), events.map((event, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: event.status === "error" ? theme.error : event.status === "done" ? theme.success : theme.accent, bold: true, children: icon(event.status) }), _jsxs(Text, { dimColor: true, children: [" ", clip(event.time || "—", 16).padEnd(16), " "] }), _jsx(Text, { bold: true, children: clip(event.title, detailWidth) })] }), event.detail && _jsxs(Text, { dimColor: true, children: [" ", i === events.length - 1 ? " " : "│", " ", clip(event.detail, detailWidth)] }), i < events.length - 1 && _jsx(Text, { dimColor: true, children: " \u2502" })] }, i)))] }));
|
|
247
|
+
}
|
|
108
248
|
/** Render inline markdown tokens as nested Ink <Text> elements. */
|
|
109
249
|
function renderInline(text, keyPrefix) {
|
|
110
250
|
const parts = [];
|
|
@@ -134,7 +274,8 @@ function renderInline(text, keyPrefix) {
|
|
|
134
274
|
parts.push(text.slice(last));
|
|
135
275
|
return parts;
|
|
136
276
|
}
|
|
137
|
-
export function Markdown({ text }) {
|
|
277
|
+
export function Markdown({ text, width }) {
|
|
278
|
+
const paneWidth = contentWidth(width);
|
|
138
279
|
const lines = text.split("\n");
|
|
139
280
|
const blocks = [];
|
|
140
281
|
let i = 0;
|
|
@@ -154,8 +295,18 @@ export function Markdown({ text }) {
|
|
|
154
295
|
blocks.push(_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: " \u25C9 mermaid diagram" }), mermaidPreview(code)] }, key++));
|
|
155
296
|
continue;
|
|
156
297
|
}
|
|
298
|
+
if ((lang === "chart" || lang.startsWith("chart:")) && code.length > 0) {
|
|
299
|
+
const chart = parseChartSpec(code.join("\n"), lang);
|
|
300
|
+
blocks.push(chart ? _jsx(Box, { children: chartPreview(chart, paneWidth) }, key++) : _jsx(Text, { dimColor: true, children: " (invalid chart data)" }, key++));
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if ((lang === "timeline" || lang === "events") && code.length > 0) {
|
|
304
|
+
const events = parseTimelineSpec(code.join("\n"));
|
|
305
|
+
blocks.push(events.length > 0 ? _jsx(Box, { children: timelinePreview(events, paneWidth) }, key++) : _jsx(Text, { dimColor: true, children: " (empty timeline)" }, key++));
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
157
308
|
// T-15: syntax-highlighted code block
|
|
158
|
-
blocks.push(_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginTop: 1, children: [lang && _jsx(Text, { dimColor: true, bold: true, children: lang }), code.length === 0 ? (_jsx(Text, { dimColor: true, children: "(empty)" })) : (highlightCode(code)), code.length > MAX_CODE_LINES && (_jsxs(Text, { dimColor: true, children: [" \u2026 +", code.length - MAX_CODE_LINES, " more lines"] }))] }, key++));
|
|
309
|
+
blocks.push(_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginTop: 1, children: [lang && _jsx(Text, { dimColor: true, bold: true, children: lang }), code.length === 0 ? (_jsx(Text, { dimColor: true, children: "(empty)" })) : (highlightCode(code, paneWidth)), code.length > MAX_CODE_LINES && (_jsxs(Text, { dimColor: true, children: [" \u2026 +", code.length - MAX_CODE_LINES, " more lines"] }))] }, key++));
|
|
159
310
|
continue;
|
|
160
311
|
}
|
|
161
312
|
// Headers
|
|
@@ -175,12 +326,19 @@ export function Markdown({ text }) {
|
|
|
175
326
|
while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i]))
|
|
176
327
|
rows.push(parseRow(lines[i++]));
|
|
177
328
|
// Column widths (bounded — wide cells clip via clip()).
|
|
178
|
-
const
|
|
329
|
+
const available = Math.max(20, paneWidth - 4);
|
|
330
|
+
const naturalWidths = header.map((h, c) => Math.min(MAX_CELL, Math.max(h.length, ...rows.map((r) => (r[c] ?? "").length))));
|
|
331
|
+
const separators = Math.max(0, header.length - 1) * 3 + 4;
|
|
332
|
+
const totalNatural = naturalWidths.reduce((sum, n) => sum + n, 0);
|
|
333
|
+
const widths = naturalWidths.map((n) => totalNatural + separators <= available ? n : Math.max(3, Math.floor((n / Math.max(1, totalNatural)) * (available - separators))));
|
|
179
334
|
const renderRow = (cells, chars, color, bold, rowKey) => {
|
|
180
|
-
const line = chars.l +
|
|
181
|
-
|
|
335
|
+
const line = chars.l + widths.map((width, ci) => {
|
|
336
|
+
const cell = cells[ci] ?? "";
|
|
337
|
+
return (cell.length > width ? cell.slice(0, Math.max(1, width - 1)) + "…" : cell).padEnd(width, " ");
|
|
338
|
+
}).join(chars.m) + chars.r;
|
|
339
|
+
return _jsx(Text, { color: color, bold: bold, children: clip(line, paneWidth) }, rowKey);
|
|
182
340
|
};
|
|
183
|
-
blocks.push(_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [renderRow(header, { l: "┌ ", m: " ┬ ", r: " ┐" },
|
|
341
|
+
blocks.push(_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [renderRow(header, { l: "┌ ", m: " ┬ ", r: " ┐" }, theme.table, true, `th${key}`), renderRow(header.map(() => ""), { l: "├", m: "┼", r: "┤" }, "gray", false, `ts${key}`), rows.map((r, ri) => renderRow(r, { l: "│ ", m: " │ ", r: " │" }, undefined, false, `tr${key}-${ri}`))] }, key++));
|
|
184
342
|
continue;
|
|
185
343
|
}
|
|
186
344
|
// Bullets + task lists: - [ ] pending · - [x] completed
|
|
@@ -222,7 +380,7 @@ export function Markdown({ text }) {
|
|
|
222
380
|
i++;
|
|
223
381
|
continue;
|
|
224
382
|
}
|
|
225
|
-
blocks.push(_jsx(Text, { children: renderInline(clip(line), `t${key}`) }, key++));
|
|
383
|
+
blocks.push(_jsx(Text, { children: renderInline(clip(line, paneWidth), `t${key}`) }, key++));
|
|
226
384
|
i++;
|
|
227
385
|
}
|
|
228
386
|
return _jsx(Box, { flexDirection: "column", children: blocks });
|
package/dist/tui/sidebar.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { contextWindowFor } from "../model-catalog.js";
|
|
4
4
|
import { resolveModelPricing } from "../constants.js";
|
|
@@ -44,20 +44,30 @@ function MetricsSection({ state }) {
|
|
|
44
44
|
const gauge = fuelGauge(state.step, state.maxSteps);
|
|
45
45
|
const cost = (state.usage.inputTokens / 1_000_000) * resolveModelPricing(state.model).input +
|
|
46
46
|
(state.usage.outputTokens / 1_000_000) * resolveModelPricing(state.model).output;
|
|
47
|
-
// tok/s
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Live estimate
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
// tok/s of the most recently completed LLM call (output tokens / call duration).
|
|
48
|
+
// This is NOT averaged over the whole run (which includes tool/build time and
|
|
49
|
+
// dilutes the number); it reflects the model's actual generation speed.
|
|
50
|
+
const tokPerSec = state.lastLlmTokPerSec;
|
|
51
|
+
// Live estimate during an active stream: chars so far / elapsed since the
|
|
52
|
+
// first delta of this stream.
|
|
53
|
+
const streamElapsedMs = state.streamStartedAt != null ? Date.now() - state.streamStartedAt : null;
|
|
54
|
+
const liveTokPerSec = state.streamingOutputChars > 0 && streamElapsedMs != null && streamElapsedMs > 250
|
|
55
|
+
? Math.round((state.streamingOutputChars / 4) / (streamElapsedMs / 1000))
|
|
54
56
|
: null;
|
|
55
57
|
const win = contextWindowFor(state.provider, state.model);
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
+
// `ctx` should reflect the CURRENT context window pressure (estimateTokens of
|
|
59
|
+
// the live message history), NOT the cumulative input tokens spent across the
|
|
60
|
+
// whole session (state.usage.inputTokens grows forever and can exceed 100%).
|
|
61
|
+
// state.context is emitted by the agent after every LLM turn; fall back to the
|
|
62
|
+
// cumulative counter only before the first turn has reported.
|
|
63
|
+
const ctxTokens = state.context?.tokens ?? (hasUsage ? state.usage.inputTokens : null);
|
|
64
|
+
const ctxWindow = state.context?.window ?? win;
|
|
65
|
+
const ctxPct = state.context?.pct ??
|
|
66
|
+
(ctxTokens != null && ctxWindow > 0 ? Math.min(100, Math.round((ctxTokens / ctxWindow) * 100)) : null);
|
|
67
|
+
// ctx giờ hiển thị đủ số: `current/ctxWin (pct%)`.
|
|
58
68
|
const ctxFmt = (n) => (n >= 1000 ? `${(n / 1000).toFixed(0)}k` : String(n));
|
|
59
|
-
const ctxDisplay = ctxPct != null
|
|
60
|
-
? `${ctxFmt(
|
|
69
|
+
const ctxDisplay = ctxTokens != null && ctxPct != null
|
|
70
|
+
? `${ctxFmt(ctxTokens)}/${ctxFmt(ctxWindow)} (${ctxPct}%)`
|
|
61
71
|
: null;
|
|
62
72
|
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 }))] })) }));
|
|
63
73
|
}
|
|
@@ -92,8 +102,21 @@ function McpSection({ state, config }) {
|
|
|
92
102
|
return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: color, children: [" ", mark, " "] }), _jsx(Text, { color: color, children: fit(n, budget) }), _jsxs(Text, { dimColor: true, children: [" ", st] })] }, n));
|
|
93
103
|
}) })) }));
|
|
94
104
|
}
|
|
105
|
+
function SubagentSection({ state }) {
|
|
106
|
+
if (state.subagents.length === 0)
|
|
107
|
+
return _jsx(_Fragment, {});
|
|
108
|
+
return (_jsx(Section, { title: "SUBAGENTS", children: state.subagents.map((s) => {
|
|
109
|
+
const color = s.status === "running" ? theme.accent :
|
|
110
|
+
s.status === "ok" ? theme.success :
|
|
111
|
+
theme.error;
|
|
112
|
+
const mark = s.status === "running" ? "◐" : s.status === "ok" ? "✔" : "✘";
|
|
113
|
+
const summary = s.summary ? ` — ${s.summary.slice(0, 50)}${s.summary.length > 50 ? "…" : ""}` : "";
|
|
114
|
+
const steps = s.steps != null ? ` (${s.steps} steps)` : "";
|
|
115
|
+
return (_jsxs(Text, { wrap: "truncate", dimColor: true, children: [_jsxs(Text, { color: color, children: [" ", mark, " "] }), _jsx(Text, { color: color, children: s.name }), _jsxs(Text, { dimColor: true, children: [summary, steps] })] }, s.id));
|
|
116
|
+
}) }));
|
|
117
|
+
}
|
|
95
118
|
// ── Root ─────────────────────────────────────────────────────────────────────
|
|
96
119
|
/** Sidebar phải — mọi trạng thái sống của run, cập nhật theo AgentEvent. */
|
|
97
120
|
export function Sidebar({ state, config }) {
|
|
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(PhaseSection, { state: state }), _jsx(PlanSection, { state: state }), _jsx(BuildSection, { state: state, config: config })] }));
|
|
121
|
+
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(PhaseSection, { state: state }), _jsx(PlanSection, { state: state }), _jsx(SubagentSection, { state: state }), _jsx(BuildSection, { state: state, config: config })] }));
|
|
99
122
|
}
|