diff --git a/research/trace-workbench/README.md b/research/trace-workbench/README.md index 71dea925b9..2e8b7f679a 100644 --- a/research/trace-workbench/README.md +++ b/research/trace-workbench/README.md @@ -37,3 +37,4 @@ Current UI capabilities: - Failed tool calls are marked in every view (red rail + `← error` chip in Trajectory, red bar in Waterfall, `Tool failed` in Chat). - Panes resize by dragging the dividers (clamped; widths persist), and all panes squeeze each other in one layer. - URL carries `?session=&view=&sel=` so any selection is a shareable deep link. +- The header's 「在 Finder 中显示」 button reveals the current session's `.jsonl` in the OS file manager (`open -R` / `explorer /select` / `xdg-open`). diff --git a/research/trace-workbench/app.js b/research/trace-workbench/app.js index fe9ce6e262..d8471e929f 100644 --- a/research/trace-workbench/app.js +++ b/research/trace-workbench/app.js @@ -442,6 +442,8 @@ function renderChrome() { ? `` : '' } + const reveal = $('#revealSession') + if (reveal) reveal.title = session.header.path ?? '' } /* ── Trajectory: step-grouped rows + navigation tree ───────────────────────── @@ -1510,6 +1512,12 @@ document.addEventListener('click', async (event) => { return } + if (event.target.closest('#revealSession')) { + const result = await postJson(`/api/sessions/${encodeURIComponent(state.session.header.id)}/reveal`, {}) + toast(result.ok ? '已在文件管理器中显示' : result.error ?? '无法定位文件') + return + } + if (event.target.closest('#newSessionButton')) { const result = await postJson('/api/sessions', {}) toast(result.error ?? 'New session created') diff --git a/research/trace-workbench/index.html b/research/trace-workbench/index.html index f71086c5fd..34f29e966d 100644 --- a/research/trace-workbench/index.html +++ b/research/trace-workbench/index.html @@ -36,6 +36,7 @@ replay Loading session... +
diff --git a/research/trace-workbench/server.js b/research/trace-workbench/server.js index f8ce6a29b1..53f899ea91 100644 --- a/research/trace-workbench/server.js +++ b/research/trace-workbench/server.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +const { execFile } = require('node:child_process') const fs = require('node:fs') const http = require('node:http') const path = require('node:path') @@ -167,6 +168,15 @@ function summarize(file) { } } +// Reveal a file in the OS file manager. The path always comes from +// findSessionFile (never from the request), and execFile takes an argv array — +// no shell, no injection surface. +function revealInFileManager(file) { + if (process.platform === 'darwin') execFile('open', ['-R', file], () => {}) + else if (process.platform === 'win32') execFile('explorer', [`/select,${file}`], () => {}) + else execFile('xdg-open', [path.dirname(file)], () => {}) +} + function findSessionFile(id) { const files = walkJsonl(SESSIONS_ROOT) const byName = files.find(file => path.basename(file, '.jsonl') === id) @@ -433,6 +443,13 @@ const server = http.createServer(async (req, res) => { if (req.method === 'GET') return send(res, 200, { feedback: readFeedback(id) }) if (req.method === 'POST') return send(res, 200, { feedback: appendFeedback(id, await readJsonBody(req)) }) } + if (req.method === 'POST' && suffix.endsWith('/reveal')) { + const id = suffix.slice(0, -'/reveal'.length) + const file = findSessionFile(id) + if (!file) return send(res, 404, { error: `session not found: ${id}` }) + revealInFileManager(file) + return send(res, 200, { ok: true, path: file }) + } if (req.method === 'POST' && suffix.endsWith('/messages')) { return send(res, 501, { error: 'Live interaction is not connected yet. This prototype is reading persisted JSONL replay data.', diff --git a/research/trace-workbench/styles.css b/research/trace-workbench/styles.css index 12be3efebe..7d9c144e0e 100644 --- a/research/trace-workbench/styles.css +++ b/research/trace-workbench/styles.css @@ -790,6 +790,23 @@ h4 { color: #2456b3; } +.reveal-btn { + flex: 0 0 auto; + border: 1px solid var(--line); + border-radius: 999px; + padding: 3px 10px; + background: #fff; + color: #4c586c; + font-size: 11px; + white-space: nowrap; +} + +.reveal-btn:hover { + border-color: #b7d5fb; + background: var(--blue-soft); + color: #2456b3; +} + .metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));