1import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
2
3/**
4 * What the floating pill says. Connection, syncing, and working share one
5 * element so the label swaps in place instead of one pill fading out for
6 * another. The connection variant is tappable and triggers a reconnect.
7 */
8export type FloatingWorkingStatus =
9 | { readonly kind: "working"; readonly startedAt: string }
10 | { readonly kind: "syncing"; readonly label: string }
11 | { readonly kind: "compacting" }
12 | {
13 readonly kind: "child-input";
14 readonly label: string;
15 readonly accessibilityLabel: string;
16 readonly onPress: () => void;
17 }
18 // The turn settled while background work it started still runs. `waiting`
19 // is false when only commands remain, such as a dev server: the agent is done.
20 | {
21 readonly kind: "background";
22 readonly label: string;
23 readonly accessibilityLabel: string;
24 readonly waiting: boolean;
25 }
26 // A native /goal on an idle thread: paused, blocked, complete, or set.
27 | { readonly kind: "goal"; readonly label: string; readonly accessibilityLabel: string }
28 // A task whose thread the server has not created yet: the worktree may
29 // still be checking out, so there is no turn to time.
30 | { readonly kind: "preparing"; readonly label: string }
31 | {
32 readonly kind: "connection";
33 readonly tone: "reconnecting" | "unavailable";
34 readonly label: string;
35 readonly onPress: () => void;
36 };
37
38/**
39 * The pill's connection variant, or null once the environment is connected and
40 * the pill is free to report sync and working state instead.
41 */
42export function connectionFloatingStatus(input: {
43 readonly connectionError: string | null;
44 readonly connectionState: EnvironmentConnectionPhase;
45 readonly environmentLabel: string | null;
46 readonly onReconnect: () => void;
47}): FloatingWorkingStatus | null {
48 const environmentLabel = input.environmentLabel ?? "Environment";
49 const unavailable = (label: string): FloatingWorkingStatus => ({
50 kind: "connection",
51 tone: "unavailable",
52 label,
53 onPress: input.onReconnect,
54 });
55
56 switch (input.connectionState) {
57 case "connecting":
58 case "reconnecting":
59 return {
60 kind: "connection",
61 tone: "reconnecting",
62 label:
63 input.connectionError === null
64 ? `Reconnecting to ${environmentLabel}...`
65 : `Failed to connect. Retrying ${environmentLabel}...`,
66 onPress: input.onReconnect,
67 };
68 case "offline":
69 return unavailable("You are offline");
70 case "unsupported":
71 return unavailable("Client not supported");
72 case "error":
73 return unavailable(
74 input.connectionError
75 ? `Failed to connect to ${environmentLabel}: ${input.connectionError}`
76 : `Failed to connect to ${environmentLabel}`,
77 );
78 case "available":
79 return unavailable(`${environmentLabel} is not connected`);
80 case "connected":
81 return null;
82 }
83}