| 1 | import 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 | */ |
| 8 | export 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 | */ |
| 42 | export 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 | } |