@jlongo78/agent-spaces 0.9.6 → 0.9.8

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 (917) hide show
  1. package/.next/standalone/.claude/settings.local.json +68 -0
  2. package/.next/standalone/.claude/spaces-env.json +1 -0
  3. package/.next/standalone/.next/BUILD_ID +1 -1
  4. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  5. package/.next/standalone/.next/build-manifest.json +2 -2
  6. package/.next/standalone/.next/prerender-manifest.json +3 -3
  7. package/.next/standalone/.next/required-server-files.json +19 -19
  8. package/.next/standalone/.next/routes-manifest.json +6 -0
  9. package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/(desktop)/admin/users/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/(desktop)/analytics/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/(desktop)/cortex/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/(desktop)/network/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/(desktop)/page_client-reference-manifest.js +1 -1
  15. package/.next/standalone/.next/server/app/(desktop)/projects/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/(desktop)/sessions/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/.next/server/app/(desktop)/settings/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/(desktop)/terminal/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/(desktop)/terminal/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/.next/server/app/(desktop)/workspaces/page_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  26. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  27. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  28. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  33. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  34. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  35. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  36. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  38. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  39. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/admin/analytics.html +1 -1
  42. package/.next/standalone/.next/server/app/admin/analytics.rsc +2 -2
  43. package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics/__PAGE__.segment.rsc +1 -1
  44. package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics.segment.rsc +1 -1
  45. package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin.segment.rsc +1 -1
  46. package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  47. package/.next/standalone/.next/server/app/admin/analytics.segments/_full.segment.rsc +2 -2
  48. package/.next/standalone/.next/server/app/admin/analytics.segments/_head.segment.rsc +1 -1
  49. package/.next/standalone/.next/server/app/admin/analytics.segments/_index.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/admin/analytics.segments/_tree.segment.rsc +2 -2
  51. package/.next/standalone/.next/server/app/admin/users.html +1 -1
  52. package/.next/standalone/.next/server/app/admin/users.rsc +2 -2
  53. package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users/__PAGE__.segment.rsc +1 -1
  54. package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users.segment.rsc +1 -1
  55. package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin.segment.rsc +1 -1
  56. package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  57. package/.next/standalone/.next/server/app/admin/users.segments/_full.segment.rsc +2 -2
  58. package/.next/standalone/.next/server/app/admin/users.segments/_head.segment.rsc +1 -1
  59. package/.next/standalone/.next/server/app/admin/users.segments/_index.segment.rsc +2 -2
  60. package/.next/standalone/.next/server/app/admin/users.segments/_tree.segment.rsc +2 -2
  61. package/.next/standalone/.next/server/app/analytics.html +1 -1
  62. package/.next/standalone/.next/server/app/analytics.rsc +2 -2
  63. package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics/__PAGE__.segment.rsc +1 -1
  64. package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics.segment.rsc +1 -1
  65. package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  66. package/.next/standalone/.next/server/app/analytics.segments/_full.segment.rsc +2 -2
  67. package/.next/standalone/.next/server/app/analytics.segments/_head.segment.rsc +1 -1
  68. package/.next/standalone/.next/server/app/analytics.segments/_index.segment.rsc +2 -2
  69. package/.next/standalone/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
  70. package/.next/standalone/.next/server/app/api/analytics/overview/route.js +1 -1
  71. package/.next/standalone/.next/server/app/api/analytics/overview/route.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/api/benchmark/lobes/route.js +1 -1
  73. package/.next/standalone/.next/server/app/api/benchmark/lobes/route.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/api/benchmark/run/route.js +1 -1
  75. package/.next/standalone/.next/server/app/api/benchmark/run/route.js.nft.json +1 -1
  76. package/.next/standalone/.next/server/app/api/benchmark/runs/[id]/route.js +1 -1
  77. package/.next/standalone/.next/server/app/api/benchmark/runs/[id]/route.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/api/benchmark/runs/route.js +1 -1
  79. package/.next/standalone/.next/server/app/api/benchmark/runs/route.js.nft.json +1 -1
  80. package/.next/standalone/.next/server/app/api/benchmark/status/route.js +1 -1
  81. package/.next/standalone/.next/server/app/api/benchmark/status/route.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/api/bulk/route.js +1 -1
  83. package/.next/standalone/.next/server/app/api/bulk/route.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/api/config/route.js +1 -1
  85. package/.next/standalone/.next/server/app/api/config/route.js.nft.json +1 -1
  86. package/.next/standalone/.next/server/app/api/cortex/context/route.js +1 -1
  87. package/.next/standalone/.next/server/app/api/cortex/context/route.js.nft.json +1 -1
  88. package/.next/standalone/.next/server/app/api/cortex/curation/assess/route.js +1 -1
  89. package/.next/standalone/.next/server/app/api/cortex/curation/assess/route.js.nft.json +1 -1
  90. package/.next/standalone/.next/server/app/api/cortex/curation/publish/route.js +1 -1
  91. package/.next/standalone/.next/server/app/api/cortex/curation/publish/route.js.nft.json +1 -1
  92. package/.next/standalone/.next/server/app/api/cortex/curation/refine/route.js +1 -1
  93. package/.next/standalone/.next/server/app/api/cortex/curation/refine/route.js.nft.json +1 -1
  94. package/.next/standalone/.next/server/app/api/cortex/curation/review/route.js +1 -1
  95. package/.next/standalone/.next/server/app/api/cortex/curation/review/route.js.nft.json +1 -1
  96. package/.next/standalone/.next/server/app/api/cortex/curation/seed/route.js +1 -1
  97. package/.next/standalone/.next/server/app/api/cortex/curation/seed/route.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/api/cortex/export/route.js +1 -1
  99. package/.next/standalone/.next/server/app/api/cortex/export/route.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/api/cortex/federation/pending/route.js +1 -1
  101. package/.next/standalone/.next/server/app/api/cortex/federation/pending/route.js.nft.json +1 -1
  102. package/.next/standalone/.next/server/app/api/cortex/federation/resolve/route.js +1 -1
  103. package/.next/standalone/.next/server/app/api/cortex/federation/resolve/route.js.nft.json +1 -1
  104. package/.next/standalone/.next/server/app/api/cortex/federation/search/route.js +1 -1
  105. package/.next/standalone/.next/server/app/api/cortex/federation/search/route.js.nft.json +1 -1
  106. package/.next/standalone/.next/server/app/api/cortex/federation/teach/route.js +1 -1
  107. package/.next/standalone/.next/server/app/api/cortex/federation/teach/route.js.nft.json +1 -1
  108. package/.next/standalone/.next/server/app/api/cortex/graph/edges/route.js +1 -1
  109. package/.next/standalone/.next/server/app/api/cortex/graph/edges/route.js.nft.json +1 -1
  110. package/.next/standalone/.next/server/app/api/cortex/graph/entities/[id]/route.js +1 -1
  111. package/.next/standalone/.next/server/app/api/cortex/graph/entities/[id]/route.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/api/cortex/graph/entities/route.js +1 -1
  113. package/.next/standalone/.next/server/app/api/cortex/graph/entities/route.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/api/cortex/graph/populate/route.js +1 -1
  115. package/.next/standalone/.next/server/app/api/cortex/graph/populate/route.js.nft.json +1 -1
  116. package/.next/standalone/.next/server/app/api/cortex/import/route.js +1 -1
  117. package/.next/standalone/.next/server/app/api/cortex/import/route.js.nft.json +1 -1
  118. package/.next/standalone/.next/server/app/api/cortex/import/status/route.js +1 -1
  119. package/.next/standalone/.next/server/app/api/cortex/import/status/route.js.nft.json +1 -1
  120. package/.next/standalone/.next/server/app/api/cortex/ingest/bootstrap/route.js +1 -1
  121. package/.next/standalone/.next/server/app/api/cortex/ingest/bootstrap/route.js.nft.json +1 -1
  122. package/.next/standalone/.next/server/app/api/cortex/ingest/status/route.js +1 -1
  123. package/.next/standalone/.next/server/app/api/cortex/ingest/status/route.js.nft.json +1 -1
  124. package/.next/standalone/.next/server/app/api/cortex/knowledge/[id]/route.js +1 -1
  125. package/.next/standalone/.next/server/app/api/cortex/knowledge/[id]/route.js.nft.json +1 -1
  126. package/.next/standalone/.next/server/app/api/cortex/knowledge/route.js +1 -1
  127. package/.next/standalone/.next/server/app/api/cortex/knowledge/route.js.nft.json +1 -1
  128. package/.next/standalone/.next/server/app/api/cortex/lobes/[id]/route.js +1 -1
  129. package/.next/standalone/.next/server/app/api/cortex/lobes/[id]/route.js.nft.json +1 -1
  130. package/.next/standalone/.next/server/app/api/cortex/lobes/route.js +1 -1
  131. package/.next/standalone/.next/server/app/api/cortex/lobes/route.js.nft.json +1 -1
  132. package/.next/standalone/.next/server/app/api/cortex/lobes/share/route.js +1 -1
  133. package/.next/standalone/.next/server/app/api/cortex/lobes/share/route.js.nft.json +1 -1
  134. package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route.js +1 -1
  135. package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route.js.nft.json +1 -1
  136. package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route.js +1 -1
  137. package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route.js.nft.json +1 -1
  138. package/.next/standalone/.next/server/app/api/cortex/mcp/call/route.js +1 -1
  139. package/.next/standalone/.next/server/app/api/cortex/mcp/call/route.js.nft.json +1 -1
  140. package/.next/standalone/.next/server/app/api/cortex/mcp/tools/route.js +1 -1
  141. package/.next/standalone/.next/server/app/api/cortex/mcp/tools/route.js.nft.json +1 -1
  142. package/.next/standalone/.next/server/app/api/cortex/search/route.js +1 -1
  143. package/.next/standalone/.next/server/app/api/cortex/search/route.js.nft.json +1 -1
  144. package/.next/standalone/.next/server/app/api/cortex/settings/route.js +1 -1
  145. package/.next/standalone/.next/server/app/api/cortex/settings/route.js.nft.json +1 -1
  146. package/.next/standalone/.next/server/app/api/cortex/status/route.js +1 -1
  147. package/.next/standalone/.next/server/app/api/cortex/status/route.js.nft.json +1 -1
  148. package/.next/standalone/.next/server/app/api/cortex/timeline/route.js +1 -1
  149. package/.next/standalone/.next/server/app/api/cortex/timeline/route.js.nft.json +1 -1
  150. package/.next/standalone/.next/server/app/api/cortex/usage/route.js +1 -1
  151. package/.next/standalone/.next/server/app/api/cortex/usage/route.js.nft.json +1 -1
  152. package/.next/standalone/.next/server/app/api/cortex/workspace/[id]/context/route.js +1 -1
  153. package/.next/standalone/.next/server/app/api/cortex/workspace/[id]/context/route.js.nft.json +1 -1
  154. package/.next/standalone/.next/server/app/api/events/route.js +1 -1
  155. package/.next/standalone/.next/server/app/api/events/route.js.nft.json +1 -1
  156. package/.next/standalone/.next/server/app/api/files/route.js +1 -1
  157. package/.next/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  158. package/.next/standalone/.next/server/app/api/folders/route.js +1 -1
  159. package/.next/standalone/.next/server/app/api/folders/route.js.nft.json +1 -1
  160. package/.next/standalone/.next/server/app/api/network/handshake/route.js +1 -1
  161. package/.next/standalone/.next/server/app/api/network/handshake/route.js.nft.json +1 -1
  162. package/.next/standalone/.next/server/app/api/network/panes/[id]/route.js +1 -1
  163. package/.next/standalone/.next/server/app/api/network/panes/[id]/route.js.nft.json +1 -1
  164. package/.next/standalone/.next/server/app/api/network/panes/route.js +1 -1
  165. package/.next/standalone/.next/server/app/api/network/panes/route.js.nft.json +1 -1
  166. package/.next/standalone/.next/server/app/api/network/projects/route.js +1 -1
  167. package/.next/standalone/.next/server/app/api/network/projects/route.js.nft.json +1 -1
  168. package/.next/standalone/.next/server/app/api/network/search/route.js +1 -1
  169. package/.next/standalone/.next/server/app/api/network/search/route.js.nft.json +1 -1
  170. package/.next/standalone/.next/server/app/api/network/sessions/[id]/messages/route.js +1 -1
  171. package/.next/standalone/.next/server/app/api/network/sessions/[id]/messages/route.js.nft.json +1 -1
  172. package/.next/standalone/.next/server/app/api/network/sessions/[id]/route.js +1 -1
  173. package/.next/standalone/.next/server/app/api/network/sessions/[id]/route.js.nft.json +1 -1
  174. package/.next/standalone/.next/server/app/api/network/sessions/route.js +1 -1
  175. package/.next/standalone/.next/server/app/api/network/sessions/route.js.nft.json +1 -1
  176. package/.next/standalone/.next/server/app/api/network/workspaces/[id]/route.js +1 -1
  177. package/.next/standalone/.next/server/app/api/network/workspaces/[id]/route.js.nft.json +1 -1
  178. package/.next/standalone/.next/server/app/api/network/workspaces/route.js +1 -1
  179. package/.next/standalone/.next/server/app/api/network/workspaces/route.js.nft.json +1 -1
  180. package/.next/standalone/.next/server/app/api/panes/[id]/diff/route.js +1 -1
  181. package/.next/standalone/.next/server/app/api/panes/[id]/diff/route.js.nft.json +1 -1
  182. package/.next/standalone/.next/server/app/api/panes/[id]/route.js +1 -1
  183. package/.next/standalone/.next/server/app/api/panes/[id]/route.js.nft.json +1 -1
  184. package/.next/standalone/.next/server/app/api/panes/route.js +1 -1
  185. package/.next/standalone/.next/server/app/api/panes/route.js.nft.json +1 -1
  186. package/.next/standalone/.next/server/app/api/projects/route.js +1 -1
  187. package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  188. package/.next/standalone/.next/server/app/api/proxy/models/[modelId]/[...path]/route.js +1 -1
  189. package/.next/standalone/.next/server/app/api/proxy/models/[modelId]/[...path]/route.js.nft.json +1 -1
  190. package/.next/standalone/.next/server/app/api/proxy/models/[modelId]/status/route.js +1 -1
  191. package/.next/standalone/.next/server/app/api/proxy/models/[modelId]/status/route.js.nft.json +1 -1
  192. package/.next/standalone/.next/server/app/api/search/route.js +2 -2
  193. package/.next/standalone/.next/server/app/api/search/route.js.nft.json +1 -1
  194. package/.next/standalone/.next/server/app/api/sessions/[id]/chat/route.js +1 -1
  195. package/.next/standalone/.next/server/app/api/sessions/[id]/chat/route.js.nft.json +1 -1
  196. package/.next/standalone/.next/server/app/api/sessions/[id]/messages/route.js +1 -1
  197. package/.next/standalone/.next/server/app/api/sessions/[id]/messages/route.js.nft.json +1 -1
  198. package/.next/standalone/.next/server/app/api/sessions/[id]/route.js +1 -1
  199. package/.next/standalone/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  200. package/.next/standalone/.next/server/app/api/sessions/route.js +2 -2
  201. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  202. package/.next/standalone/.next/server/app/api/sync/route.js +1 -1
  203. package/.next/standalone/.next/server/app/api/sync/route.js.nft.json +1 -1
  204. package/.next/standalone/.next/server/app/api/tags/route.js +1 -1
  205. package/.next/standalone/.next/server/app/api/tags/route.js.nft.json +1 -1
  206. package/.next/standalone/.next/server/app/api/tier/route.js +1 -1
  207. package/.next/standalone/.next/server/app/api/tier/route.js.nft.json +1 -1
  208. package/.next/standalone/.next/server/app/api/whisper/config/route.js +1 -1
  209. package/.next/standalone/.next/server/app/api/whisper/config/route.js.nft.json +1 -1
  210. package/.next/standalone/.next/server/app/api/whisper/route.js.nft.json +1 -1
  211. package/.next/standalone/.next/server/app/api/wizard/chart/route/app-paths-manifest.json +3 -0
  212. package/.next/standalone/.next/server/app/api/wizard/chart/route/build-manifest.json +11 -0
  213. package/.next/standalone/.next/server/app/api/wizard/chart/route/server-reference-manifest.json +4 -0
  214. package/.next/standalone/.next/server/app/api/wizard/chart/route.js +7 -0
  215. package/.next/standalone/.next/server/app/api/wizard/chart/route.js.map +5 -0
  216. package/.next/standalone/.next/server/app/api/wizard/chart/route.js.nft.json +1 -0
  217. package/.next/standalone/.next/server/app/api/wizard/chart/route_client-reference-manifest.js +2 -0
  218. package/.next/standalone/.next/server/app/api/wizard/chat/route.js +1 -1
  219. package/.next/standalone/.next/server/app/api/wizard/chat/route.js.nft.json +1 -1
  220. package/.next/standalone/.next/server/app/api/workspaces/[id]/context/[key]/route.js +1 -1
  221. package/.next/standalone/.next/server/app/api/workspaces/[id]/context/[key]/route.js.nft.json +1 -1
  222. package/.next/standalone/.next/server/app/api/workspaces/[id]/context/route.js +1 -1
  223. package/.next/standalone/.next/server/app/api/workspaces/[id]/context/route.js.nft.json +1 -1
  224. package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/[msgId]/route.js +1 -1
  225. package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/[msgId]/route.js.nft.json +1 -1
  226. package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/route.js +1 -1
  227. package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/route.js.nft.json +1 -1
  228. package/.next/standalone/.next/server/app/api/workspaces/[id]/route.js +1 -1
  229. package/.next/standalone/.next/server/app/api/workspaces/[id]/route.js.nft.json +1 -1
  230. package/.next/standalone/.next/server/app/api/workspaces/[id]/sessions/route.js +1 -1
  231. package/.next/standalone/.next/server/app/api/workspaces/[id]/sessions/route.js.nft.json +1 -1
  232. package/.next/standalone/.next/server/app/api/workspaces/route.js +2 -2
  233. package/.next/standalone/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  234. package/.next/standalone/.next/server/app/cortex.html +1 -1
  235. package/.next/standalone/.next/server/app/cortex.rsc +3 -3
  236. package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap/cortex/__PAGE__.segment.rsc +2 -2
  237. package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap/cortex.segment.rsc +1 -1
  238. package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  239. package/.next/standalone/.next/server/app/cortex.segments/_full.segment.rsc +3 -3
  240. package/.next/standalone/.next/server/app/cortex.segments/_head.segment.rsc +1 -1
  241. package/.next/standalone/.next/server/app/cortex.segments/_index.segment.rsc +2 -2
  242. package/.next/standalone/.next/server/app/cortex.segments/_tree.segment.rsc +2 -2
  243. package/.next/standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  244. package/.next/standalone/.next/server/app/login.html +1 -1
  245. package/.next/standalone/.next/server/app/login.rsc +2 -2
  246. package/.next/standalone/.next/server/app/login.segments/_full.segment.rsc +2 -2
  247. package/.next/standalone/.next/server/app/login.segments/_head.segment.rsc +1 -1
  248. package/.next/standalone/.next/server/app/login.segments/_index.segment.rsc +2 -2
  249. package/.next/standalone/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  250. package/.next/standalone/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
  251. package/.next/standalone/.next/server/app/login.segments/login.segment.rsc +1 -1
  252. package/.next/standalone/.next/server/app/m/page_client-reference-manifest.js +1 -1
  253. package/.next/standalone/.next/server/app/m/projects/page_client-reference-manifest.js +1 -1
  254. package/.next/standalone/.next/server/app/m/projects.html +1 -1
  255. package/.next/standalone/.next/server/app/m/projects.rsc +2 -2
  256. package/.next/standalone/.next/server/app/m/projects.segments/_full.segment.rsc +2 -2
  257. package/.next/standalone/.next/server/app/m/projects.segments/_head.segment.rsc +1 -1
  258. package/.next/standalone/.next/server/app/m/projects.segments/_index.segment.rsc +2 -2
  259. package/.next/standalone/.next/server/app/m/projects.segments/_tree.segment.rsc +2 -2
  260. package/.next/standalone/.next/server/app/m/projects.segments/m/projects/__PAGE__.segment.rsc +1 -1
  261. package/.next/standalone/.next/server/app/m/projects.segments/m/projects.segment.rsc +1 -1
  262. package/.next/standalone/.next/server/app/m/projects.segments/m.segment.rsc +1 -1
  263. package/.next/standalone/.next/server/app/m/sessions/[id]/page.js.nft.json +1 -1
  264. package/.next/standalone/.next/server/app/m/sessions/[id]/page_client-reference-manifest.js +1 -1
  265. package/.next/standalone/.next/server/app/m/sessions/page_client-reference-manifest.js +1 -1
  266. package/.next/standalone/.next/server/app/m/sessions.html +1 -1
  267. package/.next/standalone/.next/server/app/m/sessions.rsc +2 -2
  268. package/.next/standalone/.next/server/app/m/sessions.segments/_full.segment.rsc +2 -2
  269. package/.next/standalone/.next/server/app/m/sessions.segments/_head.segment.rsc +1 -1
  270. package/.next/standalone/.next/server/app/m/sessions.segments/_index.segment.rsc +2 -2
  271. package/.next/standalone/.next/server/app/m/sessions.segments/_tree.segment.rsc +2 -2
  272. package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions/__PAGE__.segment.rsc +1 -1
  273. package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions.segment.rsc +1 -1
  274. package/.next/standalone/.next/server/app/m/sessions.segments/m.segment.rsc +1 -1
  275. package/.next/standalone/.next/server/app/m/settings/page_client-reference-manifest.js +1 -1
  276. package/.next/standalone/.next/server/app/m/settings.html +1 -1
  277. package/.next/standalone/.next/server/app/m/settings.rsc +2 -2
  278. package/.next/standalone/.next/server/app/m/settings.segments/_full.segment.rsc +2 -2
  279. package/.next/standalone/.next/server/app/m/settings.segments/_head.segment.rsc +1 -1
  280. package/.next/standalone/.next/server/app/m/settings.segments/_index.segment.rsc +2 -2
  281. package/.next/standalone/.next/server/app/m/settings.segments/_tree.segment.rsc +2 -2
  282. package/.next/standalone/.next/server/app/m/settings.segments/m/settings/__PAGE__.segment.rsc +1 -1
  283. package/.next/standalone/.next/server/app/m/settings.segments/m/settings.segment.rsc +1 -1
  284. package/.next/standalone/.next/server/app/m/settings.segments/m.segment.rsc +1 -1
  285. package/.next/standalone/.next/server/app/m/terminal/page_client-reference-manifest.js +1 -1
  286. package/.next/standalone/.next/server/app/m/terminal.html +1 -1
  287. package/.next/standalone/.next/server/app/m/terminal.rsc +3 -3
  288. package/.next/standalone/.next/server/app/m/terminal.segments/_full.segment.rsc +3 -3
  289. package/.next/standalone/.next/server/app/m/terminal.segments/_head.segment.rsc +1 -1
  290. package/.next/standalone/.next/server/app/m/terminal.segments/_index.segment.rsc +2 -2
  291. package/.next/standalone/.next/server/app/m/terminal.segments/_tree.segment.rsc +2 -2
  292. package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal/__PAGE__.segment.rsc +2 -2
  293. package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal.segment.rsc +1 -1
  294. package/.next/standalone/.next/server/app/m/terminal.segments/m.segment.rsc +1 -1
  295. package/.next/standalone/.next/server/app/m.html +1 -1
  296. package/.next/standalone/.next/server/app/m.rsc +2 -2
  297. package/.next/standalone/.next/server/app/m.segments/_full.segment.rsc +2 -2
  298. package/.next/standalone/.next/server/app/m.segments/_head.segment.rsc +1 -1
  299. package/.next/standalone/.next/server/app/m.segments/_index.segment.rsc +2 -2
  300. package/.next/standalone/.next/server/app/m.segments/_tree.segment.rsc +2 -2
  301. package/.next/standalone/.next/server/app/m.segments/m/__PAGE__.segment.rsc +1 -1
  302. package/.next/standalone/.next/server/app/m.segments/m.segment.rsc +1 -1
  303. package/.next/standalone/.next/server/app/network.html +1 -1
  304. package/.next/standalone/.next/server/app/network.rsc +2 -2
  305. package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network/__PAGE__.segment.rsc +1 -1
  306. package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network.segment.rsc +1 -1
  307. package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  308. package/.next/standalone/.next/server/app/network.segments/_full.segment.rsc +2 -2
  309. package/.next/standalone/.next/server/app/network.segments/_head.segment.rsc +1 -1
  310. package/.next/standalone/.next/server/app/network.segments/_index.segment.rsc +2 -2
  311. package/.next/standalone/.next/server/app/network.segments/_tree.segment.rsc +2 -2
  312. package/.next/standalone/.next/server/app/projects.html +1 -1
  313. package/.next/standalone/.next/server/app/projects.rsc +2 -2
  314. package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects/__PAGE__.segment.rsc +1 -1
  315. package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects.segment.rsc +1 -1
  316. package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  317. package/.next/standalone/.next/server/app/projects.segments/_full.segment.rsc +2 -2
  318. package/.next/standalone/.next/server/app/projects.segments/_head.segment.rsc +1 -1
  319. package/.next/standalone/.next/server/app/projects.segments/_index.segment.rsc +2 -2
  320. package/.next/standalone/.next/server/app/projects.segments/_tree.segment.rsc +2 -2
  321. package/.next/standalone/.next/server/app/sessions.html +1 -1
  322. package/.next/standalone/.next/server/app/sessions.rsc +2 -2
  323. package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions/__PAGE__.segment.rsc +1 -1
  324. package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions.segment.rsc +1 -1
  325. package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  326. package/.next/standalone/.next/server/app/sessions.segments/_full.segment.rsc +2 -2
  327. package/.next/standalone/.next/server/app/sessions.segments/_head.segment.rsc +1 -1
  328. package/.next/standalone/.next/server/app/sessions.segments/_index.segment.rsc +2 -2
  329. package/.next/standalone/.next/server/app/sessions.segments/_tree.segment.rsc +2 -2
  330. package/.next/standalone/.next/server/app/settings.html +1 -1
  331. package/.next/standalone/.next/server/app/settings.rsc +2 -2
  332. package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings/__PAGE__.segment.rsc +1 -1
  333. package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings.segment.rsc +1 -1
  334. package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  335. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  336. package/.next/standalone/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  337. package/.next/standalone/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  338. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  339. package/.next/standalone/.next/server/app/terminal.html +1 -1
  340. package/.next/standalone/.next/server/app/terminal.rsc +3 -3
  341. package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal/__PAGE__.segment.rsc +2 -2
  342. package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal.segment.rsc +1 -1
  343. package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  344. package/.next/standalone/.next/server/app/terminal.segments/_full.segment.rsc +3 -3
  345. package/.next/standalone/.next/server/app/terminal.segments/_head.segment.rsc +1 -1
  346. package/.next/standalone/.next/server/app/terminal.segments/_index.segment.rsc +2 -2
  347. package/.next/standalone/.next/server/app/terminal.segments/_tree.segment.rsc +2 -2
  348. package/.next/standalone/.next/server/app/vr/page/react-loadable-manifest.json +1 -1
  349. package/.next/standalone/.next/server/app/vr/page_client-reference-manifest.js +1 -1
  350. package/.next/standalone/.next/server/app/vr.html +1 -1
  351. package/.next/standalone/.next/server/app/vr.rsc +3 -3
  352. package/.next/standalone/.next/server/app/vr.segments/_full.segment.rsc +3 -3
  353. package/.next/standalone/.next/server/app/vr.segments/_head.segment.rsc +1 -1
  354. package/.next/standalone/.next/server/app/vr.segments/_index.segment.rsc +2 -2
  355. package/.next/standalone/.next/server/app/vr.segments/_tree.segment.rsc +2 -2
  356. package/.next/standalone/.next/server/app/vr.segments/vr/__PAGE__.segment.rsc +2 -2
  357. package/.next/standalone/.next/server/app/vr.segments/vr.segment.rsc +1 -1
  358. package/.next/standalone/.next/server/app/workspaces.html +1 -1
  359. package/.next/standalone/.next/server/app/workspaces.rsc +2 -2
  360. package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces/__PAGE__.segment.rsc +1 -1
  361. package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces.segment.rsc +1 -1
  362. package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  363. package/.next/standalone/.next/server/app/workspaces.segments/_full.segment.rsc +2 -2
  364. package/.next/standalone/.next/server/app/workspaces.segments/_head.segment.rsc +1 -1
  365. package/.next/standalone/.next/server/app/workspaces.segments/_index.segment.rsc +2 -2
  366. package/.next/standalone/.next/server/app/workspaces.segments/_tree.segment.rsc +2 -2
  367. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  368. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00e90fc6._.js +98 -0
  369. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01ab8675._.js +98 -0
  370. package/.next/standalone/.next/server/chunks/[root-of-the-server]__03974f05._.js +98 -0
  371. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__bb331da9._.js → [root-of-the-server]__046c9b91._.js} +3 -3
  372. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04ae6bf0._.js +98 -0
  373. package/.next/standalone/.next/server/chunks/[root-of-the-server]__056fa416._.js +1 -1
  374. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ac4ea3f._.js +3 -0
  375. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b8e64cb._.js +98 -0
  376. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__d95165f0._.js → [root-of-the-server]__0facd39e._.js} +3 -3
  377. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10bc76a3._.js +3 -0
  378. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__9d68157b._.js → [root-of-the-server]__115f3934._.js} +3 -3
  379. package/.next/standalone/.next/server/chunks/[root-of-the-server]__11f155f1._.js +3 -0
  380. package/.next/standalone/.next/server/chunks/[root-of-the-server]__160e7c73._.js +22 -33
  381. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__91e23c96._.js → [root-of-the-server]__17a3b966._.js} +3 -3
  382. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__277d9445._.js → [root-of-the-server]__17d3a2b2._.js} +3 -3
  383. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1a86c055._.js +98 -0
  384. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20b5e9c4._.js +3 -0
  385. package/.next/standalone/.next/server/chunks/[root-of-the-server]__28d6fbd8._.js +98 -0
  386. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__04f04898._.js → [root-of-the-server]__2a3f866b._.js} +2 -2
  387. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__a95bb38b._.js → [root-of-the-server]__316617e7._.js} +2 -2
  388. package/.next/standalone/.next/server/chunks/[root-of-the-server]__32ad8f71._.js +98 -0
  389. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__6fe5e6c8._.js → [root-of-the-server]__35457394._.js} +2 -2
  390. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1f054c65._.js → [root-of-the-server]__35de78e6._.js} +3 -3
  391. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3685ffcb._.js +98 -0
  392. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__614458b7._.js → [root-of-the-server]__38954988._.js} +3 -3
  393. package/.next/standalone/.next/server/chunks/[root-of-the-server]__426ad936._.js +106 -0
  394. package/.next/standalone/.next/server/chunks/[root-of-the-server]__4985c034._.js +98 -0
  395. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5c6ce9ed._.js +98 -0
  396. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5cebe58a._.js +98 -0
  397. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5d5e4789._.js +98 -0
  398. package/.next/standalone/.next/server/chunks/[root-of-the-server]__65676930._.js +3 -0
  399. package/.next/standalone/.next/server/chunks/[root-of-the-server]__67cab326._.js +58 -0
  400. package/.next/standalone/.next/server/chunks/[root-of-the-server]__698c6f01._.js +98 -0
  401. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c64af29._.js +131 -0
  402. package/.next/standalone/.next/server/chunks/[root-of-the-server]__73aed9f5._.js +98 -0
  403. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__ac84b704._.js → [root-of-the-server]__79b6a9bb._.js} +3 -3
  404. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__e56abacf._.js → [root-of-the-server]__7db704c6._.js} +4 -4
  405. package/.next/standalone/.next/server/chunks/[root-of-the-server]__812ca02b._.js +98 -0
  406. package/.next/standalone/.next/server/chunks/[root-of-the-server]__821f50fa._.js +98 -0
  407. package/.next/standalone/.next/server/chunks/[root-of-the-server]__8716b86e._.js +98 -0
  408. package/.next/standalone/.next/server/chunks/[root-of-the-server]__884ef754._.js +98 -0
  409. package/.next/standalone/.next/server/chunks/[root-of-the-server]__88cdbd68._.js +98 -0
  410. package/.next/standalone/.next/server/chunks/[root-of-the-server]__89d9aba9._.js +98 -0
  411. package/.next/standalone/.next/server/chunks/[root-of-the-server]__8d536cb5._.js +98 -0
  412. package/.next/standalone/.next/server/chunks/[root-of-the-server]__8df8c5d1._.js +98 -0
  413. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__2d7a454e._.js → [root-of-the-server]__8f2ccc41._.js} +3 -3
  414. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__2a2c5fc5._.js → [root-of-the-server]__95c9d682._.js} +4 -4
  415. package/.next/standalone/.next/server/chunks/[root-of-the-server]__9e5d7774._.js +98 -0
  416. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__cc6e2885._.js → [root-of-the-server]__9edcff87._.js} +2 -2
  417. package/.next/standalone/.next/server/chunks/[root-of-the-server]__a049dfc2._.js +98 -0
  418. package/.next/standalone/.next/server/chunks/[root-of-the-server]__a5b4bb9a._.js +98 -0
  419. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__929ea03a._.js → [root-of-the-server]__a83262a1._.js} +2 -2
  420. package/.next/standalone/.next/server/chunks/[root-of-the-server]__a9cd1240._.js +98 -0
  421. package/.next/standalone/.next/server/chunks/[root-of-the-server]__a9d7f822._.js +98 -0
  422. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ad08c221._.js +98 -0
  423. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ad585f2f._.js +98 -0
  424. package/.next/standalone/.next/server/chunks/[root-of-the-server]__afcb8f7d._.js +98 -0
  425. package/.next/standalone/.next/server/chunks/[root-of-the-server]__bc250d43._.js +98 -0
  426. package/.next/standalone/.next/server/chunks/[root-of-the-server]__bce2a6e7._.js +98 -0
  427. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c011bf91._.js +98 -0
  428. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c0ac2895._.js +3 -0
  429. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c130a00c._.js +1 -1
  430. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c37d6380._.js +3 -0
  431. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cae392eb._.js +98 -0
  432. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cc2616bb._.js +3 -3
  433. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d501fa9b._.js +98 -0
  434. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d59c6c15._.js +98 -0
  435. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d5c1db32._.js +98 -0
  436. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d5d92527._.js +1 -1
  437. package/.next/standalone/.next/server/chunks/[root-of-the-server]__dba60c86._.js +98 -0
  438. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__2384f98e._.js → [root-of-the-server]__de14b9ae._.js} +3 -3
  439. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e10643d1._.js +98 -0
  440. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__00fdfbda._.js → [root-of-the-server]__e2a996e5._.js} +2 -2
  441. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__4d903941._.js → [root-of-the-server]__e3477417._.js} +3 -3
  442. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__32dc5513._.js → [root-of-the-server]__e4db362e._.js} +2 -2
  443. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__49e42a3a._.js → [root-of-the-server]__e4e70b86._.js} +3 -3
  444. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__ac39ecc7._.js → [root-of-the-server]__e54925af._.js} +3 -3
  445. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e8edc5b0._.js +98 -0
  446. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__eafd040b._.js → [root-of-the-server]__eab4d83b._.js} +2 -2
  447. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ead29015._.js +1 -1
  448. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__194955d4._.js → [root-of-the-server]__f056fd83._.js} +3 -3
  449. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f0e99572._.js +98 -0
  450. package/.next/standalone/.next/server/chunks/[root-of-the-server]__fe1e16d0._.js +98 -0
  451. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ff9cd277._.js +98 -0
  452. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ffaea2ce._.js +98 -0
  453. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wizard_chart_route_actions_888e2ec1.js +3 -0
  454. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__2cffc362._.js +3 -0
  455. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__f1050870._.js → [root-of-the-server]__47c97637._.js} +2 -2
  456. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__66aca5d4._.js +1 -1
  457. package/.next/standalone/.next/server/chunks/ssr/_17b946fd._.js +3 -0
  458. package/.next/standalone/.next/server/chunks/ssr/_2a1d79e7._.js +1 -1
  459. package/.next/standalone/.next/server/chunks/ssr/_5c3c4cfa._.js +7 -5
  460. package/.next/standalone/.next/server/chunks/ssr/_ba432382._.js +7 -5
  461. package/.next/standalone/.next/server/chunks/ssr/src_app_(desktop)_cortex_page_tsx_0f33d8b3._.js +1 -1
  462. package/.next/standalone/.next/server/chunks/ssr/src_app_(desktop)_terminal_page_tsx_de5e8d85._.js +4 -4
  463. package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__90eeddae._.js +1 -1
  464. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  465. package/.next/standalone/.next/server/pages/404.html +1 -1
  466. package/.next/standalone/.next/server/pages/500.html +2 -2
  467. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  468. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  469. package/.next/standalone/.next/static/chunks/0852575eb90c1e8d.js +85 -0
  470. package/.next/standalone/.next/static/chunks/{74d0ac0b1d0a1b79.js → 10b00b4f66102dcf.js} +1 -1
  471. package/.next/standalone/.next/static/chunks/{16eb77953dee9ea3.js → 19c71a8376c23c58.js} +1 -1
  472. package/.next/standalone/.next/static/chunks/2b769d1597e4fc1c.css +3 -0
  473. package/.next/standalone/.next/static/chunks/{3e91fc608659c524.js → 350271fe79509caf.js} +1 -1
  474. package/.next/standalone/.next/static/chunks/{70423c7afd8abf5f.js → 597847c22200c212.js} +1 -1
  475. package/.next/standalone/.next/static/chunks/{fb0abd1933b2b2e1.js → 7f6a14f1849fa94d.js} +1 -1
  476. package/.next/standalone/.next/static/chunks/{7ecd9bbb0ce4d68a.js → b7c8fe9b7275a84f.js} +1 -1
  477. package/.next/standalone/.next/static/chunks/{6245135a7afb8c7b.js → c9b10fc55516d142.js} +8 -6
  478. package/.next/standalone/.next/static/chunks/d0065f48eab94944.js +1 -0
  479. package/.next/standalone/.next/static/chunks/{180c1b9ff31b979f.js → f7b34c807badf95d.js} +8 -6
  480. package/.next/standalone/.spaces/cortex-context.md +50 -144
  481. package/.next/standalone/LICENSE +661 -661
  482. package/.next/standalone/NOTICE +5 -5
  483. package/.next/standalone/README.md +131 -131
  484. package/.next/standalone/bin/cortex-hook.js +79 -79
  485. package/.next/standalone/bin/cortex-hook.sh +62 -62
  486. package/.next/standalone/bin/cortex-learn-hook.js +138 -138
  487. package/.next/standalone/bin/cortex-mcp.js +60 -60
  488. package/.next/standalone/bin/cortex-pi-extension.ts +170 -170
  489. package/.next/standalone/bin/fix-standalone-externals.js +79 -79
  490. package/.next/standalone/bin/lib/auto-setup.js +110 -110
  491. package/.next/standalone/bin/mdns-service.js +171 -171
  492. package/.next/standalone/bin/postinstall.js +35 -35
  493. package/.next/standalone/bin/setup-admin.js +195 -195
  494. package/.next/standalone/bin/spaces-dev.js +247 -247
  495. package/.next/standalone/bin/spaces-install.js +660 -660
  496. package/.next/standalone/bin/spaces-postinstall.js +50 -50
  497. package/.next/standalone/bin/spaces-reset-totp.js +50 -50
  498. package/.next/standalone/bin/spaces-service.js +1046 -1046
  499. package/.next/standalone/bin/spaces-setup.js +253 -253
  500. package/.next/standalone/bin/spaces.js +808 -805
  501. package/.next/standalone/bin/ssh-auth-keys.sh +68 -68
  502. package/.next/standalone/bin/terminal-server.js +2819 -2781
  503. package/.next/standalone/cortex-hook-debug.log +57 -23
  504. package/.next/standalone/docker-compose.yml +28 -28
  505. package/.next/standalone/docs/architecture.md +387 -387
  506. package/.next/standalone/docs/cortex-integration-reference.md +268 -268
  507. package/.next/standalone/docs/cortex.md +293 -293
  508. package/.next/standalone/docs/getting-started.md +96 -96
  509. package/.next/standalone/docs/plans/2026-02-24-multi-agent-sessions-design.md +133 -133
  510. package/.next/standalone/docs/plans/2026-02-24-multi-agent-sessions-plan.md +959 -959
  511. package/.next/standalone/docs/plans/2026-03-02-security-audit.md +229 -229
  512. package/.next/standalone/docs/plans/2026-03-07-service-command-design.md +146 -146
  513. package/.next/standalone/docs/plans/2026-03-07-service-command-plan.md +254 -254
  514. package/.next/standalone/docs/server-install.md +564 -564
  515. package/.next/standalone/docs/social-card.html +150 -150
  516. package/.next/standalone/docs/superpowers/plans/2026-03-12-spaces-cortex.md +5270 -5270
  517. package/.next/standalone/docs/superpowers/plans/2026-03-13-cortex-wiring.md +1387 -1387
  518. package/.next/standalone/docs/superpowers/plans/2026-03-14-cortex-v2-entity-graph.md +1923 -1923
  519. package/.next/standalone/docs/superpowers/plans/2026-03-14-cortex-v2-knowledge-evolution.md +1113 -1113
  520. package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-boundary-engine.md +853 -853
  521. package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-context-engine.md +1274 -1274
  522. package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-signal-ingestion.md +933 -933
  523. package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-lobes.md +1080 -1080
  524. package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-v2-gravity-system.md +768 -768
  525. package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-v2-ui.md +1108 -1108
  526. package/.next/standalone/docs/superpowers/plans/2026-03-18-cortex-ui-integration.md +1846 -1846
  527. package/.next/standalone/docs/superpowers/plans/2026-03-19-vr-phase1-shell.md +1639 -1639
  528. package/.next/standalone/docs/superpowers/plans/2026-03-27-dockview-pane-layout.md +98 -98
  529. package/.next/standalone/docs/superpowers/specs/2026-03-11-universe-view-design.md +320 -320
  530. package/.next/standalone/docs/superpowers/specs/2026-03-12-spaces-brain-design.md +720 -720
  531. package/.next/standalone/docs/superpowers/specs/2026-03-13-cortex-wiring-design.md +268 -268
  532. package/.next/standalone/docs/superpowers/specs/2026-03-14-cortex-v2-design.md +623 -623
  533. package/.next/standalone/docs/superpowers/specs/2026-03-16-cortex-lobes-design.md +263 -263
  534. package/.next/standalone/docs/superpowers/specs/2026-03-16-cortex-v2-ui-design.md +240 -240
  535. package/.next/standalone/docs/superpowers/specs/2026-03-16-pane-ux-design.md +77 -77
  536. package/.next/standalone/docs/superpowers/specs/2026-03-18-cortex-ui-integration-design.md +341 -341
  537. package/.next/standalone/docs/superpowers/specs/2026-03-19-vr-phase1-shell-design.md +288 -288
  538. package/.next/standalone/docs/superpowers/specs/2026-03-27-pane-diff-review-and-project-wizard-design.md +322 -322
  539. package/.next/standalone/docs/tiers.md +104 -104
  540. package/.next/standalone/eslint.config.mjs +18 -18
  541. package/.next/standalone/next.config.ts +20 -20
  542. package/.next/standalone/nginx.conf +53 -53
  543. package/.next/standalone/node_modules/@img/sharp-win32-x64/lib/sharp-win32-x64.node +0 -0
  544. package/.next/standalone/node_modules/@img/{sharp-linux-x64 → sharp-win32-x64}/package.json +39 -46
  545. package/.next/standalone/package-lock.json +14985 -14986
  546. package/.next/standalone/package.json +111 -111
  547. package/.next/standalone/postcss.config.mjs +7 -7
  548. package/.next/standalone/scripts/rebuild.cmd +65 -65
  549. package/.next/standalone/scripts/rebuild.sh +59 -59
  550. package/.next/standalone/server.js +1 -1
  551. package/.next/standalone/src/app/(desktop)/admin/analytics/page.tsx +266 -266
  552. package/.next/standalone/src/app/(desktop)/admin/users/page.tsx +399 -399
  553. package/.next/standalone/src/app/(desktop)/analytics/page.tsx +166 -166
  554. package/.next/standalone/src/app/(desktop)/cortex/page.tsx +81 -81
  555. package/.next/standalone/src/app/(desktop)/dashboard-client.tsx +56 -56
  556. package/.next/standalone/src/app/(desktop)/layout.tsx +18 -18
  557. package/.next/standalone/src/app/(desktop)/network/page.tsx +137 -137
  558. package/.next/standalone/src/app/(desktop)/page.tsx +17 -17
  559. package/.next/standalone/src/app/(desktop)/projects/page.tsx +68 -68
  560. package/.next/standalone/src/app/(desktop)/sessions/[id]/page.tsx +519 -519
  561. package/.next/standalone/src/app/(desktop)/sessions/page.tsx +145 -145
  562. package/.next/standalone/src/app/(desktop)/settings/page.tsx +446 -446
  563. package/.next/standalone/src/app/(desktop)/terminal/layout.tsx +7 -7
  564. package/.next/standalone/src/app/(desktop)/terminal/page.tsx +1330 -1291
  565. package/.next/standalone/src/app/(desktop)/terminal/pane/[id]/page.tsx +211 -211
  566. package/.next/standalone/src/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page.tsx +252 -252
  567. package/.next/standalone/src/app/(desktop)/workspaces/page.tsx +12 -12
  568. package/.next/standalone/src/app/api/admin/analytics/route.ts +10 -10
  569. package/.next/standalone/src/app/api/admin/users/[id]/route.ts +20 -20
  570. package/.next/standalone/src/app/api/admin/users/route.ts +15 -15
  571. package/.next/standalone/src/app/api/analytics/overview/route.ts +80 -80
  572. package/.next/standalone/src/app/api/auth/login/route.ts +10 -10
  573. package/.next/standalone/src/app/api/auth/logout/route.ts +9 -9
  574. package/.next/standalone/src/app/api/auth/me/route.ts +22 -22
  575. package/.next/standalone/src/app/api/auth/totp/setup/route.ts +10 -10
  576. package/.next/standalone/src/app/api/auth/totp/status/route.ts +10 -10
  577. package/.next/standalone/src/app/api/auth/totp/verify/route.ts +10 -10
  578. package/.next/standalone/src/app/api/benchmark/lobes/route.ts +16 -16
  579. package/.next/standalone/src/app/api/benchmark/run/route.ts +113 -92
  580. package/.next/standalone/src/app/api/benchmark/runs/[id]/route.ts +26 -26
  581. package/.next/standalone/src/app/api/benchmark/runs/route.ts +16 -16
  582. package/.next/standalone/src/app/api/benchmark/status/route.ts +35 -35
  583. package/.next/standalone/src/app/api/bulk/route.ts +34 -34
  584. package/.next/standalone/src/app/api/chat/route.ts +85 -85
  585. package/.next/standalone/src/app/api/config/route.ts +30 -30
  586. package/.next/standalone/src/app/api/cortex/context/route.ts +78 -78
  587. package/.next/standalone/src/app/api/cortex/curation/assess/route.ts +27 -27
  588. package/.next/standalone/src/app/api/cortex/curation/publish/route.ts +23 -23
  589. package/.next/standalone/src/app/api/cortex/curation/refine/route.ts +23 -23
  590. package/.next/standalone/src/app/api/cortex/curation/review/route.ts +29 -29
  591. package/.next/standalone/src/app/api/cortex/curation/seed/route.ts +23 -23
  592. package/.next/standalone/src/app/api/cortex/export/route.ts +40 -40
  593. package/.next/standalone/src/app/api/cortex/federation/pending/route.ts +20 -20
  594. package/.next/standalone/src/app/api/cortex/federation/resolve/route.ts +43 -43
  595. package/.next/standalone/src/app/api/cortex/federation/search/route.ts +35 -35
  596. package/.next/standalone/src/app/api/cortex/federation/teach/route.ts +76 -76
  597. package/.next/standalone/src/app/api/cortex/graph/edges/route.ts +112 -112
  598. package/.next/standalone/src/app/api/cortex/graph/entities/[id]/route.ts +73 -73
  599. package/.next/standalone/src/app/api/cortex/graph/entities/route.ts +75 -75
  600. package/.next/standalone/src/app/api/cortex/graph/populate/route.ts +203 -203
  601. package/.next/standalone/src/app/api/cortex/import/route.ts +75 -75
  602. package/.next/standalone/src/app/api/cortex/import/status/route.ts +15 -15
  603. package/.next/standalone/src/app/api/cortex/ingest/bootstrap/route.ts +39 -39
  604. package/.next/standalone/src/app/api/cortex/ingest/status/route.ts +15 -15
  605. package/.next/standalone/src/app/api/cortex/knowledge/[id]/route.ts +91 -91
  606. package/.next/standalone/src/app/api/cortex/knowledge/route.ts +97 -97
  607. package/.next/standalone/src/app/api/cortex/lobes/[id]/route.ts +67 -67
  608. package/.next/standalone/src/app/api/cortex/lobes/route.ts +22 -22
  609. package/.next/standalone/src/app/api/cortex/lobes/share/route.ts +80 -80
  610. package/.next/standalone/src/app/api/cortex/marketplace/browse/route.ts +43 -43
  611. package/.next/standalone/src/app/api/cortex/marketplace/preview/route.ts +46 -46
  612. package/.next/standalone/src/app/api/cortex/mcp/call/route.ts +11 -11
  613. package/.next/standalone/src/app/api/cortex/mcp/tools/route.ts +8 -8
  614. package/.next/standalone/src/app/api/cortex/search/route.ts +57 -45
  615. package/.next/standalone/src/app/api/cortex/settings/route.ts +35 -35
  616. package/.next/standalone/src/app/api/cortex/status/route.ts +169 -169
  617. package/.next/standalone/src/app/api/cortex/timeline/route.ts +42 -42
  618. package/.next/standalone/src/app/api/cortex/usage/route.ts +31 -31
  619. package/.next/standalone/src/app/api/cortex/workspace/[id]/context/route.ts +41 -41
  620. package/.next/standalone/src/app/api/events/route.ts +40 -40
  621. package/.next/standalone/src/app/api/files/route.ts +187 -187
  622. package/.next/standalone/src/app/api/folders/route.ts +107 -97
  623. package/.next/standalone/src/app/api/network/connect-callback/route.ts +11 -11
  624. package/.next/standalone/src/app/api/network/connect-request/[id]/route.ts +11 -11
  625. package/.next/standalone/src/app/api/network/connect-request/route.ts +17 -17
  626. package/.next/standalone/src/app/api/network/discovered/route.ts +9 -9
  627. package/.next/standalone/src/app/api/network/handshake/route.ts +25 -25
  628. package/.next/standalone/src/app/api/network/health/route.ts +10 -10
  629. package/.next/standalone/src/app/api/network/identity/route.ts +15 -15
  630. package/.next/standalone/src/app/api/network/keys/[id]/route.ts +10 -10
  631. package/.next/standalone/src/app/api/network/keys/route.ts +15 -15
  632. package/.next/standalone/src/app/api/network/nodes/[id]/route.ts +15 -15
  633. package/.next/standalone/src/app/api/network/nodes/check/route.ts +9 -9
  634. package/.next/standalone/src/app/api/network/nodes/route.ts +15 -15
  635. package/.next/standalone/src/app/api/network/panes/[id]/route.ts +78 -62
  636. package/.next/standalone/src/app/api/network/panes/route.ts +61 -50
  637. package/.next/standalone/src/app/api/network/projects/route.ts +32 -25
  638. package/.next/standalone/src/app/api/network/proxy/[nodeId]/[...path]/route.ts +25 -25
  639. package/.next/standalone/src/app/api/network/search/route.ts +45 -38
  640. package/.next/standalone/src/app/api/network/sessions/[id]/messages/route.ts +43 -36
  641. package/.next/standalone/src/app/api/network/sessions/[id]/route.ts +41 -34
  642. package/.next/standalone/src/app/api/network/sessions/route.ts +50 -43
  643. package/.next/standalone/src/app/api/network/terminal/token/route.ts +10 -10
  644. package/.next/standalone/src/app/api/network/workspaces/[id]/route.ts +80 -71
  645. package/.next/standalone/src/app/api/network/workspaces/route.ts +87 -85
  646. package/.next/standalone/src/app/api/panes/[id]/diff/route.ts +121 -121
  647. package/.next/standalone/src/app/api/panes/[id]/route.ts +60 -60
  648. package/.next/standalone/src/app/api/panes/route.ts +39 -39
  649. package/.next/standalone/src/app/api/projects/route.ts +13 -13
  650. package/.next/standalone/src/app/api/proxy/models/[modelId]/[...path]/route.ts +80 -80
  651. package/.next/standalone/src/app/api/proxy/models/[modelId]/status/route.ts +33 -33
  652. package/.next/standalone/src/app/api/search/route.ts +47 -47
  653. package/.next/standalone/src/app/api/sessions/[id]/chat/route.ts +120 -120
  654. package/.next/standalone/src/app/api/sessions/[id]/messages/route.ts +34 -34
  655. package/.next/standalone/src/app/api/sessions/[id]/route.ts +73 -73
  656. package/.next/standalone/src/app/api/sessions/route.ts +64 -64
  657. package/.next/standalone/src/app/api/sync/route.ts +24 -24
  658. package/.next/standalone/src/app/api/tags/route.ts +35 -35
  659. package/.next/standalone/src/app/api/tier/route.ts +16 -16
  660. package/.next/standalone/src/app/api/updates/route.ts +65 -65
  661. package/.next/standalone/src/app/api/whisper/config/route.ts +50 -42
  662. package/.next/standalone/src/app/api/whisper/route.ts +91 -91
  663. package/.next/standalone/src/app/api/wizard/chart/route.ts +129 -0
  664. package/.next/standalone/src/app/api/wizard/chat/route.ts +113 -113
  665. package/.next/standalone/src/app/api/workspaces/[id]/context/[key]/route.ts +39 -39
  666. package/.next/standalone/src/app/api/workspaces/[id]/context/route.ts +28 -28
  667. package/.next/standalone/src/app/api/workspaces/[id]/messages/[msgId]/route.ts +17 -17
  668. package/.next/standalone/src/app/api/workspaces/[id]/messages/route.ts +39 -39
  669. package/.next/standalone/src/app/api/workspaces/[id]/route.ts +47 -47
  670. package/.next/standalone/src/app/api/workspaces/[id]/sessions/route.ts +62 -62
  671. package/.next/standalone/src/app/api/workspaces/route.ts +79 -79
  672. package/.next/standalone/src/app/globals.css +88 -88
  673. package/.next/standalone/src/app/layout.tsx +33 -33
  674. package/.next/standalone/src/app/login/layout.tsx +7 -7
  675. package/.next/standalone/src/app/login/page.tsx +315 -315
  676. package/.next/standalone/src/app/m/layout.tsx +16 -16
  677. package/.next/standalone/src/app/m/page.tsx +118 -118
  678. package/.next/standalone/src/app/m/projects/page.tsx +64 -64
  679. package/.next/standalone/src/app/m/sessions/[id]/page.tsx +168 -168
  680. package/.next/standalone/src/app/m/sessions/page.tsx +177 -177
  681. package/.next/standalone/src/app/m/settings/page.tsx +230 -230
  682. package/.next/standalone/src/app/m/terminal/page.tsx +413 -413
  683. package/.next/standalone/src/app/vr/page.tsx +21 -21
  684. package/.next/standalone/src/app/vr/vr-app.tsx +163 -163
  685. package/.next/standalone/src/app/vr/vr-controls.tsx +139 -139
  686. package/.next/standalone/src/app/vr/vr-door.tsx +82 -82
  687. package/.next/standalone/src/app/vr/vr-environment.tsx +71 -71
  688. package/.next/standalone/src/app/vr/vr-gaze.tsx +89 -89
  689. package/.next/standalone/src/app/vr/vr-layout.ts +49 -49
  690. package/.next/standalone/src/app/vr/vr-lobby.tsx +97 -97
  691. package/.next/standalone/src/app/vr/vr-pane.tsx +195 -195
  692. package/.next/standalone/src/app/vr/vr-room.tsx +79 -79
  693. package/.next/standalone/src/app/vr/vr-terminal.tsx +303 -303
  694. package/.next/standalone/src/components/auth/totp-gate.tsx +183 -183
  695. package/.next/standalone/src/components/bus/activity-panel.tsx +261 -261
  696. package/.next/standalone/src/components/common/color-picker.tsx +35 -35
  697. package/.next/standalone/src/components/common/dev-directory-picker.tsx +339 -339
  698. package/.next/standalone/src/components/common/folder-picker.tsx +200 -200
  699. package/.next/standalone/src/components/common/tag-picker.tsx +190 -190
  700. package/.next/standalone/src/components/common/workspace-picker.tsx +113 -113
  701. package/.next/standalone/src/components/cortex/benchmark-tab.tsx +894 -880
  702. package/.next/standalone/src/components/cortex/constants.ts +29 -29
  703. package/.next/standalone/src/components/cortex/cortex-dashboard.tsx +304 -304
  704. package/.next/standalone/src/components/cortex/cortex-indicator.tsx +44 -44
  705. package/.next/standalone/src/components/cortex/cortex-panel.tsx +140 -140
  706. package/.next/standalone/src/components/cortex/cortex-settings.tsx +280 -280
  707. package/.next/standalone/src/components/cortex/curation-tab.tsx +810 -810
  708. package/.next/standalone/src/components/cortex/entity-detail.tsx +101 -101
  709. package/.next/standalone/src/components/cortex/entity-graph.tsx +382 -382
  710. package/.next/standalone/src/components/cortex/import-dialog.tsx +212 -212
  711. package/.next/standalone/src/components/cortex/injection-badge.tsx +72 -72
  712. package/.next/standalone/src/components/cortex/knowledge-card.tsx +109 -109
  713. package/.next/standalone/src/components/cortex/knowledge-tab.tsx +158 -158
  714. package/.next/standalone/src/components/cortex/lobe-settings.tsx +215 -215
  715. package/.next/standalone/src/components/cortex/marketplace-card.tsx +126 -126
  716. package/.next/standalone/src/components/cortex/marketplace-tab.tsx +113 -113
  717. package/.next/standalone/src/components/dashboard/activity-chart.tsx +41 -41
  718. package/.next/standalone/src/components/dashboard/model-usage-chart.tsx +61 -61
  719. package/.next/standalone/src/components/dashboard/recent-sessions.tsx +68 -68
  720. package/.next/standalone/src/components/dashboard/stats-cards.tsx +36 -36
  721. package/.next/standalone/src/components/files/file-explorer.tsx +703 -703
  722. package/.next/standalone/src/components/layout/providers.tsx +38 -38
  723. package/.next/standalone/src/components/layout/sidebar.tsx +170 -170
  724. package/.next/standalone/src/components/layout/tier-provider.tsx +53 -53
  725. package/.next/standalone/src/components/layout/update-banner.tsx +92 -92
  726. package/.next/standalone/src/components/mobile/bottom-nav.tsx +46 -46
  727. package/.next/standalone/src/components/mobile/immersive-voice-button.tsx +123 -123
  728. package/.next/standalone/src/components/mobile/mobile-chat-input.tsx +244 -244
  729. package/.next/standalone/src/components/mobile/mobile-header.tsx +44 -44
  730. package/.next/standalone/src/components/mobile/mobile-session-card.tsx +56 -56
  731. package/.next/standalone/src/components/mobile/mobile-terminal-input.tsx +74 -74
  732. package/.next/standalone/src/components/mobile/mobile-terminal-pane.tsx +302 -302
  733. package/.next/standalone/src/components/mobile/mobile-terminal-toolbar.tsx +76 -76
  734. package/.next/standalone/src/components/mobile/pull-to-refresh.tsx +82 -82
  735. package/.next/standalone/src/components/mobile/voice-input.tsx +53 -53
  736. package/.next/standalone/src/components/network/api-key-list.tsx +190 -190
  737. package/.next/standalone/src/components/network/connection-requests.tsx +94 -94
  738. package/.next/standalone/src/components/network/node-add-dialog.tsx +131 -131
  739. package/.next/standalone/src/components/network/node-badge.tsx +26 -26
  740. package/.next/standalone/src/components/network/node-list.tsx +207 -207
  741. package/.next/standalone/src/components/network/node-selector.tsx +49 -49
  742. package/.next/standalone/src/components/sessions/session-filters.tsx +116 -116
  743. package/.next/standalone/src/components/sessions/session-list.tsx +485 -485
  744. package/.next/standalone/src/components/terminal/pane-diff-panel.tsx +179 -179
  745. package/.next/standalone/src/components/terminal/terminal-pane.tsx +1530 -1464
  746. package/.next/standalone/src/components/viewer/chat-input.tsx +275 -275
  747. package/.next/standalone/src/components/viewer/message-renderer.tsx +551 -551
  748. package/.next/standalone/src/components/wizard/chart-wizard.tsx +405 -0
  749. package/.next/standalone/src/components/wizard/project-wizard.tsx +153 -153
  750. package/.next/standalone/src/components/wizard/wizard-chat.tsx +99 -99
  751. package/.next/standalone/src/components/wizard/wizard-plan-summary.tsx +103 -103
  752. package/.next/standalone/src/components/wizard/wizard-review.tsx +225 -225
  753. package/.next/standalone/src/components/workspace/universe-cluster.tsx +131 -131
  754. package/.next/standalone/src/components/workspace/universe-orb.tsx +128 -128
  755. package/.next/standalone/src/components/workspace/universe-types.ts +22 -22
  756. package/.next/standalone/src/components/workspace/universe-utils.ts +11 -11
  757. package/.next/standalone/src/components/workspace/universe-view.tsx +397 -397
  758. package/.next/standalone/src/components/workspace/workspace-chooser.tsx +634 -634
  759. package/.next/standalone/src/hooks/use-benchmark.ts +72 -71
  760. package/.next/standalone/src/hooks/use-bus.ts +147 -147
  761. package/.next/standalone/src/hooks/use-idle-detection.ts +79 -79
  762. package/.next/standalone/src/hooks/use-network.ts +229 -229
  763. package/.next/standalone/src/hooks/use-sessions.ts +437 -437
  764. package/.next/standalone/src/hooks/use-speech-recognition.ts +114 -113
  765. package/.next/standalone/src/hooks/use-sse.ts +35 -35
  766. package/.next/standalone/src/hooks/use-tier.ts +39 -39
  767. package/.next/standalone/src/lib/agents.ts +97 -97
  768. package/.next/standalone/src/lib/aider/parser.ts +111 -111
  769. package/.next/standalone/src/lib/api.ts +19 -19
  770. package/.next/standalone/src/lib/auth.ts +47 -47
  771. package/.next/standalone/src/lib/claude/parser.ts +212 -212
  772. package/.next/standalone/src/lib/claude/stats.ts +204 -204
  773. package/.next/standalone/src/lib/codex/parser.test.ts +111 -111
  774. package/.next/standalone/src/lib/codex/parser.ts +287 -287
  775. package/.next/standalone/src/lib/config.ts +132 -132
  776. package/.next/standalone/src/lib/cortex/benchmark.ts +83 -67
  777. package/.next/standalone/src/lib/cortex/config.ts +42 -42
  778. package/.next/standalone/src/lib/cortex/debug.ts +10 -10
  779. package/.next/standalone/src/lib/cortex/distillation/usage-store.ts +18 -18
  780. package/.next/standalone/src/lib/cortex/graph/resolver.ts +10 -10
  781. package/.next/standalone/src/lib/cortex/graph/types.ts +22 -22
  782. package/.next/standalone/src/lib/cortex/index.ts +109 -56
  783. package/.next/standalone/src/lib/cortex/ingestion/bootstrap.ts +14 -14
  784. package/.next/standalone/src/lib/cortex/knowledge/compat.ts +14 -14
  785. package/.next/standalone/src/lib/cortex/knowledge/contradiction.ts +10 -10
  786. package/.next/standalone/src/lib/cortex/knowledge/types.ts +67 -67
  787. package/.next/standalone/src/lib/cortex/lobes/config.ts +16 -16
  788. package/.next/standalone/src/lib/cortex/lobes/resolver.ts +8 -8
  789. package/.next/standalone/src/lib/cortex/lobes/shares.ts +14 -14
  790. package/.next/standalone/src/lib/cortex/mcp/server.ts +12 -12
  791. package/.next/standalone/src/lib/cortex/portability/exporter.ts +6 -6
  792. package/.next/standalone/src/lib/cortex/portability/importer.ts +10 -10
  793. package/.next/standalone/src/lib/cortex/retrieval/context-engine.ts +10 -10
  794. package/.next/standalone/src/lib/cortex/types.ts +39 -39
  795. package/.next/standalone/src/lib/cost-calculator.ts +48 -48
  796. package/.next/standalone/src/lib/db/init.ts +71 -71
  797. package/.next/standalone/src/lib/db/queries.ts +740 -827
  798. package/.next/standalone/src/lib/db/schema.ts +206 -206
  799. package/.next/standalone/src/lib/events/sse.ts +36 -36
  800. package/.next/standalone/src/lib/forge/parser.ts +52 -52
  801. package/.next/standalone/src/lib/gemini/parser.ts +258 -258
  802. package/.next/standalone/src/lib/license.ts +56 -56
  803. package/.next/standalone/src/lib/pro.ts +31 -31
  804. package/.next/standalone/src/lib/shell-user.ts +101 -0
  805. package/.next/standalone/src/lib/sync/indexer.ts +504 -504
  806. package/.next/standalone/src/lib/sync/watcher.ts +64 -64
  807. package/.next/standalone/src/lib/teams.ts +31 -31
  808. package/.next/standalone/src/lib/telemetry.ts +75 -75
  809. package/.next/standalone/src/lib/terminal/server.ts +188 -188
  810. package/.next/standalone/src/lib/tier.ts +38 -38
  811. package/.next/standalone/src/lib/utils.ts +72 -72
  812. package/.next/standalone/src/lib/vms/manager.ts +121 -121
  813. package/.next/standalone/src/middleware.ts +133 -133
  814. package/.next/standalone/src/types/claude.ts +208 -208
  815. package/.next/standalone/src/types/network.ts +61 -61
  816. package/.next/standalone/tests/setup.ts +8 -8
  817. package/.next/standalone/tsconfig.json +34 -34
  818. package/.next/standalone/vitest.config.ts +24 -24
  819. package/LICENSE +661 -661
  820. package/README.md +131 -131
  821. package/bin/cortex-hook.js +79 -79
  822. package/bin/cortex-hook.sh +62 -62
  823. package/bin/cortex-learn-hook.js +138 -138
  824. package/bin/cortex-mcp.js +60 -60
  825. package/bin/cortex-pi-extension.ts +170 -170
  826. package/bin/fix-standalone-externals.js +79 -79
  827. package/bin/lib/auto-setup.js +110 -110
  828. package/bin/mdns-service.js +171 -171
  829. package/bin/postinstall.js +35 -35
  830. package/bin/setup-admin.js +195 -195
  831. package/bin/spaces-dev.js +247 -247
  832. package/bin/spaces-install.js +660 -660
  833. package/bin/spaces-postinstall.js +50 -50
  834. package/bin/spaces-reset-totp.js +50 -50
  835. package/bin/spaces-service.js +1046 -1046
  836. package/bin/spaces-setup.js +253 -253
  837. package/bin/spaces.js +808 -805
  838. package/bin/ssh-auth-keys.sh +68 -68
  839. package/bin/terminal-server.js +2819 -2781
  840. package/package.json +111 -111
  841. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0903a426._.js +0 -98
  842. package/.next/standalone/.next/server/chunks/[root-of-the-server]__09e8ccc9._.js +0 -98
  843. package/.next/standalone/.next/server/chunks/[root-of-the-server]__11c684b1._.js +0 -98
  844. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1572d4ef._.js +0 -98
  845. package/.next/standalone/.next/server/chunks/[root-of-the-server]__186cd0bb._.js +0 -3
  846. package/.next/standalone/.next/server/chunks/[root-of-the-server]__212760e6._.js +0 -98
  847. package/.next/standalone/.next/server/chunks/[root-of-the-server]__228595ec._.js +0 -98
  848. package/.next/standalone/.next/server/chunks/[root-of-the-server]__283c890f._.js +0 -3
  849. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2f300a68._.js +0 -98
  850. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2f452778._.js +0 -98
  851. package/.next/standalone/.next/server/chunks/[root-of-the-server]__35f8e77e._.js +0 -98
  852. package/.next/standalone/.next/server/chunks/[root-of-the-server]__379fc2e9._.js +0 -98
  853. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3b40d79f._.js +0 -98
  854. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3d3dca2b._.js +0 -98
  855. package/.next/standalone/.next/server/chunks/[root-of-the-server]__4d5b78d2._.js +0 -98
  856. package/.next/standalone/.next/server/chunks/[root-of-the-server]__54163e52._.js +0 -98
  857. package/.next/standalone/.next/server/chunks/[root-of-the-server]__563c0817._.js +0 -3
  858. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5812f90a._.js +0 -98
  859. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5c5e87f5._.js +0 -98
  860. package/.next/standalone/.next/server/chunks/[root-of-the-server]__60d15b16._.js +0 -98
  861. package/.next/standalone/.next/server/chunks/[root-of-the-server]__69d315e5._.js +0 -3
  862. package/.next/standalone/.next/server/chunks/[root-of-the-server]__71f29038._.js +0 -98
  863. package/.next/standalone/.next/server/chunks/[root-of-the-server]__74084e07._.js +0 -3
  864. package/.next/standalone/.next/server/chunks/[root-of-the-server]__7921aa80._.js +0 -98
  865. package/.next/standalone/.next/server/chunks/[root-of-the-server]__7e077dd8._.js +0 -98
  866. package/.next/standalone/.next/server/chunks/[root-of-the-server]__7ebc4280._.js +0 -131
  867. package/.next/standalone/.next/server/chunks/[root-of-the-server]__857c60bb._.js +0 -98
  868. package/.next/standalone/.next/server/chunks/[root-of-the-server]__874fe565._.js +0 -98
  869. package/.next/standalone/.next/server/chunks/[root-of-the-server]__8e2171f7._.js +0 -98
  870. package/.next/standalone/.next/server/chunks/[root-of-the-server]__95659b2d._.js +0 -98
  871. package/.next/standalone/.next/server/chunks/[root-of-the-server]__9679b91e._.js +0 -98
  872. package/.next/standalone/.next/server/chunks/[root-of-the-server]__a90729a1._.js +0 -98
  873. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ad4346fa._.js +0 -98
  874. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b0862d69._.js +0 -98
  875. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b43306ee._.js +0 -98
  876. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b689ff5e._.js +0 -106
  877. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ba87daaa._.js +0 -98
  878. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c0461005._.js +0 -98
  879. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c1deb5f3._.js +0 -98
  880. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c8a62f42._.js +0 -98
  881. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cabaac2b._.js +0 -98
  882. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cb027619._.js +0 -98
  883. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cf608218._.js +0 -98
  884. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cfc1290d._.js +0 -98
  885. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d0109b9b._.js +0 -98
  886. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d0125483._.js +0 -3
  887. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d048ee6b._.js +0 -98
  888. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d12644e7._.js +0 -98
  889. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e3cc946c._.js +0 -98
  890. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e6fd27f8._.js +0 -98
  891. package/.next/standalone/.next/server/chunks/[root-of-the-server]__efb8251e._.js +0 -98
  892. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f44c6882._.js +0 -98
  893. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f85283de._.js +0 -98
  894. package/.next/standalone/.next/server/chunks/[root-of-the-server]__feceb3e4._.js +0 -98
  895. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__843070a6._.js +0 -3
  896. package/.next/standalone/.next/server/chunks/ssr/_e84a0c06._.js +0 -3
  897. package/.next/standalone/.next/static/chunks/470cade58d4eceeb.css +0 -3
  898. package/.next/standalone/.next/static/chunks/9d4164833c2c1fd6.js +0 -85
  899. package/.next/standalone/.next/static/chunks/f091f4bf8d80fd07.js +0 -1
  900. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/README.md +0 -46
  901. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h +0 -221
  902. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +0 -1
  903. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  904. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/package.json +0 -42
  905. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
  906. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +0 -221
  907. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
  908. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  909. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
  910. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
  911. package/.next/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
  912. package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  913. package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
  914. /package/.next/standalone/.next/static/{5S4TviTCiNiTjf6KjXjBo → u1pHON3drz1mBi7owkbBP}/_buildManifest.js +0 -0
  915. /package/.next/standalone/.next/static/{5S4TviTCiNiTjf6KjXjBo → u1pHON3drz1mBi7owkbBP}/_clientMiddlewareManifest.json +0 -0
  916. /package/.next/standalone/.next/static/{5S4TviTCiNiTjf6KjXjBo → u1pHON3drz1mBi7owkbBP}/_ssgManifest.js +0 -0
  917. /package/.next/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-win32-x64}/versions.json +0 -0
@@ -1,1464 +1,1530 @@
1
- 'use client';
2
-
3
- import { useEffect, useRef, useState, useCallback } from 'react';
4
- import { createPortal } from 'react-dom';
5
- import { X, Pencil, Check, RotateCcw, Maximize2, Minimize2, ExternalLink, Globe, Users, Mic, MicOff, Upload, AudioLines, Minus, GripVertical, Settings, GitCompareArrows } from 'lucide-react';
6
- import { PaneDiffPanel } from './pane-diff-panel';
7
- import { cn } from '@/lib/utils';
8
- import { AGENT_TYPES } from '@/lib/agents';
9
- import { useTier } from '@/hooks/use-tier';
10
- import { InjectionBadge } from '@/components/cortex/injection-badge';
11
-
12
- import type { PaneData } from '@/lib/db/queries';
13
- import 'xterm/css/xterm.css';
14
-
15
- const WS_PATH = process.env.NEXT_PUBLIC_WS_PATH;
16
-
17
- interface TerminalPaneProps {
18
- pane: PaneData;
19
- onClose: (id: string) => void;
20
- onUpdate: (id: string, data: Partial<PaneData>) => void;
21
- isMaximized: boolean;
22
- onToggleMaximize: (id: string) => void;
23
- onMinimize?: (id: string) => void;
24
- onPopout?: (id: string) => void;
25
- onBrowse?: (cwd: string) => void;
26
- isPopout?: boolean;
27
- terminalToken?: string;
28
- workspaceCollaboration?: boolean;
29
- dragHandleProps?: Record<string, any>;
30
- }
31
-
32
- export function TerminalPane({ pane, onClose, onUpdate, isMaximized, onToggleMaximize, onMinimize, onPopout, onBrowse, isPopout, terminalToken, workspaceCollaboration, dragHandleProps }: TerminalPaneProps) {
33
- const { hasCortex } = useTier();
34
- const termRef = useRef<HTMLDivElement>(null);
35
- const xtermRef = useRef<any>(null);
36
- const wsRef = useRef<WebSocket | null>(null);
37
- const fitRef = useRef<any>(null);
38
-
39
- // RAF-batched write queue — coalesces rapid WebSocket messages into single frame updates
40
- // to prevent scroll jitter from hundreds of write() calls per second
41
- const writeQueueRef = useRef('');
42
- const writeRafRef = useRef<number | null>(null);
43
-
44
- const queueWrite = (data: string) => {
45
- writeQueueRef.current += data;
46
- if (writeRafRef.current === null) {
47
- writeRafRef.current = requestAnimationFrame(() => {
48
- writeRafRef.current = null;
49
- let queued = writeQueueRef.current;
50
- writeQueueRef.current = '';
51
- if (queued && xtermRef.current) {
52
- // Strip \x1b[3J (clear scrollback) — Claude Code sends this when re-rendering
53
- // its UI, which teleports the viewport to the top. Preserving scrollback is
54
- // more important for our use case.
55
- queued = queued.replace(/\x1b\[3J/g, '');
56
- xtermRef.current.write(queued);
57
- }
58
- });
59
- }
60
- };
61
-
62
- // Simple fit — xterm.js internally handles scroll preservation via
63
- // _suppressOnScrollHandler in Viewport._sync() and isUserScrolling in BufferService
64
- const safeFit = () => {
65
- try { fitRef.current?.fit(); } catch { /* ignore */ }
66
- };
67
- const [connected, setConnected] = useState(false);
68
- const [editing, setEditing] = useState(false);
69
- const [titleValue, setTitleValue] = useState(pane.title);
70
- const [showColorPicker, setShowColorPicker] = useState(false);
71
- const [colorPickerPos, setColorPickerPos] = useState({ x: 0, y: 0 });
72
- const colorPickerRef = useRef<HTMLDivElement>(null);
73
- const colorPopoverRef = useRef<HTMLDivElement>(null);
74
- const [exited, setExited] = useState(false);
75
- const [injectionCount, setInjectionCount] = useState(0);
76
- const [injectionItems, setInjectionItems] = useState<Array<{ type: string; text: string }>>([]);
77
-
78
- // Quest browser detection + voice state
79
- const [isQuest, setIsQuest] = useState(false);
80
- const [isImmersiveVoice, setIsImmersiveVoice] = useState(false);
81
- const [voiceStatus, setVoiceStatus] = useState<'idle' | 'listening' | 'transcribing' | 'waiting'>('idle');
82
- const immersiveRef = useRef(false);
83
- const [questInput, setQuestInput] = useState('');
84
- const questInputRef = useRef<HTMLInputElement>(null);
85
- const [questMicStatus, setQuestMicStatus] = useState<'off' | 'listening' | 'transcribing'>('off');
86
- const questRecorderRef = useRef<MediaRecorder | null>(null);
87
- const [questKeyboardOpen, setQuestKeyboardOpen] = useState(false);
88
- const questWheelThrottleRef = useRef(0);
89
- const [questImmersive, setQuestImmersive] = useState(false);
90
- const questImmersiveRef = useRef(false);
91
- const questWhisperCfgRef = useRef<any>(null);
92
- const [immersiveSensitivity, setImmersiveSensitivity] = useState(35);
93
- const immersiveSensitivityRef = useRef(35);
94
- const [immersivePause, setImmersivePause] = useState(2000);
95
- const immersivePauseRef = useRef(2000);
96
- const [immersiveAutoSend, setImmersiveAutoSend] = useState(true);
97
- const immersiveAutoSendRef = useRef(true);
98
- const [showVoiceSettings, setShowVoiceSettings] = useState(false);
99
- const [showDiff, setShowDiff] = useState(false);
100
- const [diffCount, setDiffCount] = useState<number | null>(null);
101
- const [vmStatus, setVmStatus] = useState<string | null>(null);
102
-
103
- useEffect(() => {
104
- const ua = navigator.userAgent || '';
105
- setIsQuest(/Quest|Oculus|Pacific/i.test(ua));
106
- }, []);
107
-
108
- // Poll for custom model VM status
109
- useEffect(() => {
110
- if (!pane.customModelId) return;
111
- const poll = () => {
112
- fetch(`/api/proxy/models/${pane.customModelId}/status`)
113
- .then(r => r.ok ? r.json() : null)
114
- .then(d => { if (d?.status) setVmStatus(d.status); })
115
- .catch(() => {});
116
- };
117
- poll();
118
- const id = setInterval(poll, 15000);
119
- return () => clearInterval(id);
120
- }, [pane.customModelId]);
121
-
122
- // Poll for diff file count badge (only if pane has a git baseline)
123
- useEffect(() => {
124
- if (!pane.diffBaselineSha) return;
125
- const poll = () => {
126
- fetch(`/api/panes/${pane.id}/diff?countOnly=true`)
127
- .then(r => r.ok ? r.json() : null)
128
- .then(d => { if (d?.fileCount !== undefined) setDiffCount(d.fileCount); })
129
- .catch(() => {});
130
- };
131
- poll();
132
- const id = setInterval(poll, 30000);
133
- return () => clearInterval(id);
134
- }, [pane.id, pane.diffBaselineSha]);
135
-
136
- // Use refs for props so the connect function never needs to re-create.
137
- // This prevents all terminals from reconnecting when parent state changes.
138
- const paneRef = useRef(pane);
139
- paneRef.current = pane;
140
- const onUpdateRef = useRef(onUpdate);
141
- onUpdateRef.current = onUpdate;
142
- const terminalTokenRef = useRef(terminalToken);
143
- terminalTokenRef.current = terminalToken;
144
-
145
- // Upload handler ref (used inside connect callback which captures refs)
146
- const uploadFilesRef = useRef<(files: File[]) => void>(() => {});
147
-
148
- // Auto-reconnect state
149
- const exitedRef = useRef(false);
150
- const intentionalCloseRef = useRef(false);
151
- const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
152
- const reconnectAttemptsRef = useRef(0);
153
-
154
- // Stable connect function — never changes reference
155
- const connect = useCallback(async () => {
156
- if (!termRef.current) return;
157
- const currentPane = paneRef.current;
158
- intentionalCloseRef.current = false;
159
- reconnectAttemptsRef.current = 0;
160
-
161
- const { Terminal } = await import('xterm');
162
- const { FitAddon } = await import('@xterm/addon-fit');
163
- const { WebLinksAddon } = await import('@xterm/addon-web-links');
164
-
165
- if (xtermRef.current) {
166
- xtermRef.current.dispose();
167
- }
168
-
169
- const isQuestUA = /Quest|Oculus|Pacific/i.test(navigator.userAgent);
170
- const term = new Terminal({
171
- cursorBlink: !isQuestUA,
172
- disableStdin: isQuestUA, // On Quest, input goes through the visible input field instead
173
- fontSize: 13,
174
- fontFamily: "'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, monospace",
175
- scrollback: 10000, // Cap scrollback buffer (default is unlimited → OOM on heavy output)
176
- fastScrollModifier: 'alt', // Alt+scroll for fast scrolling
177
- smoothScrollDuration: 0, // Disable smooth scroll animation (prevents jank on rapid output)
178
- theme: {
179
- background: '#0a0a0a',
180
- foreground: '#e4e4e7',
181
- cursor: '#6366f1',
182
- selectionBackground: '#6366f133',
183
- black: '#18181b',
184
- red: '#ef4444',
185
- green: '#22c55e',
186
- yellow: '#eab308',
187
- blue: '#3b82f6',
188
- magenta: '#a855f7',
189
- cyan: '#06b6d4',
190
- white: '#e4e4e7',
191
- brightBlack: '#52525b',
192
- brightRed: '#f87171',
193
- brightGreen: '#4ade80',
194
- brightYellow: '#facc15',
195
- brightBlue: '#60a5fa',
196
- brightMagenta: '#c084fc',
197
- brightCyan: '#22d3ee',
198
- brightWhite: '#fafafa',
199
- },
200
- allowProposedApi: true,
201
- });
202
-
203
- const fitAddon = new FitAddon();
204
- const webLinksAddon = new WebLinksAddon();
205
- term.loadAddon(fitAddon);
206
- term.loadAddon(webLinksAddon);
207
-
208
- // Defer open by one frame so the container has layout dimensions (fixes Windows xterm crash)
209
- await new Promise<void>((resolve) => requestAnimationFrame(() => {
210
- if (termRef.current) {
211
- term.open(termRef.current);
212
- try { fitAddon.fit(); } catch { /* dimensions may be wrong — corrected below */ }
213
-
214
- // On Quest: hide xterm's internal textarea so it can't grab focus and pop up the keyboard
215
- if (isQuestUA && termRef.current) {
216
- const textarea = termRef.current.querySelector('textarea');
217
- if (textarea) {
218
- textarea.setAttribute('readonly', 'true');
219
- textarea.setAttribute('inputmode', 'none');
220
- textarea.style.opacity = '0';
221
- textarea.style.pointerEvents = 'none';
222
- }
223
- }
224
- }
225
- resolve();
226
- }));
227
-
228
- // Ctrl-C copies selection; Ctrl-V: return false WITHOUT preventDefault so browser
229
- // fires its paste event naturally xterm's paste handler picks it up via onData.
230
- term.attachCustomKeyEventHandler((ev: KeyboardEvent) => {
231
- if (ev.type !== 'keydown') return true;
232
- if (ev.ctrlKey && ev.key === 'c' && term.hasSelection()) {
233
- navigator.clipboard?.writeText(term.getSelection());
234
- term.clearSelection();
235
- return false;
236
- }
237
- if (ev.ctrlKey && ev.key === 'v') {
238
- return false; // stop xterm from sending \x16; browser paste event fires naturally
239
- }
240
- return true;
241
- });
242
-
243
- // xterm.js tracks scroll state internally (isUserScrolling flag in BufferService)
244
- // — no manual tracking needed. write() won't auto-scroll when user is scrolled up.
245
-
246
- xtermRef.current = term;
247
- fitRef.current = fitAddon;
248
-
249
- // The initial fit() above kicks xterm's canvas renderer into life but the
250
- // CSS grid may not have settled yet, giving wrong cols/rows. A double-rAF
251
- // waits for a full layout+paint cycle; the 300ms fallback catches slow grids.
252
- requestAnimationFrame(() => requestAnimationFrame(safeFit));
253
- setTimeout(safeFit, 300);
254
-
255
- // Build WebSocket URL from current pane state
256
- const buildWsUrl = () => {
257
- const p = paneRef.current;
258
- const params = new URLSearchParams({
259
- paneId: p.id,
260
- cwd: p.cwd,
261
- agentType: p.agentType || 'shell',
262
- cols: String(term.cols),
263
- rows: String(term.rows),
264
- });
265
- if (p.claudeSessionId) params.set('agentSession', p.claudeSessionId);
266
- if (p.customCommand) params.set('customCommand', p.customCommand);
267
- if (p.nodeId) params.set('nodeId', p.nodeId);
268
- if ((p as any).customModelId) params.set('customModelId', (p as any).customModelId);
269
- const token = terminalTokenRef.current;
270
- if (token) params.set('terminalToken', token);
271
- const basePath = process.env.SPACES_BASE_PATH || '';
272
- const wsPath = WS_PATH || `${basePath}/ws`;
273
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
274
- return `${proto}//${location.host}${wsPath}?${params}`;
275
- };
276
-
277
- // Open (or re-open) WebSocket and wire to the existing terminal
278
- const openWs = () => {
279
- if (intentionalCloseRef.current) return;
280
-
281
- // On reconnect, clear terminal so server buffer replay is clean
282
- if (reconnectAttemptsRef.current > 0) {
283
- term.write('\x1b[2J\x1b[H');
284
- term.clear();
285
- }
286
-
287
- const ws = new WebSocket(buildWsUrl());
288
- wsRef.current = ws;
289
-
290
- ws.onopen = () => {
291
- setConnected(true);
292
- setExited(false);
293
- exitedRef.current = false;
294
- reconnectAttemptsRef.current = 0;
295
- // Re-fit after connection — the CSS grid layout may not have been
296
- // stable when the terminal first opened, so cols/rows in the URL
297
- // can be wrong. A delayed fit + resize message corrects this.
298
- setTimeout(safeFit, 150);
299
- };
300
-
301
- ws.onmessage = (event) => {
302
- try {
303
- const msg = JSON.parse(event.data);
304
- if (msg.type === 'data') {
305
- queueWrite(msg.data);
306
- } else if (msg.type === 'exit') {
307
- setExited(true);
308
- exitedRef.current = true;
309
- const reason = msg.reason ? ` — ${msg.reason}` : '';
310
- term.write(`\r\n\x1b[90m[Process exited with code ${msg.exitCode}${reason}]\x1b[0m\r\n`);
311
- } else if (msg.type === 'error') {
312
- term.write(`\r\n\x1b[31m${msg.data}\x1b[0m\r\n`);
313
- } else if (msg.type === 'session-detected') {
314
- onUpdateRef.current(paneRef.current.id, { claudeSessionId: msg.sessionId });
315
- } else if (msg.type === 'cortex-injection') {
316
- setInjectionCount(msg.count || 0);
317
- if (msg.items) setInjectionItems(msg.items);
318
- } else if (msg.type === 'collab-updated') {
319
- onUpdateRef.current(paneRef.current.id, { isCollaborating: msg.isCollaborating });
320
- }
321
- } catch {
322
- // Raw data
323
- }
324
- };
325
-
326
- ws.onclose = () => {
327
- setConnected(false);
328
- // Auto-reconnect if the process didn't exit and we didn't intentionally close
329
- if (!exitedRef.current && !intentionalCloseRef.current) {
330
- const attempts = reconnectAttemptsRef.current;
331
- if (attempts < 20) {
332
- const delay = Math.min(1000 * Math.pow(1.5, attempts), 30000);
333
- reconnectAttemptsRef.current = attempts + 1;
334
- reconnectTimerRef.current = setTimeout(openWs, delay);
335
- }
336
- }
337
- };
338
-
339
- ws.onerror = () => {
340
- setConnected(false);
341
- };
342
- };
343
-
344
- // Wire terminal input/resize to current WebSocket via ref
345
- term.onData((data: string) => {
346
- // xterm.js auto-scrolls to bottom on user input (scrollOnUserInput option)
347
- if (wsRef.current?.readyState === WebSocket.OPEN) {
348
- wsRef.current.send(JSON.stringify({ type: 'data', data }));
349
- }
350
- });
351
-
352
- term.onResize(({ cols, rows }) => {
353
- if (wsRef.current?.readyState === WebSocket.OPEN) {
354
- wsRef.current.send(JSON.stringify({ type: 'resize', cols, rows }));
355
- }
356
- });
357
-
358
- openWs();
359
- }, []); // Empty deps — uses refs for current values
360
-
361
- // Connect once on mount, clean up on unmount only
362
- useEffect(() => {
363
- connect();
364
- return () => {
365
- intentionalCloseRef.current = true;
366
- if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
367
- wsRef.current?.close();
368
- xtermRef.current?.dispose();
369
- };
370
- }, [connect]);
371
-
372
- // Resize on container changes. The ResizeObserver fires immediately on
373
- // observe(), but fitRef may not be set yet (connect() is async). Using rAF
374
- // coalesces rapid resize events and gives connect() time to finish.
375
- useEffect(() => {
376
- let rafId: number;
377
- const observer = new ResizeObserver(() => {
378
- cancelAnimationFrame(rafId);
379
- rafId = requestAnimationFrame(safeFit);
380
- });
381
- if (termRef.current) {
382
- observer.observe(termRef.current);
383
- }
384
- return () => { cancelAnimationFrame(rafId); observer.disconnect(); };
385
- }, []);
386
-
387
- // Resize when maximized changes or Quest toolbar appears/disappears
388
- useEffect(() => {
389
- setTimeout(safeFit, 50);
390
- }, [isMaximized, isQuest]);
391
-
392
- const saveTitle = () => {
393
- onUpdate(pane.id, { title: titleValue });
394
- setEditing(false);
395
- };
396
-
397
- const reconnect = () => {
398
- intentionalCloseRef.current = true;
399
- if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
400
- wsRef.current?.close();
401
- xtermRef.current?.dispose();
402
- setExited(false);
403
- exitedRef.current = false;
404
- setTimeout(connect, 100);
405
- };
406
-
407
- const handlePopout = () => {
408
- if (onPopout) {
409
- intentionalCloseRef.current = true;
410
- if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
411
- wsRef.current?.close();
412
- xtermRef.current?.dispose();
413
- onPopout(pane.id);
414
- }
415
- };
416
-
417
- // ─── Quest toolbar: send keystrokes to terminal ───
418
- const sendKey = (key: string) => {
419
- if (wsRef.current?.readyState === WebSocket.OPEN) {
420
- wsRef.current.send(JSON.stringify({ type: 'data', data: key }));
421
- }
422
- };
423
-
424
- // Send text as a bracketed paste — terminal treats it as a single block
425
- // instead of processing character-by-character (prevents prompt redraw glitches)
426
- const sendPaste = (text: string) => {
427
- if (wsRef.current?.readyState === WebSocket.OPEN) {
428
- const bracketed = `\x1b[200~${text}\x1b[201~`;
429
- wsRef.current.send(JSON.stringify({ type: 'data', data: bracketed }));
430
- }
431
- };
432
-
433
- // ─── Quest Whisper mic: record Groq/Whisper text into input field ───
434
- const toggleQuestMic = async () => {
435
- if (questMicStatus !== 'off') {
436
- // Stop recording
437
- if (questRecorderRef.current?.state === 'recording') questRecorderRef.current.stop();
438
- return;
439
- }
440
-
441
- setQuestMicStatus('listening');
442
- try {
443
- // Get Groq/Whisper config for direct browser call (no server proxy)
444
- const cfgRes = await fetch('/api/whisper/config');
445
- const cfg = cfgRes.ok ? await cfgRes.json() : null;
446
-
447
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
448
- const audioCtx = new AudioContext();
449
- const source = audioCtx.createMediaStreamSource(stream);
450
- const analyser = audioCtx.createAnalyser();
451
- analyser.fftSize = 512;
452
- source.connect(analyser);
453
-
454
- const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
455
- ? 'audio/webm;codecs=opus' : 'audio/webm';
456
- const recorder = new MediaRecorder(stream, { mimeType });
457
- const chunks: Blob[] = [];
458
- let hasSpeech = false;
459
-
460
- recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
461
-
462
- recorder.onstop = async () => {
463
- stream.getTracks().forEach(t => t.stop());
464
- try { source.disconnect(); } catch {}
465
- try { audioCtx.close(); } catch {}
466
- questRecorderRef.current = null;
467
-
468
- if (!hasSpeech || chunks.length === 0) { setQuestMicStatus('off'); return; }
469
-
470
- const blob = new Blob(chunks, { type: 'audio/webm' });
471
- if (blob.size < 500) { setQuestMicStatus('off'); return; }
472
-
473
- setQuestMicStatus('transcribing');
474
- try {
475
- let text = '';
476
- if (cfg?.apiKey) {
477
- // Direct call to Groq/OpenAI — skip server proxy for speed
478
- const form = new FormData();
479
- form.append('file', blob, 'audio.webm');
480
- form.append('model', cfg.model);
481
- form.append('response_format', 'json');
482
- form.append('language', 'en');
483
- const res = await fetch(cfg.apiUrl, {
484
- method: 'POST',
485
- headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
486
- body: form,
487
- });
488
- if (res.ok) {
489
- const data = await res.json();
490
- text = data.text || '';
491
- }
492
- } else {
493
- // Fallback to server proxy
494
- const form = new FormData();
495
- form.append('audio', blob, 'dictation.webm');
496
- const res = await fetch('/api/whisper', { method: 'POST', body: form });
497
- if (res.ok) {
498
- const data = await res.json();
499
- text = data.text || '';
500
- }
501
- }
502
- if (text.trim()) {
503
- setQuestInput(prev => prev ? `${prev} ${text.trim()}` : text.trim());
504
- }
505
- } catch {}
506
- setQuestMicStatus('off');
507
- };
508
-
509
- // VAD: detect speech, stop after 1s silence (fast trigger)
510
- const dataArr = new Uint8Array(analyser.frequencyBinCount);
511
- let lastSpeechTime = 0;
512
- let speechFrames = 0;
513
- const startTime = Date.now();
514
-
515
- const checkVAD = () => {
516
- if (!questRecorderRef.current || questRecorderRef.current.state !== 'recording') return;
517
-
518
- analyser.getByteFrequencyData(dataArr);
519
- const avg = dataArr.reduce((a, b) => a + b, 0) / dataArr.length;
520
-
521
- if (avg > 20) {
522
- speechFrames++;
523
- lastSpeechTime = Date.now();
524
- if (speechFrames >= 10) hasSpeech = true;
525
- } else if (!hasSpeech) {
526
- speechFrames = 0;
527
- }
528
-
529
- if (hasSpeech && Date.now() - lastSpeechTime > 1500) {
530
- questRecorderRef.current.stop();
531
- return;
532
- }
533
- if (!hasSpeech && Date.now() - startTime > 10000) {
534
- questRecorderRef.current.stop();
535
- return;
536
- }
537
- requestAnimationFrame(checkVAD);
538
- };
539
-
540
- recorder.start(250);
541
- questRecorderRef.current = recorder;
542
- requestAnimationFrame(checkVAD);
543
- } catch (e: any) {
544
- if (xtermRef.current) {
545
- xtermRef.current.write(`\r\n\x1b[91m[Mic] ${e.name || 'Error'}: ${e.message || 'Failed to access microphone'}\x1b[0m\r\n`);
546
- }
547
- setQuestMicStatus('off');
548
- }
549
- };
550
-
551
- // ─── Quest immersive voice: continuous loop — listen → transcribe → send → repeat ───
552
- const startImmersiveCycle = async () => {
553
- if (!questImmersiveRef.current) return;
554
- setQuestMicStatus('listening');
555
-
556
- try {
557
- // Fetch config once, cache for subsequent cycles
558
- if (!questWhisperCfgRef.current) {
559
- const cfgRes = await fetch('/api/whisper/config');
560
- if (cfgRes.ok) questWhisperCfgRef.current = await cfgRes.json();
561
- }
562
- if (!questImmersiveRef.current) return;
563
-
564
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
565
- if (!questImmersiveRef.current) { stream.getTracks().forEach(t => t.stop()); return; }
566
-
567
- const audioCtx = new AudioContext();
568
- const source = audioCtx.createMediaStreamSource(stream);
569
- const analyser = audioCtx.createAnalyser();
570
- analyser.fftSize = 512;
571
- source.connect(analyser);
572
-
573
- const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
574
- ? 'audio/webm;codecs=opus' : 'audio/webm';
575
- const recorder = new MediaRecorder(stream, { mimeType });
576
- const chunks: Blob[] = [];
577
- let hasSpeech = false;
578
-
579
- recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
580
-
581
- recorder.onstop = async () => {
582
- stream.getTracks().forEach(t => t.stop());
583
- try { source.disconnect(); } catch {}
584
- try { audioCtx.close(); } catch {}
585
- questRecorderRef.current = null;
586
-
587
- if (!questImmersiveRef.current && !questDrainingRef.current) { setQuestMicStatus('off'); return; }
588
-
589
- // No speech — restart or clean up
590
- if (!hasSpeech || chunks.length === 0) {
591
- questDrainingRef.current = false;
592
- if (questImmersiveRef.current) setTimeout(startImmersiveCycle, 300);
593
- else setQuestMicStatus('off');
594
- return;
595
- }
596
-
597
- const blob = new Blob(chunks, { type: 'audio/webm' });
598
- if (blob.size < 500) { setTimeout(startImmersiveCycle, 300); return; }
599
-
600
- setQuestMicStatus('transcribing');
601
- const cfg = questWhisperCfgRef.current;
602
- try {
603
- let text = '';
604
- if (cfg?.apiKey) {
605
- const form = new FormData();
606
- form.append('file', blob, 'audio.webm');
607
- form.append('model', cfg.model);
608
- form.append('response_format', 'json');
609
- form.append('language', 'en');
610
- const res = await fetch(cfg.apiUrl, {
611
- method: 'POST',
612
- headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
613
- body: form,
614
- });
615
- if (res.ok) text = (await res.json()).text || '';
616
- } else {
617
- const form = new FormData();
618
- form.append('audio', blob, 'dictation.webm');
619
- const res = await fetch('/api/whisper', { method: 'POST', body: form });
620
- if (res.ok) text = (await res.json()).text || '';
621
- }
622
- if (text.trim() && (questImmersiveRef.current || questDrainingRef.current)) {
623
- if (immersiveAutoSendRef.current) {
624
- // Single write: text + Enter in one message (avoids race between split sends)
625
- sendKey(text.trim() + '\r');
626
- } else {
627
- // No auto-send: put text in the input field so user can review/edit
628
- setQuestInput((prev) => prev ? prev + ' ' + text.trim() : text.trim());
629
- }
630
- }
631
- } catch {}
632
-
633
- // Loop: restart listening if still immersive, or clean up if draining
634
- questDrainingRef.current = false;
635
- if (questImmersiveRef.current) {
636
- setTimeout(startImmersiveCycle, 800);
637
- } else {
638
- setQuestMicStatus('off');
639
- }
640
- };
641
-
642
- // VAD: detect speech, stop after 1s silence
643
- const dataArr = new Uint8Array(analyser.frequencyBinCount);
644
- let lastSpeechTime = 0;
645
- let speechFrames = 0;
646
- const startTime = Date.now();
647
-
648
- const checkVAD = () => {
649
- if (!questRecorderRef.current || questRecorderRef.current.state !== 'recording') return;
650
- if (!questImmersiveRef.current) { questRecorderRef.current.stop(); return; }
651
-
652
- analyser.getByteFrequencyData(dataArr);
653
- const avg = dataArr.reduce((a, b) => a + b, 0) / dataArr.length;
654
-
655
- if (avg > immersiveSensitivityRef.current) {
656
- speechFrames++;
657
- lastSpeechTime = Date.now();
658
- if (speechFrames >= 15) hasSpeech = true;
659
- } else if (!hasSpeech) {
660
- speechFrames = 0;
661
- }
662
-
663
- // Configurable silence timeout for immersive mode
664
- if (hasSpeech && Date.now() - lastSpeechTime > immersivePauseRef.current) {
665
- questRecorderRef.current.stop();
666
- return;
667
- }
668
- if (!hasSpeech && Date.now() - startTime > 10000) {
669
- questRecorderRef.current.stop();
670
- return;
671
- }
672
- requestAnimationFrame(checkVAD);
673
- };
674
-
675
- recorder.start(250);
676
- questRecorderRef.current = recorder;
677
- requestAnimationFrame(checkVAD);
678
- } catch (e: any) {
679
- if (xtermRef.current) {
680
- xtermRef.current.write(`\r\n\x1b[91m[Mic] ${e.name || 'Error'}: ${e.message || 'Failed to access microphone'}\x1b[0m\r\n`);
681
- }
682
- if (questImmersiveRef.current) setTimeout(startImmersiveCycle, 2000);
683
- else setQuestMicStatus('off');
684
- }
685
- };
686
-
687
- const questDrainingRef = useRef(false);
688
-
689
- const toggleQuestImmersive = () => {
690
- if (questImmersiveRef.current) {
691
- // Exit immersive but let current recording finish sending
692
- questImmersiveRef.current = false;
693
- setQuestImmersive(false);
694
- if (questRecorderRef.current?.state === 'recording') {
695
- // Flag as draining so onstop still sends but doesn't restart
696
- questDrainingRef.current = true;
697
- setQuestMicStatus('transcribing');
698
- try { questRecorderRef.current.stop(); } catch {}
699
- } else {
700
- setQuestMicStatus('off');
701
- }
702
- questWhisperCfgRef.current = null;
703
- } else {
704
- // Enter immersive
705
- questImmersiveRef.current = true;
706
- setQuestImmersive(true);
707
- startImmersiveCycle();
708
- }
709
- };
710
-
711
- // ─── Voice mode: Web Speech API on desktop, Whisper/Groq mic on Quest ───
712
- const recognitionRef = useRef<any>(null);
713
- const isQuestBrowser = typeof navigator !== 'undefined' && /Quest|Oculus|Pacific/i.test(navigator.userAgent);
714
- const hasWebSpeech = typeof window !== 'undefined' && !isQuestBrowser &&
715
- ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window);
716
-
717
- // autoSend: false = text goes to terminal inline (dictation mode)
718
- // true = each final phrase gets Enter appended (immersive mode)
719
- const startWebSpeech = (autoSend: boolean) => {
720
- if (!immersiveRef.current) return;
721
- const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
722
- if (!SpeechRecognition) return;
723
-
724
- const recognition = new SpeechRecognition();
725
- recognition.continuous = true;
726
- recognition.interimResults = true;
727
- recognition.lang = 'en-US';
728
-
729
- let lastFinalLength = 0;
730
-
731
- recognition.onresult = (event: any) => {
732
- if (!immersiveRef.current) return;
733
- let finalText = '';
734
- for (let i = 0; i < event.results.length; i++) {
735
- if (event.results[i].isFinal) finalText += event.results[i][0].transcript;
736
- }
737
- if (finalText.length > lastFinalLength) {
738
- const newText = finalText.slice(lastFinalLength);
739
- if (autoSend && immersiveAutoSendRef.current) {
740
- // Single write: text + Enter in one message (avoids race between split sends)
741
- sendKey(newText.trim() + '\r');
742
- } else {
743
- sendPaste(newText.trim());
744
- }
745
- lastFinalLength = finalText.length;
746
- }
747
- };
748
-
749
- recognition.onerror = (event: any) => {
750
- if (event.error === 'not-allowed' || event.error === 'service-not-allowed') {
751
- immersiveRef.current = false;
752
- setIsImmersiveVoice(false);
753
- setVoiceStatus('idle');
754
- return;
755
- }
756
- if (immersiveRef.current) setTimeout(() => startWebSpeech(autoSend), 500);
757
- };
758
-
759
- recognition.onend = () => {
760
- if (immersiveRef.current && document.visibilityState === 'visible') {
761
- setTimeout(() => startWebSpeech(autoSend), 100);
762
- } else if (!immersiveRef.current) {
763
- setVoiceStatus('idle');
764
- }
765
- // If tab hidden but still immersive, visibility handler will restart
766
- };
767
-
768
- recognition.onstart = () => setVoiceStatus('listening');
769
-
770
- recognitionRef.current = recognition;
771
- recognition.start();
772
- };
773
-
774
- // Dictation mode: text appears inline, no auto-Enter
775
- const toggleDictation = () => {
776
- if (immersiveRef.current) {
777
- immersiveRef.current = false;
778
- setIsImmersiveVoice(false);
779
- setVoiceStatus('idle');
780
- if (recognitionRef.current) { try { recognitionRef.current.abort(); } catch {} recognitionRef.current = null; }
781
- } else if (hasWebSpeech) {
782
- immersiveRef.current = true;
783
- setIsImmersiveVoice(true);
784
- startWebSpeech(false);
785
- setTimeout(() => xtermRef.current?.focus(), 100);
786
- }
787
- };
788
-
789
- // Immersive mode: each phrase auto-sends with Enter
790
- const toggleDesktopImmersive = () => {
791
- if (immersiveRef.current) {
792
- immersiveRef.current = false;
793
- setIsImmersiveVoice(false);
794
- setVoiceStatus('idle');
795
- if (recognitionRef.current) { try { recognitionRef.current.abort(); } catch {} recognitionRef.current = null; }
796
- } else if (hasWebSpeech) {
797
- immersiveRef.current = true;
798
- setIsImmersiveVoice(true);
799
- startWebSpeech(true);
800
- setTimeout(() => xtermRef.current?.focus(), 100);
801
- }
802
- };
803
-
804
- // Restart voice when tab regains focus (mobile/desktop browsers kill audio in background)
805
- useEffect(() => {
806
- const handleVisibility = () => {
807
- if (document.visibilityState === 'visible' && immersiveRef.current && !recognitionRef.current) {
808
- startWebSpeech(true);
809
- }
810
- };
811
- document.addEventListener('visibilitychange', handleVisibility);
812
- return () => {
813
- document.removeEventListener('visibilitychange', handleVisibility);
814
- immersiveRef.current = false;
815
- if (recognitionRef.current) try { recognitionRef.current.abort(); } catch {}
816
- };
817
- }, []);
818
-
819
- // ─── File paste & drag-drop upload ───
820
- const [uploadStatus, setUploadStatus] = useState<string | null>(null);
821
- const [isDragging, setIsDragging] = useState(false);
822
- const uploadTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
823
-
824
- const uploadFiles = useCallback(async (files: File[]) => {
825
- if (files.length === 0) return;
826
- const cwd = paneRef.current.cwd || '/';
827
- const form = new FormData();
828
- form.append('dir', cwd);
829
- for (const f of files) form.append('files', f);
830
-
831
- const names = files.map(f => f.name).join(', ');
832
- setUploadStatus(`Uploading ${files.length} file${files.length > 1 ? 's' : ''}...`);
833
- if (uploadTimeoutRef.current) clearTimeout(uploadTimeoutRef.current);
834
-
835
- try {
836
- const res = await fetch('/api/files', { method: 'POST', body: form });
837
- if (res.ok) {
838
- const data = await res.json();
839
- setUploadStatus(`Uploaded ${data.files?.join(', ') || names} to ${cwd}`);
840
- // Write a note into the terminal so the user/agent knows the file is there
841
- const term = xtermRef.current;
842
- if (term) {
843
- term.write(`\r\n\x1b[90m[Uploaded ${data.files?.join(', ') || names} → ${cwd}]\x1b[0m\r\n`);
844
- }
845
- } else {
846
- setUploadStatus('Upload failed');
847
- }
848
- } catch {
849
- setUploadStatus('Upload failed');
850
- }
851
- uploadTimeoutRef.current = setTimeout(() => setUploadStatus(null), 4000);
852
- }, []);
853
- uploadFilesRef.current = uploadFiles;
854
-
855
- // Listen for paste events with files/images.
856
- // Must use document-level capture because xterm.js creates an internal <textarea>
857
- // that receives focus and swallows paste events before they reach the container.
858
- useEffect(() => {
859
- const el = termRef.current;
860
- if (!el) return;
861
-
862
- const handlePaste = (e: ClipboardEvent) => {
863
- // Only handle pastes when this terminal pane is focused
864
- if (!el.contains(e.target as Node)) return;
865
-
866
- const items = e.clipboardData?.items;
867
- if (!items) return;
868
- const files: File[] = [];
869
- for (let i = 0; i < items.length; i++) {
870
- const item = items[i];
871
- if (item.kind === 'file') {
872
- const file = item.getAsFile();
873
- if (file) files.push(file);
874
- }
875
- }
876
- if (files.length > 0) {
877
- e.preventDefault();
878
- e.stopPropagation();
879
- uploadFilesRef.current(files);
880
- }
881
- // If no files, let the normal text paste handler (Ctrl+V) handle it
882
- };
883
-
884
- // Capture phase so we intercept before xterm's textarea handler
885
- document.addEventListener('paste', handlePaste, true);
886
-
887
- // Drag-and-drop (works directly on the container — xterm doesn't intercept drag events)
888
- const handleDragOver = (e: DragEvent) => {
889
- e.preventDefault();
890
- e.stopPropagation();
891
- setIsDragging(true);
892
- };
893
- const handleDragLeave = (e: DragEvent) => {
894
- e.preventDefault();
895
- if (el && !el.contains(e.relatedTarget as Node)) {
896
- setIsDragging(false);
897
- }
898
- };
899
- const handleDrop = (e: DragEvent) => {
900
- e.preventDefault();
901
- e.stopPropagation();
902
- setIsDragging(false);
903
- const files: File[] = [];
904
- if (e.dataTransfer?.files) {
905
- for (let i = 0; i < e.dataTransfer.files.length; i++) {
906
- files.push(e.dataTransfer.files[i]);
907
- }
908
- }
909
- if (files.length > 0) uploadFilesRef.current(files);
910
- };
911
-
912
- el.addEventListener('dragover', handleDragOver);
913
- el.addEventListener('dragleave', handleDragLeave);
914
- el.addEventListener('drop', handleDrop);
915
- return () => {
916
- document.removeEventListener('paste', handlePaste, true);
917
- el.removeEventListener('dragover', handleDragOver);
918
- el.removeEventListener('dragleave', handleDragLeave);
919
- el.removeEventListener('drop', handleDrop);
920
- };
921
- }, []);
922
-
923
- // Close color picker on outside click
924
- useEffect(() => {
925
- if (!showColorPicker) return;
926
- const handler = (e: MouseEvent) => {
927
- const target = e.target as Node;
928
- if (
929
- colorPickerRef.current && !colorPickerRef.current.contains(target) &&
930
- colorPopoverRef.current && !colorPopoverRef.current.contains(target)
931
- ) {
932
- setShowColorPicker(false);
933
- }
934
- };
935
- document.addEventListener('mousedown', handler);
936
- return () => document.removeEventListener('mousedown', handler);
937
- }, [showColorPicker]);
938
-
939
- const COLORS = [
940
- '#ef4444', '#f97316', '#f59e0b', '#eab308',
941
- '#84cc16', '#22c55e', '#14b8a6', '#06b6d4',
942
- '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7',
943
- '#d946ef', '#ec4899', '#f43f5e', '#78716c',
944
- ];
945
-
946
- return (
947
- <div className={cn(
948
- 'flex flex-col border rounded-lg overflow-hidden',
949
- isMaximized ? 'h-full !rounded-none' : 'h-full',
950
- 'border-zinc-700'
951
- )} style={{ borderColor: `${pane.color}60` }}>
952
- {/* Title bar */}
953
- <div
954
- className="flex items-center gap-2 px-3 py-1.5 text-xs select-none flex-shrink-0"
955
- style={{ backgroundColor: `${pane.color}30`, borderBottom: `1px solid ${pane.color}40` }}
956
- >
957
- {/* Drag handle */}
958
- {dragHandleProps && !isPopout && (
959
- <div {...dragHandleProps} className="flex-shrink-0 cursor-grab active:cursor-grabbing text-zinc-500 hover:text-zinc-300 -ml-1">
960
- <GripVertical className="w-3.5 h-3.5" />
961
- </div>
962
- )}
963
- <div ref={colorPickerRef} className="flex-shrink-0">
964
- <button
965
- onClick={(e) => {
966
- const rect = e.currentTarget.getBoundingClientRect();
967
- setColorPickerPos({ x: rect.left, y: rect.bottom + 6 });
968
- setShowColorPicker(!showColorPicker);
969
- }}
970
- className="w-3 h-3 rounded-full hover:ring-2 hover:ring-zinc-500 ring-offset-1 ring-offset-zinc-900 transition-shadow cursor-pointer"
971
- style={{ backgroundColor: pane.color }}
972
- title="Change color"
973
- />
974
- {showColorPicker && createPortal(
975
- <div
976
- ref={colorPopoverRef}
977
- className="fixed z-[9999] bg-zinc-800 border border-zinc-600 rounded-xl p-3 shadow-2xl"
978
- style={{ left: colorPickerPos.x, top: colorPickerPos.y }}
979
- >
980
- <div className="grid grid-cols-4 gap-2.5" style={{ width: 132 }}>
981
- {COLORS.map((c) => (
982
- <button
983
- key={c}
984
- onClick={() => {
985
- onUpdate(pane.id, { color: c });
986
- setShowColorPicker(false);
987
- }}
988
- className={`w-7 h-7 rounded-full border-2 transition-all hover:scale-110 ${
989
- pane.color === c ? 'border-white scale-110 shadow-lg' : 'border-transparent hover:border-zinc-500'
990
- }`}
991
- style={{ backgroundColor: c }}
992
- />
993
- ))}
994
- </div>
995
- </div>,
996
- document.body
997
- )}
998
- </div>
999
-
1000
- {editing ? (
1001
- <div className="flex items-center gap-1 flex-1">
1002
- <input
1003
- autoFocus
1004
- value={titleValue}
1005
- onChange={(e) => setTitleValue(e.target.value)}
1006
- onKeyDown={(e) => {
1007
- if (e.key === 'Enter') saveTitle();
1008
- if (e.key === 'Escape') { setTitleValue(pane.title); setEditing(false); }
1009
- }}
1010
- className="flex-1 bg-transparent border border-zinc-600 rounded px-1.5 py-0.5 text-xs focus:outline-none focus:border-indigo-400"
1011
- />
1012
- <button onClick={saveTitle} className="text-green-400 hover:text-green-300">
1013
- <Check className="w-3 h-3" />
1014
- </button>
1015
- </div>
1016
- ) : (
1017
- <div className="flex items-center gap-1 flex-1 min-w-0 group/title">
1018
- <span
1019
- onDoubleClick={() => setEditing(true)}
1020
- className="truncate text-zinc-300 font-medium cursor-default"
1021
- >
1022
- {pane.title}
1023
- </span>
1024
- <button
1025
- onClick={() => setEditing(true)}
1026
- className="text-zinc-600 hover:text-zinc-300 opacity-0 group-hover/title:opacity-100 transition-opacity flex-shrink-0"
1027
- title="Rename"
1028
- >
1029
- <Pencil className="w-2.5 h-2.5" />
1030
- </button>
1031
- </div>
1032
- )}
1033
-
1034
- <button
1035
- onClick={() => onBrowse?.(pane.cwd)}
1036
- className="text-[10px] text-zinc-500 truncate max-w-[120px] hover:text-amber-400 transition-colors"
1037
- title={`Browse ${pane.cwd}`}
1038
- >
1039
- {pane.cwd.split(/[/\\]/).pop()}
1040
- </button>
1041
-
1042
- {pane.agentType && pane.agentType !== 'shell' && (
1043
- <span
1044
- className="text-[9px] px-1.5 py-0.5 rounded font-medium flex items-center gap-1"
1045
- style={{
1046
- backgroundColor: `${AGENT_TYPES[pane.agentType]?.color || '#6366f1'}20`,
1047
- color: AGENT_TYPES[pane.agentType]?.color || '#6366f1',
1048
- }}
1049
- >
1050
- {AGENT_TYPES[pane.agentType]?.name || pane.agentType}
1051
- {pane.customModelId && (
1052
- <span className="flex items-center gap-1 border-l border-current pl-1 ml-0.5">
1053
- <span
1054
- className={cn("w-1.5 h-1.5 rounded-full",
1055
- vmStatus === 'RUNNING' ? 'bg-green-500' :
1056
- vmStatus === 'PROVISIONING' || vmStatus === 'STAGING' ? 'bg-yellow-500 animate-pulse' :
1057
- 'bg-red-500'
1058
- )}
1059
- title={vmStatus || 'UNKNOWN'}
1060
- />
1061
- </span>
1062
- )}
1063
- </span>
1064
- )}
1065
-
1066
- {pane.nodeId && (
1067
- <span className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-blue-500/10 text-blue-400 flex items-center gap-0.5">
1068
- <Globe className="w-2.5 h-2.5" />
1069
- remote
1070
- </span>
1071
- )}
1072
-
1073
- {workspaceCollaboration && pane.agentType !== 'shell' && (
1074
- <button
1075
- onClick={async () => {
1076
- const newVal = !pane.isCollaborating;
1077
- await onUpdate(pane.id, { isCollaborating: newVal } as Partial<PaneData>);
1078
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1079
- wsRef.current.send(JSON.stringify({ type: 'collab-toggle', isCollaborating: newVal }));
1080
- }
1081
- }}
1082
- className={`transition-colors ${
1083
- pane.isCollaborating
1084
- ? 'text-indigo-400 hover:text-indigo-300'
1085
- : 'text-zinc-600 hover:text-zinc-400'
1086
- }`}
1087
- title={pane.isCollaborating ? 'Collaborating — click to opt out' : 'Not collaborating — click to opt in'}
1088
- >
1089
- <Users className="w-3 h-3" />
1090
- </button>
1091
- )}
1092
-
1093
- {!connected && !exited && (
1094
- <span className="text-[10px] text-yellow-500">connecting...</span>
1095
- )}
1096
-
1097
-
1098
- {exited && (
1099
- <button onClick={reconnect} className="text-zinc-400 hover:text-white" title="Restart">
1100
- <RotateCcw className="w-3 h-3" />
1101
- </button>
1102
- )}
1103
-
1104
- {!isPopout && onPopout && (
1105
- <button
1106
- onClick={handlePopout}
1107
- className="text-zinc-300 hover:text-white"
1108
- title="Pop out to new window"
1109
- >
1110
- <ExternalLink className="w-3 h-3" />
1111
- </button>
1112
- )}
1113
-
1114
- {pane.diffBaselineSha && (
1115
- <button
1116
- onClick={() => setShowDiff(!showDiff)}
1117
- className={cn('relative text-zinc-300 hover:text-white', showDiff && 'text-indigo-400')}
1118
- title={showDiff ? 'Hide diff' : 'Review changes'}
1119
- >
1120
- <GitCompareArrows className="w-3 h-3" />
1121
- {diffCount != null && diffCount > 0 && !showDiff && (
1122
- <span className="absolute -top-1.5 -right-1.5 text-[8px] bg-indigo-500 text-white rounded-full w-3.5 h-3.5 flex items-center justify-center font-bold">
1123
- {diffCount > 9 ? '9+' : diffCount}
1124
- </span>
1125
- )}
1126
- </button>
1127
- )}
1128
-
1129
- {onMinimize && !isPopout && (
1130
- <button
1131
- onClick={() => onMinimize(pane.id)}
1132
- className="text-zinc-300 hover:text-yellow-400"
1133
- title="Minimize"
1134
- >
1135
- <Minus className="w-3 h-3" />
1136
- </button>
1137
- )}
1138
-
1139
- <button
1140
- onClick={() => onToggleMaximize(pane.id)}
1141
- className="text-zinc-300 hover:text-white"
1142
- title={isMaximized ? 'Restore' : 'Maximize'}
1143
- >
1144
- {isMaximized ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
1145
- </button>
1146
-
1147
- <button
1148
- onClick={() => {
1149
- if (confirm('Close this terminal?')) onClose(pane.id);
1150
- }}
1151
- className="text-zinc-300 hover:text-red-400"
1152
- title="Close"
1153
- >
1154
- <X className="w-3 h-3" />
1155
- </button>
1156
- </div>
1157
-
1158
- {/* Terminal — on Quest, tapping terminal focuses the visible input instead of xterm's hidden textarea */}
1159
- <div
1160
- ref={termRef}
1161
- className="flex-1 relative bg-[#0a0a0a]"
1162
- style={{ minHeight: 0, flex: 1 }}
1163
- onClick={isQuest ? (e) => { e.preventDefault(); } : undefined}
1164
- onWheel={isQuest ? (e) => {
1165
- // Quest joystick fires wheel events — translate to ↑/↓ arrow keys
1166
- e.preventDefault();
1167
- const now = Date.now();
1168
- if (Math.abs(e.deltaY) > 10 && now - questWheelThrottleRef.current > 200) {
1169
- questWheelThrottleRef.current = now;
1170
- sendKey(e.deltaY < 0 ? '\x1b[A' : '\x1b[B');
1171
- }
1172
- } : undefined}
1173
- >
1174
-
1175
- {/* Drag overlay */}
1176
- {isDragging && (
1177
- <div className="absolute inset-0 z-10 bg-indigo-500/10 border-2 border-dashed border-indigo-400 flex items-center justify-center pointer-events-none">
1178
- <div className="flex items-center gap-2 bg-zinc-900/90 px-4 py-2 rounded-lg text-sm text-indigo-300">
1179
- <Upload className="w-4 h-4" />
1180
- Drop files to upload to {pane.cwd.split(/[/\\]/).pop() || pane.cwd}
1181
- </div>
1182
- </div>
1183
- )}
1184
-
1185
- {/* Upload status toast */}
1186
- {uploadStatus && (
1187
- <div className="absolute bottom-2 left-1/2 -translate-x-1/2 z-10 bg-zinc-800/95 border border-zinc-600 px-3 py-1.5 rounded-lg text-xs text-zinc-300 shadow-lg">
1188
- {uploadStatus}
1189
- </div>
1190
- )}
1191
- </div>
1192
-
1193
- {/* Diff panel — shown below terminal when toggled */}
1194
- {showDiff && <PaneDiffPanel paneId={pane.id} onClose={() => setShowDiff(false)} />}
1195
-
1196
- {/* Quest: input field + virtual keys — prevents xterm hidden textarea layout issues */}
1197
- {isQuest && (
1198
- <div
1199
- className="flex flex-col gap-1 px-2 py-1.5 flex-shrink-0 border-t border-zinc-700"
1200
- style={{ backgroundColor: `${pane.color}15`, borderTopColor: `${pane.color}30` }}
1201
- >
1202
- {/* Text input — readOnly by default to prevent keyboard popup */}
1203
- <div className="flex items-center gap-1">
1204
- <input
1205
- ref={questInputRef}
1206
- type="text"
1207
- value={questInput}
1208
- readOnly={!questKeyboardOpen}
1209
- onChange={(e) => setQuestInput(e.target.value)}
1210
- onKeyDown={(e) => {
1211
- if (e.key === 'Enter') {
1212
- e.preventDefault();
1213
- sendKey(questInput ? questInput + '\r' : '\r');
1214
- setQuestInput('');
1215
- // Close keyboard after sending
1216
- setQuestKeyboardOpen(false);
1217
- questInputRef.current?.blur();
1218
- }
1219
- }}
1220
- onBlur={() => setQuestKeyboardOpen(false)}
1221
- placeholder={
1222
- questImmersive && questMicStatus === 'listening' ? 'Speak now...' :
1223
- questImmersive && questMicStatus === 'transcribing' ? 'Sending...' :
1224
- questImmersive ? 'Immersive mode active' :
1225
- questMicStatus === 'listening' ? 'Listening...' :
1226
- questMicStatus === 'transcribing' ? 'Transcribing...' :
1227
- questKeyboardOpen ? 'Type here...' : 'Tap mic, keyboard, or type'
1228
- }
1229
- className={cn(
1230
- 'flex-1 text-zinc-200 text-sm px-3 py-2 rounded border focus:outline-none font-mono',
1231
- questKeyboardOpen
1232
- ? 'bg-zinc-900 border-zinc-400 placeholder:text-zinc-500'
1233
- : 'bg-zinc-900/50 border-zinc-700 placeholder:text-zinc-600'
1234
- )}
1235
- autoComplete="off"
1236
- autoCorrect="on"
1237
- spellCheck={false}
1238
- />
1239
- {/* Single-shot mic — text goes into input for review */}
1240
- {!questImmersive && (
1241
- <button
1242
- onClick={toggleQuestMic}
1243
- className={cn(
1244
- 'p-2 rounded border transition-all',
1245
- questMicStatus !== 'off' && !questImmersive
1246
- ? 'bg-red-500 border-red-400 text-white animate-pulse'
1247
- : 'bg-zinc-800 border-zinc-700 text-zinc-400 hover:text-white'
1248
- )}
1249
- title="Voice → input (review before send)"
1250
- >
1251
- {questMicStatus !== 'off' && !questImmersive ? <MicOff className="w-4 h-4" /> : <Mic className="w-4 h-4" />}
1252
- </button>
1253
- )}
1254
- {/* Immersive mode — continuous listen → auto-send loop */}
1255
- <button
1256
- onClick={toggleQuestImmersive}
1257
- className={cn(
1258
- 'p-2 rounded-full border transition-all',
1259
- questImmersive
1260
- ? 'bg-green-600 border-green-400 text-white shadow-[0_0_12px_rgba(34,197,94,0.5)] animate-pulse'
1261
- : 'bg-zinc-800 border-zinc-700 text-zinc-500 hover:text-green-400 hover:border-green-600'
1262
- )}
1263
- title={questImmersive ? 'Exit immersive voice' : 'Immersive voice (auto-send)'}
1264
- >
1265
- <AudioLines className="w-4 h-4" />
1266
- </button>
1267
- {/* Voice settings gear — opens popover with sensitivity + pause sliders */}
1268
- {questImmersive && (
1269
- <div className="relative">
1270
- <button
1271
- onClick={() => setShowVoiceSettings(!showVoiceSettings)}
1272
- className={cn(
1273
- 'p-1.5 rounded border transition-all',
1274
- showVoiceSettings
1275
- ? 'bg-zinc-700 border-zinc-600 text-zinc-200'
1276
- : 'bg-zinc-800 border-zinc-700 text-zinc-500 hover:text-zinc-300'
1277
- )}
1278
- title="Voice settings"
1279
- >
1280
- <Settings className="w-3.5 h-3.5" />
1281
- </button>
1282
- {showVoiceSettings && (
1283
- <div className="absolute bottom-full right-0 mb-2 p-3 bg-zinc-800 border border-zinc-600 rounded-lg shadow-xl z-50 min-w-[180px]">
1284
- <div className="text-[10px] text-zinc-400 font-medium mb-2">Voice Settings</div>
1285
- <div className="space-y-3">
1286
- <div>
1287
- <div className="flex justify-between text-[9px] text-zinc-500 mb-1">
1288
- <span>Sensitivity</span>
1289
- <span className="font-mono">{immersiveSensitivity}</span>
1290
- </div>
1291
- <input
1292
- type="range"
1293
- min={10}
1294
- max={120}
1295
- value={immersiveSensitivity}
1296
- onChange={(e) => {
1297
- const val = Number(e.target.value);
1298
- setImmersiveSensitivity(val);
1299
- immersiveSensitivityRef.current = val;
1300
- }}
1301
- className="w-full h-1.5 accent-green-500"
1302
- />
1303
- <div className="flex justify-between text-[8px] text-zinc-600">
1304
- <span>Quiet</span>
1305
- <span>Loud</span>
1306
- </div>
1307
- </div>
1308
- <div>
1309
- <div className="flex justify-between text-[9px] text-zinc-500 mb-1">
1310
- <span>Pause before send</span>
1311
- <span className="font-mono">{(immersivePause / 1000).toFixed(1)}s</span>
1312
- </div>
1313
- <input
1314
- type="range"
1315
- min={1000}
1316
- max={5000}
1317
- step={250}
1318
- value={immersivePause}
1319
- onChange={(e) => {
1320
- const val = Number(e.target.value);
1321
- setImmersivePause(val);
1322
- immersivePauseRef.current = val;
1323
- }}
1324
- className="w-full h-1.5 accent-green-500"
1325
- />
1326
- <div className="flex justify-between text-[8px] text-zinc-600">
1327
- <span>Fast</span>
1328
- <span>Patient</span>
1329
- </div>
1330
- </div>
1331
- <label className="flex items-center justify-between cursor-pointer">
1332
- <span className="text-[9px] text-zinc-500">Auto-send</span>
1333
- <div
1334
- onClick={() => {
1335
- const next = !immersiveAutoSend;
1336
- setImmersiveAutoSend(next);
1337
- immersiveAutoSendRef.current = next;
1338
- }}
1339
- className={cn(
1340
- 'w-7 h-4 rounded-full relative transition-colors cursor-pointer',
1341
- immersiveAutoSend ? 'bg-green-600' : 'bg-zinc-600'
1342
- )}
1343
- >
1344
- <div className={cn(
1345
- 'absolute top-0.5 w-3 h-3 rounded-full bg-white transition-transform',
1346
- immersiveAutoSend ? 'translate-x-3.5' : 'translate-x-0.5'
1347
- )} />
1348
- </div>
1349
- </label>
1350
- </div>
1351
- </div>
1352
- )}
1353
- </div>
1354
- )}
1355
- <button
1356
- onClick={() => {
1357
- sendKey(questInput ? questInput + '\r' : '\r');
1358
- setQuestInput('');
1359
- }}
1360
- className="px-3 py-2 text-[11px] font-mono bg-indigo-600 hover:bg-indigo-500 text-white rounded border border-indigo-500 font-medium"
1361
- >
1362
- Send
1363
- </button>
1364
- </div>
1365
- {/* Virtual keys row */}
1366
- <div className="flex items-center gap-1">
1367
- <button
1368
- onClick={() => {
1369
- setQuestKeyboardOpen(true);
1370
- setTimeout(() => questInputRef.current?.focus(), 50);
1371
- }}
1372
- className={cn(
1373
- 'px-2 py-1 text-[10px] font-mono rounded border',
1374
- questKeyboardOpen
1375
- ? 'bg-indigo-600 border-indigo-500 text-white'
1376
- : 'bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border-zinc-700'
1377
- )}
1378
- >
1379
-
1380
- </button>
1381
- <button
1382
- onClick={() => sendKey('\x1b')}
1383
- className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1384
- >
1385
- Esc
1386
- </button>
1387
- <button
1388
- onClick={() => sendKey('\t')}
1389
- className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1390
- >
1391
- Tab
1392
- </button>
1393
- <button
1394
- onClick={() => sendKey('\x03')}
1395
- className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1396
- >
1397
- Ctrl+C
1398
- </button>
1399
- <button
1400
- onClick={() => sendKey('\x1b[A')}
1401
- className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1402
- >
1403
-
1404
- </button>
1405
- <button
1406
- onClick={() => sendKey('\x1b[B')}
1407
- className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1408
- >
1409
-
1410
- </button>
1411
- <div className="flex-1" />
1412
- <button
1413
- onClick={() => setQuestInput('')}
1414
- className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1415
- >
1416
- Clear
1417
- </button>
1418
- </div>
1419
- </div>
1420
- )}
1421
-
1422
- {/* Desktop/Mobile: dictation + immersive voice buttons */}
1423
- {!isQuest && hasWebSpeech && (
1424
- <div
1425
- className="flex items-center justify-end gap-1.5 px-2 py-1 flex-shrink-0 border-t border-zinc-700/50"
1426
- style={{ backgroundColor: `${pane.color}08` }}
1427
- >
1428
- {isImmersiveVoice && (
1429
- <span className="text-[10px] text-green-400 animate-pulse mr-1">
1430
- {voiceStatus === 'listening' ? 'Listening...' : 'Voice active'}
1431
- </span>
1432
- )}
1433
- {/* Dictation: text appears inline, no auto-Enter */}
1434
- <button
1435
- onClick={toggleDictation}
1436
- className={cn(
1437
- 'p-1 rounded border transition-all',
1438
- isImmersiveVoice && !questImmersive
1439
- ? 'bg-green-900/50 border-green-500 text-green-400 hover:bg-red-900/50 hover:border-red-600 hover:text-red-400 shadow-[0_0_8px_rgba(34,197,94,0.3)]'
1440
- : 'bg-zinc-800/50 border-zinc-700 text-zinc-500 hover:text-zinc-300 hover:bg-zinc-700'
1441
- )}
1442
- title="Dictation (text appears, you press Enter)"
1443
- >
1444
- <Mic className="w-3.5 h-3.5" />
1445
- </button>
1446
- {/* Immersive: auto-sends each phrase with Enter */}
1447
- <button
1448
- onClick={toggleDesktopImmersive}
1449
- className={cn(
1450
- 'p-1 rounded-full border transition-all',
1451
- isImmersiveVoice && !questImmersive
1452
- ? 'bg-green-600 border-green-400 text-white shadow-[0_0_10px_rgba(34,197,94,0.5)] animate-pulse'
1453
- : questImmersive ? 'bg-zinc-800/50 border-zinc-700 text-zinc-600'
1454
- : 'bg-zinc-800/50 border-zinc-700 text-zinc-500 hover:text-green-400 hover:border-green-600'
1455
- )}
1456
- title="Immersive voice (auto-send on silence)"
1457
- >
1458
- <AudioLines className="w-3.5 h-3.5" />
1459
- </button>
1460
- </div>
1461
- )}
1462
- </div>
1463
- );
1464
- }
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState, useCallback } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { X, Pencil, Check, RotateCcw, Maximize2, Minimize2, ExternalLink, Globe, Users, Mic, MicOff, Upload, AudioLines, Minus, GripVertical, Settings, GitCompareArrows } from 'lucide-react';
6
+ import { PaneDiffPanel } from './pane-diff-panel';
7
+ import { cn } from '@/lib/utils';
8
+ import { AGENT_TYPES } from '@/lib/agents';
9
+ import { useTier } from '@/hooks/use-tier';
10
+ import { InjectionBadge } from '@/components/cortex/injection-badge';
11
+
12
+ import type { PaneData } from '@/lib/db/queries';
13
+ import 'xterm/css/xterm.css';
14
+
15
+ const WS_PATH = process.env.NEXT_PUBLIC_WS_PATH;
16
+
17
+ interface TerminalPaneProps {
18
+ pane: PaneData;
19
+ onClose: (id: string) => void;
20
+ onUpdate: (id: string, data: Partial<PaneData>) => void;
21
+ isMaximized: boolean;
22
+ onToggleMaximize: (id: string) => void;
23
+ onMinimize?: (id: string) => void;
24
+ onPopout?: (id: string) => void;
25
+ onBrowse?: (cwd: string) => void;
26
+ isPopout?: boolean;
27
+ terminalToken?: string;
28
+ workspaceCollaboration?: boolean;
29
+ dragHandleProps?: Record<string, any>;
30
+ }
31
+
32
+ export function TerminalPane({ pane, onClose, onUpdate, isMaximized, onToggleMaximize, onMinimize, onPopout, onBrowse, isPopout, terminalToken, workspaceCollaboration, dragHandleProps }: TerminalPaneProps) {
33
+ const { hasCortex } = useTier();
34
+ const termRef = useRef<HTMLDivElement>(null);
35
+ const xtermRef = useRef<any>(null);
36
+ const wsRef = useRef<WebSocket | null>(null);
37
+ const fitRef = useRef<any>(null);
38
+
39
+ // RAF-batched write queue — coalesces rapid WebSocket messages into single frame updates
40
+ // to prevent scroll jitter from hundreds of write() calls per second
41
+ const writeQueueRef = useRef('');
42
+ const writeRafRef = useRef<number | null>(null);
43
+
44
+ const queueWrite = (data: string) => {
45
+ writeQueueRef.current += data;
46
+ if (writeRafRef.current === null) {
47
+ writeRafRef.current = requestAnimationFrame(() => {
48
+ writeRafRef.current = null;
49
+ let queued = writeQueueRef.current;
50
+ writeQueueRef.current = '';
51
+ if (queued && xtermRef.current) {
52
+ // Strip \x1b[3J (clear scrollback) — Claude Code sends this when re-rendering
53
+ // its UI, which teleports the viewport to the top. Preserving scrollback is
54
+ // more important for our use case.
55
+ queued = queued.replace(/\x1b\[3J/g, '');
56
+ xtermRef.current.write(queued);
57
+ }
58
+ });
59
+ }
60
+ };
61
+
62
+ // Simple fit — xterm.js internally handles scroll preservation via
63
+ // _suppressOnScrollHandler in Viewport._sync() and isUserScrolling in BufferService
64
+ const safeFit = () => {
65
+ try { fitRef.current?.fit(); } catch { /* ignore */ }
66
+ };
67
+ const [connected, setConnected] = useState(false);
68
+ const [editing, setEditing] = useState(false);
69
+ const [titleValue, setTitleValue] = useState(pane.title);
70
+ const [showColorPicker, setShowColorPicker] = useState(false);
71
+ const [colorPickerPos, setColorPickerPos] = useState({ x: 0, y: 0 });
72
+ const colorPickerRef = useRef<HTMLDivElement>(null);
73
+ const colorPopoverRef = useRef<HTMLDivElement>(null);
74
+ const [exited, setExited] = useState(false);
75
+ const [injectionCount, setInjectionCount] = useState(0);
76
+ const [injectionItems, setInjectionItems] = useState<Array<{ type: string; text: string }>>([]);
77
+
78
+ // Quest browser detection + voice state
79
+ const [isQuest, setIsQuest] = useState(false);
80
+ const [isImmersiveVoice, setIsImmersiveVoice] = useState(false);
81
+ const [voiceStatus, setVoiceStatus] = useState<'idle' | 'listening' | 'transcribing' | 'waiting'>('idle');
82
+ const immersiveRef = useRef(false);
83
+ const [questInput, setQuestInput] = useState('');
84
+ const questInputRef = useRef<HTMLInputElement>(null);
85
+ const [questMicStatus, setQuestMicStatus] = useState<'off' | 'listening' | 'transcribing'>('off');
86
+ const questRecorderRef = useRef<MediaRecorder | null>(null);
87
+ const [questKeyboardOpen, setQuestKeyboardOpen] = useState(false);
88
+ const questWheelThrottleRef = useRef(0);
89
+ const [questImmersive, setQuestImmersive] = useState(false);
90
+ const questImmersiveRef = useRef(false);
91
+ const questWhisperCfgRef = useRef<any>(null);
92
+ const [immersiveSensitivity, setImmersiveSensitivity] = useState(35);
93
+ const immersiveSensitivityRef = useRef(35);
94
+ const [immersivePause, setImmersivePause] = useState(2000);
95
+ const immersivePauseRef = useRef(2000);
96
+ const [immersiveAutoSend, setImmersiveAutoSend] = useState(true);
97
+ const immersiveAutoSendRef = useRef(true);
98
+ const [showVoiceSettings, setShowVoiceSettings] = useState(false);
99
+ const [showDiff, setShowDiff] = useState(false);
100
+ const [diffCount, setDiffCount] = useState<number | null>(null);
101
+ const [vmStatus, setVmStatus] = useState<string | null>(null);
102
+
103
+ useEffect(() => {
104
+ const ua = navigator.userAgent || '';
105
+ setIsQuest(/Quest|Oculus|Pacific/i.test(ua));
106
+ }, []);
107
+
108
+ // Poll for custom model VM status
109
+ useEffect(() => {
110
+ if (!pane.customModelId) return;
111
+ const poll = () => {
112
+ fetch(`/api/proxy/models/${pane.customModelId}/status`)
113
+ .then(r => r.ok ? r.json() : null)
114
+ .then(d => { if (d?.status) setVmStatus(d.status); })
115
+ .catch(() => {});
116
+ };
117
+ poll();
118
+ const id = setInterval(poll, 15000);
119
+ return () => clearInterval(id);
120
+ }, [pane.customModelId]);
121
+
122
+ // Poll for diff file count badge (only if pane has a git baseline)
123
+ useEffect(() => {
124
+ if (!pane.diffBaselineSha) return;
125
+ const poll = () => {
126
+ fetch(`/api/panes/${pane.id}/diff?countOnly=true`)
127
+ .then(r => r.ok ? r.json() : null)
128
+ .then(d => { if (d?.fileCount !== undefined) setDiffCount(d.fileCount); })
129
+ .catch(() => {});
130
+ };
131
+ poll();
132
+ const id = setInterval(poll, 30000);
133
+ return () => clearInterval(id);
134
+ }, [pane.id, pane.diffBaselineSha]);
135
+
136
+ // Use refs for props so the connect function never needs to re-create.
137
+ // This prevents all terminals from reconnecting when parent state changes.
138
+ const paneRef = useRef(pane);
139
+ paneRef.current = pane;
140
+ const onUpdateRef = useRef(onUpdate);
141
+ onUpdateRef.current = onUpdate;
142
+ const terminalTokenRef = useRef(terminalToken);
143
+ terminalTokenRef.current = terminalToken;
144
+
145
+ // Upload handler ref (used inside connect callback which captures refs)
146
+ const uploadFilesRef = useRef<(files: File[]) => void>(() => {});
147
+
148
+ // Auto-reconnect state
149
+ const exitedRef = useRef(false);
150
+ const intentionalCloseRef = useRef(false);
151
+ const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
152
+ const reconnectAttemptsRef = useRef(0);
153
+
154
+ // Stable connect function — never changes reference
155
+ const connect = useCallback(async () => {
156
+ if (!termRef.current) return;
157
+ const currentPane = paneRef.current;
158
+ intentionalCloseRef.current = false;
159
+ reconnectAttemptsRef.current = 0;
160
+
161
+ const { Terminal } = await import('xterm');
162
+ const { FitAddon } = await import('@xterm/addon-fit');
163
+ const { WebLinksAddon } = await import('@xterm/addon-web-links');
164
+
165
+ if (xtermRef.current) {
166
+ xtermRef.current.dispose();
167
+ }
168
+
169
+ const isQuestUA = /Quest|Oculus|Pacific/i.test(navigator.userAgent);
170
+ const term = new Terminal({
171
+ cursorBlink: !isQuestUA,
172
+ disableStdin: isQuestUA, // On Quest, input goes through the visible input field instead
173
+ fontSize: 13,
174
+ fontFamily: "'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, monospace",
175
+ scrollback: 10000, // Cap scrollback buffer (default is unlimited → OOM on heavy output)
176
+ fastScrollModifier: 'alt', // Alt+scroll for fast scrolling
177
+ smoothScrollDuration: 0, // Disable smooth scroll animation (prevents jank on rapid output)
178
+ theme: {
179
+ background: '#0a0a0a',
180
+ foreground: '#e4e4e7',
181
+ cursor: '#6366f1',
182
+ selectionBackground: '#6366f133',
183
+ black: '#18181b',
184
+ red: '#ef4444',
185
+ green: '#22c55e',
186
+ yellow: '#eab308',
187
+ blue: '#3b82f6',
188
+ magenta: '#a855f7',
189
+ cyan: '#06b6d4',
190
+ white: '#e4e4e7',
191
+ brightBlack: '#52525b',
192
+ brightRed: '#f87171',
193
+ brightGreen: '#4ade80',
194
+ brightYellow: '#facc15',
195
+ brightBlue: '#60a5fa',
196
+ brightMagenta: '#c084fc',
197
+ brightCyan: '#22d3ee',
198
+ brightWhite: '#fafafa',
199
+ },
200
+ allowProposedApi: true,
201
+ });
202
+
203
+ const fitAddon = new FitAddon();
204
+ const webLinksAddon = new WebLinksAddon();
205
+ term.loadAddon(fitAddon);
206
+ term.loadAddon(webLinksAddon);
207
+
208
+ // Defer open by one frame so the container has layout dimensions (fixes Windows xterm crash)
209
+ await new Promise<void>((resolve) => requestAnimationFrame(() => {
210
+ if (termRef.current) {
211
+ term.open(termRef.current);
212
+ try { fitAddon.fit(); } catch { /* dimensions may be wrong — corrected below */ }
213
+
214
+ // On Quest: hide xterm's internal textarea so it can't grab focus and pop up the keyboard
215
+ if (isQuestUA && termRef.current) {
216
+ const textarea = termRef.current.querySelector('textarea');
217
+ if (textarea) {
218
+ textarea.setAttribute('readonly', 'true');
219
+ textarea.setAttribute('inputmode', 'none');
220
+ textarea.style.opacity = '0';
221
+ textarea.style.pointerEvents = 'none';
222
+ }
223
+ }
224
+ }
225
+ resolve();
226
+ }));
227
+
228
+ // Ctrl-C: when there's a selection, copy it to clipboard then clear selection.
229
+ // We return false to stop xterm sending ^C (SIGINT), and manually write to clipboard
230
+ // since xterm's selection isn't in the browser's native Selection API.
231
+ // Ctrl-V: return false WITHOUT preventDefault so browser fires its native paste event
232
+ // which xterm's paste handler picks up via onData.
233
+ term.attachCustomKeyEventHandler((ev: KeyboardEvent) => {
234
+ if (ev.type !== 'keydown') return true;
235
+ if (ev.ctrlKey && ev.key === 'c' && term.hasSelection()) {
236
+ const text = term.getSelection();
237
+ if (text && navigator.clipboard?.writeText) {
238
+ navigator.clipboard.writeText(text).catch(() => {
239
+ // Fallback: create a temporary textarea to copy
240
+ const ta = document.createElement('textarea');
241
+ ta.value = text;
242
+ ta.style.position = 'fixed';
243
+ ta.style.opacity = '0';
244
+ document.body.appendChild(ta);
245
+ ta.select();
246
+ document.execCommand('copy');
247
+ document.body.removeChild(ta);
248
+ });
249
+ }
250
+ term.clearSelection();
251
+ return false;
252
+ }
253
+ if (ev.ctrlKey && ev.key === 'v') {
254
+ return false;
255
+ }
256
+ return true;
257
+ });
258
+
259
+ // xterm.js tracks scroll state internally (isUserScrolling flag in BufferService)
260
+ // — no manual tracking needed. write() won't auto-scroll when user is scrolled up.
261
+
262
+ xtermRef.current = term;
263
+ fitRef.current = fitAddon;
264
+
265
+ // The initial fit() above kicks xterm's canvas renderer into life but the
266
+ // CSS grid may not have settled yet, giving wrong cols/rows. A double-rAF
267
+ // waits for a full layout+paint cycle; the 300ms fallback catches slow grids.
268
+ requestAnimationFrame(() => requestAnimationFrame(safeFit));
269
+ setTimeout(safeFit, 300);
270
+
271
+ // Build WebSocket URL from current pane state
272
+ const buildWsUrl = () => {
273
+ const p = paneRef.current;
274
+ const params = new URLSearchParams({
275
+ paneId: p.id,
276
+ cwd: p.cwd,
277
+ agentType: p.agentType || 'shell',
278
+ cols: String(term.cols),
279
+ rows: String(term.rows),
280
+ });
281
+ if (p.claudeSessionId) params.set('agentSession', p.claudeSessionId);
282
+ if (p.customCommand) params.set('customCommand', p.customCommand);
283
+ if (p.nodeId) params.set('nodeId', p.nodeId);
284
+ if ((p as any).customModelId) params.set('customModelId', (p as any).customModelId);
285
+ const token = terminalTokenRef.current;
286
+ if (token) params.set('terminalToken', token);
287
+ const basePath = process.env.SPACES_BASE_PATH || '';
288
+ const wsPath = WS_PATH || `${basePath}/ws`;
289
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
290
+ return `${proto}//${location.host}${wsPath}?${params}`;
291
+ };
292
+
293
+ // Open (or re-open) WebSocket and wire to the existing terminal
294
+ const openWs = () => {
295
+ if (intentionalCloseRef.current) return;
296
+
297
+ // On reconnect, clear terminal so server buffer replay is clean
298
+ if (reconnectAttemptsRef.current > 0) {
299
+ term.write('\x1b[2J\x1b[H');
300
+ term.clear();
301
+ }
302
+
303
+ const ws = new WebSocket(buildWsUrl());
304
+ wsRef.current = ws;
305
+
306
+ ws.onopen = () => {
307
+ setConnected(true);
308
+ setExited(false);
309
+ exitedRef.current = false;
310
+ reconnectAttemptsRef.current = 0;
311
+ // Re-fit after connection the CSS grid layout may not have been
312
+ // stable when the terminal first opened, so cols/rows in the URL
313
+ // can be wrong. A delayed fit + resize message corrects this.
314
+ setTimeout(safeFit, 150);
315
+ };
316
+
317
+ ws.onmessage = (event) => {
318
+ try {
319
+ const msg = JSON.parse(event.data);
320
+ if (msg.type === 'data') {
321
+ queueWrite(msg.data);
322
+ } else if (msg.type === 'exit') {
323
+ setExited(true);
324
+ exitedRef.current = true;
325
+ const reason = msg.reason ? ` — ${msg.reason}` : '';
326
+ term.write(`\r\n\x1b[90m[Process exited with code ${msg.exitCode}${reason}]\x1b[0m\r\n`);
327
+ } else if (msg.type === 'error') {
328
+ term.write(`\r\n\x1b[31m${msg.data}\x1b[0m\r\n`);
329
+ } else if (msg.type === 'session-detected') {
330
+ onUpdateRef.current(paneRef.current.id, { claudeSessionId: msg.sessionId });
331
+ } else if (msg.type === 'cortex-injection') {
332
+ setInjectionCount(msg.count || 0);
333
+ if (msg.items) setInjectionItems(msg.items);
334
+ } else if (msg.type === 'collab-updated') {
335
+ onUpdateRef.current(paneRef.current.id, { isCollaborating: msg.isCollaborating });
336
+ }
337
+ } catch {
338
+ // Raw data
339
+ }
340
+ };
341
+
342
+ ws.onclose = () => {
343
+ setConnected(false);
344
+ // Auto-reconnect if the process didn't exit and we didn't intentionally close
345
+ if (!exitedRef.current && !intentionalCloseRef.current) {
346
+ const attempts = reconnectAttemptsRef.current;
347
+ if (attempts < 20) {
348
+ const delay = Math.min(1000 * Math.pow(1.5, attempts), 30000);
349
+ reconnectAttemptsRef.current = attempts + 1;
350
+ reconnectTimerRef.current = setTimeout(openWs, delay);
351
+ }
352
+ }
353
+ };
354
+
355
+ ws.onerror = () => {
356
+ setConnected(false);
357
+ };
358
+ };
359
+
360
+ // Wire terminal input/resize to current WebSocket via ref
361
+ term.onData((data: string) => {
362
+ // xterm.js auto-scrolls to bottom on user input (scrollOnUserInput option)
363
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
364
+ wsRef.current.send(JSON.stringify({ type: 'data', data }));
365
+ }
366
+ });
367
+
368
+ term.onResize(({ cols, rows }) => {
369
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
370
+ wsRef.current.send(JSON.stringify({ type: 'resize', cols, rows }));
371
+ }
372
+ });
373
+
374
+ openWs();
375
+ }, []); // Empty deps — uses refs for current values
376
+
377
+ // Connect once on mount, clean up on unmount only
378
+ useEffect(() => {
379
+ connect();
380
+ return () => {
381
+ intentionalCloseRef.current = true;
382
+ if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
383
+ wsRef.current?.close();
384
+ xtermRef.current?.dispose();
385
+ };
386
+ }, [connect]);
387
+
388
+ // Resize on container changes. The ResizeObserver fires immediately on
389
+ // observe(), but fitRef may not be set yet (connect() is async). Using rAF
390
+ // coalesces rapid resize events and gives connect() time to finish.
391
+ useEffect(() => {
392
+ let rafId: number;
393
+ const observer = new ResizeObserver(() => {
394
+ cancelAnimationFrame(rafId);
395
+ rafId = requestAnimationFrame(safeFit);
396
+ });
397
+ if (termRef.current) {
398
+ observer.observe(termRef.current);
399
+ }
400
+ return () => { cancelAnimationFrame(rafId); observer.disconnect(); };
401
+ }, []);
402
+
403
+ // Resize when maximized changes or Quest toolbar appears/disappears
404
+ useEffect(() => {
405
+ setTimeout(safeFit, 50);
406
+ }, [isMaximized, isQuest]);
407
+
408
+ const saveTitle = () => {
409
+ onUpdate(pane.id, { title: titleValue });
410
+ setEditing(false);
411
+ };
412
+
413
+ const reconnect = () => {
414
+ intentionalCloseRef.current = true;
415
+ if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
416
+ wsRef.current?.close();
417
+ xtermRef.current?.dispose();
418
+ setExited(false);
419
+ exitedRef.current = false;
420
+ setTimeout(connect, 100);
421
+ };
422
+
423
+ const handlePopout = () => {
424
+ if (onPopout) {
425
+ intentionalCloseRef.current = true;
426
+ if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
427
+ wsRef.current?.close();
428
+ xtermRef.current?.dispose();
429
+ onPopout(pane.id);
430
+ }
431
+ };
432
+
433
+ // ─── Quest toolbar: send keystrokes to terminal ───
434
+ // Debug helper: always logs to console; writes to xterm only when enabled via
435
+ // localStorage.setItem('spaces-voice-debug', '1')
436
+ const debugVoice = (msg: string) => {
437
+ console.log('[Voice]', msg);
438
+ try {
439
+ if (localStorage.getItem('spaces-voice-debug') && xtermRef.current) {
440
+ xtermRef.current.write(`\r\n\x1b[93m[Voice] ${msg}\x1b[0m\r\n`);
441
+ }
442
+ } catch {}
443
+ };
444
+
445
+ const sendKey = (key: string) => {
446
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
447
+ wsRef.current.send(JSON.stringify({ type: 'data', data: key }));
448
+ } else {
449
+ debugVoice(`sendKey failed: WebSocket state=${wsRef.current?.readyState}`);
450
+ }
451
+ };
452
+
453
+ // Send text as a bracketed paste — terminal treats it as a single block
454
+ // instead of processing character-by-character (prevents prompt redraw glitches)
455
+ const sendPaste = (text: string) => {
456
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
457
+ const bracketed = `\x1b[200~${text}\x1b[201~`;
458
+ wsRef.current.send(JSON.stringify({ type: 'data', data: bracketed }));
459
+ } else {
460
+ debugVoice(`sendPaste failed: WebSocket state=${wsRef.current?.readyState}`);
461
+ }
462
+ };
463
+
464
+ // ─── Quest Whisper mic: record → Groq/Whisper → text into input field ───
465
+ const toggleQuestMic = async () => {
466
+ if (questMicStatus !== 'off') {
467
+ // Stop recording
468
+ if (questRecorderRef.current?.state === 'recording') questRecorderRef.current.stop();
469
+ return;
470
+ }
471
+
472
+ setQuestMicStatus('listening');
473
+ try {
474
+ // Get Groq/Whisper config for direct browser call (no server proxy)
475
+ const cfgRes = await fetch('/api/whisper/config');
476
+ const cfg = cfgRes.ok ? await cfgRes.json() : null;
477
+
478
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
479
+ const audioCtx = new AudioContext();
480
+ const source = audioCtx.createMediaStreamSource(stream);
481
+ const analyser = audioCtx.createAnalyser();
482
+ analyser.fftSize = 512;
483
+ source.connect(analyser);
484
+
485
+ const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
486
+ ? 'audio/webm;codecs=opus' : 'audio/webm';
487
+ const recorder = new MediaRecorder(stream, { mimeType });
488
+ const chunks: Blob[] = [];
489
+ let hasSpeech = false;
490
+
491
+ recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
492
+
493
+ recorder.onstop = async () => {
494
+ stream.getTracks().forEach(t => t.stop());
495
+ try { source.disconnect(); } catch {}
496
+ try { audioCtx.close(); } catch {}
497
+ questRecorderRef.current = null;
498
+
499
+ if (!hasSpeech || chunks.length === 0) { setQuestMicStatus('off'); return; }
500
+
501
+ const blob = new Blob(chunks, { type: 'audio/webm' });
502
+ if (blob.size < 500) { setQuestMicStatus('off'); return; }
503
+
504
+ setQuestMicStatus('transcribing');
505
+ try {
506
+ let text = '';
507
+ if (cfg?.apiKey) {
508
+ // Direct call to Groq/OpenAI — skip server proxy for speed
509
+ console.log('[Voice] Transcribing via', cfg.backend || 'direct');
510
+ const form = new FormData();
511
+ form.append('file', blob, 'audio.webm');
512
+ form.append('model', cfg.model);
513
+ form.append('response_format', 'json');
514
+ form.append('language', 'en');
515
+ const res = await fetch(cfg.apiUrl, {
516
+ method: 'POST',
517
+ headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
518
+ body: form,
519
+ });
520
+ if (res.ok) {
521
+ const data = await res.json();
522
+ text = data.text || '';
523
+ } else {
524
+ console.error('[Voice] API error:', res.status, await res.text().catch(() => ''));
525
+ }
526
+ } else {
527
+ // Fallback to server proxy
528
+ console.log('[Voice] Transcribing via server proxy, cfg:', cfg);
529
+ const form = new FormData();
530
+ form.append('audio', blob, 'dictation.webm');
531
+ const res = await fetch('/api/whisper', { method: 'POST', body: form });
532
+ if (res.ok) {
533
+ const data = await res.json();
534
+ text = data.text || '';
535
+ } else {
536
+ console.error('[Voice] Server proxy error:', res.status, await res.text().catch(() => ''));
537
+ }
538
+ }
539
+ console.log('[Voice] Result:', JSON.stringify(text));
540
+ if (text.trim()) {
541
+ setQuestInput(prev => prev ? `${prev} ${text.trim()}` : text.trim());
542
+ }
543
+ } catch (err) {
544
+ console.error('[Voice] Exception:', err);
545
+ }
546
+ setQuestMicStatus('off');
547
+ };
548
+
549
+ // VAD: detect speech, stop after 1s silence (fast trigger)
550
+ const dataArr = new Uint8Array(analyser.frequencyBinCount);
551
+ let lastSpeechTime = 0;
552
+ let speechFrames = 0;
553
+ const startTime = Date.now();
554
+
555
+ const checkVAD = () => {
556
+ if (!questRecorderRef.current || questRecorderRef.current.state !== 'recording') return;
557
+
558
+ analyser.getByteFrequencyData(dataArr);
559
+ const avg = dataArr.reduce((a, b) => a + b, 0) / dataArr.length;
560
+
561
+ if (avg > 20) {
562
+ speechFrames++;
563
+ lastSpeechTime = Date.now();
564
+ if (speechFrames >= 10) hasSpeech = true;
565
+ } else if (!hasSpeech) {
566
+ speechFrames = 0;
567
+ }
568
+
569
+ if (hasSpeech && Date.now() - lastSpeechTime > 1500) {
570
+ questRecorderRef.current.stop();
571
+ return;
572
+ }
573
+ if (!hasSpeech && Date.now() - startTime > 10000) {
574
+ questRecorderRef.current.stop();
575
+ return;
576
+ }
577
+ requestAnimationFrame(checkVAD);
578
+ };
579
+
580
+ recorder.start(250);
581
+ questRecorderRef.current = recorder;
582
+ requestAnimationFrame(checkVAD);
583
+ } catch (e: any) {
584
+ if (xtermRef.current) {
585
+ xtermRef.current.write(`\r\n\x1b[91m[Mic] ${e.name || 'Error'}: ${e.message || 'Failed to access microphone'}\x1b[0m\r\n`);
586
+ }
587
+ setQuestMicStatus('off');
588
+ }
589
+ };
590
+
591
+ // ─── Quest immersive voice: continuous loop — listen → transcribe → send → repeat ───
592
+ const startImmersiveCycle = async () => {
593
+ if (!questImmersiveRef.current) return;
594
+ setQuestMicStatus('listening');
595
+
596
+ try {
597
+ // Fetch config once, cache for subsequent cycles
598
+ if (!questWhisperCfgRef.current) {
599
+ debugVoice('Fetching whisper config...');
600
+ const cfgRes = await fetch('/api/whisper/config');
601
+ if (cfgRes.ok) {
602
+ questWhisperCfgRef.current = await cfgRes.json();
603
+ debugVoice(`Config: backend=${questWhisperCfgRef.current?.backend || 'none'} hasKey=${!!questWhisperCfgRef.current?.apiKey}`);
604
+ } else {
605
+ debugVoice(`Config fetch failed: ${cfgRes.status} ${await cfgRes.text().catch(() => '')}`);
606
+ }
607
+ }
608
+ if (!questImmersiveRef.current) return;
609
+
610
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
611
+ if (!questImmersiveRef.current) { stream.getTracks().forEach(t => t.stop()); return; }
612
+
613
+ const audioCtx = new AudioContext();
614
+ const source = audioCtx.createMediaStreamSource(stream);
615
+ const analyser = audioCtx.createAnalyser();
616
+ analyser.fftSize = 512;
617
+ source.connect(analyser);
618
+
619
+ const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
620
+ ? 'audio/webm;codecs=opus' : 'audio/webm';
621
+ const recorder = new MediaRecorder(stream, { mimeType });
622
+ const chunks: Blob[] = [];
623
+ let hasSpeech = false;
624
+
625
+ recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
626
+
627
+ recorder.onstop = async () => {
628
+ stream.getTracks().forEach(t => t.stop());
629
+ try { source.disconnect(); } catch {}
630
+ try { audioCtx.close(); } catch {}
631
+ questRecorderRef.current = null;
632
+
633
+ if (!questImmersiveRef.current && !questDrainingRef.current) { setQuestMicStatus('off'); return; }
634
+
635
+ // No speech — restart or clean up
636
+ if (!hasSpeech || chunks.length === 0) {
637
+ debugVoice(`No speech detected (hasSpeech=${hasSpeech} chunks=${chunks.length}) — restarting`);
638
+ questDrainingRef.current = false;
639
+ if (questImmersiveRef.current) setTimeout(startImmersiveCycle, 300);
640
+ else setQuestMicStatus('off');
641
+ return;
642
+ }
643
+
644
+ const blob = new Blob(chunks, { type: 'audio/webm' });
645
+ if (blob.size < 500) { debugVoice(`Blob too small (${blob.size}b) — restarting`); setTimeout(startImmersiveCycle, 300); return; }
646
+
647
+ setQuestMicStatus('transcribing');
648
+ const cfg = questWhisperCfgRef.current;
649
+ debugVoice(`Transcribing blob=${blob.size}b hasCfg=${!!cfg} hasKey=${!!cfg?.apiKey}`);
650
+ try {
651
+ let text = '';
652
+ if (cfg?.apiKey) {
653
+ debugVoice(`Calling ${cfg.backend}: ${cfg.apiUrl}`);
654
+ const form = new FormData();
655
+ form.append('file', blob, 'audio.webm');
656
+ form.append('model', cfg.model);
657
+ form.append('response_format', 'json');
658
+ form.append('language', 'en');
659
+ const res = await fetch(cfg.apiUrl, {
660
+ method: 'POST',
661
+ headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
662
+ body: form,
663
+ });
664
+ if (res.ok) {
665
+ text = (await res.json()).text || '';
666
+ debugVoice(`Transcribed: "${text}"`);
667
+ } else {
668
+ const errBody = await res.text().catch(() => '');
669
+ debugVoice(`API error: ${res.status} ${errBody.slice(0, 200)}`);
670
+ }
671
+ } else {
672
+ debugVoice('No API key — using server proxy /api/whisper');
673
+ const form = new FormData();
674
+ form.append('audio', blob, 'dictation.webm');
675
+ const res = await fetch('/api/whisper', { method: 'POST', body: form });
676
+ if (res.ok) {
677
+ text = (await res.json()).text || '';
678
+ debugVoice(`Transcribed: "${text}"`);
679
+ } else {
680
+ const errBody = await res.text().catch(() => '');
681
+ debugVoice(`Proxy error: ${res.status} ${errBody.slice(0, 200)}`);
682
+ }
683
+ }
684
+ debugVoice(`Result: "${text}" ws=${wsRef.current?.readyState} immersive=${questImmersiveRef.current} draining=${questDrainingRef.current}`);
685
+ if (text.trim() && (questImmersiveRef.current || questDrainingRef.current)) {
686
+ if (immersiveAutoSendRef.current) {
687
+ debugVoice(`Sending: "${text.trim()}" + Enter`);
688
+ sendKey(text.trim() + '\r');
689
+ } else {
690
+ debugVoice(`Putting in input: "${text.trim()}"`);
691
+ setQuestInput((prev) => prev ? prev + ' ' + text.trim() : text.trim());
692
+ }
693
+ }
694
+ } catch (err: any) {
695
+ debugVoice(`Exception: ${err.message || err}`);
696
+ }
697
+
698
+ // Loop: restart listening if still immersive, or clean up if draining
699
+ questDrainingRef.current = false;
700
+ if (questImmersiveRef.current) {
701
+ setTimeout(startImmersiveCycle, 800);
702
+ } else {
703
+ setQuestMicStatus('off');
704
+ }
705
+ };
706
+
707
+ // VAD: detect speech, stop after 1s silence
708
+ const dataArr = new Uint8Array(analyser.frequencyBinCount);
709
+ let lastSpeechTime = 0;
710
+ let speechFrames = 0;
711
+ const startTime = Date.now();
712
+
713
+ const checkVAD = () => {
714
+ if (!questRecorderRef.current || questRecorderRef.current.state !== 'recording') return;
715
+ if (!questImmersiveRef.current) { questRecorderRef.current.stop(); return; }
716
+
717
+ analyser.getByteFrequencyData(dataArr);
718
+ const avg = dataArr.reduce((a, b) => a + b, 0) / dataArr.length;
719
+
720
+ if (avg > immersiveSensitivityRef.current) {
721
+ speechFrames++;
722
+ lastSpeechTime = Date.now();
723
+ if (speechFrames >= 15) hasSpeech = true;
724
+ } else if (!hasSpeech) {
725
+ speechFrames = 0;
726
+ }
727
+
728
+ // Configurable silence timeout for immersive mode
729
+ if (hasSpeech && Date.now() - lastSpeechTime > immersivePauseRef.current) {
730
+ questRecorderRef.current.stop();
731
+ return;
732
+ }
733
+ if (!hasSpeech && Date.now() - startTime > 10000) {
734
+ questRecorderRef.current.stop();
735
+ return;
736
+ }
737
+ requestAnimationFrame(checkVAD);
738
+ };
739
+
740
+ recorder.start(250);
741
+ questRecorderRef.current = recorder;
742
+ requestAnimationFrame(checkVAD);
743
+ } catch (e: any) {
744
+ if (xtermRef.current) {
745
+ xtermRef.current.write(`\r\n\x1b[91m[Mic] ${e.name || 'Error'}: ${e.message || 'Failed to access microphone'}\x1b[0m\r\n`);
746
+ }
747
+ if (questImmersiveRef.current) setTimeout(startImmersiveCycle, 2000);
748
+ else setQuestMicStatus('off');
749
+ }
750
+ };
751
+
752
+ const questDrainingRef = useRef(false);
753
+
754
+ const toggleQuestImmersive = () => {
755
+ if (questImmersiveRef.current) {
756
+ // Exit immersive — but let current recording finish sending
757
+ questImmersiveRef.current = false;
758
+ setQuestImmersive(false);
759
+ if (questRecorderRef.current?.state === 'recording') {
760
+ // Flag as draining so onstop still sends but doesn't restart
761
+ questDrainingRef.current = true;
762
+ setQuestMicStatus('transcribing');
763
+ try { questRecorderRef.current.stop(); } catch {}
764
+ } else {
765
+ setQuestMicStatus('off');
766
+ }
767
+ questWhisperCfgRef.current = null;
768
+ } else {
769
+ // Enter immersive
770
+ questImmersiveRef.current = true;
771
+ setQuestImmersive(true);
772
+ startImmersiveCycle();
773
+ }
774
+ };
775
+
776
+ // ─── Voice mode: Web Speech API on desktop, Whisper/Groq mic on Quest ───
777
+ const recognitionRef = useRef<any>(null);
778
+ const isQuestBrowser = typeof navigator !== 'undefined' && /Quest|Oculus|Pacific/i.test(navigator.userAgent);
779
+ const hasWebSpeech = typeof window !== 'undefined' && !isQuestBrowser &&
780
+ ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window);
781
+
782
+ // autoSend: false = text goes to terminal inline (dictation mode)
783
+ // true = each final phrase gets Enter appended (immersive mode)
784
+ const startWebSpeech = (autoSend: boolean) => {
785
+ if (!immersiveRef.current) return;
786
+ const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
787
+ if (!SpeechRecognition) return;
788
+
789
+ const recognition = new SpeechRecognition();
790
+ recognition.continuous = true;
791
+ recognition.interimResults = true;
792
+ recognition.lang = 'en-US';
793
+
794
+ let lastFinalLength = 0;
795
+
796
+ recognition.onresult = (event: any) => {
797
+ if (!immersiveRef.current) return;
798
+ let finalText = '';
799
+ for (let i = 0; i < event.results.length; i++) {
800
+ if (event.results[i].isFinal) finalText += event.results[i][0].transcript;
801
+ }
802
+ if (finalText.length > lastFinalLength) {
803
+ const newText = finalText.slice(lastFinalLength);
804
+ console.log('[Voice] recognized:', JSON.stringify(newText.trim()), 'autoSend:', autoSend, 'ws:', wsRef.current?.readyState);
805
+ if (autoSend && immersiveAutoSendRef.current) {
806
+ // Single write: text + Enter in one message (avoids race between split sends)
807
+ sendKey(newText.trim() + '\r');
808
+ } else {
809
+ sendPaste(newText.trim());
810
+ }
811
+ lastFinalLength = finalText.length;
812
+ }
813
+ };
814
+
815
+ recognition.onerror = (event: any) => {
816
+ if (event.error === 'not-allowed' || event.error === 'service-not-allowed') {
817
+ immersiveRef.current = false;
818
+ setIsImmersiveVoice(false);
819
+ setVoiceStatus('idle');
820
+ return;
821
+ }
822
+ if (immersiveRef.current) setTimeout(() => startWebSpeech(autoSend), 500);
823
+ };
824
+
825
+ recognition.onend = () => {
826
+ if (immersiveRef.current && document.visibilityState === 'visible') {
827
+ setTimeout(() => startWebSpeech(autoSend), 100);
828
+ } else if (!immersiveRef.current) {
829
+ setVoiceStatus('idle');
830
+ }
831
+ // If tab hidden but still immersive, visibility handler will restart
832
+ };
833
+
834
+ recognition.onstart = () => setVoiceStatus('listening');
835
+
836
+ recognitionRef.current = recognition;
837
+ recognition.start();
838
+ };
839
+
840
+ // Dictation mode: text appears inline, no auto-Enter
841
+ const toggleDictation = () => {
842
+ if (immersiveRef.current) {
843
+ immersiveRef.current = false;
844
+ setIsImmersiveVoice(false);
845
+ setVoiceStatus('idle');
846
+ if (recognitionRef.current) { try { recognitionRef.current.abort(); } catch {} recognitionRef.current = null; }
847
+ } else if (hasWebSpeech) {
848
+ immersiveRef.current = true;
849
+ setIsImmersiveVoice(true);
850
+ startWebSpeech(false);
851
+ setTimeout(() => xtermRef.current?.focus(), 100);
852
+ }
853
+ };
854
+
855
+ // Immersive mode: each phrase auto-sends with Enter
856
+ const toggleDesktopImmersive = () => {
857
+ if (immersiveRef.current) {
858
+ immersiveRef.current = false;
859
+ setIsImmersiveVoice(false);
860
+ setVoiceStatus('idle');
861
+ if (recognitionRef.current) { try { recognitionRef.current.abort(); } catch {} recognitionRef.current = null; }
862
+ } else if (hasWebSpeech) {
863
+ immersiveRef.current = true;
864
+ setIsImmersiveVoice(true);
865
+ startWebSpeech(true);
866
+ setTimeout(() => xtermRef.current?.focus(), 100);
867
+ }
868
+ };
869
+
870
+ // Restart voice when tab regains focus (mobile/desktop browsers kill audio in background)
871
+ useEffect(() => {
872
+ const handleVisibility = () => {
873
+ if (document.visibilityState === 'visible' && immersiveRef.current && !recognitionRef.current) {
874
+ startWebSpeech(true);
875
+ }
876
+ };
877
+ document.addEventListener('visibilitychange', handleVisibility);
878
+ return () => {
879
+ document.removeEventListener('visibilitychange', handleVisibility);
880
+ immersiveRef.current = false;
881
+ if (recognitionRef.current) try { recognitionRef.current.abort(); } catch {}
882
+ };
883
+ }, []);
884
+
885
+ // ─── File paste & drag-drop upload ───
886
+ const [uploadStatus, setUploadStatus] = useState<string | null>(null);
887
+ const [isDragging, setIsDragging] = useState(false);
888
+ const uploadTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
889
+
890
+ const uploadFiles = useCallback(async (files: File[]) => {
891
+ if (files.length === 0) return;
892
+ const cwd = paneRef.current.cwd || '/';
893
+ const form = new FormData();
894
+ form.append('dir', cwd);
895
+ for (const f of files) form.append('files', f);
896
+
897
+ const names = files.map(f => f.name).join(', ');
898
+ setUploadStatus(`Uploading ${files.length} file${files.length > 1 ? 's' : ''}...`);
899
+ if (uploadTimeoutRef.current) clearTimeout(uploadTimeoutRef.current);
900
+
901
+ try {
902
+ const res = await fetch('/api/files', { method: 'POST', body: form });
903
+ if (res.ok) {
904
+ const data = await res.json();
905
+ setUploadStatus(`Uploaded ${data.files?.join(', ') || names} to ${cwd}`);
906
+ // Write a note into the terminal so the user/agent knows the file is there
907
+ const term = xtermRef.current;
908
+ if (term) {
909
+ term.write(`\r\n\x1b[90m[Uploaded ${data.files?.join(', ') || names} → ${cwd}]\x1b[0m\r\n`);
910
+ }
911
+ } else {
912
+ setUploadStatus('Upload failed');
913
+ }
914
+ } catch {
915
+ setUploadStatus('Upload failed');
916
+ }
917
+ uploadTimeoutRef.current = setTimeout(() => setUploadStatus(null), 4000);
918
+ }, []);
919
+ uploadFilesRef.current = uploadFiles;
920
+
921
+ // Listen for paste events with files/images.
922
+ // Must use document-level capture because xterm.js creates an internal <textarea>
923
+ // that receives focus and swallows paste events before they reach the container.
924
+ useEffect(() => {
925
+ const el = termRef.current;
926
+ if (!el) return;
927
+
928
+ const handlePaste = (e: ClipboardEvent) => {
929
+ // Only handle pastes when this terminal pane is focused
930
+ if (!el.contains(e.target as Node)) return;
931
+
932
+ const items = e.clipboardData?.items;
933
+ if (!items) return;
934
+ const files: File[] = [];
935
+ for (let i = 0; i < items.length; i++) {
936
+ const item = items[i];
937
+ if (item.kind === 'file') {
938
+ const file = item.getAsFile();
939
+ if (file) files.push(file);
940
+ }
941
+ }
942
+ if (files.length > 0) {
943
+ e.preventDefault();
944
+ e.stopPropagation();
945
+ uploadFilesRef.current(files);
946
+ }
947
+ // If no files, let the normal text paste handler (Ctrl+V) handle it
948
+ };
949
+
950
+ // Capture phase so we intercept before xterm's textarea handler
951
+ document.addEventListener('paste', handlePaste, true);
952
+
953
+ // Drag-and-drop (works directly on the container — xterm doesn't intercept drag events)
954
+ const handleDragOver = (e: DragEvent) => {
955
+ e.preventDefault();
956
+ e.stopPropagation();
957
+ setIsDragging(true);
958
+ };
959
+ const handleDragLeave = (e: DragEvent) => {
960
+ e.preventDefault();
961
+ if (el && !el.contains(e.relatedTarget as Node)) {
962
+ setIsDragging(false);
963
+ }
964
+ };
965
+ const handleDrop = (e: DragEvent) => {
966
+ e.preventDefault();
967
+ e.stopPropagation();
968
+ setIsDragging(false);
969
+ const files: File[] = [];
970
+ if (e.dataTransfer?.files) {
971
+ for (let i = 0; i < e.dataTransfer.files.length; i++) {
972
+ files.push(e.dataTransfer.files[i]);
973
+ }
974
+ }
975
+ if (files.length > 0) uploadFilesRef.current(files);
976
+ };
977
+
978
+ el.addEventListener('dragover', handleDragOver);
979
+ el.addEventListener('dragleave', handleDragLeave);
980
+ el.addEventListener('drop', handleDrop);
981
+ return () => {
982
+ document.removeEventListener('paste', handlePaste, true);
983
+ el.removeEventListener('dragover', handleDragOver);
984
+ el.removeEventListener('dragleave', handleDragLeave);
985
+ el.removeEventListener('drop', handleDrop);
986
+ };
987
+ }, []);
988
+
989
+ // Close color picker on outside click
990
+ useEffect(() => {
991
+ if (!showColorPicker) return;
992
+ const handler = (e: MouseEvent) => {
993
+ const target = e.target as Node;
994
+ if (
995
+ colorPickerRef.current && !colorPickerRef.current.contains(target) &&
996
+ colorPopoverRef.current && !colorPopoverRef.current.contains(target)
997
+ ) {
998
+ setShowColorPicker(false);
999
+ }
1000
+ };
1001
+ document.addEventListener('mousedown', handler);
1002
+ return () => document.removeEventListener('mousedown', handler);
1003
+ }, [showColorPicker]);
1004
+
1005
+ const COLORS = [
1006
+ '#ef4444', '#f97316', '#f59e0b', '#eab308',
1007
+ '#84cc16', '#22c55e', '#14b8a6', '#06b6d4',
1008
+ '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7',
1009
+ '#d946ef', '#ec4899', '#f43f5e', '#78716c',
1010
+ ];
1011
+
1012
+ return (
1013
+ <div className={cn(
1014
+ 'flex flex-col border rounded-lg overflow-hidden',
1015
+ isMaximized ? 'h-full !rounded-none' : 'h-full',
1016
+ 'border-zinc-700'
1017
+ )} style={{ borderColor: `${pane.color}60` }}>
1018
+ {/* Title bar */}
1019
+ <div
1020
+ className="flex items-center gap-2 px-3 py-1.5 text-xs select-none flex-shrink-0"
1021
+ style={{ backgroundColor: `${pane.color}30`, borderBottom: `1px solid ${pane.color}40` }}
1022
+ >
1023
+ {/* Drag handle */}
1024
+ {dragHandleProps && !isPopout && (
1025
+ <div {...dragHandleProps} className="flex-shrink-0 cursor-grab active:cursor-grabbing text-zinc-500 hover:text-zinc-300 -ml-1">
1026
+ <GripVertical className="w-3.5 h-3.5" />
1027
+ </div>
1028
+ )}
1029
+ <div ref={colorPickerRef} className="flex-shrink-0">
1030
+ <button
1031
+ onClick={(e) => {
1032
+ const rect = e.currentTarget.getBoundingClientRect();
1033
+ setColorPickerPos({ x: rect.left, y: rect.bottom + 6 });
1034
+ setShowColorPicker(!showColorPicker);
1035
+ }}
1036
+ className="w-3 h-3 rounded-full hover:ring-2 hover:ring-zinc-500 ring-offset-1 ring-offset-zinc-900 transition-shadow cursor-pointer"
1037
+ style={{ backgroundColor: pane.color }}
1038
+ title="Change color"
1039
+ />
1040
+ {showColorPicker && createPortal(
1041
+ <div
1042
+ ref={colorPopoverRef}
1043
+ className="fixed z-[9999] bg-zinc-800 border border-zinc-600 rounded-xl p-3 shadow-2xl"
1044
+ style={{ left: colorPickerPos.x, top: colorPickerPos.y }}
1045
+ >
1046
+ <div className="grid grid-cols-4 gap-2.5" style={{ width: 132 }}>
1047
+ {COLORS.map((c) => (
1048
+ <button
1049
+ key={c}
1050
+ onClick={() => {
1051
+ onUpdate(pane.id, { color: c });
1052
+ setShowColorPicker(false);
1053
+ }}
1054
+ className={`w-7 h-7 rounded-full border-2 transition-all hover:scale-110 ${
1055
+ pane.color === c ? 'border-white scale-110 shadow-lg' : 'border-transparent hover:border-zinc-500'
1056
+ }`}
1057
+ style={{ backgroundColor: c }}
1058
+ />
1059
+ ))}
1060
+ </div>
1061
+ </div>,
1062
+ document.body
1063
+ )}
1064
+ </div>
1065
+
1066
+ {editing ? (
1067
+ <div className="flex items-center gap-1 flex-1">
1068
+ <input
1069
+ autoFocus
1070
+ value={titleValue}
1071
+ onChange={(e) => setTitleValue(e.target.value)}
1072
+ onKeyDown={(e) => {
1073
+ if (e.key === 'Enter') saveTitle();
1074
+ if (e.key === 'Escape') { setTitleValue(pane.title); setEditing(false); }
1075
+ }}
1076
+ className="flex-1 bg-transparent border border-zinc-600 rounded px-1.5 py-0.5 text-xs focus:outline-none focus:border-indigo-400"
1077
+ />
1078
+ <button onClick={saveTitle} className="text-green-400 hover:text-green-300">
1079
+ <Check className="w-3 h-3" />
1080
+ </button>
1081
+ </div>
1082
+ ) : (
1083
+ <div className="flex items-center gap-1 flex-1 min-w-0 group/title">
1084
+ <span
1085
+ onDoubleClick={() => setEditing(true)}
1086
+ className="truncate text-zinc-300 font-medium cursor-default"
1087
+ >
1088
+ {pane.title}
1089
+ </span>
1090
+ <button
1091
+ onClick={() => setEditing(true)}
1092
+ className="text-zinc-600 hover:text-zinc-300 opacity-0 group-hover/title:opacity-100 transition-opacity flex-shrink-0"
1093
+ title="Rename"
1094
+ >
1095
+ <Pencil className="w-2.5 h-2.5" />
1096
+ </button>
1097
+ </div>
1098
+ )}
1099
+
1100
+ <button
1101
+ onClick={() => onBrowse?.(pane.cwd)}
1102
+ className="text-[10px] text-zinc-500 truncate max-w-[120px] hover:text-amber-400 transition-colors"
1103
+ title={`Browse ${pane.cwd}`}
1104
+ >
1105
+ {pane.cwd.split(/[/\\]/).pop()}
1106
+ </button>
1107
+
1108
+ {pane.agentType && pane.agentType !== 'shell' && (
1109
+ <span
1110
+ className="text-[9px] px-1.5 py-0.5 rounded font-medium flex items-center gap-1"
1111
+ style={{
1112
+ backgroundColor: `${AGENT_TYPES[pane.agentType]?.color || '#6366f1'}20`,
1113
+ color: AGENT_TYPES[pane.agentType]?.color || '#6366f1',
1114
+ }}
1115
+ >
1116
+ {AGENT_TYPES[pane.agentType]?.name || pane.agentType}
1117
+ {pane.customModelId && (
1118
+ <span className="flex items-center gap-1 border-l border-current pl-1 ml-0.5">
1119
+ <span
1120
+ className={cn("w-1.5 h-1.5 rounded-full",
1121
+ vmStatus === 'RUNNING' ? 'bg-green-500' :
1122
+ vmStatus === 'PROVISIONING' || vmStatus === 'STAGING' ? 'bg-yellow-500 animate-pulse' :
1123
+ 'bg-red-500'
1124
+ )}
1125
+ title={vmStatus || 'UNKNOWN'}
1126
+ />
1127
+ </span>
1128
+ )}
1129
+ </span>
1130
+ )}
1131
+
1132
+ {pane.nodeId && (
1133
+ <span className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-blue-500/10 text-blue-400 flex items-center gap-0.5">
1134
+ <Globe className="w-2.5 h-2.5" />
1135
+ remote
1136
+ </span>
1137
+ )}
1138
+
1139
+ {workspaceCollaboration && pane.agentType !== 'shell' && (
1140
+ <button
1141
+ onClick={async () => {
1142
+ const newVal = !pane.isCollaborating;
1143
+ await onUpdate(pane.id, { isCollaborating: newVal } as Partial<PaneData>);
1144
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
1145
+ wsRef.current.send(JSON.stringify({ type: 'collab-toggle', isCollaborating: newVal }));
1146
+ }
1147
+ }}
1148
+ className={`transition-colors ${
1149
+ pane.isCollaborating
1150
+ ? 'text-indigo-400 hover:text-indigo-300'
1151
+ : 'text-zinc-600 hover:text-zinc-400'
1152
+ }`}
1153
+ title={pane.isCollaborating ? 'Collaborating — click to opt out' : 'Not collaborating — click to opt in'}
1154
+ >
1155
+ <Users className="w-3 h-3" />
1156
+ </button>
1157
+ )}
1158
+
1159
+ {!connected && !exited && (
1160
+ <span className="text-[10px] text-yellow-500">connecting...</span>
1161
+ )}
1162
+
1163
+
1164
+ {exited && (
1165
+ <button onClick={reconnect} className="text-zinc-400 hover:text-white" title="Restart">
1166
+ <RotateCcw className="w-3 h-3" />
1167
+ </button>
1168
+ )}
1169
+
1170
+ {!isPopout && onPopout && (
1171
+ <button
1172
+ onClick={handlePopout}
1173
+ className="text-zinc-300 hover:text-white"
1174
+ title="Pop out to new window"
1175
+ >
1176
+ <ExternalLink className="w-3 h-3" />
1177
+ </button>
1178
+ )}
1179
+
1180
+ {pane.diffBaselineSha && (
1181
+ <button
1182
+ onClick={() => setShowDiff(!showDiff)}
1183
+ className={cn('relative text-zinc-300 hover:text-white', showDiff && 'text-indigo-400')}
1184
+ title={showDiff ? 'Hide diff' : 'Review changes'}
1185
+ >
1186
+ <GitCompareArrows className="w-3 h-3" />
1187
+ {diffCount != null && diffCount > 0 && !showDiff && (
1188
+ <span className="absolute -top-1.5 -right-1.5 text-[8px] bg-indigo-500 text-white rounded-full w-3.5 h-3.5 flex items-center justify-center font-bold">
1189
+ {diffCount > 9 ? '9+' : diffCount}
1190
+ </span>
1191
+ )}
1192
+ </button>
1193
+ )}
1194
+
1195
+ {onMinimize && !isPopout && (
1196
+ <button
1197
+ onClick={() => onMinimize(pane.id)}
1198
+ className="text-zinc-300 hover:text-yellow-400"
1199
+ title="Minimize"
1200
+ >
1201
+ <Minus className="w-3 h-3" />
1202
+ </button>
1203
+ )}
1204
+
1205
+ <button
1206
+ onClick={() => onToggleMaximize(pane.id)}
1207
+ className="text-zinc-300 hover:text-white"
1208
+ title={isMaximized ? 'Restore' : 'Maximize'}
1209
+ >
1210
+ {isMaximized ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
1211
+ </button>
1212
+
1213
+ <button
1214
+ onClick={() => {
1215
+ if (confirm('Close this terminal?')) onClose(pane.id);
1216
+ }}
1217
+ className="text-zinc-300 hover:text-red-400"
1218
+ title="Close"
1219
+ >
1220
+ <X className="w-3 h-3" />
1221
+ </button>
1222
+ </div>
1223
+
1224
+ {/* Terminal on Quest, tapping terminal focuses the visible input instead of xterm's hidden textarea */}
1225
+ <div
1226
+ ref={termRef}
1227
+ className="flex-1 relative bg-[#0a0a0a]"
1228
+ style={{ minHeight: 0, flex: 1 }}
1229
+ onClick={isQuest ? (e) => { e.preventDefault(); } : undefined}
1230
+ onWheel={isQuest ? (e) => {
1231
+ // Quest joystick fires wheel events — translate to ↑/↓ arrow keys
1232
+ e.preventDefault();
1233
+ const now = Date.now();
1234
+ if (Math.abs(e.deltaY) > 10 && now - questWheelThrottleRef.current > 200) {
1235
+ questWheelThrottleRef.current = now;
1236
+ sendKey(e.deltaY < 0 ? '\x1b[A' : '\x1b[B');
1237
+ }
1238
+ } : undefined}
1239
+ >
1240
+
1241
+ {/* Drag overlay */}
1242
+ {isDragging && (
1243
+ <div className="absolute inset-0 z-10 bg-indigo-500/10 border-2 border-dashed border-indigo-400 flex items-center justify-center pointer-events-none">
1244
+ <div className="flex items-center gap-2 bg-zinc-900/90 px-4 py-2 rounded-lg text-sm text-indigo-300">
1245
+ <Upload className="w-4 h-4" />
1246
+ Drop files to upload to {pane.cwd.split(/[/\\]/).pop() || pane.cwd}
1247
+ </div>
1248
+ </div>
1249
+ )}
1250
+
1251
+ {/* Upload status toast */}
1252
+ {uploadStatus && (
1253
+ <div className="absolute bottom-2 left-1/2 -translate-x-1/2 z-10 bg-zinc-800/95 border border-zinc-600 px-3 py-1.5 rounded-lg text-xs text-zinc-300 shadow-lg">
1254
+ {uploadStatus}
1255
+ </div>
1256
+ )}
1257
+ </div>
1258
+
1259
+ {/* Diff panel — shown below terminal when toggled */}
1260
+ {showDiff && <PaneDiffPanel paneId={pane.id} onClose={() => setShowDiff(false)} />}
1261
+
1262
+ {/* Quest: input field + virtual keys — prevents xterm hidden textarea layout issues */}
1263
+ {isQuest && (
1264
+ <div
1265
+ className="flex flex-col gap-1 px-2 py-1.5 flex-shrink-0 border-t border-zinc-700"
1266
+ style={{ backgroundColor: `${pane.color}15`, borderTopColor: `${pane.color}30` }}
1267
+ >
1268
+ {/* Text input — readOnly by default to prevent keyboard popup */}
1269
+ <div className="flex items-center gap-1">
1270
+ <input
1271
+ ref={questInputRef}
1272
+ type="text"
1273
+ value={questInput}
1274
+ readOnly={!questKeyboardOpen}
1275
+ onChange={(e) => setQuestInput(e.target.value)}
1276
+ onKeyDown={(e) => {
1277
+ if (e.key === 'Enter') {
1278
+ e.preventDefault();
1279
+ sendKey(questInput ? questInput + '\r' : '\r');
1280
+ setQuestInput('');
1281
+ // Close keyboard after sending
1282
+ setQuestKeyboardOpen(false);
1283
+ questInputRef.current?.blur();
1284
+ }
1285
+ }}
1286
+ onBlur={() => setQuestKeyboardOpen(false)}
1287
+ placeholder={
1288
+ questImmersive && questMicStatus === 'listening' ? 'Speak now...' :
1289
+ questImmersive && questMicStatus === 'transcribing' ? 'Sending...' :
1290
+ questImmersive ? 'Immersive mode active' :
1291
+ questMicStatus === 'listening' ? 'Listening...' :
1292
+ questMicStatus === 'transcribing' ? 'Transcribing...' :
1293
+ questKeyboardOpen ? 'Type here...' : 'Tap mic, keyboard, or type'
1294
+ }
1295
+ className={cn(
1296
+ 'flex-1 text-zinc-200 text-sm px-3 py-2 rounded border focus:outline-none font-mono',
1297
+ questKeyboardOpen
1298
+ ? 'bg-zinc-900 border-zinc-400 placeholder:text-zinc-500'
1299
+ : 'bg-zinc-900/50 border-zinc-700 placeholder:text-zinc-600'
1300
+ )}
1301
+ autoComplete="off"
1302
+ autoCorrect="on"
1303
+ spellCheck={false}
1304
+ />
1305
+ {/* Single-shot mic — text goes into input for review */}
1306
+ {!questImmersive && (
1307
+ <button
1308
+ onClick={toggleQuestMic}
1309
+ className={cn(
1310
+ 'p-2 rounded border transition-all',
1311
+ questMicStatus !== 'off' && !questImmersive
1312
+ ? 'bg-red-500 border-red-400 text-white animate-pulse'
1313
+ : 'bg-zinc-800 border-zinc-700 text-zinc-400 hover:text-white'
1314
+ )}
1315
+ title="Voice → input (review before send)"
1316
+ >
1317
+ {questMicStatus !== 'off' && !questImmersive ? <MicOff className="w-4 h-4" /> : <Mic className="w-4 h-4" />}
1318
+ </button>
1319
+ )}
1320
+ {/* Immersive mode — continuous listen → auto-send loop */}
1321
+ <button
1322
+ onClick={toggleQuestImmersive}
1323
+ className={cn(
1324
+ 'p-2 rounded-full border transition-all',
1325
+ questImmersive
1326
+ ? 'bg-green-600 border-green-400 text-white shadow-[0_0_12px_rgba(34,197,94,0.5)] animate-pulse'
1327
+ : 'bg-zinc-800 border-zinc-700 text-zinc-500 hover:text-green-400 hover:border-green-600'
1328
+ )}
1329
+ title={questImmersive ? 'Exit immersive voice' : 'Immersive voice (auto-send)'}
1330
+ >
1331
+ <AudioLines className="w-4 h-4" />
1332
+ </button>
1333
+ {/* Voice settings gear — opens popover with sensitivity + pause sliders */}
1334
+ {questImmersive && (
1335
+ <div className="relative">
1336
+ <button
1337
+ onClick={() => setShowVoiceSettings(!showVoiceSettings)}
1338
+ className={cn(
1339
+ 'p-1.5 rounded border transition-all',
1340
+ showVoiceSettings
1341
+ ? 'bg-zinc-700 border-zinc-600 text-zinc-200'
1342
+ : 'bg-zinc-800 border-zinc-700 text-zinc-500 hover:text-zinc-300'
1343
+ )}
1344
+ title="Voice settings"
1345
+ >
1346
+ <Settings className="w-3.5 h-3.5" />
1347
+ </button>
1348
+ {showVoiceSettings && (
1349
+ <div className="absolute bottom-full right-0 mb-2 p-3 bg-zinc-800 border border-zinc-600 rounded-lg shadow-xl z-50 min-w-[180px]">
1350
+ <div className="text-[10px] text-zinc-400 font-medium mb-2">Voice Settings</div>
1351
+ <div className="space-y-3">
1352
+ <div>
1353
+ <div className="flex justify-between text-[9px] text-zinc-500 mb-1">
1354
+ <span>Sensitivity</span>
1355
+ <span className="font-mono">{immersiveSensitivity}</span>
1356
+ </div>
1357
+ <input
1358
+ type="range"
1359
+ min={10}
1360
+ max={120}
1361
+ value={immersiveSensitivity}
1362
+ onChange={(e) => {
1363
+ const val = Number(e.target.value);
1364
+ setImmersiveSensitivity(val);
1365
+ immersiveSensitivityRef.current = val;
1366
+ }}
1367
+ className="w-full h-1.5 accent-green-500"
1368
+ />
1369
+ <div className="flex justify-between text-[8px] text-zinc-600">
1370
+ <span>Quiet</span>
1371
+ <span>Loud</span>
1372
+ </div>
1373
+ </div>
1374
+ <div>
1375
+ <div className="flex justify-between text-[9px] text-zinc-500 mb-1">
1376
+ <span>Pause before send</span>
1377
+ <span className="font-mono">{(immersivePause / 1000).toFixed(1)}s</span>
1378
+ </div>
1379
+ <input
1380
+ type="range"
1381
+ min={1000}
1382
+ max={5000}
1383
+ step={250}
1384
+ value={immersivePause}
1385
+ onChange={(e) => {
1386
+ const val = Number(e.target.value);
1387
+ setImmersivePause(val);
1388
+ immersivePauseRef.current = val;
1389
+ }}
1390
+ className="w-full h-1.5 accent-green-500"
1391
+ />
1392
+ <div className="flex justify-between text-[8px] text-zinc-600">
1393
+ <span>Fast</span>
1394
+ <span>Patient</span>
1395
+ </div>
1396
+ </div>
1397
+ <label className="flex items-center justify-between cursor-pointer">
1398
+ <span className="text-[9px] text-zinc-500">Auto-send</span>
1399
+ <div
1400
+ onClick={() => {
1401
+ const next = !immersiveAutoSend;
1402
+ setImmersiveAutoSend(next);
1403
+ immersiveAutoSendRef.current = next;
1404
+ }}
1405
+ className={cn(
1406
+ 'w-7 h-4 rounded-full relative transition-colors cursor-pointer',
1407
+ immersiveAutoSend ? 'bg-green-600' : 'bg-zinc-600'
1408
+ )}
1409
+ >
1410
+ <div className={cn(
1411
+ 'absolute top-0.5 w-3 h-3 rounded-full bg-white transition-transform',
1412
+ immersiveAutoSend ? 'translate-x-3.5' : 'translate-x-0.5'
1413
+ )} />
1414
+ </div>
1415
+ </label>
1416
+ </div>
1417
+ </div>
1418
+ )}
1419
+ </div>
1420
+ )}
1421
+ <button
1422
+ onClick={() => {
1423
+ sendKey(questInput ? questInput + '\r' : '\r');
1424
+ setQuestInput('');
1425
+ }}
1426
+ className="px-3 py-2 text-[11px] font-mono bg-indigo-600 hover:bg-indigo-500 text-white rounded border border-indigo-500 font-medium"
1427
+ >
1428
+ Send
1429
+ </button>
1430
+ </div>
1431
+ {/* Virtual keys row */}
1432
+ <div className="flex items-center gap-1">
1433
+ <button
1434
+ onClick={() => {
1435
+ setQuestKeyboardOpen(true);
1436
+ setTimeout(() => questInputRef.current?.focus(), 50);
1437
+ }}
1438
+ className={cn(
1439
+ 'px-2 py-1 text-[10px] font-mono rounded border',
1440
+ questKeyboardOpen
1441
+ ? 'bg-indigo-600 border-indigo-500 text-white'
1442
+ : 'bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border-zinc-700'
1443
+ )}
1444
+ >
1445
+
1446
+ </button>
1447
+ <button
1448
+ onClick={() => sendKey('\x1b')}
1449
+ className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1450
+ >
1451
+ Esc
1452
+ </button>
1453
+ <button
1454
+ onClick={() => sendKey('\t')}
1455
+ className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1456
+ >
1457
+ Tab
1458
+ </button>
1459
+ <button
1460
+ onClick={() => sendKey('\x03')}
1461
+ className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1462
+ >
1463
+ Ctrl+C
1464
+ </button>
1465
+ <button
1466
+ onClick={() => sendKey('\x1b[A')}
1467
+ className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1468
+ >
1469
+
1470
+ </button>
1471
+ <button
1472
+ onClick={() => sendKey('\x1b[B')}
1473
+ className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1474
+ >
1475
+
1476
+ </button>
1477
+ <div className="flex-1" />
1478
+ <button
1479
+ onClick={() => setQuestInput('')}
1480
+ className="px-2 py-1 text-[10px] font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700"
1481
+ >
1482
+ Clear
1483
+ </button>
1484
+ </div>
1485
+ </div>
1486
+ )}
1487
+
1488
+ {/* Desktop/Mobile: dictation + immersive voice buttons */}
1489
+ {!isQuest && hasWebSpeech && (
1490
+ <div
1491
+ className="flex items-center justify-end gap-1.5 px-2 py-1 flex-shrink-0 border-t border-zinc-700/50"
1492
+ style={{ backgroundColor: `${pane.color}08` }}
1493
+ >
1494
+ {isImmersiveVoice && (
1495
+ <span className="text-[10px] text-green-400 animate-pulse mr-1">
1496
+ {voiceStatus === 'listening' ? 'Listening...' : 'Voice active'}
1497
+ </span>
1498
+ )}
1499
+ {/* Dictation: text appears inline, no auto-Enter */}
1500
+ <button
1501
+ onClick={toggleDictation}
1502
+ className={cn(
1503
+ 'p-1 rounded border transition-all',
1504
+ isImmersiveVoice && !questImmersive
1505
+ ? 'bg-green-900/50 border-green-500 text-green-400 hover:bg-red-900/50 hover:border-red-600 hover:text-red-400 shadow-[0_0_8px_rgba(34,197,94,0.3)]'
1506
+ : 'bg-zinc-800/50 border-zinc-700 text-zinc-500 hover:text-zinc-300 hover:bg-zinc-700'
1507
+ )}
1508
+ title="Dictation (text appears, you press Enter)"
1509
+ >
1510
+ <Mic className="w-3.5 h-3.5" />
1511
+ </button>
1512
+ {/* Immersive: auto-sends each phrase with Enter */}
1513
+ <button
1514
+ onClick={toggleDesktopImmersive}
1515
+ className={cn(
1516
+ 'p-1 rounded-full border transition-all',
1517
+ isImmersiveVoice && !questImmersive
1518
+ ? 'bg-green-600 border-green-400 text-white shadow-[0_0_10px_rgba(34,197,94,0.5)] animate-pulse'
1519
+ : questImmersive ? 'bg-zinc-800/50 border-zinc-700 text-zinc-600'
1520
+ : 'bg-zinc-800/50 border-zinc-700 text-zinc-500 hover:text-green-400 hover:border-green-600'
1521
+ )}
1522
+ title="Immersive voice (auto-send on silence)"
1523
+ >
1524
+ <AudioLines className="w-3.5 h-3.5" />
1525
+ </button>
1526
+ </div>
1527
+ )}
1528
+ </div>
1529
+ );
1530
+ }