| 92 | * This hook derives `enabled` from coderConfig and manages only async data. |
| 93 | */ |
| 94 | export function useCoderWorkspace({ |
| 95 | coderConfig, |
| 96 | onCoderConfigChange, |
| 97 | coderInfoRefreshPolicy, |
| 98 | }: UseCoderWorkspaceOptions): UseCoderWorkspaceReturn { |
| 99 | const { api } = useAPI(); |
| 100 | |
| 101 | // Async-fetched data (owned by this hook) |
| 102 | const [coderInfo, setCoderInfo] = useState<CoderInfo | null>(null); |
| 103 | |
| 104 | // Derived state: enabled when coderConfig is present AND CLI is confirmed available |
| 105 | // Loading (null) and outdated/unavailable all result in enabled=false |
| 106 | const enabled = coderConfig != null && coderInfo?.state === "available"; |
| 107 | |
| 108 | // Refs to access current values in async callbacks (avoids stale closures) |
| 109 | const coderConfigRef = useRef(coderConfig); |
| 110 | const onCoderConfigChangeRef = useRef(onCoderConfigChange); |
| 111 | coderConfigRef.current = coderConfig; |
| 112 | onCoderConfigChangeRef.current = onCoderConfigChange; |
| 113 | const latestAuthSeqRef = useRef(0); |
| 114 | const [templates, setTemplates] = useState<CoderTemplate[]>([]); |
| 115 | const [templatesError, setTemplatesError] = useState<string | null>(null); |
| 116 | const [presets, setPresets] = useState<CoderPreset[]>([]); |
| 117 | const [presetsError, setPresetsError] = useState<string | null>(null); |
| 118 | const [existingWorkspaces, setExistingWorkspaces] = useState<CoderWorkspace[]>([]); |
| 119 | const [workspacesError, setWorkspacesError] = useState<string | null>(null); |
| 120 | |
| 121 | // Loading states |
| 122 | const [loadingTemplates, setLoadingTemplates] = useState(false); |
| 123 | const [loadingPresets, setLoadingPresets] = useState(false); |
| 124 | const [loadingWorkspaces, setLoadingWorkspaces] = useState(false); |
| 125 | |
| 126 | const fetchCoderInfo = useCallback(async () => { |
| 127 | // Advance sequence before the API guard so that an API context change |
| 128 | // (e.g., reconnecting -> api becomes null) invalidates any in-flight request. |
| 129 | const seq = ++latestAuthSeqRef.current; |
| 130 | |
| 131 | if (!api) return; |
| 132 | |
| 133 | try { |
| 134 | const info = await api.coder.getInfo(); |
| 135 | if (seq !== latestAuthSeqRef.current) { |
| 136 | return; |
| 137 | } |
| 138 | |
| 139 | setCoderInfo(info); |
| 140 | // Clear Coder config when CLI is not available (outdated or unavailable) |
| 141 | if (info.state !== "available" && coderConfigRef.current != null) { |
| 142 | onCoderConfigChangeRef.current(null); |
| 143 | } |
| 144 | } catch { |
| 145 | if (seq !== latestAuthSeqRef.current) { |
| 146 | return; |
| 147 | } |
| 148 | |
| 149 | setCoderInfo({ |
| 150 | state: "unavailable", |
| 151 | reason: { kind: "error", message: "Failed to fetch" }, |