better-codex 0.1.0

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 (405) hide show
  1. package/README.md +26 -0
  2. package/apps/backend/README.md +46 -0
  3. package/apps/backend/bun.lock +64 -0
  4. package/apps/backend/package.json +18 -0
  5. package/apps/backend/scripts/generate-protocol.ts +32 -0
  6. package/apps/backend/src/analytics/service.ts +219 -0
  7. package/apps/backend/src/analytics/store.ts +284 -0
  8. package/apps/backend/src/config.ts +98 -0
  9. package/apps/backend/src/core/app-server.ts +131 -0
  10. package/apps/backend/src/core/jsonrpc.ts +166 -0
  11. package/apps/backend/src/protocol/AbsolutePathBuf.ts +14 -0
  12. package/apps/backend/src/protocol/AddConversationListenerParams.ts +6 -0
  13. package/apps/backend/src/protocol/AddConversationSubscriptionResponse.ts +5 -0
  14. package/apps/backend/src/protocol/AgentMessageContent.ts +5 -0
  15. package/apps/backend/src/protocol/AgentMessageContentDeltaEvent.ts +5 -0
  16. package/apps/backend/src/protocol/AgentMessageDeltaEvent.ts +5 -0
  17. package/apps/backend/src/protocol/AgentMessageEvent.ts +5 -0
  18. package/apps/backend/src/protocol/AgentMessageItem.ts +6 -0
  19. package/apps/backend/src/protocol/AgentReasoningDeltaEvent.ts +5 -0
  20. package/apps/backend/src/protocol/AgentReasoningEvent.ts +5 -0
  21. package/apps/backend/src/protocol/AgentReasoningRawContentDeltaEvent.ts +5 -0
  22. package/apps/backend/src/protocol/AgentReasoningRawContentEvent.ts +5 -0
  23. package/apps/backend/src/protocol/AgentReasoningSectionBreakEvent.ts +5 -0
  24. package/apps/backend/src/protocol/Annotations.ts +9 -0
  25. package/apps/backend/src/protocol/ApplyPatchApprovalParams.ts +21 -0
  26. package/apps/backend/src/protocol/ApplyPatchApprovalRequestEvent.ts +23 -0
  27. package/apps/backend/src/protocol/ApplyPatchApprovalResponse.ts +6 -0
  28. package/apps/backend/src/protocol/ArchiveConversationParams.ts +6 -0
  29. package/apps/backend/src/protocol/ArchiveConversationResponse.ts +5 -0
  30. package/apps/backend/src/protocol/AskForApproval.ts +9 -0
  31. package/apps/backend/src/protocol/AudioContent.ts +9 -0
  32. package/apps/backend/src/protocol/AuthMode.ts +5 -0
  33. package/apps/backend/src/protocol/AuthStatusChangeNotification.ts +9 -0
  34. package/apps/backend/src/protocol/BackgroundEventEvent.ts +5 -0
  35. package/apps/backend/src/protocol/BlobResourceContents.ts +5 -0
  36. package/apps/backend/src/protocol/CallToolResult.ts +10 -0
  37. package/apps/backend/src/protocol/CancelLoginChatGptParams.ts +5 -0
  38. package/apps/backend/src/protocol/CancelLoginChatGptResponse.ts +5 -0
  39. package/apps/backend/src/protocol/ClientInfo.ts +5 -0
  40. package/apps/backend/src/protocol/ClientNotification.ts +5 -0
  41. package/apps/backend/src/protocol/ClientRequest.ts +46 -0
  42. package/apps/backend/src/protocol/CodexErrorInfo.ts +8 -0
  43. package/apps/backend/src/protocol/ContentBlock.ts +10 -0
  44. package/apps/backend/src/protocol/ContentItem.ts +5 -0
  45. package/apps/backend/src/protocol/ContextCompactedEvent.ts +5 -0
  46. package/apps/backend/src/protocol/ConversationGitInfo.ts +5 -0
  47. package/apps/backend/src/protocol/ConversationId.ts +5 -0
  48. package/apps/backend/src/protocol/ConversationSummary.ts +8 -0
  49. package/apps/backend/src/protocol/CreditsSnapshot.ts +5 -0
  50. package/apps/backend/src/protocol/CustomPrompt.ts +5 -0
  51. package/apps/backend/src/protocol/DeprecationNoticeEvent.ts +13 -0
  52. package/apps/backend/src/protocol/ElicitationRequestEvent.ts +6 -0
  53. package/apps/backend/src/protocol/EmbeddedResource.ts +13 -0
  54. package/apps/backend/src/protocol/EmbeddedResourceResource.ts +7 -0
  55. package/apps/backend/src/protocol/ErrorEvent.ts +6 -0
  56. package/apps/backend/src/protocol/EventMsg.ts +60 -0
  57. package/apps/backend/src/protocol/ExecApprovalRequestEvent.ts +32 -0
  58. package/apps/backend/src/protocol/ExecCommandApprovalParams.ts +12 -0
  59. package/apps/backend/src/protocol/ExecCommandApprovalResponse.ts +6 -0
  60. package/apps/backend/src/protocol/ExecCommandBeginEvent.ts +35 -0
  61. package/apps/backend/src/protocol/ExecCommandEndEvent.ts +59 -0
  62. package/apps/backend/src/protocol/ExecCommandOutputDeltaEvent.ts +18 -0
  63. package/apps/backend/src/protocol/ExecCommandSource.ts +5 -0
  64. package/apps/backend/src/protocol/ExecOneOffCommandParams.ts +6 -0
  65. package/apps/backend/src/protocol/ExecOneOffCommandResponse.ts +5 -0
  66. package/apps/backend/src/protocol/ExecOutputStream.ts +5 -0
  67. package/apps/backend/src/protocol/ExecPolicyAmendment.ts +12 -0
  68. package/apps/backend/src/protocol/ExitedReviewModeEvent.ts +6 -0
  69. package/apps/backend/src/protocol/FileChange.ts +5 -0
  70. package/apps/backend/src/protocol/ForcedLoginMethod.ts +5 -0
  71. package/apps/backend/src/protocol/FunctionCallOutputContentItem.ts +9 -0
  72. package/apps/backend/src/protocol/FunctionCallOutputPayload.ts +15 -0
  73. package/apps/backend/src/protocol/FuzzyFileSearchParams.ts +5 -0
  74. package/apps/backend/src/protocol/FuzzyFileSearchResponse.ts +6 -0
  75. package/apps/backend/src/protocol/FuzzyFileSearchResult.ts +8 -0
  76. package/apps/backend/src/protocol/GetAuthStatusParams.ts +5 -0
  77. package/apps/backend/src/protocol/GetAuthStatusResponse.ts +6 -0
  78. package/apps/backend/src/protocol/GetConversationSummaryParams.ts +6 -0
  79. package/apps/backend/src/protocol/GetConversationSummaryResponse.ts +6 -0
  80. package/apps/backend/src/protocol/GetHistoryEntryResponseEvent.ts +10 -0
  81. package/apps/backend/src/protocol/GetUserAgentResponse.ts +5 -0
  82. package/apps/backend/src/protocol/GetUserSavedConfigResponse.ts +6 -0
  83. package/apps/backend/src/protocol/GhostCommit.ts +8 -0
  84. package/apps/backend/src/protocol/GitDiffToRemoteParams.ts +5 -0
  85. package/apps/backend/src/protocol/GitDiffToRemoteResponse.ts +6 -0
  86. package/apps/backend/src/protocol/GitSha.ts +5 -0
  87. package/apps/backend/src/protocol/HistoryEntry.ts +5 -0
  88. package/apps/backend/src/protocol/ImageContent.ts +9 -0
  89. package/apps/backend/src/protocol/InitializeParams.ts +6 -0
  90. package/apps/backend/src/protocol/InitializeResponse.ts +5 -0
  91. package/apps/backend/src/protocol/InputItem.ts +5 -0
  92. package/apps/backend/src/protocol/InterruptConversationParams.ts +6 -0
  93. package/apps/backend/src/protocol/InterruptConversationResponse.ts +6 -0
  94. package/apps/backend/src/protocol/ItemCompletedEvent.ts +7 -0
  95. package/apps/backend/src/protocol/ItemStartedEvent.ts +7 -0
  96. package/apps/backend/src/protocol/ListConversationsParams.ts +5 -0
  97. package/apps/backend/src/protocol/ListConversationsResponse.ts +6 -0
  98. package/apps/backend/src/protocol/ListCustomPromptsResponseEvent.ts +9 -0
  99. package/apps/backend/src/protocol/ListSkillsResponseEvent.ts +9 -0
  100. package/apps/backend/src/protocol/LocalShellAction.ts +6 -0
  101. package/apps/backend/src/protocol/LocalShellExecAction.ts +5 -0
  102. package/apps/backend/src/protocol/LocalShellStatus.ts +5 -0
  103. package/apps/backend/src/protocol/LoginApiKeyParams.ts +5 -0
  104. package/apps/backend/src/protocol/LoginApiKeyResponse.ts +5 -0
  105. package/apps/backend/src/protocol/LoginChatGptCompleteNotification.ts +8 -0
  106. package/apps/backend/src/protocol/LoginChatGptResponse.ts +5 -0
  107. package/apps/backend/src/protocol/LogoutChatGptResponse.ts +5 -0
  108. package/apps/backend/src/protocol/McpAuthStatus.ts +5 -0
  109. package/apps/backend/src/protocol/McpInvocation.ts +18 -0
  110. package/apps/backend/src/protocol/McpListToolsResponseEvent.ts +25 -0
  111. package/apps/backend/src/protocol/McpStartupCompleteEvent.ts +6 -0
  112. package/apps/backend/src/protocol/McpStartupFailure.ts +5 -0
  113. package/apps/backend/src/protocol/McpStartupStatus.ts +5 -0
  114. package/apps/backend/src/protocol/McpStartupUpdateEvent.ts +14 -0
  115. package/apps/backend/src/protocol/McpToolCallBeginEvent.ts +10 -0
  116. package/apps/backend/src/protocol/McpToolCallEndEvent.ts +15 -0
  117. package/apps/backend/src/protocol/NetworkAccess.ts +8 -0
  118. package/apps/backend/src/protocol/NewConversationParams.ts +8 -0
  119. package/apps/backend/src/protocol/NewConversationResponse.ts +7 -0
  120. package/apps/backend/src/protocol/ParsedCommand.ts +12 -0
  121. package/apps/backend/src/protocol/PatchApplyBeginEvent.ts +23 -0
  122. package/apps/backend/src/protocol/PatchApplyEndEvent.ts +31 -0
  123. package/apps/backend/src/protocol/PlanItemArg.ts +6 -0
  124. package/apps/backend/src/protocol/PlanType.ts +5 -0
  125. package/apps/backend/src/protocol/Profile.ts +9 -0
  126. package/apps/backend/src/protocol/README.md +11 -0
  127. package/apps/backend/src/protocol/RateLimitSnapshot.ts +8 -0
  128. package/apps/backend/src/protocol/RateLimitWindow.ts +17 -0
  129. package/apps/backend/src/protocol/RawResponseItemEvent.ts +6 -0
  130. package/apps/backend/src/protocol/ReasoningContentDeltaEvent.ts +5 -0
  131. package/apps/backend/src/protocol/ReasoningEffort.ts +8 -0
  132. package/apps/backend/src/protocol/ReasoningItem.ts +5 -0
  133. package/apps/backend/src/protocol/ReasoningItemContent.ts +5 -0
  134. package/apps/backend/src/protocol/ReasoningItemReasoningSummary.ts +5 -0
  135. package/apps/backend/src/protocol/ReasoningRawContentDeltaEvent.ts +5 -0
  136. package/apps/backend/src/protocol/ReasoningSummary.ts +10 -0
  137. package/apps/backend/src/protocol/RemoveConversationListenerParams.ts +5 -0
  138. package/apps/backend/src/protocol/RemoveConversationSubscriptionResponse.ts +5 -0
  139. package/apps/backend/src/protocol/RequestId.ts +5 -0
  140. package/apps/backend/src/protocol/Resource.ts +9 -0
  141. package/apps/backend/src/protocol/ResourceLink.ts +11 -0
  142. package/apps/backend/src/protocol/ResourceTemplate.ts +9 -0
  143. package/apps/backend/src/protocol/ResponseItem.ts +17 -0
  144. package/apps/backend/src/protocol/ResumeConversationParams.ts +8 -0
  145. package/apps/backend/src/protocol/ResumeConversationResponse.ts +7 -0
  146. package/apps/backend/src/protocol/ReviewCodeLocation.ts +9 -0
  147. package/apps/backend/src/protocol/ReviewDecision.ts +9 -0
  148. package/apps/backend/src/protocol/ReviewFinding.ts +9 -0
  149. package/apps/backend/src/protocol/ReviewLineRange.ts +8 -0
  150. package/apps/backend/src/protocol/ReviewOutputEvent.ts +9 -0
  151. package/apps/backend/src/protocol/ReviewRequest.ts +9 -0
  152. package/apps/backend/src/protocol/ReviewTarget.ts +9 -0
  153. package/apps/backend/src/protocol/Role.ts +8 -0
  154. package/apps/backend/src/protocol/SandboxMode.ts +5 -0
  155. package/apps/backend/src/protocol/SandboxPolicy.ts +35 -0
  156. package/apps/backend/src/protocol/SandboxSettings.ts +6 -0
  157. package/apps/backend/src/protocol/SendUserMessageParams.ts +7 -0
  158. package/apps/backend/src/protocol/SendUserMessageResponse.ts +5 -0
  159. package/apps/backend/src/protocol/SendUserTurnParams.ts +11 -0
  160. package/apps/backend/src/protocol/SendUserTurnResponse.ts +5 -0
  161. package/apps/backend/src/protocol/ServerNotification.ts +36 -0
  162. package/apps/backend/src/protocol/ServerRequest.ts +13 -0
  163. package/apps/backend/src/protocol/SessionConfiguredEvent.ts +48 -0
  164. package/apps/backend/src/protocol/SessionConfiguredNotification.ts +8 -0
  165. package/apps/backend/src/protocol/SessionSource.ts +6 -0
  166. package/apps/backend/src/protocol/SetDefaultModelParams.ts +6 -0
  167. package/apps/backend/src/protocol/SetDefaultModelResponse.ts +5 -0
  168. package/apps/backend/src/protocol/SkillErrorInfo.ts +5 -0
  169. package/apps/backend/src/protocol/SkillMetadata.ts +6 -0
  170. package/apps/backend/src/protocol/SkillScope.ts +5 -0
  171. package/apps/backend/src/protocol/SkillsListEntry.ts +7 -0
  172. package/apps/backend/src/protocol/StepStatus.ts +5 -0
  173. package/apps/backend/src/protocol/StreamErrorEvent.ts +6 -0
  174. package/apps/backend/src/protocol/SubAgentSource.ts +5 -0
  175. package/apps/backend/src/protocol/TaskCompleteEvent.ts +5 -0
  176. package/apps/backend/src/protocol/TaskStartedEvent.ts +5 -0
  177. package/apps/backend/src/protocol/TerminalInteractionEvent.ts +17 -0
  178. package/apps/backend/src/protocol/TextContent.ts +9 -0
  179. package/apps/backend/src/protocol/TextResourceContents.ts +5 -0
  180. package/apps/backend/src/protocol/TokenCountEvent.ts +7 -0
  181. package/apps/backend/src/protocol/TokenUsage.ts +5 -0
  182. package/apps/backend/src/protocol/TokenUsageInfo.ts +6 -0
  183. package/apps/backend/src/protocol/Tool.ts +11 -0
  184. package/apps/backend/src/protocol/ToolAnnotations.ts +15 -0
  185. package/apps/backend/src/protocol/ToolInputSchema.ts +9 -0
  186. package/apps/backend/src/protocol/ToolOutputSchema.ts +10 -0
  187. package/apps/backend/src/protocol/Tools.ts +5 -0
  188. package/apps/backend/src/protocol/TurnAbortReason.ts +5 -0
  189. package/apps/backend/src/protocol/TurnAbortedEvent.ts +6 -0
  190. package/apps/backend/src/protocol/TurnDiffEvent.ts +5 -0
  191. package/apps/backend/src/protocol/TurnItem.ts +9 -0
  192. package/apps/backend/src/protocol/UndoCompletedEvent.ts +5 -0
  193. package/apps/backend/src/protocol/UndoStartedEvent.ts +5 -0
  194. package/apps/backend/src/protocol/UpdatePlanArgs.ts +6 -0
  195. package/apps/backend/src/protocol/UserInfoResponse.ts +5 -0
  196. package/apps/backend/src/protocol/UserInput.ts +8 -0
  197. package/apps/backend/src/protocol/UserMessageEvent.ts +5 -0
  198. package/apps/backend/src/protocol/UserMessageItem.ts +6 -0
  199. package/apps/backend/src/protocol/UserSavedConfig.ts +14 -0
  200. package/apps/backend/src/protocol/Verbosity.ts +9 -0
  201. package/apps/backend/src/protocol/ViewImageToolCallEvent.ts +13 -0
  202. package/apps/backend/src/protocol/WarningEvent.ts +5 -0
  203. package/apps/backend/src/protocol/WebSearchAction.ts +5 -0
  204. package/apps/backend/src/protocol/WebSearchBeginEvent.ts +5 -0
  205. package/apps/backend/src/protocol/WebSearchEndEvent.ts +5 -0
  206. package/apps/backend/src/protocol/WebSearchItem.ts +5 -0
  207. package/apps/backend/src/protocol/index.ts +198 -0
  208. package/apps/backend/src/protocol/serde_json/JsonValue.ts +5 -0
  209. package/apps/backend/src/protocol/v2/Account.ts +6 -0
  210. package/apps/backend/src/protocol/v2/AccountLoginCompletedNotification.ts +5 -0
  211. package/apps/backend/src/protocol/v2/AccountRateLimitsUpdatedNotification.ts +6 -0
  212. package/apps/backend/src/protocol/v2/AccountUpdatedNotification.ts +6 -0
  213. package/apps/backend/src/protocol/v2/AgentMessageDeltaNotification.ts +5 -0
  214. package/apps/backend/src/protocol/v2/ApprovalDecision.ts +6 -0
  215. package/apps/backend/src/protocol/v2/AskForApproval.ts +5 -0
  216. package/apps/backend/src/protocol/v2/CancelLoginAccountParams.ts +5 -0
  217. package/apps/backend/src/protocol/v2/CancelLoginAccountResponse.ts +6 -0
  218. package/apps/backend/src/protocol/v2/CancelLoginAccountStatus.ts +5 -0
  219. package/apps/backend/src/protocol/v2/CodexErrorInfo.ts +11 -0
  220. package/apps/backend/src/protocol/v2/CommandAction.ts +5 -0
  221. package/apps/backend/src/protocol/v2/CommandExecParams.ts +6 -0
  222. package/apps/backend/src/protocol/v2/CommandExecResponse.ts +5 -0
  223. package/apps/backend/src/protocol/v2/CommandExecutionOutputDeltaNotification.ts +5 -0
  224. package/apps/backend/src/protocol/v2/CommandExecutionRequestApprovalParams.ts +14 -0
  225. package/apps/backend/src/protocol/v2/CommandExecutionRequestApprovalResponse.ts +6 -0
  226. package/apps/backend/src/protocol/v2/CommandExecutionStatus.ts +5 -0
  227. package/apps/backend/src/protocol/v2/Config.ts +15 -0
  228. package/apps/backend/src/protocol/v2/ConfigBatchWriteParams.ts +10 -0
  229. package/apps/backend/src/protocol/v2/ConfigEdit.ts +7 -0
  230. package/apps/backend/src/protocol/v2/ConfigLayer.ts +7 -0
  231. package/apps/backend/src/protocol/v2/ConfigLayerMetadata.ts +6 -0
  232. package/apps/backend/src/protocol/v2/ConfigLayerSource.ts +6 -0
  233. package/apps/backend/src/protocol/v2/ConfigReadParams.ts +5 -0
  234. package/apps/backend/src/protocol/v2/ConfigReadResponse.ts +8 -0
  235. package/apps/backend/src/protocol/v2/ConfigValueWriteParams.ts +11 -0
  236. package/apps/backend/src/protocol/v2/ConfigWriteResponse.ts +12 -0
  237. package/apps/backend/src/protocol/v2/ContextCompactedNotification.ts +5 -0
  238. package/apps/backend/src/protocol/v2/CreditsSnapshot.ts +5 -0
  239. package/apps/backend/src/protocol/v2/DeprecationNoticeNotification.ts +13 -0
  240. package/apps/backend/src/protocol/v2/ErrorNotification.ts +6 -0
  241. package/apps/backend/src/protocol/v2/ExecPolicyAmendment.ts +5 -0
  242. package/apps/backend/src/protocol/v2/FeedbackUploadParams.ts +5 -0
  243. package/apps/backend/src/protocol/v2/FeedbackUploadResponse.ts +5 -0
  244. package/apps/backend/src/protocol/v2/FileChangeOutputDeltaNotification.ts +5 -0
  245. package/apps/backend/src/protocol/v2/FileChangeRequestApprovalParams.ts +14 -0
  246. package/apps/backend/src/protocol/v2/FileChangeRequestApprovalResponse.ts +6 -0
  247. package/apps/backend/src/protocol/v2/FileUpdateChange.ts +6 -0
  248. package/apps/backend/src/protocol/v2/GetAccountParams.ts +5 -0
  249. package/apps/backend/src/protocol/v2/GetAccountRateLimitsResponse.ts +6 -0
  250. package/apps/backend/src/protocol/v2/GetAccountResponse.ts +6 -0
  251. package/apps/backend/src/protocol/v2/GitInfo.ts +5 -0
  252. package/apps/backend/src/protocol/v2/ItemCompletedNotification.ts +6 -0
  253. package/apps/backend/src/protocol/v2/ItemStartedNotification.ts +6 -0
  254. package/apps/backend/src/protocol/v2/ListMcpServerStatusParams.ts +13 -0
  255. package/apps/backend/src/protocol/v2/ListMcpServerStatusResponse.ts +11 -0
  256. package/apps/backend/src/protocol/v2/LoginAccountParams.ts +5 -0
  257. package/apps/backend/src/protocol/v2/LoginAccountResponse.ts +9 -0
  258. package/apps/backend/src/protocol/v2/LogoutAccountResponse.ts +5 -0
  259. package/apps/backend/src/protocol/v2/McpAuthStatus.ts +5 -0
  260. package/apps/backend/src/protocol/v2/McpServerOauthLoginCompletedNotification.ts +5 -0
  261. package/apps/backend/src/protocol/v2/McpServerOauthLoginParams.ts +5 -0
  262. package/apps/backend/src/protocol/v2/McpServerOauthLoginResponse.ts +5 -0
  263. package/apps/backend/src/protocol/v2/McpServerStatus.ts +9 -0
  264. package/apps/backend/src/protocol/v2/McpToolCallError.ts +5 -0
  265. package/apps/backend/src/protocol/v2/McpToolCallProgressNotification.ts +5 -0
  266. package/apps/backend/src/protocol/v2/McpToolCallResult.ts +7 -0
  267. package/apps/backend/src/protocol/v2/McpToolCallStatus.ts +5 -0
  268. package/apps/backend/src/protocol/v2/MergeStrategy.ts +5 -0
  269. package/apps/backend/src/protocol/v2/Model.ts +7 -0
  270. package/apps/backend/src/protocol/v2/ModelListParams.ts +13 -0
  271. package/apps/backend/src/protocol/v2/ModelListResponse.ts +11 -0
  272. package/apps/backend/src/protocol/v2/NetworkAccess.ts +5 -0
  273. package/apps/backend/src/protocol/v2/OverriddenMetadata.ts +7 -0
  274. package/apps/backend/src/protocol/v2/PatchApplyStatus.ts +5 -0
  275. package/apps/backend/src/protocol/v2/PatchChangeKind.ts +5 -0
  276. package/apps/backend/src/protocol/v2/ProfileV2.ts +10 -0
  277. package/apps/backend/src/protocol/v2/RateLimitSnapshot.ts +8 -0
  278. package/apps/backend/src/protocol/v2/RateLimitWindow.ts +5 -0
  279. package/apps/backend/src/protocol/v2/RawResponseItemCompletedNotification.ts +6 -0
  280. package/apps/backend/src/protocol/v2/ReasoningEffortOption.ts +6 -0
  281. package/apps/backend/src/protocol/v2/ReasoningSummaryPartAddedNotification.ts +5 -0
  282. package/apps/backend/src/protocol/v2/ReasoningSummaryTextDeltaNotification.ts +5 -0
  283. package/apps/backend/src/protocol/v2/ReasoningTextDeltaNotification.ts +5 -0
  284. package/apps/backend/src/protocol/v2/ReviewDelivery.ts +5 -0
  285. package/apps/backend/src/protocol/v2/ReviewStartParams.ts +12 -0
  286. package/apps/backend/src/protocol/v2/ReviewStartResponse.ts +13 -0
  287. package/apps/backend/src/protocol/v2/ReviewTarget.ts +9 -0
  288. package/apps/backend/src/protocol/v2/SandboxMode.ts +5 -0
  289. package/apps/backend/src/protocol/v2/SandboxPolicy.ts +7 -0
  290. package/apps/backend/src/protocol/v2/SandboxWorkspaceWrite.ts +5 -0
  291. package/apps/backend/src/protocol/v2/SessionSource.ts +5 -0
  292. package/apps/backend/src/protocol/v2/SkillErrorInfo.ts +5 -0
  293. package/apps/backend/src/protocol/v2/SkillMetadata.ts +6 -0
  294. package/apps/backend/src/protocol/v2/SkillScope.ts +5 -0
  295. package/apps/backend/src/protocol/v2/SkillsListEntry.ts +7 -0
  296. package/apps/backend/src/protocol/v2/SkillsListParams.ts +13 -0
  297. package/apps/backend/src/protocol/v2/SkillsListResponse.ts +6 -0
  298. package/apps/backend/src/protocol/v2/TerminalInteractionNotification.ts +5 -0
  299. package/apps/backend/src/protocol/v2/Thread.ts +46 -0
  300. package/apps/backend/src/protocol/v2/ThreadArchiveParams.ts +5 -0
  301. package/apps/backend/src/protocol/v2/ThreadArchiveResponse.ts +5 -0
  302. package/apps/backend/src/protocol/v2/ThreadItem.ts +48 -0
  303. package/apps/backend/src/protocol/v2/ThreadListParams.ts +18 -0
  304. package/apps/backend/src/protocol/v2/ThreadListResponse.ts +11 -0
  305. package/apps/backend/src/protocol/v2/ThreadResumeParams.ts +35 -0
  306. package/apps/backend/src/protocol/v2/ThreadResumeResponse.ts +9 -0
  307. package/apps/backend/src/protocol/v2/ThreadStartParams.ts +15 -0
  308. package/apps/backend/src/protocol/v2/ThreadStartResponse.ts +9 -0
  309. package/apps/backend/src/protocol/v2/ThreadStartedNotification.ts +6 -0
  310. package/apps/backend/src/protocol/v2/ThreadTokenUsage.ts +6 -0
  311. package/apps/backend/src/protocol/v2/ThreadTokenUsageUpdatedNotification.ts +6 -0
  312. package/apps/backend/src/protocol/v2/TokenUsageBreakdown.ts +5 -0
  313. package/apps/backend/src/protocol/v2/ToolsV2.ts +5 -0
  314. package/apps/backend/src/protocol/v2/Turn.ts +18 -0
  315. package/apps/backend/src/protocol/v2/TurnCompletedNotification.ts +6 -0
  316. package/apps/backend/src/protocol/v2/TurnDiffUpdatedNotification.ts +9 -0
  317. package/apps/backend/src/protocol/v2/TurnError.ts +6 -0
  318. package/apps/backend/src/protocol/v2/TurnInterruptParams.ts +5 -0
  319. package/apps/backend/src/protocol/v2/TurnInterruptResponse.ts +5 -0
  320. package/apps/backend/src/protocol/v2/TurnPlanStep.ts +6 -0
  321. package/apps/backend/src/protocol/v2/TurnPlanStepStatus.ts +5 -0
  322. package/apps/backend/src/protocol/v2/TurnPlanUpdatedNotification.ts +6 -0
  323. package/apps/backend/src/protocol/v2/TurnStartParams.ts +34 -0
  324. package/apps/backend/src/protocol/v2/TurnStartResponse.ts +6 -0
  325. package/apps/backend/src/protocol/v2/TurnStartedNotification.ts +6 -0
  326. package/apps/backend/src/protocol/v2/TurnStatus.ts +5 -0
  327. package/apps/backend/src/protocol/v2/UserInput.ts +5 -0
  328. package/apps/backend/src/protocol/v2/WindowsWorldWritableWarningNotification.ts +5 -0
  329. package/apps/backend/src/protocol/v2/WriteStatus.ts +5 -0
  330. package/apps/backend/src/protocol/v2/index.ts +123 -0
  331. package/apps/backend/src/reviews/service.ts +27 -0
  332. package/apps/backend/src/reviews/store.ts +124 -0
  333. package/apps/backend/src/server.ts +531 -0
  334. package/apps/backend/src/services/profile-store.ts +114 -0
  335. package/apps/backend/src/services/supervisor.ts +102 -0
  336. package/apps/backend/src/thread-index/service.ts +75 -0
  337. package/apps/backend/src/thread-index/store.ts +195 -0
  338. package/apps/backend/src/ws/messages.ts +73 -0
  339. package/apps/backend/tsconfig.json +20 -0
  340. package/apps/web/README.md +24 -0
  341. package/apps/web/bun.lock +1062 -0
  342. package/apps/web/eslint.config.js +23 -0
  343. package/apps/web/index.html +16 -0
  344. package/apps/web/package.json +38 -0
  345. package/apps/web/src/app.tsx +83 -0
  346. package/apps/web/src/components/composer/slash-command-menu.tsx +47 -0
  347. package/apps/web/src/components/index.ts +2 -0
  348. package/apps/web/src/components/layout/account-usage-panel.tsx +167 -0
  349. package/apps/web/src/components/layout/analytics-view.tsx +296 -0
  350. package/apps/web/src/components/layout/index.ts +7 -0
  351. package/apps/web/src/components/layout/mobile-header.tsx +56 -0
  352. package/apps/web/src/components/layout/reviews-view.tsx +848 -0
  353. package/apps/web/src/components/layout/session-view.tsx +1374 -0
  354. package/apps/web/src/components/layout/settings-dialog.tsx +322 -0
  355. package/apps/web/src/components/layout/side-bar.tsx +417 -0
  356. package/apps/web/src/components/layout/thread-list.tsx +488 -0
  357. package/apps/web/src/components/layout/virtualized-message-list.tsx +748 -0
  358. package/apps/web/src/components/loading/startup-ascii.ts +652 -0
  359. package/apps/web/src/components/loading/startup-loader.tsx +37 -0
  360. package/apps/web/src/components/session-view/file-mention-menu.tsx +46 -0
  361. package/apps/web/src/components/session-view/session-auth-banner.tsx +61 -0
  362. package/apps/web/src/components/session-view/session-composer.tsx +328 -0
  363. package/apps/web/src/components/session-view/session-dialogs.tsx +280 -0
  364. package/apps/web/src/components/session-view/session-empty.tsx +47 -0
  365. package/apps/web/src/components/session-view/session-header.tsx +49 -0
  366. package/apps/web/src/components/ui/avatar.tsx +19 -0
  367. package/apps/web/src/components/ui/badge.tsx +21 -0
  368. package/apps/web/src/components/ui/button.tsx +47 -0
  369. package/apps/web/src/components/ui/collapsible-content.tsx +114 -0
  370. package/apps/web/src/components/ui/contribution-graph.tsx +182 -0
  371. package/apps/web/src/components/ui/dialog-box.tsx +203 -0
  372. package/apps/web/src/components/ui/icon-button.tsx +32 -0
  373. package/apps/web/src/components/ui/icons.tsx +187 -0
  374. package/apps/web/src/components/ui/index.tsx +15 -0
  375. package/apps/web/src/components/ui/input.tsx +43 -0
  376. package/apps/web/src/components/ui/markdown-stream.tsx +21 -0
  377. package/apps/web/src/components/ui/mobile-drawer.tsx +124 -0
  378. package/apps/web/src/components/ui/section-header.tsx +13 -0
  379. package/apps/web/src/components/ui/select.tsx +217 -0
  380. package/apps/web/src/components/ui/shimmer.tsx +138 -0
  381. package/apps/web/src/components/ui/status-dot.tsx +24 -0
  382. package/apps/web/src/config.ts +5 -0
  383. package/apps/web/src/hooks/index.ts +3 -0
  384. package/apps/web/src/hooks/use-analytics.ts +122 -0
  385. package/apps/web/src/hooks/use-hub-connection.ts +587 -0
  386. package/apps/web/src/hooks/use-mobile.ts +76 -0
  387. package/apps/web/src/hooks/use-thread-history.ts +210 -0
  388. package/apps/web/src/index.css +269 -0
  389. package/apps/web/src/main.tsx +10 -0
  390. package/apps/web/src/services/hub-client.ts +358 -0
  391. package/apps/web/src/store/index.ts +528 -0
  392. package/apps/web/src/types/index.ts +119 -0
  393. package/apps/web/src/utils/account-refresh.ts +168 -0
  394. package/apps/web/src/utils/approval-policy.ts +53 -0
  395. package/apps/web/src/utils/init-prompt.ts +41 -0
  396. package/apps/web/src/utils/item-format.ts +170 -0
  397. package/apps/web/src/utils/prompt-expander.ts +62 -0
  398. package/apps/web/src/utils/reasoning-summary.ts +48 -0
  399. package/apps/web/src/utils/slash-commands.ts +98 -0
  400. package/apps/web/tsconfig.app.json +28 -0
  401. package/apps/web/tsconfig.json +7 -0
  402. package/apps/web/tsconfig.node.json +26 -0
  403. package/apps/web/vite.config.ts +8 -0
  404. package/bin/better-codex.cjs +199 -0
  405. package/package.json +20 -0
@@ -0,0 +1,1374 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { useAppStore } from '../../store'
3
+ import { type SelectOption } from '../ui'
4
+ import { VirtualizedMessageList } from './virtualized-message-list'
5
+ import { hubClient } from '../../services/hub-client'
6
+ import type { ApprovalPolicy, Attachment, FileMention, MessageKind, ReasoningEffort, ReasoningSummary } from '../../types'
7
+ import { INIT_PROMPT } from '../../utils/init-prompt'
8
+ import { filterSlashCommands, findSlashCommand, getSlashQuery, parseSlashInput, type SlashCommandDefinition } from '../../utils/slash-commands'
9
+ import { approvalPolicyDescription, approvalPolicyLabel, normalizeApprovalPolicy } from '../../utils/approval-policy'
10
+ import { normalizeReasoningSummary, reasoningSummaryDescription, reasoningSummaryLabel } from '../../utils/reasoning-summary'
11
+ import { refreshAccountSnapshot } from '../../utils/account-refresh'
12
+ import { expandPromptTemplate, stripPromptFrontmatter } from '../../utils/prompt-expander'
13
+ import { SessionHeader } from '../session-view/session-header'
14
+ import { SessionAuthBanner } from '../session-view/session-auth-banner'
15
+ import { SessionComposer } from '../session-view/session-composer'
16
+ import { SessionDialogs } from '../session-view/session-dialogs'
17
+ import { SessionEmpty } from '../session-view/session-empty'
18
+
19
+ export function SessionView() {
20
+ const [inputValue, setInputValue] = useState('')
21
+ const [slashIndex, setSlashIndex] = useState(0)
22
+ const [showModelDialog, setShowModelDialog] = useState(false)
23
+ const [showApprovalsDialog, setShowApprovalsDialog] = useState(false)
24
+ const [showSkillsDialog, setShowSkillsDialog] = useState(false)
25
+ const [showResumeDialog, setShowResumeDialog] = useState(false)
26
+ const [showFeedbackDialog, setShowFeedbackDialog] = useState(false)
27
+ const [skillsLoading, setSkillsLoading] = useState(false)
28
+ const [skillsError, setSkillsError] = useState<string | null>(null)
29
+ const [skillsList, setSkillsList] = useState<Array<{ name: string; description: string; path: string }>>([])
30
+ const [promptCommands, setPromptCommands] = useState<SlashCommandDefinition[]>([])
31
+ const [feedbackCategory, setFeedbackCategory] = useState('bug')
32
+ const [feedbackReason, setFeedbackReason] = useState('')
33
+ const [feedbackIncludeLogs, setFeedbackIncludeLogs] = useState(true)
34
+ const [pendingModelId, setPendingModelId] = useState('')
35
+ const [pendingEffort, setPendingEffort] = useState<ReasoningEffort | ''>('')
36
+ const [pendingSummary, setPendingSummary] = useState<ReasoningSummary | ''>('')
37
+ const [pendingCwd, setPendingCwd] = useState('')
38
+ const [pendingApproval, setPendingApproval] = useState<ApprovalPolicy>('on-request')
39
+ const [showApiKeyPrompt, setShowApiKeyPrompt] = useState(false)
40
+ // Attachments and file mentions state
41
+ const [attachments, setAttachments] = useState<Attachment[]>([])
42
+ const [fileMentions, setFileMentions] = useState<FileMention[]>([])
43
+ const [mentionIndex, setMentionIndex] = useState(0)
44
+ const [fileSearchResults, setFileSearchResults] = useState<FileMention[]>([])
45
+ const [copyDialog, setCopyDialog] = useState<{ open: boolean; url: string }>({
46
+ open: false,
47
+ url: '',
48
+ })
49
+ const [alertDialog, setAlertDialog] = useState<{ open: boolean; title: string; message: string; variant: 'info' | 'warning' | 'error' }>({
50
+ open: false,
51
+ title: '',
52
+ message: '',
53
+ variant: 'info',
54
+ })
55
+ const textareaRef = useRef<HTMLTextAreaElement>(null)
56
+ const authPollRef = useRef<number | null>(null)
57
+ const {
58
+ accounts,
59
+ threads,
60
+ selectedThreadId,
61
+ selectedAccountId,
62
+ accountLoginIds,
63
+ messages,
64
+ approvals,
65
+ resolveApproval,
66
+ addMessage,
67
+ addThread,
68
+ updateAccount,
69
+ updateThread,
70
+ activeTab,
71
+ modelsByAccount,
72
+ threadModels,
73
+ threadEfforts,
74
+ threadApprovals,
75
+ threadWebSearch,
76
+ threadTurnIds,
77
+ threadTurnStartedAt,
78
+ threadLastTurnDuration,
79
+ threadTokenUsage,
80
+ setThreadModel,
81
+ setThreadEffort,
82
+ setThreadApproval,
83
+ setThreadWebSearch,
84
+ threadSummaries,
85
+ setThreadSummary,
86
+ threadCwds,
87
+ setThreadCwd,
88
+ setSelectedThreadId,
89
+ setModelsForAccount,
90
+ queuedMessages,
91
+ enqueueMessage,
92
+ clearQueuedMessages,
93
+ connectionStatus,
94
+ setMessagesForThread,
95
+ setAccountLoginId,
96
+ } = useAppStore()
97
+
98
+ const selectedThread = threads.find((thread) => thread.id === selectedThreadId)
99
+ const threadMessages = selectedThreadId ? messages[selectedThreadId] || [] : []
100
+ const pendingApprovals = approvals.filter((approval) => approval.threadId === selectedThreadId && approval.status === 'pending')
101
+ const isArchived = selectedThread?.status === 'archived'
102
+ const threadAccountId = selectedThread?.accountId ?? selectedAccountId ?? ''
103
+ const account = threadAccountId ? accounts.find((item) => item.id === threadAccountId) : undefined
104
+ const models = threadAccountId ? modelsByAccount[threadAccountId] || [] : []
105
+ const selectedModelId = selectedThreadId ? threadModels[selectedThreadId] : undefined
106
+ const defaultModel = models.find((model) => model.isDefault) ?? models[0]
107
+ const effectiveModel = selectedModelId ?? defaultModel?.id ?? ''
108
+ const modelDetails = models.find((model) => model.id === effectiveModel) ?? defaultModel
109
+ const effortOptions = (modelDetails?.supportedReasoningEfforts ?? []).map((effort) => ({
110
+ value: effort.reasoningEffort,
111
+ label: formatEffortLabel(effort.reasoningEffort),
112
+ description: effort.description,
113
+ }))
114
+ const defaultEffort = modelDetails?.defaultReasoningEffort
115
+ const selectedEffort = selectedThreadId ? threadEfforts[selectedThreadId] : undefined
116
+ const effectiveEffort = selectedEffort ?? defaultEffort ?? null
117
+ const selectedApproval = selectedThreadId ? threadApprovals[selectedThreadId] : undefined
118
+ const selectedSummary = selectedThreadId ? threadSummaries[selectedThreadId] : undefined
119
+ const selectedCwd = selectedThreadId ? threadCwds[selectedThreadId] : undefined
120
+ const selectedUsage = selectedThreadId ? threadTokenUsage[selectedThreadId] : undefined
121
+ const webSearchEnabled = selectedThreadId ? threadWebSearch[selectedThreadId] ?? false : false
122
+ const isAccountReady = account?.status === 'online'
123
+ const isAuthPending = account?.status === 'degraded'
124
+ const canInteract = connectionStatus === 'connected' && !isArchived && isAccountReady
125
+ const isTaskRunning = selectedThread?.status === 'active'
126
+ const queuedCount = selectedThreadId ? queuedMessages[selectedThreadId]?.length ?? 0 : 0
127
+ const slashInput = parseSlashInput(inputValue)
128
+ const slashQuery = getSlashQuery(inputValue)
129
+ const slashMatches = slashQuery !== null ? filterSlashCommands(slashQuery, promptCommands) : []
130
+ const slashMenuOpen = slashQuery !== null && !slashInput?.rest && slashMatches.length > 0
131
+
132
+ // @ mention detection
133
+ const getMentionQuery = (text: string): string | null => {
134
+ const cursorPos = text.length // Assume cursor at end
135
+ const beforeCursor = text.slice(0, cursorPos)
136
+ const atIndex = beforeCursor.lastIndexOf('@')
137
+ if (atIndex === -1) return null
138
+ // Check there's no space between @ and cursor
139
+ const afterAt = beforeCursor.slice(atIndex + 1)
140
+ if (afterAt.includes(' ') || afterAt.includes('\n')) return null
141
+ return afterAt
142
+ }
143
+ const mentionQuery = getMentionQuery(inputValue)
144
+ const mentionMenuOpen = mentionQuery !== null && !slashMenuOpen
145
+ const mentionMatches = mentionMenuOpen ? fileSearchResults : []
146
+
147
+ const modelOptions = models.map((model): SelectOption => ({
148
+ value: model.id,
149
+ label: model.displayName || model.model,
150
+ description: model.description,
151
+ }))
152
+ const pendingModelDetails = models.find((model) => model.id === pendingModelId) ?? defaultModel
153
+ const pendingEffortOptions = (pendingModelDetails?.supportedReasoningEfforts ?? []).map((effort) => ({
154
+ value: effort.reasoningEffort,
155
+ label: formatEffortLabel(effort.reasoningEffort),
156
+ description: effort.description,
157
+ }))
158
+ const summaryOptions: SelectOption[] = (['auto', 'concise', 'detailed', 'none'] as ReasoningSummary[]).map(
159
+ (value) => ({
160
+ value,
161
+ label: reasoningSummaryLabel(value),
162
+ description: reasoningSummaryDescription(value),
163
+ })
164
+ )
165
+ const approvalPolicyValues: ApprovalPolicy[] = [
166
+ 'untrusted',
167
+ 'on-request',
168
+ 'on-failure',
169
+ 'never',
170
+ ]
171
+ const approvalOptions: Array<{ value: ApprovalPolicy; label: string; description: string }> = approvalPolicyValues.map(
172
+ (value) => ({
173
+ value,
174
+ label: approvalPolicyLabel(value),
175
+ description: approvalPolicyDescription(value),
176
+ })
177
+ )
178
+ const resumeCandidates = threadAccountId
179
+ ? threads.filter((thread) => thread.accountId === threadAccountId)
180
+ : []
181
+
182
+ useEffect(() => {
183
+ if (slashMenuOpen) {
184
+ setSlashIndex(0)
185
+ }
186
+ }, [slashMenuOpen, slashQuery])
187
+
188
+ useEffect(() => {
189
+ if (mentionMenuOpen) {
190
+ setMentionIndex(0)
191
+ }
192
+ }, [mentionMenuOpen, mentionQuery])
193
+
194
+ useEffect(() => {
195
+ if (connectionStatus !== 'connected' || !account) {
196
+ setPromptCommands([])
197
+ return
198
+ }
199
+ let cancelled = false
200
+ const loadPrompts = async () => {
201
+ try {
202
+ const prompts = await hubClient.listPrompts(account.id)
203
+ if (cancelled) {
204
+ return
205
+ }
206
+ const commands = prompts.map((prompt) => {
207
+ return {
208
+ id: `prompts:${prompt.name}`,
209
+ description: prompt.description || 'custom prompt',
210
+ availableDuringTask: false,
211
+ } as SlashCommandDefinition
212
+ })
213
+ setPromptCommands(commands)
214
+ } catch {
215
+ if (!cancelled) {
216
+ setPromptCommands([])
217
+ }
218
+ }
219
+ }
220
+ void loadPrompts()
221
+ return () => {
222
+ cancelled = true
223
+ }
224
+ }, [account, connectionStatus])
225
+
226
+ useEffect(() => {
227
+ if (!mentionQuery || !account) {
228
+ setFileSearchResults([])
229
+ return
230
+ }
231
+
232
+ let cancelled = false
233
+ const searchFiles = async () => {
234
+ try {
235
+ const result = await hubClient.request(account.id, 'command/exec', {
236
+ command: ['find', '.', '-type', 'f', '-name', `*${mentionQuery}*`, '-not', '-path', '*/node_modules/*', '-not', '-path', '*/.git/*'],
237
+ timeoutMs: 3000,
238
+ cwd: null,
239
+ sandboxPolicy: null,
240
+ }) as { stdout: string; stderr: string; exitCode: number }
241
+
242
+ if (cancelled) return
243
+
244
+ if (result.exitCode === 0 && result.stdout) {
245
+ const files = result.stdout
246
+ .split('\n')
247
+ .filter(Boolean)
248
+ .slice(0, 10)
249
+ .map((path) => ({
250
+ path: path.replace(/^\.\//, ''),
251
+ name: path.split('/').pop() || path,
252
+ }))
253
+ setFileSearchResults(files)
254
+ } else {
255
+ setFileSearchResults([])
256
+ }
257
+ } catch {
258
+ if (!cancelled) {
259
+ setFileSearchResults([])
260
+ }
261
+ }
262
+ }
263
+
264
+ const debounce = setTimeout(searchFiles, 150)
265
+ return () => {
266
+ cancelled = true
267
+ clearTimeout(debounce)
268
+ }
269
+ }, [mentionQuery, account])
270
+
271
+ useEffect(() => {
272
+ return () => {
273
+ if (authPollRef.current) {
274
+ window.clearTimeout(authPollRef.current)
275
+ }
276
+ }
277
+ }, [])
278
+
279
+ useEffect(() => {
280
+ if (showModelDialog) {
281
+ setPendingModelId(effectiveModel)
282
+ setPendingEffort((effectiveEffort ?? '') as ReasoningEffort | '')
283
+ setPendingSummary((selectedSummary ?? 'auto') as ReasoningSummary)
284
+ setPendingCwd(selectedCwd ?? '')
285
+ }
286
+ }, [showModelDialog, effectiveModel, effectiveEffort, selectedSummary, selectedCwd])
287
+
288
+ useEffect(() => {
289
+ if (!showModelDialog) {
290
+ return
291
+ }
292
+ const supported = new Set(pendingEffortOptions.map((option) => option.value))
293
+ if (pendingEffort && !supported.has(pendingEffort)) {
294
+ setPendingEffort((pendingModelDetails?.defaultReasoningEffort ?? '') as ReasoningEffort | '')
295
+ }
296
+ }, [pendingEffort, pendingEffortOptions, pendingModelDetails, showModelDialog])
297
+
298
+ useEffect(() => {
299
+ if (showApprovalsDialog) {
300
+ setPendingApproval(selectedApproval ?? 'on-request')
301
+ }
302
+ }, [showApprovalsDialog, selectedApproval])
303
+
304
+ useEffect(() => {
305
+ if (!showSkillsDialog || !account) {
306
+ return
307
+ }
308
+ let cancelled = false
309
+ const loadSkills = async () => {
310
+ setSkillsLoading(true)
311
+ setSkillsError(null)
312
+ try {
313
+ const result = (await hubClient.request(account.id, 'skills/list', {
314
+ cwds: [],
315
+ })) as { data?: Array<{ skills?: Array<{ name: string; description?: string; shortDescription?: string; path: string }> }> }
316
+ if (cancelled) {
317
+ return
318
+ }
319
+ const skills = result.data?.flatMap((entry) => entry.skills ?? []) ?? []
320
+ setSkillsList(
321
+ skills.map((skill) => ({
322
+ name: skill.name,
323
+ description: skill.shortDescription || skill.description || 'Skill',
324
+ path: skill.path,
325
+ }))
326
+ )
327
+ } catch {
328
+ if (!cancelled) {
329
+ setSkillsError('Failed to load skills.')
330
+ }
331
+ } finally {
332
+ if (!cancelled) {
333
+ setSkillsLoading(false)
334
+ }
335
+ }
336
+ }
337
+ loadSkills()
338
+ return () => {
339
+ cancelled = true
340
+ }
341
+ }, [showSkillsDialog, account])
342
+
343
+ const focusComposer = () => {
344
+ textareaRef.current?.focus()
345
+ }
346
+
347
+ const setComposerValue = (value: string) => {
348
+ setInputValue(value)
349
+ requestAnimationFrame(() => {
350
+ focusComposer()
351
+ })
352
+ }
353
+
354
+ const autocompleteSlashCommand = (command: SlashCommandDefinition) => {
355
+ setComposerValue(`/${command.id} `)
356
+ }
357
+
358
+ const addSystemMessage = (kind: MessageKind, title: string, content: string) => {
359
+ if (!selectedThreadId) {
360
+ return
361
+ }
362
+ addMessage(selectedThreadId, {
363
+ id: `sys-${Date.now()}`,
364
+ role: 'assistant',
365
+ content,
366
+ kind,
367
+ title,
368
+ timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
369
+ })
370
+ }
371
+
372
+ const sendTurn = async (text: string) => {
373
+ if (!selectedThreadId || !selectedThread) {
374
+ return
375
+ }
376
+ if (!canInteract) {
377
+ // Show an alert if the user tries to send but can't
378
+ if (connectionStatus !== 'connected') {
379
+ setAlertDialog({
380
+ open: true,
381
+ title: 'Not Connected',
382
+ message: 'Backend not connected. Please refresh the page.',
383
+ variant: 'error',
384
+ })
385
+ } else if (!isAccountReady) {
386
+ setAlertDialog({
387
+ open: true,
388
+ title: 'Account Not Ready',
389
+ message: 'This account is not authenticated. Please sign in first.',
390
+ variant: 'warning',
391
+ })
392
+ }
393
+ return
394
+ }
395
+
396
+ if (isTaskRunning) {
397
+ enqueueMessage(selectedThreadId, {
398
+ id: `queue-${Date.now()}`,
399
+ text,
400
+ model: effectiveModel || undefined,
401
+ effort: effectiveEffort ?? null,
402
+ summary: selectedSummary ?? null,
403
+ cwd: selectedCwd ?? null,
404
+ approvalPolicy: selectedApproval ?? null,
405
+ createdAt: Date.now(),
406
+ })
407
+ setInputValue('')
408
+ return
409
+ }
410
+
411
+ let displayContent = text
412
+ if (fileMentions.length > 0) {
413
+ const mentionList = fileMentions.map(m => `@${m.path}`).join(' ')
414
+ displayContent = `${mentionList}\n\n${text}`
415
+ }
416
+
417
+ addMessage(selectedThreadId, {
418
+ id: `msg-${Date.now()}`,
419
+ role: 'user',
420
+ content: displayContent,
421
+ kind: 'chat',
422
+ timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
423
+ })
424
+ setInputValue('')
425
+
426
+ try {
427
+ updateThread(selectedThreadId, { status: 'active' })
428
+
429
+ // Build input array with text, images, and file references
430
+ const input: Array<{ type: string; text?: string; url?: string; path?: string }> = []
431
+
432
+ // Add file mentions as text references
433
+ if (fileMentions.length > 0) {
434
+ const mentionText = fileMentions.map(m => `@${m.path}`).join(' ')
435
+ input.push({ type: 'text', text: `${mentionText}\n\n${text}` })
436
+ } else {
437
+ input.push({ type: 'text', text })
438
+ }
439
+
440
+ // Add image attachments
441
+ for (const attachment of attachments) {
442
+ if (attachment.type === 'image' && attachment.url) {
443
+ input.push({ type: 'image', url: attachment.url })
444
+ }
445
+ }
446
+
447
+ const params: {
448
+ threadId: string
449
+ input: Array<{ type: string; text?: string; url?: string; path?: string }>
450
+ model?: string
451
+ effort?: string
452
+ summary?: ReasoningSummary
453
+ cwd?: string
454
+ approvalPolicy?: ApprovalPolicy
455
+ } = {
456
+ threadId: selectedThreadId,
457
+ input,
458
+ }
459
+ if (effectiveModel) {
460
+ params.model = effectiveModel
461
+ }
462
+ if (effectiveEffort) {
463
+ params.effort = effectiveEffort
464
+ }
465
+ if (selectedApproval) {
466
+ params.approvalPolicy = selectedApproval
467
+ }
468
+ if (selectedSummary) {
469
+ params.summary = selectedSummary
470
+ }
471
+ if (selectedCwd) {
472
+ params.cwd = selectedCwd
473
+ }
474
+
475
+ setAttachments([])
476
+ setFileMentions([])
477
+
478
+ await hubClient.request(selectedThread.accountId, 'turn/start', {
479
+ ...params,
480
+ })
481
+ } catch (error) {
482
+ console.error('[sendTurn] Error:', error)
483
+ updateThread(selectedThreadId, { status: 'idle' })
484
+ }
485
+ }
486
+
487
+ const startNewThread = async (accountId: string, approvalOverride?: ApprovalPolicy | null, webSearch?: boolean) => {
488
+ const accountModels = modelsByAccount[accountId] || []
489
+ const defaultThreadModel = accountModels.find((model) => model.isDefault) ?? accountModels[0]
490
+ const params: { model?: string; approvalPolicy?: ApprovalPolicy; config?: Record<string, unknown> } = {}
491
+ if (defaultThreadModel?.id) {
492
+ params.model = defaultThreadModel.id
493
+ }
494
+ if (approvalOverride) {
495
+ params.approvalPolicy = approvalOverride
496
+ }
497
+ if (webSearch) {
498
+ params.config = { 'features.web_search_request': true }
499
+ }
500
+
501
+ const result = (await hubClient.request(accountId, 'thread/start', params)) as {
502
+ thread?: {
503
+ id: string
504
+ preview?: string
505
+ modelProvider?: string
506
+ createdAt?: number
507
+ }
508
+ reasoningEffort?: ReasoningEffort | null
509
+ approvalPolicy?: ApprovalPolicy | null
510
+ }
511
+
512
+ if (!result.thread) {
513
+ return
514
+ }
515
+
516
+ const threadId = result.thread.id
517
+ addThread({
518
+ id: threadId,
519
+ accountId,
520
+ title: result.thread.preview?.trim() || 'Untitled session',
521
+ preview: result.thread.preview?.trim() || 'New session started',
522
+ model: result.thread.modelProvider ?? 'unknown',
523
+ createdAt: result.thread.createdAt
524
+ ? new Date(result.thread.createdAt * 1000).toLocaleDateString('en-US', {
525
+ month: 'short',
526
+ day: 'numeric',
527
+ })
528
+ : '',
529
+ status: 'idle',
530
+ messageCount: 0,
531
+ })
532
+ // Initialize empty messages array to prevent thread/resume from being called
533
+ setMessagesForThread(threadId, [])
534
+ setSelectedThreadId(threadId)
535
+ if (defaultThreadModel?.id) {
536
+ setThreadModel(threadId, defaultThreadModel.id)
537
+ if (defaultThreadModel.defaultReasoningEffort) {
538
+ setThreadEffort(threadId, defaultThreadModel.defaultReasoningEffort)
539
+ }
540
+ }
541
+ const effort = result.reasoningEffort ?? defaultThreadModel?.defaultReasoningEffort
542
+ if (effort) {
543
+ setThreadEffort(threadId, effort)
544
+ }
545
+ if (selectedSummary) {
546
+ setThreadSummary(threadId, selectedSummary)
547
+ }
548
+ if (selectedCwd) {
549
+ setThreadCwd(threadId, selectedCwd)
550
+ }
551
+ const normalizedApproval = normalizeApprovalPolicy(result.approvalPolicy ?? null)
552
+ const approval = normalizedApproval ?? approvalOverride ?? null
553
+ if (approval) {
554
+ setThreadApproval(threadId, approval)
555
+ }
556
+ if (webSearch) {
557
+ setThreadWebSearch(threadId, true)
558
+ }
559
+ }
560
+
561
+ const runDiffCommand = async (pathFilter?: string) => {
562
+ if (!account || !selectedThreadId) {
563
+ return
564
+ }
565
+ const exec = async (command: string[]) => {
566
+ return (await hubClient.request(account.id, 'command/exec', {
567
+ command,
568
+ timeoutMs: 15000,
569
+ cwd: null,
570
+ sandboxPolicy: null,
571
+ })) as { stdout: string; stderr: string; exitCode: number }
572
+ }
573
+ const repoCheck = await exec(['git', 'rev-parse', '--is-inside-work-tree'])
574
+ if (repoCheck.exitCode !== 0) {
575
+ addSystemMessage('command', '/diff', 'Not inside a git repository.')
576
+ return
577
+ }
578
+
579
+ const tracked = await exec(pathFilter ? ['git', 'diff', '--', pathFilter] : ['git', 'diff'])
580
+ const untrackedList = await exec(['git', 'ls-files', '--others', '--exclude-standard'])
581
+ const untrackedFiles = untrackedList.stdout.split('\n').map((line) => line.trim()).filter(Boolean)
582
+
583
+ let untrackedDiff = ''
584
+ for (const file of untrackedFiles) {
585
+ const diff = await exec(['git', 'diff', '--no-index', '--', '/dev/null', file])
586
+ if (diff.stdout) {
587
+ untrackedDiff += diff.stdout
588
+ }
589
+ }
590
+
591
+ const combined = `${tracked.stdout}${untrackedDiff}`.trim()
592
+ const content = combined
593
+ ? `\`\`\`diff\n${combined}\n\`\`\``
594
+ : 'No changes detected.'
595
+ addSystemMessage('command', '/diff', content)
596
+ }
597
+
598
+ const runStatusCommand = () => {
599
+ if (!account || !selectedThreadId) {
600
+ return
601
+ }
602
+ const usageLine = selectedUsage ? `Token usage: ${JSON.stringify(selectedUsage)}` : null
603
+ const lines = [
604
+ `Account: ${account.name} (${account.status})`,
605
+ `Model: ${effectiveModel || 'default'}`,
606
+ `Reasoning effort: ${effectiveEffort ?? 'default'}`,
607
+ `Reasoning summary: ${selectedSummary ?? 'default'}`,
608
+ `Working directory: ${selectedCwd || 'default'}`,
609
+ `Approvals: ${selectedApproval ?? 'default'}`,
610
+ `Connection: ${connectionStatus}`,
611
+ ]
612
+ if (usageLine) {
613
+ lines.push(usageLine)
614
+ }
615
+ addSystemMessage('tool', '/status', lines.join('\n'))
616
+ }
617
+
618
+ const runMcpCommand = async (target?: string) => {
619
+ if (!account) {
620
+ return
621
+ }
622
+ if (target) {
623
+ const result = (await hubClient.request(account.id, 'mcpServer/oauth/login', {
624
+ name: target,
625
+ })) as { authorization_url?: string; authorizationUrl?: string; authUrl?: string }
626
+ const authUrl = result.authorization_url ?? result.authorizationUrl ?? result.authUrl
627
+ if (authUrl) {
628
+ const opened = window.open(authUrl, '_blank', 'noopener,noreferrer')
629
+ if (!opened) {
630
+ setCopyDialog({ open: true, url: authUrl })
631
+ }
632
+ addSystemMessage('tool', '/mcp', `Opened OAuth flow for ${target}.`)
633
+ } else {
634
+ addSystemMessage('tool', '/mcp', `Unable to start OAuth flow for ${target}.`)
635
+ }
636
+ return
637
+ }
638
+
639
+ const result = (await hubClient.request(account.id, 'mcpServerStatus/list', {
640
+ limit: 100,
641
+ cursor: null,
642
+ })) as { data?: Array<{ name: string; authStatus?: string; tools?: Record<string, unknown> }> }
643
+ const servers = result.data ?? []
644
+ if (!servers.length) {
645
+ addSystemMessage('tool', '/mcp', 'No MCP servers configured.')
646
+ return
647
+ }
648
+ const content = servers
649
+ .map((server) => {
650
+ const toolCount = Object.keys(server.tools ?? {}).length
651
+ const status = server.authStatus ?? 'unknown'
652
+ return `${server.name} · ${status} · ${toolCount} tools`
653
+ })
654
+ .join('\n')
655
+ addSystemMessage('tool', '/mcp', content)
656
+ }
657
+
658
+ const runReviewCommand = async (instructions?: string) => {
659
+ if (!selectedThreadId || !account) {
660
+ return
661
+ }
662
+ const target = instructions
663
+ ? { type: 'custom', instructions }
664
+ : { type: 'uncommittedChanges' }
665
+ await hubClient.request(account.id, 'review/start', {
666
+ threadId: selectedThreadId,
667
+ target,
668
+ delivery: 'inline',
669
+ })
670
+ updateThread(selectedThreadId, { status: 'active' })
671
+ }
672
+
673
+ const runLogoutCommand = async () => {
674
+ if (!account) {
675
+ return
676
+ }
677
+ await hubClient.request(account.id, 'account/logout')
678
+ updateAccount(account.id, (prev) => ({ ...prev, status: 'offline' }))
679
+ addSystemMessage('tool', '/logout', 'Logged out. Authenticate again to resume sessions.')
680
+ }
681
+
682
+ const runSlashCommand = async (command: SlashCommandDefinition, rest: string) => {
683
+ if (!command.availableDuringTask && isTaskRunning) {
684
+ addSystemMessage('tool', `/${command.id}`, `/${command.id} is disabled while a task is running.`)
685
+ return
686
+ }
687
+ const needsConnection = ['review', 'new', 'init', 'compact', 'diff', 'mcp', 'feedback', 'logout', 'skills'].includes(command.id)
688
+ || command.id.startsWith('prompts:')
689
+ if (needsConnection) {
690
+ if (connectionStatus !== 'connected') {
691
+ setAlertDialog({
692
+ open: true,
693
+ title: 'Not Connected',
694
+ message: 'Backend not connected. Start the hub and refresh the page.',
695
+ variant: 'error',
696
+ })
697
+ return
698
+ }
699
+ if (!isAccountReady) {
700
+ setAlertDialog({
701
+ open: true,
702
+ title: 'Authentication Required',
703
+ message: 'Authenticate this account before running this command.',
704
+ variant: 'warning',
705
+ })
706
+ return
707
+ }
708
+ }
709
+
710
+ try {
711
+ if (command.id.startsWith('prompts:')) {
712
+ if (!account) {
713
+ return
714
+ }
715
+ const promptName = command.id.slice('prompts:'.length)
716
+ const raw = await hubClient.readPrompt(account.id, promptName)
717
+ const content = expandPromptTemplate(stripPromptFrontmatter(raw), rest)
718
+ if (!content.trim()) {
719
+ addSystemMessage('tool', '/prompts', `Prompt ${promptName} was empty.`)
720
+ return
721
+ }
722
+ await sendTurn(content.trim())
723
+ return
724
+ }
725
+
726
+ switch (command.id) {
727
+ case 'mention': {
728
+ const value = rest ? `@${rest}` : '@'
729
+ setComposerValue(value)
730
+ return
731
+ }
732
+ case 'skills': {
733
+ if (rest) {
734
+ setComposerValue(`$${rest}`)
735
+ return
736
+ }
737
+ setShowSkillsDialog(true)
738
+ return
739
+ }
740
+ case 'model': {
741
+ if (rest) {
742
+ const match = models.find((model) =>
743
+ [model.id, model.model, model.displayName].some(
744
+ (value) => value && value.toLowerCase() === rest.toLowerCase()
745
+ )
746
+ )
747
+ if (match && selectedThreadId) {
748
+ setThreadModel(selectedThreadId, match.id)
749
+ if (match.defaultReasoningEffort) {
750
+ setThreadEffort(selectedThreadId, match.defaultReasoningEffort)
751
+ }
752
+ return
753
+ }
754
+ }
755
+ setShowModelDialog(true)
756
+ return
757
+ }
758
+ case 'summary': {
759
+ const summary = normalizeReasoningSummary(rest)
760
+ if (summary && selectedThreadId) {
761
+ setThreadSummary(selectedThreadId, summary)
762
+ addSystemMessage('tool', '/summary', `Reasoning summary set to ${summary}.`)
763
+ return
764
+ }
765
+ setShowModelDialog(true)
766
+ return
767
+ }
768
+ case 'cwd': {
769
+ const target = rest.trim()
770
+ if (selectedThreadId && target) {
771
+ if (target === 'clear' || target === 'reset') {
772
+ setThreadCwd(selectedThreadId, '')
773
+ addSystemMessage('tool', '/cwd', 'Working directory cleared.')
774
+ return
775
+ }
776
+ setThreadCwd(selectedThreadId, target)
777
+ addSystemMessage('tool', '/cwd', `Working directory set to ${target}.`)
778
+ return
779
+ }
780
+ setShowModelDialog(true)
781
+ return
782
+ }
783
+ case 'approvals': {
784
+ const direct = normalizeApprovalPolicy(rest)
785
+ if (direct) {
786
+ if (selectedThreadId) {
787
+ setThreadApproval(selectedThreadId, direct)
788
+ addSystemMessage('tool', '/approvals', `Approval policy set to ${direct}.`)
789
+ }
790
+ return
791
+ }
792
+ setShowApprovalsDialog(true)
793
+ return
794
+ }
795
+ case 'review': {
796
+ await runReviewCommand(rest || undefined)
797
+ return
798
+ }
799
+ case 'new': {
800
+ const accountId = selectedAccountId ?? threadAccountId
801
+ if (!accountId) {
802
+ addSystemMessage('tool', '/new', 'Select an account before starting a new session.')
803
+ return
804
+ }
805
+ await startNewThread(accountId, selectedApproval, webSearchEnabled)
806
+ return
807
+ }
808
+ case 'resume': {
809
+ setShowResumeDialog(true)
810
+ return
811
+ }
812
+ case 'init': {
813
+ await sendTurn(INIT_PROMPT)
814
+ return
815
+ }
816
+ case 'compact': {
817
+ await sendTurn('Summarize the conversation so far in a concise format for compaction.')
818
+ return
819
+ }
820
+ case 'diff': {
821
+ await runDiffCommand(rest || undefined)
822
+ return
823
+ }
824
+ case 'status': {
825
+ runStatusCommand()
826
+ return
827
+ }
828
+ case 'mcp': {
829
+ await runMcpCommand(rest || undefined)
830
+ return
831
+ }
832
+ case 'feedback': {
833
+ if (rest) {
834
+ setFeedbackReason(rest)
835
+ }
836
+ setShowFeedbackDialog(true)
837
+ return
838
+ }
839
+ case 'logout': {
840
+ await runLogoutCommand()
841
+ return
842
+ }
843
+ case 'quit':
844
+ case 'exit': {
845
+ addSystemMessage('tool', `/${command.id}`, 'Close this tab to exit Codex.')
846
+ return
847
+ }
848
+ case 'experimental': {
849
+ addSystemMessage('tool', '/experimental', 'Experimental features are not yet available in the web UI.')
850
+ return
851
+ }
852
+ default:
853
+ return
854
+ }
855
+ } catch {
856
+ setAlertDialog({
857
+ open: true,
858
+ title: 'Command Failed',
859
+ message: `/${command.id} did not complete successfully.`,
860
+ variant: 'error',
861
+ })
862
+ }
863
+ }
864
+
865
+ const submitComposer = async () => {
866
+ const trimmed = inputValue.trim()
867
+ if (!trimmed) {
868
+ return
869
+ }
870
+ const parsed = slashInput
871
+ if (parsed && !parsed.name) {
872
+ return
873
+ }
874
+ if (parsed?.name) {
875
+ const match = findSlashCommand(parsed.name, promptCommands)
876
+ if (match) {
877
+ setInputValue('')
878
+ await runSlashCommand(match, parsed.rest)
879
+ return
880
+ }
881
+ }
882
+ await sendTurn(trimmed)
883
+ }
884
+
885
+ const handleKeyDown = (e: React.KeyboardEvent) => {
886
+ if (slashMenuOpen) {
887
+ if (e.key === 'ArrowDown') {
888
+ e.preventDefault()
889
+ setSlashIndex((prev) => (prev + 1) % slashMatches.length)
890
+ return
891
+ }
892
+ if (e.key === 'ArrowUp') {
893
+ e.preventDefault()
894
+ setSlashIndex((prev) => (prev - 1 + slashMatches.length) % slashMatches.length)
895
+ return
896
+ }
897
+ if (e.key === 'Tab') {
898
+ e.preventDefault()
899
+ const selected = slashMatches[slashIndex]
900
+ if (selected) {
901
+ autocompleteSlashCommand(selected)
902
+ }
903
+ return
904
+ }
905
+ if (e.key === 'Enter') {
906
+ e.preventDefault()
907
+ const selected = slashMatches[slashIndex]
908
+ if (selected) {
909
+ setInputValue('')
910
+ void runSlashCommand(selected, '')
911
+ }
912
+ return
913
+ }
914
+ if (e.key === 'Escape') {
915
+ e.preventDefault()
916
+ setInputValue('')
917
+ return
918
+ }
919
+ }
920
+
921
+ if (mentionMenuOpen && mentionMatches.length > 0) {
922
+ if (e.key === 'ArrowDown') {
923
+ e.preventDefault()
924
+ setMentionIndex((prev) => (prev + 1) % mentionMatches.length)
925
+ return
926
+ }
927
+ if (e.key === 'ArrowUp') {
928
+ e.preventDefault()
929
+ setMentionIndex((prev) => (prev - 1 + mentionMatches.length) % mentionMatches.length)
930
+ return
931
+ }
932
+ if (e.key === 'Tab' || e.key === 'Enter') {
933
+ e.preventDefault()
934
+ const selected = mentionMatches[mentionIndex]
935
+ if (selected) {
936
+ handleMentionSelect(selected)
937
+ }
938
+ return
939
+ }
940
+ if (e.key === 'Escape') {
941
+ e.preventDefault()
942
+ const atIndex = inputValue.lastIndexOf('@')
943
+ if (atIndex !== -1) {
944
+ setInputValue(inputValue.slice(0, atIndex))
945
+ }
946
+ return
947
+ }
948
+ }
949
+
950
+ if (e.key === 'Enter' && !e.shiftKey) {
951
+ e.preventDefault()
952
+ void submitComposer()
953
+ }
954
+ }
955
+
956
+ const handleMentionSelect = (mention: FileMention) => {
957
+ // Add to file mentions list
958
+ if (!fileMentions.find(m => m.path === mention.path)) {
959
+ setFileMentions([...fileMentions, mention])
960
+ }
961
+ // Remove the @query from input
962
+ const atIndex = inputValue.lastIndexOf('@')
963
+ if (atIndex !== -1) {
964
+ setInputValue(inputValue.slice(0, atIndex))
965
+ }
966
+ }
967
+
968
+ const handleArchive = async () => {
969
+ if (!selectedThreadId || !selectedThread || !canInteract) {
970
+ return
971
+ }
972
+ try {
973
+ await hubClient.request(selectedThread.accountId, 'thread/archive', {
974
+ threadId: selectedThreadId,
975
+ })
976
+ updateThread(selectedThreadId, { status: 'archived' })
977
+ clearQueuedMessages(selectedThreadId)
978
+ if (activeTab !== 'archive') {
979
+ setInputValue('')
980
+ }
981
+ } catch {
982
+ // TODO: surface error state.
983
+ }
984
+ }
985
+
986
+ const refreshAccountStatus = async (profileId: string, silent = false) => {
987
+ try {
988
+ await refreshAccountSnapshot(profileId, updateAccount, setModelsForAccount)
989
+ } catch {
990
+ if (!silent) {
991
+ setAlertDialog({
992
+ open: true,
993
+ title: 'Status Check Failed',
994
+ message: 'Unable to refresh account status right now.',
995
+ variant: 'error',
996
+ })
997
+ }
998
+ }
999
+ }
1000
+
1001
+ const scheduleAuthPolling = (profileId: string, remaining = 6) => {
1002
+ if (remaining <= 0) {
1003
+ return
1004
+ }
1005
+ if (authPollRef.current) {
1006
+ window.clearTimeout(authPollRef.current)
1007
+ }
1008
+ authPollRef.current = window.setTimeout(async () => {
1009
+ await refreshAccountStatus(profileId, true)
1010
+ const updatedAccount = useAppStore.getState().accounts.find((item) => item.id === profileId)
1011
+ if (updatedAccount?.status === 'online') {
1012
+ return
1013
+ }
1014
+ scheduleAuthPolling(profileId, remaining - 1)
1015
+ }, 5000)
1016
+ }
1017
+
1018
+ const handleChatgptAuth = async () => {
1019
+ if (!account) {
1020
+ return
1021
+ }
1022
+ try {
1023
+ const login = (await hubClient.request(account.id, 'account/login/start', {
1024
+ type: 'chatgpt',
1025
+ })) as { authUrl?: string; loginId?: string }
1026
+ updateAccount(account.id, (prev) => ({ ...prev, status: 'degraded' }))
1027
+ if (login?.loginId) {
1028
+ setAccountLoginId(account.id, login.loginId)
1029
+ }
1030
+ if (login?.authUrl) {
1031
+ const opened = window.open(login.authUrl, '_blank', 'noopener,noreferrer')
1032
+ if (!opened) {
1033
+ setCopyDialog({ open: true, url: login.authUrl })
1034
+ }
1035
+ }
1036
+ scheduleAuthPolling(account.id)
1037
+ } catch {
1038
+ setAlertDialog({
1039
+ open: true,
1040
+ title: 'Sign In Failed',
1041
+ message: 'Unable to start ChatGPT sign-in. Please try again.',
1042
+ variant: 'error',
1043
+ })
1044
+ }
1045
+ }
1046
+
1047
+ const handleApiKeyAuth = async (apiKey: string) => {
1048
+ if (!account) {
1049
+ return
1050
+ }
1051
+ try {
1052
+ updateAccount(account.id, (prev) => ({ ...prev, status: 'degraded' }))
1053
+ await hubClient.request(account.id, 'account/login/start', {
1054
+ type: 'apiKey',
1055
+ apiKey,
1056
+ })
1057
+ setAccountLoginId(account.id, null)
1058
+ await refreshAccountStatus(account.id, true)
1059
+ } catch {
1060
+ setAlertDialog({
1061
+ open: true,
1062
+ title: 'API Key Failed',
1063
+ message: 'Unable to authenticate with that API key. Please check it and try again.',
1064
+ variant: 'error',
1065
+ })
1066
+ }
1067
+ }
1068
+
1069
+ const handleCancelAuth = async () => {
1070
+ if (!account) {
1071
+ return
1072
+ }
1073
+ const loginId = accountLoginIds[account.id]
1074
+ if (!loginId) {
1075
+ return
1076
+ }
1077
+ try {
1078
+ await hubClient.request(account.id, 'account/login/cancel', {
1079
+ loginId,
1080
+ })
1081
+ setAccountLoginId(account.id, null)
1082
+ } catch {
1083
+ setAlertDialog({
1084
+ open: true,
1085
+ title: 'Cancel Failed',
1086
+ message: 'Unable to cancel the login flow right now.',
1087
+ variant: 'error',
1088
+ })
1089
+ }
1090
+ }
1091
+
1092
+ const handleEmptyNewSession = async () => {
1093
+ if (connectionStatus !== 'connected') {
1094
+ setAlertDialog({
1095
+ open: true,
1096
+ title: 'Not Connected',
1097
+ message: 'Backend not connected. Start the hub and refresh the page.',
1098
+ variant: 'error',
1099
+ })
1100
+ return
1101
+ }
1102
+ const targetAccountId = selectedAccountId ?? accounts.find((item) => item.status === 'online')?.id ?? accounts[0]?.id
1103
+ if (!targetAccountId) {
1104
+ setAlertDialog({
1105
+ open: true,
1106
+ title: 'No Accounts',
1107
+ message: 'Add an account before creating a session.',
1108
+ variant: 'warning',
1109
+ })
1110
+ return
1111
+ }
1112
+ const targetAccount = accounts.find((item) => item.id === targetAccountId)
1113
+ if (targetAccount?.status !== 'online') {
1114
+ setAlertDialog({
1115
+ open: true,
1116
+ title: 'Authentication Required',
1117
+ message: 'Authenticate this account before creating a session.',
1118
+ variant: 'warning',
1119
+ })
1120
+ return
1121
+ }
1122
+ await startNewThread(targetAccountId, null)
1123
+ }
1124
+
1125
+ const handleInterruptTurn = async () => {
1126
+ if (!account || !selectedThreadId) {
1127
+ return
1128
+ }
1129
+ const turnId = threadTurnIds[selectedThreadId]
1130
+ if (!turnId) {
1131
+ return
1132
+ }
1133
+ try {
1134
+ await hubClient.request(account.id, 'turn/interrupt', {
1135
+ threadId: selectedThreadId,
1136
+ turnId,
1137
+ })
1138
+ } catch {
1139
+ setAlertDialog({
1140
+ open: true,
1141
+ title: 'Interrupt Failed',
1142
+ message: 'Unable to stop the running turn. Please try again.',
1143
+ variant: 'error',
1144
+ })
1145
+ }
1146
+ }
1147
+
1148
+ const applyModelDialog = () => {
1149
+ if (!selectedThreadId) {
1150
+ setShowModelDialog(false)
1151
+ return
1152
+ }
1153
+ if (pendingModelId) {
1154
+ setThreadModel(selectedThreadId, pendingModelId)
1155
+ const nextModel = models.find((model) => model.id === pendingModelId)
1156
+ if (nextModel?.defaultReasoningEffort) {
1157
+ setThreadEffort(selectedThreadId, nextModel.defaultReasoningEffort)
1158
+ }
1159
+ }
1160
+ if (pendingEffort) {
1161
+ setThreadEffort(selectedThreadId, pendingEffort as ReasoningEffort)
1162
+ }
1163
+ if (pendingSummary) {
1164
+ setThreadSummary(selectedThreadId, pendingSummary as ReasoningSummary)
1165
+ }
1166
+ if (pendingCwd.trim()) {
1167
+ setThreadCwd(selectedThreadId, pendingCwd.trim())
1168
+ } else {
1169
+ setThreadCwd(selectedThreadId, '')
1170
+ }
1171
+ setShowModelDialog(false)
1172
+ }
1173
+
1174
+ const applyApprovalDialog = () => {
1175
+ if (selectedThreadId) {
1176
+ setThreadApproval(selectedThreadId, pendingApproval)
1177
+ addSystemMessage('tool', '/approvals', `Approval policy set to ${pendingApproval}.`)
1178
+ }
1179
+ setShowApprovalsDialog(false)
1180
+ }
1181
+
1182
+ const handleSelectSkill = (name: string) => {
1183
+ setShowSkillsDialog(false)
1184
+ setComposerValue(`$${name}`)
1185
+ }
1186
+
1187
+ const handleResumeThread = (threadId: string) => {
1188
+ setSelectedThreadId(threadId)
1189
+ setShowResumeDialog(false)
1190
+ }
1191
+
1192
+ const handleSendFeedback = async () => {
1193
+ if (!account) {
1194
+ return
1195
+ }
1196
+ try {
1197
+ await hubClient.request(account.id, 'feedback/upload', {
1198
+ classification: feedbackCategory,
1199
+ reason: feedbackReason || null,
1200
+ threadId: selectedThreadId ?? null,
1201
+ includeLogs: feedbackIncludeLogs,
1202
+ })
1203
+ setShowFeedbackDialog(false)
1204
+ setFeedbackReason('')
1205
+ addSystemMessage('tool', '/feedback', 'Feedback sent. Thanks!')
1206
+ } catch {
1207
+ setAlertDialog({
1208
+ open: true,
1209
+ title: 'Feedback Failed',
1210
+ message: 'Unable to send feedback right now.',
1211
+ variant: 'error',
1212
+ })
1213
+ }
1214
+ }
1215
+
1216
+ if (!selectedThread) {
1217
+ return <SessionEmpty onNewSession={handleEmptyNewSession} />
1218
+ }
1219
+
1220
+ return (
1221
+ <main className="flex-1 flex flex-col h-full bg-bg-primary overflow-hidden">
1222
+ <SessionHeader
1223
+ title={selectedThread.title}
1224
+ accountName={account?.name}
1225
+ model={selectedThread.model}
1226
+ status={selectedThread.status}
1227
+ canInteract={canInteract}
1228
+ onArchive={handleArchive}
1229
+ />
1230
+ <SessionAuthBanner
1231
+ visible={!isAccountReady}
1232
+ pending={isAuthPending}
1233
+ onChatgpt={handleChatgptAuth}
1234
+ onApiKey={() => setShowApiKeyPrompt(true)}
1235
+ onCancel={account?.id && accountLoginIds[account.id] ? handleCancelAuth : undefined}
1236
+ onRefresh={account ? () => void refreshAccountStatus(account.id) : undefined}
1237
+ />
1238
+ <VirtualizedMessageList
1239
+ messages={threadMessages}
1240
+ approvals={pendingApprovals}
1241
+ queuedMessages={selectedThreadId ? queuedMessages[selectedThreadId] || [] : []}
1242
+ threadStatus={selectedThread.status}
1243
+ turnStartedAt={selectedThreadId ? threadTurnStartedAt[selectedThreadId] : undefined}
1244
+ lastTurnDuration={selectedThreadId ? threadLastTurnDuration[selectedThreadId] : undefined}
1245
+ onApprove={(approval) => {
1246
+ hubClient.respond(approval.profileId, approval.requestId, { decision: 'accept' })
1247
+ resolveApproval(approval.id, 'approved')
1248
+ }}
1249
+ onApproveForSession={(approval) => {
1250
+ const result =
1251
+ approval.type === 'command'
1252
+ ? { decision: 'accept', acceptSettings: { forSession: true } }
1253
+ : { decision: 'accept' }
1254
+ hubClient.respond(approval.profileId, approval.requestId, result)
1255
+ resolveApproval(approval.id, 'approved')
1256
+ if (approval.threadId) {
1257
+ setThreadApproval(approval.threadId, 'never')
1258
+ addSystemMessage('tool', '/approvals', 'Approval policy set to never for this session.')
1259
+ }
1260
+ }}
1261
+ onDeny={(approval) => {
1262
+ hubClient.respond(approval.profileId, approval.requestId, { decision: 'decline' })
1263
+ resolveApproval(approval.id, 'denied')
1264
+ }}
1265
+ onInterrupt={selectedThreadId && threadTurnIds[selectedThreadId] ? handleInterruptTurn : undefined}
1266
+ />
1267
+ <SessionComposer
1268
+ inputValue={inputValue}
1269
+ onInputChange={(value) => setInputValue(value)}
1270
+ onKeyDown={handleKeyDown}
1271
+ onSend={() => void submitComposer()}
1272
+ onStop={handleInterruptTurn}
1273
+ textareaRef={textareaRef}
1274
+ canInteract={canInteract}
1275
+ slashMenuOpen={slashMenuOpen}
1276
+ slashMatches={slashMatches}
1277
+ slashIndex={slashIndex}
1278
+ isTaskRunning={isTaskRunning ?? false}
1279
+ onSlashSelect={autocompleteSlashCommand}
1280
+ onSlashHover={setSlashIndex}
1281
+ modelOptions={modelOptions}
1282
+ effortOptions={effortOptions}
1283
+ effectiveModel={effectiveModel}
1284
+ effectiveEffort={(effectiveEffort ?? '') as string}
1285
+ onModelChange={(value) => {
1286
+ if (selectedThreadId) {
1287
+ setThreadModel(selectedThreadId, value)
1288
+ const nextModel = models.find((model) => model.id === value)
1289
+ if (nextModel?.defaultReasoningEffort) {
1290
+ setThreadEffort(selectedThreadId, nextModel.defaultReasoningEffort)
1291
+ }
1292
+ }
1293
+ }}
1294
+ onEffortChange={(value) => {
1295
+ if (selectedThreadId) {
1296
+ setThreadEffort(selectedThreadId, value as ReasoningEffort)
1297
+ }
1298
+ }}
1299
+ showModelSelect={models.length > 0}
1300
+ showEffortSelect={effortOptions.length > 0}
1301
+ queuedCount={queuedCount}
1302
+ webSearchEnabled={webSearchEnabled}
1303
+ onWebSearchToggle={() => {
1304
+ if (selectedThreadId) {
1305
+ setThreadWebSearch(selectedThreadId, !webSearchEnabled)
1306
+ }
1307
+ }}
1308
+ attachments={attachments}
1309
+ onAttachmentsChange={setAttachments}
1310
+ fileMentions={fileMentions}
1311
+ onFileMentionsChange={setFileMentions}
1312
+ mentionMenuOpen={mentionMenuOpen}
1313
+ mentionQuery={mentionQuery ?? ''}
1314
+ mentionMatches={mentionMatches}
1315
+ mentionIndex={mentionIndex}
1316
+ onMentionSelect={handleMentionSelect}
1317
+ onMentionHover={setMentionIndex}
1318
+ />
1319
+ <SessionDialogs
1320
+ showModelDialog={showModelDialog}
1321
+ onCloseModelDialog={() => setShowModelDialog(false)}
1322
+ modelOptions={modelOptions}
1323
+ pendingModelId={pendingModelId}
1324
+ setPendingModelId={setPendingModelId}
1325
+ pendingEffortOptions={pendingEffortOptions}
1326
+ pendingEffort={pendingEffort}
1327
+ setPendingEffort={setPendingEffort}
1328
+ summaryOptions={summaryOptions}
1329
+ pendingSummary={pendingSummary}
1330
+ setPendingSummary={setPendingSummary}
1331
+ pendingCwd={pendingCwd}
1332
+ setPendingCwd={setPendingCwd}
1333
+ onApplyModel={applyModelDialog}
1334
+ showApprovalsDialog={showApprovalsDialog}
1335
+ onCloseApprovalsDialog={() => setShowApprovalsDialog(false)}
1336
+ approvalOptions={approvalOptions}
1337
+ pendingApproval={pendingApproval}
1338
+ setPendingApproval={setPendingApproval}
1339
+ onApplyApproval={applyApprovalDialog}
1340
+ showSkillsDialog={showSkillsDialog}
1341
+ onCloseSkillsDialog={() => setShowSkillsDialog(false)}
1342
+ skillsLoading={skillsLoading}
1343
+ skillsError={skillsError}
1344
+ skillsList={skillsList}
1345
+ onSelectSkill={handleSelectSkill}
1346
+ showResumeDialog={showResumeDialog}
1347
+ onCloseResumeDialog={() => setShowResumeDialog(false)}
1348
+ resumeCandidates={resumeCandidates}
1349
+ onResumeThread={handleResumeThread}
1350
+ showFeedbackDialog={showFeedbackDialog}
1351
+ onCloseFeedbackDialog={() => setShowFeedbackDialog(false)}
1352
+ feedbackCategory={feedbackCategory}
1353
+ setFeedbackCategory={setFeedbackCategory}
1354
+ feedbackReason={feedbackReason}
1355
+ setFeedbackReason={setFeedbackReason}
1356
+ feedbackIncludeLogs={feedbackIncludeLogs}
1357
+ setFeedbackIncludeLogs={setFeedbackIncludeLogs}
1358
+ onSendFeedback={handleSendFeedback}
1359
+ showApiKeyPrompt={showApiKeyPrompt}
1360
+ onCloseApiKeyPrompt={() => setShowApiKeyPrompt(false)}
1361
+ onApiKeySubmit={handleApiKeyAuth}
1362
+ copyDialog={copyDialog}
1363
+ setCopyDialog={setCopyDialog}
1364
+ alertDialog={alertDialog}
1365
+ setAlertDialog={setAlertDialog}
1366
+ />
1367
+ </main>
1368
+ )
1369
+ }
1370
+
1371
+ const formatEffortLabel = (effort: string) => {
1372
+ if (effort === 'xhigh') return 'X-High'
1373
+ return effort.charAt(0).toUpperCase() + effort.slice(1)
1374
+ }