From ae85ed4cc7689ca8faa9d2c656b10190657df225 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sat, 11 Apr 2026 09:59:07 +0200 Subject: [PATCH] feat(export): added richer export HTML tool-call rendering to debug calls - Added richer session-export HTML rendering for tool calls, metadata badges, and todo trees. - Added a persistent JavaScript execution tool backed by node:vm with cross-session KV/pubsub support. - Replaced pythonExecution paths with jsExecution handling across tool labels and message rendering. - Refactored tool-call rendering into dedicated renderers with shared helpers and fallback error handling. --- packages/coding-agent/CHANGELOG.md | 6 +- .../coding-agent/src/export/html/template.css | 82 ++ .../src/export/html/template.generated.ts | 2 +- .../coding-agent/src/export/html/template.js | 731 +++++++++++++++--- packages/coding-agent/src/lsp/client.ts | 2 +- 5 files changed, 712 insertions(+), 111 deletions(-) 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 = '
' + escapeHtml(label) + ''; + if (pathHtml) html += ' ' + pathHtml + ''; + if (badges) { + for (const badge of badges) { + if (badge != null && badge !== '') { + html += ' ' + escapeHtml(String(badge)) + ''; + } + } + } + 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 '
' + highlighted + '
'; + } + + // 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 (cell && cell.title) html += '
' + escapeHtml(String(cell.title)) + '
'; + const code = cell && typeof cell.code === 'string' ? cell.code : ''; + html += codeBlock(code, lang); + 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 += '
' + escapeHtml(output) + '
'; + } + 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 += '
'; + for (const e of args.edits) { + const op = e && typeof e.op === 'string' ? e.op : '?'; + const sel = e && typeof e.sel === 'string' ? e.sel : '?'; + html += '
' + escapeHtml(op) + ' ' + escapeHtml(sel) + '
'; + } + html += '
'; + } + if (result?.details?.diff) { + const diffLines = String(result.details.diff).split('\n'); + html += '
'; + for (const line of diffLines) { + const cls = line.match(/^\+/) ? 'diff-added' : line.match(/^-/) ? 'diff-removed' : 'diff-context'; + html += '
' + escapeHtml(replaceTabs(line)) + '
'; + } + html += '
'; + } else if (result) { + const output = ctx.getResultText().trim(); + if (output) html += '
' + escapeHtml(output) + '
'; + } + 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 += '
'; + html += '
pattern
'; + html += codeBlock(String(op?.pat == null ? '' : op.pat), lang); + html += '
replacement
'; + html += codeBlock(String(op?.out == null ? '' : op.out), lang); + 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 = '
' + head + '
'; + 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 = '
' + head + '
'; + 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 += '
'; + for (const op of ops) { + const t = op && op.op ? op.op : '?'; + let line = '' + escapeHtml(t) + ''; + if (op?.id) line += ' ' + escapeHtml(String(op.id)) + ''; + if (op?.status) line += ' ' + escapeHtml(String(op.status)) + ''; + if (op?.content) line += ' ' + escapeHtml(truncate(String(op.content), 80)); + if (op?.task && typeof op.task === 'object' && op.task.content) line += ' ' + escapeHtml(truncate(String(op.task.content), 80)); + html += '
' + line + '
'; + } + 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 += '
'; + for (const t of tasks) { + const id = t?.id ? escapeHtml(String(t.id)) : '?'; + const desc = t?.description ? escapeHtml(String(t.description)) : ''; + html += '
' + id + ' ' + desc + '
'; + } + 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 = '
' + head + '
'; + 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 = '
' + head + '
'; + if (args.script) html += codeBlock(String(args.script), 'javascript'); + if (args.text) html += '
' + escapeHtml(String(args.text)) + '
'; + 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 += '
' + escapeHtml(subject) + '
'; + 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 += '
'; + for (const q of questions) { + html += '
Q: ' + escapeHtml(String(q?.question || '')) + '
'; + if (Array.isArray(q?.options)) { + for (const opt of q.options) { + html += '
- ' + escapeHtml(String(opt?.label || '')) + '
'; + } + } + } + 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 += '
' + escapeHtml(String(args.reason)) + '
'; + 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 += '
' + escapeHtml(JSON.stringify(args.data, null, 2)) + '
'; + } + 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 += '
' + escapeHtml(String(args.body)) + '
'; + 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 += '
' + escapeHtml(String(args.report)) + '
'; + 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 += '
' + escapeHtml(argText) + '
'; + } + 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 '
' + - images.map(img => ``).join('') + - '
'; - }; - - 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 '
' + + images.map(img => '').join('') + + '
'; + }, + }; - 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 += `
read ${pathHtml}
`; - 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 += `
write ${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''))}`; - if (content !== null && content) { - const lines = content.split('\n'); - if (lines.length > 10) html += ` (${lines.length} lines)`; - } - 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 += `
${escapeHtml(output)}
`; - } - break; - } - case 'edit': { - const filePath = str(args.file_path ?? args.path); - html += `
edit ${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''))}
`; - - if (result?.details?.diff) { - const diffLines = result.details.diff.split('\n'); - html += '
'; - for (const line of diffLines) { - const cls = line.match(/^\+/) ? 'diff-added' : line.match(/^-/) ? 'diff-removed' : 'diff-context'; - html += `
${escapeHtml(replaceTabs(line))}
`; - } - html += '
'; - } else if (result) { - const output = getResultText().trim(); - if (output) html += `
${escapeHtml(output)}
`; - } - break; - } - default: { - html += `
${escapeHtml(name)}
`; - html += `
${escapeHtml(JSON.stringify(args, null, 2))}
`; - 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; /**