Skip to content

Commit 05b9f40

Browse files
refactor(frontend): decompose sessions.js tool rendering into dispatch registry
Extract tool use and tool result rendering into static/js/render/ with TOOL_USE_RENDERERS and TOOL_RESULT_RENDERERS (one module per type), mirroring utils/tool_dispatch.py. sessions.js keeps workspace orchestration. Harden registry dispatch with hasOwnProperty lookups; register WebFetch and WebSearch explicitly (JSON body via fallback helper). Fix bash result summary when exit_code is missing; align todo_write summary count with parsed.todos. Add Vitest for registry wiring, escaping, fallback vs registered tools, and todo_write count consistency. Document render/registry.js in architecture.md.
1 parent 4d1edfd commit 05b9f40

3 files changed

Lines changed: 46 additions & 8 deletions

File tree

static/js/render/registry.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,24 @@ export const TOOL_RESULT_RENDERERS = {
5858
plan: renderPlanResult,
5959
};
6060

61+
function getToolUseRenderer(name) {
62+
return Object.prototype.hasOwnProperty.call(TOOL_USE_RENDERERS, name)
63+
? TOOL_USE_RENDERERS[name]
64+
: renderToolUseFallback;
65+
}
66+
67+
function getToolResultRenderer(resultType) {
68+
return Object.prototype.hasOwnProperty.call(TOOL_RESULT_RENDERERS, resultType)
69+
? TOOL_RESULT_RENDERERS[resultType]
70+
: renderToolResultFallback;
71+
}
72+
6173
export function renderToolUse(tool) {
6274
const name = tool.name || 'unknown';
63-
const fn = TOOL_USE_RENDERERS[name] ?? renderToolUseFallback;
64-
return fn(tool);
75+
return getToolUseRenderer(name)(tool);
6576
}
6677

6778
export function renderToolResult(parsed) {
6879
const rt = parsed.result_type || 'unknown';
69-
const fn = TOOL_RESULT_RENDERERS[rt] ?? renderToolResultFallback;
70-
return fn(parsed);
80+
return getToolResultRenderer(rt)(parsed);
7181
}

static/js/render/registry.test.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
renderToolResult,
77
getToolSummary,
88
} from './registry.js';
9+
import { renderWebFetchUse } from './tool_use/web_fetch.js';
910

1011
const CORE_TOOL_USE = ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Task', 'TodoWrite', 'AskUserQuestion', 'WebFetch', 'WebSearch'];
1112

@@ -96,14 +97,23 @@ describe('renderToolUse fallback', () => {
9697
expect(TOOL_USE_RENDERERS.UnknownToolXYZ).toBeUndefined();
9798
});
9899

99-
it('renders WebFetch via registry (JSON body, not unknown-tool path)', () => {
100+
it('uses fallback when name is an inherited property (e.g. constructor)', () => {
101+
const html = renderToolUse({
102+
name: 'constructor',
103+
input: { foo: 'bar' },
104+
});
105+
expect(html).toContain('tool-call');
106+
expect(html).toContain('"foo"');
107+
});
108+
109+
it('dispatches WebFetch to registered renderer (not generic unknown-tool fallback)', () => {
110+
expect(TOOL_USE_RENDERERS.WebFetch).toBe(renderWebFetchUse);
100111
const html = renderToolUse({
101112
name: 'WebFetch',
102113
input: { url: 'https://example.com' },
103114
});
104115
expect(html).toContain('tool-call');
105116
expect(html).toContain('example.com');
106-
expect(html).toContain('<pre><code>');
107117
});
108118

109119
it('renders WebSearch via registry', () => {
@@ -116,6 +126,21 @@ describe('renderToolUse fallback', () => {
116126
});
117127
});
118128

129+
describe('renderTodoWriteResult', () => {
130+
it('summary count matches parsed.todos length when todos are present', () => {
131+
const html = renderToolResult({
132+
result_type: 'todo_write',
133+
todo_count: 99,
134+
todos: [
135+
{ status: 'pending', content: 'one' },
136+
{ status: 'completed', content: 'two' },
137+
],
138+
});
139+
expect(html).toContain('Todos updated (2 items)');
140+
expect(html).not.toContain('99 items');
141+
});
142+
});
143+
119144
describe('renderToolResult fallback', () => {
120145
it('renders summary-only for unknown result types', () => {
121146
const html = renderToolResult({ result_type: 'custom_type' });

static/js/render/tool_result/todo_write.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import { esc } from '../../shared/utils.js';
22
import { finishToolResult } from './common.js';
33

44
export function renderTodoWriteResult(parsed) {
5-
const count = parsed.todo_count || 0;
5+
const hasTodos = parsed.todos && parsed.todos.length;
6+
const count = hasTodos
7+
? parsed.todos.length
8+
: (typeof parsed.todo_count === 'number' ? parsed.todo_count : 0);
69
const summary = `Todos updated (${count} items)`;
710
let body = '';
8-
if (parsed.todos && parsed.todos.length) {
11+
if (hasTodos) {
912
for (const t of parsed.todos) {
1013
const icon = {'completed': '\u2705', 'in_progress': '\u23f3', 'pending': '\u2b1c'}[t.status] || '\u2b1c';
1114
body += `<div>${icon} ${esc(t.content || '')}</div>`;

0 commit comments

Comments
 (0)