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,848 @@
1
+ import { useState, useMemo, useEffect, useRef } from 'react'
2
+ import { useAppStore } from '../../store'
3
+ import { hubClient } from '../../services/hub-client'
4
+ import { Icons, Button, Select, Input, Markdown } from '../ui'
5
+ import type { Message } from '../../types'
6
+ import { buildSystemMessage } from '../../utils/item-format'
7
+
8
+ const CWD_MAX_RESULTS = 8
9
+ type ThreadResumeResult = {
10
+ thread?: {
11
+ turns?: TurnData[]
12
+ }
13
+ model?: string | null
14
+ cwd?: string | null
15
+ }
16
+
17
+ type TurnData = {
18
+ id?: string
19
+ items?: ThreadItem[]
20
+ }
21
+
22
+ type ThreadItem = {
23
+ type: string
24
+ id: string
25
+ content?: UserInput[]
26
+ text?: string
27
+ review?: string
28
+ }
29
+
30
+ type UserInput =
31
+ | { type: 'text'; text: string }
32
+ | { type: 'image'; url: string }
33
+ | { type: 'localImage'; path: string }
34
+
35
+ const toUserContent = (content: UserInput[] = []) => {
36
+ const parts = content
37
+ .map((input) => {
38
+ if (input.type === 'text') {
39
+ return input.text
40
+ }
41
+ if (input.type === 'image') {
42
+ return `[image: ${input.url}]`
43
+ }
44
+ if (input.type === 'localImage') {
45
+ return `[image: ${input.path}]`
46
+ }
47
+ return ''
48
+ })
49
+ .filter((part) => part.trim().length > 0)
50
+
51
+ return parts.join('\n')
52
+ }
53
+
54
+ const buildMessagesFromTurns = (turns: TurnData[] = []): Message[] => {
55
+ const messages: Message[] = []
56
+ turns.forEach((turn) => {
57
+ turn.items?.forEach((item) => {
58
+ if (item.type === 'userMessage') {
59
+ const content = toUserContent(item.content ?? [])
60
+ if (!content) {
61
+ return
62
+ }
63
+ messages.push({
64
+ id: item.id,
65
+ role: 'user',
66
+ content,
67
+ kind: 'chat',
68
+ timestamp: '',
69
+ })
70
+ return
71
+ }
72
+
73
+ if (item.type === 'agentMessage') {
74
+ if (!item.text?.trim()) {
75
+ return
76
+ }
77
+ messages.push({
78
+ id: item.id,
79
+ role: 'assistant',
80
+ content: item.text,
81
+ kind: 'chat',
82
+ timestamp: '',
83
+ })
84
+ return
85
+ }
86
+
87
+ const systemMessage = buildSystemMessage(item)
88
+ if (systemMessage) {
89
+ messages.push(systemMessage)
90
+ }
91
+ })
92
+ })
93
+ return messages
94
+ }
95
+
96
+ const findReviewOutput = (turns: TurnData[] = []) => {
97
+ for (const turn of turns) {
98
+ for (const item of turn.items ?? []) {
99
+ if (item.type === 'exitedReviewMode' && typeof item.review === 'string' && item.review.trim()) {
100
+ return item.review
101
+ }
102
+ }
103
+ }
104
+ return ''
105
+ }
106
+
107
+ export function ReviewsView() {
108
+ const {
109
+ accounts,
110
+ selectedAccountId,
111
+ setSelectedAccountId,
112
+ modelsByAccount,
113
+ reviewSessions,
114
+ upsertReviewSession,
115
+ updateReviewSession,
116
+ setMessagesForThread,
117
+ connectionStatus,
118
+ messages,
119
+ } = useAppStore()
120
+ const [projectPath, setProjectPath] = useState('')
121
+ const [selectedModel, setSelectedModel] = useState<string>('')
122
+ const [selectedReviewId, setSelectedReviewId] = useState<string | null>(null)
123
+ const [isStarting, setIsStarting] = useState(false)
124
+ const [targetType, setTargetType] = useState<'uncommitted' | 'base' | 'commit' | 'custom'>('uncommitted')
125
+ const [targetBranch, setTargetBranch] = useState('main')
126
+ const [targetCommit, setTargetCommit] = useState('')
127
+ const [targetCommitTitle, setTargetCommitTitle] = useState('')
128
+ const [targetInstructions, setTargetInstructions] = useState('')
129
+ const [cwdMatches, setCwdMatches] = useState<string[]>([])
130
+ const [cwdMenuOpen, setCwdMenuOpen] = useState(false)
131
+ const [cwdIndex, setCwdIndex] = useState(0)
132
+ const [cwdRoot, setCwdRoot] = useState<string>('')
133
+
134
+ const selectedAccount = accounts.find((a) => a.id === selectedAccountId)
135
+ const availableModels = selectedAccountId ? (modelsByAccount[selectedAccountId] ?? []) : []
136
+ const orderedSessions = useMemo(
137
+ () => [...reviewSessions].sort((a, b) => b.startedAt - a.startedAt),
138
+ [reviewSessions]
139
+ )
140
+
141
+ const modelOptions = useMemo(
142
+ () =>
143
+ availableModels.map((model) => ({
144
+ value: model.id,
145
+ label: model.displayName,
146
+ description: model.description,
147
+ })),
148
+ [availableModels]
149
+ )
150
+
151
+ const accountOptions = useMemo(
152
+ () =>
153
+ accounts
154
+ .filter((account) => account.status === 'online')
155
+ .map((account) => ({
156
+ value: account.id,
157
+ label: account.name,
158
+ })),
159
+ [accounts]
160
+ )
161
+
162
+ const targetOptions = useMemo(
163
+ () => [
164
+ { value: 'uncommitted', label: 'Uncommitted changes' },
165
+ { value: 'base', label: 'Base branch' },
166
+ { value: 'commit', label: 'Commit' },
167
+ { value: 'custom', label: 'Custom instructions' },
168
+ ],
169
+ []
170
+ )
171
+
172
+ const selectedReview = reviewSessions.find((session) => session.id === selectedReviewId)
173
+ const reviewMessages = selectedReview?.threadId ? messages[selectedReview.threadId] ?? [] : []
174
+ const hasReviewStream = reviewMessages.length > 0
175
+ const resumeInFlight = useRef<Set<string>>(new Set())
176
+
177
+ useEffect(() => {
178
+ if (!selectedAccountId || connectionStatus !== 'connected' || cwdRoot) {
179
+ return
180
+ }
181
+
182
+ let cancelled = false
183
+ const loadRoot = async () => {
184
+ try {
185
+ const result = (await hubClient.request(selectedAccountId, 'command/exec', {
186
+ command: ['pwd'],
187
+ timeoutMs: 1500,
188
+ })) as { stdout?: string }
189
+ const root = (result.stdout ?? '').split('\n')[0]?.trim() ?? ''
190
+ if (!cancelled && root) {
191
+ setCwdRoot(root)
192
+ }
193
+ } catch {
194
+ // keep empty; we'll fall back to relative paths
195
+ }
196
+ }
197
+
198
+ loadRoot()
199
+
200
+ return () => {
201
+ cancelled = true
202
+ }
203
+ }, [selectedAccountId, connectionStatus, cwdRoot])
204
+
205
+ useEffect(() => {
206
+ if (!selectedReview?.threadId || connectionStatus !== 'connected') {
207
+ return
208
+ }
209
+
210
+ const existing = messages[selectedReview.threadId]
211
+ if (existing && existing.length) {
212
+ return
213
+ }
214
+
215
+ if (resumeInFlight.current.has(selectedReview.threadId)) {
216
+ return
217
+ }
218
+
219
+ let cancelled = false
220
+ resumeInFlight.current.add(selectedReview.threadId)
221
+
222
+ const loadHistory = async () => {
223
+ try {
224
+ const result = (await hubClient.request(selectedReview.profileId, 'thread/resume', {
225
+ threadId: selectedReview.threadId,
226
+ })) as ThreadResumeResult
227
+
228
+ if (cancelled) {
229
+ return
230
+ }
231
+
232
+ const resumeThread = result.thread
233
+ if (!resumeThread?.turns) {
234
+ return
235
+ }
236
+
237
+ const loadedMessages = buildMessagesFromTurns(resumeThread.turns)
238
+ setMessagesForThread(selectedReview.threadId, loadedMessages)
239
+
240
+ const reviewOutput = findReviewOutput(resumeThread.turns)
241
+ if (reviewOutput) {
242
+ updateReviewSession(selectedReview.id, {
243
+ status: 'completed',
244
+ review: reviewOutput,
245
+ completedAt: Date.now(),
246
+ })
247
+ }
248
+ } finally {
249
+ resumeInFlight.current.delete(selectedReview.threadId)
250
+ }
251
+ }
252
+
253
+ loadHistory().catch(() => {
254
+ resumeInFlight.current.delete(selectedReview.threadId)
255
+ })
256
+
257
+ return () => {
258
+ cancelled = true
259
+ }
260
+ }, [selectedReview, connectionStatus, messages, setMessagesForThread, updateReviewSession])
261
+
262
+ useEffect(() => {
263
+ if (connectionStatus !== 'connected') {
264
+ return
265
+ }
266
+
267
+ let cancelled = false
268
+
269
+ const loadReviews = async () => {
270
+ try {
271
+ const sessions = await hubClient.listReviews({ limit: 200 })
272
+ if (cancelled) {
273
+ return
274
+ }
275
+ sessions.forEach((session) => {
276
+ upsertReviewSession({
277
+ id: session.id,
278
+ threadId: session.threadId,
279
+ profileId: session.profileId,
280
+ model: session.model ?? undefined,
281
+ cwd: session.cwd ?? undefined,
282
+ status: session.status,
283
+ startedAt: session.startedAt,
284
+ completedAt: session.completedAt ?? undefined,
285
+ label: session.label ?? undefined,
286
+ review: session.review ?? undefined,
287
+ })
288
+ })
289
+ } catch (error) {
290
+ console.warn('[Reviews] Failed to load review sessions', error)
291
+ }
292
+ }
293
+
294
+ loadReviews()
295
+
296
+ return () => {
297
+ cancelled = true
298
+ }
299
+ }, [connectionStatus, upsertReviewSession])
300
+
301
+ useEffect(() => {
302
+ const query = projectPath.trim()
303
+ if (!query || !selectedAccountId || connectionStatus !== 'connected') {
304
+ setCwdMatches([])
305
+ setCwdMenuOpen(false)
306
+ return
307
+ }
308
+
309
+ let cancelled = false
310
+ const timeoutId = window.setTimeout(async () => {
311
+ const { baseDir, needle, isRooted } = splitPath(query, cwdRoot)
312
+ const searchCwd = isRooted ? baseDir : null
313
+ const maxDepth = needle ? '4' : '2'
314
+ const command = needle
315
+ ? ['find', '.', '-maxdepth', maxDepth, '-type', 'd', '-name', `*${needle}*`, '-not', '-path', '*/.git/*', '-not', '-path', '*/node_modules/*', '-not', '-path', '*/.codex/*', '-not', '-path', '*/.cache/*']
316
+ : ['find', '.', '-maxdepth', maxDepth, '-type', 'd', '-not', '-name', '.', '-not', '-path', '*/.git/*', '-not', '-path', '*/node_modules/*', '-not', '-path', '*/.codex/*', '-not', '-path', '*/.cache/*']
317
+ try {
318
+ const result = (await hubClient.request(selectedAccountId, 'command/exec', {
319
+ command,
320
+ timeoutMs: 3000,
321
+ cwd: searchCwd,
322
+ sandboxPolicy: null,
323
+ })) as { stdout?: string; exitCode?: number }
324
+
325
+ if (cancelled) {
326
+ return
327
+ }
328
+
329
+ const lines = (result.stdout ?? '')
330
+ .split('\n')
331
+ .map((line) => line.trim())
332
+ .filter(Boolean)
333
+
334
+ const resolved = lines.map((line) => resolveCwdSuggestion(baseDir, line))
335
+ setCwdMatches(resolved.slice(0, CWD_MAX_RESULTS))
336
+ setCwdMenuOpen(resolved.length > 0)
337
+ setCwdIndex(0)
338
+ } catch {
339
+ if (!cancelled) {
340
+ setCwdMatches([])
341
+ setCwdMenuOpen(false)
342
+ }
343
+ }
344
+ }, 200)
345
+
346
+ return () => {
347
+ cancelled = true
348
+ window.clearTimeout(timeoutId)
349
+ }
350
+ }, [projectPath, selectedAccountId, connectionStatus, cwdRoot])
351
+
352
+ const handleStartReview = async () => {
353
+ if (!projectPath || !selectedModel || !selectedAccountId) {
354
+ return
355
+ }
356
+
357
+ setIsStarting(true)
358
+ try {
359
+ const resolvedProjectPath = resolveProjectPath(projectPath, cwdRoot)
360
+ const threadResult = (await hubClient.request(selectedAccountId, 'thread/start', {
361
+ model: selectedModel,
362
+ cwd: resolvedProjectPath,
363
+ })) as { thread?: { id?: string } }
364
+ const threadId = threadResult.thread?.id
365
+ if (!threadId) {
366
+ throw new Error('Failed to start review thread')
367
+ }
368
+
369
+ const { target, label } = buildReviewTarget({
370
+ type: targetType,
371
+ branch: targetBranch,
372
+ commit: targetCommit,
373
+ commitTitle: targetCommitTitle,
374
+ instructions: targetInstructions,
375
+ })
376
+
377
+ const reviewResult = (await hubClient.request(selectedAccountId, 'review/start', {
378
+ threadId,
379
+ delivery: 'inline',
380
+ target,
381
+ })) as { turn?: { id?: string; status?: string } }
382
+
383
+ const turnId = reviewResult.turn?.id ?? `review-${Date.now()}`
384
+ upsertReviewSession({
385
+ id: turnId,
386
+ threadId,
387
+ profileId: selectedAccountId,
388
+ model: selectedModel,
389
+ cwd: resolvedProjectPath,
390
+ status: 'running',
391
+ startedAt: Date.now(),
392
+ label: label ? `${projectLabel(resolvedProjectPath)} · ${label}` : projectLabel(resolvedProjectPath),
393
+ })
394
+ setSelectedReviewId(turnId)
395
+ setProjectPath('')
396
+ } catch (error) {
397
+ const fallbackId = `review-${Date.now()}`
398
+ upsertReviewSession({
399
+ id: fallbackId,
400
+ threadId: '',
401
+ profileId: selectedAccountId,
402
+ model: selectedModel,
403
+ cwd: resolveProjectPath(projectPath, cwdRoot),
404
+ status: 'failed',
405
+ startedAt: Date.now(),
406
+ })
407
+ setSelectedReviewId(fallbackId)
408
+ console.error(error)
409
+ updateReviewSession(fallbackId, { status: 'failed' })
410
+ } finally {
411
+ setIsStarting(false)
412
+ }
413
+ }
414
+
415
+ const targetReady =
416
+ targetType === 'uncommitted' ||
417
+ (targetType === 'base' && targetBranch.trim()) ||
418
+ (targetType === 'commit' && targetCommit.trim()) ||
419
+ (targetType === 'custom' && targetInstructions.trim())
420
+
421
+ const canStart =
422
+ projectPath &&
423
+ selectedModel &&
424
+ selectedAccountId &&
425
+ selectedAccount?.status === 'online' &&
426
+ connectionStatus === 'connected' &&
427
+ targetReady
428
+
429
+ if (selectedReview) {
430
+ return (
431
+ <div className="flex-1 flex flex-col h-full bg-bg-primary overflow-hidden">
432
+ <header className="shrink-0 px-5 py-3 border-b border-border flex items-center gap-3">
433
+ <button
434
+ onClick={() => setSelectedReviewId(null)}
435
+ className="p-1.5 -ml-1.5 rounded-md hover:bg-bg-hover transition-colors"
436
+ >
437
+ <Icons.ChevronLeft className="w-4 h-4 text-text-muted" />
438
+ </button>
439
+ <div className="flex-1 min-w-0">
440
+ <h1 className="text-sm font-medium text-text-primary truncate">
441
+ {selectedReview.label ?? 'Code Review'}
442
+ </h1>
443
+ <p className="text-[11px] text-text-muted truncate">
444
+ {selectedReview.model ?? 'default model'} · {formatRelativeTime(new Date(selectedReview.startedAt))}
445
+ </p>
446
+ </div>
447
+ {selectedReview.status === 'running' && (
448
+ <span className="text-[11px] text-text-muted">Analyzing...</span>
449
+ )}
450
+ </header>
451
+
452
+ <div className="flex-1 overflow-y-auto pb-10">
453
+ {selectedReview.review ? (
454
+ <div className="p-4">
455
+ <Markdown content={selectedReview.review} className="text-xs" />
456
+ </div>
457
+ ) : hasReviewStream ? (
458
+ <div className="p-4">
459
+ {selectedReview.status === 'running' && (
460
+ <div className="flex items-center gap-2 text-[11px] text-text-muted mb-3">
461
+ <Icons.Loader className="w-3.5 h-3.5 text-text-muted" />
462
+ Review running · live output
463
+ </div>
464
+ )}
465
+ <ReviewStream messages={reviewMessages} />
466
+ </div>
467
+ ) : selectedReview.status === 'running' ? (
468
+ <div className="flex flex-col items-center justify-center h-full px-6">
469
+ <Icons.Loader className="w-5 h-5 text-text-muted mb-3" />
470
+ <p className="text-sm text-text-secondary">
471
+ Review started · {selectedReview.label ?? 'Scanning codebase'}
472
+ </p>
473
+ <p className="text-[11px] text-text-muted mt-2 text-center max-w-md">
474
+ Review output appears when the analysis completes. Large repos can take a few minutes.
475
+ </p>
476
+ </div>
477
+ ) : (
478
+ <div className="flex flex-col items-center justify-center h-full">
479
+ <p className="text-sm text-text-secondary">Waiting for review output...</p>
480
+ </div>
481
+ )}
482
+ </div>
483
+ </div>
484
+ )
485
+ }
486
+
487
+ const hasReviews = orderedSessions.length > 0
488
+
489
+ const renderForm = (centered: boolean) => (
490
+ <div className="space-y-2.5">
491
+ <div className="relative">
492
+ <Input
493
+ placeholder={centered ? 'Path or GitHub URL (https://github.com/org/repo)' : 'Path or GitHub URL'}
494
+ value={projectPath}
495
+ onChange={setProjectPath}
496
+ onFocus={() => setCwdMenuOpen(cwdMatches.length > 0)}
497
+ onBlur={() => window.setTimeout(() => setCwdMenuOpen(false), 150)}
498
+ onKeyDown={(event) => handleCwdKeyDown(event, cwdMatches, cwdIndex, setCwdIndex, (value) => {
499
+ setProjectPath(value)
500
+ setCwdMenuOpen(false)
501
+ })}
502
+ />
503
+ {cwdMenuOpen && cwdMatches.length > 0 && (
504
+ <div className="absolute z-20 mt-1 w-full rounded-lg border border-border bg-bg-elevated shadow-lg overflow-hidden">
505
+ {cwdMatches.map((match, index) => (
506
+ <button
507
+ key={match}
508
+ onMouseDown={() => {
509
+ setProjectPath(match)
510
+ setCwdMenuOpen(false)
511
+ }}
512
+ className={`w-full text-left px-3 py-1.5 text-[11px] font-mono transition-colors first:pt-2 last:pb-2 ${
513
+ index === cwdIndex ? 'bg-bg-hover text-text-primary' : 'text-text-secondary'
514
+ }`}
515
+ >
516
+ {match}
517
+ </button>
518
+ ))}
519
+ </div>
520
+ )}
521
+ </div>
522
+
523
+ <div className="flex gap-2 text-[11px] text-text-muted">
524
+ <button
525
+ type="button"
526
+ onClick={() => setProjectPath('~/projects/my-app')}
527
+ className="flex items-center gap-1 px-2 py-1 rounded-md border border-border bg-bg-tertiary hover:bg-bg-hover transition-colors"
528
+ >
529
+ <Icons.Terminal className="w-3.5 h-3.5" />
530
+ Local path
531
+ </button>
532
+ <button
533
+ type="button"
534
+ onClick={() => setProjectPath('https://github.com/org/repo')}
535
+ className="flex items-center gap-1 px-2 py-1 rounded-md border border-border bg-bg-tertiary hover:bg-bg-hover transition-colors"
536
+ >
537
+ <Icons.Globe className="w-3.5 h-3.5" />
538
+ GitHub repo
539
+ </button>
540
+ </div>
541
+
542
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 items-stretch">
543
+ <Select
544
+ options={accountOptions}
545
+ value={selectedAccountId ?? ''}
546
+ onChange={(value) => setSelectedAccountId(value)}
547
+ placeholder="Account"
548
+ size="sm"
549
+ className="flex-1 min-w-0"
550
+ />
551
+ <Select
552
+ options={modelOptions}
553
+ value={selectedModel}
554
+ onChange={setSelectedModel}
555
+ placeholder="Model"
556
+ disabled={!availableModels.length}
557
+ size="sm"
558
+ className="flex-1 min-w-0"
559
+ />
560
+ <Select
561
+ options={targetOptions}
562
+ value={targetType}
563
+ onChange={(value) => setTargetType(value as 'uncommitted' | 'base' | 'commit' | 'custom')}
564
+ placeholder="Target"
565
+ size="sm"
566
+ className="flex-1 min-w-0"
567
+ />
568
+ </div>
569
+
570
+ {targetType === 'base' && (
571
+ <Input
572
+ placeholder="Base branch (e.g. main)"
573
+ value={targetBranch}
574
+ onChange={setTargetBranch}
575
+ />
576
+ )}
577
+ {targetType === 'commit' && (
578
+ <div className="flex gap-2">
579
+ <Input
580
+ placeholder="Commit SHA"
581
+ value={targetCommit}
582
+ onChange={setTargetCommit}
583
+ className="flex-1"
584
+ />
585
+ <Input
586
+ placeholder="Title (optional)"
587
+ value={targetCommitTitle}
588
+ onChange={setTargetCommitTitle}
589
+ className="flex-[1.5]"
590
+ />
591
+ </div>
592
+ )}
593
+ {targetType === 'custom' && (
594
+ <textarea
595
+ className="w-full bg-bg-tertiary border border-border rounded-lg px-3 py-2 text-xs text-text-primary placeholder:text-text-muted outline-none focus:border-text-muted transition-colors resize-none"
596
+ rows={3}
597
+ placeholder="Describe what you want reviewed..."
598
+ value={targetInstructions}
599
+ onChange={(event) => setTargetInstructions(event.target.value)}
600
+ />
601
+ )}
602
+
603
+ <Button variant="primary" fullWidth disabled={!canStart || isStarting} onClick={handleStartReview}>
604
+ {isStarting ? 'Starting...' : centered ? 'Start Review' : 'Review'}
605
+ </Button>
606
+ </div>
607
+ )
608
+
609
+ return (
610
+ <div className="flex-1 flex flex-col h-full bg-bg-primary overflow-y-auto">
611
+ {hasReviews ? (
612
+ <div className="max-w-3xl mx-auto w-full px-5 py-8">
613
+ <div className="mb-5">
614
+ {renderForm(false)}
615
+ </div>
616
+
617
+ <div>
618
+ <p className="text-[10px] text-text-muted uppercase tracking-wide mb-2">Recent</p>
619
+ <div className="space-y-1">
620
+ {orderedSessions.map((session) => (
621
+ <button
622
+ key={session.id}
623
+ onClick={() => setSelectedReviewId(session.id)}
624
+ className="w-full text-left px-3 py-2.5 rounded-lg hover:bg-bg-hover transition-colors group"
625
+ >
626
+ <div className="flex items-center gap-3">
627
+ <div className={`w-1.5 h-1.5 rounded-full shrink-0 ${
628
+ session.status === 'running'
629
+ ? 'bg-accent-blue animate-pulse'
630
+ : session.status === 'completed'
631
+ ? 'bg-accent-green'
632
+ : session.status === 'failed'
633
+ ? 'bg-accent-red'
634
+ : 'bg-text-muted'
635
+ }`} />
636
+ <div className="flex-1 min-w-0">
637
+ <div className="flex items-baseline gap-2">
638
+ <span className="text-xs text-text-primary truncate">
639
+ {session.label ?? 'Code Review'}
640
+ </span>
641
+ <span className="text-[10px] text-text-muted shrink-0">
642
+ {formatRelativeTime(new Date(session.startedAt))}
643
+ </span>
644
+ </div>
645
+ <p className="text-[10px] text-text-muted truncate mt-0.5">
646
+ {accounts.find((a) => a.id === session.profileId)?.name ?? 'Unknown'} · {session.model}
647
+ </p>
648
+ </div>
649
+ <Icons.ChevronRight className="w-3.5 h-3.5 text-text-muted opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
650
+ </div>
651
+ </button>
652
+ ))}
653
+ </div>
654
+ </div>
655
+ </div>
656
+ ) : (
657
+ <div className="flex-1 flex items-center justify-center px-4 py-10">
658
+ <div className="w-full max-w-sm">
659
+ <div className="text-center mb-6">
660
+ <div className="inline-flex items-center justify-center w-10 h-10 rounded-xl bg-bg-tertiary mb-3">
661
+ <Icons.Search className="w-5 h-5 text-text-secondary" />
662
+ </div>
663
+ <h1 className="text-lg font-medium text-text-primary">Code Review</h1>
664
+ <p className="text-xs text-text-muted mt-1.5 max-w-[260px] mx-auto leading-relaxed">
665
+ Analyze your codebase for issues, improvements, and best practices
666
+ </p>
667
+ </div>
668
+
669
+ {renderForm(true)}
670
+
671
+ {accounts.length > 0 && !accountOptions.length && (
672
+ <p className="text-[11px] text-text-muted text-center mt-4">Sign in to an account to start</p>
673
+ )}
674
+ </div>
675
+ </div>
676
+ )}
677
+ </div>
678
+ )
679
+ }
680
+
681
+ function buildReviewTarget(options: {
682
+ type: 'uncommitted' | 'base' | 'commit' | 'custom'
683
+ branch: string
684
+ commit: string
685
+ commitTitle: string
686
+ instructions: string
687
+ }) {
688
+ switch (options.type) {
689
+ case 'base':
690
+ return {
691
+ target: { type: 'baseBranch', branch: options.branch.trim() || 'main' },
692
+ label: `base ${options.branch.trim() || 'main'}`,
693
+ }
694
+ case 'commit':
695
+ return {
696
+ target: { type: 'commit', sha: options.commit.trim(), title: options.commitTitle.trim() || undefined },
697
+ label: `commit ${options.commit.trim().slice(0, 8)}`,
698
+ }
699
+ case 'custom':
700
+ return {
701
+ target: { type: 'custom', instructions: options.instructions.trim() },
702
+ label: 'custom instructions',
703
+ }
704
+ default:
705
+ return { target: { type: 'uncommittedChanges' }, label: 'uncommitted changes' }
706
+ }
707
+ }
708
+
709
+ function splitPath(input: string, root: string): { baseDir: string; needle: string; isRooted: boolean } {
710
+ const normalized = input.trim()
711
+ const lastSlash = normalized.lastIndexOf('/')
712
+ if (lastSlash === -1) {
713
+ const baseDir = root ? normalizeJoin(root, '.') : '.'
714
+ return { baseDir, needle: normalized, isRooted: Boolean(root) }
715
+ }
716
+ let baseDir = normalized.slice(0, lastSlash) || '/'
717
+ const needle = normalized.slice(lastSlash + 1)
718
+ const isRooted = baseDir.startsWith('/')
719
+ if (!isRooted && root) {
720
+ baseDir = normalizeJoin(root, baseDir)
721
+ }
722
+ return { baseDir, needle, isRooted: isRooted || Boolean(root) }
723
+ }
724
+
725
+ function resolveCwdSuggestion(baseDir: string, match: string): string {
726
+ const cleaned = match.replace(/^\.\//, '')
727
+ if (baseDir === '.' || baseDir === '') {
728
+ return cleaned
729
+ }
730
+ if (baseDir === '/') {
731
+ return `/${cleaned}`
732
+ }
733
+ return normalizeJoin(baseDir, cleaned)
734
+ }
735
+
736
+ function normalizeJoin(root: string, relative: string): string {
737
+ const cleanedRoot = root.replace(/\/$/, '')
738
+ const cleanedRelative = relative.replace(/^\.\/+/, '').replace(/^\/+/, '')
739
+ if (!cleanedRelative || cleanedRelative === '.') {
740
+ return cleanedRoot || '/'
741
+ }
742
+ return `${cleanedRoot}/${cleanedRelative}`
743
+ }
744
+
745
+ function resolveProjectPath(input: string, root: string): string {
746
+ const trimmed = input.trim()
747
+ if (!trimmed) {
748
+ return trimmed
749
+ }
750
+ if (trimmed.startsWith('/')) {
751
+ return trimmed
752
+ }
753
+ if (!root) {
754
+ return trimmed
755
+ }
756
+ return normalizeJoin(root, trimmed)
757
+ }
758
+
759
+
760
+ function handleCwdKeyDown(
761
+ event: React.KeyboardEvent<HTMLInputElement>,
762
+ matches: string[],
763
+ index: number,
764
+ setIndex: (value: number) => void,
765
+ onSelect: (value: string) => void
766
+ ) {
767
+ if (!matches.length) {
768
+ return
769
+ }
770
+ if (event.key === 'ArrowDown') {
771
+ event.preventDefault()
772
+ setIndex((index + 1) % matches.length)
773
+ return
774
+ }
775
+ if (event.key === 'ArrowUp') {
776
+ event.preventDefault()
777
+ setIndex((index - 1 + matches.length) % matches.length)
778
+ return
779
+ }
780
+ if (event.key === 'Enter') {
781
+ event.preventDefault()
782
+ onSelect(matches[index])
783
+ }
784
+ }
785
+
786
+ function projectLabel(path: string): string {
787
+ const trimmed = path.trim()
788
+ if (!trimmed) {
789
+ return 'Code Review'
790
+ }
791
+ const parts = trimmed.split('/')
792
+ return parts[parts.length - 1] || trimmed
793
+ }
794
+
795
+ function formatRelativeTime(date: Date): string {
796
+ const now = new Date()
797
+ const diffMs = now.getTime() - date.getTime()
798
+ const diffMins = Math.floor(diffMs / 60000)
799
+ const diffHours = Math.floor(diffMins / 60)
800
+
801
+ if (diffMins < 1) return 'now'
802
+ if (diffMins < 60) return `${diffMins}m`
803
+ if (diffHours < 24) return `${diffHours}h`
804
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
805
+ }
806
+
807
+ function ReviewStream({ messages }: { messages: Message[] }) {
808
+ const endRef = useRef<HTMLDivElement | null>(null)
809
+
810
+ useEffect(() => {
811
+ endRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' })
812
+ }, [messages])
813
+
814
+ return (
815
+ <div className="space-y-3 overflow-y-auto max-h-[70vh] pr-1">
816
+ {messages.map((message) => (
817
+ <div key={message.id} className="rounded-lg border border-border bg-bg-tertiary px-3 py-2">
818
+ <div className="flex items-center justify-between gap-3 mb-1">
819
+ <span className="text-[10px] uppercase tracking-wide text-text-muted">
820
+ {formatReviewLabel(message)}
821
+ </span>
822
+ {message.timestamp && (
823
+ <span className="text-[10px] text-text-muted">{message.timestamp}</span>
824
+ )}
825
+ </div>
826
+ <Markdown content={message.content} className="text-[11px] text-text-secondary" streaming />
827
+ </div>
828
+ ))}
829
+ <div ref={endRef} />
830
+ </div>
831
+ )
832
+ }
833
+
834
+ function formatReviewLabel(message: Message): string {
835
+ if (message.role === 'user') {
836
+ return 'You'
837
+ }
838
+ if (message.title) {
839
+ return message.title
840
+ }
841
+ if (message.kind === 'tool') {
842
+ return 'Tool'
843
+ }
844
+ if (message.kind === 'reasoning') {
845
+ return 'Reasoning'
846
+ }
847
+ return 'Assistant'
848
+ }