@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.
Files changed (192) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/.github/workflows/ci.yml +69 -0
  3. package/ARCHITECTURE.md +6 -95
  4. package/CLAUDE.md +196 -316
  5. package/README.md +148 -4
  6. package/docs/ARCHITECTURE.md +1321 -0
  7. package/docs/CONFIG.md +340 -0
  8. package/docs/HISTORY.md +245 -0
  9. package/examples/embed.js +74 -0
  10. package/index.js +251 -10
  11. package/lib/agent.js +856 -120
  12. package/lib/api.js +239 -50
  13. package/lib/args.js +74 -2
  14. package/lib/audit.js +23 -1
  15. package/lib/background.js +584 -0
  16. package/lib/checkpoints.js +757 -0
  17. package/lib/commands/auth.js +94 -0
  18. package/lib/commands/chat-session.js +489 -0
  19. package/lib/commands/chat-slash.js +415 -0
  20. package/lib/commands/chat-turn.js +669 -0
  21. package/lib/commands/chat.js +407 -0
  22. package/lib/commands/custom.js +157 -0
  23. package/lib/commands/history-utils.js +66 -0
  24. package/lib/commands/index.js +268 -0
  25. package/lib/commands/mcp.js +113 -0
  26. package/lib/commands/oneshot.js +193 -0
  27. package/lib/commands/registry.js +269 -0
  28. package/lib/commands/tasks.js +89 -0
  29. package/lib/compact.js +87 -0
  30. package/lib/config.js +360 -11
  31. package/lib/constants.js +401 -3
  32. package/lib/deny.js +199 -0
  33. package/lib/doctor.js +160 -0
  34. package/lib/headless.js +202 -0
  35. package/lib/hooks.js +286 -0
  36. package/lib/images.js +270 -0
  37. package/lib/internals.js +49 -0
  38. package/lib/mcp/boundary.js +131 -0
  39. package/lib/mcp/client.js +270 -0
  40. package/lib/mcp/oauth.js +134 -0
  41. package/lib/memory.js +209 -0
  42. package/lib/metrics.js +37 -2
  43. package/lib/payload.js +54 -0
  44. package/lib/permission-rules.js +401 -0
  45. package/lib/permissions.js +123 -26
  46. package/lib/pricing.js +67 -0
  47. package/lib/proc.js +62 -0
  48. package/lib/prompts.js +99 -8
  49. package/lib/sandbox.js +568 -0
  50. package/lib/sdk.js +328 -0
  51. package/lib/secrets.js +211 -0
  52. package/lib/skills.js +223 -0
  53. package/lib/subagents.js +516 -0
  54. package/lib/tool_registry.js +2862 -0
  55. package/lib/tool_specs.js +263 -9
  56. package/lib/tools.js +352 -1039
  57. package/lib/ui/anim.js +86 -0
  58. package/lib/ui/ansi.js +17 -27
  59. package/lib/ui/chat-history.js +253 -71
  60. package/lib/ui/create-ui.js +67 -24
  61. package/lib/ui/diff.js +90 -25
  62. package/lib/ui/file-activity.js +236 -0
  63. package/lib/ui/format.js +195 -29
  64. package/lib/ui/input-field.js +21 -11
  65. package/lib/ui/md-stream.js +234 -0
  66. package/lib/ui/render-operation.js +113 -0
  67. package/lib/ui/select.js +1 -4
  68. package/lib/ui/status-bar.js +146 -36
  69. package/lib/ui/stream.js +20 -13
  70. package/lib/ui/theme.js +190 -44
  71. package/lib/ui/tool-operation.js +190 -0
  72. package/lib/ui/utils.js +9 -5
  73. package/lib/ui/web-activity.js +270 -0
  74. package/lib/ui/writer.js +159 -45
  75. package/lib/ui.js +1 -1
  76. package/lib/verify.js +229 -0
  77. package/lib/web-extract.js +213 -0
  78. package/lib/web-summarize.js +68 -0
  79. package/package.json +19 -4
  80. package/scripts/lint.js +57 -0
  81. package/test/agent-loop.test.js +389 -0
  82. package/test/anim-driver.test.js +153 -0
  83. package/test/ask-user-display.test.js +226 -0
  84. package/test/ask-user-gate.test.js +231 -0
  85. package/test/background.test.js +414 -0
  86. package/test/chat-history-nocolor.test.js +155 -0
  87. package/test/chat-relogin.test.js +207 -0
  88. package/test/chat.test.js +114 -0
  89. package/test/checkpoints-agent.test.js +181 -0
  90. package/test/checkpoints.test.js +650 -0
  91. package/test/command-registry.test.js +160 -0
  92. package/test/compact.test.js +116 -0
  93. package/test/completion-lazy.test.js +52 -0
  94. package/test/config-merge.test.js +324 -0
  95. package/test/config-quarantine.test.js +128 -0
  96. package/test/config-write-guard-allow-anywhere.test.js +56 -0
  97. package/test/config-write-guard-skip.test.js +46 -0
  98. package/test/config-write-guard.test.js +153 -0
  99. package/test/context-split.test.js +215 -0
  100. package/test/cost-doctor.test.js +142 -0
  101. package/test/custom-commands-chat.test.js +106 -0
  102. package/test/custom-commands.test.js +230 -0
  103. package/test/defer-detail-band.test.js +403 -0
  104. package/test/deny-windows.test.js +120 -0
  105. package/test/deny.test.js +83 -0
  106. package/test/detail-band-tab-flatten.test.js +242 -0
  107. package/test/download-allow-anywhere.test.js +66 -0
  108. package/test/download-confine.test.js +153 -0
  109. package/test/exec-diff.test.js +268 -0
  110. package/test/executors.test.js +599 -0
  111. package/test/extract-tool-calls.test.js +349 -0
  112. package/test/fetch-url-validation.test.js +219 -0
  113. package/test/file-activity.test.js +522 -0
  114. package/test/fixtures/tool-calls.js +57 -0
  115. package/test/fixtures/web-page.js +91 -0
  116. package/test/git-tools.test.js +384 -0
  117. package/test/grep-glob-serialize.test.js +242 -0
  118. package/test/grep-glob.test.js +268 -0
  119. package/test/grep-path-target.test.js +227 -0
  120. package/test/harness/README.md +57 -0
  121. package/test/harness/chat-harness.js +143 -0
  122. package/test/harness/memwarn-headless-child.js +65 -0
  123. package/test/harness/mock-llm.js +120 -0
  124. package/test/harness/mock-mcp-server.js +142 -0
  125. package/test/harness/sse-server.js +69 -0
  126. package/test/headless.test.js +348 -0
  127. package/test/history-utils.test.js +88 -0
  128. package/test/hooks-agent.test.js +238 -0
  129. package/test/hooks-verify-sandbox.test.js +232 -0
  130. package/test/hooks.test.js +216 -0
  131. package/test/http-get-user-agent.test.js +142 -0
  132. package/test/images-api.test.js +208 -0
  133. package/test/images.test.js +238 -0
  134. package/test/input-field-ctrl-o.test.js +37 -0
  135. package/test/live-height-physical.test.js +281 -0
  136. package/test/max-iterations.test.js +218 -0
  137. package/test/mcp-boundary.test.js +57 -0
  138. package/test/mcp-client.test.js +267 -0
  139. package/test/mcp-oauth.test.js +86 -0
  140. package/test/md-stream.test.js +183 -0
  141. package/test/memory-truncation-warning.test.js +222 -0
  142. package/test/memory.test.js +198 -0
  143. package/test/native-dispatch.test.js +409 -0
  144. package/test/native-live-narration.test.js +254 -0
  145. package/test/output-chokepoint.test.js +188 -0
  146. package/test/output-heredoc-leak.test.js +195 -0
  147. package/test/output-preview.test.js +245 -0
  148. package/test/path-guards.test.js +134 -0
  149. package/test/payload.test.js +99 -0
  150. package/test/permission-rules-agent.test.js +210 -0
  151. package/test/permission-rules.test.js +297 -0
  152. package/test/permissions.test.js +362 -0
  153. package/test/plan-mode.test.js +167 -0
  154. package/test/read-paginate.test.js +275 -0
  155. package/test/readonly-tools.test.js +177 -0
  156. package/test/render-operation.test.js +317 -0
  157. package/test/replay-descriptor-xml.test.js +216 -0
  158. package/test/replay-descriptor.test.js +189 -0
  159. package/test/replay-web-aggregate.test.js +291 -0
  160. package/test/replay-web-persist.test.js +241 -0
  161. package/test/result-cap.test.js +233 -0
  162. package/test/running-glyph-anim.test.js +111 -0
  163. package/test/sandbox-agent.test.js +147 -0
  164. package/test/sandbox-integration.test.js +216 -0
  165. package/test/sandbox.test.js +408 -0
  166. package/test/sdk.test.js +234 -0
  167. package/test/shell-output-cap.test.js +181 -0
  168. package/test/skills-chat.test.js +110 -0
  169. package/test/skills.test.js +295 -0
  170. package/test/smoke.test.js +68 -0
  171. package/test/status-bar-driver.test.js +93 -0
  172. package/test/status-bar-pause.test.js +164 -0
  173. package/test/status-bar-resync.test.js +188 -0
  174. package/test/stream-parser.test.js +171 -0
  175. package/test/subagents-agent.test.js +178 -0
  176. package/test/subagents.test.js +222 -0
  177. package/test/theme-palette.test.js +166 -0
  178. package/test/tool-registry.test.js +85 -0
  179. package/test/trim-budget.test.js +101 -0
  180. package/test/truncate-visible.test.js +78 -0
  181. package/test/verify-agent.test.js +317 -0
  182. package/test/verify.test.js +141 -0
  183. package/test/view-image.test.js +199 -0
  184. package/test/web-activity-ordering.test.js +203 -0
  185. package/test/web-activity.test.js +207 -0
  186. package/test/web-data-extraction-guidance.test.js +71 -0
  187. package/test/web-extract.test.js +185 -0
  188. package/test/web-fetch-agent.test.js +291 -0
  189. package/test/web-fetch-mode.test.js +193 -0
  190. package/test/web-search.test.js +380 -0
  191. package/lib/commands.js +0 -1438
  192. package/path +0 -1
package/lib/ui/theme.js CHANGED
@@ -1,39 +1,113 @@
1
1
  'use strict';
2
2
 
3
- const { bg256, fg256, bgRGB, fgRGB } = require('./ansi');
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
- // Shared chrome palette. Used by tool-status lines, debug blocks, meta
6
- // messages, and any other non-content surface. Diff bodies have their own
7
- // background palette below. Keep palette choices out of renderers if a
8
- // colour is added to a status line, it should come from here.
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: fg256(114), // bright mint
11
- warning: fg256(179), // amber
12
- error: fg256(174), // salmon
13
- info: fg256(75), // light blue
14
- muted: fg256(240), // dim gray — frames, metadata
15
- subtle: fg256(244), // slightly less dim secondary text
16
- accent: fg256(141), // soft purple — tool names, IDs
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 textdurations, meta, tokens
77
+ accent: fg(141), // purple — model name, IDs
17
78
  default: '\x1b[39m', // terminal default fg
18
- // Per-category accent variations for tool-line chrome. Chosen from the
19
- // pastel/light bands (100–180 range) so concurrent pending lines read
20
- // as a coordinated palette rather than a rainbow. Keys mirror the
21
- // values produced by TOOL_CATEGORIES so a lookup is categories[cat].
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
- net: fg256(110), // dusty blue — http, download, upload
24
- file: fg256(151), // soft sage — file ops
25
- cmd: fg256(180), // warm tan — shell / exec
26
- user: fg256(217), // pale rose — ask_user
27
- memory: fg256(183), // light lavender — memory
28
- env: fg256(186), // pale gold — env vars
29
- system: fg256(109), // steel blue — system_info
30
- debug: fg256(244), // muted gray — debug blocks
31
- tool: fg256(141), // accent purple — fallback
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
- // Indicator glyphs for status lines. Separated from colour so the renderer
36
- // can swap either independently (e.g. an ASCII-only fallback later).
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 can pick based on COLORTERM at start.
48
- // Only this file names colors keep magic numbers out of diff.js.
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: bg256(22), // deep forest green
125
+ bg256: bg(22), // deep forest green
52
126
  bgTC: bgRGB(14, 40, 23),
53
- signFg: fg256(114), // bright mint
127
+ signFg: fg(40), // vivid green
54
128
  sign: '+',
55
129
  },
56
130
  removed: {
57
- bg256: bg256(52), // deep burgundy
131
+ bg256: bg(52), // deep burgundy
58
132
  bgTC: bgRGB(50, 18, 20),
59
- signFg: fg256(174), // bright salmon
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: fg256(240), // dim gray
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: fg256(238), // very dim gray
146
+ frame: fg(242), // lifted from 238 — visible structure
73
147
  };
74
148
 
75
- // Maps an agent-loop tool tag onto the short category shown before the
76
- // operation in a tool line ("file", "net", "shell"). Renderers should
77
- // fall back to the tag itself if the tag isn't listed here.
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
- module.exports = { DIFF_THEME, UI_THEME, UI_ICONS, TOOL_CATEGORIES };
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. Always terminates with `\x1b[0m` so the terminal isn't
49
- // left in a colored state at the truncation point (safe when no escape
50
- // preceded the cut a redundant reset is a no-op).
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 '\x1b[0m';
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.