@semalt-ai/code 1.8.5 → 1.20.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/.claude/settings.local.json +7 -1
- package/.github/workflows/ci.yml +69 -0
- package/ARCHITECTURE.md +6 -95
- package/CLAUDE.md +196 -316
- package/README.md +148 -4
- package/docs/ARCHITECTURE.md +1321 -0
- package/docs/CONFIG.md +340 -0
- package/docs/HISTORY.md +245 -0
- package/examples/embed.js +74 -0
- package/index.js +251 -10
- package/lib/agent.js +856 -120
- package/lib/api.js +239 -50
- package/lib/args.js +74 -2
- package/lib/audit.js +23 -1
- package/lib/background.js +584 -0
- package/lib/checkpoints.js +757 -0
- package/lib/commands/auth.js +94 -0
- package/lib/commands/chat-session.js +489 -0
- package/lib/commands/chat-slash.js +415 -0
- package/lib/commands/chat-turn.js +669 -0
- package/lib/commands/chat.js +407 -0
- package/lib/commands/custom.js +157 -0
- package/lib/commands/history-utils.js +66 -0
- package/lib/commands/index.js +268 -0
- package/lib/commands/mcp.js +113 -0
- package/lib/commands/oneshot.js +193 -0
- package/lib/commands/registry.js +269 -0
- package/lib/commands/tasks.js +89 -0
- package/lib/compact.js +87 -0
- package/lib/config.js +360 -11
- package/lib/constants.js +401 -3
- package/lib/deny.js +199 -0
- package/lib/doctor.js +160 -0
- package/lib/headless.js +202 -0
- package/lib/hooks.js +286 -0
- package/lib/images.js +270 -0
- package/lib/internals.js +49 -0
- package/lib/mcp/boundary.js +131 -0
- package/lib/mcp/client.js +270 -0
- package/lib/mcp/oauth.js +134 -0
- package/lib/memory.js +209 -0
- package/lib/metrics.js +37 -2
- package/lib/payload.js +54 -0
- package/lib/permission-rules.js +401 -0
- package/lib/permissions.js +123 -26
- package/lib/pricing.js +67 -0
- package/lib/proc.js +62 -0
- package/lib/prompts.js +99 -8
- package/lib/sandbox.js +568 -0
- package/lib/sdk.js +328 -0
- package/lib/secrets.js +211 -0
- package/lib/skills.js +223 -0
- package/lib/subagents.js +516 -0
- package/lib/tool_registry.js +2862 -0
- package/lib/tool_specs.js +263 -9
- package/lib/tools.js +352 -1039
- package/lib/ui/anim.js +86 -0
- package/lib/ui/ansi.js +17 -27
- package/lib/ui/chat-history.js +253 -71
- package/lib/ui/create-ui.js +67 -24
- package/lib/ui/diff.js +90 -25
- package/lib/ui/file-activity.js +236 -0
- package/lib/ui/format.js +195 -29
- package/lib/ui/input-field.js +21 -11
- package/lib/ui/md-stream.js +234 -0
- package/lib/ui/render-operation.js +113 -0
- package/lib/ui/select.js +1 -4
- package/lib/ui/status-bar.js +146 -36
- package/lib/ui/stream.js +20 -13
- package/lib/ui/theme.js +190 -44
- package/lib/ui/tool-operation.js +190 -0
- package/lib/ui/utils.js +9 -5
- package/lib/ui/web-activity.js +270 -0
- package/lib/ui/writer.js +159 -45
- package/lib/ui.js +1 -1
- package/lib/verify.js +229 -0
- package/lib/web-extract.js +213 -0
- package/lib/web-summarize.js +68 -0
- package/package.json +19 -4
- package/scripts/lint.js +57 -0
- package/test/agent-loop.test.js +389 -0
- package/test/anim-driver.test.js +153 -0
- package/test/ask-user-display.test.js +226 -0
- package/test/ask-user-gate.test.js +231 -0
- package/test/background.test.js +414 -0
- package/test/chat-history-nocolor.test.js +155 -0
- package/test/chat-relogin.test.js +207 -0
- package/test/chat.test.js +114 -0
- package/test/checkpoints-agent.test.js +181 -0
- package/test/checkpoints.test.js +650 -0
- package/test/command-registry.test.js +160 -0
- package/test/compact.test.js +116 -0
- package/test/completion-lazy.test.js +52 -0
- package/test/config-merge.test.js +324 -0
- package/test/config-quarantine.test.js +128 -0
- package/test/config-write-guard-allow-anywhere.test.js +56 -0
- package/test/config-write-guard-skip.test.js +46 -0
- package/test/config-write-guard.test.js +153 -0
- package/test/context-split.test.js +215 -0
- package/test/cost-doctor.test.js +142 -0
- package/test/custom-commands-chat.test.js +106 -0
- package/test/custom-commands.test.js +230 -0
- package/test/defer-detail-band.test.js +403 -0
- package/test/deny-windows.test.js +120 -0
- package/test/deny.test.js +83 -0
- package/test/detail-band-tab-flatten.test.js +242 -0
- package/test/download-allow-anywhere.test.js +66 -0
- package/test/download-confine.test.js +153 -0
- package/test/exec-diff.test.js +268 -0
- package/test/executors.test.js +599 -0
- package/test/extract-tool-calls.test.js +349 -0
- package/test/fetch-url-validation.test.js +219 -0
- package/test/file-activity.test.js +522 -0
- package/test/fixtures/tool-calls.js +57 -0
- package/test/fixtures/web-page.js +91 -0
- package/test/git-tools.test.js +384 -0
- package/test/grep-glob-serialize.test.js +242 -0
- package/test/grep-glob.test.js +268 -0
- package/test/grep-path-target.test.js +227 -0
- package/test/harness/README.md +57 -0
- package/test/harness/chat-harness.js +143 -0
- package/test/harness/memwarn-headless-child.js +65 -0
- package/test/harness/mock-llm.js +120 -0
- package/test/harness/mock-mcp-server.js +142 -0
- package/test/harness/sse-server.js +69 -0
- package/test/headless.test.js +348 -0
- package/test/history-utils.test.js +88 -0
- package/test/hooks-agent.test.js +238 -0
- package/test/hooks-verify-sandbox.test.js +232 -0
- package/test/hooks.test.js +216 -0
- package/test/http-get-user-agent.test.js +142 -0
- package/test/images-api.test.js +208 -0
- package/test/images.test.js +238 -0
- package/test/input-field-ctrl-o.test.js +37 -0
- package/test/live-height-physical.test.js +281 -0
- package/test/max-iterations.test.js +218 -0
- package/test/mcp-boundary.test.js +57 -0
- package/test/mcp-client.test.js +267 -0
- package/test/mcp-oauth.test.js +86 -0
- package/test/md-stream.test.js +183 -0
- package/test/memory-truncation-warning.test.js +222 -0
- package/test/memory.test.js +198 -0
- package/test/native-dispatch.test.js +409 -0
- package/test/native-live-narration.test.js +254 -0
- package/test/output-chokepoint.test.js +188 -0
- package/test/output-heredoc-leak.test.js +195 -0
- package/test/output-preview.test.js +245 -0
- package/test/path-guards.test.js +134 -0
- package/test/payload.test.js +99 -0
- package/test/permission-rules-agent.test.js +210 -0
- package/test/permission-rules.test.js +297 -0
- package/test/permissions.test.js +362 -0
- package/test/plan-mode.test.js +167 -0
- package/test/read-paginate.test.js +275 -0
- package/test/readonly-tools.test.js +177 -0
- package/test/render-operation.test.js +317 -0
- package/test/replay-descriptor-xml.test.js +216 -0
- package/test/replay-descriptor.test.js +189 -0
- package/test/replay-web-aggregate.test.js +291 -0
- package/test/replay-web-persist.test.js +241 -0
- package/test/result-cap.test.js +233 -0
- package/test/running-glyph-anim.test.js +111 -0
- package/test/sandbox-agent.test.js +147 -0
- package/test/sandbox-integration.test.js +216 -0
- package/test/sandbox.test.js +408 -0
- package/test/sdk.test.js +234 -0
- package/test/shell-output-cap.test.js +181 -0
- package/test/skills-chat.test.js +110 -0
- package/test/skills.test.js +295 -0
- package/test/smoke.test.js +68 -0
- package/test/status-bar-driver.test.js +93 -0
- package/test/status-bar-pause.test.js +164 -0
- package/test/status-bar-resync.test.js +188 -0
- package/test/stream-parser.test.js +171 -0
- package/test/subagents-agent.test.js +178 -0
- package/test/subagents.test.js +222 -0
- package/test/theme-palette.test.js +166 -0
- package/test/tool-registry.test.js +85 -0
- package/test/trim-budget.test.js +101 -0
- package/test/truncate-visible.test.js +78 -0
- package/test/verify-agent.test.js +317 -0
- package/test/verify.test.js +141 -0
- package/test/view-image.test.js +199 -0
- package/test/web-activity-ordering.test.js +203 -0
- package/test/web-activity.test.js +207 -0
- package/test/web-data-extraction-guidance.test.js +71 -0
- package/test/web-extract.test.js +185 -0
- package/test/web-fetch-agent.test.js +291 -0
- package/test/web-fetch-mode.test.js +193 -0
- package/test/web-search.test.js +380 -0
- package/lib/commands.js +0 -1438
- package/path +0 -1
package/lib/ui/theme.js
CHANGED
|
@@ -1,39 +1,113 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// THE single palette table (Output Refactor — Phase 2.5).
|
|
4
|
+
//
|
|
5
|
+
// Every colour the chrome surfaces use is defined here: tool-status lines,
|
|
6
|
+
// the status bar, diff bodies, the web-activity summary, debug/meta blocks,
|
|
7
|
+
// and the legacy message-role palette (`THEME` / `FG_*`) that the rest of the
|
|
8
|
+
// app still imports. `ansi.js` holds only the ANSI *primitives* (the SGR
|
|
9
|
+
// builders + spinners) and re-exports this palette for back-compat.
|
|
10
|
+
//
|
|
11
|
+
// Consolidation note: this module deliberately does NOT `require('./ansi')`.
|
|
12
|
+
// It defines its own private SGR builders below. That keeps the dependency
|
|
13
|
+
// one-directional (ansi.js → theme.js) so the two files never form a require
|
|
14
|
+
// cycle, regardless of which one an entrypoint loads first.
|
|
15
|
+
|
|
16
|
+
// Private SGR builders — not exported (ansi.js owns the public ones).
|
|
17
|
+
const fg = (n) => `\x1b[38;5;${n}m`;
|
|
18
|
+
const bg = (n) => `\x1b[48;5;${n}m`;
|
|
19
|
+
const fgRGB = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
|
|
20
|
+
const bgRGB = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
|
|
21
|
+
|
|
22
|
+
// ── NO_COLOR + non-TTY gate ─────────────────────────────────────────────────
|
|
23
|
+
// The single place colour is switched off. `colorEnabled()` is dynamic (read
|
|
24
|
+
// per call) so the same process can answer differently for a piped child or a
|
|
25
|
+
// test that sets NO_COLOR. Per the NO_COLOR spec, ANY non-empty value disables
|
|
26
|
+
// colour; an empty string does not. `colorize()` is the resolver-side helper —
|
|
27
|
+
// it returns the code when colour is on, '' otherwise.
|
|
28
|
+
function colorEnabled() {
|
|
29
|
+
return process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
30
|
+
}
|
|
31
|
+
function colorize(code) {
|
|
32
|
+
return colorEnabled() ? code : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Legacy message-role palette (relocated verbatim from ansi.js) ───────────
|
|
36
|
+
// Values are unchanged — these drive message roles, menus, prompts and syntax
|
|
37
|
+
// chrome, which are out of scope for the Phase 2.5 saturation pass. They live
|
|
38
|
+
// here only so colour has a single home; ansi.js re-exports them.
|
|
39
|
+
const THEME = {
|
|
40
|
+
user: '\x1b[36m',
|
|
41
|
+
agent: '\x1b[32m',
|
|
42
|
+
sys: '\x1b[33m',
|
|
43
|
+
error: '\x1b[31;1m',
|
|
44
|
+
warn: '\x1b[33;1m',
|
|
45
|
+
tool: '\x1b[35m',
|
|
46
|
+
dim: '\x1b[2m',
|
|
47
|
+
reset: '\x1b[0m',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const FG_GRAY = fg(245);
|
|
51
|
+
const FG_DARK = fg(240);
|
|
52
|
+
const FG_BLUE = fg(75);
|
|
53
|
+
const FG_CYAN = fg(116);
|
|
54
|
+
const FG_GREEN = fg(114);
|
|
55
|
+
const FG_YELLOW = fg(222);
|
|
56
|
+
const FG_RED = fg(203);
|
|
57
|
+
const FG_TEAL = fg(73);
|
|
58
|
+
|
|
59
|
+
const FG_CODE_BG = bg(236);
|
|
60
|
+
const BG_SELECTED = bg(237);
|
|
61
|
+
const FG_CODE_BORDER = fg(240);
|
|
62
|
+
const FG_CODE_LANG = fg(75);
|
|
63
|
+
const FG_TAG = fg(176);
|
|
64
|
+
const FG_FILEPATH = fg(222);
|
|
4
65
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
66
|
+
// ── Saturated chrome palette (Phase 2.5) ────────────────────────────────────
|
|
67
|
+
// Dark-terminal assumption: full saturation, differentiate categories. Tunable
|
|
68
|
+
// here — every colour the tool line / status bar / web summary read resolves
|
|
69
|
+
// through `resolveLineColors`, so a category that reads wrong is a one-line edit.
|
|
9
70
|
const UI_THEME = {
|
|
10
|
-
success:
|
|
11
|
-
warning:
|
|
12
|
-
error:
|
|
13
|
-
info:
|
|
14
|
-
muted:
|
|
15
|
-
subtle:
|
|
16
|
-
accent:
|
|
71
|
+
success: fg(40), // vivid green — ok glyph, replay-success indicator
|
|
72
|
+
warning: fg(214), // amber
|
|
73
|
+
error: fg(203), // alarming red (not pink)
|
|
74
|
+
info: fg(75), // light blue
|
|
75
|
+
muted: fg(240), // dim gray — frames, separators
|
|
76
|
+
subtle: fg(244), // secondary text — durations, meta, tokens
|
|
77
|
+
accent: fg(141), // purple — model name, IDs
|
|
17
78
|
default: '\x1b[39m', // terminal default fg
|
|
18
|
-
// Per-category accent
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
79
|
+
// Per-category accent. Keyed by the category STRING produced by
|
|
80
|
+
// `categoryForTag` (shell/file/net/…), so a lookup is `categories[cat]`.
|
|
81
|
+
// Saturated + differentiated; `git`/`mcp` are first-class (no longer folded
|
|
82
|
+
// into the `tool` fallback); `tool` is demoted to gray so real categories pop.
|
|
22
83
|
categories: {
|
|
23
|
-
|
|
24
|
-
file:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
84
|
+
shell: fg(214), // orange
|
|
85
|
+
file: fg(77), // green
|
|
86
|
+
net: fg(39), // bright blue
|
|
87
|
+
web: fg(44), // cyan
|
|
88
|
+
git: fg(170), // magenta-pink
|
|
89
|
+
mcp: fg(141), // purple
|
|
90
|
+
user: fg(211), // pink
|
|
91
|
+
memory: fg(183), // lavender
|
|
92
|
+
env: fg(186), // pale gold
|
|
93
|
+
system: fg(109), // steel blue
|
|
94
|
+
debug: fg(244), // gray
|
|
95
|
+
tool: fg(245), // gray — fallback, demoted so categories differentiate
|
|
32
96
|
},
|
|
33
97
|
};
|
|
34
98
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
99
|
+
// Status → glyph colour. Saturated; the running indicator is never gray (it's
|
|
100
|
+
// the most time-sensitive element — Phase 3 animates it, but even static it
|
|
101
|
+
// must read as live).
|
|
102
|
+
const STATUS_COLORS = {
|
|
103
|
+
ok: fg(40), // vivid green ✓
|
|
104
|
+
error: fg(203), // red ✗ (alarming, not pink)
|
|
105
|
+
running: fg(39), // cyan ● — fallback when a category has no vivid tint
|
|
106
|
+
warning: fg(214), // amber ⚠
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Indicator glyphs for status lines. Separated from colour so the renderer can
|
|
110
|
+
// swap either independently (e.g. an ASCII-only fallback later).
|
|
37
111
|
const UI_ICONS = {
|
|
38
112
|
pending: '●',
|
|
39
113
|
success: '✓',
|
|
@@ -44,19 +118,19 @@ const UI_ICONS = {
|
|
|
44
118
|
};
|
|
45
119
|
|
|
46
120
|
// Diff rendering palette. Each change-type carries both a 256-color and a
|
|
47
|
-
// truecolor background so the renderer
|
|
48
|
-
//
|
|
121
|
+
// truecolor background so the renderer picks based on COLORTERM at start.
|
|
122
|
+
// Foregrounds saturated (Phase 2.5); gutters lifted one step so structure shows.
|
|
49
123
|
const DIFF_THEME = {
|
|
50
124
|
added: {
|
|
51
|
-
bg256:
|
|
125
|
+
bg256: bg(22), // deep forest green
|
|
52
126
|
bgTC: bgRGB(14, 40, 23),
|
|
53
|
-
signFg:
|
|
127
|
+
signFg: fg(40), // vivid green
|
|
54
128
|
sign: '+',
|
|
55
129
|
},
|
|
56
130
|
removed: {
|
|
57
|
-
bg256:
|
|
131
|
+
bg256: bg(52), // deep burgundy
|
|
58
132
|
bgTC: bgRGB(50, 18, 20),
|
|
59
|
-
signFg:
|
|
133
|
+
signFg: fg(203), // red
|
|
60
134
|
sign: '-',
|
|
61
135
|
},
|
|
62
136
|
context: {
|
|
@@ -66,20 +140,16 @@ const DIFF_THEME = {
|
|
|
66
140
|
sign: ' ',
|
|
67
141
|
},
|
|
68
142
|
gutter: { bg256: '', bgTC: '' },
|
|
69
|
-
lineNumber:
|
|
143
|
+
lineNumber: fg(244), // lifted from 240 — visible gutter
|
|
70
144
|
code: '\x1b[39m', // terminal default fg
|
|
71
145
|
header: '\x1b[1;38;5;75m', // bold light blue
|
|
72
|
-
frame:
|
|
146
|
+
frame: fg(242), // lifted from 238 — visible structure
|
|
73
147
|
};
|
|
74
148
|
|
|
75
|
-
// Maps
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// Keys include BOTH the XML tag names (read_file, write_file, shell) and
|
|
80
|
-
// the internal action names emitted by the native-function mapper (read,
|
|
81
|
-
// write, exec). Normalization happens at lookup time so neither side needs
|
|
82
|
-
// to know about the other's naming.
|
|
149
|
+
// Maps a tool tag onto the short category shown before the operation in a tool
|
|
150
|
+
// line. Keys include BOTH XML tag names (read_file, shell) and native action
|
|
151
|
+
// names (read, exec) — normalized at lookup. `git_*` tags map to `git`; MCP
|
|
152
|
+
// tools (`mcp__server__tool`) resolve via a prefix rule in `categoryForTag`.
|
|
83
153
|
const TOOL_CATEGORIES = {
|
|
84
154
|
exec: 'shell', shell: 'shell', run: 'shell', run_command: 'shell', bash: 'shell',
|
|
85
155
|
read: 'file', write: 'file', append: 'file',
|
|
@@ -93,7 +163,83 @@ const TOOL_CATEGORIES = {
|
|
|
93
163
|
store_memory: 'memory', recall_memory: 'memory', list_memories: 'memory',
|
|
94
164
|
get_env: 'env', set_env: 'env',
|
|
95
165
|
system_info: 'system',
|
|
166
|
+
git_status: 'git', git_diff: 'git', git_log: 'git', git_add: 'git',
|
|
167
|
+
git_commit: 'git', git_branch: 'git', git_checkout: 'git', git_worktree: 'git',
|
|
96
168
|
debug: 'debug',
|
|
97
169
|
};
|
|
98
170
|
|
|
99
|
-
|
|
171
|
+
// The XML extractor and native mapper hand *action* names (read, exec) that
|
|
172
|
+
// differ from *tag* names (read_file, shell). Normalize so category lookups
|
|
173
|
+
// resolve regardless of which rail produced the call tuple.
|
|
174
|
+
const ACTION_TO_TAG = {
|
|
175
|
+
read: 'read_file',
|
|
176
|
+
write: 'write_file',
|
|
177
|
+
append: 'append_file',
|
|
178
|
+
exec: 'shell',
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
function _normalizeTag(tag) {
|
|
182
|
+
return ACTION_TO_TAG[tag] || tag || 'tool';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Single source of truth for tag → category. Both `tool-operation.js` (the
|
|
186
|
+
// descriptor) and `format.js` (the rendered label) consume this so the
|
|
187
|
+
// descriptor's category and the line's category can never diverge.
|
|
188
|
+
function categoryForTag(tag) {
|
|
189
|
+
const t = _normalizeTag(tag);
|
|
190
|
+
if (TOOL_CATEGORIES[t]) return TOOL_CATEGORIES[t];
|
|
191
|
+
if (t.startsWith('git_')) return 'git';
|
|
192
|
+
if (t.startsWith('mcp__') || t.startsWith('mcp_')) return 'mcp';
|
|
193
|
+
return 'tool';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Normalize an incoming status to the resolver vocabulary. Accepts both the
|
|
197
|
+
// runtime forms ('pending'/'success'/'failure') and the descriptor forms
|
|
198
|
+
// ('pending'/'running'/'ok'/'error').
|
|
199
|
+
function _normStatus(s) {
|
|
200
|
+
if (s === 'pending' || s === 'running') return s;
|
|
201
|
+
if (s === 'error' || s === 'failure') return 'error';
|
|
202
|
+
return 'ok';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// THE colour resolver. Maps a descriptor's {category, status} to the colours
|
|
206
|
+
// for its glyph, category label, operation text, duration and meta. This is the
|
|
207
|
+
// single seam every chrome line goes through — `formatToolLine`,
|
|
208
|
+
// `formatWebSummaryLine`, etc. consume it instead of re-deriving colour inline,
|
|
209
|
+
// which is what makes the palette a one-table change. Honours NO_COLOR/non-TTY
|
|
210
|
+
// via `colorize` — when colour is off, every field resolves to ''.
|
|
211
|
+
function resolveLineColors(category, status) {
|
|
212
|
+
const cat = category || 'tool';
|
|
213
|
+
const catColor = colorize(UI_THEME.categories[cat] || UI_THEME.categories.tool);
|
|
214
|
+
const st = _normStatus(status);
|
|
215
|
+
|
|
216
|
+
let glyph;
|
|
217
|
+
if (st === 'error') glyph = colorize(STATUS_COLORS.error);
|
|
218
|
+
else if (st === 'ok') glyph = colorize(STATUS_COLORS.ok);
|
|
219
|
+
else {
|
|
220
|
+
// pending / running — category-tinted for vivid categories, cyan otherwise.
|
|
221
|
+
// NEVER gray: the `tool`/`debug` fallbacks would tint gray, so use cyan.
|
|
222
|
+
glyph = (cat !== 'tool' && cat !== 'debug')
|
|
223
|
+
? catColor
|
|
224
|
+
: colorize(STATUS_COLORS.running);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const tail = colorize(st === 'error' ? UI_THEME.error : UI_THEME.subtle);
|
|
228
|
+
return {
|
|
229
|
+
glyph, // status-keyed
|
|
230
|
+
label: catColor, // category-keyed (the 5-char label)
|
|
231
|
+
op: catColor, // category-keyed (the operation text — painted too)
|
|
232
|
+
dur: tail, // subtle, or error red on failure
|
|
233
|
+
meta: tail, // subtle, or error red on failure
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
// chrome palette + resolver
|
|
239
|
+
DIFF_THEME, UI_THEME, UI_ICONS, STATUS_COLORS, TOOL_CATEGORIES,
|
|
240
|
+
categoryForTag, resolveLineColors, colorEnabled, colorize,
|
|
241
|
+
// legacy palette (re-exported by ansi.js for back-compat)
|
|
242
|
+
THEME,
|
|
243
|
+
FG_GRAY, FG_DARK, FG_BLUE, FG_CYAN, FG_GREEN, FG_YELLOW, FG_RED, FG_TEAL,
|
|
244
|
+
FG_CODE_BG, BG_SELECTED, FG_CODE_BORDER, FG_CODE_LANG, FG_TAG, FG_FILEPATH,
|
|
245
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ToolOperation descriptor (Output Refactor — Phase 1).
|
|
4
|
+
//
|
|
5
|
+
// The single, immutable description of one tool invocation's display state,
|
|
6
|
+
// produced ONCE from the normalized `[action, ...opts]` tuple the agent loop
|
|
7
|
+
// already computes (so it covers the XML and native rails identically — the
|
|
8
|
+
// tuple is their convergence point). It is the display analogue of the
|
|
9
|
+
// `boundToolOutput` token chokepoint: every chrome line for a tool call is
|
|
10
|
+
// rendered from this one descriptor by `render-operation.js`, instead of each
|
|
11
|
+
// call site assembling its own formatToolLine arguments.
|
|
12
|
+
//
|
|
13
|
+
// Phase 1 is a pure RE-ROUTING — the descriptor carries exactly the data the
|
|
14
|
+
// existing formatters consume, so the renderer can reproduce today's output
|
|
15
|
+
// byte-for-byte. Later phases collapse duplicates and fold in web/MCP/replay.
|
|
16
|
+
//
|
|
17
|
+
// Shape (all fields are plain data — no ANSI, no IO):
|
|
18
|
+
// {
|
|
19
|
+
// id invocation id (writer activity-region key), or null
|
|
20
|
+
// category short tag for the line ("file", "cmd", "net", …) — display only
|
|
21
|
+
// glyph indicator glyph for the status ('●' | '✓' | '✗')
|
|
22
|
+
// tag the ORIGINAL tool tag (formatToolLine normalizes it itself)
|
|
23
|
+
// target the thing acted on — command / path / url / arg, stated once
|
|
24
|
+
// attrs parsed tuple options (the operation detail source)
|
|
25
|
+
// status 'pending' | 'running' | 'ok' | 'error'
|
|
26
|
+
// durationMs elapsed/total ms, or null
|
|
27
|
+
// meta category-specific tail (exit_code, bytes, count, status_code)
|
|
28
|
+
// error error shape ({ message, code }), or null
|
|
29
|
+
// noDuration suppress the duration segment (blocking tools, e.g. ask_user)
|
|
30
|
+
// detail optional body, COLLAPSED per the Phase 5 detail policy:
|
|
31
|
+
// { kind: 'diff', payload: { before, after, path } }
|
|
32
|
+
// — file edits; rendered EXPANDED to diff_max_lines.
|
|
33
|
+
// { kind: 'output', payload: { body, category } }
|
|
34
|
+
// — shell/MCP/subagent SUCCESS output; rendered as a
|
|
35
|
+
// shell_preview_lines preview + `… N more lines`.
|
|
36
|
+
// or null. Errors carry NO detail here — the error body renders
|
|
37
|
+
// EXPANDED on the existing chat-history error path (unchanged).
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
const { UI_ICONS, categoryForTag } = require('./theme');
|
|
41
|
+
const { extractDisplayBody } = require('./format');
|
|
42
|
+
|
|
43
|
+
// Category resolution (incl. action→tag normalization and the git_*/mcp__
|
|
44
|
+
// prefix rules) lives in theme.js so the descriptor's `category` and the
|
|
45
|
+
// rendered line's category can never diverge.
|
|
46
|
+
function _categoryFor(tag) {
|
|
47
|
+
return categoryForTag(tag);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Normalize an incoming status (runtime forms 'success'/'failure' OR descriptor
|
|
51
|
+
// forms 'ok'/'error') to the descriptor vocabulary. `error` present forces error.
|
|
52
|
+
function _normalizeStatus(status, error) {
|
|
53
|
+
if (status === 'pending') return 'pending';
|
|
54
|
+
if (status === 'running') return 'running';
|
|
55
|
+
if (status === 'error' || status === 'failure' || error) return 'error';
|
|
56
|
+
return 'ok';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _glyphFor(status) {
|
|
60
|
+
if (status === 'pending' || status === 'running') return UI_ICONS.pending;
|
|
61
|
+
if (status === 'error') return UI_ICONS.error;
|
|
62
|
+
return UI_ICONS.success;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Build the detail body per the Phase 5 policy. Order matters: errors carry no
|
|
66
|
+
// preview detail (the error body renders expanded elsewhere); a file edit's diff
|
|
67
|
+
// always wins over an output preview; otherwise a shell/MCP/subagent success
|
|
68
|
+
// with non-empty output gets an output-preview detail.
|
|
69
|
+
function _detailFor(spec, category, status) {
|
|
70
|
+
if (status === 'error') return null;
|
|
71
|
+
const { diff } = spec;
|
|
72
|
+
if (diff && typeof diff.before === 'string' && typeof diff.after === 'string') {
|
|
73
|
+
return { kind: 'diff', payload: { before: diff.before, after: diff.after, path: diff.path || '' } };
|
|
74
|
+
}
|
|
75
|
+
const previewable = category === 'shell' || category === 'mcp' || spec.tag === 'spawn_agent';
|
|
76
|
+
if (previewable && typeof spec.output === 'string') {
|
|
77
|
+
const body = extractDisplayBody(spec.output);
|
|
78
|
+
if (body && body.trim()) return { kind: 'output', payload: { body, category } };
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Produce the immutable descriptor. `spec` mirrors the data already available
|
|
84
|
+
// at the onToolStart/onToolEnd callbacks:
|
|
85
|
+
// { id, tag, arg, attrs, status, durationMs, meta, error, diff, noDuration }
|
|
86
|
+
// `tag`/`arg`/`attrs` are the parsed tuple (call[0], call[1], _attrsFromCall).
|
|
87
|
+
function buildToolOperation(spec) {
|
|
88
|
+
const s = spec || {};
|
|
89
|
+
const tag = s.tag || 'tool';
|
|
90
|
+
const status = _normalizeStatus(s.status, s.error);
|
|
91
|
+
const category = _categoryFor(tag);
|
|
92
|
+
const descriptor = {
|
|
93
|
+
id: s.id != null ? s.id : null,
|
|
94
|
+
category,
|
|
95
|
+
glyph: _glyphFor(status),
|
|
96
|
+
tag,
|
|
97
|
+
target: s.arg != null ? s.arg : '',
|
|
98
|
+
attrs: s.attrs || null,
|
|
99
|
+
status,
|
|
100
|
+
durationMs: Number.isFinite(s.durationMs) ? s.durationMs : null,
|
|
101
|
+
meta: s.meta || null,
|
|
102
|
+
error: s.error || null,
|
|
103
|
+
noDuration: !!s.noDuration,
|
|
104
|
+
detail: _detailFor(s, category, status),
|
|
105
|
+
};
|
|
106
|
+
return Object.freeze(descriptor);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Serialization for replay (Output Refactor — Phase 6) ─────────────────────
|
|
110
|
+
//
|
|
111
|
+
// A built descriptor is transient: it exists only for the duration of one live
|
|
112
|
+
// tool turn. To replay a saved session (/history, /chats, --resume) with the
|
|
113
|
+
// SAME fidelity a fresh turn shows — real diff, real duration, real status —
|
|
114
|
+
// the *terminal-state* core of the descriptor is persisted next to the tool
|
|
115
|
+
// result message (as a sibling `_display` key, see agent.js) and rebuilt here
|
|
116
|
+
// on load. Both helpers are pure data transforms (no ANSI, no IO).
|
|
117
|
+
//
|
|
118
|
+
// `serializeOperation(op)` extracts the plain-data core `renderOperation` reads
|
|
119
|
+
// for a COMPLETED (non-animating) op. The two live/animation-only fields are
|
|
120
|
+
// dropped:
|
|
121
|
+
// - id — the live activity-region key, meaningless once the turn ended.
|
|
122
|
+
// - glyph — recomputed from `status` by the renderer (formatToolLine).
|
|
123
|
+
// Everything the renderer DOES read for phase 'result' / 'detail' is kept:
|
|
124
|
+
// tag, target, attrs (the operation text is derived from attrs by _operation()
|
|
125
|
+
// in format.js — NOT in §1's field list, added here because the result line is
|
|
126
|
+
// not byte-identical without it), category, status, durationMs, meta, error,
|
|
127
|
+
// noDuration (suppresses the duration segment for blocking tools — also beyond
|
|
128
|
+
// §1's list, kept for the same byte-identity reason), and the collapsed detail.
|
|
129
|
+
const DISPLAY_FORMAT_VERSION = 1;
|
|
130
|
+
|
|
131
|
+
// `operationCore(op)` is the SINGLE descriptor→plain-data mapping shared by
|
|
132
|
+
// `serializeOperation` (persistence/replay) and `renderOperation`'s json/event
|
|
133
|
+
// modes (embeddable output — Phase 6d). It emits the descriptor's own vocabulary
|
|
134
|
+
// (tag/target/attrs/category/status/durationMs/meta/error/noDuration/detail) and
|
|
135
|
+
// nothing else — no `v` version tag (that is a persistence concern added by
|
|
136
|
+
// serializeOperation), no ANSI, no IO, no live/animation fields (id, glyph). The
|
|
137
|
+
// two live-only fields are dropped exactly as serializeOperation always dropped
|
|
138
|
+
// them: `id` (the activity-region key) and `glyph` (recomputed from `status`).
|
|
139
|
+
// Returns null for a non-object op so callers degrade gracefully.
|
|
140
|
+
function operationCore(op) {
|
|
141
|
+
if (!op || typeof op !== 'object') return null;
|
|
142
|
+
return {
|
|
143
|
+
tag: op.tag,
|
|
144
|
+
target: op.target,
|
|
145
|
+
attrs: op.attrs || null,
|
|
146
|
+
category: op.category,
|
|
147
|
+
status: op.status,
|
|
148
|
+
durationMs: Number.isFinite(op.durationMs) ? op.durationMs : null,
|
|
149
|
+
meta: op.meta || null,
|
|
150
|
+
error: op.error || null,
|
|
151
|
+
noDuration: !!op.noDuration,
|
|
152
|
+
detail: op.detail || null,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function serializeOperation(op) {
|
|
157
|
+
const core = operationCore(op);
|
|
158
|
+
if (!core) return null;
|
|
159
|
+
// `v` first, then the shared core — byte-identical key order to the pre-6d
|
|
160
|
+
// hand-rolled object (replay/persistence depend on this exact shape).
|
|
161
|
+
return { v: DISPLAY_FORMAT_VERSION, ...core };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Rebuild a renderable descriptor from a persisted core. Returns null when the
|
|
165
|
+
// core is absent OR its version is unknown (`v` missing / not 1) — the caller
|
|
166
|
+
// then falls back to the legacy summarizeToolResult path, so old sessions and
|
|
167
|
+
// any future format degrade gracefully instead of crashing. The returned object
|
|
168
|
+
// mirrors buildToolOperation's output shape so renderOperation treats it
|
|
169
|
+
// identically to a fresh descriptor (it is not frozen — replay never mutates it).
|
|
170
|
+
function descriptorFromStored(core) {
|
|
171
|
+
if (!core || typeof core !== 'object') return null;
|
|
172
|
+
if (core.v !== DISPLAY_FORMAT_VERSION) return null;
|
|
173
|
+
const status = core.status || 'ok';
|
|
174
|
+
return {
|
|
175
|
+
id: null,
|
|
176
|
+
category: core.category,
|
|
177
|
+
glyph: _glyphFor(status),
|
|
178
|
+
tag: core.tag || 'tool',
|
|
179
|
+
target: core.target != null ? core.target : '',
|
|
180
|
+
attrs: core.attrs || null,
|
|
181
|
+
status,
|
|
182
|
+
durationMs: Number.isFinite(core.durationMs) ? core.durationMs : null,
|
|
183
|
+
meta: core.meta || null,
|
|
184
|
+
error: core.error || null,
|
|
185
|
+
noDuration: !!core.noDuration,
|
|
186
|
+
detail: core.detail || null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { buildToolOperation, operationCore, serializeOperation, descriptorFromStored };
|
package/lib/ui/utils.js
CHANGED
|
@@ -45,13 +45,17 @@ function termWidth(str) {
|
|
|
45
45
|
// Walks the string once; CSI escape sequences (`\x1b[…final`) are copied
|
|
46
46
|
// through verbatim without consuming any visible width. Visible-width math
|
|
47
47
|
// uses _cpWidth, so a 2-col CJK glyph or emoji counts as 2 and combining
|
|
48
|
-
// marks count as 0.
|
|
49
|
-
//
|
|
50
|
-
//
|
|
48
|
+
// marks count as 0. Terminates with `\x1b[0m` ONLY when the (possibly
|
|
49
|
+
// truncated) output actually contains an escape, so a cut that left an SGR
|
|
50
|
+
// span open is closed without leaving the terminal colored at the cut point.
|
|
51
|
+
// When the output is escape-free we append nothing: a trailing reset on
|
|
52
|
+
// escape-free text is a stray escape that leaks through NO_COLOR. The gate is
|
|
53
|
+
// content-driven (out.indexOf('\x1b')), not flag-driven, so a preview body
|
|
54
|
+
// line carrying a child process's own ANSI is still closed defensively.
|
|
51
55
|
function truncateVisible(str, maxCols) {
|
|
52
56
|
if (!str) return '';
|
|
53
57
|
const max = Math.max(0, maxCols | 0);
|
|
54
|
-
if (max === 0) return '
|
|
58
|
+
if (max === 0) return '';
|
|
55
59
|
const s = String(str);
|
|
56
60
|
const len = s.length;
|
|
57
61
|
let out = '';
|
|
@@ -82,7 +86,7 @@ function truncateVisible(str, maxCols) {
|
|
|
82
86
|
cols += w;
|
|
83
87
|
i += clen;
|
|
84
88
|
}
|
|
85
|
-
return out + '\x1b[0m';
|
|
89
|
+
return out.indexOf('\x1b') !== -1 ? out + '\x1b[0m' : out;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
// Repeat a single-column glyph so the visible row is exactly `width` columns.
|