@seqyuan/annodex 0.1.10 → 0.1.12

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 (519) hide show
  1. package/app/api/agent/[id]/events/route.ts +94 -0
  2. package/app/api/agent/[id]/route.ts +83 -0
  3. package/app/api/agent/new/route.ts +53 -0
  4. package/app/api/auth/all-providers/route.ts +21 -0
  5. package/app/api/auth/api-key/[provider]/route.ts +7 -0
  6. package/app/api/auth/login/[provider]/route.ts +7 -0
  7. package/app/api/auth/login/route.ts +22 -0
  8. package/app/api/auth/logout/[provider]/route.ts +7 -0
  9. package/app/api/auth/providers/route.ts +15 -0
  10. package/app/api/auth/status/route.ts +6 -0
  11. package/app/api/default-cwd/route.ts +22 -0
  12. package/app/api/files/[...path]/route.ts +621 -0
  13. package/app/api/harness/route.ts +47 -0
  14. package/app/api/home/route.ts +6 -0
  15. package/app/api/internal/runtime/route.ts +26 -0
  16. package/app/api/models/route.ts +67 -0
  17. package/app/api/models-config/discover/route.ts +42 -0
  18. package/app/api/models-config/route.ts +152 -0
  19. package/app/api/models-config/test/route.ts +154 -0
  20. package/app/api/projects/browse/route.ts +51 -0
  21. package/app/api/projects/route.ts +83 -0
  22. package/app/api/reports/[id]/route.ts +108 -0
  23. package/app/api/search/route.ts +122 -0
  24. package/app/api/sessions/[id]/context/route.ts +23 -0
  25. package/app/api/sessions/[id]/route.ts +124 -0
  26. package/app/api/sessions/new/route.ts +5 -0
  27. package/app/api/sessions/route.ts +16 -0
  28. package/app/api/settings/route.ts +51 -0
  29. package/app/api/skills/install/route.ts +249 -0
  30. package/app/api/skills/route.ts +161 -0
  31. package/app/api/skills/search/route.ts +121 -0
  32. package/app/api/soul/route.ts +47 -0
  33. package/app/api/version/route.ts +55 -0
  34. package/app/globals.css +736 -0
  35. package/app/layout.tsx +40 -0
  36. package/app/login/page.tsx +133 -0
  37. package/app/page.tsx +10 -0
  38. package/components/AppShell.tsx +1058 -0
  39. package/components/ChatInput.tsx +1103 -0
  40. package/components/ChatMinimap.tsx +381 -0
  41. package/components/ChatWindow.tsx +576 -0
  42. package/components/CodeMirrorEditor.tsx +137 -0
  43. package/components/ConversationSearch.tsx +369 -0
  44. package/components/DataTableViewer.tsx +248 -0
  45. package/components/FileExplorer.tsx +758 -0
  46. package/components/FileIcons.tsx +241 -0
  47. package/components/FileViewer.tsx +1273 -0
  48. package/components/GlobalFileEditor.tsx +98 -0
  49. package/components/MarkdownRenderer.tsx +331 -0
  50. package/components/MermaidDiagram.tsx +80 -0
  51. package/components/MessageView.tsx +1141 -0
  52. package/components/ModelsConfig.tsx +1991 -0
  53. package/components/ProjectContext.tsx +252 -0
  54. package/components/ProjectFolderPicker.tsx +202 -0
  55. package/components/ProjectsConfig.tsx +288 -0
  56. package/components/ProviderIcons.tsx +91 -0
  57. package/components/ReportPanel.tsx +237 -0
  58. package/components/ResizeHandle.tsx +105 -0
  59. package/components/SessionSidebar.tsx +1464 -0
  60. package/components/SettingsDialog.tsx +287 -0
  61. package/components/SkillsConfig.tsx +1093 -0
  62. package/components/SubagentPanel.tsx +191 -0
  63. package/components/TabBar.tsx +115 -0
  64. package/components/ToolPanel.tsx +131 -0
  65. package/components/WidgetRenderer.tsx +505 -0
  66. package/components/viewers/DocumentToolbar.tsx +78 -0
  67. package/components/viewers/DocxViewer.tsx +97 -0
  68. package/components/viewers/PdfViewer.tsx +206 -0
  69. package/components/viewers/PptxViewer.tsx +240 -0
  70. package/components/viewers/XlsxViewer.tsx +143 -0
  71. package/hooks/useAgentSession.ts +710 -0
  72. package/hooks/useAudio.ts +50 -0
  73. package/hooks/useDragDrop.ts +52 -0
  74. package/hooks/useResizable.ts +60 -0
  75. package/hooks/useTheme.ts +85 -0
  76. package/lib/agent-client.ts +39 -0
  77. package/lib/annodex-config.ts +556 -0
  78. package/lib/auth-token.ts +74 -0
  79. package/lib/auth.ts +90 -0
  80. package/lib/brand.ts +5 -0
  81. package/lib/code-theme.ts +32 -0
  82. package/lib/codex-compat-proxy.ts +1603 -0
  83. package/lib/codex-home.ts +6 -0
  84. package/lib/codex-server.ts +796 -0
  85. package/lib/codex-session.ts +590 -0
  86. package/lib/codex-usage.ts +213 -0
  87. package/lib/file-paths.ts +34 -0
  88. package/lib/model-discovery.ts +379 -0
  89. package/lib/normalize.ts +30 -0
  90. package/lib/npx.ts +87 -0
  91. package/lib/pi-types.ts +49 -0
  92. package/lib/projects.ts +269 -0
  93. package/lib/provider-api.ts +88 -0
  94. package/lib/report-prompt.ts +61 -0
  95. package/lib/report-store.ts +597 -0
  96. package/lib/report-update-parser.ts +66 -0
  97. package/lib/rpc-manager.ts +668 -0
  98. package/lib/runtime-state.ts +117 -0
  99. package/lib/session-reader.ts +903 -0
  100. package/lib/session-runtime.ts +105 -0
  101. package/lib/subagent-progress.ts +279 -0
  102. package/lib/types.ts +241 -0
  103. package/lib/widget-export.ts +318 -0
  104. package/lib/widget-guidelines.ts +288 -0
  105. package/lib/widget-prompt.ts +76 -0
  106. package/lib/widget-utils.ts +523 -0
  107. package/package.json +23 -18
  108. package/postcss.config.mjs +8 -0
  109. package/proxy.ts +64 -0
  110. package/scripts/postinstall.cjs +25 -0
  111. package/tsconfig.json +41 -0
  112. package/.next/BUILD_ID +0 -1
  113. package/.next/app-path-routes-manifest.json +0 -39
  114. package/.next/build-manifest.json +0 -20
  115. package/.next/diagnostics/build-diagnostics.json +0 -6
  116. package/.next/diagnostics/framework.json +0 -1
  117. package/.next/export-marker.json +0 -6
  118. package/.next/images-manifest.json +0 -68
  119. package/.next/next-minimal-server.js.nft.json +0 -1
  120. package/.next/next-server.js.nft.json +0 -1
  121. package/.next/package.json +0 -1
  122. package/.next/prerender-manifest.json +0 -109
  123. package/.next/react-loadable-manifest.json +0 -2320
  124. package/.next/required-server-files.js +0 -343
  125. package/.next/required-server-files.json +0 -343
  126. package/.next/routes-manifest.json +0 -286
  127. package/.next/server/app/_global-error/page.js +0 -32
  128. package/.next/server/app/_global-error/page.js.nft.json +0 -1
  129. package/.next/server/app/_global-error/page_client-reference-manifest.js +0 -1
  130. package/.next/server/app/_global-error.html +0 -1
  131. package/.next/server/app/_global-error.meta +0 -16
  132. package/.next/server/app/_global-error.rsc +0 -14
  133. package/.next/server/app/_global-error.segments/_full.segment.rsc +0 -14
  134. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +0 -5
  135. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +0 -5
  136. package/.next/server/app/_global-error.segments/_head.segment.rsc +0 -5
  137. package/.next/server/app/_global-error.segments/_index.segment.rsc +0 -5
  138. package/.next/server/app/_global-error.segments/_tree.segment.rsc +0 -1
  139. package/.next/server/app/_not-found/page.js +0 -2
  140. package/.next/server/app/_not-found/page.js.nft.json +0 -1
  141. package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  142. package/.next/server/app/_not-found.html +0 -1
  143. package/.next/server/app/_not-found.meta +0 -16
  144. package/.next/server/app/_not-found.rsc +0 -18
  145. package/.next/server/app/_not-found.segments/_full.segment.rsc +0 -18
  146. package/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
  147. package/.next/server/app/_not-found.segments/_index.segment.rsc +0 -5
  148. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
  149. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -5
  150. package/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -4
  151. package/.next/server/app/api/agent/[id]/events/route.js +0 -3
  152. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +0 -1
  153. package/.next/server/app/api/agent/[id]/events/route_client-reference-manifest.js +0 -1
  154. package/.next/server/app/api/agent/[id]/route.js +0 -1
  155. package/.next/server/app/api/agent/[id]/route.js.nft.json +0 -1
  156. package/.next/server/app/api/agent/[id]/route_client-reference-manifest.js +0 -1
  157. package/.next/server/app/api/agent/new/route.js +0 -1
  158. package/.next/server/app/api/agent/new/route.js.nft.json +0 -1
  159. package/.next/server/app/api/agent/new/route_client-reference-manifest.js +0 -1
  160. package/.next/server/app/api/auth/all-providers/route.js +0 -1
  161. package/.next/server/app/api/auth/all-providers/route.js.nft.json +0 -1
  162. package/.next/server/app/api/auth/all-providers/route_client-reference-manifest.js +0 -1
  163. package/.next/server/app/api/auth/api-key/[provider]/route.js +0 -1
  164. package/.next/server/app/api/auth/api-key/[provider]/route.js.nft.json +0 -1
  165. package/.next/server/app/api/auth/api-key/[provider]/route_client-reference-manifest.js +0 -1
  166. package/.next/server/app/api/auth/login/[provider]/route.js +0 -1
  167. package/.next/server/app/api/auth/login/[provider]/route.js.nft.json +0 -1
  168. package/.next/server/app/api/auth/login/[provider]/route_client-reference-manifest.js +0 -1
  169. package/.next/server/app/api/auth/login/route.js +0 -1
  170. package/.next/server/app/api/auth/login/route.js.nft.json +0 -1
  171. package/.next/server/app/api/auth/login/route_client-reference-manifest.js +0 -1
  172. package/.next/server/app/api/auth/logout/[provider]/route.js +0 -1
  173. package/.next/server/app/api/auth/logout/[provider]/route.js.nft.json +0 -1
  174. package/.next/server/app/api/auth/logout/[provider]/route_client-reference-manifest.js +0 -1
  175. package/.next/server/app/api/auth/providers/route.js +0 -1
  176. package/.next/server/app/api/auth/providers/route.js.nft.json +0 -1
  177. package/.next/server/app/api/auth/providers/route_client-reference-manifest.js +0 -1
  178. package/.next/server/app/api/auth/status/route.js +0 -1
  179. package/.next/server/app/api/auth/status/route.js.nft.json +0 -1
  180. package/.next/server/app/api/auth/status/route_client-reference-manifest.js +0 -1
  181. package/.next/server/app/api/default-cwd/route.js +0 -1
  182. package/.next/server/app/api/default-cwd/route.js.nft.json +0 -1
  183. package/.next/server/app/api/default-cwd/route_client-reference-manifest.js +0 -1
  184. package/.next/server/app/api/files/[...path]/route.js +0 -4
  185. package/.next/server/app/api/files/[...path]/route.js.nft.json +0 -1
  186. package/.next/server/app/api/files/[...path]/route_client-reference-manifest.js +0 -1
  187. package/.next/server/app/api/harness/route.js +0 -1
  188. package/.next/server/app/api/harness/route.js.nft.json +0 -1
  189. package/.next/server/app/api/harness/route_client-reference-manifest.js +0 -1
  190. package/.next/server/app/api/home/route.js +0 -1
  191. package/.next/server/app/api/home/route.js.nft.json +0 -1
  192. package/.next/server/app/api/home/route_client-reference-manifest.js +0 -1
  193. package/.next/server/app/api/internal/runtime/route.js +0 -1
  194. package/.next/server/app/api/internal/runtime/route.js.nft.json +0 -1
  195. package/.next/server/app/api/internal/runtime/route_client-reference-manifest.js +0 -1
  196. package/.next/server/app/api/models/route.js +0 -1
  197. package/.next/server/app/api/models/route.js.nft.json +0 -1
  198. package/.next/server/app/api/models/route_client-reference-manifest.js +0 -1
  199. package/.next/server/app/api/models-config/discover/route.js +0 -1
  200. package/.next/server/app/api/models-config/discover/route.js.nft.json +0 -1
  201. package/.next/server/app/api/models-config/discover/route_client-reference-manifest.js +0 -1
  202. package/.next/server/app/api/models-config/route.js +0 -1
  203. package/.next/server/app/api/models-config/route.js.nft.json +0 -1
  204. package/.next/server/app/api/models-config/route_client-reference-manifest.js +0 -1
  205. package/.next/server/app/api/models-config/test/route.js +0 -1
  206. package/.next/server/app/api/models-config/test/route.js.nft.json +0 -1
  207. package/.next/server/app/api/models-config/test/route_client-reference-manifest.js +0 -1
  208. package/.next/server/app/api/projects/browse/route.js +0 -1
  209. package/.next/server/app/api/projects/browse/route.js.nft.json +0 -1
  210. package/.next/server/app/api/projects/browse/route_client-reference-manifest.js +0 -1
  211. package/.next/server/app/api/projects/route.js +0 -1
  212. package/.next/server/app/api/projects/route.js.nft.json +0 -1
  213. package/.next/server/app/api/projects/route_client-reference-manifest.js +0 -1
  214. package/.next/server/app/api/reports/[id]/route.js +0 -10
  215. package/.next/server/app/api/reports/[id]/route.js.nft.json +0 -1
  216. package/.next/server/app/api/reports/[id]/route_client-reference-manifest.js +0 -1
  217. package/.next/server/app/api/search/route.js +0 -1
  218. package/.next/server/app/api/search/route.js.nft.json +0 -1
  219. package/.next/server/app/api/search/route_client-reference-manifest.js +0 -1
  220. package/.next/server/app/api/sessions/[id]/context/route.js +0 -1
  221. package/.next/server/app/api/sessions/[id]/context/route.js.nft.json +0 -1
  222. package/.next/server/app/api/sessions/[id]/context/route_client-reference-manifest.js +0 -1
  223. package/.next/server/app/api/sessions/[id]/route.js +0 -1
  224. package/.next/server/app/api/sessions/[id]/route.js.nft.json +0 -1
  225. package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +0 -1
  226. package/.next/server/app/api/sessions/new/route.js +0 -1
  227. package/.next/server/app/api/sessions/new/route.js.nft.json +0 -1
  228. package/.next/server/app/api/sessions/new/route_client-reference-manifest.js +0 -1
  229. package/.next/server/app/api/sessions/route.js +0 -1
  230. package/.next/server/app/api/sessions/route.js.nft.json +0 -1
  231. package/.next/server/app/api/sessions/route_client-reference-manifest.js +0 -1
  232. package/.next/server/app/api/settings/route.js +0 -1
  233. package/.next/server/app/api/settings/route.js.nft.json +0 -1
  234. package/.next/server/app/api/settings/route_client-reference-manifest.js +0 -1
  235. package/.next/server/app/api/skills/install/route.js +0 -5
  236. package/.next/server/app/api/skills/install/route.js.nft.json +0 -1
  237. package/.next/server/app/api/skills/install/route_client-reference-manifest.js +0 -1
  238. package/.next/server/app/api/skills/route.js +0 -6
  239. package/.next/server/app/api/skills/route.js.nft.json +0 -1
  240. package/.next/server/app/api/skills/route_client-reference-manifest.js +0 -1
  241. package/.next/server/app/api/skills/search/route.js +0 -1
  242. package/.next/server/app/api/skills/search/route.js.nft.json +0 -1
  243. package/.next/server/app/api/skills/search/route_client-reference-manifest.js +0 -1
  244. package/.next/server/app/api/soul/route.js +0 -1
  245. package/.next/server/app/api/soul/route.js.nft.json +0 -1
  246. package/.next/server/app/api/soul/route_client-reference-manifest.js +0 -1
  247. package/.next/server/app/api/version/route.js +0 -1
  248. package/.next/server/app/api/version/route.js.nft.json +0 -1
  249. package/.next/server/app/api/version/route_client-reference-manifest.js +0 -1
  250. package/.next/server/app/index.html +0 -1
  251. package/.next/server/app/index.meta +0 -14
  252. package/.next/server/app/index.rsc +0 -17
  253. package/.next/server/app/index.segments/__PAGE__.segment.rsc +0 -6
  254. package/.next/server/app/index.segments/_full.segment.rsc +0 -17
  255. package/.next/server/app/index.segments/_head.segment.rsc +0 -6
  256. package/.next/server/app/index.segments/_index.segment.rsc +0 -5
  257. package/.next/server/app/index.segments/_tree.segment.rsc +0 -4
  258. package/.next/server/app/login/page.js +0 -2
  259. package/.next/server/app/login/page.js.nft.json +0 -1
  260. package/.next/server/app/login/page_client-reference-manifest.js +0 -1
  261. package/.next/server/app/login.html +0 -1
  262. package/.next/server/app/login.meta +0 -15
  263. package/.next/server/app/login.rsc +0 -22
  264. package/.next/server/app/login.segments/_full.segment.rsc +0 -22
  265. package/.next/server/app/login.segments/_head.segment.rsc +0 -6
  266. package/.next/server/app/login.segments/_index.segment.rsc +0 -5
  267. package/.next/server/app/login.segments/_tree.segment.rsc +0 -4
  268. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +0 -9
  269. package/.next/server/app/login.segments/login.segment.rsc +0 -5
  270. package/.next/server/app/page.js +0 -261
  271. package/.next/server/app/page.js.nft.json +0 -1
  272. package/.next/server/app/page_client-reference-manifest.js +0 -1
  273. package/.next/server/app-paths-manifest.json +0 -39
  274. package/.next/server/chunks/1048.js +0 -1
  275. package/.next/server/chunks/1367.js +0 -77
  276. package/.next/server/chunks/1381.js +0 -1
  277. package/.next/server/chunks/165.js +0 -1
  278. package/.next/server/chunks/1681.js +0 -215
  279. package/.next/server/chunks/1688.js +0 -45
  280. package/.next/server/chunks/1703.js +0 -79
  281. package/.next/server/chunks/1712.js +0 -43
  282. package/.next/server/chunks/1813.js +0 -1
  283. package/.next/server/chunks/2325.js +0 -80
  284. package/.next/server/chunks/258.js +0 -1
  285. package/.next/server/chunks/2671.js +0 -287
  286. package/.next/server/chunks/2778.js +0 -1
  287. package/.next/server/chunks/2943.js +0 -1
  288. package/.next/server/chunks/3031.js +0 -226
  289. package/.next/server/chunks/3181.js +0 -1
  290. package/.next/server/chunks/3493.js +0 -1
  291. package/.next/server/chunks/3672.js +0 -1
  292. package/.next/server/chunks/3701.js +0 -104
  293. package/.next/server/chunks/4013.js +0 -1
  294. package/.next/server/chunks/402.js +0 -2
  295. package/.next/server/chunks/4035.js +0 -80
  296. package/.next/server/chunks/4248.js +0 -153
  297. package/.next/server/chunks/4367.js +0 -1
  298. package/.next/server/chunks/4406.js +0 -141
  299. package/.next/server/chunks/4741.js +0 -18
  300. package/.next/server/chunks/4768.js +0 -1
  301. package/.next/server/chunks/4858.js +0 -148
  302. package/.next/server/chunks/4980.js +0 -1
  303. package/.next/server/chunks/5155.js +0 -5
  304. package/.next/server/chunks/5293.js +0 -166
  305. package/.next/server/chunks/5399.js +0 -8
  306. package/.next/server/chunks/5409.js +0 -1
  307. package/.next/server/chunks/5797.js +0 -93
  308. package/.next/server/chunks/5851.js +0 -36
  309. package/.next/server/chunks/6206.js +0 -1
  310. package/.next/server/chunks/6296.js +0 -1
  311. package/.next/server/chunks/63.js +0 -45
  312. package/.next/server/chunks/6346.js +0 -1
  313. package/.next/server/chunks/6406.js +0 -23
  314. package/.next/server/chunks/642.js +0 -1
  315. package/.next/server/chunks/6429.js +0 -50
  316. package/.next/server/chunks/6729.js +0 -64
  317. package/.next/server/chunks/6907.js +0 -115
  318. package/.next/server/chunks/6980.js +0 -1
  319. package/.next/server/chunks/7073.js +0 -24
  320. package/.next/server/chunks/7233.js +0 -24
  321. package/.next/server/chunks/7307.js +0 -1
  322. package/.next/server/chunks/7362.js +0 -9
  323. package/.next/server/chunks/7567.js +0 -29
  324. package/.next/server/chunks/7765.js +0 -1
  325. package/.next/server/chunks/7890.js +0 -1
  326. package/.next/server/chunks/8065.js +0 -1
  327. package/.next/server/chunks/8238.js +0 -34
  328. package/.next/server/chunks/8276.js +0 -1
  329. package/.next/server/chunks/8336.js +0 -1
  330. package/.next/server/chunks/8477.js +0 -3
  331. package/.next/server/chunks/8490.js +0 -1
  332. package/.next/server/chunks/8916.js +0 -1
  333. package/.next/server/chunks/9280.js +0 -252
  334. package/.next/server/chunks/9315.js +0 -1
  335. package/.next/server/chunks/9537.js +0 -90
  336. package/.next/server/chunks/966.js +0 -1
  337. package/.next/server/chunks/9818.js +0 -21
  338. package/.next/server/chunks/static/media/pdf.worker.min.c476e1a0.mjs +0 -6
  339. package/.next/server/functions-config-manifest.json +0 -16
  340. package/.next/server/interception-route-rewrite-manifest.js +0 -1
  341. package/.next/server/middleware-build-manifest.js +0 -1
  342. package/.next/server/middleware-manifest.json +0 -6
  343. package/.next/server/middleware-react-loadable-manifest.js +0 -1
  344. package/.next/server/middleware.js +0 -18
  345. package/.next/server/middleware.js.nft.json +0 -1
  346. package/.next/server/next-font-manifest.js +0 -1
  347. package/.next/server/next-font-manifest.json +0 -1
  348. package/.next/server/pages/404.html +0 -1
  349. package/.next/server/pages/500.html +0 -1
  350. package/.next/server/pages-manifest.json +0 -4
  351. package/.next/server/prefetch-hints.json +0 -1
  352. package/.next/server/server-reference-manifest.js +0 -1
  353. package/.next/server/server-reference-manifest.json +0 -1
  354. package/.next/server/webpack-runtime.js +0 -1
  355. package/.next/static/6cuMSvcr0FVO-GiK5RJZh/_buildManifest.js +0 -1
  356. package/.next/static/6cuMSvcr0FVO-GiK5RJZh/_ssgManifest.js +0 -1
  357. package/.next/static/chunks/0b9a0da7.9075af772487e743.js +0 -62
  358. package/.next/static/chunks/1413.922d232de90c0c41.js +0 -115
  359. package/.next/static/chunks/1643.467a526a1f24f54d.js +0 -24
  360. package/.next/static/chunks/1852.5543122f11aa7fed.js +0 -1
  361. package/.next/static/chunks/1960.b1e26436d7a5f586.js +0 -1
  362. package/.next/static/chunks/2170a4aa.4213bb2183c9cdf9.js +0 -1
  363. package/.next/static/chunks/2274.6cd173f80a1405a2.js +0 -21
  364. package/.next/static/chunks/2419.347fdfe3c170854d.js +0 -166
  365. package/.next/static/chunks/2619.9aac8983f30c7c8a.js +0 -1
  366. package/.next/static/chunks/2623.d20fabd8e18197c6.js +0 -287
  367. package/.next/static/chunks/2729.f5365061a849d659.js +0 -34
  368. package/.next/static/chunks/2821.934bcf60fbdc28c6.js +0 -1
  369. package/.next/static/chunks/2918becc.abff2ece1de37bc1.js +0 -153
  370. package/.next/static/chunks/2947.114e51cb06d1c01a.js +0 -23
  371. package/.next/static/chunks/3079.4c511fa1144e3adf.js +0 -79
  372. package/.next/static/chunks/3274.208ca44844cd7d95.js +0 -148
  373. package/.next/static/chunks/3308.465a94263d04bfea.js +0 -73
  374. package/.next/static/chunks/3325.e4bfe1ca657f3b5b.js +0 -80
  375. package/.next/static/chunks/3506.2a7eaa08b9f55337.js +0 -90
  376. package/.next/static/chunks/363642f4-043c1475ab9af70e.js +0 -1
  377. package/.next/static/chunks/3794-123fdf632563f469.js +0 -32
  378. package/.next/static/chunks/3837.a755ccfe6f9c1c1c.js +0 -5
  379. package/.next/static/chunks/394.91597771688df6d0.js +0 -1
  380. package/.next/static/chunks/3997.1009c06025691712.js +0 -1
  381. package/.next/static/chunks/4453.91a357dc43c21745.js +0 -1
  382. package/.next/static/chunks/4491.44fdf20580ac72bd.js +0 -24
  383. package/.next/static/chunks/4829.cf1d50e43e6d9db5.js +0 -1
  384. package/.next/static/chunks/498.fe1d9da9ecad6c36.js +0 -1
  385. package/.next/static/chunks/4bd1b696-e356ca5ba0218e27.js +0 -1
  386. package/.next/static/chunks/5019.b5a1a2b8daf17525.js +0 -1
  387. package/.next/static/chunks/5034.8f16c3fa3ce75411.js +0 -1
  388. package/.next/static/chunks/5074.d16651da01ec4e02.js +0 -1
  389. package/.next/static/chunks/51fb665c.0950e1b79671348d.js +0 -45
  390. package/.next/static/chunks/532.5956ed631aff722b.js +0 -9
  391. package/.next/static/chunks/5326.69460442bdcd6cd3.js +0 -1
  392. package/.next/static/chunks/5403.ff110bf5bf600758.js +0 -64
  393. package/.next/static/chunks/547.902a733488cfe3f7.js +0 -77
  394. package/.next/static/chunks/5567.540d7fc108ad6ee5.js +0 -215
  395. package/.next/static/chunks/5590.ef62922166d308b4.js +0 -1
  396. package/.next/static/chunks/5690.9d6eb1edb1399995.js +0 -1
  397. package/.next/static/chunks/5749.25faee4a1e55b854.js +0 -226
  398. package/.next/static/chunks/58bb9007.1ccb6bba34b4c635.js +0 -80
  399. package/.next/static/chunks/6121.f3f43f1896ea0cd9.js +0 -1
  400. package/.next/static/chunks/6600.583c88eef37aa524.js +0 -1
  401. package/.next/static/chunks/6696.a41aec266e657d54.js +0 -141
  402. package/.next/static/chunks/6922.42148793782d2fe7.js +0 -1
  403. package/.next/static/chunks/7006.e191611ffc2b9528.js +0 -43
  404. package/.next/static/chunks/7343.9fbb58204d8ac681.js +0 -1
  405. package/.next/static/chunks/73972abe.25a4cffa03b2bcef.js +0 -119
  406. package/.next/static/chunks/7547.58bda8a2aabba0d4.js +0 -93
  407. package/.next/static/chunks/7648.4ae2f183b4db0353.js +0 -1
  408. package/.next/static/chunks/7874.8db6929b94cdf697.js +0 -1
  409. package/.next/static/chunks/7959.1f20a35df316216a.js +0 -104
  410. package/.next/static/chunks/83.85d62d7fc9850b75.js +0 -29
  411. package/.next/static/chunks/8436.cab94b59cca0a8ff.js +0 -1
  412. package/.next/static/chunks/8451.ff6ff72b57dc52e1.js +0 -1
  413. package/.next/static/chunks/8489.45f22859734f514f.js +0 -36
  414. package/.next/static/chunks/8568.f85d8b36fc9a9037.js +0 -1
  415. package/.next/static/chunks/8771-3e14b6810486df1f.js +0 -1
  416. package/.next/static/chunks/8863.be51033a67436277.js +0 -1
  417. package/.next/static/chunks/90542734.dc1a2723e4f6affb.js +0 -1
  418. package/.next/static/chunks/9500.1488aec06ee78127.js +0 -1
  419. package/.next/static/chunks/9633.155548b5fca6e580.js +0 -1
  420. package/.next/static/chunks/9779.673004a62d70e36a.js +0 -1
  421. package/.next/static/chunks/app/_global-error/page-cc518af6b1ffb191.js +0 -1
  422. package/.next/static/chunks/app/_not-found/page-c72daab99269beff.js +0 -1
  423. package/.next/static/chunks/app/api/agent/[id]/events/route-cc518af6b1ffb191.js +0 -1
  424. package/.next/static/chunks/app/api/agent/[id]/route-cc518af6b1ffb191.js +0 -1
  425. package/.next/static/chunks/app/api/agent/new/route-cc518af6b1ffb191.js +0 -1
  426. package/.next/static/chunks/app/api/auth/all-providers/route-cc518af6b1ffb191.js +0 -1
  427. package/.next/static/chunks/app/api/auth/api-key/[provider]/route-cc518af6b1ffb191.js +0 -1
  428. package/.next/static/chunks/app/api/auth/login/[provider]/route-cc518af6b1ffb191.js +0 -1
  429. package/.next/static/chunks/app/api/auth/login/route-cc518af6b1ffb191.js +0 -1
  430. package/.next/static/chunks/app/api/auth/logout/[provider]/route-cc518af6b1ffb191.js +0 -1
  431. package/.next/static/chunks/app/api/auth/providers/route-cc518af6b1ffb191.js +0 -1
  432. package/.next/static/chunks/app/api/auth/status/route-cc518af6b1ffb191.js +0 -1
  433. package/.next/static/chunks/app/api/default-cwd/route-cc518af6b1ffb191.js +0 -1
  434. package/.next/static/chunks/app/api/files/[...path]/route-cc518af6b1ffb191.js +0 -1
  435. package/.next/static/chunks/app/api/harness/route-cc518af6b1ffb191.js +0 -1
  436. package/.next/static/chunks/app/api/home/route-cc518af6b1ffb191.js +0 -1
  437. package/.next/static/chunks/app/api/internal/runtime/route-cc518af6b1ffb191.js +0 -1
  438. package/.next/static/chunks/app/api/models/route-cc518af6b1ffb191.js +0 -1
  439. package/.next/static/chunks/app/api/models-config/discover/route-cc518af6b1ffb191.js +0 -1
  440. package/.next/static/chunks/app/api/models-config/route-cc518af6b1ffb191.js +0 -1
  441. package/.next/static/chunks/app/api/models-config/test/route-cc518af6b1ffb191.js +0 -1
  442. package/.next/static/chunks/app/api/projects/browse/route-cc518af6b1ffb191.js +0 -1
  443. package/.next/static/chunks/app/api/projects/route-cc518af6b1ffb191.js +0 -1
  444. package/.next/static/chunks/app/api/reports/[id]/route-cc518af6b1ffb191.js +0 -1
  445. package/.next/static/chunks/app/api/search/route-cc518af6b1ffb191.js +0 -1
  446. package/.next/static/chunks/app/api/sessions/[id]/context/route-cc518af6b1ffb191.js +0 -1
  447. package/.next/static/chunks/app/api/sessions/[id]/route-cc518af6b1ffb191.js +0 -1
  448. package/.next/static/chunks/app/api/sessions/new/route-cc518af6b1ffb191.js +0 -1
  449. package/.next/static/chunks/app/api/sessions/route-cc518af6b1ffb191.js +0 -1
  450. package/.next/static/chunks/app/api/settings/route-cc518af6b1ffb191.js +0 -1
  451. package/.next/static/chunks/app/api/skills/install/route-cc518af6b1ffb191.js +0 -1
  452. package/.next/static/chunks/app/api/skills/route-cc518af6b1ffb191.js +0 -1
  453. package/.next/static/chunks/app/api/skills/search/route-cc518af6b1ffb191.js +0 -1
  454. package/.next/static/chunks/app/api/soul/route-cc518af6b1ffb191.js +0 -1
  455. package/.next/static/chunks/app/api/version/route-cc518af6b1ffb191.js +0 -1
  456. package/.next/static/chunks/app/layout-be148b7ae915b22a.js +0 -1
  457. package/.next/static/chunks/app/login/page-ebf0e6de99062783.js +0 -1
  458. package/.next/static/chunks/app/page-c45d98ea81c548ca.js +0 -260
  459. package/.next/static/chunks/d3ac728e.7964f816a1ca64e5.js +0 -1
  460. package/.next/static/chunks/framework-711ef29bc66f648c.js +0 -1
  461. package/.next/static/chunks/main-app-45a0f19af99d61b6.js +0 -1
  462. package/.next/static/chunks/main-f74964b7ae52493e.js +0 -5
  463. package/.next/static/chunks/next/dist/client/components/builtin/app-error-cc518af6b1ffb191.js +0 -1
  464. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cc518af6b1ffb191.js +0 -1
  465. package/.next/static/chunks/next/dist/client/components/builtin/global-error-9bfa08b9491621f2.js +0 -1
  466. package/.next/static/chunks/next/dist/client/components/builtin/not-found-cc518af6b1ffb191.js +0 -1
  467. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cc518af6b1ffb191.js +0 -1
  468. package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  469. package/.next/static/chunks/webpack-fcf4a889ecbd753c.js +0 -1
  470. package/.next/static/css/45029451a1d7255d.css +0 -3
  471. package/.next/static/media/15605e25b523335c-s.woff2 +0 -0
  472. package/.next/static/media/1a3dce5cfb5f7760-s.woff2 +0 -0
  473. package/.next/static/media/1cdd02902f937a18-s.woff2 +0 -0
  474. package/.next/static/media/4c4b3b30b6bcb2be-s.woff2 +0 -0
  475. package/.next/static/media/641a7b8a5800ee0e-s.woff2 +0 -0
  476. package/.next/static/media/7deddc85b7ffd1dc-s.p.woff2 +0 -0
  477. package/.next/static/media/ec14413c594b3356-s.p.woff2 +0 -0
  478. package/.next/static/media/pdf.worker.min.29aaf158.mjs +0 -6
  479. package/.next/trace +0 -74
  480. package/.next/trace-build +0 -1
  481. package/.next/types/app/api/agent/[id]/events/route.ts +0 -351
  482. package/.next/types/app/api/agent/[id]/route.ts +0 -351
  483. package/.next/types/app/api/agent/new/route.ts +0 -351
  484. package/.next/types/app/api/auth/all-providers/route.ts +0 -351
  485. package/.next/types/app/api/auth/api-key/[provider]/route.ts +0 -351
  486. package/.next/types/app/api/auth/login/[provider]/route.ts +0 -351
  487. package/.next/types/app/api/auth/login/route.ts +0 -351
  488. package/.next/types/app/api/auth/logout/[provider]/route.ts +0 -351
  489. package/.next/types/app/api/auth/providers/route.ts +0 -351
  490. package/.next/types/app/api/auth/status/route.ts +0 -351
  491. package/.next/types/app/api/default-cwd/route.ts +0 -351
  492. package/.next/types/app/api/files/[...path]/route.ts +0 -351
  493. package/.next/types/app/api/harness/route.ts +0 -351
  494. package/.next/types/app/api/home/route.ts +0 -351
  495. package/.next/types/app/api/internal/runtime/route.ts +0 -351
  496. package/.next/types/app/api/models/route.ts +0 -351
  497. package/.next/types/app/api/models-config/discover/route.ts +0 -351
  498. package/.next/types/app/api/models-config/route.ts +0 -351
  499. package/.next/types/app/api/models-config/test/route.ts +0 -351
  500. package/.next/types/app/api/projects/browse/route.ts +0 -351
  501. package/.next/types/app/api/projects/route.ts +0 -351
  502. package/.next/types/app/api/reports/[id]/route.ts +0 -351
  503. package/.next/types/app/api/search/route.ts +0 -351
  504. package/.next/types/app/api/sessions/[id]/context/route.ts +0 -351
  505. package/.next/types/app/api/sessions/[id]/route.ts +0 -351
  506. package/.next/types/app/api/sessions/new/route.ts +0 -351
  507. package/.next/types/app/api/sessions/route.ts +0 -351
  508. package/.next/types/app/api/settings/route.ts +0 -351
  509. package/.next/types/app/api/skills/install/route.ts +0 -351
  510. package/.next/types/app/api/skills/route.ts +0 -351
  511. package/.next/types/app/api/skills/search/route.ts +0 -351
  512. package/.next/types/app/api/soul/route.ts +0 -351
  513. package/.next/types/app/api/version/route.ts +0 -351
  514. package/.next/types/app/layout.ts +0 -87
  515. package/.next/types/app/login/page.ts +0 -87
  516. package/.next/types/app/page.ts +0 -87
  517. package/.next/types/package.json +0 -1
  518. package/.next/types/routes.d.ts +0 -106
  519. package/.next/types/validator.ts +0 -376
@@ -0,0 +1,1464 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback, useRef, useMemo } from "react";
4
+ import type { ProjectInfo, SessionInfo } from "@/lib/types";
5
+ import { FileExplorer } from "./FileExplorer";
6
+ import { ConversationSearch } from "./ConversationSearch";
7
+ import { ProjectFolderPicker } from "./ProjectFolderPicker";
8
+ import { APP_NAME } from "@/lib/brand";
9
+
10
+ interface Props {
11
+ selectedSessionId: string | null;
12
+ onSelectSession: (session: SessionInfo, isRestore?: boolean) => void;
13
+ onNewSession?: (sessionId: string, cwd: string) => void;
14
+ initialSessionId?: string | null;
15
+ onInitialRestoreDone?: () => void;
16
+ refreshKey?: number;
17
+ onSessionDeleted?: (sessionId: string) => void;
18
+ selectedCwd?: string | null;
19
+ onCwdChange?: (cwd: string | null) => void;
20
+ onOpenFile?: (filePath: string, fileName: string) => void;
21
+ explorerRefreshKey?: number;
22
+ onAtMention?: (relativePath: string) => void;
23
+ }
24
+
25
+ const SESSION_TRUNCATE_LIMIT = 10;
26
+
27
+ async function getResponseError(label: string, res: Response): Promise<string> {
28
+ const fallback = `${label}: HTTP ${res.status}`;
29
+ try {
30
+ const data = await res.json() as { error?: unknown };
31
+ return typeof data.error === "string" && data.error ? `${label}: ${data.error}` : fallback;
32
+ } catch {
33
+ return fallback;
34
+ }
35
+ }
36
+
37
+ function formatRelativeTime(dateStr: string): string {
38
+ const date = new Date(dateStr);
39
+ const now = new Date();
40
+ const diff = now.getTime() - date.getTime();
41
+ const mins = Math.floor(diff / 60000);
42
+ const hours = Math.floor(diff / 3600000);
43
+ const days = Math.floor(diff / 86400000);
44
+ if (mins < 1) return "just now";
45
+ if (mins < 60) return `${mins}m ago`;
46
+ if (hours < 24) return `${hours}h ago`;
47
+ if (days < 7) return `${days}d ago`;
48
+ return date.toLocaleDateString();
49
+ }
50
+
51
+ function shortenCwd(cwd: string, homeDir?: string): string {
52
+ const path = (homeDir && cwd.startsWith(homeDir)) ? "~" + cwd.slice(homeDir.length) : cwd;
53
+ const sep = path.includes("/") ? "/" : "\\";
54
+ const parts = path.split(sep).filter(Boolean);
55
+ if (parts.length <= 2) return path;
56
+ return "…/" + parts.slice(-2).join(sep);
57
+ }
58
+
59
+ function projectNameFromCwd(cwd: string, homeDir?: string): string {
60
+ if (!cwd) return "No Project";
61
+ const normalized = cwd.replace(/[\\/]+$/, "");
62
+ const sep = normalized.includes("/") ? "/" : "\\";
63
+ const name = normalized.split(sep).filter(Boolean).pop();
64
+ return name || shortenCwd(cwd, homeDir);
65
+ }
66
+
67
+ function getProjectDisplayName(projects: ProjectInfo[], cwd: string | null, homeDir?: string): string {
68
+ if (!cwd) return "Open project...";
69
+ return projects.find((project) => project.cwd === cwd)?.displayName || projectNameFromCwd(cwd, homeDir);
70
+ }
71
+
72
+ function projectLastAccessed(project: ProjectInfo): string {
73
+ return project.lastAccessed ?? project.created ?? "";
74
+ }
75
+
76
+ interface SessionTreeNode {
77
+ session: SessionInfo;
78
+ children: SessionTreeNode[];
79
+ }
80
+
81
+ function buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] {
82
+ const byId = new Map<string, SessionTreeNode>();
83
+ for (const s of sessions) {
84
+ byId.set(s.id, { session: s, children: [] });
85
+ }
86
+
87
+ // Build a map of parentSessionId chains so we can resolve missing ancestors
88
+ const parentOf = new Map<string, string>();
89
+ for (const s of sessions) {
90
+ if (s.parentSessionId) parentOf.set(s.id, s.parentSessionId);
91
+ }
92
+
93
+ // Walk up the parentSessionId chain to find the nearest ancestor that exists in byId
94
+ function resolveAncestor(id: string): string | null {
95
+ let cur = parentOf.get(id);
96
+ const visited = new Set<string>();
97
+ while (cur) {
98
+ if (visited.has(cur)) return null; // cycle guard
99
+ visited.add(cur);
100
+ if (byId.has(cur)) return cur;
101
+ cur = parentOf.get(cur);
102
+ }
103
+ return null;
104
+ }
105
+
106
+ const roots: SessionTreeNode[] = [];
107
+ for (const node of byId.values()) {
108
+ const ancestor = resolveAncestor(node.session.id);
109
+ if (ancestor) {
110
+ byId.get(ancestor)!.children.push(node);
111
+ } else {
112
+ roots.push(node);
113
+ }
114
+ }
115
+
116
+ // Sort each level by modified desc
117
+ const sort = (nodes: SessionTreeNode[]) => {
118
+ nodes.sort((a, b) => b.session.modified.localeCompare(a.session.modified));
119
+ nodes.forEach((n) => sort(n.children));
120
+ };
121
+ sort(roots);
122
+ return roots;
123
+ }
124
+
125
+ function containsSession(node: SessionTreeNode, sessionId: string | null): boolean {
126
+ if (!sessionId) return false;
127
+ if (node.session.id === sessionId) return true;
128
+ return node.children.some((child) => containsSession(child, sessionId));
129
+ }
130
+
131
+ const SCRAMBLE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
132
+
133
+ function useScramble(target: string, running: boolean): string {
134
+ const [display, setDisplay] = useState(target);
135
+ const frameRef = useRef<number | null>(null);
136
+ const iterRef = useRef(0);
137
+
138
+ useEffect(() => {
139
+ if (!running) {
140
+ setDisplay(target);
141
+ return;
142
+ }
143
+ iterRef.current = 0;
144
+ const totalFrames = target.length * 4;
145
+
146
+ const step = () => {
147
+ iterRef.current += 1;
148
+ const progress = iterRef.current / totalFrames;
149
+ const resolved = Math.floor(progress * target.length);
150
+
151
+ setDisplay(
152
+ target
153
+ .split("")
154
+ .map((char, i) => {
155
+ if (char === " ") return " ";
156
+ if (i < resolved) return char;
157
+ return SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
158
+ })
159
+ .join("")
160
+ );
161
+
162
+ if (iterRef.current < totalFrames) {
163
+ frameRef.current = requestAnimationFrame(step);
164
+ } else {
165
+ setDisplay(target);
166
+ }
167
+ };
168
+
169
+ frameRef.current = requestAnimationFrame(step);
170
+ return () => { if (frameRef.current) cancelAnimationFrame(frameRef.current); };
171
+ }, [target, running]);
172
+
173
+ return display;
174
+ }
175
+
176
+ function AnnodexTitle() {
177
+ const [showVersion, setShowVersion] = useState(false);
178
+ const [scrambling, setScrambling] = useState(false);
179
+ const revertTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
180
+
181
+ const target = showVersion ? `v${process.env.NEXT_PUBLIC_APP_VERSION ?? "0.0.0"}` : APP_NAME;
182
+ const display = useScramble(target, scrambling);
183
+
184
+ const triggerScramble = useCallback((toVersion: boolean) => {
185
+ setShowVersion(toVersion);
186
+ setScrambling(true);
187
+ setTimeout(() => setScrambling(false), (toVersion ? 6 : 8) * 4 * (1000 / 60) + 100);
188
+ }, []);
189
+
190
+ const handleClick = useCallback(() => {
191
+ if (revertTimerRef.current) clearTimeout(revertTimerRef.current);
192
+
193
+ const next = !showVersion;
194
+ triggerScramble(next);
195
+
196
+ if (next) {
197
+ revertTimerRef.current = setTimeout(() => triggerScramble(false), 3000);
198
+ }
199
+ }, [showVersion, triggerScramble]);
200
+
201
+ useEffect(() => () => { if (revertTimerRef.current) clearTimeout(revertTimerRef.current); }, []);
202
+
203
+ return (
204
+ <button
205
+ onClick={handleClick}
206
+ style={{
207
+ background: "none", border: "none", padding: 0, cursor: "default",
208
+ fontWeight: 700, fontSize: 15, letterSpacing: "-0.01em",
209
+ color: showVersion ? "var(--accent)" : "var(--text)",
210
+ fontFamily: "var(--font-mono)",
211
+ minWidth: "9ch",
212
+ }}
213
+ >
214
+ {display}
215
+ </button>
216
+ );
217
+ }
218
+
219
+ export function SessionSidebar({ selectedSessionId, onSelectSession, onNewSession, initialSessionId, onInitialRestoreDone, refreshKey, onSessionDeleted, selectedCwd: selectedCwdProp, onCwdChange, onOpenFile, explorerRefreshKey, onAtMention }: Props) {
220
+ const [allSessions, setAllSessions] = useState<SessionInfo[]>([]);
221
+ const [projects, setProjects] = useState<ProjectInfo[]>([]);
222
+ const [loading, setLoading] = useState(true);
223
+ const [isRefreshing, setIsRefreshing] = useState(false);
224
+ const [error, setError] = useState<string | null>(null);
225
+ const [selectedCwd, setSelectedCwd] = useState<string | null>(null);
226
+ const [homeDir, setHomeDir] = useState<string>("");
227
+ const [dropdownOpen, setDropdownOpen] = useState(false);
228
+ const [customPathError, setCustomPathError] = useState<string | null>(null);
229
+ const [folderPickerOpen, setFolderPickerOpen] = useState(false);
230
+ const [conversationSearchOpen, setConversationSearchOpen] = useState(false);
231
+ const [sessionsExpanded, setSessionsExpanded] = useState(false);
232
+ const selectingSessionRef = useRef(false);
233
+ const projectSelectSyncRef = useRef(false);
234
+ const dropdownRef = useRef<HTMLDivElement>(null);
235
+ const restoredRef = useRef(false);
236
+ const [projectOpen, setProjectOpen] = useState(true);
237
+ const [chatsOpen, setChatsOpen] = useState(true);
238
+ const [explorerOpen, setExplorerOpen] = useState(true);
239
+ const [explorerKey, setExplorerKey] = useState(0);
240
+ const [explorerFileFilter, setExplorerFileFilter] = useState("");
241
+ const loadProjects = useCallback(async (showLoading = false) => {
242
+ try {
243
+ if (showLoading) setLoading(true);
244
+ const projectsRes = await fetch("/api/projects?sessionStats=1");
245
+ if (!projectsRes.ok) throw new Error(await getResponseError("projects", projectsRes));
246
+ const projectsData = await projectsRes.json() as { projects: ProjectInfo[] };
247
+ setProjects(projectsData.projects);
248
+ setError(null);
249
+ // no-op — refresh signal handled by re-render
250
+ } catch (e) {
251
+ setError(String(e));
252
+ } finally {
253
+ if (showLoading) setLoading(false);
254
+ }
255
+ }, []);
256
+
257
+ const loadSessionsForCwd = useCallback(async (cwd: string | null, showLoading = false) => {
258
+ if (!cwd) {
259
+ setAllSessions([]);
260
+ return;
261
+ }
262
+ try {
263
+ if (showLoading) setLoading(true);
264
+ const sessionsRes = await fetch(`/api/sessions?cwd=${encodeURIComponent(cwd)}`);
265
+ if (!sessionsRes.ok) throw new Error(await getResponseError("sessions", sessionsRes));
266
+ const sessionsData = await sessionsRes.json() as { sessions: SessionInfo[] };
267
+ setAllSessions(sessionsData.sessions);
268
+ setError(null);
269
+ } catch (e) {
270
+ setError(String(e));
271
+ } finally {
272
+ if (showLoading) setLoading(false);
273
+ }
274
+ }, []);
275
+
276
+ const initialLoadDone = useRef(false);
277
+ useEffect(() => {
278
+ const isFirst = !initialLoadDone.current;
279
+ initialLoadDone.current = true;
280
+ loadProjects(isFirst);
281
+ }, [loadProjects, refreshKey]);
282
+
283
+ useEffect(() => {
284
+ if (explorerRefreshKey !== undefined) setExplorerKey((k) => k + 1);
285
+ }, [explorerRefreshKey]);
286
+
287
+ useEffect(() => {
288
+ fetch("/api/home").then((r) => r.json()).then((d: { home?: string }) => {
289
+ if (d.home) setHomeDir(d.home);
290
+ }).catch(() => {});
291
+ }, []);
292
+
293
+ useEffect(() => {
294
+ if (selectingSessionRef.current) {
295
+ selectingSessionRef.current = false;
296
+ return;
297
+ }
298
+ if (projectSelectSyncRef.current) {
299
+ projectSelectSyncRef.current = false;
300
+ return;
301
+ }
302
+ onCwdChange?.(selectedCwd);
303
+ }, [selectedCwd, onCwdChange]);
304
+
305
+ const registerProject = useCallback(async (cwd: string, create = false): Promise<boolean> => {
306
+ try {
307
+ const res = await fetch("/api/projects", {
308
+ method: "POST",
309
+ headers: { "Content-Type": "application/json" },
310
+ body: JSON.stringify({ cwd, create }),
311
+ });
312
+ const data = await res.json() as { error?: string };
313
+ if (!res.ok || data.error) {
314
+ setCustomPathError(data.error ?? `HTTP ${res.status}`);
315
+ return false;
316
+ }
317
+ void loadProjects(false);
318
+ return true;
319
+ } catch (error) {
320
+ setCustomPathError(error instanceof Error ? error.message : String(error));
321
+ return false;
322
+ }
323
+ }, [loadProjects]);
324
+
325
+ const touchProject = useCallback((cwd: string) => {
326
+ fetch("/api/projects", {
327
+ method: "PATCH",
328
+ headers: { "Content-Type": "application/json" },
329
+ body: JSON.stringify({ cwd, touch: true }),
330
+ }).catch(() => {});
331
+ }, []);
332
+
333
+ useEffect(() => {
334
+ if (!initialSessionId || restoredRef.current) return;
335
+ let cancelled = false;
336
+ fetch("/api/sessions")
337
+ .then((res) => res.ok ? res.json() : null)
338
+ .then((data: { sessions?: SessionInfo[] } | null) => {
339
+ if (cancelled || restoredRef.current) return;
340
+ const target = data?.sessions?.find((session) => session.id === initialSessionId);
341
+ if (!target) return;
342
+ restoredRef.current = true;
343
+ setSelectedCwd(target.cwd);
344
+ touchProject(target.cwd);
345
+ onSelectSession(target, true);
346
+ })
347
+ .finally(() => {
348
+ if (!cancelled && !restoredRef.current) onInitialRestoreDone?.();
349
+ });
350
+ return () => { cancelled = true; };
351
+ }, [initialSessionId, onInitialRestoreDone, onSelectSession, touchProject]);
352
+
353
+ const chooseProject = useCallback((cwd: string) => {
354
+ projectSelectSyncRef.current = true;
355
+ setSelectedCwd(cwd);
356
+ setSessionsExpanded(false);
357
+ setExplorerOpen(true);
358
+ setExplorerFileFilter("");
359
+ setExplorerKey((k) => k + 1);
360
+ onCwdChange?.(cwd);
361
+ touchProject(cwd);
362
+ }, [onCwdChange, touchProject]);
363
+
364
+ // Auto-select cwd and restore session from URL on first load
365
+ useEffect(() => {
366
+ if (allSessions.length === 0 && projects.length === 0) return;
367
+
368
+ if (selectedCwd === null) {
369
+ if (initialSessionId && !restoredRef.current) return;
370
+ const firstProject = projects[0]?.cwd;
371
+ if (firstProject) {
372
+ setSelectedCwd(firstProject);
373
+ touchProject(firstProject);
374
+ }
375
+ }
376
+ }, [allSessions, projects, selectedCwd, initialSessionId, onSelectSession, onInitialRestoreDone, touchProject]);
377
+
378
+ const handleDefaultCwd = useCallback(async () => {
379
+ try {
380
+ const res = await fetch("/api/default-cwd", { method: "POST" });
381
+ const data = await res.json() as { cwd?: string; error?: string };
382
+ if (data.cwd) {
383
+ await registerProject(data.cwd, false);
384
+ chooseProject(data.cwd);
385
+ setDropdownOpen(false);
386
+ }
387
+ } catch {
388
+ // ignore
389
+ }
390
+ }, [chooseProject, registerProject]);
391
+
392
+ const handleFolderPickerSelect = useCallback(async (path: string) => {
393
+ const ok = await registerProject(path, false);
394
+ if (!ok) return;
395
+ chooseProject(path);
396
+ setFolderPickerOpen(false);
397
+ setCustomPathError(null);
398
+ setDropdownOpen(false);
399
+ }, [chooseProject, registerProject]);
400
+
401
+ // Close dropdown on outside click
402
+ useEffect(() => {
403
+ const handler = (e: MouseEvent) => {
404
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
405
+ setDropdownOpen(false);
406
+ setCustomPathError(null);
407
+ }
408
+ };
409
+ document.addEventListener("mousedown", handler);
410
+ return () => document.removeEventListener("mousedown", handler);
411
+ }, []);
412
+
413
+ const effectiveCwd = selectedCwdProp ?? selectedCwd;
414
+
415
+ const handleRefreshProject = useCallback(async () => {
416
+ setIsRefreshing(true);
417
+ try {
418
+ await Promise.all([
419
+ loadProjects(false),
420
+ loadSessionsForCwd(effectiveCwd, false),
421
+ ]);
422
+ } finally {
423
+ setIsRefreshing(false);
424
+ }
425
+ }, [effectiveCwd, loadProjects, loadSessionsForCwd]);
426
+
427
+ useEffect(() => {
428
+ void loadSessionsForCwd(effectiveCwd, false);
429
+ }, [effectiveCwd, loadSessionsForCwd, refreshKey]);
430
+
431
+ const createSessionInCwd = useCallback((cwd: string | null) => {
432
+ if (!cwd) {
433
+ setDropdownOpen(true);
434
+ return;
435
+ }
436
+ // Generate a temporary UUID client-side — no backend call needed.
437
+ // Pi will be spawned lazily when the user sends the first message.
438
+ const tempId = typeof crypto.randomUUID === "function"
439
+ ? crypto.randomUUID()
440
+ : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
441
+ onNewSession?.(tempId, cwd);
442
+ }, [onNewSession]);
443
+
444
+ const handleNewSession = useCallback(() => {
445
+ createSessionInCwd(effectiveCwd);
446
+ }, [createSessionInCwd, effectiveCwd]);
447
+
448
+ const sortedProjects = useMemo(() => {
449
+ const withActive = effectiveCwd && !projects.some((project) => project.cwd === effectiveCwd)
450
+ ? [{
451
+ cwd: effectiveCwd,
452
+ displayName: projectNameFromCwd(effectiveCwd, homeDir),
453
+ chatCount: allSessions.filter((session) => session.cwd === effectiveCwd).length,
454
+ lastAccessed: null,
455
+ created: null,
456
+ exists: true,
457
+ source: "manual" as const,
458
+ }]
459
+ : [];
460
+ return [...withActive, ...projects].sort((a, b) => {
461
+ if (effectiveCwd && a.cwd === effectiveCwd) return -1;
462
+ if (effectiveCwd && b.cwd === effectiveCwd) return 1;
463
+ const aTime = projectLastAccessed(a);
464
+ const bTime = projectLastAccessed(b);
465
+ if (aTime !== bTime) return bTime.localeCompare(aTime);
466
+ return a.displayName.localeCompare(b.displayName);
467
+ });
468
+ }, [allSessions, effectiveCwd, homeDir, projects]);
469
+
470
+ const currentProjectSessions = useMemo(
471
+ () => allSessions
472
+ .filter((session) => session.cwd === effectiveCwd)
473
+ .sort((a, b) => b.modified.localeCompare(a.modified)),
474
+ [allSessions, effectiveCwd],
475
+ );
476
+
477
+ const currentProject = effectiveCwd
478
+ ? sortedProjects.find((project) => project.cwd === effectiveCwd)
479
+ : undefined;
480
+ const currentProjectName = getProjectDisplayName(sortedProjects, effectiveCwd, homeDir);
481
+ const currentChatCount = currentProject?.chatCount ?? currentProjectSessions.length;
482
+ const currentSessionTree = useMemo(() => buildSessionTree(currentProjectSessions), [currentProjectSessions]);
483
+ const flatRootCount = currentSessionTree.length;
484
+ let visibleSessionTree = currentSessionTree;
485
+ if (flatRootCount > SESSION_TRUNCATE_LIMIT && !sessionsExpanded) {
486
+ visibleSessionTree = currentSessionTree.slice(0, SESSION_TRUNCATE_LIMIT);
487
+ const activeRoot = currentSessionTree.find((node) => containsSession(node, selectedSessionId));
488
+ if (activeRoot && !visibleSessionTree.includes(activeRoot)) visibleSessionTree = [...visibleSessionTree, activeRoot];
489
+ }
490
+ const hiddenSessionCount = flatRootCount - visibleSessionTree.length;
491
+
492
+ const handleSelectSessionFromList = useCallback((session: SessionInfo, isRestore = false) => {
493
+ if (session.cwd !== effectiveCwd) {
494
+ selectingSessionRef.current = true;
495
+ setSelectedCwd(session.cwd);
496
+ }
497
+ touchProject(session.cwd);
498
+ onSelectSession(session, isRestore);
499
+ }, [onSelectSession, effectiveCwd, touchProject]);
500
+
501
+ return (
502
+ <div style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
503
+ {/* Header */}
504
+ <div
505
+ style={{
506
+ padding: "12px 10px 10px",
507
+ borderBottom: "1px solid var(--border)",
508
+ flexShrink: 0,
509
+ }}
510
+ >
511
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }}>
512
+ <AnnodexTitle />
513
+ <div style={{ display: "flex", gap: 6 }}>
514
+ <button
515
+ onClick={handleNewSession}
516
+ style={{
517
+ display: "flex", alignItems: "center", justifyContent: "center", gap: 5,
518
+ background: "var(--bg-hover)",
519
+ border: "1px solid var(--border)",
520
+ color: "var(--text-muted)",
521
+ cursor: "pointer",
522
+ height: 32,
523
+ paddingLeft: 10,
524
+ paddingRight: 12,
525
+ borderRadius: 7,
526
+ fontSize: 12,
527
+ fontWeight: 500,
528
+ letterSpacing: "-0.01em",
529
+ flexShrink: 0,
530
+ transition: "background 0.12s, color 0.12s, border-color 0.12s",
531
+ }}
532
+ title={effectiveCwd ? `New session in ${effectiveCwd}` : "Select a project first"}
533
+ onMouseEnter={(e) => {
534
+ e.currentTarget.style.background = "var(--bg-selected)";
535
+ e.currentTarget.style.color = "var(--accent)";
536
+ e.currentTarget.style.borderColor = "rgba(37,99,235,0.35)";
537
+ }}
538
+ onMouseLeave={(e) => {
539
+ e.currentTarget.style.background = "var(--bg-hover)";
540
+ e.currentTarget.style.color = "var(--text-muted)";
541
+ e.currentTarget.style.borderColor = "var(--border)";
542
+ }}
543
+ >
544
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
545
+ <line x1="6" y1="1" x2="6" y2="11" />
546
+ <line x1="1" y1="6" x2="11" y2="6" />
547
+ </svg>
548
+ New
549
+ </button>
550
+ <button
551
+ onClick={() => setConversationSearchOpen(true)}
552
+ style={{
553
+ display: "flex", alignItems: "center", justifyContent: "center",
554
+ background: "var(--bg-hover)",
555
+ border: "1px solid var(--border)",
556
+ color: "var(--text-muted)",
557
+ cursor: "pointer",
558
+ width: 32, height: 32,
559
+ borderRadius: 7,
560
+ padding: 0,
561
+ flexShrink: 0,
562
+ transition: "background 0.12s, color 0.12s, border-color 0.12s",
563
+ }}
564
+ title="Search conversations"
565
+ onMouseEnter={(e) => {
566
+ e.currentTarget.style.background = "var(--bg-selected)";
567
+ e.currentTarget.style.color = "var(--accent)";
568
+ e.currentTarget.style.borderColor = "rgba(37,99,235,0.35)";
569
+ }}
570
+ onMouseLeave={(e) => {
571
+ e.currentTarget.style.background = "var(--bg-hover)";
572
+ e.currentTarget.style.color = "var(--text-muted)";
573
+ e.currentTarget.style.borderColor = "var(--border)";
574
+ }}
575
+ >
576
+ <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
577
+ <circle cx="7" cy="7" r="4.4" />
578
+ <path d="M10.4 10.4 13.2 13.2" />
579
+ </svg>
580
+ </button>
581
+ </div>
582
+ </div>
583
+
584
+ {/* Project picker */}
585
+ <div ref={dropdownRef} style={{ position: "relative" }}>
586
+ <div style={{ marginBottom: projectOpen ? 5 : 0, display: "flex", alignItems: "center", justifyContent: "space-between", color: "var(--text-dim)" }}>
587
+ <button
588
+ type="button"
589
+ onClick={() => {
590
+ setProjectOpen((value) => {
591
+ const next = !value;
592
+ if (!next) setDropdownOpen(false);
593
+ return next;
594
+ });
595
+ void handleRefreshProject();
596
+ }}
597
+ title={projectOpen ? "Collapse project" : "Expand project"}
598
+ aria-expanded={projectOpen}
599
+ style={{
600
+ display: "flex",
601
+ alignItems: "center",
602
+ gap: 6,
603
+ padding: "4px 0",
604
+ background: "none",
605
+ border: "none",
606
+ color: "var(--text-dim)",
607
+ cursor: "pointer",
608
+ fontSize: 10,
609
+ fontWeight: 600,
610
+ letterSpacing: "0.05em",
611
+ textTransform: "uppercase",
612
+ textAlign: "left",
613
+ }}
614
+ >
615
+ <svg
616
+ width="9"
617
+ height="9"
618
+ viewBox="0 0 10 10"
619
+ fill="none"
620
+ stroke="currentColor"
621
+ strokeWidth="1.8"
622
+ strokeLinecap="round"
623
+ strokeLinejoin="round"
624
+ style={{ transform: projectOpen ? "rotate(90deg)" : "none", transition: "transform 0.15s", flexShrink: 0 }}
625
+ >
626
+ <polyline points="3 2 7 5 3 8" />
627
+ </svg>
628
+ Project
629
+ </button>
630
+ <button
631
+ type="button"
632
+ onClick={(event) => {
633
+ event.stopPropagation();
634
+ void handleRefreshProject();
635
+ }}
636
+ disabled={isRefreshing}
637
+ title="Refresh projects and chats"
638
+ aria-label="Refresh projects and chats"
639
+ style={{
640
+ width: 22,
641
+ height: 22,
642
+ display: "flex",
643
+ alignItems: "center",
644
+ justifyContent: "center",
645
+ padding: 0,
646
+ border: "1px solid var(--border)",
647
+ borderRadius: 5,
648
+ background: "var(--bg-panel)",
649
+ color: isRefreshing ? "var(--accent)" : "var(--text-dim)",
650
+ cursor: isRefreshing ? "default" : "pointer",
651
+ flexShrink: 0,
652
+ transition: "color 0.12s, background 0.12s, border-color 0.12s",
653
+ }}
654
+ onMouseEnter={(event) => {
655
+ if (isRefreshing) return;
656
+ event.currentTarget.style.color = "var(--text)";
657
+ event.currentTarget.style.background = "var(--bg-hover)";
658
+ }}
659
+ onMouseLeave={(event) => {
660
+ if (isRefreshing) return;
661
+ event.currentTarget.style.color = "var(--text-dim)";
662
+ event.currentTarget.style.background = "var(--bg-panel)";
663
+ }}
664
+ >
665
+ <svg
666
+ width="12"
667
+ height="12"
668
+ viewBox="0 0 24 24"
669
+ fill="none"
670
+ stroke="currentColor"
671
+ strokeWidth="2"
672
+ strokeLinecap="round"
673
+ strokeLinejoin="round"
674
+ style={{ animation: isRefreshing ? "spin 0.9s linear infinite" : undefined }}
675
+ >
676
+ <path d="M21 12a9 9 0 0 1-15.5 6.2" />
677
+ <path d="M3 12A9 9 0 0 1 18.5 5.8" />
678
+ <path d="M7 18H5.5v1.5" />
679
+ <path d="M17 6h1.5V4.5" />
680
+ </svg>
681
+ </button>
682
+ </div>
683
+
684
+ {projectOpen && (
685
+ <>
686
+ <button
687
+ onClick={() => setDropdownOpen((v) => !v)}
688
+ style={{
689
+ width: "100%",
690
+ display: "flex",
691
+ alignItems: "center",
692
+ gap: 9,
693
+ padding: "8px 10px",
694
+ background: effectiveCwd ? "var(--bg-hover)" : "rgba(37,99,235,0.06)",
695
+ border: effectiveCwd ? "1px solid var(--border)" : "1px solid rgba(37,99,235,0.4)",
696
+ borderRadius: 7,
697
+ cursor: "pointer",
698
+ fontSize: 12,
699
+ color: "var(--text)",
700
+ textAlign: "left",
701
+ transition: "border-color 0.15s, background 0.15s",
702
+ }}
703
+ >
704
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={{ color: effectiveCwd ? "var(--accent)" : "var(--text-dim)", flexShrink: 0 }}>
705
+ <path d="M3 6.5A2.5 2.5 0 0 1 5.5 4H10l2 2.5h6.5A2.5 2.5 0 0 1 21 9v8.5A2.5 2.5 0 0 1 18.5 20h-13A2.5 2.5 0 0 1 3 17.5v-11Z" />
706
+ </svg>
707
+ <span style={{ flex: 1, minWidth: 0 }}>
708
+ <span style={{ display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: 12, fontWeight: 650, color: effectiveCwd ? "var(--text)" : "var(--text-dim)" }}>
709
+ {effectiveCwd ? currentProjectName : (initialSessionId && !restoredRef.current ? "" : "Open project...")}
710
+ </span>
711
+ {effectiveCwd && (
712
+ <span style={{ display: "block", marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--text-dim)" }} title={effectiveCwd}>
713
+ {shortenCwd(effectiveCwd, homeDir)}
714
+ </span>
715
+ )}
716
+ </span>
717
+ {effectiveCwd && (
718
+ <span style={{ flexShrink: 0, color: "var(--text-dim)", fontSize: 10, whiteSpace: "nowrap" }}>
719
+ {currentChatCount} chats
720
+ </span>
721
+ )}
722
+ <svg width="11" height="11" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={{ color: "var(--text-dim)", flexShrink: 0, transform: dropdownOpen ? "rotate(180deg)" : "none", transition: "transform 0.15s" }}>
723
+ <polyline points="2 3.5 5 6.5 8 3.5" />
724
+ </svg>
725
+ </button>
726
+
727
+ {dropdownOpen && (
728
+ <div
729
+ style={{
730
+ position: "absolute",
731
+ top: "calc(100% + 4px)",
732
+ left: 0,
733
+ right: 0,
734
+ zIndex: 100,
735
+ background: "var(--bg)",
736
+ border: "1px solid var(--border)",
737
+ borderRadius: 8,
738
+ boxShadow: "0 6px 20px rgba(0,0,0,0.10)",
739
+ overflow: "hidden",
740
+ }}
741
+ >
742
+ <div style={{ maxHeight: 260, overflowY: "auto" }}>
743
+ {sortedProjects.length === 0 && (
744
+ <div style={{ padding: "10px 12px", color: "var(--text-muted)", fontSize: 12 }}>
745
+ No projects yet
746
+ </div>
747
+ )}
748
+ {sortedProjects.map((project) => {
749
+ const active = project.cwd === effectiveCwd;
750
+ const last = projectLastAccessed(project);
751
+ return (
752
+ <button
753
+ key={project.cwd}
754
+ onClick={() => {
755
+ chooseProject(project.cwd);
756
+ setCustomPathError(null);
757
+ setDropdownOpen(false);
758
+ }}
759
+ style={{
760
+ display: "flex",
761
+ alignItems: "center",
762
+ gap: 8,
763
+ width: "100%",
764
+ padding: "8px 10px",
765
+ background: active ? "var(--bg-selected)" : "none",
766
+ border: "none",
767
+ borderBottom: "1px solid var(--border)",
768
+ color: active ? "var(--text)" : "var(--text-muted)",
769
+ cursor: "pointer",
770
+ textAlign: "left",
771
+ fontSize: 11,
772
+ }}
773
+ title={project.cwd}
774
+ >
775
+ {active ? (
776
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="var(--accent)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
777
+ <polyline points="1.5 5 4 7.5 8.5 2.5" />
778
+ </svg>
779
+ ) : (
780
+ <span style={{ width: 10, flexShrink: 0 }} />
781
+ )}
782
+ <span style={{ flex: 1, minWidth: 0 }}>
783
+ <span style={{ display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: active ? "var(--text)" : "var(--text-muted)", fontWeight: active ? 650 : 500 }}>
784
+ {project.displayName}
785
+ </span>
786
+ <span style={{ display: "block", marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--text-dim)", fontSize: 10 }}>
787
+ {project.chatCount} chats{last ? ` / ${formatRelativeTime(last)}` : ""}{!project.exists ? " / missing" : ""}
788
+ </span>
789
+ </span>
790
+ </button>
791
+ );
792
+ })}
793
+ </div>
794
+
795
+ <div style={{ borderTop: sortedProjects.length > 0 ? "1px solid var(--border)" : "none" }}>
796
+ <button
797
+ onClick={(e) => { e.stopPropagation(); handleDefaultCwd(); }}
798
+ style={{
799
+ display: "flex",
800
+ alignItems: "center",
801
+ gap: 7,
802
+ width: "100%",
803
+ padding: "8px 10px",
804
+ background: "none",
805
+ border: "none",
806
+ color: "var(--text-muted)",
807
+ cursor: "pointer",
808
+ textAlign: "left",
809
+ fontSize: 11,
810
+ }}
811
+ >
812
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
813
+ <path d="M1 3A1 1 0 0 1 2 2H4L5 3.5H8.5a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-7A.5.5 0 0 1 1 8V3Z" />
814
+ </svg>
815
+ <span>Use default directory</span>
816
+ </button>
817
+ <button
818
+ onClick={(e) => {
819
+ e.stopPropagation();
820
+ setCustomPathError(null);
821
+ setFolderPickerOpen(true);
822
+ }}
823
+ style={{
824
+ display: "flex",
825
+ alignItems: "center",
826
+ gap: 7,
827
+ width: "100%",
828
+ padding: "8px 10px",
829
+ background: "none",
830
+ border: "none",
831
+ color: "var(--text-muted)",
832
+ cursor: "pointer",
833
+ textAlign: "left",
834
+ fontSize: 11,
835
+ }}
836
+ >
837
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
838
+ <path d="M1 3A1 1 0 0 1 2 2h1.5L4.5 3H8a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V3Z" />
839
+ <path d="M5 4.5v3M3.5 6h3" />
840
+ </svg>
841
+ <span>Select project path...</span>
842
+ </button>
843
+ {customPathError && (
844
+ <div style={{ padding: "0 10px 8px 27px", color: "#f87171", fontSize: 10, lineHeight: 1.4 }}>
845
+ {customPathError}
846
+ </div>
847
+ )}
848
+ </div>
849
+ </div>
850
+ )}
851
+ </>
852
+ )}
853
+ </div>
854
+
855
+ </div>
856
+
857
+ <ConversationSearch
858
+ open={conversationSearchOpen}
859
+ cwd={effectiveCwd}
860
+ homeDir={homeDir}
861
+ onClose={() => setConversationSearchOpen(false)}
862
+ onSelectSession={(session) => {
863
+ handleSelectSessionFromList(session);
864
+ }}
865
+ />
866
+
867
+ {/* Chats section */}
868
+ {effectiveCwd && (
869
+ <div style={{ padding: "0 10px 4px", display: "flex", alignItems: "center", justifyContent: "space-between", color: "var(--text-dim)", flexShrink: 0 }}>
870
+ <button
871
+ type="button"
872
+ onClick={() => setChatsOpen((v) => !v)}
873
+ title={chatsOpen ? "Collapse chats" : "Expand chats"}
874
+ aria-expanded={chatsOpen}
875
+ style={{
876
+ display: "flex", alignItems: "center", gap: 6,
877
+ padding: "4px 0", background: "none", border: "none",
878
+ color: "var(--text-dim)", cursor: "pointer",
879
+ fontSize: 10, fontWeight: 600, letterSpacing: "0.05em",
880
+ textTransform: "uppercase", textAlign: "left",
881
+ }}
882
+ >
883
+ <svg
884
+ width="9" height="9" viewBox="0 0 10 10" fill="none"
885
+ stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"
886
+ style={{ transform: chatsOpen ? "rotate(90deg)" : "none", transition: "transform 0.15s", flexShrink: 0 }}
887
+ >
888
+ <polyline points="3 2 7 5 3 8" />
889
+ </svg>
890
+ Chats
891
+ </button>
892
+ <span style={{ fontSize: 10, fontWeight: 500 }}>{currentChatCount} sessions</span>
893
+ </div>
894
+ )}
895
+
896
+ {/* Current project sessions */}
897
+ {chatsOpen && (
898
+ <div style={{ flex: explorerOpen && effectiveCwd ? "1 1 0" : "1 1 auto", overflowY: "auto", padding: "6px 8px 8px", minHeight: 120 }}>
899
+ {loading && (
900
+ <div style={{ padding: "16px 14px", color: "var(--text-muted)", fontSize: 12 }}>
901
+ Loading...
902
+ </div>
903
+ )}
904
+ {error && (
905
+ <div style={{ padding: "12px 14px", color: "#f87171", fontSize: 12 }}>
906
+ {error}
907
+ </div>
908
+ )}
909
+ {!loading && !error && !effectiveCwd && (
910
+ <div style={{ padding: "16px 14px", color: "var(--text-muted)", fontSize: 12 }}>
911
+ Open or create a project to start chatting.
912
+ </div>
913
+ )}
914
+ {!loading && !error && effectiveCwd && (
915
+ <>
916
+ {currentProjectSessions.length === 0 ? (
917
+ <button
918
+ type="button"
919
+ onClick={() => createSessionInCwd(effectiveCwd)}
920
+ style={{
921
+ width: "100%",
922
+ padding: "12px",
923
+ background: "transparent",
924
+ border: "none",
925
+ color: "var(--text-muted)",
926
+ cursor: "pointer",
927
+ textAlign: "left",
928
+ fontSize: 12,
929
+ }}
930
+ onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-hover)"; e.currentTarget.style.color = "var(--text)"; }}
931
+ onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = "var(--text-muted)"; }}
932
+ >
933
+ Start a new session in this project
934
+ </button>
935
+ ) : (
936
+ visibleSessionTree.map((node) => (
937
+ <SessionTreeItem
938
+ key={node.session.id}
939
+ node={node}
940
+ selectedSessionId={selectedSessionId}
941
+ onSelectSession={handleSelectSessionFromList}
942
+ onRenamed={() => loadSessionsForCwd(effectiveCwd, false)}
943
+ onSessionDeleted={(id) => {
944
+ onSessionDeleted?.(id);
945
+ void loadSessionsForCwd(effectiveCwd, false);
946
+ }}
947
+ depth={0}
948
+ />
949
+ ))
950
+ )}
951
+ {(sessionsExpanded || hiddenSessionCount > 0) && flatRootCount > SESSION_TRUNCATE_LIMIT && (
952
+ <button
953
+ type="button"
954
+ onClick={() => setSessionsExpanded((value) => !value)}
955
+ style={{
956
+ width: "100%",
957
+ padding: "6px 10px",
958
+ background: "transparent",
959
+ border: "none",
960
+ color: "var(--text-dim)",
961
+ cursor: "pointer",
962
+ fontSize: 11,
963
+ textAlign: "center",
964
+ }}
965
+ >
966
+ {sessionsExpanded ? "Show less" : `Show ${Math.max(0, hiddenSessionCount)} more`}
967
+ </button>
968
+ )}
969
+ </>
970
+ )}
971
+ </div>
972
+ )}
973
+
974
+ {/* File Explorer section */}
975
+ {effectiveCwd && (
976
+ <div
977
+ style={{
978
+ borderTop: "1px solid var(--border)",
979
+ display: "flex",
980
+ flexDirection: "column",
981
+ flex: explorerOpen ? "1 1 0" : "0 0 auto",
982
+ minHeight: 0,
983
+ overflow: "hidden",
984
+ }}
985
+ >
986
+ <div style={{ display: "flex", alignItems: "center", flexShrink: 0, gap: 0, padding: "2px 4px" }}>
987
+ <button
988
+ onClick={() => {
989
+ setExplorerOpen((v) => !v);
990
+ setExplorerKey((k) => k + 1);
991
+ }}
992
+ style={{
993
+ display: "flex",
994
+ alignItems: "center",
995
+ gap: 6,
996
+ padding: "6px 6px 6px 6px",
997
+ background: "none",
998
+ border: "none",
999
+ color: "var(--text-muted)",
1000
+ cursor: "pointer",
1001
+ fontSize: 11,
1002
+ fontWeight: 600,
1003
+ letterSpacing: "0.05em",
1004
+ textTransform: "uppercase",
1005
+ textAlign: "left",
1006
+ flexShrink: 0,
1007
+ }}
1008
+ >
1009
+ <svg
1010
+ width="9" height="9" viewBox="0 0 10 10" fill="none"
1011
+ stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"
1012
+ style={{ transform: explorerOpen ? "rotate(90deg)" : "none", transition: "transform 0.15s", flexShrink: 0 }}
1013
+ >
1014
+ <polyline points="3 2 7 5 3 8" />
1015
+ </svg>
1016
+ Explorer
1017
+ </button>
1018
+ <div style={{ position: "relative", flex: 1, minWidth: 0 }}>
1019
+ <svg
1020
+ width="12"
1021
+ height="12"
1022
+ viewBox="0 0 16 16"
1023
+ fill="none"
1024
+ stroke="currentColor"
1025
+ strokeWidth="1.7"
1026
+ strokeLinecap="round"
1027
+ strokeLinejoin="round"
1028
+ style={{
1029
+ position: "absolute",
1030
+ left: 8,
1031
+ top: "50%",
1032
+ transform: "translateY(-50%)",
1033
+ color: "var(--text-dim)",
1034
+ pointerEvents: "none",
1035
+ }}
1036
+ >
1037
+ <circle cx="7" cy="7" r="4.4" />
1038
+ <path d="M10.4 10.4 13.2 13.2" />
1039
+ </svg>
1040
+ <input
1041
+ value={explorerFileFilter}
1042
+ onChange={(e) => setExplorerFileFilter(e.target.value)}
1043
+ placeholder="Filter files..."
1044
+ style={{
1045
+ width: "100%",
1046
+ height: 28,
1047
+ boxSizing: "border-box",
1048
+ padding: "0 26px 0 26px",
1049
+ borderRadius: 6,
1050
+ border: "1px solid var(--border)",
1051
+ background: "var(--bg)",
1052
+ color: "var(--text)",
1053
+ fontSize: 12,
1054
+ outline: "none",
1055
+ }}
1056
+ />
1057
+ {explorerFileFilter && (
1058
+ <button
1059
+ type="button"
1060
+ onClick={() => setExplorerFileFilter("")}
1061
+ title="Clear filter"
1062
+ style={{
1063
+ position: "absolute",
1064
+ right: 4,
1065
+ top: "50%",
1066
+ transform: "translateY(-50%)",
1067
+ width: 20,
1068
+ height: 20,
1069
+ display: "flex",
1070
+ alignItems: "center",
1071
+ justifyContent: "center",
1072
+ border: "none",
1073
+ borderRadius: 4,
1074
+ background: "transparent",
1075
+ color: "var(--text-dim)",
1076
+ cursor: "pointer",
1077
+ padding: 0,
1078
+ }}
1079
+ >
1080
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
1081
+ <line x1="2" y1="2" x2="8" y2="8" />
1082
+ <line x1="8" y1="2" x2="2" y2="8" />
1083
+ </svg>
1084
+ </button>
1085
+ )}
1086
+ </div>
1087
+ </div>
1088
+ {explorerOpen && (
1089
+ <div style={{ flex: 1, overflowY: "auto", overflowX: "hidden" }}>
1090
+ <FileExplorer
1091
+ cwd={effectiveCwd}
1092
+ onOpenFile={onOpenFile ?? (() => {})}
1093
+ refreshKey={explorerKey}
1094
+ onAtMention={onAtMention}
1095
+ searchQuery={explorerFileFilter}
1096
+ onSearchChange={setExplorerFileFilter}
1097
+ />
1098
+ </div>
1099
+ )}
1100
+ </div>
1101
+ )}
1102
+ <ProjectFolderPicker
1103
+ open={folderPickerOpen}
1104
+ initialPath={effectiveCwd || homeDir || null}
1105
+ onClose={() => setFolderPickerOpen(false)}
1106
+ onSelect={(path) => void handleFolderPickerSelect(path)}
1107
+ />
1108
+ </div>
1109
+ );
1110
+ }
1111
+
1112
+ function SessionTreeItem({
1113
+ node,
1114
+ selectedSessionId,
1115
+ onSelectSession,
1116
+ onRenamed,
1117
+ onSessionDeleted,
1118
+ depth,
1119
+ }: {
1120
+ node: SessionTreeNode;
1121
+ selectedSessionId: string | null;
1122
+ onSelectSession: (s: SessionInfo) => void;
1123
+ onRenamed?: () => void;
1124
+ onSessionDeleted?: (id: string) => void;
1125
+ depth: number;
1126
+ }) {
1127
+ const [collapsed, setCollapsed] = useState(false);
1128
+ const hasChildren = node.children.length > 0;
1129
+
1130
+ return (
1131
+ <div>
1132
+ <div style={{ position: "relative" }}>
1133
+ {/* Indent line for child sessions */}
1134
+ {depth > 0 && (
1135
+ <div style={{
1136
+ position: "absolute",
1137
+ left: depth * 12 + 6,
1138
+ top: 0, bottom: 0,
1139
+ width: 1,
1140
+ background: "var(--border)",
1141
+ pointerEvents: "none",
1142
+ }} />
1143
+ )}
1144
+ <SessionItem
1145
+ session={node.session}
1146
+ isSelected={node.session.id === selectedSessionId}
1147
+ onClick={() => onSelectSession(node.session)}
1148
+ onRenamed={onRenamed}
1149
+ onDeleted={(id) => onSessionDeleted?.(id)}
1150
+ depth={depth}
1151
+ hasChildren={hasChildren}
1152
+ collapsed={collapsed}
1153
+ onToggleCollapse={() => setCollapsed((v) => !v)}
1154
+ />
1155
+ </div>
1156
+ {hasChildren && !collapsed && (
1157
+ <div>
1158
+ {node.children.map((child) => (
1159
+ <SessionTreeItem
1160
+ key={child.session.id}
1161
+ node={child}
1162
+ selectedSessionId={selectedSessionId}
1163
+ onSelectSession={onSelectSession}
1164
+ onRenamed={onRenamed}
1165
+ onSessionDeleted={onSessionDeleted}
1166
+ depth={depth + 1}
1167
+ />
1168
+ ))}
1169
+ </div>
1170
+ )}
1171
+ </div>
1172
+ );
1173
+ }
1174
+
1175
+ function SessionItem({
1176
+ session,
1177
+ isSelected,
1178
+ onClick,
1179
+ onRenamed,
1180
+ onDeleted,
1181
+ depth = 0,
1182
+ hasChildren = false,
1183
+ collapsed = false,
1184
+ onToggleCollapse,
1185
+ }: {
1186
+ session: SessionInfo;
1187
+ isSelected: boolean;
1188
+ onClick: () => void;
1189
+ onRenamed?: () => void;
1190
+ onDeleted?: (id: string) => void;
1191
+ depth?: number;
1192
+ hasChildren?: boolean;
1193
+ collapsed?: boolean;
1194
+ onToggleCollapse?: () => void;
1195
+ }) {
1196
+ const [hovered, setHovered] = useState(false);
1197
+ const [renaming, setRenaming] = useState(false);
1198
+ const [renameValue, setRenameValue] = useState("");
1199
+ const [confirmDelete, setConfirmDelete] = useState(false);
1200
+ const [deleting, setDeleting] = useState(false);
1201
+ const inputRef = useRef<HTMLInputElement>(null);
1202
+
1203
+ const title = session.name || session.firstMessage.slice(0, 50) || session.id.slice(0, 12);
1204
+
1205
+ const startRename = useCallback((e: React.MouseEvent) => {
1206
+ e.stopPropagation();
1207
+ setRenameValue(session.name ?? "");
1208
+ setRenaming(true);
1209
+ setTimeout(() => inputRef.current?.select(), 0);
1210
+ }, [session.name]);
1211
+
1212
+ const commitRename = useCallback(async () => {
1213
+ const name = renameValue.trim();
1214
+ setRenaming(false);
1215
+ if (name === (session.name ?? "")) return;
1216
+ try {
1217
+ await fetch(`/api/sessions/${encodeURIComponent(session.id)}`, {
1218
+ method: "PATCH",
1219
+ headers: { "Content-Type": "application/json" },
1220
+ body: JSON.stringify({ name }),
1221
+ });
1222
+ onRenamed?.();
1223
+ } catch {
1224
+ // ignore
1225
+ }
1226
+ }, [renameValue, session.id, session.name, onRenamed]);
1227
+
1228
+ const handleDeleteClick = useCallback((e: React.MouseEvent) => {
1229
+ e.stopPropagation();
1230
+ setConfirmDelete(true);
1231
+ }, []);
1232
+
1233
+ const handleDeleteConfirm = useCallback(async (e: React.MouseEvent) => {
1234
+ e.stopPropagation();
1235
+ setConfirmDelete(false);
1236
+ setDeleting(true);
1237
+ try {
1238
+ await fetch(`/api/sessions/${encodeURIComponent(session.id)}`, { method: "DELETE" });
1239
+ onDeleted?.(session.id);
1240
+ } catch {
1241
+ setDeleting(false);
1242
+ }
1243
+ }, [session.id, onDeleted]);
1244
+
1245
+ const handleDeleteCancel = useCallback((e: React.MouseEvent) => {
1246
+ e.stopPropagation();
1247
+ setConfirmDelete(false);
1248
+ }, []);
1249
+
1250
+ // Fixed-height outer wrapper — content swaps in place so the list never reflows
1251
+ const ITEM_HEIGHT = 54;
1252
+
1253
+ return (
1254
+ <div
1255
+ onClick={confirmDelete || renaming ? undefined : onClick}
1256
+ onMouseEnter={() => setHovered(true)}
1257
+ onMouseLeave={() => { setHovered(false); }}
1258
+ style={{
1259
+ height: ITEM_HEIGHT,
1260
+ display: "flex",
1261
+ alignItems: "center",
1262
+ paddingLeft: depth > 0 ? depth * 12 + 14 : 14,
1263
+ paddingRight: 8,
1264
+ cursor: confirmDelete || renaming ? "default" : "pointer",
1265
+ background: confirmDelete
1266
+ ? "rgba(239,68,68,0.06)"
1267
+ : isSelected ? "var(--bg-selected)" : hovered ? "var(--bg-hover)" : "transparent",
1268
+ borderLeft: confirmDelete
1269
+ ? "2px solid #ef4444"
1270
+ : isSelected ? "2px solid var(--accent)" : "2px solid transparent",
1271
+ transition: "background 0.1s",
1272
+ opacity: deleting ? 0.5 : 1,
1273
+ gap: 6,
1274
+ overflow: "hidden",
1275
+ }}
1276
+ >
1277
+ {confirmDelete ? (
1278
+ /* ── Delete confirmation: same height, two flat buttons ── */
1279
+ <>
1280
+ <div style={{ flex: 1, minWidth: 0, fontSize: 12, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
1281
+ Delete <span style={{ fontWeight: 600 }}>&ldquo;{title.slice(0, 22)}{title.length > 22 ? "…" : ""}&rdquo;</span>?
1282
+ </div>
1283
+ <div style={{ display: "flex", gap: 5, flexShrink: 0 }}>
1284
+ <button
1285
+ onClick={handleDeleteConfirm}
1286
+ style={{
1287
+ display: "flex", alignItems: "center", justifyContent: "center", gap: 4,
1288
+ height: 30, padding: "0 11px",
1289
+ background: "#ef4444", border: "none",
1290
+ borderRadius: 6, color: "#fff",
1291
+ cursor: "pointer", fontSize: 12, fontWeight: 600,
1292
+ whiteSpace: "nowrap",
1293
+ }}
1294
+ >
1295
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1296
+ <polyline points="3 6 5 6 21 6" />
1297
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
1298
+ <path d="M10 11v6M14 11v6" />
1299
+ <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
1300
+ </svg>
1301
+ Delete
1302
+ </button>
1303
+ <button
1304
+ onClick={handleDeleteCancel}
1305
+ style={{
1306
+ display: "flex", alignItems: "center", justifyContent: "center",
1307
+ height: 30, padding: "0 11px",
1308
+ background: "var(--bg)", border: "1px solid var(--border)",
1309
+ borderRadius: 6, color: "var(--text-muted)",
1310
+ cursor: "pointer", fontSize: 12, fontWeight: 500,
1311
+ whiteSpace: "nowrap",
1312
+ }}
1313
+ >
1314
+ Cancel
1315
+ </button>
1316
+ </div>
1317
+ </>
1318
+ ) : renaming ? (
1319
+ /* ── Rename: input fills the same row ── */
1320
+ <input
1321
+ ref={inputRef}
1322
+ value={renameValue}
1323
+ onChange={(e) => setRenameValue(e.target.value)}
1324
+ onBlur={commitRename}
1325
+ onKeyDown={(e) => {
1326
+ if (e.key === "Enter") commitRename();
1327
+ if (e.key === "Escape") setRenaming(false);
1328
+ }}
1329
+ autoFocus
1330
+ style={{
1331
+ flex: 1,
1332
+ fontSize: 12,
1333
+ padding: "5px 8px",
1334
+ border: "1px solid var(--accent)",
1335
+ borderRadius: 5,
1336
+ outline: "none",
1337
+ background: "var(--bg)",
1338
+ color: "var(--text)",
1339
+ height: 30,
1340
+ }}
1341
+ />
1342
+ ) : (
1343
+ /* ── Normal view ── */
1344
+ <>
1345
+ {/* Fork indicator for child sessions */}
1346
+ {depth > 0 && (
1347
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
1348
+ <line x1="6" y1="3" x2="6" y2="15" />
1349
+ <circle cx="18" cy="6" r="3" />
1350
+ <circle cx="6" cy="18" r="3" />
1351
+ <path d="M18 9a9 9 0 0 1-9 9" />
1352
+ </svg>
1353
+ )}
1354
+ <div style={{ flex: 1, minWidth: 0 }}>
1355
+ <div
1356
+ style={{
1357
+ fontSize: 12,
1358
+ fontWeight: isSelected ? 500 : 400,
1359
+ lineHeight: 1.4,
1360
+ overflow: "hidden",
1361
+ textOverflow: "ellipsis",
1362
+ whiteSpace: "nowrap",
1363
+ color: "var(--text)",
1364
+ }}
1365
+ title={title}
1366
+ >
1367
+ {title}
1368
+ </div>
1369
+ <div style={{ marginTop: 2, display: "flex", gap: 8, color: "var(--text-dim)", fontSize: 11 }}>
1370
+ <span title={session.modified}>{formatRelativeTime(session.modified)}</span>
1371
+ <span>{session.messageCount} msgs</span>
1372
+ {session.cwdExists === false && (
1373
+ <span title={`Project path missing: ${session.cwd}`} style={{ color: "#f87171" }}>
1374
+ missing path
1375
+ </span>
1376
+ )}
1377
+ </div>
1378
+ </div>
1379
+
1380
+ {/* Collapse toggle — always visible when has children */}
1381
+ {hasChildren && (
1382
+ <button
1383
+ onClick={(e) => { e.stopPropagation(); onToggleCollapse?.(); }}
1384
+ title={collapsed ? "Expand forks" : "Collapse forks"}
1385
+ style={{
1386
+ display: "flex", alignItems: "center", justifyContent: "center",
1387
+ width: 20, height: 20, padding: 0, flexShrink: 0,
1388
+ background: "none", border: "none",
1389
+ color: "var(--text-dim)", cursor: "pointer",
1390
+ transform: collapsed ? "rotate(-90deg)" : "none",
1391
+ transition: "transform 0.15s",
1392
+ }}
1393
+ >
1394
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
1395
+ <polyline points="2 3.5 5 6.5 8 3.5" />
1396
+ </svg>
1397
+ </button>
1398
+ )}
1399
+
1400
+ {/* Action buttons — shown on hover */}
1401
+ {hovered && (
1402
+ <div style={{ display: "flex", gap: 4, flexShrink: 0 }}>
1403
+ <button
1404
+ onClick={startRename}
1405
+ title="Rename"
1406
+ style={{
1407
+ display: "flex", alignItems: "center", justifyContent: "center",
1408
+ width: 32, height: 32, padding: 0,
1409
+ background: "var(--bg-hover)", border: "1px solid var(--border)",
1410
+ borderRadius: 7, color: "var(--text-muted)",
1411
+ cursor: "pointer", flexShrink: 0,
1412
+ transition: "background 0.12s, color 0.12s, border-color 0.12s",
1413
+ }}
1414
+ onMouseEnter={(e) => {
1415
+ e.currentTarget.style.background = "var(--bg-selected)";
1416
+ e.currentTarget.style.color = "var(--accent)";
1417
+ e.currentTarget.style.borderColor = "rgba(37,99,235,0.35)";
1418
+ }}
1419
+ onMouseLeave={(e) => {
1420
+ e.currentTarget.style.background = "var(--bg-hover)";
1421
+ e.currentTarget.style.color = "var(--text-muted)";
1422
+ e.currentTarget.style.borderColor = "var(--border)";
1423
+ }}
1424
+ >
1425
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1426
+ <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />
1427
+ </svg>
1428
+ </button>
1429
+ <button
1430
+ onClick={handleDeleteClick}
1431
+ title="Delete"
1432
+ style={{
1433
+ display: "flex", alignItems: "center", justifyContent: "center",
1434
+ width: 32, height: 32, padding: 0,
1435
+ background: "var(--bg-hover)", border: "1px solid var(--border)",
1436
+ borderRadius: 7, color: "var(--text-muted)",
1437
+ cursor: "pointer", flexShrink: 0,
1438
+ transition: "background 0.12s, color 0.12s, border-color 0.12s",
1439
+ }}
1440
+ onMouseEnter={(e) => {
1441
+ e.currentTarget.style.background = "rgba(239,68,68,0.08)";
1442
+ e.currentTarget.style.color = "#ef4444";
1443
+ e.currentTarget.style.borderColor = "rgba(239,68,68,0.35)";
1444
+ }}
1445
+ onMouseLeave={(e) => {
1446
+ e.currentTarget.style.background = "var(--bg-hover)";
1447
+ e.currentTarget.style.color = "var(--text-muted)";
1448
+ e.currentTarget.style.borderColor = "var(--border)";
1449
+ }}
1450
+ >
1451
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1452
+ <polyline points="3 6 5 6 21 6" />
1453
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
1454
+ <path d="M10 11v6M14 11v6" />
1455
+ <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
1456
+ </svg>
1457
+ </button>
1458
+ </div>
1459
+ )}
1460
+ </>
1461
+ )}
1462
+ </div>
1463
+ );
1464
+ }