1import {
2 WORKTREE_SETUP_ACTIVITY_KIND,
3 WorktreeSetupSnapshot,
4 type ThreadId,
5} from "@t3tools/contracts";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8
9const decodeWorktreeSetupSnapshot = Schema.decodeUnknownOption(WorktreeSetupSnapshot);
10
11/**
12 * The worktree setup the server recorded on the thread, if any: running once
13 * the bootstrap created the thread, then the settled outcome. It is what a
14 * reload or a second client renders, and what tells them to attach the live
15 * stream while it still says running.
16 */
17export function findRecordedWorktreeSetup(
18 activities: ReadonlyArray<{ readonly kind: string; readonly payload: unknown }>,
19 threadId: ThreadId,
20): WorktreeSetupSnapshot | null {
21 for (let index = activities.length - 1; index >= 0; index -= 1) {
22 const activity = activities[index]!;
23 if (activity.kind !== WORKTREE_SETUP_ACTIVITY_KIND) continue;
24 const decoded = decodeWorktreeSetupSnapshot(activity.payload);
25 if (Option.isSome(decoded) && decoded.value.threadId === threadId) return decoded.value;
26 }
27 return null;
28}
29
30/**
31 * Which setup snapshot the timeline shows, if any. The live stream wins while
32 * it has a newer sequence; the recorded activity covers everything else. A
33 * running setup always shows. The setup belongs to the thread's first turn:
34 * once the user has sent a follow-up it is history and nothing about it is
35 * shown again, whatever its outcome. Within that first turn, a clean finish
36 * leaves no trace once the turn is live (the setup is a means to the reply,
37 * not part of the conversation), while a failed script, a failed setup, or a
38 * cancelled one stays so the outcome, exit code, and terminal are reachable.
39 * Before the turn is live everything stays so nothing collapses in the
40 * handoff gap. Visibility never depends on whether a turn happens to be
41 * running, which would make the row come and go.
42 */
43export function resolveVisibleWorktreeSetup(input: {
44 live: WorktreeSetupSnapshot | null;
45 recorded: WorktreeSetupSnapshot | null;
46 turnStarted: boolean;
47 /** The user sent a message after the one that created the worktree. */
48 followUpSent: boolean;
49}): WorktreeSetupSnapshot | null {
50 const snapshot =
51 input.live && (!input.recorded || input.live.sequence >= input.recorded.sequence)
52 ? input.live
53 : input.recorded;
54 if (!snapshot) return null;
55 if (snapshot.phase === "running") return snapshot;
56 if (input.followUpSent) return null;
57 if (snapshot.phase !== "done") return snapshot;
58 if (!input.turnStarted) return snapshot;
59 return snapshot.stages.some((stage) => stage.status === "failed") ? snapshot : null;
60}
61
62export function worktreeSetupAgentStarted(snapshot: WorktreeSetupSnapshot): boolean {
63 return snapshot.stages.some((stage) => stage.id === "agent" && stage.status === "done");
64}