@semalt-ai/code 1.19.0 → 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 (81) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/ARCHITECTURE.md +6 -95
  3. package/CLAUDE.md +196 -1874
  4. package/README.md +1 -1
  5. package/docs/ARCHITECTURE.md +1321 -0
  6. package/docs/CONFIG.md +340 -0
  7. package/docs/HISTORY.md +245 -0
  8. package/index.js +1 -1
  9. package/lib/agent.js +145 -16
  10. package/lib/api.js +28 -3
  11. package/lib/commands/chat-session.js +187 -4
  12. package/lib/commands/chat-slash.js +16 -0
  13. package/lib/commands/chat-turn.js +272 -49
  14. package/lib/commands/chat.js +12 -8
  15. package/lib/config.js +27 -0
  16. package/lib/constants.js +30 -1
  17. package/lib/headless.js +36 -1
  18. package/lib/images.js +8 -2
  19. package/lib/permissions.js +23 -16
  20. package/lib/prompts.js +15 -3
  21. package/lib/tool_registry.js +357 -53
  22. package/lib/tool_specs.js +42 -8
  23. package/lib/tools.js +80 -19
  24. package/lib/ui/anim.js +86 -0
  25. package/lib/ui/ansi.js +17 -27
  26. package/lib/ui/chat-history.js +253 -71
  27. package/lib/ui/create-ui.js +67 -24
  28. package/lib/ui/diff.js +90 -25
  29. package/lib/ui/file-activity.js +236 -0
  30. package/lib/ui/format.js +173 -28
  31. package/lib/ui/input-field.js +5 -4
  32. package/lib/ui/md-stream.js +234 -0
  33. package/lib/ui/render-operation.js +113 -0
  34. package/lib/ui/select.js +1 -4
  35. package/lib/ui/status-bar.js +99 -57
  36. package/lib/ui/stream.js +20 -13
  37. package/lib/ui/theme.js +190 -45
  38. package/lib/ui/tool-operation.js +190 -0
  39. package/lib/ui/utils.js +9 -5
  40. package/lib/ui/web-activity.js +58 -6
  41. package/lib/ui/writer.js +159 -45
  42. package/lib/ui.js +1 -1
  43. package/package.json +1 -1
  44. package/test/anim-driver.test.js +153 -0
  45. package/test/ask-user-display.test.js +226 -0
  46. package/test/ask-user-gate.test.js +231 -0
  47. package/test/chat-history-nocolor.test.js +155 -0
  48. package/test/chat-relogin.test.js +207 -0
  49. package/test/defer-detail-band.test.js +403 -0
  50. package/test/detail-band-tab-flatten.test.js +242 -0
  51. package/test/exec-diff.test.js +268 -0
  52. package/test/executors.test.js +250 -13
  53. package/test/extract-tool-calls.test.js +37 -3
  54. package/test/file-activity.test.js +522 -0
  55. package/test/grep-path-target.test.js +227 -0
  56. package/test/harness/chat-harness.js +2 -1
  57. package/test/headless.test.js +146 -1
  58. package/test/input-field-ctrl-o.test.js +37 -0
  59. package/test/live-height-physical.test.js +281 -0
  60. package/test/max-iterations.test.js +9 -7
  61. package/test/md-stream.test.js +183 -0
  62. package/test/native-dispatch.test.js +53 -0
  63. package/test/native-live-narration.test.js +254 -0
  64. package/test/output-heredoc-leak.test.js +195 -0
  65. package/test/output-preview.test.js +245 -0
  66. package/test/permissions.test.js +199 -0
  67. package/test/read-paginate.test.js +1 -1
  68. package/test/render-operation.test.js +317 -0
  69. package/test/replay-descriptor-xml.test.js +216 -0
  70. package/test/replay-descriptor.test.js +189 -0
  71. package/test/replay-web-aggregate.test.js +291 -0
  72. package/test/replay-web-persist.test.js +241 -0
  73. package/test/running-glyph-anim.test.js +111 -0
  74. package/test/status-bar-driver.test.js +93 -0
  75. package/test/status-bar-resync.test.js +188 -0
  76. package/test/stream-parser.test.js +24 -0
  77. package/test/theme-palette.test.js +166 -0
  78. package/test/truncate-visible.test.js +78 -0
  79. package/test/view-image.test.js +199 -0
  80. package/test/web-activity-ordering.test.js +12 -3
  81. package/path +0 -1
package/lib/headless.js CHANGED
@@ -26,6 +26,8 @@
26
26
  const { setUIActive, isUIActive } = require('./tools');
27
27
  const { priceForModel, computeCost } = require('./pricing');
28
28
  const { DEFAULT_MAX_ITERATIONS } = require('./constants');
29
+ const { buildToolOperation } = require('./ui/tool-operation');
30
+ const { renderOperation } = require('./ui/render-operation');
29
31
 
30
32
  const MACHINE_MODES = new Set(['json', 'stream-json']);
31
33
 
@@ -89,7 +91,40 @@ function createHeadlessSink(mode, emitLine, { model = null, priceOverrides = nul
89
91
  const call = meta && Array.isArray(meta.call) ? meta.call : null;
90
92
  const args = call ? call.slice(1) : [];
91
93
  const ok = !(meta && meta.error);
92
- const rec = { tool: tag, args, ok, ms };
94
+ // Legacy per-tool fields computed EXACTLY as before so their names,
95
+ // types, and values can never drift (the contract pin).
96
+ const legacy = { tool: tag, args, ok, ms };
97
+ // Phase 6d-ii — sink-local descriptor build (option A): build the same
98
+ // ToolOperation the interactive sink builds (chat-turn.js) from the `meta`
99
+ // already passed, then merge its json-mode core (descriptor-native plain
100
+ // data: status/category/durationMs/detail/meta/target/attrs/…) ADDITIVELY
101
+ // BENEATH the legacy fields. `legacy` spreads last so tool/args/ok/ms win
102
+ // on any name clash → byte-identical to pre-6d-ii. Web ops are ordinary
103
+ // tools here (NO web-activity collapse — N per-op events is the contract).
104
+ let core = null;
105
+ try {
106
+ const attrs = meta ? meta.attrs : null;
107
+ const operation = buildToolOperation({
108
+ id: meta ? meta.id : null,
109
+ tag,
110
+ arg: attrs ? (attrs.command || attrs.path || attrs.url || attrs.src || attrs.key || attrs.name || attrs.pattern) : '',
111
+ attrs,
112
+ status: ok ? 'ok' : 'error',
113
+ durationMs: ms,
114
+ meta: meta ? meta.meta : null,
115
+ error: meta ? meta.error : null,
116
+ diff: meta ? meta.diff : null,
117
+ // Model-facing result → lets the descriptor derive an output-preview
118
+ // detail (shell/MCP/subagent). Chrome-only; context is untouched.
119
+ output: typeof resultStr === 'string' ? resultStr : null,
120
+ noDuration: tag === 'ask_user',
121
+ });
122
+ core = renderOperation(operation, { mode: 'json' });
123
+ } catch (_e) {
124
+ // No-descriptor safety: fall back to the bare legacy-only rec, never crash.
125
+ core = null;
126
+ }
127
+ const rec = core ? { ...core, ...legacy } : { ...legacy };
93
128
  toolCalls.push(rec);
94
129
  if (mode === 'stream-json') emitLine({ type: 'tool', ...rec });
95
130
  };
package/lib/images.js CHANGED
@@ -162,8 +162,14 @@ function selectImageFormat(config = {}, model = '') {
162
162
  // to these can never work, so we fail loud rather than send a doomed payload.
163
163
  const KNOWN_TEXT_ONLY = /(?:^|[-/_])(?:text-embedding|embedding|embed|whisper|tts|moderation|rerank|reranker)/i;
164
164
  // Well-known vision-capable families: a positive signal so an attach proceeds
165
- // without needing per-profile config.
166
- const KNOWN_VISION = /(gpt-4o|gpt-4\.1|gpt-4-vision|gpt-4-turbo|claude-3|claude-opus|claude-sonnet|claude-haiku|claude-fable|claude-4|gemini|llava|qwen[\d.]*-?vl|pixtral|llama[-\d.]*(?:-)?vision|internvl|minicpm-v|-vl\b|vision|multimodal)/i;
165
+ // without needing per-profile config. `minimax` is here because a live probe
166
+ // confirmed MiniMax-M3 accepts OpenAI image_url/data-URI vision input — so the
167
+ // attach proceeds (true) rather than relying on a speculative endpoint round-trip
168
+ // (null). This is the family-signal mechanism (like gpt-4o / claude-3 / gemini);
169
+ // per-profile `vision:true` remains for private/local profiles. NOTE: the qwen
170
+ // entry is deliberately narrow (`qwen…-vl` only) — plain Qwen coder models are
171
+ // NOT confirmed vision-capable and must stay null.
172
+ const KNOWN_VISION = /(gpt-4o|gpt-4\.1|gpt-4-vision|gpt-4-turbo|claude-3|claude-opus|claude-sonnet|claude-haiku|claude-fable|claude-4|gemini|llava|qwen[\d.]*-?vl|pixtral|llama[-\d.]*(?:-)?vision|internvl|minicpm-v|minimax|-vl\b|vision|multimodal)/i;
167
173
 
168
174
  // Determine vision capability from config/model metadata where available.
169
175
  // true — accept the image
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const writer = require('./ui/writer');
4
- const messages = require('./ui/messages');
4
+ const dbg = require('./debug');
5
5
  const { resolvePermission, normalizeCall } = require('./permission-rules');
6
6
 
7
7
  const TIER_FS = ['read_file', 'write_file', 'append_file', 'delete_file', 'list_dir', 'make_dir', 'move_file', 'copy_file', 'file_stat', 'search_files', 'store_memory', 'recall_memory'];
@@ -81,10 +81,10 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false, skip
81
81
  // The picker renders into the writer's modal region — a live band above
82
82
  // the status bar that redraws in place on every keystroke. Arrow-key
83
83
  // navigation rebuilds the lines array and calls onShowModal again; nothing
84
- // lands in scrollback until the user confirms. On resolve/cancel the
85
- // modal is cleared and a single summary line is emitted to scrollback
86
- // (for multi-line descriptions e.g. a file-write diff the full body
87
- // is retained so the user can still see what was approved).
84
+ // lands in scrollback until the user confirms. On resolve/cancel the modal
85
+ // is simply cleared NO summary line is emitted (Output Refactor Phase 2,
86
+ // D1): the execution result line is the single post-approval confirmation,
87
+ // so an echo here would just duplicate it.
88
88
  function requestPermission(description, onShowModal, onCloseModal, onCaptureNavigation) {
89
89
  // Serialize dialogs: each permission waits for the previous one to be answered
90
90
  const myTurn = _permissionQueueTail;
@@ -124,12 +124,13 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false, skip
124
124
 
125
125
  function finish(result) {
126
126
  const chosen = result === 'cancel' ? 'no' : options[selectedIdx].toLowerCase();
127
- const glyph = (chosen === 'no') ? '✗' : '✓';
128
- // The full `description` is preserved in the summary so multi-line
129
- // bodies (e.g. file-write diffs) remain visible in scrollback after
130
- // the modal closes. chatHistory's system-message renderer styles the
131
- // first line by the leading glyph and indents continuations.
132
- onCloseModal(`${glyph} ${description}`);
127
+ // Output Refactor Phase 2 (D1): close the modal WITHOUT emitting a
128
+ // post-close summary line. The execution result line (the descriptor's
129
+ // `result` phase via renderOperation) is the single confirmation of the
130
+ // operation, so a `✓ shell: ls` / `✓ file: Edit line N` echo here just
131
+ // duplicated it. Manual-approve now produces the same post-execution
132
+ // output as auto-approve: only the result line.
133
+ onCloseModal();
133
134
  releaseQueue();
134
135
  resolve(chosen);
135
136
  }
@@ -152,12 +153,18 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false, skip
152
153
  }));
153
154
  }
154
155
 
156
+ // Per-auto-approved-command breadcrumb. By DEFAULT this writes nothing to
157
+ // scrollback/UI: the per-command result line (command + outcome + duration,
158
+ // emitted for every tool) and the one-time "Auto-approve enabled for `tag`"
159
+ // grant line already cover it — a per-command `✓ Auto-approved: <cmd>` line
160
+ // just duplicates the command and reads as auto-approve chatter on every call.
161
+ // The diagnostic detail (incl. the `[rule: …]` / `[--dangerously-skip-...]`
162
+ // context the call sites embed in `description`) is preserved under
163
+ // --debug, routed through dbg.log — which is silent in 'off' mode (the
164
+ // default), scrollback in --debug, and the file in --debug-file. The audit
165
+ // log records every tool call independently, so nothing is lost regardless.
155
166
  function _emitAutoApproved(description) {
156
- if (uiCallbacks) {
157
- uiCallbacks.onAddMessage({ role: 'system', content: `✓ Auto-approved: ${description}` });
158
- } else {
159
- messages.sysSuccess(`Auto-approved: ${description}`);
160
- }
167
+ dbg.log(`[permission] auto-approved: ${description}`);
161
168
  }
162
169
 
163
170
  async function askPermission(actionType, description, tag, ruleVerdict = null) {
package/lib/prompts.js CHANGED
@@ -20,6 +20,7 @@ const TOOL_TAG_SPECS = {
20
20
  exec: { attrs: [], purpose: 'Run a shell command (inline content).' },
21
21
  shell: { attrs: [], purpose: 'Run a shell command (inline content).' },
22
22
  read_file: { attrs: ['path?', 'start_line?', 'end_line?', 'show_line_numbers?'], purpose: 'Read a file, paginated (~2000 lines); start_line/end_line for a slice, show_line_numbers for edit refs.' },
23
+ view_image: { attrs: ['path?'], purpose: 'Load a LOCAL image (PNG/JPEG/GIF/WebP) into YOUR OWN vision context to analyze it (inline content or path attr = image path). Makes it visible to you, the model — NOT to the user. For a URL image, download it first, then view_image the saved path.' },
23
24
  write_file: { attrs: ['path'], purpose: 'Write file with inline content (overwrites).' },
24
25
  create_file: { attrs: ['path'], purpose: 'Create file with inline content.' },
25
26
  append_file: { attrs: ['path'], purpose: 'Append inline content to file.' },
@@ -30,19 +31,19 @@ const TOOL_TAG_SPECS = {
30
31
  move_file: { attrs: ['src', 'dst'], purpose: 'Move or rename a file.' },
31
32
  copy_file: { attrs: ['src', 'dst'], purpose: 'Copy a file.' },
32
33
  file_stat: { attrs: [], purpose: 'Stat a file (inline content = path).' },
33
- edit_file: { attrs: ['path', 'line'], purpose: 'Replace a single line in a file (inline content = new line).' },
34
+ edit_file: { attrs: ['path', 'line', 'end_line?'], purpose: 'Replace a single line in a file (inline content = new line). Add end_line to replace the whole line range line..end_line with the inline content (a regex-free block edit; pairs with a numbered read_file slice).' },
34
35
  search_files: { attrs: ['pattern?', 'dir?'], purpose: 'Find files by glob pattern.' },
35
36
  grep: { attrs: ['pattern', 'path?', 'ignore_case?', 'output_mode?', 'head_limit?', 'offset?'], purpose: 'Regex search file contents; returns file:line:text so you can read just the matching slice. output_mode="content" (default file:line:text), "files_with_matches" (paths only), or "count" (how many). Bounded by head_limit (default 100) with a truncation notice. Honors .gitignore, skips binaries and node_modules.' },
36
37
  glob: { attrs: ['pattern', 'path?', 'head_limit?', 'offset?'], purpose: 'List files matching a glob (relative paths), bounded by head_limit (default 100) with a truncation notice.' },
37
38
  search_in_file: { attrs: ['path'], purpose: 'Regex search inside a file (inline content = pattern).' },
38
- replace_in_file: { attrs: ['path', 'search', 'replace'], purpose: 'Regex replace inside a file; inline content is interpreted as regex flags (e.g. g, i, gi).' },
39
+ replace_in_file: { attrs: ['path', 'search', 'replace', 'regex?', 'replace_all?'], purpose: 'Exact string replace (Claude Code Edit model): search is matched LITERALLY by default — paste verbatim code incl. ( ) { } . [ ]. The match must be UNIQUE: not-found ERRORS (file unchanged); >1 match ERRORS unless replace_all="true". Set regex="true" for regex mode (inline content = flags). Returns the honest replaced count.' },
39
40
  get_env: { attrs: [], purpose: 'Read an env var (inline content = name).' },
40
41
  set_env: { attrs: ['name', 'value'], purpose: 'Set an env var for this process.' },
41
42
  download: { attrs: ['path'], purpose: 'HTTP download (inline content = URL). Saves to the CWD by default; optional path attr sets the destination (confined to the CWD; size-capped).' },
42
43
  upload: { attrs: ['path'], purpose: 'Write base64-encoded content to file.' },
43
44
  http_get: { attrs: ['url', 'mode?', 'intent?'], purpose: 'HTTP GET → web-fetch pipeline. mode="summarized" (default) extracts main content → Markdown → secondary-model summary; "extracted" = main-content Markdown, no summary; "raw" = original HTML/content (for analyzing markup/CSS/JS). Token-capped in every mode. To extract specific VALUES (colors, versions, IDs), prefer download+grep instead — see web-extraction guidance below.' },
44
45
  web_search: { attrs: ['query'], purpose: 'Search the web; returns a compact list of {title,url,snippet}. Pick the relevant result(s) and fetch them with http_get — do NOT fetch every result.' },
45
- ask_user: { attrs: ['question'], purpose: 'Ask the user a question and receive an answer.' },
46
+ ask_user: { attrs: ['question'], purpose: 'Ask the user a question and receive their answer. To present a choice, include a numbered list in the question — two or more lines formatted "1. Option A" / "2. Option B" — and it renders as an arrow-key menu returning the chosen option; without a numbered list it takes a free-text reply.' },
46
47
  store_memory: { attrs: ['key'], purpose: 'Persist a key/value to local memory (inline content = value).' },
47
48
  recall_memory: { attrs: ['key'], purpose: 'Read a key from local memory.' },
48
49
  list_memories: { attrs: [], purpose: 'List memory keys.' },
@@ -98,12 +99,21 @@ const LOCAL_NAVIGATION_NOTICE = `## Navigating a codebase efficiently:
98
99
 
99
100
  To explore code, LOCATE FIRST with \`grep\`/\`glob\` — don't read whole files hunting for something. Use \`grep\` output_mode="files_with_matches" to find WHICH files mention a symbol, output_mode="count" for HOW MANY, and the default content mode (file:line:text) to see the matching lines in place. Then \`read_file\` only the relevant slice with \`start_line\`/\`end_line\` (add \`show_line_numbers\` when you need line refs to drive \`edit_file\`) — reading an entire large file dumps it into context and is paginated anyway. For large command output, redirect it to a file and \`grep\` that file rather than letting the whole output enter context.`;
100
101
 
102
+ // Guidance: view_image makes a LOCAL image visible to the MODEL (not the user),
103
+ // and the URL→vision path is a deliberate two-step (download, then view_image) —
104
+ // the same split Claude Code uses (fetch ≠ view). Kept brief on purpose.
105
+ const IMAGE_VIEW_NOTICE = `## Viewing images:
106
+
107
+ \`view_image <path>\` loads a LOCAL image file (PNG/JPEG/GIF/WebP) into YOUR OWN vision context so you can analyze it. It makes the image visible to YOU (the model), NOT to the user — never say "take a look" or describe it as something the user can see. To analyze an image from a URL, first \`download\` it to the working directory, then \`view_image\` the saved path.`;
108
+
101
109
  const SYSTEM_PROMPT_TEMPLATE = `You are Semalt.AI, an expert AI coding assistant running in the user's terminal. You have the ability to execute shell commands and file operations.
102
110
 
103
111
  ${UNTRUSTED_CONTENT_NOTICE}
104
112
 
105
113
  ${WEB_EXTRACTION_NOTICE}
106
114
 
115
+ ${IMAGE_VIEW_NOTICE}
116
+
107
117
  ${LOCAL_NAVIGATION_NOTICE}
108
118
 
109
119
  ## Available tool tags:
@@ -146,6 +156,8 @@ ${UNTRUSTED_CONTENT_NOTICE}
146
156
 
147
157
  ${WEB_EXTRACTION_NOTICE}
148
158
 
159
+ ${IMAGE_VIEW_NOTICE}
160
+
149
161
  ${LOCAL_NAVIGATION_NOTICE}
150
162
 
151
163
  Use \`<think>...</think>\` for internal reasoning (runtime-handled; never emit as an action). Use \`<plan>...</plan>\` to record a short plan for the agent framework.