1// @effect-diagnostics nodeBuiltinImport:off globalTimers:off globalDate:off - This host-side fixture creates an isolated local T3 environment.
2import * as NodeChildProcess from "node:child_process";
3import * as NodeFSP from "node:fs/promises";
4import * as NodePath from "node:path";
5import * as NodeSqlite from "node:sqlite";
6import * as NodeUtil from "node:util";
7
8const execFile = NodeUtil.promisify(NodeChildProcess.execFile);
9
10export const SHOWCASE_PROJECT_ID = "t3code";
11export const SHOWCASE_THREAD_ID = "remote-command-center";
12export const SHOWCASE_TERMINAL_ID = "term-1";
13
14export const SHOWCASE_SCENES = [
15 "threads",
16 "thread",
17 "terminal",
18 "review",
19 "environments",
20 "agent-activity",
21] as const;
22export type ShowcaseScene = (typeof SHOWCASE_SCENES)[number];
23
24const PROJECTOR_NAMES = [
25 "projection.projects",
26 "projection.threads",
27 "projection.thread-messages",
28 "projection.thread-proposed-plans",
29 "projection.thread-activities",
30 "projection.thread-sessions",
31 "projection.thread-turns",
32 "projection.checkpoints",
33 "projection.pending-approvals",
34] as const;
35
36const MODEL_SELECTION = JSON.stringify({ instanceId: "codex", model: "gpt-5.4" });
37const PROJECT_SCRIPTS = JSON.stringify([
38 {
39 id: "dev",
40 name: "Dev",
41 command: "pnpm dev",
42 icon: "play",
43 runOnWorktreeCreate: false,
44 },
45 {
46 id: "test",
47 name: "Tests",
48 command: "pnpm test",
49 icon: "test",
50 runOnWorktreeCreate: false,
51 },
52]);
53
54const SHOWCASE_TERMINAL_PROMPT =
55 "\u001b[1;32m→\u001b[0m \u001b[1;36mt3code\u001b[0m \u001b[1;34mgit:(\u001b[1;31mfeat/remote-command-center\u001b[1;34m)\u001b[0m \u001b[1;33m✗\u001b[0m ";
56
57// A dev-server startup mirroring the web settings' terminal font preview:
58// zsh-style prompt, brand line, addresses, the thread's 612-test summary,
59// and a READY badge, so the scene exercises bold, dim, underline, the six
60// accent colors, and a background cell.
61export const SHOWCASE_TERMINAL_BUFFER = [
62 `${SHOWCASE_TERMINAL_PROMPT}vpr dev`,
63 "",
64 " \u001b[1;32mVITE\u001b[0m \u001b[32mv7.1.1\u001b[0m \u001b[2mready in\u001b[0m \u001b[1m1.24s\u001b[0m",
65 "",
66 " \u001b[32m→\u001b[0m \u001b[2mLocal:\u001b[0m \u001b[4;36mhttp://127.0.0.1:5173/\u001b[0m",
67 " \u001b[32m→\u001b[0m \u001b[2mNetwork:\u001b[0m \u001b[4;36mhttp://192.168.1.24:5173/\u001b[0m",
68 " \u001b[32m→\u001b[0m \u001b[2mProject:\u001b[0m \u001b[1mt3code\u001b[0m \u001b[2m— ~/Code/t3code\u001b[0m",
69 "",
70 " \u001b[32m✓ 612 passed\u001b[0m \u001b[33m△ 2 warnings\u001b[0m \u001b[31m✗ 0 failed\u001b[0m",
71 "",
72 " \u001b[42;30m READY \u001b[0m \u001b[2mwatching for changes — press\u001b[0m \u001b[1mq\u001b[0m \u001b[2mto quit\u001b[0m",
73 "",
74 SHOWCASE_TERMINAL_PROMPT,
75].join("\r\n");
76
77const BASE_ENVIRONMENT_PRESENCE = `export function environmentLabel(count: number): string {
78 return \`${"${count}"} environments\`;
79}
80`;
81
82const UPDATED_ENVIRONMENT_PRESENCE = `const PULSE = ["✦", "✧", "·", "✧"] as const;
83
84export function environmentLabel(connected: number, total: number, frame: number): string {
85 const pulse = PULSE[frame % PULSE.length];
86 return \`${"${pulse} ${connected}/${total}"} ready\`;
87}
88`;
89
90const REMOTE_HANDOFF_CARD = `import { View, Text } from "react-native";
91
92export function RemoteHandoffCard(props: { machine: string; latencyMs: number }) {
93 return (
94 <View className="rounded-2xl bg-surface-2 p-4">
95 <Text className="font-semibold">Ready on {props.machine}</Text>
96 <Text className="text-success">Handoff in {props.latencyMs}ms</Text>
97 </View>
98 );
99}
100`;
101
102const PROJECT_FAVICONS = {
103 t3code: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
104 <rect width="128" height="128" rx="10" fill="#000"/>
105 <path d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z" fill="#fff"/>
106</svg>`,
107 react: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
108 <rect width="64" height="64" rx="15" fill="#20232a"/>
109 <g fill="none" stroke="#61dafb" stroke-width="2.8"><ellipse cx="32" cy="32" rx="25" ry="9"/><ellipse cx="32" cy="32" rx="25" ry="9" transform="rotate(60 32 32)"/><ellipse cx="32" cy="32" rx="25" ry="9" transform="rotate(120 32 32)"/></g>
110 <circle cx="32" cy="32" r="4.8" fill="#61dafb"/>
111</svg>`,
112 linux: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
113 <rect width="64" height="64" rx="15" fill="#f7c948"/>
114 <ellipse cx="32" cy="35" rx="17" ry="22" fill="#202124"/>
115 <ellipse cx="32" cy="40" rx="12" ry="14" fill="#f5f5f2"/>
116 <circle cx="27" cy="24" r="5" fill="white"/><circle cx="37" cy="24" r="5" fill="white"/>
117 <circle cx="28" cy="25" r="2"/><circle cx="36" cy="25" r="2"/>
118 <path d="M27 31l5-4 5 4-5 4z" fill="#f28c28"/><path d="M16 55h14l-7-5zM34 55h14l-7-5z" fill="#f28c28"/>
119</svg>`,
120} as const;
121
122export const SHOWCASE_PROJECTS = [
123 {
124 id: "t3code",
125 title: "T3 Code",
126 directory: "t3code",
127 repositoryUrl: "https://github.com/pingdotgg/t3code.git",
128 favicon: PROJECT_FAVICONS.t3code,
129 },
130 {
131 id: "react",
132 title: "React",
133 directory: "react",
134 repositoryUrl: "https://github.com/facebook/react.git",
135 favicon: PROJECT_FAVICONS.react,
136 },
137 {
138 id: "linux",
139 title: "Linux",
140 directory: "linux",
141 repositoryUrl: "https://github.com/torvalds/linux.git",
142 favicon: PROJECT_FAVICONS.linux,
143 },
144] as const;
145
146export const SHOWCASE_ENVIRONMENTS = [
147 {
148 id: "moonbase-terminal",
149 label: "Moonbase Terminal",
150 projectIds: ["t3code"],
151 },
152 {
153 id: "suspense-station",
154 label: "Suspense Station",
155 projectIds: ["react"],
156 },
157 {
158 id: "kernel-cabin",
159 label: "Kernel Cabin",
160 projectIds: ["linux"],
161 },
162] as const;
163
164export const SHOWCASE_THREADS = [
165 {
166 id: SHOWCASE_THREAD_ID,
167 projectId: "t3code",
168 title: "Make remote coding feel local ✦",
169 branch: "feat/remote-command-center",
170 minutesAgo: 3,
171 request:
172 "Give T3 Code a remote-first command center. Make three machines feel one tap away, keep agent work in sync, and make every handoff feel instant.",
173 response:
174 "T3 Code now treats every machine like it is right here in the room. ✦\n\n- Moonbase, Suspense Station, and Kernel Cabin stay live together\n- Terminal state follows you without losing a single line\n- Agent work remains perfectly in sync across devices\n- Handoffs land before your train of thought can wander\n\nI also ran the changed workspace: **612 tests passed**.",
175 },
176 {
177 id: "pocket-command-center",
178 projectId: "t3code",
179 title: "Put the command center in your pocket",
180 branch: "feat/pocket-command-center",
181 minutesAgo: 21,
182 state: "approval" as const,
183 request: "Make switching between desktop, phone, and tablet feel like one continuous session.",
184 response:
185 "The handoff flow preserves the selected thread, terminal buffer, and working diff. The final motion treatment is ready for approval.",
186 },
187 {
188 id: "buttery-suspense",
189 projectId: "react",
190 title: "Make Suspense transitions buttery",
191 branch: "perf/buttery-suspense",
192 minutesAgo: 12,
193 state: "working" as const,
194 request:
195 "Trace the last few dropped frames in nested Suspense transitions and make them disappear.",
196 response: null,
197 },
198 {
199 id: "hydration-haikus",
200 projectId: "react",
201 title: "Turn hydration warnings into haikus",
202 branch: "dev/hydration-haikus",
203 minutesAgo: 44,
204 request:
205 "Keep hydration errors precise, but make the development copy unexpectedly delightful.",
206 response:
207 "The diagnostics still lead with the exact mismatch and component stack. A tiny optional haiku now closes the expanded explanation.",
208 snoozeMinutes: 90,
209 },
210 {
211 id: "beautiful-boot",
212 projectId: "linux",
213 title: "Make boot logs oddly beautiful",
214 branch: "feat/beautiful-boot",
215 minutesAgo: 34,
216 state: "plan" as const,
217 request:
218 "Design a clearer boot timeline that remains useful over serial and never hides kernel detail.",
219 response:
220 "The plan groups milestones without changing the underlying log stream, preserves plain-text output, and adds zero work to the hot path.",
221 },
222 {
223 id: "patient-penguins",
224 projectId: "linux",
225 title: "Teach penguins to wait patiently",
226 branch: "feat/patient-penguins",
227 minutesAgo: 52,
228 request: "Make delayed work easier to follow without adding noise to the scheduler trace.",
229 response:
230 "Delayed work now carries a concise reason through the trace, so the wait is legible without changing scheduling behavior.",
231 snoozeMinutes: 8 * 60,
232 },
233 // Finished work, settled by hand: the list keeps it as a receded tail so
234 // the active block above reads as everything still in flight. The active
235 // block stays small enough that the settled tail begins above the fold —
236 // a store screenshot has to show that history exists, not just imply it.
237 {
238 id: "handoff-haptics",
239 projectId: "t3code",
240 title: "Tune the handoff haptics",
241 branch: "feat/handoff-haptics",
242 minutesAgo: 5 * 60,
243 settled: true,
244 request: "Give the desktop-to-phone handoff a haptic that lands with the animation.",
245 response:
246 "The handoff now taps once as the thread lands and stays silent on failure, so the phone never celebrates a handoff that did not happen.",
247 },
248 {
249 id: "streaming-shell",
250 projectId: "react",
251 title: "Stream the shell before the data",
252 branch: "feat/streaming-shell",
253 minutesAgo: 28 * 60,
254 settled: true,
255 request: "Get the app shell painted before any data request resolves.",
256 response:
257 "The shell now flushes on first byte and the data boundaries hydrate underneath it, so the first paint no longer waits on the slowest query.",
258 },
259 {
260 id: "quieter-oom",
261 projectId: "linux",
262 title: "Make the OOM killer explain itself",
263 branch: "feat/quieter-oom",
264 minutesAgo: 2 * 24 * 60,
265 settled: true,
266 request: "Make out-of-memory kills legible without adding a single allocation to the hot path.",
267 response:
268 "Kills now report the winning heuristic and the runner-up alongside the usual dump, assembled entirely from data the path already had.",
269 },
270] as const;
271
272function minutesBefore(now: number, minutes: number): string {
273 return new Date(now - minutes * 60_000).toISOString();
274}
275
276async function runGit(workspaceRoot: string, args: ReadonlyArray<string>): Promise<void> {
277 await execFile("git", [...args], {
278 cwd: workspaceRoot,
279 env: {
280 ...process.env,
281 GIT_AUTHOR_NAME: "Alex Rivera",
282 GIT_AUTHOR_EMAIL: "alex@lumen.test",
283 GIT_COMMITTER_NAME: "Alex Rivera",
284 GIT_COMMITTER_EMAIL: "alex@lumen.test",
285 },
286 });
287}
288
289async function initializeRepository(input: {
290 readonly workspaceRoot: string;
291 readonly repositoryUrl: string;
292 readonly commitMessage: string;
293}): Promise<void> {
294 await runGit(input.workspaceRoot, ["init", "-b", "main"]);
295 await runGit(input.workspaceRoot, ["remote", "add", "origin", input.repositoryUrl]);
296 await runGit(input.workspaceRoot, ["add", "."]);
297 await runGit(input.workspaceRoot, ["commit", "-m", input.commitMessage]);
298}
299
300async function seedT3CodeWorkspace(workspaceRoot: string): Promise<void> {
301 await NodeFSP.mkdir(NodePath.join(workspaceRoot, "apps/mobile/src/features/home"), {
302 recursive: true,
303 });
304 await NodeFSP.writeFile(
305 NodePath.join(workspaceRoot, "package.json"),
306 `${JSON.stringify({ name: "t3code", private: true, scripts: { test: "vp test" } }, null, 2)}\n`,
307 );
308 await NodeFSP.writeFile(NodePath.join(workspaceRoot, "favicon.svg"), PROJECT_FAVICONS.t3code);
309 await NodeFSP.writeFile(
310 NodePath.join(workspaceRoot, "apps/mobile/src/features/home/environmentPresence.ts"),
311 BASE_ENVIRONMENT_PRESENCE,
312 );
313 await initializeRepository({
314 workspaceRoot,
315 repositoryUrl: "https://github.com/pingdotgg/t3code.git",
316 commitMessage: "Show connected environments",
317 });
318 await runGit(workspaceRoot, ["checkout", "-b", "feat/remote-command-center"]);
319 await NodeFSP.writeFile(
320 NodePath.join(workspaceRoot, "apps/mobile/src/features/home/environmentPresence.ts"),
321 UPDATED_ENVIRONMENT_PRESENCE,
322 );
323 await NodeFSP.writeFile(
324 NodePath.join(workspaceRoot, "apps/mobile/src/features/home/RemoteHandoffCard.tsx"),
325 REMOTE_HANDOFF_CARD,
326 );
327}
328
329async function seedCompanionWorkspace(input: {
330 readonly workspaceRoot: string;
331 readonly title: string;
332 readonly repositoryUrl: string;
333 readonly favicon: string;
334}): Promise<void> {
335 await NodeFSP.mkdir(input.workspaceRoot, { recursive: true });
336 await NodeFSP.writeFile(NodePath.join(input.workspaceRoot, "favicon.svg"), input.favicon);
337 await NodeFSP.writeFile(
338 NodePath.join(input.workspaceRoot, "README.md"),
339 `# ${input.title}\n\nSeeded by the T3 Code mobile screenshot harness.\n`,
340 );
341 await initializeRepository({
342 workspaceRoot: input.workspaceRoot,
343 repositoryUrl: input.repositoryUrl,
344 commitMessage: `Seed ${input.title} workspace`,
345 });
346}
347
348function insertThread(
349 database: NodeSqlite.DatabaseSync,
350 now: number,
351 input: {
352 readonly id: string;
353 readonly projectId: string;
354 readonly title: string;
355 readonly branch: string;
356 readonly minutesAgo: number;
357 readonly state?: "working" | "approval" | "plan";
358 readonly settled?: boolean;
359 readonly snoozeMinutes?: number;
360 readonly workspaceRoot: string;
361 },
362): void {
363 const turnId = `${input.id}-turn`;
364 const updatedAt = minutesBefore(now, input.minutesAgo);
365 const isWorking = input.state === "working";
366 const snoozedUntil =
367 input.snoozeMinutes === undefined
368 ? null
369 : new Date(now + input.snoozeMinutes * 60_000).toISOString();
370 const snoozedAt =
371 input.snoozeMinutes === undefined
372 ? null
373 : minutesBefore(now, Math.max(1, Math.floor(input.minutesAgo / 2)));
374 database
375 .prepare(
376 `INSERT INTO projection_threads (
377 thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode,
378 branch, worktree_path, latest_turn_id, latest_user_message_at, pending_approval_count,
379 pending_user_input_count, has_actionable_proposed_plan, created_at, updated_at,
380 archived_at, deleted_at, settled_override, settled_at, snoozed_until, snoozed_at
381 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, NULL, NULL, ?, ?, ?, ?)`,
382 )
383 .run(
384 input.id,
385 input.projectId,
386 input.title,
387 MODEL_SELECTION,
388 "full-access",
389 input.state === "plan" ? "plan" : "default",
390 input.branch,
391 input.workspaceRoot,
392 turnId,
393 minutesBefore(now, input.minutesAgo + 1),
394 input.state === "approval" ? 1 : 0,
395 input.state === "plan" ? 1 : 0,
396 minutesBefore(now, input.minutesAgo + 120),
397 updatedAt,
398 input.settled ? "settled" : null,
399 input.settled ? updatedAt : null,
400 snoozedUntil,
401 snoozedAt,
402 );
403 database
404 .prepare(
405 `INSERT INTO projection_turns (
406 thread_id, turn_id, pending_message_id, assistant_message_id, state, requested_at,
407 started_at, completed_at, checkpoint_turn_count, checkpoint_ref, checkpoint_status,
408 checkpoint_files_json, source_proposed_plan_thread_id, source_proposed_plan_id
409 ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, NULL, NULL, NULL, '[]', NULL, NULL)`,
410 )
411 .run(
412 input.id,
413 turnId,
414 isWorking ? null : `${input.id}-answer`,
415 isWorking ? "running" : "completed",
416 minutesBefore(now, input.minutesAgo + 2),
417 minutesBefore(now, input.minutesAgo + 2),
418 isWorking ? null : updatedAt,
419 );
420 database
421 .prepare(
422 `INSERT INTO projection_thread_sessions (
423 thread_id, status, provider_name, provider_instance_id, provider_session_id,
424 provider_thread_id, runtime_mode, active_turn_id, last_error, updated_at
425 ) VALUES (?, ?, 'Codex', 'codex', NULL, NULL, 'full-access', ?, NULL, ?)`,
426 )
427 .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt);
428}
429
430// V1 tables this seed owns. `projection_projects` is not listed: V2 still
431// stores projects there, so the seed upserts its own rows instead. V2 clients
432// do not read the V1 thread rows; moving the seed to V2 is tracked in
433// https://github.com/pingdotgg/t3code/issues/15013.
434const SEEDED_V1_TABLES = [
435 "projection_pending_approvals",
436 "projection_thread_proposed_plans",
437 "projection_thread_activities",
438 "projection_thread_messages",
439 "projection_thread_sessions",
440 "projection_turns",
441 "projection_threads",
442 "projection_state",
443] as const;
444
445const SEEDED_PROJECTION_TABLES = [...SEEDED_V1_TABLES, "projection_projects"] as const;
446
447const SEEDED_THREAD_COLUMNS = ["snoozed_until", "snoozed_at"] as const;
448
449function hasSeedableSchema(dbPath: string): boolean {
450 let database: NodeSqlite.DatabaseSync;
451 try {
452 database = new NodeSqlite.DatabaseSync(dbPath, { readOnly: true });
453 } catch {
454 return false;
455 }
456 try {
457 const tableCount = database
458 .prepare(
459 `SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`,
460 )
461 .get(...SEEDED_PROJECTION_TABLES) as { count: number };
462 if (tableCount.count !== SEEDED_PROJECTION_TABLES.length) return false;
463
464 const threadColumns = database.prepare("PRAGMA table_info(projection_threads)").all() as Array<{
465 name: string;
466 }>;
467 const threadColumnNames = new Set(threadColumns.map((column) => column.name));
468 return SEEDED_THREAD_COLUMNS.every((column) => threadColumnNames.has(column));
469 } catch {
470 return false;
471 } finally {
472 database.close();
473 }
474}
475
476async function waitForSeedableSchema(dbPath: string, timeoutMs = 60_000): Promise<void> {
477 const deadline = Date.now() + timeoutMs;
478 while (Date.now() < deadline) {
479 if (hasSeedableSchema(dbPath)) return;
480 await new Promise((resolve) => setTimeout(resolve, 250));
481 }
482 throw new Error(`The environment server did not migrate ${dbPath} within ${timeoutMs}ms.`);
483}
484
485function seedDatabase(
486 dbPath: string,
487 workspaceRoots: ReadonlyMap<string, string>,
488 projects: ReadonlyArray<(typeof SHOWCASE_PROJECTS)[number]>,
489 threads: ReadonlyArray<(typeof SHOWCASE_THREADS)[number]>,
490 now: number,
491): void {
492 // The environment server is already running against this file and keeps
493 // writing (migrations, projections) while we seed, so the write lock is
494 // genuinely contended — without a busy timeout `BEGIN IMMEDIATE` fails
495 // instantly with SQLITE_BUSY on a loaded machine.
496 const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 });
497 try {
498 database.exec("BEGIN IMMEDIATE");
499 for (const table of SEEDED_V1_TABLES) {
500 database.exec(`DELETE FROM ${table}`);
501 }
502 const insertProject = database.prepare(
503 `INSERT OR REPLACE INTO projection_projects (
504 project_id, title, workspace_root, default_model_selection_json, scripts_json,
505 created_at, updated_at, deleted_at
506 ) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,
507 );
508 for (const [index, project] of projects.entries()) {
509 const workspaceRoot = workspaceRoots.get(project.id);
510 if (!workspaceRoot) throw new Error(`Missing workspace root for ${project.id}.`);
511 const latestThreadMinutes = Math.min(
512 ...threads
513 .filter((thread) => thread.projectId === project.id)
514 .map((thread) => thread.minutesAgo),
515 );
516 insertProject.run(
517 project.id,
518 project.title,
519 workspaceRoot,
520 MODEL_SELECTION,
521 PROJECT_SCRIPTS,
522 minutesBefore(now, 60 * 24 * (90 - index * 12)),
523 minutesBefore(now, latestThreadMinutes),
524 );
525 }
526
527 for (const thread of threads) {
528 const workspaceRoot = workspaceRoots.get(thread.projectId);
529 if (!workspaceRoot) throw new Error(`Missing workspace root for ${thread.projectId}.`);
530 insertThread(database, now, {
531 ...thread,
532 ...("state" in thread ? { state: thread.state } : {}),
533 workspaceRoot,
534 });
535 }
536
537 const insertMessage = database.prepare(
538 `INSERT INTO projection_thread_messages (
539 message_id, thread_id, turn_id, role, text, is_streaming, attachments_json,
540 created_at, updated_at
541 ) VALUES (?, ?, ?, ?, ?, 0, NULL, ?, ?)`,
542 );
543 for (const thread of threads) {
544 const turnId = `${thread.id}-turn`;
545 const requestTime = minutesBefore(now, thread.minutesAgo + 5);
546 insertMessage.run(
547 `${thread.id}-request`,
548 thread.id,
549 turnId,
550 "user",
551 thread.request,
552 requestTime,
553 requestTime,
554 );
555 if (thread.response !== null) {
556 const responseTime = minutesBefore(now, thread.minutesAgo);
557 insertMessage.run(
558 `${thread.id}-answer`,
559 thread.id,
560 turnId,
561 "assistant",
562 thread.response,
563 responseTime,
564 responseTime,
565 );
566 }
567 }
568
569 const turnId = `${SHOWCASE_THREAD_ID}-turn`;
570 const insertActivity = database.prepare(
571 `INSERT INTO projection_thread_activities (
572 activity_id, thread_id, turn_id, tone, kind, summary, payload_json, sequence, created_at
573 ) VALUES (?, ?, ?, 'tool', 'tool.completed', ?, ?, ?, ?)`,
574 );
575 insertActivity.run(
576 "trace-remote-handoff",
577 SHOWCASE_THREAD_ID,
578 turnId,
579 "Traced the remote handoff path",
580 JSON.stringify({
581 itemType: "command_execution",
582 title: "Traced the remote handoff path",
583 detail: "Three environments, one continuous workspace",
584 status: "completed",
585 }),
586 1,
587 minutesBefore(now, 8),
588 );
589 insertActivity.run(
590 "sync-command-center",
591 SHOWCASE_THREAD_ID,
592 turnId,
593 "Synced the command center",
594 JSON.stringify({
595 itemType: "file_change",
596 title: "Synced the command center",
597 detail: "2 files changed · instant handoffs · calm reconnects",
598 status: "completed",
599 }),
600 2,
601 minutesBefore(now, 6),
602 );
603 insertActivity.run(
604 "run-changed-suite",
605 SHOWCASE_THREAD_ID,
606 turnId,
607 "Ran the changed workspace",
608 JSON.stringify({
609 itemType: "command_execution",
610 title: "Ran the changed workspace",
611 detail: "612 tests passed · 3 environments online",
612 status: "completed",
613 }),
614 3,
615 minutesBefore(now, 4),
616 );
617
618 for (const [index, projector] of PROJECTOR_NAMES.entries()) {
619 database
620 .prepare(
621 "INSERT INTO projection_state (projector, last_applied_sequence, updated_at) VALUES (?, ?, ?)",
622 )
623 .run(projector, index + 1, minutesBefore(now, 1));
624 }
625 database.exec("COMMIT");
626 } catch (error) {
627 // A failed BEGIN (or an error SQLite already auto-rolled back) leaves no
628 // transaction, and the rollback's own "cannot rollback" error would then
629 // replace the one that actually explains the failure.
630 try {
631 database.exec("ROLLBACK");
632 } catch {
633 // Nothing to roll back.
634 }
635 throw error;
636 } finally {
637 database.close();
638 }
639}
640
641export async function seedShowcaseEnvironment(input: {
642 readonly baseDir: string;
643 readonly projectIds?: ReadonlyArray<string>;
644 readonly now?: number;
645}): Promise<{ readonly dbPath: string; readonly workspaceRoot: string }> {
646 const now = input.now ?? Date.now();
647 const selectedProjectIds = new Set(
648 input.projectIds ?? SHOWCASE_PROJECTS.map((project) => project.id),
649 );
650 const projects = SHOWCASE_PROJECTS.filter((project) => selectedProjectIds.has(project.id));
651 if (projects.length === 0) throw new Error("At least one showcase project must be selected.");
652 const threads = SHOWCASE_THREADS.filter((thread) => selectedProjectIds.has(thread.projectId));
653 const workspaceBase = NodePath.join(input.baseDir, "workspace");
654 const workspaceRoots = new Map(
655 projects.map(
656 (project) => [project.id, NodePath.join(workspaceBase, project.directory)] as const,
657 ),
658 );
659 const primaryProject =
660 projects.find((project) => project.id === SHOWCASE_PROJECT_ID) ?? projects[0];
661 if (!primaryProject) throw new Error("The primary showcase workspace is not configured.");
662 const workspaceRoot = workspaceRoots.get(primaryProject.id);
663 if (!workspaceRoot) throw new Error("The primary showcase workspace is not configured.");
664 const dbPath = NodePath.join(input.baseDir, "userdata", "statev2.sqlite");
665 if (primaryProject.id === SHOWCASE_PROJECT_ID) {
666 await seedT3CodeWorkspace(workspaceRoot);
667 }
668 await Promise.all(
669 projects
670 .filter((project) => project.id !== SHOWCASE_PROJECT_ID)
671 .map(async (project) => {
672 const projectWorkspaceRoot = workspaceRoots.get(project.id);
673 if (!projectWorkspaceRoot) throw new Error(`Missing workspace root for ${project.id}.`);
674 await seedCompanionWorkspace({
675 workspaceRoot: projectWorkspaceRoot,
676 title: project.title,
677 repositoryUrl: project.repositoryUrl,
678 favicon: project.favicon,
679 });
680 }),
681 );
682 // The environment server begins listening before it finishes migrating the
683 // database, so wait for the schema before deleting from and reseeding it.
684 await waitForSeedableSchema(dbPath);
685 seedDatabase(dbPath, workspaceRoots, projects, threads, now);
686
687 const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals");
688 if (selectedProjectIds.has(SHOWCASE_PROJECT_ID)) {
689 const safeThreadId = Buffer.from(SHOWCASE_THREAD_ID).toString("base64url");
690 await NodeFSP.mkdir(terminalDirectory, { recursive: true });
691 await NodeFSP.writeFile(
692 NodePath.join(terminalDirectory, `terminal_${safeThreadId}.log`),
693 SHOWCASE_TERMINAL_BUFFER,
694 );
695 }
696 return { dbPath, workspaceRoot };
697}