Skip to content

Commit a73519b

Browse files
phodalQoder-AI
andcommitted
feat(studio): mount the React Inspector workbench as the primary Sessions surface
Sessions previously reused only Inspector's session discovery and rendered a compact catalog, while the full workbench lived in a standalone HTML page whose `workbench.js` Studio executed inside a scoped shadow-DOM runtime. Rebuild the complete Inspector interaction natively in React: Capability/Date navigation, scope metrics, evidence workbench cards, and the retained Session drawer are now React-owned state and components, so Studio no longer loads or evals the legacy Inspector script. The original Inspector stylesheet stays the visual contract, and the compact catalog remains an explicit secondary Catalog & Compare view via an accessible, keyboard-usable tab switch. Build a bounded HarnessInspectorReportV1 during workspace discovery from the same normalized provider sessions (commits, checkpoints, correlation, and the workspace Feature Tree), retain it only in the server-side active workspace, and serve it through a workspace-scoped `GET /api/workspace-inspector-report` with `Cache-Control: no-store`. A new `workspaceWorkbenchEnabled` config flag drives the default surface and falls back to the catalog with an actionable status when structured data is unavailable. Drop the `workbench.js` build copy. Spec: docs/specs/2026-08-20-harness-studio-local-web-workspace.md Test: studio typecheck clean; 121 unit + 16 Playwright pass; 1412 repo tests pass Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
1 parent 4a601c7 commit a73519b

12 files changed

Lines changed: 557 additions & 314 deletions

docs/specs/2026-08-20-harness-studio-local-web-workspace.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ surface exists.
5757
server-only directory as `cwd` and the default capability source root; no
5858
absolute path is returned to the browser and no scripted demo executor is
5959
substituted in production.
60+
- **D-9: Sessions reuses the Inspector workbench instead of only its
61+
discovery.** The workspace provider returns the same privacy-filtered
62+
`HarnessInspectorReportV1` projection consumed by
63+
`scripts/harness-inspector/ui/workbench.html`. Studio rewrites that complete
64+
workbench interaction in React: Capability/Date navigation, scope metrics,
65+
evidence workbench cards, and the retained Session drawer are React-owned
66+
state and components. The original Inspector stylesheet/class contract stays
67+
the visual source, while `workbench.js` remains only the standalone HTML
68+
renderer and is never executed by Studio. The compact catalog remains an
69+
explicit secondary view for two-Session selection and comparison.
6070

6171
## Acceptance Scenarios
6272

@@ -96,12 +106,23 @@ surface exists.
96106
executes through the real built-in Qoder adapter in the selected workspace.
97107
A caller-supplied harness/runtime remains authoritative, and a Studio server
98108
without workspace discovery does not silently acquire a runnable endpoint.
109+
- **AC-13:** After selecting a project workspace, Sessions defaults to the
110+
native Harness Inspector workbench backed by the workspace's structured
111+
report. Capability/date navigation, scope metrics, workbench rows, and the
112+
retained Session drawer execute as React components matching the complete
113+
`workbench.html` design; Studio does not load or execute `workbench.js`. A visible,
114+
keyboard-usable view switch exposes Catalog & Compare without losing the
115+
active workspace or requiring a restart/CLI argument. When a provider cannot
116+
supply structured Inspector data, Studio falls back to the catalog with an
117+
actionable status rather than an empty workbench.
99118

100119
## Non-goals
101120

102121
- Treating observational Session Compare as an experiment verdict.
103122
- Uploading an entire repository or agent home to the loopback server.
104123
- Reimplementing host transcript discovery inside the Studio package or UI.
124+
- Maintaining an independent visual language for the React Inspector; the
125+
standalone `workbench.html` and its stylesheet remain the design contract.
105126
- Editing, replaying, or writing back into imported sessions.
106127
- Shipping the public `better-harness web` package boundary in this first UI
107128
migration; the server and packaged-app ownership must be resolved first.
@@ -148,6 +169,15 @@ bind execution to the selected workspace on the server, label the active
148169
default in the UI, and exercise the same AG-UI/live-run path as configured
149170
harnesses.
150171

172+
### 7. Mount Inspector as the primary Sessions workbench
173+
174+
Build a bounded `HarnessInspectorReportV1` from the same normalized provider
175+
sessions during workspace discovery, retain it only in the server-side active
176+
workspace, and expose it through a workspace-scoped structured endpoint. Rewrite
177+
the complete Inspector workbench UI as React components using the standalone
178+
HTML/CSS contract, with no `workbench.js` execution, plus an accessible
179+
Inspector/Catalog & Compare switch and a catalog fallback.
180+
151181
## Test and Review Evidence
152182

153183
- AC-1/AC-8: empty-start model and Playwright assertions for UI copy and actions.
@@ -168,6 +198,13 @@ harnesses.
168198
deterministic executor while asserting the selected workspace `cwd` and
169199
source root; Playwright opens Debugger after workspace discovery and completes
170200
a live run through the default AG-UI path.
201+
- AC-13: provider and server tests assert a privacy-filtered structured
202+
workspace report, Git/Session correlation, and no absolute-path disclosure.
203+
Playwright verifies the React-owned Inspector workbench is the default
204+
Sessions surface, opens a retained Session through its Date navigator,
205+
switches to Catalog & Compare, repeats wide/compact/narrow overflow plus
206+
console/page-error checks, and asserts that Studio never requests the legacy
207+
`workbench.js` runtime.
171208

172209
Implementation evidence (2026-08-20):
173210

@@ -178,10 +215,12 @@ Implementation evidence (2026-08-20):
178215
passed, including the workspace intake, discovered Session detail, Compare,
179216
the default live Debugger flow, animated discovery status,
180217
wide/compact/narrow screenshots, keyboard focus, overflow, console, and page
181-
error checks.
218+
error checks. The Sessions flow also exercises the React Inspector drawer and
219+
asserts that no legacy Inspector script is loaded.
182220
- `npm test` at the repository root: 99 files, 1412 tests passed.
183221
- A live in-process discovery smoke against this repository returned the bounded
184-
100-Session catalog: Qoder 79, Codex 16, and Claude 5.
222+
React Inspector report with 100 Sessions and 50 commits: Qoder 79, Codex 16,
223+
and Claude 5.
185224

186225
### Risks
187226

@@ -197,3 +236,6 @@ Implementation evidence (2026-08-20):
197236
return an actionable unavailable state when no chooser exists.
198237
- Large histories can exhaust memory. Discovery retains Inspector's global
199238
Session bound and hydrates only selected recent candidates.
239+
- The Inspector workbench is a substantial retained-evidence renderer. Studio
240+
must reuse its assets lazily and keep the compact catalog available if the
241+
structured report or client runtime cannot be mounted.

packages/harness-studio/scripts/build-app.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ await Promise.all([
3131
copyFile(join(packageRoot, "src", "app", "styles", file), join(appDir, "assets", file)),
3232
),
3333
copyFile(join(inspectorAssetRoot, "workbench.css"), join(appDir, "assets", "inspector-workbench.css")),
34-
copyFile(join(inspectorAssetRoot, "workbench.js"), join(appDir, "assets", "inspector-workbench.js")),
3534
]);
3635
process.stdout.write(`Built studio app into ${appDir}\n`);
3736
process.exit(0);

packages/harness-studio/scripts/inspector-workspace-provider.mjs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
1+
import { readFile } from "node:fs/promises";
12
import path from "node:path";
23

34
import {
5+
attachCheckpointFactsToSessions,
6+
collectCommitFacts,
7+
collectEntireCheckpointFacts,
48
collectMultiPlatformSessionSummaries,
9+
correlateCommitsWithSessions,
510
resolveRepoRoot,
611
} from "../../../scripts/commit-session-link/index.mjs";
712
import { SUPPORTED_SESSION_PROVIDERS } from "../../../scripts/session-analysis/index.mjs";
13+
import {
14+
buildHarnessInspectorReport,
15+
emptyFeatureTree,
16+
parseFeatureTreeMarkdown,
17+
} from "../../../scripts/harness-inspector/index.mjs";
818

919
const MAX_SESSIONS = 100;
20+
const MAX_COMMITS = 50;
1021

1122
/**
1223
* Repository integration adapter: reuse Inspector's real provider discovery in
1324
* process and project its privacy-safe summaries into Studio's Session model.
1425
*/
1526
export function createInspectorWorkspaceSessionProvider({
1627
collect = collectMultiPlatformSessionSummaries,
28+
collectCommits = collectCommitFacts,
29+
collectCheckpoints = collectEntireCheckpointFacts,
30+
correlate = correlateCommitsWithSessions,
1731
repoRootFor = resolveRepoRoot,
1832
platforms = SUPPORTED_SESSION_PROVIDERS,
1933
} = {}) {
@@ -28,9 +42,44 @@ export function createInspectorWorkspaceSessionProvider({
2842
includeToolTrace: true,
2943
includeDialogue: true,
3044
});
45+
const { commits } = collectCommits({ workspace: repoRoot, limit: MAX_COMMITS });
46+
const checkpointResolution = collectCheckpoints({ repoRoot, commits });
47+
const sessionsWithCheckpoints = attachCheckpointFactsToSessions(sessions, checkpointResolution.checkpoints);
48+
const correlated = correlate(commits, sessionsWithCheckpoints);
49+
const filesByCommit = new Map(commits.map((commit) => [commit.hash, commit.files]));
50+
const correlation = {
51+
...correlated,
52+
commits: correlated.commits.map((commit) => ({
53+
...commit,
54+
files: filesByCommit.get(commit.hash) ?? [],
55+
})),
56+
};
57+
const { featureTree, diagnostics } = await loadWorkspaceFeatureTree(repoRoot);
58+
const inspectorReport = buildHarnessInspectorReport({
59+
repoRoot,
60+
featureTree,
61+
sessions: sessionsWithCheckpoints,
62+
correlation,
63+
providers,
64+
filters: {
65+
platform: "all",
66+
since: null,
67+
until: null,
68+
stage: null,
69+
commitLimit: MAX_COMMITS,
70+
sessionLimit: MAX_SESSIONS,
71+
},
72+
diagnostics: [
73+
...diagnostics,
74+
...(checkpointResolution.unresolved.length > 0
75+
? [`${checkpointResolution.unresolved.length} Entire checkpoint link(s) could not be resolved locally.`]
76+
: []),
77+
],
78+
});
3179
return {
3280
label: path.basename(repoRoot),
33-
sessions: sessions.map(projectInspectorSession).filter(Boolean),
81+
inspectorReport,
82+
sessions: sessionsWithCheckpoints.map(projectInspectorSession).filter(Boolean),
3483
providers: providers.map((provider) => ({
3584
provider: provider.platform,
3685
status: provider.status,
@@ -43,6 +92,24 @@ export function createInspectorWorkspaceSessionProvider({
4392
};
4493
}
4594

95+
async function loadWorkspaceFeatureTree(repoRoot) {
96+
const featureTreePath = path.join(repoRoot, ".better-harness", "feature-tree.md");
97+
try {
98+
return {
99+
featureTree: parseFeatureTreeMarkdown(await readFile(featureTreePath, "utf8"), { source: "workspace feature tree" }),
100+
diagnostics: [],
101+
};
102+
} catch (error) {
103+
if (error && typeof error === "object" && error.code === "ENOENT") {
104+
return { featureTree: emptyFeatureTree(), diagnostics: [] };
105+
}
106+
return {
107+
featureTree: emptyFeatureTree(),
108+
diagnostics: ["The workspace Feature Tree could not be parsed; Date mode remains available."],
109+
};
110+
}
111+
}
112+
46113
function projectInspectorSession(summary) {
47114
const savedAt = validTimestamp(summary.lastSeen) ?? validTimestamp(summary.firstSeen);
48115
if (!savedAt || !summary.sessionId || !summary.platform) return null;

packages/harness-studio/src/app/App.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { DebuggerSession } from "./session-debugger-model.js";
1919
import { useRovingFocus } from "./roving-tablist.js";
2020

2121
const StudioDiff = lazy(() => import("./StudioDiff.js"));
22+
const InspectorWorkbench = lazy(async () => ({ default: (await import("./InspectorWorkbench.js")).InspectorWorkbench }));
2223
import {
2324
capabilitySummary,
2425
compareSurfaces,
@@ -63,6 +64,7 @@ const EMPTY_CONFIG: StudioConfig = {
6364
harnessMode: "none",
6465
historyEnabled: false,
6566
inspectorEnabled: false,
67+
workspaceWorkbenchEnabled: false,
6668
workspaceDiscoveryEnabled: false,
6769
workspaceConnected: false,
6870
sessionCount: 0,
@@ -349,7 +351,7 @@ function Overview(props: { config: StudioConfig; onOpen: (area: StudioArea) => v
349351
const inputs = [
350352
["Project workspace", props.config.workspaceConnected],
351353
["Artifact catalog", props.config.artifactsEnabled],
352-
["Inspector report", props.config.inspectorEnabled],
354+
["Inspector workbench", props.config.workspaceWorkbenchEnabled || props.config.inspectorEnabled],
353355
["Harness runtime", props.config.aguiEnabled],
354356
["Experiment manifest", props.config.experimentEnabled],
355357
["Compare evidence", props.config.evidenceEnabled],
@@ -397,6 +399,9 @@ function SessionsWorkspace(props: {
397399
const [detail, setDetail] = useState<DebuggerSession>();
398400
const [failure, setFailure] = useState<string>();
399401
const [detailFailure, setDetailFailure] = useState<string>();
402+
const [surface, setSurface] = useState<"inspector" | "catalog">(
403+
props.config.workspaceWorkbenchEnabled ? "inspector" : "catalog",
404+
);
400405

401406
useEffect(() => {
402407
if (!props.config.workspaceConnected) return;
@@ -462,7 +467,7 @@ function SessionsWorkspace(props: {
462467
if (sessions === undefined) return <p className="artifact-status" role="status">Indexing sessions…</p>;
463468

464469
const pair = [...compareIds];
465-
return <section className="session-browser-workspace" aria-label="Project workspace sessions">
470+
const catalog = <section className="session-browser-workspace" aria-label="Project workspace sessions">
466471
<aside className="session-catalog-pane">
467472
<header><div><small>Local workspace</small><h2 title={workspaceLabel}>{workspaceLabel}</h2></div><span>{sessions.length}</span></header>
468473
{omittedCount > 0 && <p className="session-omissions">{omittedCount} unsupported or malformed file{omittedCount === 1 ? "" : "s"} omitted.</p>}
@@ -480,6 +485,24 @@ function SessionsWorkspace(props: {
480485
: <SessionDetail session={detail} />}
481486
</main>
482487
</section>;
488+
489+
if (!props.config.workspaceWorkbenchEnabled) return catalog;
490+
return <section className="session-workbench-stack" aria-label="Workspace Session evidence">
491+
<header className="session-workbench-toolbar">
492+
<div><strong>{workspaceLabel}</strong><span>Inspector-owned workspace evidence</span></div>
493+
<div className="session-surface-tabs" role="tablist" aria-label="Session views">
494+
<button id="session-tab-inspector" type="button" role="tab" aria-controls="session-workbench-panel" aria-selected={surface === "inspector"} tabIndex={surface === "inspector" ? 0 : -1} className={surface === "inspector" ? "selected" : undefined} onClick={() => setSurface("inspector")} onKeyDown={(event) => { if (event.key === "ArrowRight") { event.preventDefault(); setSurface("catalog"); (event.currentTarget.nextElementSibling as HTMLButtonElement | null)?.focus(); } }}>Inspector</button>
495+
<button id="session-tab-catalog" type="button" role="tab" aria-controls="session-workbench-panel" aria-selected={surface === "catalog"} tabIndex={surface === "catalog" ? 0 : -1} className={surface === "catalog" ? "selected" : undefined} onClick={() => setSurface("catalog")} onKeyDown={(event) => { if (event.key === "ArrowLeft") { event.preventDefault(); setSurface("inspector"); (event.currentTarget.previousElementSibling as HTMLButtonElement | null)?.focus(); } }}>Catalog &amp; Compare</button>
496+
</div>
497+
</header>
498+
<div id="session-workbench-panel" className="session-workbench-surface" role="tabpanel" aria-labelledby={surface === "inspector" ? "session-tab-inspector" : "session-tab-catalog"}>
499+
{surface === "inspector"
500+
? <Suspense fallback={<p className="artifact-status" role="status">Loading Inspector workbench…</p>}>
501+
<InspectorWorkbench reportUrl="api/workspace-inspector-report" fallback={catalog} />
502+
</Suspense>
503+
: catalog}
504+
</div>
505+
</section>;
483506
}
484507

485508
function SessionDetail({ session }: { session: DebuggerSession }): React.JSX.Element {

0 commit comments

Comments
 (0)