@semalt-ai/code 1.19.0 → 1.20.1

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 (83) 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 +188 -4
  12. package/lib/commands/chat-slash.js +16 -0
  13. package/lib/commands/chat-turn.js +319 -52
  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 +229 -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 +542 -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/narration-ordering.test.js +309 -0
  63. package/test/native-dispatch.test.js +53 -0
  64. package/test/native-live-narration.test.js +254 -0
  65. package/test/output-heredoc-leak.test.js +195 -0
  66. package/test/output-preview.test.js +245 -0
  67. package/test/permission-flush.test.js +302 -0
  68. package/test/permissions.test.js +199 -0
  69. package/test/read-paginate.test.js +1 -1
  70. package/test/render-operation.test.js +317 -0
  71. package/test/replay-descriptor-xml.test.js +216 -0
  72. package/test/replay-descriptor.test.js +189 -0
  73. package/test/replay-web-aggregate.test.js +291 -0
  74. package/test/replay-web-persist.test.js +241 -0
  75. package/test/running-glyph-anim.test.js +111 -0
  76. package/test/status-bar-driver.test.js +93 -0
  77. package/test/status-bar-resync.test.js +188 -0
  78. package/test/stream-parser.test.js +24 -0
  79. package/test/theme-palette.test.js +166 -0
  80. package/test/truncate-visible.test.js +78 -0
  81. package/test/view-image.test.js +199 -0
  82. package/test/web-activity-ordering.test.js +12 -3
  83. package/path +0 -1
package/lib/constants.js CHANGED
@@ -14,7 +14,7 @@ const DEFAULT_API_TIMEOUT_MS = 15 * 60 * 1000;
14
14
  // even a caller that omits the value gets a real cap rather than an unbounded
15
15
  // loop. A config value of 0 (the "unlimited" sentinel) opts out — see
16
16
  // resolveMaxIterations in lib/config.js.
17
- const DEFAULT_MAX_ITERATIONS = 50;
17
+ const DEFAULT_MAX_ITERATIONS = 125;
18
18
 
19
19
  // Self-verification (Task 4.2). When the agent declares a task done, an optional
20
20
  // configured shell command (e.g. `npm test`) is run and its result fed back.
@@ -110,6 +110,28 @@ const OUTPUT_HEAD_RATIO = 0.6;
110
110
  // line-bounded output — it only catches the pathological few-but-huge-lines case.
111
111
  const DEFAULT_OUTPUT_MAX_TOKENS = 10000;
112
112
 
113
+ // File-edit diff display bound (execution-time diff rendering). Every mutating
114
+ // file edit (write/append/edit_file/replace_in_file) renders its diff at the
115
+ // moment it executes — decoupled from the permission modal, so an auto-approved
116
+ // edit shows its changes just like a manually-approved one. `diff_max_lines`
117
+ // caps the number of CHANGED (+/-) lines shown: a small edit (or a series of
118
+ // small edits) renders in full; one large edit shows head+tail of the changed
119
+ // lines with a `… K more changed lines (N total)` notice (mirrors the W.6
120
+ // shell head+tail discipline). Operator-overridable via config.diff_max_lines.
121
+ const DEFAULT_DIFF_MAX_LINES = 50;
122
+
123
+ // Collapsed output-preview bound (Output Refactor — Phase 5). Shell / MCP /
124
+ // subagent output is shown in MODERATION in the chrome: the first
125
+ // `shell_preview_lines` lines render below the result line, then a static
126
+ // `… N more lines` hint. There is no in-terminal way to expand — full viewing is
127
+ // deferred to the planned transcript viewer. This is DISPLAY-ONLY — the model
128
+ // still receives the full output via boundToolOutput; this cap never touches
129
+ // context.
130
+ // Diffs (file edits) are NOT subject to this — they render expanded to
131
+ // `diff_max_lines` (the user explicitly wants to see diffs). Operator-overridable
132
+ // via config.shell_preview_lines.
133
+ const DEFAULT_SHELL_PREVIEW_LINES = 5;
134
+
113
135
  // MCP & subagent result context bounds (Task W.8). MCP tool results
114
136
  // (lib/mcp/client.js mcpResultToText) and subagent final text (lib/subagents.js)
115
137
  // were the last two UNBOUNDED paths into context — both are fenced as untrusted,
@@ -185,6 +207,11 @@ const DEFAULT_CONFIG = {
185
207
  // head+tail line cap (max_output_lines) bounds the common case; this bounds the
186
208
  // pathological few-but-huge-lines case (a single minified line, a binary cat).
187
209
  max_output_tokens: DEFAULT_OUTPUT_MAX_TOKENS,
210
+ // Changed-line cap for execution-time file-edit diffs (see DEFAULT_DIFF_MAX_LINES).
211
+ diff_max_lines: DEFAULT_DIFF_MAX_LINES,
212
+ // Preview-line count for shell/MCP/subagent output chrome (see
213
+ // DEFAULT_SHELL_PREVIEW_LINES). Display-only — never affects model context.
214
+ shell_preview_lines: DEFAULT_SHELL_PREVIEW_LINES,
188
215
  // Max agent-loop iterations per user turn. A positive integer caps the loop;
189
216
  // 0 means deliberately unbounded (power-user choice). Default 50.
190
217
  max_iterations: DEFAULT_MAX_ITERATIONS,
@@ -359,6 +386,7 @@ const TAG_REGISTRY = {
359
386
  exec: { type: 'tool', streaming: false, label: 'Running command' },
360
387
  shell: { type: 'tool', streaming: false, label: 'Running shell' },
361
388
  read_file: { type: 'tool', streaming: false, label: 'Reading file' },
389
+ view_image: { type: 'tool', streaming: false, label: 'Viewing image' },
362
390
  write_file: { type: 'tool', streaming: false, label: 'Writing file' },
363
391
  create_file: { type: 'tool', streaming: false, label: 'Creating file' },
364
392
  append_file: { type: 'tool', streaming: false, label: 'Appending to file' },
@@ -510,6 +538,7 @@ module.exports = {
510
538
  DEFAULT_MAX_OUTPUT_LINES,
511
539
  OUTPUT_HEAD_RATIO,
512
540
  DEFAULT_OUTPUT_MAX_TOKENS,
541
+ DEFAULT_DIFF_MAX_LINES,
513
542
  DEFAULT_MCP_MAX_RESULT_TOKENS,
514
543
  DEFAULT_SUBAGENT_MAX_RESULT_TOKENS,
515
544
  DEFAULT_WEB_MAX_CONTENT_TOKENS,
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.