From 512e71c3f027be6236d6af6bbcfc2e49089037f4 Mon Sep 17 00:00:00 2001 From: yu-med Date: Wed, 3 Jun 2026 02:02:58 +0800 Subject: [PATCH 1/5] refactor(frontend): decompose sessions.js tool rendering into dispatch registry Extract tool use and tool result rendering from sessions.js into static/js/render/ with TOOL_USE_RENDERERS and TOOL_RESULT_RENDERERS, one module per tool type, mirroring utils/tool_dispatch.py. sessions.js retains workspace and message orchestration only. Add Vitest coverage for registry wiring, HTML escaping, and getToolSummary. Document the frontend registry in docs/architecture.md. No intended visual or behavior change for existing sessions. --- docs/architecture.md | 5 +- static/js/render/registry.js | 67 ++++++++ static/js/render/registry.test.js | 99 ++++++++++++ static/js/render/tool_result/bash.js | 12 ++ static/js/render/tool_result/common.js | 8 + static/js/render/tool_result/fallback.js | 7 + static/js/render/tool_result/file_edit.js | 6 + static/js/render/tool_result/file_read.js | 7 + static/js/render/tool_result/file_write.js | 6 + static/js/render/tool_result/glob.js | 7 + static/js/render/tool_result/grep.js | 6 + static/js/render/tool_result/plan.js | 6 + static/js/render/tool_result/task.js | 13 ++ static/js/render/tool_result/todo_write.js | 15 ++ static/js/render/tool_result/user_input.js | 19 +++ static/js/render/tool_result/utils.js | 8 + static/js/render/tool_result/web_fetch.js | 6 + static/js/render/tool_result/web_search.js | 6 + .../js/render/tool_use/ask_user_question.js | 14 ++ static/js/render/tool_use/bash.js | 12 ++ static/js/render/tool_use/common.js | 5 + static/js/render/tool_use/edit.js | 12 ++ static/js/render/tool_use/fallback.js | 12 ++ static/js/render/tool_use/glob.js | 10 ++ static/js/render/tool_use/grep.js | 10 ++ static/js/render/tool_use/read.js | 10 ++ static/js/render/tool_use/summary.js | 16 ++ static/js/render/tool_use/task.js | 11 ++ static/js/render/tool_use/todo_write.js | 15 ++ static/js/render/tool_use/write.js | 11 ++ static/js/sessions.js | 144 +----------------- 31 files changed, 443 insertions(+), 142 deletions(-) create mode 100644 static/js/render/registry.js create mode 100644 static/js/render/registry.test.js create mode 100644 static/js/render/tool_result/bash.js create mode 100644 static/js/render/tool_result/common.js create mode 100644 static/js/render/tool_result/fallback.js create mode 100644 static/js/render/tool_result/file_edit.js create mode 100644 static/js/render/tool_result/file_read.js create mode 100644 static/js/render/tool_result/file_write.js create mode 100644 static/js/render/tool_result/glob.js create mode 100644 static/js/render/tool_result/grep.js create mode 100644 static/js/render/tool_result/plan.js create mode 100644 static/js/render/tool_result/task.js create mode 100644 static/js/render/tool_result/todo_write.js create mode 100644 static/js/render/tool_result/user_input.js create mode 100644 static/js/render/tool_result/utils.js create mode 100644 static/js/render/tool_result/web_fetch.js create mode 100644 static/js/render/tool_result/web_search.js create mode 100644 static/js/render/tool_use/ask_user_question.js create mode 100644 static/js/render/tool_use/bash.js create mode 100644 static/js/render/tool_use/common.js create mode 100644 static/js/render/tool_use/edit.js create mode 100644 static/js/render/tool_use/fallback.js create mode 100644 static/js/render/tool_use/glob.js create mode 100644 static/js/render/tool_use/grep.js create mode 100644 static/js/render/tool_use/read.js create mode 100644 static/js/render/tool_use/summary.js create mode 100644 static/js/render/tool_use/task.js create mode 100644 static/js/render/tool_use/todo_write.js create mode 100644 static/js/render/tool_use/write.js diff --git a/docs/architecture.md b/docs/architecture.md index 3463132..ea2601a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -105,10 +105,13 @@ The UI is a **hash-routed** SPA with ES modules under `static/js/`: - `app.js` — routing and boot - `projects.js`, `sessions.js`, `search.js`, `export.js` — route handlers +- `render/registry.js` — **tool dispatch registry** for session UI: `TOOL_USE_RENDERERS` and `TOOL_RESULT_RENDERERS` map tool name / `result_type` → render function (one module per type under `render/tool_use/` and `render/tool_result/`). Parallels backend `utils/tool_dispatch.py` (backend uses ordered predicates; frontend uses direct key lookup + fallback). - `shared/markdown.js` — markdown + **DOMPurify** sanitization (do not render raw LLM HTML) - `shared/state.js`, `shared/utils.js`, `shared/theme.js` — shared UI state and helpers -No bundler step — modern browsers load modules directly. Frontend unit tests use **vitest** + **jsdom** (`npm test`). +`sessions.js` keeps workspace/session orchestration and message bubbles; tool cards delegate to `render/registry.js`. + +No bundler step — modern browsers load modules directly. Frontend unit tests use **vitest** + **jsdom** (`npm test`), including `static/js/render/registry.test.js` for registry wiring and renderer escaping. ## Continuous integration diff --git a/static/js/render/registry.js b/static/js/render/registry.js new file mode 100644 index 0000000..c37cbb4 --- /dev/null +++ b/static/js/render/registry.js @@ -0,0 +1,67 @@ +import { renderBashUse } from './tool_use/bash.js'; +import { renderReadUse } from './tool_use/read.js'; +import { renderWriteUse } from './tool_use/write.js'; +import { renderEditUse } from './tool_use/edit.js'; +import { renderGlobUse } from './tool_use/glob.js'; +import { renderGrepUse } from './tool_use/grep.js'; +import { renderTaskUse } from './tool_use/task.js'; +import { renderTodoWriteUse } from './tool_use/todo_write.js'; +import { renderAskUserQuestionUse } from './tool_use/ask_user_question.js'; +import { renderToolUseFallback } from './tool_use/fallback.js'; +import { getToolSummary } from './tool_use/summary.js'; + +import { renderBashResult } from './tool_result/bash.js'; +import { renderFileReadResult } from './tool_result/file_read.js'; +import { renderFileEditResult } from './tool_result/file_edit.js'; +import { renderFileWriteResult } from './tool_result/file_write.js'; +import { renderGlobResult } from './tool_result/glob.js'; +import { renderGrepResult } from './tool_result/grep.js'; +import { renderWebSearchResult } from './tool_result/web_search.js'; +import { renderWebFetchResult } from './tool_result/web_fetch.js'; +import { renderTaskResult } from './tool_result/task.js'; +import { renderTodoWriteResult } from './tool_result/todo_write.js'; +import { renderUserInputResult } from './tool_result/user_input.js'; +import { renderPlanResult } from './tool_result/plan.js'; +import { renderToolResultFallback } from './tool_result/fallback.js'; +import { toolResultHasBody } from './tool_result/utils.js'; + +export { getToolSummary, toolResultHasBody }; + +export const TOOL_USE_RENDERERS = { + Bash: renderBashUse, + Read: renderReadUse, + Write: renderWriteUse, + Edit: renderEditUse, + Glob: renderGlobUse, + Grep: renderGrepUse, + Task: renderTaskUse, + TodoWrite: renderTodoWriteUse, + AskUserQuestion: renderAskUserQuestionUse, +}; + +export const TOOL_RESULT_RENDERERS = { + bash: renderBashResult, + file_read: renderFileReadResult, + file_edit: renderFileEditResult, + file_write: renderFileWriteResult, + glob: renderGlobResult, + grep: renderGrepResult, + web_search: renderWebSearchResult, + web_fetch: renderWebFetchResult, + task: renderTaskResult, + todo_write: renderTodoWriteResult, + user_input: renderUserInputResult, + plan: renderPlanResult, +}; + +export function renderToolUse(tool) { + const name = tool.name || 'unknown'; + const fn = TOOL_USE_RENDERERS[name] ?? renderToolUseFallback; + return fn(tool); +} + +export function renderToolResult(parsed) { + const rt = parsed.result_type || 'unknown'; + const fn = TOOL_RESULT_RENDERERS[rt] ?? renderToolResultFallback; + return fn(parsed); +} diff --git a/static/js/render/registry.test.js b/static/js/render/registry.test.js new file mode 100644 index 0000000..2eba6e2 --- /dev/null +++ b/static/js/render/registry.test.js @@ -0,0 +1,99 @@ +import { describe, it, expect } from 'vitest'; +import { + TOOL_USE_RENDERERS, + TOOL_RESULT_RENDERERS, + renderToolUse, + renderToolResult, + getToolSummary, +} from './registry.js'; + +const CORE_TOOL_USE = ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Task', 'TodoWrite', 'AskUserQuestion']; + +const CORE_TOOL_RESULT = [ + 'bash', + 'file_read', + 'file_edit', + 'file_write', + 'glob', + 'grep', + 'web_search', + 'web_fetch', + 'task', + 'todo_write', + 'user_input', + 'plan', +]; + +describe('TOOL_USE_RENDERERS', () => { + it('registers core tool names', () => { + for (const name of CORE_TOOL_USE) { + expect(TOOL_USE_RENDERERS[name], name).toBeTypeOf('function'); + } + }); + + it('renderBashUse escapes HTML in command', () => { + const html = renderToolUse({ + name: 'Bash', + input: { command: '' }, + }); + expect(html).not.toContain('