free-claude-code 6.2.64__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (429) hide show
  1. free_claude_code/__init__.py +1 -0
  2. free_claude_code/api/__init__.py +1 -0
  3. free_claude_code/api/admin_cache.py +49 -0
  4. free_claude_code/api/admin_routes.py +501 -0
  5. free_claude_code/api/admin_security.py +67 -0
  6. free_claude_code/api/admin_static/admin.css +980 -0
  7. free_claude_code/api/admin_static/admin.js +1934 -0
  8. free_claude_code/api/admin_static/code_sessions.css +109 -0
  9. free_claude_code/api/admin_static/code_sessions.js +1693 -0
  10. free_claude_code/api/admin_static/form_controls.js +15 -0
  11. free_claude_code/api/admin_static/index.html +201 -0
  12. free_claude_code/api/admin_static/model_combobox.js +194 -0
  13. free_claude_code/api/admin_static/providers/LICENSE-llamacpp.txt +21 -0
  14. free_claude_code/api/admin_static/providers/LICENSE-lobe-icons.txt +21 -0
  15. free_claude_code/api/admin_static/providers/SOURCES.md +56 -0
  16. free_claude_code/api/admin_static/providers/agnesai.svg +1 -0
  17. free_claude_code/api/admin_static/providers/azure-color.svg +1 -0
  18. free_claude_code/api/admin_static/providers/bedrock-color.svg +1 -0
  19. free_claude_code/api/admin_static/providers/cerebras-color.svg +1 -0
  20. free_claude_code/api/admin_static/providers/chutes.png +0 -0
  21. free_claude_code/api/admin_static/providers/cline.svg +1 -0
  22. free_claude_code/api/admin_static/providers/cloudflare-color.svg +1 -0
  23. free_claude_code/api/admin_static/providers/cohere-color.svg +1 -0
  24. free_claude_code/api/admin_static/providers/deepinfra-color.svg +1 -0
  25. free_claude_code/api/admin_static/providers/deepseek-color.svg +1 -0
  26. free_claude_code/api/admin_static/providers/experiential.svg +9 -0
  27. free_claude_code/api/admin_static/providers/featherless-color.svg +1 -0
  28. free_claude_code/api/admin_static/providers/fireworks-color.svg +1 -0
  29. free_claude_code/api/admin_static/providers/gemini-color.svg +1 -0
  30. free_claude_code/api/admin_static/providers/githubcopilot.svg +1 -0
  31. free_claude_code/api/admin_static/providers/groq.svg +1 -0
  32. free_claude_code/api/admin_static/providers/huggingface-color.svg +1 -0
  33. free_claude_code/api/admin_static/providers/kilocode.svg +1 -0
  34. free_claude_code/api/admin_static/providers/kimi-color.svg +1 -0
  35. free_claude_code/api/admin_static/providers/lightning.png +0 -0
  36. free_claude_code/api/admin_static/providers/llamacpp.svg +77 -0
  37. free_claude_code/api/admin_static/providers/llm7.png +0 -0
  38. free_claude_code/api/admin_static/providers/lmstudio.svg +1 -0
  39. free_claude_code/api/admin_static/providers/minimax-color.svg +1 -0
  40. free_claude_code/api/admin_static/providers/mistral-color.svg +1 -0
  41. free_claude_code/api/admin_static/providers/nararoute.png +0 -0
  42. free_claude_code/api/admin_static/providers/nebius.svg +1 -0
  43. free_claude_code/api/admin_static/providers/novita-color.svg +1 -0
  44. free_claude_code/api/admin_static/providers/nvidia-color.svg +1 -0
  45. free_claude_code/api/admin_static/providers/ollama.svg +1 -0
  46. free_claude_code/api/admin_static/providers/openai.svg +1 -0
  47. free_claude_code/api/admin_static/providers/opencode.svg +1 -0
  48. free_claude_code/api/admin_static/providers/openrouter.svg +1 -0
  49. free_claude_code/api/admin_static/providers/poolside-color.svg +1 -0
  50. free_claude_code/api/admin_static/providers/qwen-color.svg +1 -0
  51. free_claude_code/api/admin_static/providers/sambanova-color.svg +1 -0
  52. free_claude_code/api/admin_static/providers/scaleway.svg +11 -0
  53. free_claude_code/api/admin_static/providers/siliconcloud-color.svg +1 -0
  54. free_claude_code/api/admin_static/providers/together-color.svg +1 -0
  55. free_claude_code/api/admin_static/providers/tokenrouter.png +0 -0
  56. free_claude_code/api/admin_static/providers/vercel.svg +1 -0
  57. free_claude_code/api/admin_static/providers/vertexai-color.svg +1 -0
  58. free_claude_code/api/admin_static/providers/wafer.png +0 -0
  59. free_claude_code/api/admin_static/providers/wandb.png +0 -0
  60. free_claude_code/api/admin_static/providers/xai.svg +1 -0
  61. free_claude_code/api/admin_static/providers/zai.svg +1 -0
  62. free_claude_code/api/admin_static/providers/zenmux.svg +1 -0
  63. free_claude_code/api/admin_static/session_layout.css +552 -0
  64. free_claude_code/api/admin_static/session_ui.js +202 -0
  65. free_claude_code/api/app.py +149 -0
  66. free_claude_code/api/code_sessions_routes.py +386 -0
  67. free_claude_code/api/command_utils.py +164 -0
  68. free_claude_code/api/dependencies.py +128 -0
  69. free_claude_code/api/detection.py +185 -0
  70. free_claude_code/api/handlers/__init__.py +7 -0
  71. free_claude_code/api/handlers/classifier_response.py +66 -0
  72. free_claude_code/api/handlers/messages.py +359 -0
  73. free_claude_code/api/handlers/responses.py +163 -0
  74. free_claude_code/api/handlers/token_count.py +87 -0
  75. free_claude_code/api/markdown.py +71 -0
  76. free_claude_code/api/model_catalog.py +282 -0
  77. free_claude_code/api/optimization_handlers.py +162 -0
  78. free_claude_code/api/ports.py +97 -0
  79. free_claude_code/api/request_errors.py +100 -0
  80. free_claude_code/api/request_ids.py +100 -0
  81. free_claude_code/api/request_lifetime.py +130 -0
  82. free_claude_code/api/response_streams.py +422 -0
  83. free_claude_code/api/routes.py +269 -0
  84. free_claude_code/api/validation_log.py +46 -0
  85. free_claude_code/application/__init__.py +1 -0
  86. free_claude_code/application/code_sessions/__init__.py +33 -0
  87. free_claude_code/application/code_sessions/models.py +227 -0
  88. free_claude_code/application/code_sessions/ports.py +197 -0
  89. free_claude_code/application/code_sessions/service.py +1123 -0
  90. free_claude_code/application/code_sessions/state.py +659 -0
  91. free_claude_code/application/connected_accounts.py +96 -0
  92. free_claude_code/application/errors.py +41 -0
  93. free_claude_code/application/execution.py +467 -0
  94. free_claude_code/application/model_catalog.py +95 -0
  95. free_claude_code/application/model_metadata.py +26 -0
  96. free_claude_code/application/ports.py +127 -0
  97. free_claude_code/application/readiness.py +43 -0
  98. free_claude_code/application/reasoning.py +134 -0
  99. free_claude_code/application/routing.py +291 -0
  100. free_claude_code/application/session_events.py +140 -0
  101. free_claude_code/application/web_tools/__init__.py +1 -0
  102. free_claude_code/application/web_tools/ports.py +35 -0
  103. free_claude_code/application/web_tools/request.py +348 -0
  104. free_claude_code/application/web_tools/service.py +481 -0
  105. free_claude_code/assets/app-icon.icns +0 -0
  106. free_claude_code/assets/app-icon.ico +0 -0
  107. free_claude_code/assets/app-icon.png +0 -0
  108. free_claude_code/assets/app-icon.svg +14 -0
  109. free_claude_code/cli/__init__.py +5 -0
  110. free_claude_code/cli/commands.py +379 -0
  111. free_claude_code/cli/desktop.py +156 -0
  112. free_claude_code/cli/desktop_assets.py +31 -0
  113. free_claude_code/cli/desktop_entrypoint.py +29 -0
  114. free_claude_code/cli/desktop_tray.py +67 -0
  115. free_claude_code/cli/entrypoints.py +25 -0
  116. free_claude_code/cli/launchers/__init__.py +1 -0
  117. free_claude_code/cli/launchers/aider.py +53 -0
  118. free_claude_code/cli/launchers/aider_config.py +80 -0
  119. free_claude_code/cli/launchers/catalog_http.py +134 -0
  120. free_claude_code/cli/launchers/claude.py +34 -0
  121. free_claude_code/cli/launchers/cline.py +60 -0
  122. free_claude_code/cli/launchers/cline_config.py +121 -0
  123. free_claude_code/cli/launchers/codex.py +47 -0
  124. free_claude_code/cli/launchers/common.py +103 -0
  125. free_claude_code/cli/launchers/dsh.py +69 -0
  126. free_claude_code/cli/launchers/dsh_config.py +124 -0
  127. free_claude_code/cli/launchers/grok.py +116 -0
  128. free_claude_code/cli/launchers/hermes.py +85 -0
  129. free_claude_code/cli/launchers/hermes_config.py +134 -0
  130. free_claude_code/cli/launchers/muse.py +68 -0
  131. free_claude_code/cli/launchers/opencode.py +83 -0
  132. free_claude_code/cli/launchers/opencode_config.py +107 -0
  133. free_claude_code/cli/launchers/pi.py +70 -0
  134. free_claude_code/cli/launchers/pi_extension.ts +190 -0
  135. free_claude_code/cli/launchers/runner.py +155 -0
  136. free_claude_code/cli/local_http.py +12 -0
  137. free_claude_code/cli/managed/__init__.py +6 -0
  138. free_claude_code/cli/managed/claude.py +225 -0
  139. free_claude_code/cli/managed/diagnostics.py +47 -0
  140. free_claude_code/cli/managed/manager.py +207 -0
  141. free_claude_code/cli/managed/session.py +293 -0
  142. free_claude_code/cli/process_registry.py +76 -0
  143. free_claude_code/cli/server_socket.py +71 -0
  144. free_claude_code/cli/uvicorn_server.py +41 -0
  145. free_claude_code/config/__init__.py +10 -0
  146. free_claude_code/config/admin/__init__.py +1 -0
  147. free_claude_code/config/admin/manifest.py +610 -0
  148. free_claude_code/config/admin/persistence.py +202 -0
  149. free_claude_code/config/admin/provider_manifest.py +443 -0
  150. free_claude_code/config/admin/specs.py +77 -0
  151. free_claude_code/config/admin/state.py +18 -0
  152. free_claude_code/config/admin/status.py +116 -0
  153. free_claude_code/config/admin/validation.py +40 -0
  154. free_claude_code/config/admin/values.py +115 -0
  155. free_claude_code/config/constants.py +9 -0
  156. free_claude_code/config/env_files.py +74 -0
  157. free_claude_code/config/env_migrations.py +457 -0
  158. free_claude_code/config/loader.py +196 -0
  159. free_claude_code/config/logging_config.py +197 -0
  160. free_claude_code/config/model_refs.py +118 -0
  161. free_claude_code/config/nim.py +121 -0
  162. free_claude_code/config/paths.py +104 -0
  163. free_claude_code/config/provider_catalog.py +746 -0
  164. free_claude_code/config/provider_proxies.py +44 -0
  165. free_claude_code/config/reasoning.py +24 -0
  166. free_claude_code/config/server_urls.py +55 -0
  167. free_claude_code/config/settings.py +812 -0
  168. free_claude_code/core/__init__.py +15 -0
  169. free_claude_code/core/anthropic/__init__.py +99 -0
  170. free_claude_code/core/anthropic/content.py +31 -0
  171. free_claude_code/core/anthropic/conversion.py +876 -0
  172. free_claude_code/core/anthropic/errors.py +97 -0
  173. free_claude_code/core/anthropic/image_sources.py +77 -0
  174. free_claude_code/core/anthropic/models.py +193 -0
  175. free_claude_code/core/anthropic/native.py +184 -0
  176. free_claude_code/core/anthropic/native_stream.py +306 -0
  177. free_claude_code/core/anthropic/request_serialization.py +57 -0
  178. free_claude_code/core/anthropic/request_snapshot.py +35 -0
  179. free_claude_code/core/anthropic/server_tool_sse.py +234 -0
  180. free_claude_code/core/anthropic/server_tool_types.py +9 -0
  181. free_claude_code/core/anthropic/sse_aggregation.py +144 -0
  182. free_claude_code/core/anthropic/stream_contracts.py +208 -0
  183. free_claude_code/core/anthropic/streaming/__init__.py +43 -0
  184. free_claude_code/core/anthropic/streaming/decoder.py +49 -0
  185. free_claude_code/core/anthropic/streaming/emitter.py +62 -0
  186. free_claude_code/core/anthropic/streaming/ledger.py +538 -0
  187. free_claude_code/core/anthropic/streaming/recovery.py +211 -0
  188. free_claude_code/core/anthropic/thinking.py +140 -0
  189. free_claude_code/core/anthropic/tokens.py +111 -0
  190. free_claude_code/core/anthropic/tool_results.py +63 -0
  191. free_claude_code/core/anthropic/tool_schema.py +94 -0
  192. free_claude_code/core/anthropic/tools.py +493 -0
  193. free_claude_code/core/anthropic/usage.py +38 -0
  194. free_claude_code/core/anthropic/utils.py +9 -0
  195. free_claude_code/core/async_iterators.py +26 -0
  196. free_claude_code/core/async_tasks.py +26 -0
  197. free_claude_code/core/data/README.md +12 -0
  198. free_claude_code/core/data/TIKTOKEN-LICENSE +21 -0
  199. free_claude_code/core/data/cl100k_base.tiktoken +100256 -0
  200. free_claude_code/core/diagnostics.py +319 -0
  201. free_claude_code/core/failures.py +50 -0
  202. free_claude_code/core/gateway_model_ids.py +73 -0
  203. free_claude_code/core/history_replay.py +561 -0
  204. free_claude_code/core/interprocess_lock.py +90 -0
  205. free_claude_code/core/json_types.py +7 -0
  206. free_claude_code/core/model_capabilities.py +10 -0
  207. free_claude_code/core/openai_chat.py +54 -0
  208. free_claude_code/core/openai_responses/__init__.py +90 -0
  209. free_claude_code/core/openai_responses/chat_request.py +747 -0
  210. free_claude_code/core/openai_responses/errors.py +78 -0
  211. free_claude_code/core/openai_responses/events.py +82 -0
  212. free_claude_code/core/openai_responses/ids.py +32 -0
  213. free_claude_code/core/openai_responses/items.py +35 -0
  214. free_claude_code/core/openai_responses/messages_request.py +624 -0
  215. free_claude_code/core/openai_responses/messages_stream.py +404 -0
  216. free_claude_code/core/openai_responses/models.py +33 -0
  217. free_claude_code/core/openai_responses/native.py +147 -0
  218. free_claude_code/core/openai_responses/provider_input.py +409 -0
  219. free_claude_code/core/openai_responses/provider_stream.py +282 -0
  220. free_claude_code/core/openai_responses/reasoning.py +73 -0
  221. free_claude_code/core/openai_responses/streaming/__init__.py +1 -0
  222. free_claude_code/core/openai_responses/streaming/blocks.py +36 -0
  223. free_claude_code/core/openai_responses/streaming/completion.py +169 -0
  224. free_claude_code/core/openai_responses/streaming/error_mapping.py +28 -0
  225. free_claude_code/core/openai_responses/streaming/event_builders.py +198 -0
  226. free_claude_code/core/openai_responses/streaming/ledger.py +96 -0
  227. free_claude_code/core/openai_responses/tokens.py +133 -0
  228. free_claude_code/core/openai_responses/tool_adaptation.py +828 -0
  229. free_claude_code/core/openai_responses/tool_search.py +204 -0
  230. free_claude_code/core/openai_responses/tools.py +169 -0
  231. free_claude_code/core/openai_tool_names.py +178 -0
  232. free_claude_code/core/rate_limit.py +72 -0
  233. free_claude_code/core/reasoning.py +136 -0
  234. free_claude_code/core/token_estimation.py +76 -0
  235. free_claude_code/core/tool_schema_patterns.py +168 -0
  236. free_claude_code/core/trace.py +197 -0
  237. free_claude_code/core/version.py +15 -0
  238. free_claude_code/core/web_tools.py +17 -0
  239. free_claude_code/core/windows_dpi.py +27 -0
  240. free_claude_code/harnesses/__init__.py +1 -0
  241. free_claude_code/harnesses/claude.py +42 -0
  242. free_claude_code/harnesses/claude_desktop_integration.py +418 -0
  243. free_claude_code/harnesses/claude_integration.py +167 -0
  244. free_claude_code/harnesses/codex.py +94 -0
  245. free_claude_code/harnesses/codex_integration.py +147 -0
  246. free_claude_code/harnesses/codex_model_catalog.py +109 -0
  247. free_claude_code/harnesses/config_file.py +33 -0
  248. free_claude_code/harnesses/environment.py +78 -0
  249. free_claude_code/harnesses/jetbrains_acp_integration.py +242 -0
  250. free_claude_code/harnesses/launch.py +21 -0
  251. free_claude_code/harnesses/resources.py +42 -0
  252. free_claude_code/messaging/__init__.py +16 -0
  253. free_claude_code/messaging/cli_event_constants.py +67 -0
  254. free_claude_code/messaging/command_context.py +99 -0
  255. free_claude_code/messaging/command_dispatcher.py +31 -0
  256. free_claude_code/messaging/commands.py +182 -0
  257. free_claude_code/messaging/event_parser.py +184 -0
  258. free_claude_code/messaging/limiter.py +342 -0
  259. free_claude_code/messaging/managed_protocols.py +35 -0
  260. free_claude_code/messaging/models.py +57 -0
  261. free_claude_code/messaging/node_event_pipeline.py +120 -0
  262. free_claude_code/messaging/node_runner.py +408 -0
  263. free_claude_code/messaging/platforms/__init__.py +20 -0
  264. free_claude_code/messaging/platforms/discord.py +319 -0
  265. free_claude_code/messaging/platforms/discord_inbound.py +112 -0
  266. free_claude_code/messaging/platforms/discord_io.py +167 -0
  267. free_claude_code/messaging/platforms/factory.py +109 -0
  268. free_claude_code/messaging/platforms/outbox.py +143 -0
  269. free_claude_code/messaging/platforms/ports.py +99 -0
  270. free_claude_code/messaging/platforms/telegram.py +300 -0
  271. free_claude_code/messaging/platforms/telegram_inbound.py +133 -0
  272. free_claude_code/messaging/platforms/telegram_io.py +287 -0
  273. free_claude_code/messaging/platforms/voice_flow.py +388 -0
  274. free_claude_code/messaging/rendering/__init__.py +3 -0
  275. free_claude_code/messaging/rendering/discord_markdown.py +318 -0
  276. free_claude_code/messaging/rendering/markdown_tables.py +47 -0
  277. free_claude_code/messaging/rendering/profiles.py +53 -0
  278. free_claude_code/messaging/rendering/telegram_markdown.py +331 -0
  279. free_claude_code/messaging/safe_diagnostics.py +15 -0
  280. free_claude_code/messaging/session/__init__.py +5 -0
  281. free_claude_code/messaging/session/managed_message_log.py +135 -0
  282. free_claude_code/messaging/session/persistence.py +161 -0
  283. free_claude_code/messaging/session/store.py +162 -0
  284. free_claude_code/messaging/transcript/__init__.py +6 -0
  285. free_claude_code/messaging/transcript/buffer.py +220 -0
  286. free_claude_code/messaging/transcript/context.py +18 -0
  287. free_claude_code/messaging/transcript/renderer.py +65 -0
  288. free_claude_code/messaging/transcript/segments.py +176 -0
  289. free_claude_code/messaging/transcript/subagents.py +108 -0
  290. free_claude_code/messaging/transcription.py +132 -0
  291. free_claude_code/messaging/trees/__init__.py +41 -0
  292. free_claude_code/messaging/trees/graph.py +250 -0
  293. free_claude_code/messaging/trees/identity.py +16 -0
  294. free_claude_code/messaging/trees/manager.py +587 -0
  295. free_claude_code/messaging/trees/node.py +56 -0
  296. free_claude_code/messaging/trees/processor.py +272 -0
  297. free_claude_code/messaging/trees/queue.py +47 -0
  298. free_claude_code/messaging/trees/repository.py +135 -0
  299. free_claude_code/messaging/trees/runtime.py +487 -0
  300. free_claude_code/messaging/trees/snapshot.py +221 -0
  301. free_claude_code/messaging/trees/transitions.py +174 -0
  302. free_claude_code/messaging/turn_intake.py +241 -0
  303. free_claude_code/messaging/ui_updates.py +99 -0
  304. free_claude_code/messaging/voice.py +376 -0
  305. free_claude_code/messaging/workflow.py +714 -0
  306. free_claude_code/providers/__init__.py +12 -0
  307. free_claude_code/providers/admission.py +942 -0
  308. free_claude_code/providers/anthropic_messages/__init__.py +1 -0
  309. free_claude_code/providers/anthropic_messages/request_policy.py +162 -0
  310. free_claude_code/providers/anthropic_messages/transport.py +499 -0
  311. free_claude_code/providers/base.py +73 -0
  312. free_claude_code/providers/cloudflare/__init__.py +8 -0
  313. free_claude_code/providers/cloudflare/client.py +166 -0
  314. free_claude_code/providers/credential_validation.py +314 -0
  315. free_claude_code/providers/deepseek/__init__.py +5 -0
  316. free_claude_code/providers/deepseek/client.py +117 -0
  317. free_claude_code/providers/deepseek/compat.py +241 -0
  318. free_claude_code/providers/endpoint.py +21 -0
  319. free_claude_code/providers/endpoint_types.py +21 -0
  320. free_claude_code/providers/failure_policy.py +574 -0
  321. free_claude_code/providers/gemini/__init__.py +5 -0
  322. free_claude_code/providers/gemini/client.py +42 -0
  323. free_claude_code/providers/github_copilot/__init__.py +1 -0
  324. free_claude_code/providers/github_copilot/auth.py +502 -0
  325. free_claude_code/providers/github_copilot/broker.py +248 -0
  326. free_claude_code/providers/github_copilot/lifecycle.py +18 -0
  327. free_claude_code/providers/github_copilot/login.py +112 -0
  328. free_claude_code/providers/github_copilot/native_cli.py +82 -0
  329. free_claude_code/providers/github_copilot/provider.py +367 -0
  330. free_claude_code/providers/github_copilot/request_policy.py +119 -0
  331. free_claude_code/providers/github_copilot/responses_events.py +90 -0
  332. free_claude_code/providers/github_copilot/sdk.py +496 -0
  333. free_claude_code/providers/github_copilot/types.py +67 -0
  334. free_claude_code/providers/google_openai/__init__.py +17 -0
  335. free_claude_code/providers/google_openai/provider.py +73 -0
  336. free_claude_code/providers/google_openai/reasoning.py +150 -0
  337. free_claude_code/providers/google_openai/thought_signatures.py +117 -0
  338. free_claude_code/providers/groq/__init__.py +5 -0
  339. free_claude_code/providers/groq/client.py +342 -0
  340. free_claude_code/providers/groq/tpm.py +114 -0
  341. free_claude_code/providers/history_replay.py +260 -0
  342. free_claude_code/providers/http.py +105 -0
  343. free_claude_code/providers/kilo/__init__.py +5 -0
  344. free_claude_code/providers/kilo/client.py +45 -0
  345. free_claude_code/providers/kilo/models.py +60 -0
  346. free_claude_code/providers/lmstudio/__init__.py +5 -0
  347. free_claude_code/providers/lmstudio/client.py +220 -0
  348. free_claude_code/providers/mistral/__init__.py +5 -0
  349. free_claude_code/providers/mistral/client.py +91 -0
  350. free_claude_code/providers/mistral/reasoning.py +410 -0
  351. free_claude_code/providers/model_listing.py +479 -0
  352. free_claude_code/providers/nvidia_nim/__init__.py +5 -0
  353. free_claude_code/providers/nvidia_nim/client.py +224 -0
  354. free_claude_code/providers/nvidia_nim/native_tool_stream.py +693 -0
  355. free_claude_code/providers/nvidia_nim/request_options.py +141 -0
  356. free_claude_code/providers/nvidia_nim/retry.py +85 -0
  357. free_claude_code/providers/nvidia_nim/tool_schema.py +255 -0
  358. free_claude_code/providers/nvidia_nim/voice.py +107 -0
  359. free_claude_code/providers/open_router/__init__.py +5 -0
  360. free_claude_code/providers/open_router/client.py +113 -0
  361. free_claude_code/providers/openai_api/__init__.py +5 -0
  362. free_claude_code/providers/openai_api/provider.py +150 -0
  363. free_claude_code/providers/openai_chat/__init__.py +72 -0
  364. free_claude_code/providers/openai_chat/base_url.py +7 -0
  365. free_claude_code/providers/openai_chat/behavior.py +133 -0
  366. free_claude_code/providers/openai_chat/client.py +40 -0
  367. free_claude_code/providers/openai_chat/extra_body.py +63 -0
  368. free_claude_code/providers/openai_chat/output_cap.py +161 -0
  369. free_claude_code/providers/openai_chat/profiles.py +778 -0
  370. free_claude_code/providers/openai_chat/provider.py +244 -0
  371. free_claude_code/providers/openai_chat/reasoning.py +169 -0
  372. free_claude_code/providers/openai_chat/reasoning_details.py +128 -0
  373. free_claude_code/providers/openai_chat/request_policy.py +140 -0
  374. free_claude_code/providers/openai_chat/stream_output.py +752 -0
  375. free_claude_code/providers/openai_chat/tool_calls.py +462 -0
  376. free_claude_code/providers/openai_chat/transport.py +1643 -0
  377. free_claude_code/providers/openai_chat/usage.py +117 -0
  378. free_claude_code/providers/openai_client.py +74 -0
  379. free_claude_code/providers/openai_codex/__init__.py +1 -0
  380. free_claude_code/providers/openai_codex/auth.py +626 -0
  381. free_claude_code/providers/openai_codex/endpoint.py +53 -0
  382. free_claude_code/providers/openai_codex/login.py +265 -0
  383. free_claude_code/providers/openai_codex/provider.py +259 -0
  384. free_claude_code/providers/openai_responses/__init__.py +5 -0
  385. free_claude_code/providers/openai_responses/presentation.py +115 -0
  386. free_claude_code/providers/openai_responses/transport.py +621 -0
  387. free_claude_code/providers/openai_stream.py +21 -0
  388. free_claude_code/providers/opencode/__init__.py +5 -0
  389. free_claude_code/providers/opencode/catalog.py +340 -0
  390. free_claude_code/providers/opencode/provider.py +359 -0
  391. free_claude_code/providers/reasoning_compatibility.py +199 -0
  392. free_claude_code/providers/request_recovery.py +122 -0
  393. free_claude_code/providers/runtime/__init__.py +10 -0
  394. free_claude_code/providers/runtime/config.py +91 -0
  395. free_claude_code/providers/runtime/discovery.py +63 -0
  396. free_claude_code/providers/runtime/factory.py +282 -0
  397. free_claude_code/providers/runtime/model_cache.py +93 -0
  398. free_claude_code/providers/runtime/runtime.py +108 -0
  399. free_claude_code/providers/stream_recovery.py +164 -0
  400. free_claude_code/providers/vertex/__init__.py +5 -0
  401. free_claude_code/providers/vertex/auth.py +105 -0
  402. free_claude_code/providers/vertex/client.py +140 -0
  403. free_claude_code/providers/vertex/endpoint.py +50 -0
  404. free_claude_code/providers/vertex/models.py +52 -0
  405. free_claude_code/runtime/__init__.py +1 -0
  406. free_claude_code/runtime/application.py +1121 -0
  407. free_claude_code/runtime/asgi.py +63 -0
  408. free_claude_code/runtime/bootstrap.py +153 -0
  409. free_claude_code/runtime/code_sessions_sqlite.py +785 -0
  410. free_claude_code/runtime/codex_app_server.py +775 -0
  411. free_claude_code/runtime/codex_catalog.py +62 -0
  412. free_claude_code/runtime/codex_protocol.py +662 -0
  413. free_claude_code/runtime/configuration.py +53 -0
  414. free_claude_code/runtime/folder_picker.py +134 -0
  415. free_claude_code/runtime/native_folder_dialog.py +123 -0
  416. free_claude_code/runtime/provider_manager.py +716 -0
  417. free_claude_code/runtime/retired_chat.py +47 -0
  418. free_claude_code/runtime/web_tools/__init__.py +1 -0
  419. free_claude_code/runtime/web_tools/client.py +238 -0
  420. free_claude_code/runtime/web_tools/constants.py +17 -0
  421. free_claude_code/runtime/web_tools/egress.py +89 -0
  422. free_claude_code/runtime/web_tools/parsers.py +78 -0
  423. free_claude_code-6.2.64.data/scripts/fcc-update +4 -0
  424. free_claude_code-6.2.64.data/scripts/fcc-update.cmd +5 -0
  425. free_claude_code-6.2.64.dist-info/METADATA +555 -0
  426. free_claude_code-6.2.64.dist-info/RECORD +429 -0
  427. free_claude_code-6.2.64.dist-info/WHEEL +4 -0
  428. free_claude_code-6.2.64.dist-info/entry_points.txt +15 -0
  429. free_claude_code-6.2.64.dist-info/licenses/LICENSE +239 -0
@@ -0,0 +1,1934 @@
1
+ const adminAssetBase = new URL(".", document.currentScript.src);
2
+
3
+ const state = {
4
+ config: null,
5
+ applying: false,
6
+ restart: null,
7
+ fields: new Map(),
8
+ modelOptions: [],
9
+ modelComboboxes: new Set(),
10
+ authPollers: new Map(),
11
+ authStatuses: new Map(),
12
+ providerChecks: new Map(),
13
+ providerId: null,
14
+ localStatusRequest: null,
15
+ startup: null,
16
+ startupRequest: null,
17
+ startupTimer: null,
18
+ startupAgain: false,
19
+ codeCatalogRetry: false,
20
+ modelOptionsRequest: 0,
21
+ activeView: viewFromLocation(),
22
+ };
23
+
24
+ const MASKED_SECRET = "********";
25
+ const NULL_VALUE = "__FCC_NULL__";
26
+ const VIEW_GROUPS = [
27
+ {
28
+ id: "providers",
29
+ label: "Providers",
30
+ title: "Providers",
31
+ sections: ["runtime"],
32
+ containerId: "providersSections",
33
+ },
34
+ {
35
+ id: "model_config",
36
+ label: "Model Config",
37
+ title: "Model Config",
38
+ sections: ["models", "reasoning", "web_tools"],
39
+ containerId: "modelConfigSections",
40
+ },
41
+ {
42
+ id: "messaging",
43
+ label: "Messaging",
44
+ title: "Messaging",
45
+ sections: ["messaging", "voice"],
46
+ containerId: "messagingSections",
47
+ },
48
+ {
49
+ id: "integrations",
50
+ label: "Integrations",
51
+ title: "Integrations",
52
+ sections: [],
53
+ containerId: "view-integrations",
54
+ },
55
+ {
56
+ id: "code",
57
+ label: "Code sessions",
58
+ title: "Code sessions",
59
+ sections: [],
60
+ containerId: "codeRoot",
61
+ },
62
+ ];
63
+
64
+ function viewFromLocation() {
65
+ return window.location.pathname.split("/")[2] || "providers";
66
+ }
67
+
68
+ const byId = (id) => document.getElementById(id);
69
+
70
+ function sourceLabel(source) {
71
+ const labels = {
72
+ default: "default",
73
+ managed_env: "",
74
+ process: "process env",
75
+ };
76
+ return Object.prototype.hasOwnProperty.call(labels, source) ? labels[source] : source;
77
+ }
78
+
79
+ function sourceText(field) {
80
+ const parts = [];
81
+ const label = sourceLabel(field.source);
82
+ if (label) {
83
+ parts.push(label);
84
+ }
85
+ if (field.locked) {
86
+ parts.push("locked");
87
+ }
88
+ return parts.join(" ");
89
+ }
90
+
91
+ async function api(path, options = {}) {
92
+ const response = await fetch(path, {
93
+ headers: { "Content-Type": "application/json", ...(options.headers || {}) },
94
+ ...options,
95
+ cache: "no-store",
96
+ });
97
+ if (!response.ok) {
98
+ let detail = "";
99
+ try {
100
+ const payload = await response.json();
101
+ detail = typeof payload.detail === "string" ? payload.detail : "";
102
+ } catch {
103
+ // The status remains useful when an upstream proxy returns a non-JSON page.
104
+ }
105
+ const error = new Error(detail || `${response.status} ${response.statusText}`);
106
+ error.status = response.status;
107
+ throw error;
108
+ }
109
+ return response.json();
110
+ }
111
+
112
+ function startupButton(button, loading) {
113
+ if (button.dataset.operationBusy) return;
114
+ button.disabled = loading;
115
+ button.classList.toggle("startup-busy", loading);
116
+ button.setAttribute("aria-busy", String(loading));
117
+ }
118
+
119
+ function renderStartup() {
120
+ const startup = state.startup?.startup;
121
+ const message = byId("startupMessage");
122
+ if (message) {
123
+ const messaging = startup?.messaging;
124
+ const visible = state.activeView === "messaging" && ["starting", "failed"].includes(messaging?.state);
125
+ message.hidden = !visible;
126
+ message.classList.toggle("startup-spinner", visible && messaging.state === "starting");
127
+ message.classList.toggle("error", messaging?.state === "failed");
128
+ message.textContent = messaging?.state === "failed" ? messaging.message || "Messaging could not start." : "";
129
+ message.setAttribute("aria-label", "Messaging is starting");
130
+ }
131
+ if (!startup) return;
132
+ document.querySelectorAll("[data-startup-provider]").forEach((button) => {
133
+ startupButton(button, startup.providers?.[button.dataset.startupProvider] === "starting");
134
+ });
135
+ document.querySelectorAll("[data-startup-catalog]").forEach((button) => {
136
+ startupButton(button, Object.values(startup.providers || {}).includes("starting"));
137
+ });
138
+ state.config.provider_status.forEach((provider) => {
139
+ renderProviderCheckResult(provider.provider_id);
140
+ });
141
+ renderClaudeIntegration();
142
+ renderJetBrainsIntegration();
143
+ renderCodexIntegration();
144
+ renderClaudeDesktopIntegration();
145
+ }
146
+
147
+ async function refreshStartup() {
148
+ if (!state.config || document.hidden || state.restart) return;
149
+ if (state.startupRequest) { state.startupAgain = true; return; }
150
+ clearTimeout(state.startupTimer);
151
+ state.startupTimer = null;
152
+ const request = { controller: new AbortController(), config: state.config };
153
+ state.startupRequest = request;
154
+ let pending = false;
155
+ try {
156
+ const result = await api("/admin/api/status", { signal: request.controller.signal });
157
+ if (state.startupRequest !== request || state.config !== request.config) return;
158
+ const previous = state.startup;
159
+ const current = result.startup;
160
+ if (!current) return;
161
+ if (previous?.instance_id === result.instance_id && (
162
+ current.generation_id < previous.startup.generation_id ||
163
+ (current.generation_id === previous.startup.generation_id &&
164
+ current.catalog_revision < previous.startup.catalog_revision)
165
+ )) return;
166
+ state.startup = result;
167
+ renderStartup();
168
+ refreshIntegrationUpdates(
169
+ previous?.instance_id === result.instance_id ? previous.startup.integrations : null,
170
+ current.integrations,
171
+ );
172
+ const changed = !previous || previous.instance_id !== result.instance_id ||
173
+ JSON.stringify(previous.startup) !== JSON.stringify(current);
174
+ if (changed) void hydrateModelOptions();
175
+ if (changed || state.codeCatalogRetry) state.codeCatalogRetry = await window.CodeSessions?.refresh(result) === false;
176
+ pending = state.codeCatalogRetry || current.catalog === "starting" || current.catalog_file === "starting" ||
177
+ current.code?.state === "starting" || current.messaging?.state === "starting" ||
178
+ Object.values(current.integrations || {}).some((update) => update.state === "starting") ||
179
+ Object.values(current.providers || {}).includes("starting");
180
+ } catch (error) {
181
+ if (error.name !== "AbortError") pending = true;
182
+ } finally {
183
+ if (state.startupRequest === request) {
184
+ state.startupRequest = null;
185
+ if ((pending || state.startupAgain) && !document.hidden) state.startupTimer = setTimeout(refreshStartup, 500);
186
+ state.startupAgain = false;
187
+ }
188
+ }
189
+ }
190
+
191
+ document.addEventListener("visibilitychange", () => {
192
+ clearTimeout(state.startupTimer);
193
+ if (document.hidden) {
194
+ state.startupRequest?.controller.abort();
195
+ state.startupRequest = null;
196
+ } else {
197
+ void refreshStartup();
198
+ }
199
+ });
200
+ window.addEventListener("pagehide", () => {
201
+ clearTimeout(state.startupTimer);
202
+ state.startupRequest?.controller.abort();
203
+ state.startupRequest = null;
204
+ });
205
+
206
+ async function load({ providersOnly = false } = {}) {
207
+ state.localStatusRequest = null;
208
+ state.startupRequest?.controller.abort();
209
+ state.startupRequest = null;
210
+ showMessage("Loading admin config");
211
+ const config = await api("/admin/api/config");
212
+ state.config = config;
213
+ state.startup = null;
214
+ state.fields = new Map(config.fields.map((field) => [field.key, field]));
215
+ if (!providersOnly) renderNav();
216
+ state.providerChecks.clear();
217
+ renderProviders(config.provider_status);
218
+ if (!providersOnly) renderSections(config.sections, config.fields);
219
+ byId("configPath").textContent = config.paths.managed;
220
+ void refreshLocalStatus(config);
221
+ void refreshStartup();
222
+ await Promise.all([
223
+ refreshConnectedAccounts(),
224
+ hydrateModelOptions(),
225
+ providersOnly ? window.CodeSessions.refresh() : window.CodeSessions.initialize(api),
226
+ ]);
227
+ if (state.config !== config) return;
228
+ updateDirtyState();
229
+ showMessage("");
230
+ }
231
+
232
+ function renderNav() {
233
+ const nav = byId("sectionNav");
234
+ nav.innerHTML = "";
235
+ VIEW_GROUPS.forEach((view, index) => {
236
+ const button = document.createElement("button");
237
+ button.type = "button";
238
+ button.className = `nav-link${index === 0 ? " active" : ""}`;
239
+ button.dataset.view = view.id;
240
+ button.textContent = view.label;
241
+ if (index === 0) {
242
+ button.setAttribute("aria-current", "page");
243
+ }
244
+ button.addEventListener("click", () => {
245
+ navigateToView(view.id);
246
+ });
247
+ nav.appendChild(button);
248
+ });
249
+ setActiveView(state.activeView, { scroll: false });
250
+ }
251
+
252
+ function setActiveView(viewId, { scroll = false } = {}) {
253
+ const activeView =
254
+ VIEW_GROUPS.find((view) => view.id === viewId) || VIEW_GROUPS[0];
255
+ state.activeView = activeView.id;
256
+ byId("pageTitle").textContent = activeView.title;
257
+ renderStartup();
258
+ if (activeView.id === "code") state.codeCatalogRetry = true;
259
+ void refreshStartup();
260
+ const sessionActive = activeView.id === "code";
261
+ document.querySelector(".app-shell").classList.toggle("session-active", sessionActive);
262
+ document.querySelector(".main").classList.toggle("session-main", sessionActive);
263
+ document.querySelector(".topbar").hidden = sessionActive;
264
+ document.querySelector(".action-bar").hidden = sessionActive || activeView.id === "integrations";
265
+
266
+ document.querySelectorAll(".nav-link").forEach((link) => {
267
+ const selected = link.dataset.view === activeView.id;
268
+ link.classList.toggle("active", selected);
269
+ if (selected) {
270
+ link.setAttribute("aria-current", "page");
271
+ } else {
272
+ link.removeAttribute("aria-current");
273
+ }
274
+ });
275
+
276
+ document.querySelectorAll(".admin-view").forEach((view) => {
277
+ const selected = view.dataset.view === activeView.id;
278
+ view.classList.toggle("active", selected);
279
+ view.hidden = !selected;
280
+ });
281
+
282
+ if (scroll) {
283
+ window.scrollTo({ top: 0, behavior: "smooth" });
284
+ }
285
+ if (activeView.id === "code") window.CodeSessions.activate(window.location.pathname);
286
+ else window.CodeSessions.deactivate();
287
+ if (activeView.id === "integrations") {
288
+ refreshClaudeIntegration();
289
+ refreshJetBrainsIntegration();
290
+ refreshCodexIntegration();
291
+ refreshClaudeDesktopIntegration();
292
+ }
293
+ }
294
+
295
+ function navigateToView(viewId) {
296
+ const target = viewId === "providers" ? "/admin" : `/admin/${viewId}`;
297
+ if (window.location.pathname + window.location.search !== target) {
298
+ window.history.pushState({}, "", target);
299
+ }
300
+ setActiveView(viewId, { scroll: true });
301
+ }
302
+
303
+ function renderProviders(providerStatus) {
304
+ const container = byId("providerGroups");
305
+ container.replaceChildren();
306
+ [
307
+ ["oauth", "OAuth providers"],
308
+ ["cloud", "Cloud providers"],
309
+ ["local", "Local providers"],
310
+ ].forEach(([kind, label]) => {
311
+ const group = document.createElement("section");
312
+ group.className = "provider-strip";
313
+ group.dataset.providerGroup = kind;
314
+ const heading = document.createElement("h3");
315
+ heading.textContent = label;
316
+ group.appendChild(heading);
317
+ const subgroups = [
318
+ ["configured", kind === "oauth" ? "Connected" : "Configured"],
319
+ ["unconfigured", kind === "oauth" ? "Not connected" : "Not configured"],
320
+ ];
321
+ if (kind === "oauth") subgroups.unshift(["loading", "Checking account status"]);
322
+ subgroups.forEach(([subgroupId, subgroupLabel]) => {
323
+ const subgroup = document.createElement("div");
324
+ subgroup.dataset.providerSubgroup = subgroupId;
325
+ subgroup.hidden = true;
326
+ const title = document.createElement("h4");
327
+ title.textContent = subgroupLabel;
328
+ title.hidden = subgroupId === "loading";
329
+ const grid = document.createElement("div");
330
+ grid.className = "provider-grid";
331
+ grid.id = `providers-${kind}-${subgroupId}`;
332
+ subgroup.append(title, grid);
333
+ group.appendChild(subgroup);
334
+ });
335
+ container.appendChild(group);
336
+ });
337
+ providerStatus.forEach(updateProviderCard);
338
+ }
339
+
340
+ function updateProviderCard(provider) {
341
+ const oauth = provider.kind === "connected_account";
342
+ const status = state.authStatuses.get(provider.provider_id);
343
+ const kind = oauth ? "oauth" : provider.kind === "local" ? "local" : "cloud";
344
+ const configured = oauth ? status?.connected : provider.status === "configured";
345
+ const subgroup = oauth && configured == null ? "loading" : configured ? "configured" : "unconfigured";
346
+ const grid = byId(`providers-${kind}-${subgroup}`);
347
+ let card = document.querySelector(`[data-provider="${provider.provider_id}"]`);
348
+ if (!card) {
349
+ card = document.createElement("article");
350
+ card.className = "provider-card";
351
+ card.dataset.provider = provider.provider_id;
352
+ }
353
+ const focused = card.contains(document.activeElement);
354
+ const focusedLabel = focused ? document.activeElement.textContent : null;
355
+ const title = document.createElement("span");
356
+ title.className = "provider-title";
357
+ const name = document.createElement("strong");
358
+ name.textContent = connectedAccountName(provider);
359
+ const website = document.createElement("a");
360
+ website.href = provider.website_url;
361
+ website.target = "_blank";
362
+ website.rel = "noopener noreferrer";
363
+ const logo = document.createElement("img");
364
+ logo.className = "provider-logo";
365
+ logo.src = new URL(`providers/${provider.logo_filename}`, adminAssetBase);
366
+ logo.alt = "";
367
+ logo.width = 20;
368
+ logo.height = 20;
369
+ website.append(name, logo);
370
+ title.appendChild(website);
371
+ const meta = document.createElement("span");
372
+ meta.className = "provider-meta";
373
+ meta.hidden = !oauth;
374
+ const result = document.createElement("span");
375
+ result.className = "provider-check-result";
376
+ result.dataset.providerCheckResult = provider.provider_id;
377
+ result.hidden = true;
378
+ const actions = document.createElement("div");
379
+ actions.className = "provider-actions";
380
+ if (oauth) populateConnectedAccountActions(provider, status, actions);
381
+ if (provider.settings_keys?.length) {
382
+ const edit = oauth || configured;
383
+ const settings = authButton(edit ? "Edit" : "Configure", () => openProviderDialog(provider.provider_id), edit ? "secondary-button" : "primary-button");
384
+ settings.dataset.providerSettings = "true";
385
+ settings.setAttribute("aria-haspopup", "dialog");
386
+ settings.setAttribute("aria-controls", "providerDialog");
387
+ actions.appendChild(settings);
388
+ }
389
+ card.replaceChildren(title, meta, result, actions);
390
+ const next = [...grid.children].find((other) => other !== card &&
391
+ connectedAccountName(provider).localeCompare(providerDisplayName(other.dataset.provider), "en", { sensitivity: "base" }) < 0);
392
+ if (card.parentElement !== grid || card.nextElementSibling !== (next || null)) grid.insertBefore(card, next || null);
393
+ document.querySelectorAll("[data-provider-subgroup]").forEach((section) => {
394
+ section.hidden = section.querySelector(".provider-grid").childElementCount === 0;
395
+ });
396
+ if (focused) {
397
+ const controls = [...card.querySelectorAll("a[href], button:not(:disabled)")];
398
+ (controls.find((control) => control.textContent === focusedLabel) || actions.querySelector("button:not(:disabled)"))?.focus({ preventScroll: true });
399
+ }
400
+ renderProviderCheckResult(provider.provider_id);
401
+ }
402
+
403
+ function openProviderDialog(providerId) {
404
+ state.providerId = providerId;
405
+ const provider = connectedAccountDescriptor(providerId);
406
+ byId("providerDialogTitle").textContent = connectedAccountName(provider);
407
+ byId("providerMessage").textContent = "";
408
+ const fields = byId("providerFields");
409
+ fields.replaceChildren();
410
+ (provider.settings_keys || []).forEach((key) => {
411
+ const field = state.fields.get(key);
412
+ const wrapper = renderField(field);
413
+ const shared = state.config.provider_status.filter((other) =>
414
+ other.provider_id !== providerId && other.settings_keys?.includes(key),
415
+ );
416
+ if (shared.length) {
417
+ const note = document.createElement("div");
418
+ note.className = "field-description";
419
+ note.textContent = `Shared with ${shared.map(connectedAccountName).join(", ")}.`;
420
+ wrapper.appendChild(note);
421
+ }
422
+ fields.appendChild(wrapper);
423
+ });
424
+ renderProviderDialogActions(provider);
425
+ updateDirtyState();
426
+ byId("providerDialog").showModal();
427
+ const missing = provider.missing_configuration_keys?.[0];
428
+ const first = missing ? byId(`field-${missing}`) : fields.querySelector("input:not(:disabled), select:not(:disabled), textarea:not(:disabled)");
429
+ first?.focus();
430
+ }
431
+
432
+ function renderProviderDialogActions(provider) {
433
+ if (state.providerId !== provider.provider_id) return;
434
+ const actions = byId("providerDialogActions");
435
+ actions.replaceChildren();
436
+ const oauth = provider.kind === "connected_account";
437
+ if (!oauth && !provider.missing_configuration_keys.length) {
438
+ const button = authButton(provider.kind === "local" ? "Test" : "Refresh models", (target) => testProvider(provider.provider_id, target), "secondary-button");
439
+ button.dataset.startupProvider = provider.provider_id;
440
+ if (state.providerChecks.get(provider.provider_id)?.status === "checking") {
441
+ button.dataset.operationBusy = "true";
442
+ button.disabled = true;
443
+ button.textContent = "Checking...";
444
+ }
445
+ actions.appendChild(button);
446
+ }
447
+ const check = oauth ? null : providerCheckResult(provider.provider_id);
448
+ const result = byId("providerDialogCheck");
449
+ result.className = `provider-check-result ${check?.status || ""}`;
450
+ result.textContent = check?.message || "";
451
+ result.hidden = !check?.message;
452
+ byId("saveProvider").hidden = !provider.settings_keys?.length;
453
+ renderStartup();
454
+ }
455
+
456
+ function connectedAccountName(provider) {
457
+ return provider.display_name || provider.provider_id;
458
+ }
459
+
460
+ function connectedAccountMeta(provider, status) {
461
+ if (!status) return "Checking account status…";
462
+ const providerName = connectedAccountName(provider);
463
+ if (status.state === "connecting") {
464
+ if (status.mode === "device" && status.user_code && status.verification_url) {
465
+ return `Enter code ${status.user_code} at ${status.verification_url}`;
466
+ }
467
+ return status.message || "Finish signing in, then return to this page.";
468
+ }
469
+ if (status.connected) {
470
+ return providerCheckResult(provider.provider_id)?.message || "Checking models…";
471
+ }
472
+ return status.message || `Connect your ${providerName} account to discover models.`;
473
+ }
474
+
475
+ function populateConnectedAccountActions(provider, status, actions) {
476
+ const providerId = provider.provider_id;
477
+ if (!status) {
478
+ const loading = authButton("Loading…", () => {}, "secondary-button startup-busy");
479
+ loading.disabled = true;
480
+ loading.setAttribute("aria-busy", "true");
481
+ actions.appendChild(loading);
482
+ return;
483
+ }
484
+ if (status.state === "connecting") {
485
+ const target = status.authorization_url || status.verification_url;
486
+ if (target) {
487
+ actions.appendChild(authButton("Open sign-in", () => window.open(target, "_blank", "noopener")));
488
+ }
489
+ if (status.mode === "device" && status.user_code) {
490
+ actions.appendChild(
491
+ authButton("Copy code", () => copyDeviceCode(status.user_code), "secondary-button"),
492
+ );
493
+ }
494
+ actions.appendChild(
495
+ authButton("Cancel sign-in", () => cancelConnectedAccountLogin(providerId), "secondary-button"),
496
+ );
497
+ return;
498
+ }
499
+ if (status.connected) {
500
+ actions.appendChild(authButton("Disconnect", () => disconnectConnectedAccount(providerId), "danger-button"));
501
+ return;
502
+ }
503
+ const modes = Array.isArray(status.supported_login_modes) ? status.supported_login_modes : [];
504
+ const defaultMode = status.default_login_mode;
505
+ if (!["browser", "device"].includes(defaultMode) || !modes.includes(defaultMode)) {
506
+ actions.appendChild(authButton("Retry", () => refreshConnectedAccount(provider)));
507
+ return;
508
+ }
509
+ actions.appendChild(
510
+ authButton(
511
+ "Connect",
512
+ (button) => startConnectedAccountLogin(providerId, defaultMode, button),
513
+ ),
514
+ );
515
+ }
516
+
517
+ function authButton(label, action, className = "primary-button") {
518
+ const button = document.createElement("button");
519
+ button.type = "button";
520
+ button.className = className;
521
+ button.textContent = label;
522
+ button.addEventListener("click", () => action(button));
523
+ return button;
524
+ }
525
+
526
+ async function refreshConnectedAccounts() {
527
+ const providers = (state.config?.provider_status || []).filter(
528
+ (provider) => provider.kind === "connected_account",
529
+ );
530
+ await Promise.all(providers.map(refreshConnectedAccount));
531
+ }
532
+
533
+ async function refreshConnectedAccount(provider) {
534
+ const config = state.config;
535
+ clearConnectedAccountPoll(provider.provider_id);
536
+ updateConnectedAccountCard(provider, null);
537
+ try {
538
+ const status = await api(`/admin/api/providers/${provider.provider_id}/auth`);
539
+ if (config !== state.config) return;
540
+ updateConnectedAccountCard(provider, status);
541
+ if (status.state === "connecting") pollConnectedAccount(provider);
542
+ } catch (error) {
543
+ if (config !== state.config) return;
544
+ updateConnectedAccountCard(provider, {
545
+ state: "error",
546
+ connected: null,
547
+ message: error.message,
548
+ });
549
+ }
550
+ }
551
+
552
+ function updateConnectedAccountCard(provider, status) {
553
+ if (status) void refreshStartup();
554
+ if (JSON.stringify(state.authStatuses.get(provider.provider_id)) === JSON.stringify(status)) return;
555
+ state.authStatuses.set(provider.provider_id, status);
556
+ updateProviderCard(provider);
557
+ renderProviderDialogActions(provider);
558
+ }
559
+
560
+ async function startConnectedAccountLogin(providerId, mode, button) {
561
+ const buttons = button.closest(".provider-actions").querySelectorAll("button");
562
+ buttons.forEach((action) => { action.disabled = true; });
563
+ clearConnectedAccountPoll(providerId);
564
+ const popup = mode === "browser" ? window.open("about:blank", "_blank") : null;
565
+ if (popup) popup.opener = null;
566
+ try {
567
+ const status = await api(`/admin/api/providers/${providerId}/auth/login`, {
568
+ method: "POST",
569
+ body: JSON.stringify({ mode }),
570
+ });
571
+ const provider = connectedAccountDescriptor(providerId);
572
+ updateConnectedAccountCard(provider, status);
573
+ const target = status.authorization_url || status.verification_url;
574
+ if (mode === "browser") {
575
+ if (target && popup) {
576
+ popup.location.replace(target);
577
+ } else if (target) {
578
+ window.open(target, "_blank", "noopener");
579
+ } else if (popup) {
580
+ popup.close();
581
+ }
582
+ }
583
+ if (status.state === "connecting") pollConnectedAccount(provider);
584
+ else if (status.connected) await hydrateModelOptions();
585
+ } catch (error) {
586
+ if (popup) popup.close();
587
+ showMessage(error.message, true);
588
+ buttons.forEach((action) => { action.disabled = false; });
589
+ }
590
+ }
591
+
592
+ async function cancelConnectedAccountLogin(providerId) {
593
+ clearConnectedAccountPoll(providerId);
594
+ const provider = connectedAccountDescriptor(providerId);
595
+ try {
596
+ const status = await api(`/admin/api/providers/${providerId}/auth/cancel`, {
597
+ method: "POST",
598
+ });
599
+ updateConnectedAccountCard(provider, status);
600
+ } catch (error) {
601
+ showMessage(error.message, true);
602
+ pollConnectedAccount(provider);
603
+ }
604
+ }
605
+
606
+ async function disconnectConnectedAccount(providerId) {
607
+ const provider = connectedAccountDescriptor(providerId);
608
+ if (!window.confirm(`Disconnect this ${connectedAccountName(provider)} account from FCC?`)) return;
609
+ clearConnectedAccountPoll(providerId);
610
+ try {
611
+ const status = await api(`/admin/api/providers/${providerId}/auth`, { method: "DELETE" });
612
+ updateConnectedAccountCard(provider, status);
613
+ await hydrateModelOptions();
614
+ } catch (error) {
615
+ showMessage(error.message, true);
616
+ }
617
+ }
618
+
619
+ function pollConnectedAccount(provider) {
620
+ const providerId = provider.provider_id;
621
+ clearConnectedAccountPoll(providerId);
622
+ const poller = { timer: null };
623
+ state.authPollers.set(providerId, poller);
624
+ const poll = async () => {
625
+ try {
626
+ const status = await api(`/admin/api/providers/${providerId}/auth`);
627
+ if (state.authPollers.get(providerId) !== poller) return;
628
+ updateConnectedAccountCard(provider, status);
629
+ if (status.state === "connecting") {
630
+ poller.timer = window.setTimeout(poll, 1000);
631
+ } else {
632
+ state.authPollers.delete(providerId);
633
+ if (status.connected) await hydrateModelOptions();
634
+ }
635
+ } catch (error) {
636
+ if (state.authPollers.get(providerId) !== poller) return;
637
+ state.authPollers.delete(providerId);
638
+ showMessage(error.message, true);
639
+ }
640
+ };
641
+ poller.timer = window.setTimeout(poll, 1000);
642
+ }
643
+
644
+ function clearConnectedAccountPoll(providerId) {
645
+ const poller = state.authPollers.get(providerId);
646
+ if (poller?.timer) window.clearTimeout(poller.timer);
647
+ state.authPollers.delete(providerId);
648
+ }
649
+
650
+ function connectedAccountDescriptor(providerId) {
651
+ return state.config.provider_status.find(
652
+ (provider) => provider.provider_id === providerId,
653
+ );
654
+ }
655
+
656
+ async function copyDeviceCode(code) {
657
+ try {
658
+ await navigator.clipboard.writeText(code);
659
+ showMessage("Device code copied.");
660
+ } catch {
661
+ showMessage(`Copy this device code: ${code}`);
662
+ }
663
+ }
664
+
665
+ function modelCountMessage(count) {
666
+ return `${count} model${count === 1 ? "" : "s"} available`;
667
+ }
668
+
669
+ function providerCheckResult(providerId) {
670
+ const check = state.providerChecks.get(providerId);
671
+ // An explicit check takes precedence over background discovery and reachability.
672
+ if (check?.source === "manual") return check;
673
+ const discovery = state.startup?.startup?.providers?.[providerId];
674
+ if (discovery === "starting") return { status: "checking", message: "Checking models…" };
675
+ if (discovery === "failed") {
676
+ return { status: "error", message: "Could not load models. Check the provider's settings and retry." };
677
+ }
678
+ if (discovery === "ready") {
679
+ return { status: "ok", message: modelCountMessage(state.startup.cached_models[providerId]?.length || 0) };
680
+ }
681
+ return check;
682
+ }
683
+
684
+ function updateProviderCheckResult(providerId, status, message, source = "manual") {
685
+ state.providerChecks.set(providerId, { status, message, source });
686
+ renderProviderCheckResult(providerId);
687
+ }
688
+
689
+ function renderProviderCheckResult(providerId) {
690
+ const { status = "", message = "" } = providerCheckResult(providerId) || {};
691
+ const card = document.querySelector(`[data-provider="${providerId}"]`);
692
+ if (!card) return;
693
+ const provider = connectedAccountDescriptor(providerId);
694
+ if (provider.kind === "connected_account") {
695
+ const account = state.authStatuses.get(providerId);
696
+ const meta = card.querySelector(".provider-meta");
697
+ meta.textContent = connectedAccountMeta(provider, account);
698
+ meta.className = "provider-meta";
699
+ if (account?.connected && account.state !== "connecting") {
700
+ meta.classList.add("provider-check-result", status || "checking");
701
+ }
702
+ if (account?.state === "error") meta.classList.add("error");
703
+ return;
704
+ }
705
+ const result = card.querySelector(".provider-check-result");
706
+ result.className = `provider-check-result ${status}`;
707
+ result.textContent = message;
708
+ result.hidden = !message;
709
+ if (state.providerId === providerId) {
710
+ const modalResult = byId("providerDialogCheck");
711
+ modalResult.className = result.className;
712
+ modalResult.textContent = message;
713
+ modalResult.hidden = !message;
714
+ }
715
+ }
716
+
717
+ function renderSections(sections, fields) {
718
+ state.modelComboboxes.clear();
719
+ VIEW_GROUPS.filter((view) => view.sections.length).forEach((view) => {
720
+ byId(view.containerId).innerHTML = "";
721
+ });
722
+
723
+ const sectionById = new Map(sections.map((section) => [section.id, section]));
724
+ const bySection = new Map();
725
+ sections.forEach((section) => bySection.set(section.id, []));
726
+ fields.forEach((field) => {
727
+ if (!bySection.has(field.section)) bySection.set(field.section, []);
728
+ bySection.get(field.section).push(field);
729
+ });
730
+
731
+ VIEW_GROUPS.forEach((view) => {
732
+ const container = byId(view.containerId);
733
+ view.sections.forEach((sectionId) => {
734
+ const section = sectionById.get(sectionId);
735
+ const sectionFields = bySection.get(sectionId) || [];
736
+ if (!section || sectionFields.length === 0) return;
737
+
738
+ const sectionEl = document.createElement("section");
739
+ sectionEl.className = "settings-section";
740
+ sectionEl.id = `section-${section.id}`;
741
+
742
+ const heading = document.createElement("div");
743
+ heading.className = "section-heading";
744
+ heading.innerHTML = `<div><h3>${section.label}</h3><p>${section.description}</p></div>`;
745
+ if (section.id === "models") {
746
+ const refreshButton = document.createElement("button");
747
+ refreshButton.type = "button";
748
+ refreshButton.className = "secondary-button";
749
+ refreshButton.textContent = "Refresh models";
750
+ refreshButton.dataset.startupCatalog = "true";
751
+ refreshButton.addEventListener("click", () => refreshModelOptions(refreshButton));
752
+ heading.appendChild(refreshButton);
753
+ }
754
+ sectionEl.appendChild(heading);
755
+
756
+ const grid = document.createElement("div");
757
+ grid.className = "field-grid";
758
+ sectionFields.forEach((field) => {
759
+ grid.appendChild(renderField(field));
760
+ });
761
+ sectionEl.appendChild(grid);
762
+
763
+ if (sectionFields.some((field) => field.advanced)) {
764
+ const toggle = document.createElement("button");
765
+ toggle.type = "button";
766
+ toggle.className = "ghost-button advanced-toggle";
767
+ toggle.textContent = "Show advanced";
768
+ toggle.addEventListener("click", () => {
769
+ const showing = sectionEl.classList.toggle("show-advanced");
770
+ toggle.textContent = showing ? "Hide advanced" : "Show advanced";
771
+ });
772
+ sectionEl.appendChild(toggle);
773
+ }
774
+
775
+ container.appendChild(sectionEl);
776
+ });
777
+ });
778
+ }
779
+
780
+ function renderField(field) {
781
+ const wrapper = document.createElement("div");
782
+ wrapper.className = `field${field.advanced ? " advanced-field" : ""}`;
783
+ wrapper.dataset.key = field.key;
784
+
785
+ const label = document.createElement("label");
786
+ label.htmlFor = `field-${field.key}`;
787
+ const labelText = document.createElement("span");
788
+ labelText.textContent = field.label;
789
+ label.appendChild(labelText);
790
+
791
+ const source = sourceText(field);
792
+ if (source) {
793
+ const sourceEl = document.createElement("span");
794
+ sourceEl.className = "field-source";
795
+ sourceEl.textContent = source;
796
+ label.appendChild(sourceEl);
797
+ }
798
+
799
+ const input = window.FccFormControls.configure(inputForField(field));
800
+ input.id = `field-${field.key}`;
801
+ input.dataset.key = field.key;
802
+ input.dataset.original = comparableValue(field.value);
803
+ input.dataset.secret = field.secret ? "true" : "false";
804
+ input.dataset.configured = field.configured ? "true" : "false";
805
+ input.dataset.nullable = field.nullable ? "true" : "false";
806
+ input.dataset.remove = "false";
807
+ input.dataset.fieldType = field.type;
808
+ input.disabled = field.locked;
809
+ input.addEventListener("input", updateDirtyState);
810
+ input.addEventListener("change", updateDirtyState);
811
+ input.addEventListener("input", () => {
812
+ input.dataset.remove = "false";
813
+ clearCredentialError(input);
814
+ });
815
+ if (field.type === "optional_model") {
816
+ input.addEventListener("blur", () => {
817
+ if (!input.value.trim() || input.value.trim().toLowerCase() === "none") {
818
+ input.value = "None";
819
+ updateDirtyState();
820
+ }
821
+ });
822
+ }
823
+
824
+ let control = input;
825
+ if (field.type === "model" || field.type === "optional_model") {
826
+ control = createModelCombobox(input, field).element;
827
+ } else if (field.type === "model_list") {
828
+ const editor = new ModelListEditor(input, field);
829
+ label.htmlFor = editor.inputId;
830
+ control = editor.element;
831
+ }
832
+ wrapper.append(label, control);
833
+ if (field.secret && field.nullable && field.configured && !field.locked) {
834
+ const removeButton = document.createElement("button");
835
+ removeButton.type = "button";
836
+ removeButton.className = "ghost-button secret-remove";
837
+ removeButton.textContent = "Remove";
838
+ removeButton.addEventListener("click", () => {
839
+ const removing = input.dataset.remove !== "true";
840
+ input.dataset.remove = removing ? "true" : "false";
841
+ input.readOnly = removing;
842
+ removeButton.textContent = removing ? "Undo removal" : "Remove";
843
+ clearCredentialError(input);
844
+ updateDirtyState();
845
+ });
846
+ wrapper.appendChild(removeButton);
847
+ }
848
+ if (field.description) {
849
+ const description = document.createElement("div");
850
+ description.className = "field-description";
851
+ description.textContent = field.description;
852
+ wrapper.appendChild(description);
853
+ }
854
+ return wrapper;
855
+ }
856
+
857
+ function inputForField(field) {
858
+ if (field.type === "boolean") {
859
+ const input = document.createElement("input");
860
+ input.type = "checkbox";
861
+ input.checked = String(field.value).toLowerCase() === "true";
862
+ input.dataset.original = input.checked ? "true" : "false";
863
+ return input;
864
+ }
865
+
866
+ if (field.type === "select") {
867
+ const select = document.createElement("select");
868
+ field.options.forEach((item) =>
869
+ select.appendChild(option(item.value, item.label)),
870
+ );
871
+ select.value = field.value || field.options[0]?.value || "";
872
+ return select;
873
+ }
874
+
875
+ if (field.type === "textarea") {
876
+ const textarea = document.createElement("textarea");
877
+ textarea.value = field.value || "";
878
+ return textarea;
879
+ }
880
+
881
+ if (field.type === "model" || field.type === "optional_model") {
882
+ const input = document.createElement("input");
883
+ input.type = "text";
884
+ input.value = field.value || (field.type === "optional_model" ? "None" : "");
885
+ input.autocomplete = "off";
886
+ return input;
887
+ }
888
+
889
+ if (field.type === "model_list") {
890
+ const input = document.createElement("input");
891
+ input.type = "hidden";
892
+ input.value = field.value || "";
893
+ return input;
894
+ }
895
+
896
+ const input = document.createElement("input");
897
+ input.type = field.type === "number" ? "number" : "text";
898
+ if (field.type === "secret") {
899
+ input.setAttribute("autocapitalize", "none");
900
+ input.spellcheck = false;
901
+ input.setAttribute("autocorrect", "off");
902
+ input.placeholder = field.configured
903
+ ? "Configured - enter a new value to replace"
904
+ : "Not configured";
905
+ input.value = "";
906
+ input.autocomplete = "off";
907
+ } else {
908
+ input.value = field.value || "";
909
+ }
910
+ return input;
911
+ }
912
+
913
+ function createModelCombobox(input, field) {
914
+ return new window.FccModelCombobox(input, {
915
+ listboxId: `model-options-${field.key}`,
916
+ label: field.label,
917
+ values: () =>
918
+ field.type === "optional_model"
919
+ ? ["None", ...state.modelOptions]
920
+ : state.modelOptions,
921
+ emptyMessage: () =>
922
+ state.modelOptions.length
923
+ ? "No matching models. You can still enter a custom slug."
924
+ : "No discovered models. Refresh models or enter a custom slug.",
925
+ registry: state.modelComboboxes,
926
+ });
927
+ }
928
+
929
+ class ModelListEditor {
930
+ constructor(input, field) {
931
+ this.input = input;
932
+ this.field = field;
933
+ this.values = input.value
934
+ ? input.value.split(",").map((value) => value.trim()).filter(Boolean)
935
+ : [];
936
+ this.inputId = `field-${field.key}-add`;
937
+
938
+ this.element = document.createElement("div");
939
+ this.element.className = "model-list-editor";
940
+
941
+ const addRow = document.createElement("div");
942
+ addRow.className = "model-list-add";
943
+ this.addInput = window.FccFormControls.configure(document.createElement("input"));
944
+ this.addInput.id = this.inputId;
945
+ this.addInput.type = "text";
946
+ this.addInput.autocomplete = "off";
947
+ this.addInput.placeholder = "provider/model";
948
+ this.addInput.disabled = field.locked;
949
+ const addCombobox = createModelCombobox(this.addInput, {
950
+ ...field,
951
+ key: `${field.key}-add`,
952
+ label: "fallback model",
953
+ type: "model",
954
+ });
955
+
956
+ this.addButton = document.createElement("button");
957
+ this.addButton.type = "button";
958
+ this.addButton.className = "secondary-button";
959
+ this.addButton.textContent = "Add";
960
+ this.addButton.disabled = field.locked;
961
+ this.addButton.addEventListener("click", () => this.add());
962
+ addRow.append(addCombobox.element, this.addButton);
963
+
964
+ this.rows = document.createElement("div");
965
+ this.rows.className = "model-list-rows";
966
+ this.element.append(input, addRow, this.rows);
967
+ this.renderRows();
968
+ }
969
+
970
+ add() {
971
+ const value = this.addInput.value.trim();
972
+ if (!value) {
973
+ showMessage("Enter a full provider/model fallback.", "error");
974
+ return;
975
+ }
976
+ if (this.values.includes(value)) {
977
+ showMessage("That fallback model is already in the list.", "error");
978
+ return;
979
+ }
980
+ this.values.push(value);
981
+ this.addInput.value = "";
982
+ showMessage("");
983
+ this.sync();
984
+ }
985
+
986
+ move(index, offset) {
987
+ const destination = index + offset;
988
+ if (destination < 0 || destination >= this.values.length) return;
989
+ [this.values[index], this.values[destination]] = [
990
+ this.values[destination],
991
+ this.values[index],
992
+ ];
993
+ this.sync();
994
+ }
995
+
996
+ remove(index) {
997
+ this.values.splice(index, 1);
998
+ this.sync();
999
+ }
1000
+
1001
+ sync() {
1002
+ this.input.value = this.values.join(",");
1003
+ this.input.dataset.remove = "false";
1004
+ this.input.dispatchEvent(new Event("input", { bubbles: true }));
1005
+ this.renderRows();
1006
+ }
1007
+
1008
+ renderRows() {
1009
+ this.rows.innerHTML = "";
1010
+ if (this.values.length === 0) {
1011
+ const empty = document.createElement("div");
1012
+ empty.className = "model-list-empty";
1013
+ empty.textContent = "No fallback models configured.";
1014
+ this.rows.appendChild(empty);
1015
+ return;
1016
+ }
1017
+
1018
+ this.values.forEach((value, index) => {
1019
+ const row = document.createElement("div");
1020
+ row.className = "model-list-row";
1021
+
1022
+ const model = document.createElement("span");
1023
+ model.className = "model-list-value";
1024
+ model.textContent = value;
1025
+
1026
+ const up = this.actionButton("Move up", `Move ${value} up`, () =>
1027
+ this.move(index, -1),
1028
+ );
1029
+ up.disabled = this.field.locked || index === 0;
1030
+ const down = this.actionButton("Move down", `Move ${value} down`, () =>
1031
+ this.move(index, 1),
1032
+ );
1033
+ down.disabled = this.field.locked || index === this.values.length - 1;
1034
+ const remove = this.actionButton("Remove", `Remove ${value}`, () =>
1035
+ this.remove(index),
1036
+ );
1037
+ remove.disabled = this.field.locked;
1038
+
1039
+ row.append(model, up, down, remove);
1040
+ this.rows.appendChild(row);
1041
+ });
1042
+ }
1043
+
1044
+ actionButton(text, label, action) {
1045
+ const button = document.createElement("button");
1046
+ button.type = "button";
1047
+ button.className = "ghost-button model-list-action";
1048
+ button.textContent = text;
1049
+ button.setAttribute("aria-label", label);
1050
+ button.addEventListener("click", action);
1051
+ return button;
1052
+ }
1053
+ }
1054
+
1055
+ function option(value, label) {
1056
+ const optionEl = document.createElement("option");
1057
+ optionEl.value = value;
1058
+ optionEl.textContent = label;
1059
+ return optionEl;
1060
+ }
1061
+
1062
+ function readFieldValue(input) {
1063
+ if (input.type === "checkbox") return input.checked ? "true" : "false";
1064
+ if (input.dataset.remove === "true") return null;
1065
+ if (
1066
+ input.dataset.fieldType === "optional_model" &&
1067
+ input.value.trim().toLowerCase() === "none"
1068
+ ) {
1069
+ return null;
1070
+ }
1071
+ if (input.dataset.secret === "true" && input.dataset.configured === "true") {
1072
+ return input.value ? input.value : MASKED_SECRET;
1073
+ }
1074
+ if (input.dataset.nullable === "true" && !input.value.trim()) return null;
1075
+ return input.value;
1076
+ }
1077
+
1078
+ function comparableValue(value) {
1079
+ return value === null ? NULL_VALUE : String(value);
1080
+ }
1081
+
1082
+ function changedValues(root = byId("adminViews")) {
1083
+ const values = {};
1084
+ root.querySelectorAll("[data-key]").forEach((input) => {
1085
+ if (input.disabled || !input.matches("input, select, textarea")) return;
1086
+ const value = readFieldValue(input);
1087
+ if (comparableValue(value) !== input.dataset.original) {
1088
+ values[input.dataset.key] = value;
1089
+ }
1090
+ });
1091
+ return values;
1092
+ }
1093
+
1094
+ function updateDirtyState() {
1095
+ const count = Object.keys(changedValues()).length;
1096
+ byId("dirtyState").textContent =
1097
+ state.restart ? "Changes saved" : count === 0 ? "No changes" : `${count} unsaved change${count === 1 ? "" : "s"}`;
1098
+ byId("applyButton").disabled = state.applying || (!state.restart && count === 0);
1099
+ byId("saveProvider").disabled = state.applying || !!state.restart || !Object.keys(changedValues(byId("providerFields"))).length;
1100
+ }
1101
+
1102
+ function clearCredentialError(input) {
1103
+ byId(`${input.id}-error`)?.remove();
1104
+ input.removeAttribute("aria-invalid");
1105
+ input.removeAttribute("aria-describedby");
1106
+ }
1107
+
1108
+ function showCredentialErrors(checks) {
1109
+ let first = null;
1110
+ checks.forEach((check) => {
1111
+ const input = byId(`field-${check.key}`);
1112
+ if (!input) return;
1113
+ clearCredentialError(input);
1114
+ if (check.status !== "rejected") return;
1115
+ const error = document.createElement("div");
1116
+ error.id = `${input.id}-error`;
1117
+ error.className = "field-error";
1118
+ error.textContent = check.message;
1119
+ input.closest(".field").appendChild(error);
1120
+ input.setAttribute("aria-invalid", "true");
1121
+ input.setAttribute("aria-describedby", error.id);
1122
+ first ||= input;
1123
+ });
1124
+ return first;
1125
+ }
1126
+
1127
+ function setApplying(applying) {
1128
+ state.applying = applying;
1129
+ VIEW_GROUPS.filter((view) => view.id !== "code").forEach((view) => {
1130
+ byId(`view-${view.id}`).inert = applying || !!state.restart;
1131
+ });
1132
+ if (applying) state.modelComboboxes.forEach((combobox) => combobox.close());
1133
+ byId("providerDialogBody").inert = applying || !!state.restart;
1134
+ byId("closeProviderDialog").disabled = applying;
1135
+ byId("cancelProviderDialog").disabled = applying;
1136
+ byId("saveProvider").textContent = applying ? "Saving…" : "Save";
1137
+ byId("saveProvider").setAttribute("aria-busy", String(applying));
1138
+ byId("applyButton").textContent = state.restart
1139
+ ? applying ? "Reconnecting…" : "Reconnect"
1140
+ : applying ? "Applying…" : "Apply";
1141
+ updateDirtyState();
1142
+ }
1143
+
1144
+ async function waitForRestart(restart, target) {
1145
+ const deadline = performance.now() + 30_000;
1146
+ const statusUrl = new URL("/admin/api/status", target);
1147
+ while (performance.now() < deadline) {
1148
+ try {
1149
+ const response = await fetch(statusUrl, {
1150
+ cache: "no-store",
1151
+ credentials: "omit",
1152
+ signal: AbortSignal.timeout(1500),
1153
+ });
1154
+ if (response.ok) {
1155
+ const status = await response.json();
1156
+ if (status.status === "running" && typeof status.instance_id === "string"
1157
+ && status.instance_id !== restart.instance_id) return;
1158
+ }
1159
+ } catch {
1160
+ // Closing listeners and unfinished startup are expected during a restart.
1161
+ }
1162
+ await new Promise((resolve) => setTimeout(resolve, 250));
1163
+ }
1164
+ throw new Error("The server has not reconnected yet.");
1165
+ }
1166
+
1167
+ function appendAdminLink(target) {
1168
+ const link = document.createElement("a");
1169
+ link.href = target.href;
1170
+ link.textContent = "Open Admin";
1171
+ byId("messageArea").append(document.createElement("br"), link);
1172
+ }
1173
+
1174
+ async function reconnectAfterRestart() {
1175
+ clearTimeout(state.startupTimer);
1176
+ state.startupTimer = null;
1177
+ state.startupRequest?.controller.abort();
1178
+ state.startupRequest = null;
1179
+ state.startupAgain = false;
1180
+ const { restart, warnings, providersOnly } = state.restart;
1181
+ const target = new URL(restart.admin_url || "/admin", window.location.href);
1182
+ setApplying(true);
1183
+ showMessage(["Applied. Reconnecting to the server…", ...warnings].join("\n"), warnings.length ? "warn" : "ok");
1184
+ try {
1185
+ await waitForRestart(restart, target);
1186
+ if (target.origin !== window.location.origin) {
1187
+ // Carry only the safe warning text across an address change, never edits.
1188
+ target.hash = new URLSearchParams({ "fcc-applied": JSON.stringify(warnings) }).toString();
1189
+ window.location.replace(target.href);
1190
+ return;
1191
+ }
1192
+ await load({ providersOnly });
1193
+ state.restart = null;
1194
+ void refreshStartup();
1195
+ showMessage(["Applied", ...warnings].join("\n"), warnings.length ? "warn" : "ok");
1196
+ } catch (error) {
1197
+ showMessage([`Settings were saved. ${error.message} Use Reconnect to try again.`, ...warnings].join("\n"), "warn");
1198
+ appendAdminLink(target);
1199
+ } finally {
1200
+ setApplying(false);
1201
+ }
1202
+ }
1203
+
1204
+ function showRestartNotice() {
1205
+ const url = new URL(window.location.href);
1206
+ const fragment = new URLSearchParams(url.hash.slice(1));
1207
+ const notice = fragment.get("fcc-applied");
1208
+ if (notice === null) return;
1209
+ fragment.delete("fcc-applied");
1210
+ url.hash = fragment.toString();
1211
+ window.history.replaceState(window.history.state, "", url);
1212
+ try {
1213
+ const warnings = JSON.parse(notice);
1214
+ if (Array.isArray(warnings) && warnings.every((warning) => typeof warning === "string")) {
1215
+ showMessage(["Applied", ...warnings].join("\n"), warnings.length ? "warn" : "ok");
1216
+ }
1217
+ } catch {
1218
+ // A malformed navigation notice must not prevent normal Admin use.
1219
+ }
1220
+ }
1221
+
1222
+ async function apply(providerId = null) {
1223
+ if (state.applying) return;
1224
+ if (state.restart) {
1225
+ await reconnectAfterRestart();
1226
+ return;
1227
+ }
1228
+ const values = changedValues(providerId ? byId("providerFields") : byId("adminViews"));
1229
+ if (!Object.keys(values).length) return;
1230
+ const checkingKeys = Object.keys(values).some((key) => {
1231
+ const field = state.fields.get(key);
1232
+ return field?.secret && field.section === "providers" && values[key] !== null;
1233
+ });
1234
+ let rejectedField = null;
1235
+ let applied = false;
1236
+ setApplying(true);
1237
+ showMessage(checkingKeys ? "Checking API keys…" : "Applying…");
1238
+ try {
1239
+ const result = await api("/admin/api/config/apply", {
1240
+ method: "POST",
1241
+ body: JSON.stringify({ values }),
1242
+ });
1243
+ const checks = result.credential_checks || [];
1244
+ if (!result.applied) {
1245
+ rejectedField = showCredentialErrors(checks);
1246
+ showMessage(rejectedField ? "Not applied. Check the highlighted API keys." : result.errors.join("; "), "error");
1247
+ return;
1248
+ }
1249
+ applied = true;
1250
+ if (providerId) byId("providerDialog").close();
1251
+ const warnings = checks.filter((check) => check.status === "unverified").map((check) =>
1252
+ `${state.fields.get(check.key)?.label || check.key}: ${check.message}`
1253
+ );
1254
+ const restart = result.restart || {};
1255
+ if (restart.required && restart.automatic) {
1256
+ state.restart = { restart, warnings, providersOnly: !!providerId };
1257
+ await reconnectAfterRestart();
1258
+ return;
1259
+ }
1260
+ const pending = restart.required ? restart.fields || [] : result.pending_fields || [];
1261
+ await load({ providersOnly: !!providerId });
1262
+ const message = pending.length
1263
+ ? `Applied. Restart fcc-server to use: ${pending.join(", ")}`
1264
+ : "Applied";
1265
+ showMessage([message, ...warnings].join("\n"), warnings.length ? "warn" : "ok");
1266
+ } catch (error) {
1267
+ showMessage(applied ? `Applied, but could not reload settings: ${error.message}` : `Could not apply settings: ${error.message}`, "error");
1268
+ } finally {
1269
+ setApplying(false);
1270
+ if (rejectedField) {
1271
+ if (!providerId) navigateToView("providers");
1272
+ rejectedField.closest(".settings-section")?.classList.add("show-advanced");
1273
+ rejectedField.scrollIntoView({ block: "center", behavior: "instant" });
1274
+ rejectedField.focus();
1275
+ } else if (providerId && applied) {
1276
+ document.querySelector(`[data-provider="${providerId}"] [data-provider-settings]`)?.focus({ preventScroll: true });
1277
+ }
1278
+ }
1279
+ }
1280
+
1281
+ async function refreshLocalStatus(config) {
1282
+ const request = {
1283
+ providerIds: new Set(config.provider_status.filter((provider) =>
1284
+ provider.kind === "local" && provider.status === "configured",
1285
+ ).map((provider) => provider.provider_id)),
1286
+ };
1287
+ state.localStatusRequest = request;
1288
+ try {
1289
+ const result = await api("/admin/api/providers/local-status");
1290
+ if (state.localStatusRequest !== request) return;
1291
+ result.providers.forEach((provider) => {
1292
+ if (!request.providerIds.has(provider.provider_id) || provider.status === "missing_url") return;
1293
+ if (provider.status === "reachable") {
1294
+ updateProviderCheckResult(
1295
+ provider.provider_id,
1296
+ "ok",
1297
+ `Reachable: ${provider.base_url}`,
1298
+ "availability",
1299
+ );
1300
+ return;
1301
+ }
1302
+ const detail = provider.message
1303
+ ? provider.message
1304
+ : provider.status_code
1305
+ ? `${provider.base_url} returned HTTP ${provider.status_code}`
1306
+ : "The local provider did not respond.";
1307
+ updateProviderCheckResult(
1308
+ provider.provider_id,
1309
+ "error",
1310
+ `Unavailable: ${detail}`,
1311
+ "availability",
1312
+ );
1313
+ });
1314
+ } catch {
1315
+ if (state.localStatusRequest !== request) return;
1316
+ request.providerIds.forEach((providerId) => {
1317
+ updateProviderCheckResult(
1318
+ providerId,
1319
+ "error",
1320
+ "Availability check failed. Use Test to retry.",
1321
+ "availability",
1322
+ );
1323
+ });
1324
+ } finally {
1325
+ if (state.localStatusRequest === request) state.localStatusRequest = null;
1326
+ }
1327
+ }
1328
+
1329
+ async function testProvider(providerId, button) {
1330
+ const config = state.config;
1331
+ state.localStatusRequest?.providerIds.delete(providerId);
1332
+ const original = button.textContent;
1333
+ button.dataset.operationBusy = "true";
1334
+ button.disabled = true;
1335
+ button.textContent = "Checking...";
1336
+ updateProviderCheckResult(providerId, "checking", "Checking...");
1337
+ try {
1338
+ const result = await api(`/admin/api/providers/${providerId}/test`, {
1339
+ method: "POST",
1340
+ body: "{}",
1341
+ });
1342
+ if (config !== state.config) return;
1343
+ if (result.ok) {
1344
+ updateProviderCheckResult(
1345
+ providerId,
1346
+ "ok",
1347
+ modelCountMessage(result.models.length),
1348
+ );
1349
+ await hydrateModelOptions();
1350
+ } else {
1351
+ updateProviderCheckResult(
1352
+ providerId,
1353
+ "error",
1354
+ `Unavailable: ${result.message || "Provider check failed."}`,
1355
+ );
1356
+ }
1357
+ } catch {
1358
+ if (config !== state.config) return;
1359
+ updateProviderCheckResult(
1360
+ providerId,
1361
+ "error",
1362
+ "Provider check could not be completed.",
1363
+ );
1364
+ } finally {
1365
+ delete button.dataset.operationBusy;
1366
+ button.disabled = false;
1367
+ button.textContent = original;
1368
+ if (config === state.config) renderProviderDialogActions(connectedAccountDescriptor(providerId));
1369
+ void refreshStartup();
1370
+ renderStartup();
1371
+ }
1372
+ }
1373
+
1374
+ async function hydrateModelOptions() {
1375
+ try {
1376
+ await loadModelOptions();
1377
+ } catch {
1378
+ // Model fields remain editable when optional catalog hydration is unavailable.
1379
+ }
1380
+ }
1381
+
1382
+ async function loadModelOptions(refresh = false) {
1383
+ const request = ++state.modelOptionsRequest;
1384
+ const config = state.config;
1385
+ const result = await api("/admin/api/models" + (refresh ? "/refresh" : ""), {
1386
+ method: refresh ? "POST" : "GET",
1387
+ });
1388
+ if (request === state.modelOptionsRequest && config === state.config) setModelOptions(result.models);
1389
+ if (refresh && window.CodeSessions) await window.CodeSessions.refresh();
1390
+ return result;
1391
+ }
1392
+
1393
+ async function refreshModelOptions(button) {
1394
+ const original = button.textContent;
1395
+ button.dataset.operationBusy = "true";
1396
+ button.disabled = true;
1397
+ button.textContent = "Refreshing";
1398
+ try {
1399
+ const result = await loadModelOptions(true);
1400
+ const failedProviders = result.failed_providers || [];
1401
+ if (failedProviders.length) {
1402
+ const labels = failedProviders.map(providerDisplayName).join(", ");
1403
+ showMessage(
1404
+ `${state.modelOptions.length} models available; could not refresh ${labels}`,
1405
+ "warn",
1406
+ );
1407
+ } else {
1408
+ showMessage(`${state.modelOptions.length} models available`, "ok");
1409
+ }
1410
+ } catch (error) {
1411
+ showMessage(`Could not refresh models: ${error.message}`, "error");
1412
+ } finally {
1413
+ delete button.dataset.operationBusy;
1414
+ button.disabled = false;
1415
+ button.textContent = original;
1416
+ void refreshStartup();
1417
+ renderStartup();
1418
+ }
1419
+ }
1420
+
1421
+ function providerDisplayName(providerId) {
1422
+ const provider = state.config?.provider_status?.find(
1423
+ (candidate) => candidate.provider_id === providerId,
1424
+ );
1425
+ return provider?.display_name || providerId;
1426
+ }
1427
+
1428
+ function setModelOptions(models) {
1429
+ state.modelOptions = Array.from(
1430
+ new Set(models.filter((model) => typeof model === "string" && model.trim())),
1431
+ );
1432
+ state.modelComboboxes.forEach((combobox) => {
1433
+ if (combobox.isOpen) combobox.render(combobox.query);
1434
+ });
1435
+ }
1436
+
1437
+ function showMessage(message, kind = "") {
1438
+ const area = byId("messageArea");
1439
+ area.textContent = message;
1440
+ area.className = `message-area ${kind}`.trim();
1441
+ if (byId("providerDialog").open) {
1442
+ byId("providerMessage").textContent = message;
1443
+ byId("providerMessage").className = area.className;
1444
+ }
1445
+ }
1446
+
1447
+ byId("applyButton").addEventListener("click", () => apply());
1448
+ byId("saveProvider").addEventListener("click", () => apply(state.providerId));
1449
+ byId("closeProviderDialog").addEventListener("click", () => byId("providerDialog").close());
1450
+ byId("cancelProviderDialog").addEventListener("click", () => byId("providerDialog").close());
1451
+ byId("providerDialog").addEventListener("cancel", (event) => {
1452
+ if (state.applying) event.preventDefault();
1453
+ });
1454
+ byId("providerDialog").addEventListener("click", (event) => {
1455
+ if (event.target !== event.currentTarget || state.applying) return;
1456
+ const rect = event.currentTarget.getBoundingClientRect();
1457
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) event.currentTarget.close();
1458
+ });
1459
+ byId("providerDialog").addEventListener("close", () => {
1460
+ if (byId("providerDialog").open) return;
1461
+ const providerId = state.providerId;
1462
+ state.providerId = null;
1463
+ byId("providerFields").replaceChildren();
1464
+ updateDirtyState();
1465
+ document.querySelector(`[data-provider="${providerId}"] [data-provider-settings]`)?.focus({ preventScroll: true });
1466
+ });
1467
+ document.addEventListener("pointerdown", (event) => {
1468
+ state.modelComboboxes.forEach((combobox) => {
1469
+ if (combobox.isOpen && !combobox.element.contains(event.target)) combobox.close();
1470
+ });
1471
+ });
1472
+
1473
+ window.addEventListener("popstate", () => {
1474
+ const viewId = viewFromLocation();
1475
+ setActiveView(viewId, { scroll: false });
1476
+ });
1477
+
1478
+ try {
1479
+ for (const key of Object.keys(sessionStorage)) {
1480
+ if (key.startsWith("fcc.chat.draft.")) sessionStorage.removeItem(key);
1481
+ }
1482
+ } catch {
1483
+ console.warn("Chat draft cleanup deferred until the next page load: storage unavailable");
1484
+ }
1485
+
1486
+ const claudeIntegrationDialog = byId("claudeIntegrationDialog");
1487
+ const claudeIntegration = { connected: null, busy: false, paths: null, update: null };
1488
+ const claudeIntegrationPath = "/admin/api/integrations/claude-vscode";
1489
+
1490
+ function integrationMessage(id, message, error = false) {
1491
+ const element = byId(id);
1492
+ element.textContent = message;
1493
+ element.hidden = !message;
1494
+ element.classList.toggle("error", error);
1495
+ }
1496
+
1497
+ function integrationUpdating(integration, id) {
1498
+ return integration.update?.state === "starting" ||
1499
+ state.startup?.startup?.integrations?.[id]?.state === "starting";
1500
+ }
1501
+
1502
+ function refreshIntegrationUpdates(previous, current) {
1503
+ if (state.activeView !== "integrations") return;
1504
+ for (const [id, integration, refresh, messageId, message] of [
1505
+ ["claude-vscode", claudeIntegration, refreshClaudeIntegration, "claudeIntegrationMessage", "Settings updated. Reload VS Code."],
1506
+ ["jetbrains-acp", jetBrainsIntegration, refreshJetBrainsIntegration, "jetBrainsIntegrationMessage", "Settings updated. Reopen JetBrains and start a new chat."],
1507
+ ["codex", codexIntegration, refreshCodexIntegration, "codexIntegrationMessage", "Settings updated. Restart Codex."],
1508
+ ["claude-desktop", claudeDesktopIntegration, refreshClaudeDesktopIntegration, "claudeDesktopIntegrationMessage", "Settings updated. Reopen Claude Desktop."],
1509
+ ]) {
1510
+ const phase = current?.[id]?.state;
1511
+ if (phase && phase !== "starting" && (
1512
+ previous?.[id]?.state === "starting" || integration.update?.state === "starting"
1513
+ )) void refresh(false, { background: true });
1514
+ if (previous?.[id]?.state === "starting" && phase === "ready" && current[id].changed === true) {
1515
+ integrationMessage(messageId, message);
1516
+ }
1517
+ }
1518
+ }
1519
+
1520
+ function renderClaudeIntegration() {
1521
+ const { connected, paths } = claudeIntegration;
1522
+ const busy = claudeIntegration.busy || integrationUpdating(claudeIntegration, "claude-vscode");
1523
+ const action = connected ? "Disconnect" : "Connect";
1524
+ byId("openClaudeIntegration").textContent = busy ? "Loading…" : connected === null ? "Retry" : action;
1525
+ byId("openClaudeIntegration").disabled = busy;
1526
+ byId("openClaudeIntegration").setAttribute("aria-busy", String(busy));
1527
+ byId("confirmClaudeIntegration").textContent = busy ? "Saving…" : action;
1528
+ byId("confirmClaudeIntegration").disabled = busy || connected === null;
1529
+ byId("openClaudeIntegration").className = connected && !busy ? "danger-button" : "primary-button";
1530
+ byId("confirmClaudeIntegration").className = connected ? "danger-button" : "primary-button";
1531
+ byId("claudeIntegrationDescription").textContent = connected
1532
+ ? "Remove FCC's VS Code settings. Claude onboarding stays completed."
1533
+ : "Will set FCC's URL and token, enable model discovery, skip VS Code login, and complete Claude onboarding.";
1534
+ const files = byId("claudeIntegrationFiles");
1535
+ files.replaceChildren();
1536
+ if (paths) {
1537
+ const targets = connected ? [paths.vscode_settings] : [paths.vscode_settings, paths.claude_state];
1538
+ targets.forEach((path) => {
1539
+ const item = document.createElement("li");
1540
+ const code = document.createElement("code");
1541
+ code.textContent = path;
1542
+ item.appendChild(code);
1543
+ files.appendChild(item);
1544
+ });
1545
+ }
1546
+ }
1547
+
1548
+ async function refreshClaudeIntegration(retry = false, { background = false } = {}) {
1549
+ if (!background) integrationMessage("claudeIntegrationMessage", "");
1550
+ if (claudeIntegration.busy) return;
1551
+ claudeIntegration.busy = true;
1552
+ renderClaudeIntegration();
1553
+ try {
1554
+ if (retry) await api(`${claudeIntegrationPath}/refresh`, { method: "POST" });
1555
+ const result = await api(claudeIntegrationPath);
1556
+ claudeIntegration.connected = result.connected;
1557
+ claudeIntegration.paths = result.paths;
1558
+ claudeIntegration.update = result.update;
1559
+ if (result.update?.state === "failed") integrationMessage("claudeIntegrationMessage", result.update.message, true);
1560
+ else if (byId("claudeIntegrationMessage").classList.contains("error")) integrationMessage("claudeIntegrationMessage", "");
1561
+ } catch (error) {
1562
+ claudeIntegration.connected = null;
1563
+ claudeIntegration.update = null;
1564
+ integrationMessage("claudeIntegrationMessage", error.message, true);
1565
+ } finally {
1566
+ claudeIntegration.busy = false;
1567
+ renderClaudeIntegration();
1568
+ if (claudeIntegration.update?.state === "starting") void refreshStartup();
1569
+ }
1570
+ }
1571
+
1572
+ byId("openClaudeIntegration").addEventListener("click", () => {
1573
+ if (claudeIntegration.connected === null) {
1574
+ refreshClaudeIntegration(claudeIntegration.update?.state === "failed");
1575
+ return;
1576
+ }
1577
+ integrationMessage("claudeIntegrationDialogMessage", "");
1578
+ claudeIntegrationDialog.showModal();
1579
+ });
1580
+ byId("confirmClaudeIntegration").addEventListener("click", async () => {
1581
+ if (byId("confirmClaudeIntegration").disabled) return;
1582
+ const disconnect = claudeIntegration.connected;
1583
+ claudeIntegration.busy = true;
1584
+ renderClaudeIntegration();
1585
+ integrationMessage("claudeIntegrationDialogMessage", "");
1586
+ integrationMessage("claudeIntegrationMessage", "");
1587
+ try {
1588
+ const result = await api(`${claudeIntegrationPath}/${disconnect ? "disconnect" : "connect"}`, { method: "POST" });
1589
+ claudeIntegration.connected = result.connected;
1590
+ claudeIntegration.update = null;
1591
+ claudeIntegrationDialog.close();
1592
+ integrationMessage("claudeIntegrationMessage", disconnect
1593
+ ? "Settings removed. Reload VS Code to disconnect."
1594
+ : "Settings saved. Reload VS Code to connect.");
1595
+ } catch (error) {
1596
+ integrationMessage("claudeIntegrationDialogMessage", error.message, true);
1597
+ integrationMessage("claudeIntegrationMessage", error.message, true);
1598
+ } finally {
1599
+ claudeIntegration.busy = false;
1600
+ renderClaudeIntegration();
1601
+ }
1602
+ });
1603
+ byId("closeClaudeIntegration").addEventListener("click", () => claudeIntegrationDialog.close());
1604
+ claudeIntegrationDialog.addEventListener("click", (event) => {
1605
+ if (event.target !== claudeIntegrationDialog) return;
1606
+ const bounds = claudeIntegrationDialog.getBoundingClientRect();
1607
+ if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) {
1608
+ claudeIntegrationDialog.close();
1609
+ }
1610
+ });
1611
+
1612
+ const codexIntegrationDialog = byId("codexIntegrationDialog");
1613
+ const codexIntegration = { connected: null, busy: false, paths: null, update: null };
1614
+ const codexIntegrationPath = "/admin/api/integrations/codex";
1615
+
1616
+ function renderCodexIntegration() {
1617
+ const { connected, paths } = codexIntegration;
1618
+ const initializing = connected === false && state.startup?.startup?.catalog_file === "starting";
1619
+ const unavailable = connected === false && state.startup?.startup?.catalog_file === "failed";
1620
+ const busy = codexIntegration.busy || initializing || integrationUpdating(codexIntegration, "codex");
1621
+ const catalogError = "Could not prepare the Codex model catalog. Refresh models to retry.";
1622
+ if (unavailable) integrationMessage("codexIntegrationMessage", catalogError, true);
1623
+ else if (byId("codexIntegrationMessage").textContent === catalogError) integrationMessage("codexIntegrationMessage", "");
1624
+ const action = connected ? "Disconnect" : "Connect";
1625
+ byId("openCodexIntegration").textContent = busy ? "Loading…" : connected === null ? "Retry" : action;
1626
+ byId("openCodexIntegration").disabled = busy;
1627
+ byId("openCodexIntegration").setAttribute("aria-busy", String(busy));
1628
+ byId("confirmCodexIntegration").textContent = busy ? "Saving…" : action;
1629
+ byId("confirmCodexIntegration").disabled = busy || connected === null || unavailable;
1630
+ byId("openCodexIntegration").className = connected && !busy ? "danger-button" : "primary-button";
1631
+ byId("confirmCodexIntegration").className = connected ? "danger-button" : "primary-button";
1632
+ byId("codexIntegrationDescription").textContent = connected
1633
+ ? "Remove FCC's Codex configuration. Other settings stay unchanged."
1634
+ : "Configure Codex to use FCC. Your selected model stays unchanged.";
1635
+ const files = byId("codexIntegrationFiles");
1636
+ files.replaceChildren();
1637
+ if (paths) {
1638
+ const targets = [paths.codex_config];
1639
+ targets.forEach((path) => {
1640
+ const item = document.createElement("li");
1641
+ const code = document.createElement("code");
1642
+ code.textContent = path;
1643
+ item.appendChild(code);
1644
+ files.appendChild(item);
1645
+ });
1646
+ }
1647
+ }
1648
+
1649
+ async function refreshCodexIntegration(retry = false, { background = false } = {}) {
1650
+ if (!background) integrationMessage("codexIntegrationMessage", "");
1651
+ if (codexIntegration.busy) return;
1652
+ codexIntegration.busy = true;
1653
+ renderCodexIntegration();
1654
+ try {
1655
+ if (retry) await api(`${codexIntegrationPath}/refresh`, { method: "POST" });
1656
+ const result = await api(codexIntegrationPath);
1657
+ codexIntegration.connected = result.connected;
1658
+ codexIntegration.paths = result.paths;
1659
+ codexIntegration.update = result.update;
1660
+ if (result.update?.state === "failed") integrationMessage("codexIntegrationMessage", result.update.message, true);
1661
+ else if (byId("codexIntegrationMessage").classList.contains("error")) integrationMessage("codexIntegrationMessage", "");
1662
+ } catch (error) {
1663
+ codexIntegration.connected = null;
1664
+ codexIntegration.update = null;
1665
+ integrationMessage("codexIntegrationMessage", error.message, true);
1666
+ } finally {
1667
+ codexIntegration.busy = false;
1668
+ renderCodexIntegration();
1669
+ if (codexIntegration.update?.state === "starting") void refreshStartup();
1670
+ }
1671
+ }
1672
+
1673
+ byId("openCodexIntegration").addEventListener("click", () => {
1674
+ if (codexIntegration.connected === null) {
1675
+ refreshCodexIntegration(codexIntegration.update?.state === "failed");
1676
+ return;
1677
+ }
1678
+ integrationMessage("codexIntegrationDialogMessage", "");
1679
+ codexIntegrationDialog.showModal();
1680
+ });
1681
+ byId("confirmCodexIntegration").addEventListener("click", async () => {
1682
+ if (byId("confirmCodexIntegration").disabled) return;
1683
+ const disconnect = codexIntegration.connected;
1684
+ codexIntegration.busy = true;
1685
+ renderCodexIntegration();
1686
+ integrationMessage("codexIntegrationDialogMessage", "");
1687
+ integrationMessage("codexIntegrationMessage", "");
1688
+ try {
1689
+ const result = await api(`${codexIntegrationPath}/${disconnect ? "disconnect" : "connect"}`, { method: "POST" });
1690
+ codexIntegration.connected = result.connected;
1691
+ codexIntegration.paths = result.paths;
1692
+ codexIntegration.update = null;
1693
+ codexIntegrationDialog.close();
1694
+ integrationMessage("codexIntegrationMessage", disconnect
1695
+ ? "Settings removed. Restart Codex to disconnect."
1696
+ : "Settings saved. Restart Codex and select an FCC model.");
1697
+ } catch (error) {
1698
+ integrationMessage("codexIntegrationDialogMessage", error.message, true);
1699
+ integrationMessage("codexIntegrationMessage", error.message, true);
1700
+ } finally {
1701
+ codexIntegration.busy = false;
1702
+ renderCodexIntegration();
1703
+ }
1704
+ });
1705
+ byId("closeCodexIntegration").addEventListener("click", () => codexIntegrationDialog.close());
1706
+ codexIntegrationDialog.addEventListener("click", (event) => {
1707
+ if (event.target !== codexIntegrationDialog) return;
1708
+ const bounds = codexIntegrationDialog.getBoundingClientRect();
1709
+ if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) {
1710
+ codexIntegrationDialog.close();
1711
+ }
1712
+ });
1713
+
1714
+ const jetBrainsIntegrationDialog = byId("jetBrainsIntegrationDialog");
1715
+ const jetBrainsIntegration = { connected: null, busy: false, paths: null, update: null };
1716
+ const jetBrainsIntegrationPath = "/admin/api/integrations/jetbrains-acp";
1717
+
1718
+ function renderJetBrainsIntegration() {
1719
+ const { connected, paths } = jetBrainsIntegration;
1720
+ const busy = jetBrainsIntegration.busy || integrationUpdating(jetBrainsIntegration, "jetbrains-acp");
1721
+ const action = connected ? "Disconnect" : "Connect";
1722
+ byId("openJetBrainsIntegration").textContent = busy ? "Loading…" : connected === null ? "Retry" : action;
1723
+ byId("openJetBrainsIntegration").disabled = busy;
1724
+ byId("openJetBrainsIntegration").setAttribute("aria-busy", String(busy));
1725
+ byId("confirmJetBrainsIntegration").textContent = busy ? "Saving…" : action;
1726
+ byId("confirmJetBrainsIntegration").disabled = busy || connected === null;
1727
+ byId("openJetBrainsIntegration").className = connected && !busy ? "danger-button" : "primary-button";
1728
+ byId("confirmJetBrainsIntegration").className = connected ? "danger-button" : "primary-button";
1729
+ byId("jetBrainsIntegrationDescription").textContent = connected
1730
+ ? "Remove the Claude Code (FCC) agent, including its custom settings. Finish any configuration edits first."
1731
+ : "Add Claude Code (FCC) using JetBrains' installed Claude Agent. Start that agent once before connecting, and finish any configuration edits.";
1732
+ const files = byId("jetBrainsIntegrationFiles");
1733
+ files.replaceChildren();
1734
+ if (paths) {
1735
+ const targets = [paths.acp_config];
1736
+ targets.forEach((path) => {
1737
+ const item = document.createElement("li");
1738
+ const code = document.createElement("code");
1739
+ code.textContent = path;
1740
+ item.appendChild(code);
1741
+ files.appendChild(item);
1742
+ });
1743
+ }
1744
+ }
1745
+
1746
+ async function refreshJetBrainsIntegration(retry = false, { background = false } = {}) {
1747
+ if (!background) integrationMessage("jetBrainsIntegrationMessage", "");
1748
+ if (jetBrainsIntegration.busy) return;
1749
+ jetBrainsIntegration.busy = true;
1750
+ renderJetBrainsIntegration();
1751
+ try {
1752
+ if (retry) await api(`${jetBrainsIntegrationPath}/refresh`, { method: "POST" });
1753
+ const result = await api(jetBrainsIntegrationPath);
1754
+ jetBrainsIntegration.connected = result.connected;
1755
+ jetBrainsIntegration.paths = result.paths;
1756
+ jetBrainsIntegration.update = result.update;
1757
+ if (result.update?.state === "failed") integrationMessage("jetBrainsIntegrationMessage", result.update.message, true);
1758
+ else if (byId("jetBrainsIntegrationMessage").classList.contains("error")) integrationMessage("jetBrainsIntegrationMessage", "");
1759
+ } catch (error) {
1760
+ jetBrainsIntegration.connected = null;
1761
+ jetBrainsIntegration.update = null;
1762
+ integrationMessage("jetBrainsIntegrationMessage", error.message, true);
1763
+ } finally {
1764
+ jetBrainsIntegration.busy = false;
1765
+ renderJetBrainsIntegration();
1766
+ if (jetBrainsIntegration.update?.state === "starting") void refreshStartup();
1767
+ }
1768
+ }
1769
+
1770
+ byId("openJetBrainsIntegration").addEventListener("click", () => {
1771
+ if (jetBrainsIntegration.connected === null) {
1772
+ refreshJetBrainsIntegration(jetBrainsIntegration.update?.state === "failed");
1773
+ return;
1774
+ }
1775
+ integrationMessage("jetBrainsIntegrationDialogMessage", "");
1776
+ jetBrainsIntegrationDialog.showModal();
1777
+ });
1778
+ byId("confirmJetBrainsIntegration").addEventListener("click", async () => {
1779
+ if (byId("confirmJetBrainsIntegration").disabled) return;
1780
+ const disconnect = jetBrainsIntegration.connected;
1781
+ jetBrainsIntegration.busy = true;
1782
+ renderJetBrainsIntegration();
1783
+ integrationMessage("jetBrainsIntegrationDialogMessage", "");
1784
+ integrationMessage("jetBrainsIntegrationMessage", "");
1785
+ try {
1786
+ const result = await api(`${jetBrainsIntegrationPath}/${disconnect ? "disconnect" : "connect"}`, { method: "POST" });
1787
+ jetBrainsIntegration.connected = result.connected;
1788
+ jetBrainsIntegration.update = null;
1789
+ jetBrainsIntegrationDialog.close();
1790
+ integrationMessage("jetBrainsIntegrationMessage", disconnect
1791
+ ? "Agent removed. Reopen JetBrains to disconnect."
1792
+ : "Settings saved. Reopen JetBrains, select Claude Code (FCC), and start a new chat.");
1793
+ } catch (error) {
1794
+ integrationMessage("jetBrainsIntegrationDialogMessage", error.message, true);
1795
+ integrationMessage("jetBrainsIntegrationMessage", error.message, true);
1796
+ } finally {
1797
+ jetBrainsIntegration.busy = false;
1798
+ renderJetBrainsIntegration();
1799
+ }
1800
+ });
1801
+ byId("closeJetBrainsIntegration").addEventListener("click", () => jetBrainsIntegrationDialog.close());
1802
+ jetBrainsIntegrationDialog.addEventListener("click", (event) => {
1803
+ if (event.target !== jetBrainsIntegrationDialog) return;
1804
+ const bounds = jetBrainsIntegrationDialog.getBoundingClientRect();
1805
+ if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) {
1806
+ jetBrainsIntegrationDialog.close();
1807
+ }
1808
+ });
1809
+
1810
+ const claudeDesktopIntegrationDialog = byId("claudeDesktopIntegrationDialog");
1811
+ const claudeDesktopIntegration = { connected: null, disconnectPending: false, busy: false, paths: null, update: null };
1812
+ const claudeDesktopIntegrationPath = "/admin/api/integrations/claude-desktop";
1813
+
1814
+ function renderClaudeDesktopIntegration() {
1815
+ const { connected, paths, disconnectPending } = claudeDesktopIntegration;
1816
+ const busy = claudeDesktopIntegration.busy || integrationUpdating(claudeDesktopIntegration, "claude-desktop");
1817
+ const disconnect = disconnectPending || connected;
1818
+ const action = disconnectPending ? "Retry disconnect" : connected ? "Disconnect" : "Connect";
1819
+ byId("openClaudeDesktopIntegration").textContent = busy ? "Loading…" : connected === null && !disconnectPending ? "Retry" : action;
1820
+ byId("openClaudeDesktopIntegration").disabled = busy;
1821
+ byId("openClaudeDesktopIntegration").setAttribute("aria-busy", String(busy));
1822
+ byId("confirmClaudeDesktopIntegration").textContent = busy ? "Saving…" : action;
1823
+ byId("confirmClaudeDesktopIntegration").disabled = busy || (connected === null && !disconnectPending);
1824
+ byId("openClaudeDesktopIntegration").className = disconnect && !busy ? "danger-button" : "primary-button";
1825
+ byId("confirmClaudeDesktopIntegration").className = disconnect ? "danger-button" : "primary-button";
1826
+ byId("claudeDesktopIntegrationDescription").textContent = disconnectPending
1827
+ ? "Finish removing FCC's configuration. Fully quit Claude Desktop before retrying, then reopen it."
1828
+ : connected
1829
+ ? "Remove FCC's configuration and return to normal Claude sign-in. Fully quit Claude Desktop first, then reopen it."
1830
+ : "Set FCC as Claude Desktop's gateway. Fully quit Claude Desktop before connecting, then reopen it.";
1831
+ const files = byId("claudeDesktopIntegrationFiles");
1832
+ files.replaceChildren();
1833
+ if (paths) {
1834
+ const targets = Object.values(paths);
1835
+ targets.forEach((path) => {
1836
+ const item = document.createElement("li");
1837
+ const code = document.createElement("code");
1838
+ code.textContent = path;
1839
+ item.appendChild(code);
1840
+ files.appendChild(item);
1841
+ });
1842
+ }
1843
+ }
1844
+
1845
+ async function refreshClaudeDesktopIntegration(retry = false, { background = false } = {}) {
1846
+ if (!background) integrationMessage("claudeDesktopIntegrationMessage", "");
1847
+ if (claudeDesktopIntegration.busy) return;
1848
+ claudeDesktopIntegration.busy = true;
1849
+ renderClaudeDesktopIntegration();
1850
+ try {
1851
+ if (retry) await api(`${claudeDesktopIntegrationPath}/refresh`, { method: "POST" });
1852
+ const result = await api(claudeDesktopIntegrationPath);
1853
+ claudeDesktopIntegration.connected = result.connected;
1854
+ claudeDesktopIntegration.disconnectPending = result.disconnect_pending;
1855
+ claudeDesktopIntegration.paths = result.paths;
1856
+ claudeDesktopIntegration.update = result.update;
1857
+ if (result.update?.state === "failed") integrationMessage("claudeDesktopIntegrationMessage", result.update.message, true);
1858
+ else if (byId("claudeDesktopIntegrationMessage").classList.contains("error")) integrationMessage("claudeDesktopIntegrationMessage", "");
1859
+ } catch (error) {
1860
+ claudeDesktopIntegration.connected = null;
1861
+ claudeDesktopIntegration.disconnectPending = false;
1862
+ claudeDesktopIntegration.update = null;
1863
+ integrationMessage("claudeDesktopIntegrationMessage", error.message, true);
1864
+ } finally {
1865
+ claudeDesktopIntegration.busy = false;
1866
+ renderClaudeDesktopIntegration();
1867
+ if (claudeDesktopIntegration.update?.state === "starting") void refreshStartup();
1868
+ }
1869
+ }
1870
+
1871
+ byId("openClaudeDesktopIntegration").addEventListener("click", () => {
1872
+ if (claudeDesktopIntegration.connected === null && !claudeDesktopIntegration.disconnectPending) {
1873
+ refreshClaudeDesktopIntegration(claudeDesktopIntegration.update?.state === "failed");
1874
+ return;
1875
+ }
1876
+ integrationMessage("claudeDesktopIntegrationDialogMessage", "");
1877
+ claudeDesktopIntegrationDialog.showModal();
1878
+ });
1879
+ byId("confirmClaudeDesktopIntegration").addEventListener("click", async () => {
1880
+ if (byId("confirmClaudeDesktopIntegration").disabled) return;
1881
+ const disconnect = claudeDesktopIntegration.disconnectPending || claudeDesktopIntegration.connected;
1882
+ claudeDesktopIntegration.busy = true;
1883
+ renderClaudeDesktopIntegration();
1884
+ integrationMessage("claudeDesktopIntegrationDialogMessage", "");
1885
+ integrationMessage("claudeDesktopIntegrationMessage", "");
1886
+ try {
1887
+ const result = await api(`${claudeDesktopIntegrationPath}/${disconnect ? "disconnect" : "connect"}`, { method: "POST" });
1888
+ claudeDesktopIntegration.connected = result.connected;
1889
+ claudeDesktopIntegration.disconnectPending = result.disconnect_pending;
1890
+ claudeDesktopIntegration.paths = result.paths;
1891
+ claudeDesktopIntegration.update = null;
1892
+ claudeDesktopIntegrationDialog.close();
1893
+ integrationMessage("claudeDesktopIntegrationMessage", disconnect
1894
+ ? "Settings removed. Reopen Claude Desktop to disconnect."
1895
+ : "Settings saved. Reopen Claude Desktop to connect.");
1896
+ } catch (error) {
1897
+ if (disconnect) {
1898
+ try {
1899
+ const result = await api(claudeDesktopIntegrationPath);
1900
+ claudeDesktopIntegration.connected = result.connected;
1901
+ claudeDesktopIntegration.disconnectPending = result.disconnect_pending;
1902
+ claudeDesktopIntegration.paths = result.paths;
1903
+ claudeDesktopIntegration.update = result.update;
1904
+ } catch {
1905
+ // Keep the user's disconnect action available if status is unreadable too.
1906
+ }
1907
+ }
1908
+ integrationMessage("claudeDesktopIntegrationDialogMessage", error.message, true);
1909
+ integrationMessage("claudeDesktopIntegrationMessage", error.message, true);
1910
+ } finally {
1911
+ claudeDesktopIntegration.busy = false;
1912
+ renderClaudeDesktopIntegration();
1913
+ }
1914
+ });
1915
+ byId("closeClaudeDesktopIntegration").addEventListener("click", () => claudeDesktopIntegrationDialog.close());
1916
+ claudeDesktopIntegrationDialog.addEventListener("click", (event) => {
1917
+ if (event.target !== claudeDesktopIntegrationDialog) return;
1918
+ const bounds = claudeDesktopIntegrationDialog.getBoundingClientRect();
1919
+ if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) {
1920
+ claudeDesktopIntegrationDialog.close();
1921
+ }
1922
+ });
1923
+
1924
+ // Keep footer clearance exact when messages wrap or a view hides the bar.
1925
+ new ResizeObserver(([entry]) => {
1926
+ document.documentElement.style.setProperty(
1927
+ "--action-bar-height",
1928
+ `${entry.target.getBoundingClientRect().height}px`,
1929
+ );
1930
+ }).observe(document.querySelector(".action-bar"), { box: "border-box" });
1931
+
1932
+ load().then(showRestartNotice).catch((error) => {
1933
+ showMessage(error.message, "error");
1934
+ });