@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.
- package/.claude/settings.local.json +2 -1
- package/ARCHITECTURE.md +6 -95
- package/CLAUDE.md +196 -1874
- package/README.md +1 -1
- package/docs/ARCHITECTURE.md +1321 -0
- package/docs/CONFIG.md +340 -0
- package/docs/HISTORY.md +245 -0
- package/index.js +1 -1
- package/lib/agent.js +145 -16
- package/lib/api.js +28 -3
- package/lib/commands/chat-session.js +187 -4
- package/lib/commands/chat-slash.js +16 -0
- package/lib/commands/chat-turn.js +272 -49
- package/lib/commands/chat.js +12 -8
- package/lib/config.js +27 -0
- package/lib/constants.js +30 -1
- package/lib/headless.js +36 -1
- package/lib/images.js +8 -2
- package/lib/permissions.js +23 -16
- package/lib/prompts.js +15 -3
- package/lib/tool_registry.js +357 -53
- package/lib/tool_specs.js +42 -8
- package/lib/tools.js +80 -19
- 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 +173 -28
- package/lib/ui/input-field.js +5 -4
- 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 +99 -57
- package/lib/ui/stream.js +20 -13
- package/lib/ui/theme.js +190 -45
- package/lib/ui/tool-operation.js +190 -0
- package/lib/ui/utils.js +9 -5
- package/lib/ui/web-activity.js +58 -6
- package/lib/ui/writer.js +159 -45
- package/lib/ui.js +1 -1
- package/package.json +1 -1
- 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/chat-history-nocolor.test.js +155 -0
- package/test/chat-relogin.test.js +207 -0
- package/test/defer-detail-band.test.js +403 -0
- package/test/detail-band-tab-flatten.test.js +242 -0
- package/test/exec-diff.test.js +268 -0
- package/test/executors.test.js +250 -13
- package/test/extract-tool-calls.test.js +37 -3
- package/test/file-activity.test.js +522 -0
- package/test/grep-path-target.test.js +227 -0
- package/test/harness/chat-harness.js +2 -1
- package/test/headless.test.js +146 -1
- 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 +9 -7
- package/test/md-stream.test.js +183 -0
- package/test/native-dispatch.test.js +53 -0
- package/test/native-live-narration.test.js +254 -0
- package/test/output-heredoc-leak.test.js +195 -0
- package/test/output-preview.test.js +245 -0
- package/test/permissions.test.js +199 -0
- package/test/read-paginate.test.js +1 -1
- 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/running-glyph-anim.test.js +111 -0
- package/test/status-bar-driver.test.js +93 -0
- package/test/status-bar-resync.test.js +188 -0
- package/test/stream-parser.test.js +24 -0
- package/test/theme-palette.test.js +166 -0
- package/test/truncate-visible.test.js +78 -0
- package/test/view-image.test.js +199 -0
- package/test/web-activity-ordering.test.js +12 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
package/lib/permissions.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const writer = require('./ui/writer');
|
|
4
|
-
const
|
|
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
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
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
|
-
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
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
|
-
|
|
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'],
|
|
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: '
|
|
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
|
|
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.
|