conductor-oss 0.18.2 → 0.18.4

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 (254) hide show
  1. package/README.md +1 -21
  2. package/package.json +5 -5
  3. package/web/.next/standalone/packages/web/.next/BUILD_ID +1 -1
  4. package/web/.next/standalone/packages/web/.next/app-path-routes-manifest.json +1 -3
  5. package/web/.next/standalone/packages/web/.next/build-manifest.json +2 -2
  6. package/web/.next/standalone/packages/web/.next/prerender-manifest.json +3 -3
  7. package/web/.next/standalone/packages/web/.next/routes-manifest.json +6 -22
  8. package/web/.next/standalone/packages/web/.next/server/app/_global-error.html +2 -2
  9. package/web/.next/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
  10. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page/server-reference-manifest.json +7 -7
  16. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/web/.next/standalone/packages/web/.next/server/app/_not-found.html +1 -1
  19. package/web/.next/standalone/packages/web/.next/server/app/_not-found.rsc +4 -4
  20. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +4 -4
  21. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +4 -4
  23. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  26. package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js +1 -1
  27. package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js.nft.json +1 -1
  28. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js +1 -1
  29. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.nft.json +1 -1
  30. package/web/.next/standalone/packages/web/.next/server/app/api/app-update/route.js +1 -1
  31. package/web/.next/standalone/packages/web/.next/server/app/api/app-update/route.js.nft.json +1 -1
  32. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js +1 -1
  33. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js.nft.json +1 -1
  34. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route.js +1 -1
  35. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route.js.nft.json +1 -1
  36. package/web/.next/standalone/packages/web/.next/server/app/api/boards/comments/route.js +1 -1
  37. package/web/.next/standalone/packages/web/.next/server/app/api/boards/comments/route.js.nft.json +1 -1
  38. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js +1 -1
  39. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js.nft.json +1 -1
  40. package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js +1 -1
  41. package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js.nft.json +1 -1
  42. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/open/route.js +1 -1
  43. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/open/route.js.nft.json +1 -1
  44. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js +1 -1
  45. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js.nft.json +1 -1
  46. package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js +1 -1
  47. package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js.nft.json +1 -1
  48. package/web/.next/standalone/packages/web/.next/server/app/api/executor/health/route.js +1 -1
  49. package/web/.next/standalone/packages/web/.next/server/app/api/executor/health/route.js.nft.json +1 -1
  50. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js +1 -1
  51. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js.nft.json +1 -1
  52. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/pick-directory/route.js +1 -1
  53. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/pick-directory/route.js.nft.json +1 -1
  54. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js +1 -1
  55. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js.nft.json +1 -1
  56. package/web/.next/standalone/packages/web/.next/server/app/api/github/webhook/route.js +1 -1
  57. package/web/.next/standalone/packages/web/.next/server/app/api/github/webhook/route.js.nft.json +1 -1
  58. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js +1 -1
  59. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js.nft.json +1 -1
  60. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js +1 -1
  61. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js.nft.json +1 -1
  62. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js +1 -1
  63. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js.nft.json +1 -1
  64. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js +1 -1
  65. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js.nft.json +1 -1
  66. package/web/.next/standalone/packages/web/.next/server/app/api/remote-access/route.js +1 -1
  67. package/web/.next/standalone/packages/web/.next/server/app/api/remote-access/route.js.nft.json +1 -1
  68. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/[id]/route.js +1 -1
  69. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/[id]/route.js.nft.json +1 -1
  70. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js +1 -1
  71. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js.nft.json +1 -1
  72. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js +1 -1
  73. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js.nft.json +1 -1
  74. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/archive/route.js +1 -1
  75. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  76. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js +1 -1
  77. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js.nft.json +1 -1
  78. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js +1 -1
  79. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js.nft.json +1 -1
  80. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/route.js +1 -1
  81. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/route.js.nft.json +1 -1
  82. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js +1 -1
  83. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js.nft.json +1 -1
  84. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js +1 -1
  85. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js.nft.json +1 -1
  86. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/interrupt/route.js +1 -1
  87. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/interrupt/route.js.nft.json +1 -1
  88. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js +1 -1
  89. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
  90. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js +1 -1
  91. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js.nft.json +1 -1
  92. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/preview/dom/route.js +1 -1
  93. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/preview/dom/route.js.nft.json +1 -1
  94. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/preview/route.js +1 -1
  95. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/preview/route.js.nft.json +1 -1
  96. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/preview/screenshot/route.js +1 -1
  97. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/preview/screenshot/route.js.nft.json +1 -1
  98. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js +1 -1
  99. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
  100. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js +1 -1
  101. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  102. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/snapshot/route.js +1 -1
  103. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/snapshot/route.js.nft.json +1 -1
  104. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/token/route/app-paths-manifest.json +3 -0
  105. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/token/route.js +10 -0
  106. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/token/route.js.nft.json +1 -0
  107. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/token/route_client-reference-manifest.js +2 -0
  108. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js +1 -1
  109. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js.nft.json +1 -1
  110. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js +1 -1
  111. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js.nft.json +1 -1
  112. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js +1 -1
  113. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js.nft.json +1 -1
  114. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js +1 -1
  115. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  116. package/web/.next/standalone/packages/web/.next/server/app/page/react-loadable-manifest.json +1 -1
  117. package/web/.next/standalone/packages/web/.next/server/app/page/server-reference-manifest.json +7 -7
  118. package/web/.next/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
  119. package/web/.next/standalone/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  120. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/react-loadable-manifest.json +1 -1
  121. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/server-reference-manifest.json +7 -7
  122. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  123. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  124. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page/server-reference-manifest.json +7 -7
  125. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page.js.nft.json +1 -1
  126. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page_client-reference-manifest.js +1 -1
  127. package/web/.next/standalone/packages/web/.next/server/app/unlock/page/server-reference-manifest.json +7 -7
  128. package/web/.next/standalone/packages/web/.next/server/app/unlock/page.js.nft.json +1 -1
  129. package/web/.next/standalone/packages/web/.next/server/app/unlock/page_client-reference-manifest.js +1 -1
  130. package/web/.next/standalone/packages/web/.next/server/app-paths-manifest.json +1 -3
  131. package/web/.next/standalone/packages/web/.next/server/chunks/26076_server_app_api_sessions_[id]_terminal_token_route_actions_9c4b3c06.js +3 -0
  132. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__63017d21._.js +3 -0
  133. package/web/.next/standalone/packages/web/.next/server/chunks/{[root-of-the-server]__f3d09d5c._.js → [root-of-the-server]__9279c912._.js} +1 -1
  134. package/web/.next/standalone/packages/web/.next/server/chunks/_2c837d66._.js +80 -0
  135. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__379d412d._.js +1 -1
  136. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__da08a50a._.js → [root-of-the-server]__443ba186._.js} +2 -2
  137. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__a565f9a3._.js → [root-of-the-server]__742dad30._.js} +2 -2
  138. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__749fe4b2._.js +1 -1
  139. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__992cdcf8._.js → [root-of-the-server]__a8fa29c1._.js} +2 -2
  140. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_0e1412de._.js +1 -1
  141. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_532f707d._.js +1 -1
  142. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_69e05fca._.js +1 -1
  143. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_80efe193._.js +1 -1
  144. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{_62d206cc._.js → _9bf43d8d._.js} +2 -2
  145. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_c0f0e227._.js +1 -1
  146. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_f36ddaa9._.js +1 -1
  147. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{node_modules_5646ec2d._.js → node_modules_91aa5708._.js} +1 -1
  148. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_3964db17._.js +3 -0
  149. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_5c863a0e._.js +3 -0
  150. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{node_modules_6d2fa1ea._.js → node_modules_be1275d0._.js} +1 -1
  151. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_components_sessions_SessionTerminal_tsx_eaf9458b._.js +1 -1
  152. package/web/.next/standalone/packages/web/.next/server/functions-config-manifest.json +2 -4
  153. package/web/.next/standalone/packages/web/.next/server/pages/404.html +1 -1
  154. package/web/.next/standalone/packages/web/.next/server/pages/500.html +2 -2
  155. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
  156. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.json +8 -8
  157. package/web/.next/standalone/packages/web/.next/static/chunks/{c1e720eabb98af26.js → 2037d1500c64fbef.js} +1 -1
  158. package/web/.next/standalone/packages/web/.next/static/chunks/{58a9b117e5684e7c.js → 3ad6d404d5657604.js} +1 -1
  159. package/web/.next/standalone/packages/web/.next/static/chunks/4d288f280972fd06.js +1 -0
  160. package/web/.next/standalone/packages/web/.next/static/chunks/65bc9229d60adf9f.css +4 -0
  161. package/web/.next/standalone/packages/web/.next/static/chunks/97e7e5343941de65.js +1 -0
  162. package/web/.next/{static/chunks/8d05dc3b261207bb.js → standalone/packages/web/.next/static/chunks/ab8cea4266d5034c.js} +1 -1
  163. package/web/.next/standalone/packages/web/.next/static/chunks/{655db4d21daaca4d.js → b2b84b9e8ccbeafa.js} +1 -1
  164. package/web/.next/standalone/packages/web/.next/static/chunks/b9a43bac36046bf9.js +138 -0
  165. package/web/.next/{static/chunks/9331c73d4edcd945.js → standalone/packages/web/.next/static/chunks/d1cbb83a98e765b5.js} +1 -1
  166. package/web/.next/standalone/packages/web/.next/static/chunks/{301802e8e898dd01.js → f2fea305b6822999.js} +1 -1
  167. package/web/.next/standalone/packages/web/.next/static/chunks/f48f57293e98e0d8.js +1 -0
  168. package/web/.next/standalone/packages/web/.next/static/chunks/fe52c44944adc7f2.js +1 -0
  169. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/terminal/token/route.ts +13 -0
  170. package/web/.next/standalone/packages/web/src/components/sessions/SessionTerminal.tsx +77 -39
  171. package/web/.next/standalone/packages/web/src/components/sessions/sessionTerminalUtils.test.ts +0 -122
  172. package/web/.next/standalone/packages/web/src/components/sessions/sessionTerminalUtils.ts +0 -220
  173. package/web/.next/standalone/packages/web/src/components/sessions/terminal/terminalApi.ts +89 -87
  174. package/web/.next/standalone/packages/web/src/components/sessions/terminal/terminalCache.ts +2 -73
  175. package/web/.next/standalone/packages/web/src/components/sessions/terminal/terminalConstants.ts +0 -8
  176. package/web/.next/standalone/packages/web/src/components/sessions/terminal/terminalTypes.ts +0 -19
  177. package/web/.next/standalone/packages/web/src/components/sessions/terminal/ttydClient.ts +122 -27
  178. package/web/.next/standalone/packages/web/src/components/sessions/terminal/useTtydConnection.ts +9 -12
  179. package/web/.next/standalone/packages/web/src/lib/sessionState.ts +0 -473
  180. package/web/.next/static/chunks/{c1e720eabb98af26.js → 2037d1500c64fbef.js} +1 -1
  181. package/web/.next/static/chunks/{58a9b117e5684e7c.js → 3ad6d404d5657604.js} +1 -1
  182. package/web/.next/static/chunks/4d288f280972fd06.js +1 -0
  183. package/web/.next/static/chunks/65bc9229d60adf9f.css +4 -0
  184. package/web/.next/static/chunks/97e7e5343941de65.js +1 -0
  185. package/web/.next/{standalone/packages/web/.next/static/chunks/8d05dc3b261207bb.js → static/chunks/ab8cea4266d5034c.js} +1 -1
  186. package/web/.next/static/chunks/{655db4d21daaca4d.js → b2b84b9e8ccbeafa.js} +1 -1
  187. package/web/.next/static/chunks/b9a43bac36046bf9.js +138 -0
  188. package/web/.next/{standalone/packages/web/.next/static/chunks/9331c73d4edcd945.js → static/chunks/d1cbb83a98e765b5.js} +1 -1
  189. package/web/.next/static/chunks/{301802e8e898dd01.js → f2fea305b6822999.js} +1 -1
  190. package/web/.next/static/chunks/f48f57293e98e0d8.js +1 -0
  191. package/web/.next/static/chunks/fe52c44944adc7f2.js +1 -0
  192. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/stream/route/app-paths-manifest.json +0 -3
  193. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/stream/route.js +0 -10
  194. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/stream/route.js.nft.json +0 -1
  195. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/stream/route_client-reference-manifest.js +0 -2
  196. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route/app-paths-manifest.json +0 -3
  197. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route/build-manifest.json +0 -11
  198. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route/server-reference-manifest.json +0 -4
  199. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js +0 -10
  200. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js.map +0 -5
  201. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js.nft.json +0 -1
  202. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route_client-reference-manifest.js +0 -2
  203. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/connection/route/app-paths-manifest.json +0 -3
  204. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/connection/route/build-manifest.json +0 -11
  205. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/connection/route/server-reference-manifest.json +0 -4
  206. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/connection/route.js +0 -10
  207. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/connection/route.js.map +0 -5
  208. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/connection/route.js.nft.json +0 -1
  209. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/connection/route_client-reference-manifest.js +0 -2
  210. package/web/.next/standalone/packages/web/.next/server/chunks/26076_server_app_api_sessions_[id]_terminal_connection_route_actions_46c114ee.js +0 -3
  211. package/web/.next/standalone/packages/web/.next/server/chunks/29f24__next-internal_server_app_api_sessions_[id]_feed_stream_route_actions_1262f517.js +0 -3
  212. package/web/.next/standalone/packages/web/.next/server/chunks/43d70_next-internal_server_app_api_sessions_[id]_output_stream_route_actions_9bfa500e.js +0 -3
  213. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1029f927._.js +0 -3
  214. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d74c0f7a._.js +0 -3
  215. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ddad8d14._.js +0 -3
  216. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ede5c8ca._.js +0 -3
  217. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f56e5b36._.js +0 -3
  218. package/web/.next/standalone/packages/web/.next/server/chunks/_24c4e75d._.js +0 -80
  219. package/web/.next/standalone/packages/web/.next/server/chunks/_3d39aff4._.js +0 -80
  220. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_307d7608._.js +0 -3
  221. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_3ed93faf._.js +0 -3
  222. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_4f296b1d._.js +0 -3
  223. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_599a1810._.js +0 -3
  224. package/web/.next/standalone/packages/web/.next/static/chunks/06eb75e40dff98f1.css +0 -4
  225. package/web/.next/standalone/packages/web/.next/static/chunks/1382eff030c401e3.js +0 -1
  226. package/web/.next/standalone/packages/web/.next/static/chunks/1684a3f76eefe776.js +0 -1
  227. package/web/.next/standalone/packages/web/.next/static/chunks/267e541b481c3c75.js +0 -1
  228. package/web/.next/standalone/packages/web/.next/static/chunks/810a3d36795ae9fd.js +0 -138
  229. package/web/.next/standalone/packages/web/.next/static/chunks/a8cd591e904d769e.js +0 -1
  230. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/feed/stream/route.ts +0 -80
  231. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/output/stream/route.ts +0 -80
  232. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/terminal/connection/route.test.ts +0 -343
  233. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/terminal/connection/route.ts +0 -120
  234. package/web/.next/standalone/packages/web/src/components/Dashboard.tsx +0 -3444
  235. package/web/.next/standalone/packages/web/src/components/TerminalView.tsx +0 -770
  236. package/web/.next/standalone/packages/web/src/components/sessions/ChatPanel.tsx +0 -2097
  237. package/web/.next/standalone/packages/web/src/hooks/useSessionFeed.ts +0 -10
  238. package/web/.next/standalone/packages/web/src/hooks/useSessionOutputStream.ts +0 -166
  239. package/web/.next/standalone/packages/web/src/lib/chatFeed.ts +0 -196
  240. package/web/.next/static/chunks/06eb75e40dff98f1.css +0 -4
  241. package/web/.next/static/chunks/1382eff030c401e3.js +0 -1
  242. package/web/.next/static/chunks/1684a3f76eefe776.js +0 -1
  243. package/web/.next/static/chunks/267e541b481c3c75.js +0 -1
  244. package/web/.next/static/chunks/810a3d36795ae9fd.js +0 -138
  245. package/web/.next/static/chunks/a8cd591e904d769e.js +0 -1
  246. /package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/{feed/stream → terminal/token}/route/build-manifest.json +0 -0
  247. /package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/{feed/stream → terminal/token}/route/server-reference-manifest.json +0 -0
  248. /package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/{feed/stream → terminal/token}/route.js.map +0 -0
  249. /package/web/.next/standalone/packages/web/.next/static/{FHjp9qazH2xWUCRt6mqg4 → O7I3Iz18_tPidBRAWeKh8}/_buildManifest.js +0 -0
  250. /package/web/.next/standalone/packages/web/.next/static/{FHjp9qazH2xWUCRt6mqg4 → O7I3Iz18_tPidBRAWeKh8}/_clientMiddlewareManifest.json +0 -0
  251. /package/web/.next/standalone/packages/web/.next/static/{FHjp9qazH2xWUCRt6mqg4 → O7I3Iz18_tPidBRAWeKh8}/_ssgManifest.js +0 -0
  252. /package/web/.next/static/{FHjp9qazH2xWUCRt6mqg4 → O7I3Iz18_tPidBRAWeKh8}/_buildManifest.js +0 -0
  253. /package/web/.next/static/{FHjp9qazH2xWUCRt6mqg4 → O7I3Iz18_tPidBRAWeKh8}/_clientMiddlewareManifest.json +0 -0
  254. /package/web/.next/static/{FHjp9qazH2xWUCRt6mqg4 → O7I3Iz18_tPidBRAWeKh8}/_ssgManifest.js +0 -0
@@ -1,2097 +0,0 @@
1
- "use client";
2
-
3
- import { useRouter } from "next/navigation";
4
- import {
5
- useCallback,
6
- useEffect,
7
- useLayoutEffect,
8
- useMemo,
9
- useRef,
10
- useState,
11
- type ChangeEvent,
12
- type ComponentProps,
13
- type KeyboardEvent,
14
- } from "react";
15
- import ReactMarkdown from "react-markdown";
16
- import remarkGfm from "remark-gfm";
17
- import {
18
- ArrowRightLeft,
19
- BrainCircuit,
20
- ChevronDown,
21
- ChevronRight,
22
- Code2,
23
- FileSearch2,
24
- FileText,
25
- Globe,
26
- ListTodo,
27
- Loader2,
28
- Paperclip,
29
- PencilLine,
30
- Search,
31
- Shield,
32
- TerminalSquare,
33
- UserRound,
34
- Wrench,
35
- type LucideIcon,
36
- } from "lucide-react";
37
- import { AgentTileIcon } from "@/components/AgentTileIcon";
38
- import { useAgents } from "@/hooks/useAgents";
39
- import { useSessionFeed } from "@/hooks/useSessionFeed";
40
- import { normalizeChatText, type NormalizedChatEntry } from "@/lib/chatFeed";
41
- import { normalizeAgentName } from "@/lib/agentUtils";
42
- import {
43
- formatCurrentModelLabel,
44
- getAllStaticModelOptions,
45
- } from "@/lib/sessionModelCatalog";
46
- import type { RuntimeAgentModelCatalog } from "@/lib/runtimeAgentModelsShared";
47
- import { SessionRuntimeStatusBar } from "./SessionRuntimeStatusBar";
48
- import { uploadProjectAttachments } from "./attachmentUploads";
49
- import {
50
- getAgentModelCatalog,
51
- getAvailableAgentModels,
52
- getAvailableAgentReasoningEfforts,
53
- type AgentModelOption,
54
- type AgentReasoningOption,
55
- type ModelAccessPreferences,
56
- } from "@conductor-oss/core/types";
57
-
58
- interface ChatPanelProps {
59
- sessionId: string;
60
- agentName?: string | null;
61
- projectId?: string | null;
62
- sessionModel?: string | null;
63
- sessionReasoningEffort?: string | null;
64
- }
65
-
66
- interface AttachmentDraft {
67
- file: File;
68
- }
69
-
70
- interface ModelOption {
71
- id: string;
72
- label: string;
73
- helper: string;
74
- }
75
-
76
- interface SlashCommandOption {
77
- command: string;
78
- label: string;
79
- description: string;
80
- exact?: boolean;
81
- }
82
-
83
- const COMMON_SLASH_COMMANDS: SlashCommandOption[] = [
84
- {
85
- command: "/help",
86
- label: "/help",
87
- description: "Show the agent help and available commands.",
88
- },
89
- {
90
- command: "/model",
91
- label: "/model",
92
- description: "Inspect or switch the active model.",
93
- },
94
- {
95
- command: "/clear",
96
- label: "/clear",
97
- description: "Clear the current agent conversation context.",
98
- },
99
- {
100
- command: "/review",
101
- label: "/review",
102
- description: "Ask the agent to review current changes.",
103
- },
104
- {
105
- command: "/diff",
106
- label: "/diff",
107
- description: "Inspect the current diff from inside the agent.",
108
- },
109
- ];
110
-
111
- function findRuntimeCatalog(
112
- agents: ReturnType<typeof useAgents>["agents"],
113
- agentName: string,
114
- ): RuntimeAgentModelCatalog | null {
115
- const normalizedAgentName = normalizeAgentName(agentName);
116
- if (!normalizedAgentName) return null;
117
-
118
- return agents.find((agent) => normalizeAgentName(agent.name) === normalizedAgentName)?.runtimeModelCatalog ?? null;
119
- }
120
-
121
-
122
- function getModelOptions(
123
- runtimeCatalog: RuntimeAgentModelCatalog | null,
124
- agentName: string,
125
- currentModel: string,
126
- ): ModelOption[] {
127
- const options = new Map<string, ModelOption>();
128
-
129
- for (const model of getAllStaticModelOptions(agentName)) {
130
- const id = model.id.trim();
131
- if (!id || options.has(id)) continue;
132
- options.set(id, {
133
- id,
134
- label: model.label.trim() || id,
135
- helper: "Built-in catalog",
136
- });
137
- }
138
-
139
- for (const modelList of Object.values(runtimeCatalog?.modelsByAccess ?? {})) {
140
- if (!Array.isArray(modelList)) continue;
141
- for (const model of modelList) {
142
- const id = typeof model.id === "string" ? model.id.trim() : "";
143
- if (!id || options.has(id)) continue;
144
- const label = typeof model.label === "string" && model.label.trim().length > 0 ? model.label.trim() : id;
145
- options.set(id, {
146
- id,
147
- label,
148
- helper: "Detected locally",
149
- });
150
- }
151
- }
152
-
153
- if (currentModel.trim() && !options.has(currentModel.trim())) {
154
- options.set(currentModel.trim(), {
155
- id: currentModel.trim(),
156
- label: formatCurrentModelLabel(agentName, currentModel),
157
- helper: "Current session model",
158
- });
159
- }
160
-
161
- return [...options.values()].sort((left, right) => left.label.localeCompare(right.label));
162
- }
163
-
164
- function getReasoningOptions(
165
- agentName: string,
166
- runtimeCatalog: RuntimeAgentModelCatalog | null,
167
- model: string,
168
- ): AgentReasoningOption[] {
169
- const normalizedModel = model.trim();
170
- const options = new Map<string, AgentReasoningOption>();
171
-
172
- const push = (candidate: AgentReasoningOption | null | undefined) => {
173
- if (!candidate?.id || options.has(candidate.id)) return;
174
- options.set(candidate.id, candidate);
175
- };
176
-
177
- if (normalizedModel) {
178
- for (const option of runtimeCatalog?.reasoningOptionsByModel?.[normalizedModel] ?? []) {
179
- push(option);
180
- }
181
- }
182
-
183
- for (const group of Object.values(runtimeCatalog?.reasoningOptionsByAccess ?? {})) {
184
- if (!Array.isArray(group)) continue;
185
- for (const option of group) {
186
- push(option);
187
- }
188
- }
189
-
190
- const catalog = getAgentModelCatalog(agentName);
191
- if (catalog) {
192
- for (const accessOption of catalog.accessOptions) {
193
- const preferences = { [catalog.accessKey]: accessOption.id } as ModelAccessPreferences;
194
- for (const option of getAvailableAgentReasoningEfforts(agentName, preferences)) {
195
- push(option);
196
- }
197
- }
198
- } else {
199
- for (const option of getAvailableAgentReasoningEfforts(agentName, undefined)) {
200
- push(option);
201
- }
202
- }
203
-
204
- return [...options.values()];
205
- }
206
-
207
- function formatReasoningLabel(value: string): string {
208
- const normalized = value.trim().toLowerCase();
209
- if (!normalized) return "Session default";
210
- if (normalized === "xhigh") return "Extra High";
211
- return normalized
212
- .split(/[_\s-]+/g)
213
- .filter(Boolean)
214
- .map((part) => part[0]?.toUpperCase() + part.slice(1))
215
- .join(" ");
216
- }
217
-
218
- function getDefaultReasoningSelection(
219
- agentName: string,
220
- runtimeCatalog: RuntimeAgentModelCatalog | null,
221
- model: string,
222
- sessionReasoningEffort: string,
223
- ): string {
224
- const options = getReasoningOptions(agentName, runtimeCatalog, model);
225
- const normalizedSession = sessionReasoningEffort.trim().toLowerCase();
226
- if (normalizedSession && options.some((option) => option.id === normalizedSession)) {
227
- return normalizedSession;
228
- }
229
-
230
- const normalizedModel = model.trim();
231
- const runtimeDefault = normalizedModel
232
- ? runtimeCatalog?.defaultReasoningByModel?.[normalizedModel] ?? null
233
- : null;
234
- const normalizedRuntimeDefault = runtimeDefault?.trim().toLowerCase() ?? "";
235
- if (normalizedRuntimeDefault && options.some((option) => option.id === normalizedRuntimeDefault)) {
236
- return normalizedRuntimeDefault;
237
- }
238
-
239
- for (const candidate of Object.values(runtimeCatalog?.defaultReasoningByAccess ?? {})) {
240
- const normalizedCandidate = candidate?.trim().toLowerCase() ?? "";
241
- if (normalizedCandidate && options.some((option) => option.id === normalizedCandidate)) {
242
- return normalizedCandidate;
243
- }
244
- }
245
-
246
- return options[0]?.id ?? "";
247
- }
248
-
249
- function getCustomModelPlaceholder(
250
- agentName: string,
251
- runtimeCatalog: RuntimeAgentModelCatalog | null,
252
- ): string {
253
- const runtimePlaceholder = runtimeCatalog?.customModelPlaceholder?.trim();
254
- if (runtimePlaceholder) return runtimePlaceholder;
255
- const label = getAgentModelCatalog(agentName)?.label ?? "agent";
256
- return `Enter exact ${label} model id`;
257
- }
258
-
259
- function getSlashCommandOptions(agentName: string, message: string): SlashCommandOption[] {
260
- const normalizedMessage = message.trimStart();
261
- if (!normalizedMessage.startsWith("/")) {
262
- return [];
263
- }
264
-
265
- const normalizedAgent = agentName.trim() || "agent";
266
- const query = normalizedMessage.toLowerCase();
267
- const options = new Map<string, SlashCommandOption>();
268
-
269
- options.set(normalizedMessage, {
270
- command: normalizedMessage,
271
- label: normalizedMessage,
272
- description: `Send this raw slash command directly to ${normalizedAgent}.`,
273
- exact: true,
274
- });
275
-
276
- for (const option of COMMON_SLASH_COMMANDS) {
277
- const haystack = `${option.command} ${option.label} ${option.description}`.toLowerCase();
278
- if (!query || haystack.includes(query)) {
279
- options.set(option.command, option);
280
- }
281
- }
282
-
283
- return [...options.values()];
284
- }
285
-
286
- function formatTimestamp(value: string | null): string | null {
287
- if (!value) return null;
288
- const timestamp = Date.parse(value);
289
- if (Number.isNaN(timestamp)) return null;
290
- return new Intl.DateTimeFormat(undefined, {
291
- hour: "numeric",
292
- minute: "2-digit",
293
- month: "short",
294
- day: "numeric",
295
- }).format(new Date(timestamp));
296
- }
297
-
298
- function getStatusPresentation(text: string): {
299
- Icon: LucideIcon;
300
- compact: boolean;
301
- showDot: boolean;
302
- } {
303
- const normalizedText = text.trim();
304
-
305
- if (/^thinking\b/i.test(normalizedText)) {
306
- return { Icon: Code2, compact: true, showDot: false };
307
- }
308
-
309
- if (/^searched\b/i.test(normalizedText)) {
310
- return { Icon: Search, compact: true, showDot: true };
311
- }
312
-
313
- if (
314
- /^(git|pnpm|npm|npx|yarn|cargo|bun|node|ls|cd|cat|rg|find|sed|touch|mkdir|rm|cp|mv|gh|python|uv)\b/i.test(normalizedText)
315
- || normalizedText.includes("&&")
316
- ) {
317
- return { Icon: TerminalSquare, compact: true, showDot: true };
318
- }
319
-
320
- if (/session status:/i.test(normalizedText)) {
321
- return { Icon: Code2, compact: true, showDot: false };
322
- }
323
-
324
- return { Icon: Code2, compact: false, showDot: false };
325
- }
326
-
327
- function isCommandLikeStatus(text: string): boolean {
328
- return /^(git|pnpm|npm|npx|yarn|cargo|bun|node|ls|cd|cat|rg|find|sed|touch|mkdir|rm|cp|mv|gh|python|uv)\b/i.test(text.trim())
329
- || text.includes("&&");
330
- }
331
-
332
- function extractComposerSummary(entries: NormalizedChatEntry[]): string | null {
333
- const candidates = [
334
- ...entries.map((entry) => entry.text),
335
- ].map((value) => value.trim()).filter(Boolean);
336
-
337
- return candidates.find((value) => /files changed/i.test(value) || (/\+\d+/.test(value) && /-\d+/.test(value))) ?? null;
338
- }
339
-
340
- function parseComposerSummary(summary: string): {
341
- label: string;
342
- additions: string | null;
343
- deletions: string | null;
344
- } {
345
- const match = summary.match(/^(.*?files changed)(?:\s+(\+\d+))?(?:\s+(-\d+))?$/i);
346
- if (!match) {
347
- return {
348
- label: summary,
349
- additions: null,
350
- deletions: null,
351
- };
352
- }
353
-
354
- return {
355
- label: match[1]?.trim() || summary,
356
- additions: match[2] ?? null,
357
- deletions: match[3] ?? null,
358
- };
359
- }
360
-
361
- const markdownComponents = {
362
- a: (props: ComponentProps<"a">) => (
363
- <a
364
- {...props}
365
- target="_blank"
366
- rel="noreferrer"
367
- className="text-[#ea7a2a] underline underline-offset-2"
368
- />
369
- ),
370
- p: (props: ComponentProps<"p">) => (
371
- <p
372
- {...props}
373
- className={[props.className, "whitespace-pre-wrap"].filter(Boolean).join(" ")}
374
- />
375
- ),
376
- li: (props: ComponentProps<"li">) => (
377
- <li
378
- {...props}
379
- className={[props.className, "whitespace-pre-wrap"].filter(Boolean).join(" ")}
380
- />
381
- ),
382
- pre: (props: ComponentProps<"pre">) => (
383
- <pre
384
- {...props}
385
- className={[
386
- props.className,
387
- "max-w-full overflow-x-hidden whitespace-pre-wrap break-words rounded-[6px] border border-[#333] bg-[#1c1c1c] px-3 py-3 text-[12px] leading-[18px] [overflow-wrap:anywhere] sm:text-[13px] sm:leading-[20px]",
388
- ].filter(Boolean).join(" ")}
389
- />
390
- ),
391
- code: (props: ComponentProps<"code"> & { inline?: boolean }) => (
392
- <code
393
- {...props}
394
- className={[
395
- props.className,
396
- "font-mono whitespace-pre-wrap break-all [overflow-wrap:anywhere]",
397
- props.inline ? "rounded bg-[rgba(0,0,0,0.22)] px-1 py-[1px] text-[#f1f1f1]" : "",
398
- ].filter(Boolean).join(" ")}
399
- />
400
- ),
401
- };
402
-
403
- function SetupScriptHint() {
404
- return (
405
- <div className="flex items-start gap-3 rounded-[3px] px-2 py-1 text-[#8f8f8f]">
406
- <div className="flex h-[21px] w-[21px] items-center justify-center pt-[1px] text-[#8f8f8f]">
407
- <TerminalSquare className="h-[15px] w-[15px]" strokeWidth={1.6} />
408
- </div>
409
- <div className="min-w-0 space-y-[2px]">
410
- <p className="text-[14px] leading-[21px] text-[#8f8f8f]">Setup Script</p>
411
- <p className="text-[12px] leading-[18px] text-[#c4c4c4]">
412
- No setup script configured. Setup scripts run before the coding agent starts.
413
- </p>
414
- </div>
415
- </div>
416
- );
417
- }
418
-
419
- function MarkdownBlock({ text, className }: { text: string; className: string }) {
420
- return (
421
- <ReactMarkdown
422
- className={className}
423
- remarkPlugins={[remarkGfm]}
424
- components={markdownComponents}
425
- >
426
- {text}
427
- </ReactMarkdown>
428
- );
429
- }
430
-
431
- type ParsedReviewFinding = {
432
- id: string;
433
- severity: "critical" | "high" | "medium" | "low" | "neutral";
434
- title: string;
435
- body: string;
436
- };
437
-
438
- type ParsedReviewContent = {
439
- intro: string | null;
440
- findingsHeading: string;
441
- findings: ParsedReviewFinding[];
442
- closing: string | null;
443
- };
444
-
445
- type ParsedOutlineSection = {
446
- id: string;
447
- title: string;
448
- items: string[];
449
- };
450
-
451
- function parseReviewContent(text: string): ParsedReviewContent | null {
452
- const normalized = text.replace(/\r/g, "");
453
- const lines = normalized.split("\n");
454
- const findingsIndex = lines.findIndex((line) => /^(?:#{1,6}\s*)?findings\b/i.test(line.trim()));
455
- if (findingsIndex < 0) {
456
- return null;
457
- }
458
-
459
- const intro = lines.slice(0, findingsIndex).join("\n").trim() || null;
460
- const findingsHeading = lines[findingsIndex]?.replace(/^#{1,6}\s*/, "").trim() || "Findings";
461
- const remaining = lines.slice(findingsIndex + 1);
462
-
463
- const trailingHeadingIndex = remaining.findIndex((line) =>
464
- /^(?:#{1,6}\s*)?(assumptions|open questions|assumptions\s*\/\s*open questions|next steps)\b/i.test(line.trim())
465
- );
466
-
467
- const findingsLines = trailingHeadingIndex >= 0 ? remaining.slice(0, trailingHeadingIndex) : remaining;
468
- const closing = trailingHeadingIndex >= 0 ? remaining.slice(trailingHeadingIndex).join("\n").trim() || null : null;
469
-
470
- const chunks: string[][] = [];
471
- let current: string[] = [];
472
-
473
- for (const line of findingsLines) {
474
- if (/^\s*\d+[.)]\s+/.test(line)) {
475
- if (current.length > 0) {
476
- chunks.push(current);
477
- }
478
- current = [line];
479
- } else if (current.length > 0) {
480
- current.push(line);
481
- }
482
- }
483
-
484
- if (current.length > 0) {
485
- chunks.push(current);
486
- }
487
-
488
- const findings = chunks
489
- .map((chunk, index) => {
490
- const [firstLine, ...rest] = chunk;
491
- const cleanedFirst = firstLine.replace(/^\s*\d+[.)]\s+/, "").replace(/\*\*/g, "").trim();
492
- const severityMatch = cleanedFirst.match(/\b(critical|high|medium|low)\b/i);
493
- const severity = (severityMatch?.[1]?.toLowerCase() ?? "neutral") as ParsedReviewFinding["severity"];
494
- const title = cleanedFirst
495
- .replace(/\b(critical|high|medium|low)\b\s*[—:-]\s*/i, "")
496
- .replace(/\b(critical|high|medium|low)\b/i, "")
497
- .trim();
498
- const body = rest.join("\n").trim();
499
-
500
- return {
501
- id: `finding-${index}`,
502
- severity,
503
- title: title || cleanedFirst,
504
- body,
505
- };
506
- })
507
- .filter((finding) => finding.title.length > 0);
508
-
509
- if (findings.length === 0) {
510
- return null;
511
- }
512
-
513
- return {
514
- intro,
515
- findingsHeading,
516
- findings,
517
- closing,
518
- };
519
- }
520
-
521
- function isLikelyOutlineTitle(line: string): boolean {
522
- const trimmed = line.trim();
523
- if (!trimmed) return false;
524
- if (/^[-*+]\s|^\d+[.)]\s|^#{1,6}\s|^>\s|^```/.test(trimmed)) return false;
525
- if (trimmed.length > 72) return false;
526
- if (/[.!?;]$/.test(trimmed)) return false;
527
- const wordCount = trimmed.split(/\s+/).length;
528
- return wordCount <= 8;
529
- }
530
-
531
- function parseOutlineContent(text: string): ParsedOutlineSection[] | null {
532
- const normalized = text.replace(/\r/g, "");
533
- const lines = normalized.split("\n");
534
- if (lines.some((line) => /^\s*[-*+]\s|^\s*\d+[.)]\s|^\s*#{1,6}\s/.test(line))) {
535
- return null;
536
- }
537
-
538
- const titleIndices = lines
539
- .map((line, index) => ({ line, index }))
540
- .filter(({ line, index }) => {
541
- const next = lines[index + 1]?.trim() ?? "";
542
- return isLikelyOutlineTitle(line) && next.length > 0 && !isLikelyOutlineTitle(next);
543
- })
544
- .map(({ index }) => index);
545
-
546
- if (titleIndices.length < 2) {
547
- return null;
548
- }
549
-
550
- const sections: ParsedOutlineSection[] = [];
551
- for (let i = 0; i < titleIndices.length; i += 1) {
552
- const start = titleIndices[i];
553
- const end = titleIndices[i + 1] ?? lines.length;
554
- const title = lines[start]!.trim().replace(/:$/, "");
555
- const items = lines
556
- .slice(start + 1, end)
557
- .map((line) => line.trim())
558
- .filter((line) => line.length > 0);
559
-
560
- if (title.length === 0 || items.length === 0) {
561
- continue;
562
- }
563
-
564
- sections.push({
565
- id: `outline-${i}`,
566
- title,
567
- items,
568
- });
569
- }
570
-
571
- return sections.length >= 2 ? sections : null;
572
- }
573
-
574
- function getFindingTone(finding: ParsedReviewFinding): { badge: string; border: string; surface: string } {
575
- switch (finding.severity) {
576
- case "critical":
577
- case "high":
578
- return {
579
- badge: "bg-[rgba(210,81,81,0.14)] text-[#f1b0b0]",
580
- border: "border-[rgba(210,81,81,0.22)]",
581
- surface: "bg-[rgba(210,81,81,0.05)]",
582
- };
583
- case "medium":
584
- return {
585
- badge: "bg-[rgba(214,164,60,0.14)] text-[#f0d28b]",
586
- border: "border-[rgba(214,164,60,0.2)]",
587
- surface: "bg-[rgba(214,164,60,0.05)]",
588
- };
589
- case "low":
590
- return {
591
- badge: "bg-[rgba(84,176,79,0.14)] text-[#b8dfb5]",
592
- border: "border-[rgba(84,176,79,0.2)]",
593
- surface: "bg-[rgba(84,176,79,0.05)]",
594
- };
595
- default:
596
- return {
597
- badge: "bg-[rgba(143,143,143,0.14)] text-[#c4c4c4]",
598
- border: "border-[var(--vk-border)]",
599
- surface: "bg-[rgba(255,255,255,0.03)]",
600
- };
601
- }
602
- }
603
-
604
- function ReviewFindingsBlock({ text }: { text: string }) {
605
- const parsed = parseReviewContent(text);
606
- if (!parsed) {
607
- return null;
608
- }
609
-
610
- return (
611
- <div className="space-y-5">
612
- {parsed.intro ? (
613
- <MarkdownBlock
614
- text={parsed.intro}
615
- className="prose prose-invert max-w-none text-[16px] leading-[28px] text-[#cfcfcf] prose-headings:mb-3 prose-headings:mt-6 prose-headings:text-[#f1f1f1] prose-p:my-4 prose-p:text-[#cfcfcf] prose-strong:text-[#f1f1f1] prose-ol:my-4 prose-ol:pl-6 prose-ul:my-4 prose-ul:pl-6 prose-li:my-2 prose-li:text-[#cfcfcf] prose-pre:my-4 prose-pre:overflow-x-auto prose-pre:rounded-[6px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c] prose-code:text-[#d7d7d7]"
616
- />
617
- ) : null}
618
-
619
- <div className="overflow-hidden rounded-[8px] border border-[var(--vk-border)] bg-[rgba(255,255,255,0.03)]">
620
- <div className="border-b border-[var(--vk-border)] px-4 py-3">
621
- <p className="text-[12px] uppercase tracking-[0.16em] text-[#8f8f8f]">{parsed.findingsHeading}</p>
622
- </div>
623
- <div className="space-y-4 p-4">
624
- {parsed.findings.map((finding, index) => {
625
- const tone = getFindingTone(finding);
626
- return (
627
- <div
628
- key={finding.id}
629
- className={`rounded-[6px] border px-4 py-3 ${tone.border} ${tone.surface}`}
630
- >
631
- <div className="flex flex-wrap items-center gap-3">
632
- <span className="text-[13px] font-medium leading-[20px] text-[#8f8f8f]">
633
- {index + 1}.
634
- </span>
635
- <span className={`rounded-full px-2 py-[3px] text-[11px] font-medium uppercase tracking-[0.12em] ${tone.badge}`}>
636
- {finding.severity}
637
- </span>
638
- <p className="min-w-0 flex-1 text-[16px] leading-[24px] text-[#f1f1f1]">
639
- {finding.title}
640
- </p>
641
- </div>
642
- {finding.body ? (
643
- <MarkdownBlock
644
- text={finding.body}
645
- className="prose prose-invert mt-3 max-w-none text-[14px] leading-[24px] text-[#c9c9c9] prose-p:my-3 prose-p:text-[#c9c9c9] prose-strong:text-[#f1f1f1] prose-ul:my-3 prose-ul:pl-5 prose-ol:my-3 prose-ol:pl-5 prose-li:my-1.5 prose-li:text-[#c9c9c9] prose-code:text-[#d7d7d7] prose-pre:my-3 prose-pre:overflow-x-auto prose-pre:rounded-[6px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c]"
646
- />
647
- ) : null}
648
- </div>
649
- );
650
- })}
651
- </div>
652
- </div>
653
-
654
- {parsed.closing ? (
655
- <div className="rounded-[8px] border border-[var(--vk-border)] bg-[rgba(255,255,255,0.03)] px-4 py-4">
656
- <MarkdownBlock
657
- text={parsed.closing}
658
- className="prose prose-invert max-w-none text-[15px] leading-[25px] text-[#c9c9c9] prose-headings:mb-2 prose-headings:mt-5 prose-headings:text-[#f1f1f1] prose-p:my-3 prose-p:text-[#c9c9c9] prose-strong:text-[#f1f1f1] prose-ol:my-3 prose-ol:pl-5 prose-ul:my-3 prose-ul:pl-5 prose-li:my-1.5 prose-li:text-[#c9c9c9] prose-code:text-[#d7d7d7]"
659
- />
660
- </div>
661
- ) : null}
662
- </div>
663
- );
664
- }
665
-
666
- function OutlineSummaryBlock({ sections }: { sections: ParsedOutlineSection[] }) {
667
- return (
668
- <div className="space-y-6">
669
- {sections.map((section) => (
670
- <section key={section.id} className="space-y-3">
671
- <h3 className="text-[22px] font-medium leading-[30px] text-[#f1f1f1]">
672
- {section.title}
673
- </h3>
674
- <div className="space-y-3">
675
- {section.items.map((item, index) => (
676
- <div
677
- key={`${section.id}-item-${index}`}
678
- className="rounded-[6px] border border-[rgba(255,255,255,0.05)] bg-[rgba(255,255,255,0.02)] px-4 py-3"
679
- >
680
- <MarkdownBlock
681
- text={item}
682
- className="prose prose-invert max-w-none text-[16px] leading-[28px] text-[#cfcfcf] prose-p:my-0 prose-p:text-[#cfcfcf] prose-strong:text-[#f1f1f1] prose-code:text-[#d7d7d7]"
683
- />
684
- </div>
685
- ))}
686
- </div>
687
- </section>
688
- ))}
689
- </div>
690
- );
691
- }
692
-
693
- function extractToolContent(entry: NormalizedChatEntry): string[] {
694
- const raw = entry.metadata?.toolContent;
695
- if (Array.isArray(raw)) {
696
- return raw
697
- .map((value) => typeof value === "string" ? normalizeChatText(value) : "")
698
- .filter((value): value is string => value.trim().length > 0);
699
- }
700
- const normalized = normalizeChatText(entry.text);
701
- return normalized.trim().length > 0 ? [normalized.trim()] : [];
702
- }
703
-
704
- function getToolInlineSummary(entry: NormalizedChatEntry, content: string[]): string | null {
705
- const title = typeof entry.metadata?.toolTitle === "string" ? normalizeChatText(entry.metadata.toolTitle).trim() : "";
706
- const first = content[0]?.trim() ?? "";
707
- if (!first) return null;
708
- if (title && first.toLowerCase() === title.toLowerCase()) {
709
- return null;
710
- }
711
- return first.length > 84 ? `${first.slice(0, 81)}...` : first;
712
- }
713
-
714
- function getToolStatusTone(status: string | null | undefined): "pending" | "running" | "success" | "error" | "cancelled" {
715
- const lower = status?.trim().toLowerCase() ?? "";
716
- if (lower.includes("complete") || lower.includes("success") || lower.includes("done")) {
717
- return "success";
718
- }
719
- if (lower.includes("error") || lower.includes("fail")) {
720
- return "error";
721
- }
722
- if (lower.includes("cancel")) {
723
- return "cancelled";
724
- }
725
- if (lower.includes("running") || lower.includes("progress")) {
726
- return "running";
727
- }
728
- return "pending";
729
- }
730
-
731
- function getToolIcon(entry: NormalizedChatEntry): LucideIcon {
732
- const toolKind = typeof entry.metadata?.toolKind === "string" ? entry.metadata.toolKind.toLowerCase() : "";
733
- const title = (typeof entry.metadata?.toolTitle === "string" ? entry.metadata.toolTitle : entry.text).toLowerCase();
734
- if (toolKind.includes("thinking") || title.includes("thinking")) {
735
- return BrainCircuit;
736
- }
737
- if (toolKind.includes("web") || title.includes("web search") || title.includes("web fetch")) {
738
- return Globe;
739
- }
740
- if (
741
- toolKind.includes("grep")
742
- || toolKind.includes("glob")
743
- || toolKind.includes("search")
744
- || toolKind.includes("find")
745
- || title.includes("search")
746
- || title.includes("grep")
747
- || title.includes("glob")
748
- || title.includes("find")
749
- ) {
750
- return FileSearch2;
751
- }
752
- if (toolKind.includes("read") || title.includes("read")) {
753
- return FileText;
754
- }
755
- if (
756
- toolKind.includes("edit")
757
- || toolKind.includes("write")
758
- || toolKind.includes("multiedit")
759
- || title.includes("edit")
760
- || title.includes("write")
761
- ) {
762
- return PencilLine;
763
- }
764
- if (toolKind.includes("task") || toolKind.includes("todo") || title.includes("todo")) {
765
- return ListTodo;
766
- }
767
- if (toolKind.includes("search") || title.includes("search") || title.includes("rg ") || title.includes("find ")) {
768
- return Search;
769
- }
770
- if (toolKind.includes("permission") || title.includes("permission") || title.includes("auth")) {
771
- return Shield;
772
- }
773
- if (toolKind.includes("command") || title.includes("bash") || title.includes("git ") || title.includes("bun ")) {
774
- return TerminalSquare;
775
- }
776
- return Wrench;
777
- }
778
-
779
- function AttachmentPills({
780
- attachments,
781
- onRemove,
782
- }: {
783
- attachments: string[];
784
- onRemove?: (index: number) => void;
785
- }) {
786
- if (!attachments.length) return null;
787
-
788
- return (
789
- <div className="mt-3 flex flex-wrap gap-2">
790
- {attachments.map((attachment, index) => {
791
- const label = attachment.split("/").pop() || attachment;
792
-
793
- if (onRemove) {
794
- return (
795
- <button
796
- key={`${attachment}-${index}`}
797
- type="button"
798
- onClick={() => onRemove(index)}
799
- className="rounded-[3px] border border-[#333] bg-[#1c1c1c] px-2 py-1 text-[12px] leading-[18px] text-[#c4c4c4] transition hover:bg-[#292929]"
800
- >
801
- {label}
802
- </button>
803
- );
804
- }
805
-
806
- return (
807
- <span
808
- key={`${attachment}-${index}`}
809
- className="rounded-[3px] border border-[#333] bg-[#1c1c1c] px-2 py-1 text-[12px] leading-[18px] text-[#c4c4c4]"
810
- >
811
- {label}
812
- </span>
813
- );
814
- })}
815
- </div>
816
- );
817
- }
818
-
819
- function SummaryChip({ summary }: { summary: string }) {
820
- const parsed = parseComposerSummary(summary);
821
-
822
- return (
823
- <div className="inline-flex min-h-[29px] items-center gap-1 rounded-[3px] bg-[#292929] px-3 py-[5px] text-[14px] leading-[21px] text-[#c4c4c4]">
824
- <span>{parsed.label}</span>
825
- {parsed.additions ? <span className="text-[#54b04f]">{parsed.additions}</span> : null}
826
- {parsed.deletions ? <span className="text-[#d25151]">{parsed.deletions}</span> : null}
827
- </div>
828
- );
829
- }
830
-
831
- function ParserStateBanner({
832
- kind,
833
- message,
834
- command,
835
- }: {
836
- kind: string;
837
- message: string;
838
- command: string | null;
839
- }) {
840
- const title = kind === "auth_required"
841
- ? "Authentication required"
842
- : kind === "interactive_required"
843
- ? "Terminal interaction required"
844
- : "Waiting for input";
845
- const border = kind === "auth_required" ? "border-[#6b5533] bg-[rgba(234,122,42,0.12)] text-[#f1c49f]" : "border-[#36506b] bg-[rgba(68,114,164,0.12)] text-[#bfd5ee]";
846
-
847
- return (
848
- <div className={`rounded-[3px] border px-3 py-2 ${border}`}>
849
- <div className="flex items-start gap-3">
850
- <div className="flex h-[20px] w-[20px] items-center justify-center pt-[1px]">
851
- <TerminalSquare className="h-[15px] w-[15px]" strokeWidth={1.6} />
852
- </div>
853
- <div className="min-w-0 space-y-1">
854
- <p className="text-[13px] font-medium leading-[20px]">{title}</p>
855
- <p className="whitespace-pre-wrap break-words text-[13px] leading-[20px]">{message}</p>
856
- {command ? (
857
- <p className="text-[12px] leading-[18px] text-[#c4c4c4]">
858
- Run locally: <code className="rounded bg-[rgba(0,0,0,0.22)] px-1 py-[1px] whitespace-pre-wrap break-all text-[#f1f1f1] [overflow-wrap:anywhere]">{command}</code>
859
- </p>
860
- ) : null}
861
- </div>
862
- </div>
863
- </div>
864
- );
865
- }
866
-
867
- function UserEntryCard({ entry }: { entry: NormalizedChatEntry }) {
868
- const timestamp = formatTimestamp(entry.createdAt);
869
-
870
- return (
871
- <div className="space-y-3">
872
- <div className="flex items-center gap-2">
873
- <UserRound className="h-[15px] w-[15px] shrink-0 text-[#8f8f8f]" strokeWidth={1.7} />
874
- <span className="text-[14px] leading-[21px] text-[#c4c4c4]">You</span>
875
- {timestamp ? (
876
- <span className="text-[12px] leading-[18px] text-[#5f5f5f]">{timestamp}</span>
877
- ) : null}
878
- </div>
879
- <MarkdownBlock
880
- text={entry.text}
881
- className="prose prose-invert max-w-none text-[16px] leading-[24px] text-[#c4c4c4] prose-headings:text-[#c4c4c4] prose-p:my-0 prose-p:text-[#c4c4c4] prose-pre:my-3 prose-pre:overflow-x-auto prose-pre:rounded-[3px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c] prose-code:text-[#c4c4c4]"
882
- />
883
- <AttachmentPills attachments={entry.attachments} />
884
- </div>
885
- );
886
- }
887
-
888
- function StatusEntry({ entry }: { entry: NormalizedChatEntry }) {
889
- const presentation = getStatusPresentation(entry.text);
890
- const commandLike = isCommandLikeStatus(entry.text);
891
-
892
- if (!presentation.compact) {
893
- return (
894
- <div className="space-y-3">
895
- <MarkdownBlock
896
- text={entry.text}
897
- className="prose prose-invert max-w-none text-[16px] leading-[24px] text-[#c4c4c4] prose-headings:text-[#c4c4c4] prose-p:my-0 prose-p:text-[#c4c4c4] prose-pre:my-3 prose-pre:overflow-x-auto prose-pre:rounded-[3px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c] prose-code:text-[#c4c4c4]"
898
- />
899
- </div>
900
- );
901
- }
902
-
903
- const { Icon } = presentation;
904
-
905
- return (
906
- <div className="flex items-start gap-2 py-[2px]">
907
- <div className="relative flex h-[22px] w-[20px] items-start pt-[2px] text-[#8f8f8f]">
908
- <Icon className="h-[20px] w-[20px]" strokeWidth={1.6} />
909
- {presentation.showDot ? (
910
- <span className="absolute bottom-0 left-[-2px] h-[6px] w-[6px] rounded-full bg-[#54b04f]" />
911
- ) : null}
912
- </div>
913
- <p className={`min-w-0 flex-1 whitespace-pre-wrap break-words text-[#8f8f8f] ${commandLike ? "font-mono text-[13px] leading-[20px]" : "text-[14px] leading-[21px]"}`}>
914
- {entry.text}
915
- </p>
916
- </div>
917
- );
918
- }
919
-
920
- function ToolEntry({ entry }: { entry: NormalizedChatEntry }) {
921
- const [expanded, setExpanded] = useState(false);
922
- const content = extractToolContent(entry);
923
- const inlineSummary = getToolInlineSummary(entry, content);
924
- const toolTitle = typeof entry.metadata?.toolTitle === "string" && entry.metadata.toolTitle.trim().length > 0
925
- ? normalizeChatText(entry.metadata.toolTitle).trim()
926
- : normalizeChatText(entry.text).trim() || "Tool call";
927
- const toolStatus = typeof entry.metadata?.toolStatus === "string" ? entry.metadata.toolStatus : null;
928
- const statusTone = getToolStatusTone(toolStatus);
929
- const Icon = getToolIcon(entry);
930
- const statusIndicator = statusTone === "running"
931
- ? (
932
- <span
933
- className="h-[9px] w-[9px] shrink-0 rounded-full border-2 border-[rgba(148,163,184,0.35)] border-t-[rgba(148,163,184,0.95)] animate-spin"
934
- aria-hidden="true"
935
- />
936
- )
937
- : (
938
- <span
939
- className={[
940
- "h-[9px] w-[9px] shrink-0 rounded-full",
941
- statusTone === "success"
942
- ? "bg-[var(--vk-green)]"
943
- : statusTone === "error"
944
- ? "bg-[var(--vk-red)]"
945
- : statusTone === "cancelled"
946
- ? "bg-[var(--vk-text-muted)]"
947
- : "bg-[rgba(148,163,184,0.7)]",
948
- ].join(" ")}
949
- aria-hidden="true"
950
- />
951
- );
952
-
953
- return (
954
- <div className={`group ${expanded ? "is-expanded" : ""}`}>
955
- <button
956
- type="button"
957
- onClick={() => setExpanded((current) => !current)}
958
- className="flex w-full items-start gap-2 py-1 text-left text-[var(--vk-text-muted)] transition hover:text-[var(--vk-text-normal)]"
959
- >
960
- {statusIndicator}
961
- <Icon className="mt-[2px] h-[14px] w-[14px] shrink-0 text-[var(--vk-text-muted)]" strokeWidth={1.7} />
962
- <div className="min-w-0 flex-1">
963
- <div className="flex min-w-0 flex-col items-start gap-1 sm:flex-row sm:flex-wrap sm:items-center sm:gap-2">
964
- <span className="min-w-0 max-w-full break-words text-[13px] font-medium leading-[20px] text-[#d0d0d0] sm:text-[14px] sm:leading-[21px]">
965
- {toolTitle}
966
- </span>
967
- {inlineSummary ? (
968
- <span className="min-w-0 w-full whitespace-pre-wrap break-all rounded-[3px] bg-[rgba(255,255,255,0.06)] px-2 py-[2px] font-mono text-[11px] leading-[17px] text-[#a9a9a9] [overflow-wrap:anywhere] sm:w-auto sm:max-w-full sm:text-[12px] sm:leading-[18px]">
969
- {inlineSummary}
970
- </span>
971
- ) : null}
972
- </div>
973
- </div>
974
- <ChevronRight
975
- className={`mt-[2px] h-[14px] w-[14px] shrink-0 text-[var(--vk-text-muted)] transition-all duration-200 ${
976
- expanded ? "rotate-90 opacity-100" : "opacity-0 group-hover:opacity-100"
977
- }`}
978
- strokeWidth={1.8}
979
- />
980
- </button>
981
- {expanded ? (
982
- <div className="mt-2 max-h-[300px] overflow-y-auto border-t border-[var(--vk-border)] bg-[var(--vk-bg-surface)] px-3 py-3">
983
- {content.length > 0 ? (
984
- <div className="space-y-2">
985
- {content
986
- .filter((line, index) => !(inlineSummary && index === 0 && line.trim() === inlineSummary))
987
- .map((line, index) => (
988
- <p
989
- key={`${entry.id}-tool-line-${index}`}
990
- className="whitespace-pre-wrap break-words font-mono text-[12px] leading-[18px] text-[var(--vk-text-normal)]"
991
- >
992
- {line}
993
- </p>
994
- ))}
995
- </div>
996
- ) : null}
997
- </div>
998
- ) : null}
999
- </div>
1000
- );
1001
- }
1002
-
1003
- type FeedRenderItem =
1004
- | { kind: "entry"; entry: NormalizedChatEntry }
1005
- | { kind: "tool-group"; id: string; entries: NormalizedChatEntry[] };
1006
-
1007
- function buildFeedRenderItems(entries: NormalizedChatEntry[]): FeedRenderItem[] {
1008
- const items: FeedRenderItem[] = [];
1009
- let toolBuffer: NormalizedChatEntry[] = [];
1010
-
1011
- const flushTools = () => {
1012
- if (toolBuffer.length === 0) return;
1013
- items.push({
1014
- kind: "tool-group",
1015
- id: `tool-group-${toolBuffer[0]?.id ?? items.length}`,
1016
- entries: toolBuffer,
1017
- });
1018
- toolBuffer = [];
1019
- };
1020
-
1021
- for (const entry of entries) {
1022
- if (entry.kind === "tool") {
1023
- toolBuffer.push(entry);
1024
- continue;
1025
- }
1026
-
1027
- flushTools();
1028
- items.push({ kind: "entry", entry });
1029
- }
1030
-
1031
- flushTools();
1032
- return items;
1033
- }
1034
-
1035
- function ToolGroup({
1036
- entries,
1037
- autoCollapsed,
1038
- }: {
1039
- entries: NormalizedChatEntry[];
1040
- autoCollapsed: boolean;
1041
- }) {
1042
- const [expanded, setExpanded] = useState(!autoCollapsed);
1043
- const countLabel = `${entries.length} tool call${entries.length === 1 ? "" : "s"}`;
1044
-
1045
- useEffect(() => {
1046
- if (autoCollapsed) {
1047
- setExpanded(false);
1048
- }
1049
- }, [autoCollapsed]);
1050
-
1051
- return (
1052
- <div className="space-y-3">
1053
- <button
1054
- type="button"
1055
- onClick={() => setExpanded((current) => !current)}
1056
- className="flex items-center gap-2 text-[15px] leading-[22px] text-[#9b9b9b] transition hover:text-[#c4c4c4]"
1057
- >
1058
- <ChevronDown
1059
- className={`h-[14px] w-[14px] transition-transform ${expanded ? "" : "-rotate-90"}`}
1060
- strokeWidth={1.8}
1061
- />
1062
- <span>{countLabel}</span>
1063
- </button>
1064
- {expanded ? (
1065
- <div className="space-y-1 pl-3 sm:pl-5">
1066
- {entries.map((entry) => (
1067
- <ToolEntry key={entry.id} entry={entry} />
1068
- ))}
1069
- </div>
1070
- ) : null}
1071
- </div>
1072
- );
1073
- }
1074
-
1075
- function AssistantEntry({
1076
- entry,
1077
- agentName,
1078
- }: {
1079
- entry: NormalizedChatEntry;
1080
- agentName: string;
1081
- }) {
1082
- const timestamp = formatTimestamp(entry.createdAt);
1083
- const reviewBlock = parseReviewContent(entry.text);
1084
- const outlineSections = reviewBlock ? null : parseOutlineContent(entry.text);
1085
-
1086
- return (
1087
- <div className="space-y-4">
1088
- {entry.streaming ? (
1089
- <div className="flex items-center gap-2 text-[#8f8f8f]">
1090
- {agentName ? (
1091
- <AgentTileIcon seed={{ label: agentName }} className="h-[20px] w-[20px] border-none bg-transparent" />
1092
- ) : (
1093
- <Code2 className="h-[20px] w-[20px]" strokeWidth={1.6} />
1094
- )}
1095
- <span className="text-[14px] leading-[21px]">Thinking</span>
1096
- </div>
1097
- ) : null}
1098
- {reviewBlock ? (
1099
- <ReviewFindingsBlock text={entry.text} />
1100
- ) : outlineSections ? (
1101
- <OutlineSummaryBlock sections={outlineSections} />
1102
- ) : (
1103
- <MarkdownBlock
1104
- text={entry.text}
1105
- className="prose prose-invert max-w-none text-[16px] leading-[28px] text-[#cfcfcf] prose-headings:mb-3 prose-headings:mt-6 prose-headings:text-[#f1f1f1] prose-p:my-4 prose-p:text-[#cfcfcf] prose-strong:text-[#f1f1f1] prose-ol:my-4 prose-ol:pl-6 prose-ul:my-4 prose-ul:pl-6 prose-li:my-2 prose-li:text-[#cfcfcf] prose-pre:my-4 prose-pre:overflow-x-auto prose-pre:rounded-[6px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c] prose-code:text-[#d7d7d7] prose-blockquote:border-l prose-blockquote:border-[#333] prose-blockquote:pl-4 prose-blockquote:text-[#b7b7b7]"
1106
- />
1107
- )}
1108
- <AttachmentPills attachments={entry.attachments} />
1109
- {timestamp ? (
1110
- <p className="text-[12px] leading-[18px] text-[#5f5f5f]">{timestamp}</p>
1111
- ) : null}
1112
- </div>
1113
- );
1114
- }
1115
-
1116
- function asOptionalMetadataText(value: unknown): string | null {
1117
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1118
- }
1119
-
1120
- function extractSessionPreferenceUpdate(entry: NormalizedChatEntry) {
1121
- const eventType = asOptionalMetadataText(entry.metadata?.eventType);
1122
- if (eventType !== "session_preferences_updated" && entry.source !== "session_preferences") {
1123
- return null;
1124
- }
1125
-
1126
- const previousModel = asOptionalMetadataText(entry.metadata?.previousModel);
1127
- const nextModel = asOptionalMetadataText(entry.metadata?.model);
1128
- const previousReasoningEffort = asOptionalMetadataText(entry.metadata?.previousReasoningEffort);
1129
- const nextReasoningEffort = asOptionalMetadataText(entry.metadata?.reasoningEffort);
1130
- const modelChanged = typeof entry.metadata?.modelChanged === "boolean"
1131
- ? entry.metadata.modelChanged
1132
- : Boolean(nextModel && nextModel !== previousModel);
1133
- const reasoningChanged = typeof entry.metadata?.reasoningChanged === "boolean"
1134
- ? entry.metadata.reasoningChanged
1135
- : Boolean(nextReasoningEffort && nextReasoningEffort !== previousReasoningEffort);
1136
-
1137
- if (!modelChanged && !reasoningChanged) {
1138
- return null;
1139
- }
1140
-
1141
- return {
1142
- previousModel,
1143
- nextModel,
1144
- previousReasoningEffort,
1145
- nextReasoningEffort,
1146
- modelChanged,
1147
- reasoningChanged,
1148
- };
1149
- }
1150
-
1151
- function ModelEventChip({
1152
- agentName,
1153
- model,
1154
- tone,
1155
- }: {
1156
- agentName: string;
1157
- model: string | null;
1158
- tone: "muted" | "accent";
1159
- }) {
1160
- const palette = tone === "accent"
1161
- ? "border-[#5a452f] bg-[rgba(234,122,42,0.08)] text-[#f2c79f]"
1162
- : "border-[#384244] bg-[rgba(255,255,255,0.04)] text-[#d6d6d6]";
1163
-
1164
- return (
1165
- <span className={`inline-flex items-center gap-2 rounded-full border px-2 py-1 text-[12px] leading-[18px] ${palette}`}>
1166
- <span>{model ? formatCurrentModelLabel(agentName, model) : "Session default"}</span>
1167
- </span>
1168
- );
1169
- }
1170
-
1171
- function SessionPreferenceEntry({
1172
- entry,
1173
- agentName,
1174
- }: {
1175
- entry: NormalizedChatEntry;
1176
- agentName: string;
1177
- }) {
1178
- const update = extractSessionPreferenceUpdate(entry);
1179
- const timestamp = formatTimestamp(entry.createdAt);
1180
-
1181
- if (!update) {
1182
- return null;
1183
- }
1184
-
1185
- const heading = update.modelChanged
1186
- ? "Model switched"
1187
- : "Reasoning updated";
1188
-
1189
- return (
1190
- <div className="rounded-[3px] border border-[#5a452f] bg-[rgba(234,122,42,0.08)] px-3 py-3">
1191
- <div className="flex flex-wrap items-center gap-2">
1192
- <span className="inline-flex items-center gap-2 text-[12px] font-medium uppercase tracking-[0.18em] text-[#f2c79f]">
1193
- <ArrowRightLeft className="h-[14px] w-[14px]" strokeWidth={1.8} />
1194
- {heading}
1195
- </span>
1196
- {timestamp ? (
1197
- <span className="text-[12px] leading-[18px] text-[#b38358]">{timestamp}</span>
1198
- ) : null}
1199
- </div>
1200
-
1201
- <div className="mt-3 space-y-3">
1202
- {update.modelChanged ? (
1203
- <div className="flex flex-wrap items-center gap-2">
1204
- <ModelEventChip agentName={agentName} model={update.previousModel} tone="muted" />
1205
- <ChevronRight className="h-[14px] w-[14px] text-[#b38358]" strokeWidth={1.8} />
1206
- <ModelEventChip agentName={agentName} model={update.nextModel} tone="accent" />
1207
- </div>
1208
- ) : null}
1209
- {update.reasoningChanged ? (
1210
- <div className="flex flex-wrap items-center gap-2 text-[12px] leading-[18px] text-[#efd8c1]">
1211
- <span className="rounded-full border border-[#384244] bg-[rgba(255,255,255,0.04)] px-2 py-1">
1212
- {formatReasoningLabel(update.previousReasoningEffort ?? "")}
1213
- </span>
1214
- <ChevronRight className="h-[14px] w-[14px] text-[#b38358]" strokeWidth={1.8} />
1215
- <span className="rounded-full border border-[#5a452f] bg-[rgba(234,122,42,0.08)] px-2 py-1 text-[#f2c79f]">
1216
- {formatReasoningLabel(update.nextReasoningEffort ?? "")}
1217
- </span>
1218
- </div>
1219
- ) : null}
1220
- </div>
1221
- </div>
1222
- );
1223
- }
1224
-
1225
- function SystemEntry({
1226
- entry,
1227
- agentName,
1228
- }: {
1229
- entry: NormalizedChatEntry;
1230
- agentName: string;
1231
- }) {
1232
- const preferenceEntry = extractSessionPreferenceUpdate(entry);
1233
- if (preferenceEntry) {
1234
- return <SessionPreferenceEntry entry={entry} agentName={agentName} />;
1235
- }
1236
-
1237
- return (
1238
- <div className="rounded-[3px] border border-[#315434] bg-[rgba(84,176,79,0.08)] px-3 py-2">
1239
- <p className="text-[12px] uppercase tracking-[0.18em] text-[#8bc886]">System</p>
1240
- <MarkdownBlock
1241
- text={entry.text}
1242
- className="prose prose-invert mt-2 max-w-none text-[14px] leading-[21px] text-[#d8ead6] prose-p:my-0 prose-p:text-[#d8ead6] prose-strong:text-white prose-code:text-[#d8ead6]"
1243
- />
1244
- <AttachmentPills attachments={entry.attachments} />
1245
- </div>
1246
- );
1247
- }
1248
-
1249
- function FeedEntry({
1250
- entry,
1251
- agentName,
1252
- }: {
1253
- entry: NormalizedChatEntry;
1254
- agentName: string;
1255
- }) {
1256
- switch (entry.kind) {
1257
- case "user":
1258
- return <UserEntryCard entry={entry} />;
1259
- case "assistant":
1260
- return <AssistantEntry entry={entry} agentName={agentName} />;
1261
- case "tool":
1262
- return <ToolEntry entry={entry} />;
1263
- case "system":
1264
- return <SystemEntry entry={entry} agentName={agentName} />;
1265
- default:
1266
- return <StatusEntry entry={entry} />;
1267
- }
1268
- }
1269
-
1270
- async function postSessionTerminalInterrupt(
1271
- sessionId: string,
1272
- ): Promise<void> {
1273
- const response = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/interrupt`, {
1274
- method: "POST",
1275
- headers: {
1276
- "Content-Type": "application/json",
1277
- },
1278
- body: JSON.stringify({}),
1279
- });
1280
- const data = (await response.json().catch(() => null)) as { error?: string } | null;
1281
- if (!response.ok) {
1282
- throw new Error(data?.error ?? `Failed to interrupt session: ${response.status}`);
1283
- }
1284
- }
1285
-
1286
- export function ChatPanel({
1287
- sessionId,
1288
- agentName,
1289
- projectId,
1290
- sessionModel,
1291
- sessionReasoningEffort,
1292
- }: ChatPanelProps) {
1293
- const { agents } = useAgents();
1294
- const { entries, error, loading, parserState, runtimeStatus, sessionStatus, refresh } = useSessionFeed(sessionId);
1295
-
1296
- const [message, setMessage] = useState("");
1297
- const [sending, setSending] = useState(false);
1298
- const [interrupting, setInterrupting] = useState(false);
1299
- const [sendError, setSendError] = useState<string | null>(null);
1300
- const [modelMenuOpen, setModelMenuOpen] = useState(false);
1301
- const [reasoningMenuOpen, setReasoningMenuOpen] = useState(false);
1302
- const [selectedModel, setSelectedModel] = useState(sessionModel?.trim() || "");
1303
- const [customModelInput, setCustomModelInput] = useState("");
1304
- const [selectedReasoning, setSelectedReasoning] = useState(sessionReasoningEffort?.trim().toLowerCase() || "");
1305
- const [attachments, setAttachments] = useState<AttachmentDraft[]>([]);
1306
- const [selectedSlashIndex, setSelectedSlashIndex] = useState(0);
1307
-
1308
- const endRef = useRef<HTMLDivElement | null>(null);
1309
- const scrollContainerRef = useRef<HTMLDivElement | null>(null);
1310
- const composerRef = useRef<HTMLTextAreaElement | null>(null);
1311
- const fileInputRef = useRef<HTMLInputElement | null>(null);
1312
- const modelMenuRef = useRef<HTMLDivElement | null>(null);
1313
- const reasoningMenuRef = useRef<HTMLDivElement | null>(null);
1314
- const shouldStickToBottomRef = useRef(true);
1315
- const previousScrollTopRef = useRef(0);
1316
- const normalizedAgentName = agentName?.trim() || "";
1317
- const normalizedSessionStatus = sessionStatus?.trim().toLowerCase() ?? "";
1318
- const runtimeCatalog = useMemo(
1319
- () => findRuntimeCatalog(agents, normalizedAgentName),
1320
- [agents, normalizedAgentName],
1321
- );
1322
- const modelOptions = useMemo(
1323
- () => getModelOptions(runtimeCatalog, normalizedAgentName, sessionModel?.trim() || ""),
1324
- [runtimeCatalog, normalizedAgentName, sessionModel],
1325
- );
1326
- const router = useRouter();
1327
- const modelPlaceholder = useMemo(
1328
- () => getCustomModelPlaceholder(normalizedAgentName, runtimeCatalog),
1329
- [normalizedAgentName, runtimeCatalog],
1330
- );
1331
- const resolvedModelValue = selectedModel.trim();
1332
- const reasoningOptions = useMemo(
1333
- () => getReasoningOptions(
1334
- normalizedAgentName,
1335
- runtimeCatalog,
1336
- resolvedModelValue || sessionModel?.trim() || "",
1337
- ),
1338
- [normalizedAgentName, runtimeCatalog, resolvedModelValue, sessionModel],
1339
- );
1340
- const defaultReasoningSelection = useMemo(
1341
- () => getDefaultReasoningSelection(
1342
- normalizedAgentName,
1343
- runtimeCatalog,
1344
- resolvedModelValue || sessionModel?.trim() || "",
1345
- sessionReasoningEffort?.trim() || "",
1346
- ),
1347
- [normalizedAgentName, runtimeCatalog, resolvedModelValue, sessionModel, sessionReasoningEffort],
1348
- );
1349
-
1350
- useEffect(() => {
1351
- setSelectedModel(sessionModel?.trim() || "");
1352
- setCustomModelInput(sessionModel?.trim() || "");
1353
- }, [sessionModel]);
1354
-
1355
- useEffect(() => {
1356
- setSelectedReasoning(sessionReasoningEffort?.trim().toLowerCase() || "");
1357
- }, [sessionReasoningEffort]);
1358
-
1359
- useEffect(() => {
1360
- if (!selectedModel.trim()) {
1361
- setCustomModelInput("");
1362
- return;
1363
- }
1364
- if (modelOptions.some((option) => option.id === selectedModel.trim())) {
1365
- setCustomModelInput("");
1366
- return;
1367
- }
1368
- setCustomModelInput(selectedModel.trim());
1369
- }, [modelOptions, selectedModel]);
1370
-
1371
- useEffect(() => {
1372
- if (!selectedReasoning) {
1373
- return;
1374
- }
1375
- if (reasoningOptions.some((option) => option.id === selectedReasoning)) {
1376
- return;
1377
- }
1378
- setSelectedReasoning(defaultReasoningSelection || "");
1379
- }, [defaultReasoningSelection, reasoningOptions, selectedReasoning]);
1380
-
1381
- // Clear transient error state when the session changes.
1382
- useEffect(() => {
1383
- setSendError(null);
1384
- }, [sessionId]);
1385
-
1386
- // Close model/reasoning menus on click outside.
1387
- useEffect(() => {
1388
- if (!modelMenuOpen && !reasoningMenuOpen) return;
1389
- function onClickOutside(event: globalThis.MouseEvent) {
1390
- if (modelMenuRef.current && !modelMenuRef.current.contains(event.target as Node)) {
1391
- setModelMenuOpen(false);
1392
- }
1393
- if (reasoningMenuRef.current && !reasoningMenuRef.current.contains(event.target as Node)) {
1394
- setReasoningMenuOpen(false);
1395
- }
1396
- }
1397
- document.addEventListener("mousedown", onClickOutside);
1398
- return () => document.removeEventListener("mousedown", onClickOutside);
1399
- }, [modelMenuOpen, reasoningMenuOpen]);
1400
-
1401
- const displayEntries = useMemo(
1402
- () => {
1403
- if (normalizedSessionStatus !== "working" && normalizedSessionStatus !== "running") {
1404
- return entries;
1405
- }
1406
- return entries.filter((entry) => !(entry.kind === "status" && entry.source === "session-status"));
1407
- },
1408
- [entries, normalizedSessionStatus],
1409
- );
1410
-
1411
- const hasStreamingEntry = displayEntries.some((entry) => entry.streaming);
1412
- const isSessionRunning = normalizedSessionStatus === "running" || normalizedSessionStatus === "working" || hasStreamingEntry;
1413
- const hasLiveTerminalSession = normalizedSessionStatus === "running"
1414
- || normalizedSessionStatus === "working"
1415
- || normalizedSessionStatus === "needs_input"
1416
- || normalizedSessionStatus === "stuck";
1417
- const composerSummary = useMemo(
1418
- () => extractComposerSummary(displayEntries),
1419
- [displayEntries],
1420
- );
1421
- const slashCommandOptions = useMemo(
1422
- () => getSlashCommandOptions(normalizedAgentName, message),
1423
- [message, normalizedAgentName],
1424
- );
1425
- const showSlashCommandMenu = slashCommandOptions.length > 0;
1426
- const feedItems = useMemo(
1427
- () => buildFeedRenderItems(displayEntries),
1428
- [displayEntries],
1429
- );
1430
-
1431
- const normalizeWhitespaceOnlyDraft = useCallback(() => {
1432
- setMessage((current) => (current.trim().length === 0 ? "" : current));
1433
- }, []);
1434
-
1435
- const updateBottomStickiness = useCallback(() => {
1436
- const container = scrollContainerRef.current;
1437
- if (!container) {
1438
- shouldStickToBottomRef.current = true;
1439
- return;
1440
- }
1441
-
1442
- previousScrollTopRef.current = container.scrollTop;
1443
- const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
1444
- shouldStickToBottomRef.current = distanceFromBottom <= 16;
1445
- }, []);
1446
-
1447
- const scrollToBottom = useCallback((behavior: ScrollBehavior) => {
1448
- shouldStickToBottomRef.current = true;
1449
- const container = scrollContainerRef.current;
1450
- if (!container) {
1451
- endRef.current?.scrollIntoView({ behavior, block: "end" });
1452
- return;
1453
- }
1454
-
1455
- window.requestAnimationFrame(() => {
1456
- container.scrollTo({
1457
- top: container.scrollHeight,
1458
- behavior,
1459
- });
1460
- });
1461
- }, []);
1462
-
1463
- useEffect(() => {
1464
- shouldStickToBottomRef.current = true;
1465
- previousScrollTopRef.current = 0;
1466
- }, [sessionId]);
1467
-
1468
- useLayoutEffect(() => {
1469
- const container = scrollContainerRef.current;
1470
- if (!container) {
1471
- return;
1472
- }
1473
-
1474
- if (shouldStickToBottomRef.current) {
1475
- previousScrollTopRef.current = container.scrollTop;
1476
- return;
1477
- }
1478
-
1479
- container.scrollTop = previousScrollTopRef.current;
1480
- }, [feedItems, hasStreamingEntry, parserState, runtimeStatus, sending]);
1481
-
1482
- useEffect(() => {
1483
- const handleVisibilityChange = () => {
1484
- if (document.hidden) {
1485
- return;
1486
- }
1487
- normalizeWhitespaceOnlyDraft();
1488
- };
1489
-
1490
- const handleWindowFocus = () => {
1491
- normalizeWhitespaceOnlyDraft();
1492
- };
1493
-
1494
- document.addEventListener("visibilitychange", handleVisibilityChange);
1495
- window.addEventListener("focus", handleWindowFocus);
1496
- return () => {
1497
- document.removeEventListener("visibilitychange", handleVisibilityChange);
1498
- window.removeEventListener("focus", handleWindowFocus);
1499
- };
1500
- }, [normalizeWhitespaceOnlyDraft]);
1501
-
1502
- const prevEntryCountRef = useRef(0);
1503
- useEffect(() => {
1504
- const count = displayEntries.length;
1505
- const countChanged = count !== prevEntryCountRef.current;
1506
- if ((countChanged || hasStreamingEntry || sending) && shouldStickToBottomRef.current) {
1507
- scrollToBottom(hasStreamingEntry ? "auto" : "smooth");
1508
- }
1509
- prevEntryCountRef.current = count;
1510
- }, [displayEntries, hasStreamingEntry, scrollToBottom, sending]);
1511
-
1512
- useEffect(() => {
1513
- if (!showSlashCommandMenu) {
1514
- setSelectedSlashIndex(0);
1515
- return;
1516
- }
1517
- setSelectedSlashIndex((current) => Math.min(current, Math.max(slashCommandOptions.length - 1, 0)));
1518
- }, [showSlashCommandMenu, slashCommandOptions.length]);
1519
-
1520
- const selectedModelLabel = useMemo(() => {
1521
- if (!selectedModel) {
1522
- return sessionModel?.trim()
1523
- ? formatCurrentModelLabel(normalizedAgentName, sessionModel)
1524
- : "Session default";
1525
- }
1526
- return modelOptions.find((option) => option.id === selectedModel)?.label ?? selectedModel;
1527
- }, [modelOptions, normalizedAgentName, selectedModel, sessionModel]);
1528
- const selectedReasoningLabel = useMemo(() => {
1529
- if (!selectedReasoning) {
1530
- return sessionReasoningEffort?.trim()
1531
- ? formatReasoningLabel(sessionReasoningEffort)
1532
- : defaultReasoningSelection
1533
- ? formatReasoningLabel(defaultReasoningSelection)
1534
- : "Reasoning";
1535
- }
1536
- return reasoningOptions.find((option) => option.id === selectedReasoning)?.label
1537
- ?? formatReasoningLabel(selectedReasoning);
1538
- }, [defaultReasoningSelection, reasoningOptions, selectedReasoning, sessionReasoningEffort]);
1539
-
1540
- const handleInterrupt = useCallback(async () => {
1541
- if (!hasLiveTerminalSession || interrupting) return;
1542
-
1543
- setInterrupting(true);
1544
- setSendError(null);
1545
-
1546
- try {
1547
- const response = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/interrupt`, {
1548
- method: "POST",
1549
- });
1550
-
1551
- const data = (await response.json().catch(() => null)) as
1552
- | { error?: string }
1553
- | null;
1554
-
1555
- if (!response.ok) {
1556
- throw new Error(data?.error ?? `Failed to interrupt agent: ${response.status}`);
1557
- }
1558
- } catch (err) {
1559
- setSendError(err instanceof Error ? err.message : "Failed to interrupt agent");
1560
- } finally {
1561
- setInterrupting(false);
1562
- }
1563
- }, [hasLiveTerminalSession, interrupting, sessionId]);
1564
-
1565
- const handleSendTerminalSpecial = useCallback(async (special: string) => {
1566
- if (!hasLiveTerminalSession) return;
1567
-
1568
- // Only Ctrl+C and Ctrl+D trigger an interrupt via the backend.
1569
- // All other keys are handled directly by the TTyD WebSocket in the terminal.
1570
- if (special !== "C-c" && special !== "C-d") return;
1571
-
1572
- shouldStickToBottomRef.current = true;
1573
- setSendError(null);
1574
-
1575
- try {
1576
- await postSessionTerminalInterrupt(sessionId);
1577
- } catch (err) {
1578
- setSendError(err instanceof Error ? err.message : "Failed to interrupt session");
1579
- }
1580
- }, [hasLiveTerminalSession, sessionId]);
1581
-
1582
- const handleSend = useCallback(async () => {
1583
- if (isSessionRunning || interrupting) return;
1584
- const trimmedMessage = message.trim();
1585
- if (!trimmedMessage && attachments.length === 0) return;
1586
-
1587
- shouldStickToBottomRef.current = true;
1588
- setSending(true);
1589
- setSendError(null);
1590
-
1591
- try {
1592
- const attachmentPaths = await uploadProjectAttachments({
1593
- files: attachments.map((attachment) => attachment.file),
1594
- projectId: projectId ?? "",
1595
- preferAbsolute: true,
1596
- });
1597
-
1598
- const response = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/send`, {
1599
- method: "POST",
1600
- headers: {
1601
- "Content-Type": "application/json",
1602
- },
1603
- body: JSON.stringify({
1604
- message: trimmedMessage,
1605
- attachments: attachmentPaths,
1606
- model: selectedModel || null,
1607
- reasoningEffort: selectedReasoning || null,
1608
- projectId: projectId || null,
1609
- }),
1610
- });
1611
-
1612
- const data = (await response.json().catch(() => null)) as
1613
- | { error?: string; sessionId?: string | null }
1614
- | null;
1615
-
1616
- if (!response.ok) {
1617
- throw new Error(data?.error ?? `Failed to send message: ${response.status}`);
1618
- }
1619
-
1620
- setMessage("");
1621
- setAttachments([]);
1622
- if (data?.sessionId && data.sessionId !== sessionId) {
1623
- router.push(`/sessions/${encodeURIComponent(data.sessionId)}`);
1624
- return;
1625
- }
1626
- await refresh();
1627
- } catch (err) {
1628
- setSendError(err instanceof Error ? err.message : "Failed to send message");
1629
- } finally {
1630
- setSending(false);
1631
- }
1632
- }, [isSessionRunning, interrupting, message, attachments, sessionId, selectedModel, selectedReasoning, projectId, router, refresh]);
1633
-
1634
- function applySlashCommand(option: SlashCommandOption) {
1635
- setMessage(option.command);
1636
- setSendError(null);
1637
- }
1638
-
1639
- function handleComposerKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
1640
- const composerEmpty = message.length === 0 && attachments.length === 0;
1641
- const shouldPassthroughSpecial =
1642
- hasLiveTerminalSession && composerEmpty && !showSlashCommandMenu;
1643
-
1644
- if (showSlashCommandMenu && event.key === "ArrowDown") {
1645
- event.preventDefault();
1646
- setSelectedSlashIndex((current) => (
1647
- current >= slashCommandOptions.length - 1 ? 0 : current + 1
1648
- ));
1649
- return;
1650
- }
1651
-
1652
- if (showSlashCommandMenu && event.key === "ArrowUp") {
1653
- event.preventDefault();
1654
- setSelectedSlashIndex((current) => (
1655
- current <= 0 ? Math.max(slashCommandOptions.length - 1, 0) : current - 1
1656
- ));
1657
- return;
1658
- }
1659
-
1660
- if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey && hasLiveTerminalSession) {
1661
- if (event.key.toLowerCase() === "c") {
1662
- event.preventDefault();
1663
- void handleInterrupt();
1664
- return;
1665
- }
1666
- if (event.key.toLowerCase() === "d") {
1667
- event.preventDefault();
1668
- void handleSendTerminalSpecial("C-d");
1669
- return;
1670
- }
1671
- }
1672
-
1673
- if (shouldPassthroughSpecial && event.key === "Escape") {
1674
- event.preventDefault();
1675
- void handleSendTerminalSpecial("Escape");
1676
- return;
1677
- }
1678
-
1679
- if (event.key === "Enter" && !event.shiftKey) {
1680
- event.preventDefault();
1681
- if (showSlashCommandMenu) {
1682
- const selectedOption = slashCommandOptions[selectedSlashIndex];
1683
- if (selectedOption) {
1684
- if (selectedOption.exact && !isSessionRunning) {
1685
- void handleSend();
1686
- } else {
1687
- applySlashCommand(selectedOption);
1688
- }
1689
- return;
1690
- }
1691
- }
1692
- if (shouldPassthroughSpecial) {
1693
- void handleSendTerminalSpecial("Enter");
1694
- return;
1695
- }
1696
- if (!isSessionRunning) {
1697
- void handleSend();
1698
- }
1699
- return;
1700
- }
1701
-
1702
- if (shouldPassthroughSpecial && event.key === "Tab") {
1703
- event.preventDefault();
1704
- void handleSendTerminalSpecial("Tab");
1705
- return;
1706
- }
1707
-
1708
- if (shouldPassthroughSpecial && (
1709
- event.key === "ArrowUp"
1710
- || event.key === "ArrowDown"
1711
- || event.key === "ArrowLeft"
1712
- || event.key === "ArrowRight"
1713
- || event.key === "Backspace"
1714
- )) {
1715
- event.preventDefault();
1716
- void handleSendTerminalSpecial(event.key);
1717
- }
1718
- }
1719
-
1720
- function handleAttachmentChange(event: ChangeEvent<HTMLInputElement>) {
1721
- const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
1722
- const files = Array.from(event.target.files ?? []);
1723
- if (!files.length) return;
1724
-
1725
- const validFiles: File[] = [];
1726
- for (const file of files) {
1727
- if (file.size > MAX_FILE_SIZE) {
1728
- setSendError(`File "${file.name}" exceeds 10 MB limit`);
1729
- continue;
1730
- }
1731
- validFiles.push(file);
1732
- }
1733
-
1734
- if (validFiles.length > 0) {
1735
- setAttachments((current) => [
1736
- ...current,
1737
- ...validFiles.map((file) => ({ file })),
1738
- ]);
1739
- }
1740
-
1741
- event.target.value = "";
1742
- }
1743
-
1744
- return (
1745
- <div className="relative flex h-full min-h-0 flex-col overflow-hidden bg-[radial-gradient(circle_at_top,rgba(45,45,45,0.32),rgba(33,33,33,1)_40%)] text-[#c4c4c4]">
1746
- <div className="mx-auto flex h-full min-h-0 w-full max-w-[855px] flex-col">
1747
- <div
1748
- ref={scrollContainerRef}
1749
- onScroll={updateBottomStickiness}
1750
- onWheelCapture={(event) => {
1751
- if (event.deltaY < 0) {
1752
- shouldStickToBottomRef.current = false;
1753
- }
1754
- }}
1755
- className="flex-1 overflow-y-auto px-3 pt-3 sm:px-4 [overflow-anchor:none]"
1756
- >
1757
- <div className="mx-auto flex w-full max-w-[768px] flex-col gap-5 pb-6">
1758
- {loading && displayEntries.length === 0 ? (
1759
- <div className="flex items-center gap-2 px-4 text-[#8f8f8f]">
1760
- <Loader2 className="h-4 w-4 animate-spin" />
1761
- <span className="text-[14px] leading-[21px]">Loading conversation</span>
1762
- </div>
1763
- ) : null}
1764
-
1765
- {!loading && displayEntries.length === 0 ? (
1766
- <div className="px-4 py-2 text-[16px] leading-[24px] text-[#8f8f8f]">
1767
- Start the next turn to stream follow-up work into this panel.
1768
- </div>
1769
- ) : null}
1770
-
1771
- {feedItems.map((item) => (
1772
- item.kind === "tool-group" ? (
1773
- <ToolGroup key={item.id} entries={item.entries} autoCollapsed={!isSessionRunning} />
1774
- ) : (
1775
- <FeedEntry key={item.entry.id} entry={item.entry} agentName={normalizedAgentName} />
1776
- )
1777
- ))}
1778
-
1779
- {error ? (
1780
- <div className="rounded-[3px] border border-[#603535] bg-[rgba(210,81,81,0.12)] px-3 py-2 text-[13px] leading-[20px] text-[#f0b5b5]">
1781
- {error}
1782
- </div>
1783
- ) : null}
1784
-
1785
- {sendError ? (
1786
- <div className="rounded-[3px] border border-[#603535] bg-[rgba(210,81,81,0.12)] px-3 py-2 text-[13px] leading-[20px] text-[#f0b5b5]">
1787
- {sendError}
1788
- </div>
1789
- ) : null}
1790
-
1791
- {parserState ? (
1792
- <ParserStateBanner
1793
- kind={parserState.kind}
1794
- message={parserState.message}
1795
- command={parserState.command}
1796
- />
1797
- ) : null}
1798
-
1799
- <div ref={endRef} />
1800
- </div>
1801
- </div>
1802
-
1803
- <div className="shrink-0 px-3 pb-4 pt-0 sm:px-4">
1804
- <div className="mx-auto w-full max-w-[768px]">
1805
- <div className="rounded-[3px] border border-[#333] bg-[#1c1c1c] shadow-[0_12px_40px_rgba(0,0,0,0.22)]">
1806
- <div className="flex flex-wrap items-center gap-2 border-b border-[#333] px-2 py-2">
1807
- {composerSummary ? <SummaryChip summary={composerSummary} /> : <div className="min-h-[29px]" />}
1808
- <div className="ml-auto flex min-w-0 items-center gap-2">
1809
- {normalizedAgentName ? (
1810
- <div className="hidden items-center overflow-hidden sm:flex">
1811
- <AgentTileIcon seed={{ label: normalizedAgentName }} className="h-[25px] w-[25px] border-none bg-transparent" />
1812
- </div>
1813
- ) : null}
1814
- <div className="hidden h-[20px] w-[20px] items-center justify-center text-[#8f8f8f] sm:flex">
1815
- <Code2 className="h-[15px] w-[15px]" strokeWidth={1.7} />
1816
- </div>
1817
- <div className="relative" ref={modelMenuRef}>
1818
- <button
1819
- type="button"
1820
- onClick={() => {
1821
- setReasoningMenuOpen(false);
1822
- setModelMenuOpen((open) => !open);
1823
- }}
1824
- className="inline-flex min-h-[38px] sm:min-h-[31px] items-center gap-2 rounded-[3px] border border-[#333] bg-[#1c1c1c] px-[9px] py-[5px] text-[14px] leading-[21px] text-[#c4c4c4] transition hover:bg-[#292929]"
1825
- >
1826
- <span className="max-w-[140px] truncate">{selectedModelLabel}</span>
1827
- <ChevronDown className="h-[10px] w-[10px] text-[#8f8f8f]" strokeWidth={1.8} />
1828
- </button>
1829
-
1830
- {modelMenuOpen ? (
1831
- <div className="absolute bottom-[calc(100%+8px)] right-0 z-20 w-[calc(100vw-2rem)] max-w-[320px] sm:w-[320px] overflow-hidden rounded-[4px] border border-[#333] bg-[#1c1c1c] shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
1832
- <div className="border-b border-[#333] px-3 py-2">
1833
- <p className="text-[12px] font-medium uppercase tracking-[0.16em] text-[#8f8f8f]">Model</p>
1834
- <p className="mt-1 text-[12px] leading-[18px] text-[#6f6f6f]">
1835
- Presets come from the local runtime catalog. You can also force any exact model id.
1836
- </p>
1837
- </div>
1838
- <div className="max-h-72 overflow-y-auto p-1">
1839
- <button
1840
- type="button"
1841
- onClick={() => {
1842
- setSelectedModel(sessionModel?.trim() || "");
1843
- setCustomModelInput("");
1844
- setModelMenuOpen(false);
1845
- }}
1846
- className="flex w-full flex-col rounded-[3px] px-3 py-2 text-left transition hover:bg-[#292929]"
1847
- >
1848
- <span className="text-[14px] leading-[21px] text-[#c4c4c4]">Session default</span>
1849
- <span className="text-[12px] leading-[18px] text-[#8f8f8f]">
1850
- {sessionModel?.trim()
1851
- ? `Use ${formatCurrentModelLabel(normalizedAgentName, sessionModel)} for follow-up turns.`
1852
- : "Keep using the model configured on this session."}
1853
- </span>
1854
- </button>
1855
-
1856
- {modelOptions.map((option) => (
1857
- <button
1858
- key={option.id}
1859
- type="button"
1860
- onClick={() => {
1861
- setSelectedModel(option.id);
1862
- setCustomModelInput("");
1863
- setModelMenuOpen(false);
1864
- }}
1865
- className={`flex w-full flex-col rounded-[3px] px-3 py-2 text-left transition hover:bg-[#292929] ${selectedModel === option.id ? "bg-[#292929]" : ""}`}
1866
- >
1867
- <span className="text-[14px] leading-[21px] text-[#c4c4c4]">{option.label}</span>
1868
- <span className="text-[12px] leading-[18px] text-[#8f8f8f]">{option.helper}</span>
1869
- </button>
1870
- ))}
1871
- </div>
1872
- <div className="border-t border-[#333] p-3">
1873
- <label className="mb-2 block text-[12px] leading-[18px] text-[#8f8f8f]">
1874
- Exact model id
1875
- </label>
1876
- <div className="flex items-center gap-2">
1877
- <input
1878
- value={customModelInput}
1879
- onChange={(event) => setCustomModelInput(event.target.value)}
1880
- onKeyDown={(event) => {
1881
- if (event.key !== "Enter") return;
1882
- event.preventDefault();
1883
- const nextModel = customModelInput.trim();
1884
- if (!nextModel) return;
1885
- setSelectedModel(nextModel);
1886
- setModelMenuOpen(false);
1887
- }}
1888
- placeholder={modelPlaceholder}
1889
- className="min-w-0 flex-1 rounded-[3px] border border-[#333] bg-[#171717] px-3 py-2 text-[13px] leading-[20px] text-[#d0d0d0] outline-none placeholder:text-[#666] focus:border-[#4a4a4a]"
1890
- />
1891
- <button
1892
- type="button"
1893
- onClick={() => {
1894
- const nextModel = customModelInput.trim();
1895
- if (!nextModel) return;
1896
- setSelectedModel(nextModel);
1897
- setModelMenuOpen(false);
1898
- }}
1899
- className="rounded-[3px] border border-[#333] px-3 py-2 text-[12px] leading-[18px] text-[#c4c4c4] transition hover:bg-[#292929]"
1900
- >
1901
- Use
1902
- </button>
1903
- </div>
1904
- </div>
1905
- </div>
1906
- ) : null}
1907
- </div>
1908
- <div className="relative" ref={reasoningMenuRef}>
1909
- <button
1910
- type="button"
1911
- onClick={() => {
1912
- setModelMenuOpen(false);
1913
- setReasoningMenuOpen((open) => !open);
1914
- }}
1915
- className="inline-flex min-h-[38px] sm:min-h-[31px] items-center gap-2 rounded-[3px] border border-[#333] bg-[#1c1c1c] px-[9px] py-[5px] text-[14px] leading-[21px] text-[#c4c4c4] transition hover:bg-[#292929]"
1916
- >
1917
- <span className="max-w-[120px] truncate">{selectedReasoningLabel}</span>
1918
- <ChevronDown className="h-[10px] w-[10px] text-[#8f8f8f]" strokeWidth={1.8} />
1919
- </button>
1920
-
1921
- {reasoningMenuOpen ? (
1922
- <div className="absolute bottom-[calc(100%+8px)] right-0 z-20 w-[calc(100vw-2rem)] max-w-[260px] sm:w-[260px] overflow-hidden rounded-[4px] border border-[#333] bg-[#1c1c1c] shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
1923
- <div className="border-b border-[#333] px-3 py-2">
1924
- <p className="text-[12px] font-medium uppercase tracking-[0.16em] text-[#8f8f8f]">Reasoning</p>
1925
- <p className="mt-1 text-[12px] leading-[18px] text-[#6f6f6f]">
1926
- Override how hard the agent thinks for the next turns.
1927
- </p>
1928
- </div>
1929
- <div className="max-h-72 overflow-y-auto p-1">
1930
- <button
1931
- type="button"
1932
- onClick={() => {
1933
- setSelectedReasoning("");
1934
- setReasoningMenuOpen(false);
1935
- }}
1936
- className="flex w-full flex-col rounded-[3px] px-3 py-2 text-left transition hover:bg-[#292929]"
1937
- >
1938
- <span className="text-[14px] leading-[21px] text-[#c4c4c4]">Session default</span>
1939
- <span className="text-[12px] leading-[18px] text-[#8f8f8f]">
1940
- {sessionReasoningEffort?.trim()
1941
- ? `Use ${formatReasoningLabel(sessionReasoningEffort)}.`
1942
- : "Use the default reasoning level for the selected model."}
1943
- </span>
1944
- </button>
1945
-
1946
- {reasoningOptions.map((option) => (
1947
- <button
1948
- key={option.id}
1949
- type="button"
1950
- onClick={() => {
1951
- setSelectedReasoning(option.id.trim().toLowerCase());
1952
- setReasoningMenuOpen(false);
1953
- }}
1954
- className={`flex w-full flex-col rounded-[3px] px-3 py-2 text-left transition hover:bg-[#292929] ${selectedReasoning === option.id.trim().toLowerCase() ? "bg-[#292929]" : ""}`}
1955
- >
1956
- <span className="text-[14px] leading-[21px] text-[#c4c4c4]">{option.label}</span>
1957
- <span className="text-[12px] leading-[18px] text-[#8f8f8f]">{option.description}</span>
1958
- </button>
1959
- ))}
1960
- {reasoningOptions.length === 0 ? (
1961
- <div className="px-3 py-2 text-[12px] leading-[18px] text-[#8f8f8f]">
1962
- No explicit reasoning controls were detected for this agent.
1963
- </div>
1964
- ) : null}
1965
- </div>
1966
- <div className="border-t border-[#333] px-3 py-2 text-[12px] leading-[18px] text-[#6f6f6f]">
1967
- Reasoning options can change with the selected model.
1968
- </div>
1969
- </div>
1970
- ) : null}
1971
- </div>
1972
- </div>
1973
- </div>
1974
-
1975
- <div className="px-2 py-2">
1976
- <textarea
1977
- ref={composerRef}
1978
- value={message}
1979
- onChange={(event) => setMessage(event.target.value)}
1980
- onFocus={() => {
1981
- normalizeWhitespaceOnlyDraft();
1982
- }}
1983
- onKeyDown={handleComposerKeyDown}
1984
- placeholder="Continue working on this task..."
1985
- rows={1}
1986
- className="min-h-[24px] w-full resize-none bg-transparent px-1 py-0 text-[16px] leading-[24px] text-[#8f8f8f] outline-none placeholder:text-[#8f8f8f]"
1987
- />
1988
-
1989
- {showSlashCommandMenu ? (
1990
- <div className="mt-3 max-w-full overflow-hidden rounded-[3px] border border-[#333] bg-[#171717]">
1991
- <div className="border-b border-[#333] px-3 py-2 text-[11px] uppercase tracking-[0.18em] text-[#8f8f8f]">
1992
- Slash commands
1993
- </div>
1994
- <div className="max-h-[min(220px,40vh)] max-w-full overflow-y-auto py-1">
1995
- {slashCommandOptions.map((option, index) => (
1996
- <button
1997
- key={option.command}
1998
- type="button"
1999
- onClick={() => applySlashCommand(option)}
2000
- className={`flex w-full flex-col gap-1 px-3 py-2 text-left transition ${
2001
- index === selectedSlashIndex ? "bg-[#242424]" : "hover:bg-[#1f1f1f]"
2002
- }`}
2003
- >
2004
- <span className="text-[14px] leading-[21px] text-[#f1f1f1]">{option.label}</span>
2005
- <span className="text-[12px] leading-[18px] text-[#8f8f8f]">{option.description}</span>
2006
- </button>
2007
- ))}
2008
- </div>
2009
- <div className="border-t border-[#333] px-3 py-2 text-[12px] leading-[18px] text-[#8f8f8f]">
2010
- Slash commands are passed straight to the live agent session under the chat UI.
2011
- </div>
2012
- </div>
2013
- ) : null}
2014
-
2015
- <AttachmentPills
2016
- attachments={attachments.map((attachment) => attachment.file.name)}
2017
- onRemove={(index) => {
2018
- setAttachments((current) => {
2019
- const next = [...current];
2020
- next.splice(index, 1);
2021
- return next;
2022
- });
2023
- }}
2024
- />
2025
-
2026
- <div className="mt-3 flex items-end justify-between gap-3">
2027
- <div className="flex min-w-0 flex-wrap items-center gap-2">
2028
- <input
2029
- ref={fileInputRef}
2030
- type="file"
2031
- multiple
2032
- className="hidden"
2033
- onChange={handleAttachmentChange}
2034
- />
2035
- <button
2036
- type="button"
2037
- onClick={() => fileInputRef.current?.click()}
2038
- className="inline-flex h-[38px] w-[38px] sm:h-[29px] sm:w-[33px] items-center justify-center rounded-[3px] border border-[#333] bg-[#1c1c1c] text-[#c4c4c4] transition hover:bg-[#292929]"
2039
- aria-label="Add attachment"
2040
- >
2041
- <Paperclip className="h-[15px] w-[15px]" strokeWidth={1.7} />
2042
- </button>
2043
- <div className="inline-flex h-[29px] items-center justify-center rounded-[3px] border border-[#333] bg-[#1c1c1c] px-[9px] text-[#8f8f8f]">
2044
- <Code2 className="h-[15px] w-[15px]" strokeWidth={1.7} />
2045
- </div>
2046
- <div className="hidden min-h-[29px] items-center rounded-[3px] bg-[#1c1c1c] px-[9px] py-[5px] text-[12px] leading-[18px] text-[#8f8f8f] sm:inline-flex">
2047
- {selectedReasoningLabel}
2048
- </div>
2049
- {hasLiveTerminalSession ? (
2050
- <div className="hidden min-h-[29px] items-center rounded-[3px] border border-[#333] bg-[#171717] px-[9px] py-[5px] text-[12px] leading-[18px] text-[#8f8f8f] sm:inline-flex">
2051
- Terminal keys: empty composer forwards Enter, Tab, arrows, Esc, Backspace. Ctrl+C interrupts.
2052
- </div>
2053
- ) : null}
2054
- </div>
2055
-
2056
- <button
2057
- type="button"
2058
- onClick={() => void (isSessionRunning ? handleInterrupt() : handleSend())}
2059
- disabled={interrupting || (!isSessionRunning && (sending || (!message.trim() && attachments.length === 0)))}
2060
- className={`inline-flex min-h-[38px] sm:min-h-[29px] items-center justify-center rounded-[3px] px-4 py-[6px] text-[16px] leading-[16px] transition disabled:cursor-not-allowed disabled:opacity-40 ${
2061
- isSessionRunning
2062
- ? "border border-[#603535] bg-[rgba(210,81,81,0.12)] text-[#f0b5b5] hover:bg-[rgba(210,81,81,0.18)]"
2063
- : "bg-[#292929] text-[#c4c4c4] hover:bg-[#313131]"
2064
- }`}
2065
- >
2066
- {interrupting ? (
2067
- <Loader2 className="h-4 w-4 animate-spin" />
2068
- ) : isSessionRunning ? (
2069
- <span className="inline-flex items-center gap-2">
2070
- <span>Stop</span>
2071
- <span className="hidden text-[11px] leading-[11px] text-[#8f8f8f] sm:inline">Ctrl+C</span>
2072
- </span>
2073
- ) : sending ? (
2074
- <Loader2 className="h-4 w-4 animate-spin" />
2075
- ) : (
2076
- "Send"
2077
- )}
2078
- </button>
2079
- </div>
2080
- </div>
2081
- </div>
2082
-
2083
- <SessionRuntimeStatusBar
2084
- agentName={normalizedAgentName}
2085
- sessionModel={sessionModel?.trim() || null}
2086
- sessionReasoningEffort={sessionReasoningEffort?.trim() || null}
2087
- runtimeStatus={runtimeStatus}
2088
- agents={agents}
2089
- />
2090
- </div>
2091
- </div>
2092
- </div>
2093
- </div>
2094
- );
2095
- }
2096
-
2097
- export default ChatPanel;