diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index e8244a3d0..bc6c7f22e 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -1,9 +1,10 @@
# Changelog
## [Unreleased]
-
### Added
+- Added richer tool rendering details in session export HTML, including metadata badges, argument formatting, and todo task tree styling for exported tool and workflow messages
+- Added a persistent `js` tool backed by `node:vm`, with cross-session `highway` KV/pubsub, tool calls from inside JS cells, and `$` / `$$` interactive JavaScript execution
- Added SQLite database read support to the `read` tool for `.sqlite`, `.sqlite3`, `.db`, and `.db3` files with table listing, schema + sample output, row lookup, paginated query filtering, and read-only `q=SELECT` mode
- Added SQLite mutation support to the `write` tool so `db.sqlite:table` inserts JSON5 rows and `db.sqlite:table:key` updates or deletes rows via row key
- Added rendering of usage report entries for accounts with no usage limits, including account label and optional plan type with a `-- no limits` indicator
@@ -13,6 +14,7 @@
### Changed
+- Replaced the LLM-callable Python execution path with JavaScript execution in the shared VM context, including updated renderers, prompts, session messages, and extension events
- Updated interactive and CLI model listings/selectors to work with canonical model ids while resolving them to concrete provider variants for actual execution
- Updated role assignment persistence so selected model settings now store the selector used by users, including thinking-level suffixes, while runtime continues to run against the resolved concrete provider model
- Updated model scope resolution to expand exact canonical model ids into all matching provider variants when filtering supported model sets
@@ -22,6 +24,8 @@
### Fixed
+- Fixed session export rendering so JavaScript execution messages now use `jsExecution` labels and content instead of `pythonExecution`, matching current tool behavior
+- Fixed JavaScript cell execution to auto-display returned values once and preserve persistent VM bindings across calls until reset
- Fixed `.db`/`.db3` reads to verify SQLite file headers and fall back to normal file reading when the extension matches but the content is not a SQLite database
- Fixed SQLite selector parsing and resolution to correctly route requests to database operations at the file-extension boundary instead of misrouting through plain file/archive handlers
- Fixed unsupported or unsafe selectors by rejecting missing tables, composite primary keys for row lookups, unknown query parameters, and row operations on non-existent tables
diff --git a/packages/coding-agent/src/export/html/template.css b/packages/coding-agent/src/export/html/template.css
index 4a5287052..1cf74a5ff 100644
--- a/packages/coding-agent/src/export/html/template.css
+++ b/packages/coding-agent/src/export/html/template.css
@@ -705,6 +705,88 @@
color: var(--error);
}
+ /* Tool renderer extras */
+ .tool-meta {
+ margin-top: 4px;
+ }
+
+ .tool-badge {
+ display: inline-block;
+ padding: 0 6px;
+ margin-right: 4px;
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.06);
+ color: var(--dim);
+ font-size: 11px;
+ font-weight: normal;
+ vertical-align: baseline;
+ }
+
+ .tool-pattern {
+ color: var(--warning);
+ }
+
+ .tool-args {
+ margin-top: 4px;
+ color: var(--toolOutput);
+ }
+
+ .tool-arg {
+ display: block;
+ line-height: var(--line-height);
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+
+ .tool-arg-key {
+ color: var(--dim);
+ }
+
+ .tool-arg-val {
+ color: var(--text);
+ }
+
+ .tool-cell {
+ margin-top: var(--line-height);
+ }
+
+ .tool-cell-title {
+ color: var(--dim);
+ font-size: 11px;
+ margin-bottom: 2px;
+ }
+
+ /* Todo write tree */
+ .todo-tree {
+ margin-top: var(--line-height);
+ }
+
+ .todo-phase {
+ margin-top: 6px;
+ color: var(--accent);
+ font-weight: bold;
+ }
+
+ .todo-task {
+ padding-left: 12px;
+ line-height: var(--line-height);
+ }
+
+ .todo-icon {
+ display: inline-block;
+ width: 14px;
+ text-align: center;
+ color: var(--dim);
+ }
+
+ .todo-completed { color: var(--toolDiffAdded); }
+ .todo-completed .todo-icon { color: var(--toolDiffAdded); }
+ .todo-in_progress { color: var(--warning); }
+ .todo-in_progress .todo-icon { color: var(--warning); }
+ .todo-abandoned { color: var(--toolDiffRemoved); }
+ .todo-abandoned .todo-icon { color: var(--toolDiffRemoved); }
+ .todo-pending { color: var(--toolOutput); }
+
/* Images */
.message-images {
margin-bottom: 12px;
diff --git a/packages/coding-agent/src/export/html/template.generated.ts b/packages/coding-agent/src/export/html/template.generated.ts
index 8be39c480..e79916587 100644
--- a/packages/coding-agent/src/export/html/template.generated.ts
+++ b/packages/coding-agent/src/export/html/template.generated.ts
@@ -1,2 +1,2 @@
// Auto-generated by scripts/generate-template.ts - DO NOT EDIT
-export const TEMPLATE = "\n\n
\n \n \n Session Export \n \n \n\n\n \n \n \n \n \n
\n \n
\n \n
\n
\n
\n
\n\n \n \n \n \n\n\n";
+export const TEMPLATE = "\n\n\n \n \n Session Export \n \n \n\n\n \n \n \n \n \n
\n \n
\n \n
\n
\n
\n
\n\n \n \n \n \n\n\n";
diff --git a/packages/coding-agent/src/export/html/template.js b/packages/coding-agent/src/export/html/template.js
index 06a9a4406..cd0fe4b43 100644
--- a/packages/coding-agent/src/export/html/template.js
+++ b/packages/coding-agent/src/export/html/template.js
@@ -292,7 +292,7 @@
parts.push(msg.role);
if (msg.content) parts.push(extractContent(msg.content));
if (msg.role === 'bashExecution' && msg.command) parts.push(msg.command);
- if (msg.role === 'pythonExecution' && msg.code) parts.push(msg.code);
+ if (msg.role === 'jsExecution' && msg.code) parts.push(msg.code);
break;
}
case 'custom_message':
@@ -480,9 +480,9 @@
const cmd = truncate(normalize(msg.command || ''));
return labelHtml + `[bash]: ${escapeHtml(cmd)}`;
}
- if (msg.role === 'pythonExecution') {
+ if (msg.role === 'jsExecution') {
const code = truncate(normalize(msg.code || ''));
- return labelHtml + `[python]: ${escapeHtml(code)}`;
+ return labelHtml + `[js]: ${escapeHtml(code)}`;
}
return labelHtml + `[${msg.role}] `;
}
@@ -702,123 +702,638 @@
return out;
}
+ // ============================================================
+ // TOOL CALL RENDERING
+ // ============================================================
+
+ // Shared helpers for per-tool renderers.
+ function toolHead(label, pathHtml, badges) {
+ let html = '';
+ return html;
+ }
+
+ function invalidArgHtml() {
+ return '[invalid arg] ';
+ }
+
+ function pathDisplay(filePath, offset, limit) {
+ if (filePath == null) return invalidArgHtml();
+ let html = escapeHtml(shortenPath(filePath || ''));
+ if (offset !== undefined || limit !== undefined) {
+ const start = offset == null ? 1 : offset;
+ const end = limit !== undefined ? start + limit - 1 : '';
+ html += ':' + start + (end ? '-' + end : '') + ' ';
+ }
+ return html;
+ }
+
+ function codeBlock(code, lang) {
+ if (code == null || code === '') return '';
+ const text = String(code);
+ let highlighted;
+ try {
+ highlighted = lang ? hljs.highlight(text, { language: lang }).value : escapeHtml(text);
+ } catch {
+ highlighted = escapeHtml(text);
+ }
+ return '';
+ }
+
+ // Per-tool renderers. Each accepts (name, args, result, ctx) and returns the inner HTML.
+ function renderBash(name, args, result, ctx) {
+ const command = str(args.command);
+ const cwd = str(args.cwd);
+ const env = args.env && typeof args.env === 'object' ? args.env : null;
+ const cmdDisplay = command === null ? invalidArgHtml() : escapeHtml(command || '...');
+ let prefix = '';
+ if (env) {
+ for (const [k, v] of Object.entries(env)) {
+ prefix += escapeHtml(k) + '=' + escapeHtml(String(v)) + ' ';
+ }
+ }
+ let html = '$ ' + prefix + cmdDisplay + '
';
+ const badges = [];
+ if (cwd) badges.push('cwd=' + shortenPath(cwd));
+ if (args.timeout) badges.push('timeout=' + args.timeout + 's');
+ if (args.pty) badges.push('pty');
+ if (args.head) badges.push('head=' + args.head);
+ if (args.tail) badges.push('tail=' + args.tail);
+ if (badges.length) {
+ html += '' + badges.map(b => '' + escapeHtml(b) + ' ').join(' ') + '
';
+ }
+ if (result) {
+ html += ctx.renderResultImages();
+ const output = ctx.getResultText().trim();
+ if (output) html += formatExpandableOutput(output, 5);
+ }
+ return html;
+ }
+
+ function renderJsLike(name, args, result, ctx) {
+ const lang = name === 'python' ? 'python' : 'javascript';
+ const badges = [];
+ if (args.cwd) badges.push('cwd=' + shortenPath(String(args.cwd)));
+ if (args.timeout) badges.push('timeout=' + args.timeout + 's');
+ if (args.reset) badges.push('reset');
+ let html = toolHead(name, '', badges);
+ const cells = Array.isArray(args.cells) ? args.cells : null;
+ if (!cells) {
+ html += '[missing cells]
';
+ } else {
+ for (const cell of cells) {
+ html += '';
+ }
+ }
+ if (result) {
+ html += ctx.renderResultImages();
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 10);
+ }
+ return html;
+ }
+
+ function renderRead(name, args, result, ctx) {
+ const filePath = str(args.file_path == null ? args.path : args.file_path);
+ let pathHtml = pathDisplay(filePath, args.offset, args.limit);
+ if (args.sel) pathHtml += ':' + escapeHtml(String(args.sel)) + ' ';
+ let html = toolHead('read', pathHtml);
+ if (result) {
+ html += ctx.renderResultImages();
+ const output = ctx.getResultText();
+ const lang = filePath ? getLanguageFromPath(filePath) : null;
+ if (output) html += formatExpandableOutput(output, 10, lang);
+ }
+ return html;
+ }
+
+ function renderWrite(name, args, result, ctx) {
+ const filePath = str(args.file_path == null ? args.path : args.file_path);
+ const content = str(args.content);
+ const pathHtml = filePath === null ? invalidArgHtml() : escapeHtml(shortenPath(filePath || ''));
+ const lineCount = (content != null && content !== '') ? content.split('\n').length : 0;
+ const badges = lineCount > 10 ? ['(' + lineCount + ' lines)'] : null;
+ let html = toolHead('write', pathHtml, badges);
+ if (content === null) {
+ html += '[invalid content arg - expected string]
';
+ } else if (content) {
+ const lang = filePath ? getLanguageFromPath(filePath) : null;
+ html += formatExpandableOutput(content, 10, lang);
+ }
+ if (result) {
+ const output = ctx.getResultText().trim();
+ if (output) html += '';
+ }
+ return html;
+ }
+
+ function renderEdit(name, args, result, ctx) {
+ const filePath = str(args.file_path == null ? args.path : args.file_path);
+ const pathHtml = filePath === null ? invalidArgHtml() : escapeHtml(shortenPath(filePath || ''));
+ let html = toolHead('edit', pathHtml);
+ if (Array.isArray(args.edits)) {
+ html += '';
+ }
+ if (result?.details?.diff) {
+ const diffLines = String(result.details.diff).split('\n');
+ html += '';
+ } else if (result) {
+ const output = ctx.getResultText().trim();
+ if (output) html += '';
+ }
+ return html;
+ }
+
+ function renderAstEdit(name, args, result, ctx) {
+ const lang = args.lang || null;
+ const pathHtml = args.path ? escapeHtml(shortenPath(String(args.path))) : '';
+ const badges = [];
+ if (lang) badges.push(lang);
+ if (args.glob) badges.push('glob=' + args.glob);
+ if (args.sel) badges.push('sel=' + args.sel);
+ let html = toolHead('ast_edit', pathHtml, badges);
+ if (Array.isArray(args.ops)) {
+ for (const op of args.ops) {
+ html += '';
+ }
+ }
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 10);
+ }
+ return html;
+ }
+
+ function renderAstGrep(name, args, result, ctx) {
+ const lang = args.lang || null;
+ const pathHtml = args.path ? escapeHtml(shortenPath(String(args.path))) : '';
+ const badges = [];
+ if (lang) badges.push(lang);
+ if (args.glob) badges.push('glob=' + args.glob);
+ if (args.sel) badges.push('sel=' + args.sel);
+ let html = toolHead('ast_grep', pathHtml, badges);
+ if (Array.isArray(args.pat)) {
+ for (const pat of args.pat) {
+ html += '' + codeBlock(String(pat == null ? '' : pat), lang) + '
';
+ }
+ }
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 10);
+ }
+ return html;
+ }
+
+ function renderGrep(name, args, result, ctx) {
+ const pattern = str(args.pattern);
+ const pathHtml = args.path ? escapeHtml(shortenPath(String(args.path))) : escapeHtml('.');
+ const patHtml = pattern === null ? invalidArgHtml() : escapeHtml(pattern);
+ let head = 'grep /' + patHtml + '/ ';
+ head += ' in ' + pathHtml + ' ';
+ const badges = [];
+ if (args.glob) badges.push('glob=' + args.glob);
+ if (args.type) badges.push('type=' + args.type);
+ if (args.i) badges.push('i');
+ if (args.multiline) badges.push('multiline');
+ for (const b of badges) head += ' ' + escapeHtml(b) + ' ';
+ let html = '';
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 10);
+ }
+ return html;
+ }
+
+ function renderFind(name, args, result, ctx) {
+ const pattern = str(args.pattern);
+ const patHtml = pattern === null ? invalidArgHtml() : escapeHtml(pattern);
+ const badges = args.limit ? ['limit=' + args.limit] : null;
+ let html = toolHead('find', '' + patHtml + ' ', badges);
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 10);
+ }
+ return html;
+ }
+
+ function renderLsp(name, args, result, ctx) {
+ const action = str(args.action) || '?';
+ let head = 'lsp ' + escapeHtml(action) + ' ';
+ if (args.file && args.file !== '*') {
+ head += ' ' + escapeHtml(shortenPath(String(args.file))) + ' ';
+ } else if (args.file === '*') {
+ head += ' workspace ';
+ }
+ if (args.line) head += ':' + args.line + ' ';
+ if (args.symbol) head += ' ' + escapeHtml(String(args.symbol)) + ' ';
+ if (args.query) head += ' query= ' + escapeHtml(String(args.query)) + ' ';
+ if (args.new_name) head += ' → ' + escapeHtml(String(args.new_name)) + ' ';
+ let html = '';
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 12);
+ }
+ return html;
+ }
+
+ function renderTodoWrite(name, args, result, ctx) {
+ let html = toolHead('todo_write');
+ const ops = Array.isArray(args.ops) ? args.ops : null;
+ if (ops) {
+ html += '';
+ }
+ const phases = result?.details?.phases;
+ if (Array.isArray(phases)) {
+ html += '';
+ for (const phase of phases) {
+ html += '
' + escapeHtml(String(phase.name || '')) + '
';
+ if (Array.isArray(phase.tasks)) {
+ for (const task of phase.tasks) {
+ const status = task.status || 'pending';
+ const icon = status === 'completed' ? '✓' : status === 'in_progress' ? '→' : status === 'abandoned' ? '✕' : '○';
+ html += '
' + icon + ' ' + escapeHtml(String(task.content || '')) + '
';
+ }
+ }
+ }
+ html += '
';
+ } else if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 8);
+ }
+ return html;
+ }
+
+ function renderTask(name, args, result, ctx) {
+ const agent = str(args.agent) || '?';
+ const tasks = Array.isArray(args.tasks) ? args.tasks : [];
+ const badges = ['agent=' + agent, tasks.length + ' subtask' + (tasks.length === 1 ? '' : 's')];
+ if (args.isolated) badges.push('isolated');
+ let html = toolHead('task', '', badges);
+ if (tasks.length) {
+ html += '';
+ }
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 12);
+ }
+ return html;
+ }
+
+ function renderWebSearch(name, args, result, ctx) {
+ const query = str(args.query);
+ const queryHtml = query === null ? invalidArgHtml() : escapeHtml(query);
+ const badges = [];
+ if (args.recency) badges.push('recency=' + args.recency);
+ if (args.limit) badges.push('limit=' + args.limit);
+ let html = toolHead('web_search', '' + queryHtml + ' ', badges);
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 12, 'markdown');
+ }
+ return html;
+ }
+
+ function renderFetch(name, args, result, ctx) {
+ const url = str(args.url) || '';
+ const badges = args.method ? [String(args.method)] : null;
+ let html = toolHead('fetch', '' + escapeHtml(url) + ' ', badges);
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 10);
+ }
+ return html;
+ }
+
+ function renderDebug(name, args, result, ctx) {
+ const action = str(args.action) || '?';
+ const badges = [];
+ if (args.adapter) badges.push(args.adapter);
+ if (args.program) badges.push('program=' + shortenPath(String(args.program)));
+ if (args.file) badges.push('file=' + shortenPath(String(args.file)));
+ if (args.line) badges.push('line=' + args.line);
+ let head = 'debug ' + escapeHtml(action) + ' ';
+ for (const b of badges) head += ' ' + escapeHtml(String(b)) + ' ';
+ let html = '';
+ if (args.expression) html += codeBlock(String(args.expression));
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 10);
+ }
+ return html;
+ }
+
+ function renderPuppeteer(name, args, result, ctx) {
+ const action = str(args.action) || '?';
+ const badges = [];
+ if (args.url) badges.push(String(args.url));
+ if (args.selector) badges.push('selector=' + args.selector);
+ if (args.element_id != null) badges.push('id=' + args.element_id);
+ let head = 'puppeteer ' + escapeHtml(action) + ' ';
+ for (const b of badges) head += ' ' + escapeHtml(String(b)) + ' ';
+ let html = '';
+ if (args.script) html += codeBlock(String(args.script), 'javascript');
+ if (args.text) html += '';
+ if (result) {
+ html += ctx.renderResultImages();
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 10);
+ }
+ return html;
+ }
+
+ function renderInspectImage(name, args, result, ctx) {
+ const p = str(args.path == null ? args.url : args.path) || '';
+ let html = toolHead('inspect_image', escapeHtml(shortenPath(p)));
+ if (result) {
+ html += ctx.renderResultImages();
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 8);
+ }
+ return html;
+ }
+
+ function renderGenerateImage(name, args, result, ctx) {
+ const subject = str(args.subject) || '';
+ const badges = args.aspect_ratio ? [String(args.aspect_ratio)] : null;
+ let html = toolHead('generate_image', '', badges);
+ if (subject) html += '';
+ if (result) {
+ html += ctx.renderResultImages();
+ }
+ return html;
+ }
+
+ function renderAsk(name, args, result, ctx) {
+ let html = toolHead('ask');
+ const questions = Array.isArray(args.questions) ? args.questions : null;
+ if (questions) {
+ html += '';
+ }
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 8);
+ }
+ return html;
+ }
+
+ function renderExitPlanMode(name, args, result, ctx) {
+ const badges = args.title ? [String(args.title)] : null;
+ let html = toolHead('exit_plan_mode', '', badges);
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 8);
+ }
+ return html;
+ }
+
+ function renderResolve(name, args, result, ctx) {
+ const action = str(args.action) || '?';
+ let html = toolHead('resolve', '', [action]);
+ if (args.reason) html += '';
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 6);
+ }
+ return html;
+ }
+
+ function renderGh(name, args, result, ctx) {
+ const badges = [];
+ if (args.repo) badges.push(String(args.repo));
+ if (args.issue) badges.push('#' + args.issue);
+ if (args.pr) badges.push('PR ' + args.pr);
+ if (args.branch) badges.push('branch=' + args.branch);
+ if (args.query) badges.push('query=' + args.query);
+ if (args.run) badges.push('run=' + args.run);
+ let html = toolHead(name, '', badges);
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 12, 'markdown');
+ }
+ return html;
+ }
+
+ function renderMermaid(name, args, result, ctx) {
+ let html = toolHead('render_mermaid');
+ const code = args.code || args.source;
+ if (code) html += codeBlock(String(code), 'mermaid');
+ if (result) {
+ html += ctx.renderResultImages();
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 6);
+ }
+ return html;
+ }
+
+ function renderSubmitResult(name, args, result, ctx) {
+ let html = toolHead('submit_result');
+ if (args.data !== undefined) {
+ html += '';
+ }
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 6);
+ }
+ return html;
+ }
+
+ function renderReportFinding(name, args, result, ctx) {
+ const badges = [];
+ if (args.priority) badges.push('priority=' + args.priority);
+ if (args.confidence != null) badges.push('confidence=' + args.confidence);
+ if (args.file_path) badges.push(shortenPath(String(args.file_path)));
+ let html = toolHead('report_finding', args.title ? escapeHtml(String(args.title)) : '', badges);
+ if (args.body) html += '';
+ return html;
+ }
+
+ function renderReportToolIssue(name, args, result, ctx) {
+ const pathHtml = args.tool ? '' + escapeHtml(String(args.tool)) + ' ' : '';
+ let html = toolHead('report_tool_issue', pathHtml);
+ if (args.report) html += '';
+ return html;
+ }
+
+ function renderCalc(name, args, result, ctx) {
+ let html = toolHead('calc');
+ const exprs = args.expressions || (args.expression ? [args.expression] : []);
+ for (const e of exprs) html += codeBlock(String(e), 'plaintext');
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 6);
+ }
+ return html;
+ }
+
+ function renderAwait(name, args, result, ctx) {
+ const badges = [];
+ if (Array.isArray(args.jobIds)) badges.push(args.jobIds.length + ' job' + (args.jobIds.length === 1 ? '' : 's'));
+ let html = toolHead('await', '', badges);
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 8);
+ }
+ return html;
+ }
+
+ function renderCancelJob(name, args, result, ctx) {
+ let html = toolHead('cancel_job', args.jobId ? escapeHtml(String(args.jobId)) : '');
+ if (result) {
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 4);
+ }
+ return html;
+ }
+
+ function renderGenericTool(name, args, result, ctx) {
+ let html = toolHead(name);
+ const argText = JSON.stringify(args, null, 2);
+ if (argText && argText !== '{}') {
+ html += '';
+ }
+ if (result) {
+ html += ctx.renderResultImages();
+ const output = ctx.getResultText();
+ if (output) html += formatExpandableOutput(output, 10);
+ }
+ return html;
+ }
+
+ const TOOL_RENDERERS = {
+ bash: renderBash,
+ js: renderJsLike,
+ python: renderJsLike,
+ notebook: renderJsLike,
+ read: renderRead,
+ write: renderWrite,
+ edit: renderEdit,
+ ast_edit: renderAstEdit,
+ ast_grep: renderAstGrep,
+ grep: renderGrep,
+ find: renderFind,
+ lsp: renderLsp,
+ todo_write: renderTodoWrite,
+ task: renderTask,
+ web_search: renderWebSearch,
+ fetch: renderFetch,
+ debug: renderDebug,
+ puppeteer: renderPuppeteer,
+ inspect_image: renderInspectImage,
+ generate_image: renderGenerateImage,
+ ask: renderAsk,
+ exit_plan_mode: renderExitPlanMode,
+ resolve: renderResolve,
+ gh_repo_view: renderGh,
+ gh_issue_view: renderGh,
+ gh_pr_view: renderGh,
+ gh_pr_diff: renderGh,
+ gh_pr_checkout: renderGh,
+ gh_pr_push: renderGh,
+ gh_run_watch: renderGh,
+ gh_search_issues: renderGh,
+ gh_search_prs: renderGh,
+ render_mermaid: renderMermaid,
+ submit_result: renderSubmitResult,
+ report_finding: renderReportFinding,
+ report_tool_issue: renderReportToolIssue,
+ calc: renderCalc,
+ calculator: renderCalc,
+ await: renderAwait,
+ cancel_job: renderCancelJob,
+ };
+
function renderToolCall(call) {
const result = findToolResult(call.id);
const isError = result?.isError || false;
const statusClass = result ? (isError ? 'error' : 'success') : 'pending';
-
- const getResultText = () => {
- if (!result) return '';
- const textBlocks = result.content.filter(c => c.type === 'text');
- return textBlocks.map(c => c.text).join('\n');
- };
-
- const getResultImages = () => {
- if (!result) return [];
- return result.content.filter(c => c.type === 'image');
- };
-
- const renderResultImages = () => {
- const images = getResultImages();
- if (images.length === 0) return '';
- return '';
- };
-
- let html = ``;
const args = call.arguments || {};
const name = call.name;
- const invalidArg = '
[invalid arg] ';
+ const ctx = {
+ getResultText: () => {
+ if (!result) return '';
+ const textBlocks = result.content.filter(c => c.type === 'text');
+ return textBlocks.map(c => c.text).join('\n');
+ },
+ getResultImages: () => {
+ if (!result) return [];
+ return result.content.filter(c => c.type === 'image');
+ },
+ renderResultImages: () => {
+ if (!result) return '';
+ const images = result.content.filter(c => c.type === 'image');
+ if (images.length === 0) return '';
+ return '
';
+ },
+ };
- switch (name) {
- case 'bash': {
- const command = str(args.command);
- const cmdDisplay = command === null ? invalidArg : escapeHtml(command || '...');
- html += `
$ ${cmdDisplay}
`;
- if (result) {
- const output = getResultText().trim();
- if (output) html += formatExpandableOutput(output, 5);
- }
- break;
- }
- case 'read': {
- const filePath = str(args.file_path ?? args.path);
- const offset = args.offset;
- const limit = args.limit;
-
- let pathHtml = filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''));
- if (filePath !== null && (offset !== undefined || limit !== undefined)) {
- const startLine = offset ?? 1;
- const endLine = limit !== undefined ? startLine + limit - 1 : '';
- pathHtml += `
:${startLine}${endLine ? '-' + endLine : ''} `;
- }
-
- html += ``;
- if (result) {
- html += renderResultImages();
- const output = getResultText();
- const lang = filePath ? getLanguageFromPath(filePath) : null;
- if (output) html += formatExpandableOutput(output, 10, lang);
- }
- break;
- }
- case 'write': {
- const filePath = str(args.file_path ?? args.path);
- const content = str(args.content);
-
- html += `';
-
- if (content === null) {
- html += `
[invalid content arg - expected string]
`;
- } else if (content) {
- const lang = filePath ? getLanguageFromPath(filePath) : null;
- html += formatExpandableOutput(content, 10, lang);
- }
- if (result) {
- const output = getResultText().trim();
- if (output) html += `
`;
- }
- break;
- }
- case 'edit': {
- const filePath = str(args.file_path ?? args.path);
- html += ``;
-
- if (result?.details?.diff) {
- const diffLines = result.details.diff.split('\n');
- html += '
';
- } else if (result) {
- const output = getResultText().trim();
- if (output) html += `
`;
- }
- break;
- }
- default: {
- html += ``;
- html += `
`;
- if (result) {
- const output = getResultText();
- if (output) html += formatExpandableOutput(output, 10);
- }
- }
+ const renderer = TOOL_RENDERERS[name] || renderGenericTool;
+ let html = '
';
+ try {
+ html += renderer(name, args, result, ctx);
+ } catch (err) {
+ html += renderGenericTool(name, args, result, ctx);
}
-
html += '
';
return html;
}
+
/**
* Build a shareable URL for a specific message.
* URL format: base?gistId&leafId=
&targetId=
@@ -977,7 +1492,7 @@
return html;
}
- if (msg.role === 'pythonExecution') {
+ if (msg.role === 'jsExecution') {
const isError = msg.cancelled || (msg.exitCode !== 0 && msg.exitCode !== null);
let html = `${tsHtml}`;
html += `
$ ${escapeHtml(msg.code)}
`;
diff --git a/packages/coding-agent/src/lsp/client.ts b/packages/coding-agent/src/lsp/client.ts
index b6a9b1918..e05a7eb12 100644
--- a/packages/coding-agent/src/lsp/client.ts
+++ b/packages/coding-agent/src/lsp/client.ts
@@ -23,7 +23,7 @@ const fileOperationLocks = new Map
>();
// Idle timeout configuration (disabled by default)
let idleTimeoutMs: number | null = null;
-let idleCheckInterval: Timer | null = null;
+let idleCheckInterval: NodeJS.Timeout | null = null;
const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
/**