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));