@workos/quickstudy 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -231,6 +231,27 @@ quickstudy export <run-id> --out ./site
231
231
  npx gh-pages -d site # or push ./site to any static host
232
232
  ```
233
233
 
234
+ ### Embedding the viewers
235
+
236
+ A host that renders exported bundles in its own UI can use this UI's transcript and
237
+ diff viewers directly instead of copying them:
238
+
239
+ ```tsx
240
+ import { DiffViewer, TranscriptViewer } from "@workos/quickstudy/ui/viewers";
241
+ import "@workos/quickstudy/ui/viewers.css";
242
+
243
+ // Fetch data/runs/<run>/attempts/<id>.transcript.txt yourself (the bundle lazy-loads it
244
+ // the same way), then render it; `lines` highlights and scrolls to a cited range.
245
+ <div className="my-theme"><TranscriptViewer text={text} lines={[21, 60]} /></div>
246
+ ```
247
+
248
+ The components import React only (an optional peer dependency). Every rule in
249
+ `viewers.css` is scoped under the components' root class `.quickstudy-viewer`, so importing
250
+ it restyles nothing else in the host. It reads the theme through custom properties the host sets on a wrapping element: `--surface`, `--bg`,
251
+ `--line`, `--muted`, `--accent`, `--green`, `--amber`, `--amber-bg`, `--green-bg`,
252
+ `--red-bg`, `--gray-bg` and `--mono`. Turn parsing, roles, collapsing and the `L<n>`
253
+ anchors are exactly what this UI shows, so evidence links carry over unchanged.
254
+
234
255
  ## Development
235
256
 
236
257
  Run the CLI from source as `bun src/cli.ts <cmd>` (what `quickstudy` is,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/quickstudy",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "An open-source, company-agnostic eval harness for measuring whether coding agents can complete real integration tasks, across an agent x feature x framework x surface matrix.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,9 @@
9
9
  },
10
10
  "exports": {
11
11
  ".": "./src/index.ts",
12
- "./package.json": "./package.json"
12
+ "./package.json": "./package.json",
13
+ "./ui/viewers": "./ui/src/viewers.ts",
14
+ "./ui/viewers.css": "./ui/src/components/viewers.css"
13
15
  },
14
16
  "files": [
15
17
  "src",
@@ -17,7 +19,11 @@
17
19
  "images",
18
20
  "examples",
19
21
  "README.md",
20
- "LICENSE"
22
+ "LICENSE",
23
+ "ui/src/viewers.ts",
24
+ "ui/src/components/TranscriptViewer.tsx",
25
+ "ui/src/components/DiffViewer.tsx",
26
+ "ui/src/components/viewers.css"
21
27
  ],
22
28
  "engines": {
23
29
  "bun": ">=1.1"
@@ -49,5 +55,13 @@
49
55
  "react-dom": "^19.3.0",
50
56
  "typescript": "^7.0.2",
51
57
  "vite": "^8.3.0"
58
+ },
59
+ "peerDependencies": {
60
+ "react": ">=18"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "react": {
64
+ "optional": true
65
+ }
52
66
  }
53
67
  }
@@ -9,7 +9,7 @@ export const EMBEDDED_BUILD_INFO: {
9
9
  // (the `x-release-please-version` marker anchors the replacement). It is
10
10
  // restored as-is after a binary build (scripts/build-binary.ts), so the
11
11
  // committed source `--version` stays in lockstep with package.json.
12
- version: "0.2.0", // x-release-please-version
12
+ version: "0.3.0", // x-release-please-version
13
13
  revision: null,
14
14
  dirty: null,
15
15
  sourceHash: null,
@@ -246,8 +246,13 @@ export async function createAttemptContainer(opts: CreateAttemptContainerOptions
246
246
  execInWorkspace: async (cmd, execOpts = {}) => {
247
247
  const result = await runDocker(["exec", "-w", CONTAINER_WORKSPACE, id, ...cmd], execOpts);
248
248
  // Killing only the Docker client leaves the exec process alive in the
249
- // daemon. Stop the whole attempt on timeout, including descendants.
250
- if (result.timedOut) await runDocker(["kill", id], { timeoutMs: 10_000 });
249
+ // daemon. Stop the whole attempt on timeout, including descendants —
250
+ // then restart PID 1 (`sleep infinity`) so the dead attempt's workspace
251
+ // stays exec-able for the diff. Descendants do not come back with it.
252
+ if (result.timedOut) {
253
+ await runDocker(["kill", id], { timeoutMs: 10_000 });
254
+ await runDocker(["start", id], { timeoutMs: 10_000 });
255
+ }
251
256
  return result;
252
257
  },
253
258
  writeFile: async (path, content) => {
@@ -826,8 +826,26 @@ async function runContainerAttemptV3(
826
826
  );
827
827
  }
828
828
  }
829
+ // `git add -A` stages new files so the diff captures them; diffing
830
+ // the recorded baseline SHA (not HEAD) also captures any commits the
831
+ // agent made itself.
832
+ const captureDiff = async (): Promise<void> => {
833
+ if (baselineSha === undefined) return;
834
+ await mustExec(container, ["git", "add", "-A"]);
835
+ const diff = (await mustExec(container, ["git", "diff", baselineSha])).stdout;
836
+ persisted.diffRef = await artifacts.writeDiff(runId, attemptId, redactSecrets(diff, secretEnv));
837
+ };
838
+ if (agentResult.timedOut || agentResult.exitCode !== 0) {
839
+ // The workspace is evidence of what the agent got done before it
840
+ // died; a failed capture must not mask the attempt's own outcome.
841
+ try {
842
+ await captureDiff();
843
+ } catch (err) {
844
+ (options.log ?? console.log)(`attempt ${attemptId}: diff unavailable after failure: ${err instanceof Error ? err.message : String(err)}`);
845
+ }
846
+ }
829
847
  if (agentResult.timedOut) {
830
- await artifacts.writeError(runId, attemptId, `attempt timed out after ${args.attemptTimeoutMs}ms — container removed`);
848
+ await artifacts.writeError(runId, attemptId, `attempt timed out after ${args.attemptTimeoutMs}ms — container killed`);
831
849
  return { status: "incomplete", error: `attempt timed out after ${args.attemptTimeoutMs}ms` };
832
850
  }
833
851
  if (agentResult.exitCode !== 0) {
@@ -836,14 +854,7 @@ async function runContainerAttemptV3(
836
854
  return { status: "error", error: `agent command exited ${agentResult.exitCode}` };
837
855
  }
838
856
 
839
- if (baselineSha !== undefined) {
840
- // `git add -A` stages new files so the diff captures them; diffing
841
- // the recorded baseline SHA (not HEAD) also captures any commits
842
- // the agent made itself.
843
- await mustExec(container, ["git", "add", "-A"]);
844
- const diff = (await mustExec(container, ["git", "diff", baselineSha])).stdout;
845
- persisted.diffRef = await artifacts.writeDiff(runId, attemptId, redactSecrets(diff, secretEnv));
846
- }
857
+ await captureDiff();
847
858
 
848
859
  // Export the workspace BEFORE scoring, with the container still alive:
849
860
  // file checks read the export, ctx.exec reaches into the live sandbox.