hermium 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +56 -0
  2. package/bin/hermium.mjs +185 -164
  3. package/dist/api.mjs +3513 -0
  4. package/dist/public/assets/css/index-Dfs9RUU9.css +1 -0
  5. package/dist/public/assets/css/styles-B8p6jk5Z.css +1 -0
  6. package/dist/public/assets/js/ChatInputBlock-Bw7AL70H.js +1 -0
  7. package/dist/public/assets/js/MarkdownMessage-8d7Y6VL-.js +1 -0
  8. package/dist/public/assets/js/base-ui-BvQbAt_1.js +1 -0
  9. package/dist/public/assets/js/chat._sessionId-BG6lVraH.js +1 -0
  10. package/dist/public/assets/js/chat.index-D2zdMPTT.js +1 -0
  11. package/dist/public/assets/js/index-C0AK45FU.js +60 -0
  12. package/dist/public/assets/js/index-Cx5En4FK.js +1 -0
  13. package/dist/public/assets/js/memory-CeSRdTkW.js +3 -0
  14. package/dist/public/assets/js/router-8uDKazL-.js +1 -0
  15. package/dist/public/assets/js/settings-Bc3Y5zXO.js +1 -0
  16. package/dist/public/assets/js/skills-DZv7sA_5.js +1 -0
  17. package/dist/public/assets/js/theme-CPkdkpaj.js +1 -0
  18. package/dist/public/assets/js/usage-DXQsT9_b.js +1 -0
  19. package/dist/public/assets/woff2/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  20. package/dist/public/assets/woff2/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  21. package/dist/public/assets/woff2/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  22. package/dist/public/assets/woff2/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  23. package/dist/public/assets/woff2/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  24. package/dist/public/favicon.ico +0 -0
  25. package/dist/public/logo.png +0 -0
  26. package/package.json +1 -1
  27. package/dist/public/assets/IconAlertCircle-BHkmI3j7.js +0 -1
  28. package/dist/public/assets/IconAlertTriangle-wCJudlVg.js +0 -1
  29. package/dist/public/assets/IconCheck-CFuEh_p7.js +0 -1
  30. package/dist/public/assets/IconLoader2-BIx3OuF9.js +0 -1
  31. package/dist/public/assets/IconRefresh-Dgm93w3T.js +0 -1
  32. package/dist/public/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  33. package/dist/public/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  34. package/dist/public/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  35. package/dist/public/assets/index-Bbz3abmO.js +0 -14
  36. package/dist/public/assets/index-CWUaRwcE.js +0 -1
  37. package/dist/public/assets/index-CinLq3cd.js +0 -1
  38. package/dist/public/assets/index-CrQs9n6q.js +0 -29
  39. package/dist/public/assets/index-CtacpN3I.js +0 -1
  40. package/dist/public/assets/index-DY7aE-9s.js +0 -2
  41. package/dist/public/assets/index-DkYGodJj.js +0 -94
  42. package/dist/public/assets/index-DvDLadUx.js +0 -1
  43. package/dist/public/assets/index-U6RcWedt.js +0 -1
  44. package/dist/public/assets/index-_6iFZ0fh.js +0 -1
  45. package/dist/public/assets/index-enFS26SU.js +0 -1
  46. package/dist/public/assets/input-eNcwlDHp.js +0 -1
  47. package/dist/public/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  48. package/dist/public/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  49. package/dist/public/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  50. package/dist/public/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  51. package/dist/public/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  52. package/dist/public/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  53. package/dist/public/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  54. package/dist/public/assets/queries-iHRgZzw2.js +0 -1
  55. package/dist/public/assets/styles-KcflDlA_.css +0 -1
  56. package/dist/public/assets/switch-B1DcZLwL.js +0 -1
  57. package/dist/public/assets/syntax-highlighter-DWPF-A_h.js +0 -6
  58. package/dist/public/assets/textarea-Di_syYTS.js +0 -1
  59. package/dist/public/favicon.png +0 -0
  60. package/dist/public/nous-logo.png +0 -0
  61. package/dist/server/index.mjs +0 -244
  62. package/dist/web-server/__23tanstack-start-plugin-adapters-Cwee5PKy.mjs +0 -6
  63. package/dist/web-server/_chunks/ssr-renderer.mjs +0 -22
  64. package/dist/web-server/_libs/babel__runtime.mjs +0 -237
  65. package/dist/web-server/_libs/bail.mjs +0 -8
  66. package/dist/web-server/_libs/base-ui__react.mjs +0 -9554
  67. package/dist/web-server/_libs/base-ui__utils.mjs +0 -1101
  68. package/dist/web-server/_libs/ccount.mjs +0 -16
  69. package/dist/web-server/_libs/character-entities-legacy.mjs +0 -111
  70. package/dist/web-server/_libs/character-entities.mjs +0 -2130
  71. package/dist/web-server/_libs/character-reference-invalid.mjs +0 -33
  72. package/dist/web-server/_libs/class-variance-authority.mjs +0 -44
  73. package/dist/web-server/_libs/clsx.mjs +0 -16
  74. package/dist/web-server/_libs/comma-separated-tokens.mjs +0 -31
  75. package/dist/web-server/_libs/cookie-es.mjs +0 -44
  76. package/dist/web-server/_libs/croner.mjs +0 -1
  77. package/dist/web-server/_libs/crossws.mjs +0 -1
  78. package/dist/web-server/_libs/decode-named-character-reference+[...].mjs +0 -8
  79. package/dist/web-server/_libs/devlop.mjs +0 -8
  80. package/dist/web-server/_libs/escape-string-regexp.mjs +0 -9
  81. package/dist/web-server/_libs/estree-util-is-identifier-name.mjs +0 -11
  82. package/dist/web-server/_libs/extend.mjs +0 -97
  83. package/dist/web-server/_libs/fault.mjs +0 -1
  84. package/dist/web-server/_libs/floating-ui__core.mjs +0 -663
  85. package/dist/web-server/_libs/floating-ui__dom.mjs +0 -624
  86. package/dist/web-server/_libs/floating-ui__react-dom.mjs +0 -279
  87. package/dist/web-server/_libs/floating-ui__utils.mjs +0 -322
  88. package/dist/web-server/_libs/format.mjs +0 -1
  89. package/dist/web-server/_libs/h3.mjs +0 -408
  90. package/dist/web-server/_libs/hast-util-parse-selector.mjs +0 -39
  91. package/dist/web-server/_libs/hast-util-to-jsx-runtime.mjs +0 -388
  92. package/dist/web-server/_libs/hast-util-whitespace.mjs +0 -10
  93. package/dist/web-server/_libs/hastscript.mjs +0 -200
  94. package/dist/web-server/_libs/highlight.js.mjs +0 -1
  95. package/dist/web-server/_libs/hookable.mjs +0 -1
  96. package/dist/web-server/_libs/html-url-attributes.mjs +0 -26
  97. package/dist/web-server/_libs/inline-style-parser.mjs +0 -142
  98. package/dist/web-server/_libs/is-alphabetical.mjs +0 -7
  99. package/dist/web-server/_libs/is-alphanumerical.mjs +0 -8
  100. package/dist/web-server/_libs/is-decimal.mjs +0 -7
  101. package/dist/web-server/_libs/is-hexadecimal.mjs +0 -7
  102. package/dist/web-server/_libs/is-plain-obj.mjs +0 -10
  103. package/dist/web-server/_libs/isbot.mjs +0 -21
  104. package/dist/web-server/_libs/longest-streak.mjs +0 -25
  105. package/dist/web-server/_libs/lowlight.mjs +0 -1
  106. package/dist/web-server/_libs/markdown-table.mjs +0 -142
  107. package/dist/web-server/_libs/mdast-util-find-and-replace.mjs +0 -109
  108. package/dist/web-server/_libs/mdast-util-from-markdown.mjs +0 -717
  109. package/dist/web-server/_libs/mdast-util-gfm-autolink-literal+[...].mjs +0 -156
  110. package/dist/web-server/_libs/mdast-util-gfm-footnote.mjs +0 -117
  111. package/dist/web-server/_libs/mdast-util-gfm-strikethrough.mjs +0 -54
  112. package/dist/web-server/_libs/mdast-util-gfm-table.mjs +0 -157
  113. package/dist/web-server/_libs/mdast-util-gfm-task-list-item.mjs +0 -77
  114. package/dist/web-server/_libs/mdast-util-gfm.mjs +0 -29
  115. package/dist/web-server/_libs/mdast-util-phrasing.mjs +0 -30
  116. package/dist/web-server/_libs/mdast-util-to-hast.mjs +0 -710
  117. package/dist/web-server/_libs/mdast-util-to-markdown.mjs +0 -798
  118. package/dist/web-server/_libs/mdast-util-to-string.mjs +0 -38
  119. package/dist/web-server/_libs/micromark-core-commonmark.mjs +0 -2259
  120. package/dist/web-server/_libs/micromark-extension-gfm-autolink-literal+[...].mjs +0 -344
  121. package/dist/web-server/_libs/micromark-extension-gfm-footnote+[...].mjs +0 -279
  122. package/dist/web-server/_libs/micromark-extension-gfm-strikethrough+[...].mjs +0 -98
  123. package/dist/web-server/_libs/micromark-extension-gfm-table.mjs +0 -491
  124. package/dist/web-server/_libs/micromark-extension-gfm-tagfilter+[...].mjs +0 -1
  125. package/dist/web-server/_libs/micromark-extension-gfm-task-list-item+[...].mjs +0 -77
  126. package/dist/web-server/_libs/micromark-extension-gfm.mjs +0 -18
  127. package/dist/web-server/_libs/micromark-factory-destination.mjs +0 -94
  128. package/dist/web-server/_libs/micromark-factory-label.mjs +0 -63
  129. package/dist/web-server/_libs/micromark-factory-space.mjs +0 -24
  130. package/dist/web-server/_libs/micromark-factory-title.mjs +0 -65
  131. package/dist/web-server/_libs/micromark-factory-whitespace.mjs +0 -22
  132. package/dist/web-server/_libs/micromark-util-character.mjs +0 -44
  133. package/dist/web-server/_libs/micromark-util-chunked.mjs +0 -36
  134. package/dist/web-server/_libs/micromark-util-classify-character+[...].mjs +0 -12
  135. package/dist/web-server/_libs/micromark-util-combine-extensions+[...].mjs +0 -41
  136. package/dist/web-server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +0 -19
  137. package/dist/web-server/_libs/micromark-util-decode-string.mjs +0 -21
  138. package/dist/web-server/_libs/micromark-util-encode.mjs +0 -1
  139. package/dist/web-server/_libs/micromark-util-html-tag-name.mjs +0 -69
  140. package/dist/web-server/_libs/micromark-util-normalize-identifier+[...].mjs +0 -6
  141. package/dist/web-server/_libs/micromark-util-resolve-all.mjs +0 -15
  142. package/dist/web-server/_libs/micromark-util-sanitize-uri.mjs +0 -41
  143. package/dist/web-server/_libs/micromark-util-subtokenize.mjs +0 -346
  144. package/dist/web-server/_libs/micromark.mjs +0 -906
  145. package/dist/web-server/_libs/ocache.mjs +0 -1
  146. package/dist/web-server/_libs/ohash.mjs +0 -1
  147. package/dist/web-server/_libs/parse-entities.mjs +0 -245
  148. package/dist/web-server/_libs/property-information.mjs +0 -1210
  149. package/dist/web-server/_libs/react-dom.mjs +0 -10779
  150. package/dist/web-server/_libs/react-markdown.mjs +0 -147
  151. package/dist/web-server/_libs/react-syntax-highlighter.mjs +0 -941
  152. package/dist/web-server/_libs/react.mjs +0 -513
  153. package/dist/web-server/_libs/refractor.mjs +0 -2425
  154. package/dist/web-server/_libs/remark-gfm.mjs +0 -20
  155. package/dist/web-server/_libs/remark-parse.mjs +0 -19
  156. package/dist/web-server/_libs/remark-rehype.mjs +0 -21
  157. package/dist/web-server/_libs/reselect.mjs +0 -1
  158. package/dist/web-server/_libs/rou3.mjs +0 -8
  159. package/dist/web-server/_libs/seroval-plugins.mjs +0 -58
  160. package/dist/web-server/_libs/seroval.mjs +0 -1775
  161. package/dist/web-server/_libs/space-separated-tokens.mjs +0 -11
  162. package/dist/web-server/_libs/srvx.mjs +0 -781
  163. package/dist/web-server/_libs/style-to-js.mjs +0 -72
  164. package/dist/web-server/_libs/style-to-object.mjs +0 -38
  165. package/dist/web-server/_libs/tabler__icons-react.mjs +0 -230
  166. package/dist/web-server/_libs/tanstack__history.mjs +0 -204
  167. package/dist/web-server/_libs/tanstack__query-core.mjs +0 -2552
  168. package/dist/web-server/_libs/tanstack__react-query.mjs +0 -190
  169. package/dist/web-server/_libs/tanstack__react-router.mjs +0 -1120
  170. package/dist/web-server/_libs/tanstack__react-store.mjs +0 -2
  171. package/dist/web-server/_libs/tanstack__router-core.mjs +0 -4288
  172. package/dist/web-server/_libs/tanstack__store.mjs +0 -1
  173. package/dist/web-server/_libs/trim-lines.mjs +0 -41
  174. package/dist/web-server/_libs/trough.mjs +0 -85
  175. package/dist/web-server/_libs/ufo.mjs +0 -54
  176. package/dist/web-server/_libs/unctx.mjs +0 -1
  177. package/dist/web-server/_libs/ungap__structured-clone.mjs +0 -224
  178. package/dist/web-server/_libs/unified.mjs +0 -661
  179. package/dist/web-server/_libs/unist-util-is.mjs +0 -100
  180. package/dist/web-server/_libs/unist-util-position.mjs +0 -27
  181. package/dist/web-server/_libs/unist-util-stringify-position.mjs +0 -27
  182. package/dist/web-server/_libs/unist-util-visit-parents.mjs +0 -83
  183. package/dist/web-server/_libs/unist-util-visit.mjs +0 -24
  184. package/dist/web-server/_libs/unstorage.mjs +0 -1
  185. package/dist/web-server/_libs/use-sync-external-store.mjs +0 -139
  186. package/dist/web-server/_libs/vfile-message.mjs +0 -138
  187. package/dist/web-server/_libs/vfile.mjs +0 -467
  188. package/dist/web-server/_libs/zod.mjs +0 -3915
  189. package/dist/web-server/_libs/zustand.mjs +0 -343
  190. package/dist/web-server/_libs/zwitch.mjs +0 -1
  191. package/dist/web-server/_ssr/index-0n2Z3BPQ.mjs +0 -369
  192. package/dist/web-server/_ssr/index-6itDALOw.mjs +0 -339
  193. package/dist/web-server/_ssr/index-BIRTrOmp.mjs +0 -449
  194. package/dist/web-server/_ssr/index-BPzfADac.mjs +0 -66
  195. package/dist/web-server/_ssr/index-BQE3bF14.mjs +0 -1870
  196. package/dist/web-server/_ssr/index-C5HpvlUP.mjs +0 -190
  197. package/dist/web-server/_ssr/index-C_ZxnypN.mjs +0 -213
  198. package/dist/web-server/_ssr/index-Ca8JFH8f.mjs +0 -612
  199. package/dist/web-server/_ssr/index-DNVESZiA.mjs +0 -513
  200. package/dist/web-server/_ssr/index.mjs +0 -1558
  201. package/dist/web-server/_ssr/input-CqXjTRQg.mjs +0 -20
  202. package/dist/web-server/_ssr/queries-3H_19mUt.mjs +0 -16
  203. package/dist/web-server/_ssr/router-sbsNus0Y.mjs +0 -2093
  204. package/dist/web-server/_ssr/start-HYkvq4Ni.mjs +0 -4
  205. package/dist/web-server/_ssr/switch-usf2F1UM.mjs +0 -33
  206. package/dist/web-server/_ssr/syntax-highlighter-5vezNTce.mjs +0 -62
  207. package/dist/web-server/_ssr/textarea-DfRheWY0.mjs +0 -18
  208. package/dist/web-server/_tanstack-start-manifest_v-DqW-pKEH.mjs +0 -4
  209. package/dist/web-server/index.mjs +0 -597
@@ -1,1870 +0,0 @@
1
- import { r as reactExports, j as jsxRuntimeExports } from "../_libs/react.mjs";
2
- import { u as useShallow } from "../_libs/zustand.mjs";
3
- import { n as Route, h as useChatStore, c as createSession, D as DropdownMenu, b as DropdownMenuTrigger, d as DropdownMenuContent, e as DropdownMenuItem, r as renameSession, B as Button, u as updateModelConfig, a as cn, l as deleteSessionApi, m as respondApproval } from "./router-sbsNus0Y.mjs";
4
- import { T as Textarea } from "./textarea-DfRheWY0.mjs";
5
- import { u as useModels } from "./queries-3H_19mUt.mjs";
6
- import { d as useNavigate } from "../_libs/tanstack__react-router.mjs";
7
- import { T as IconPlus, v as IconTrash, r as IconDots, s as IconPencil, I as IconSearch, Z as IconBell, _ as IconMail, z as IconCode, U as IconClock, $ as IconChartLine, a0 as IconShieldCheck, R as IconAlertTriangle, O as IconCheck, a as IconX, a1 as IconCommand, a2 as IconPaperclip, B as IconCloud, C as IconLoader2, a3 as IconPlayerStop, D as IconArrowUp, a4 as IconPhoto, a5 as IconFile, Y as IconChevronRight, u as IconCopy } from "../_libs/tabler__icons-react.mjs";
8
- import { M as Markdown } from "../_libs/react-markdown.mjs";
9
- import { r as remarkGfm } from "../_libs/remark-gfm.mjs";
10
- import "../_libs/tanstack__query-core.mjs";
11
- import "../_libs/tanstack__react-query.mjs";
12
- import "../_libs/clsx.mjs";
13
- import "../_libs/class-variance-authority.mjs";
14
- import "../_libs/base-ui__react.mjs";
15
- import "../_libs/base-ui__utils.mjs";
16
- import "../_libs/use-sync-external-store.mjs";
17
- import "../_libs/react-dom.mjs";
18
- import "util";
19
- import "crypto";
20
- import "async_hooks";
21
- import "stream";
22
- import "../_libs/floating-ui__utils.mjs";
23
- import "../_libs/floating-ui__react-dom.mjs";
24
- import "../_libs/floating-ui__dom.mjs";
25
- import "../_libs/floating-ui__core.mjs";
26
- import "../_libs/zod.mjs";
27
- import "../_libs/tanstack__router-core.mjs";
28
- import "../_libs/tanstack__history.mjs";
29
- import "../_libs/cookie-es.mjs";
30
- import "../_libs/seroval.mjs";
31
- import "../_libs/seroval-plugins.mjs";
32
- import "node:stream/web";
33
- import "node:stream";
34
- import "../_libs/isbot.mjs";
35
- import "../_libs/devlop.mjs";
36
- import "../_libs/unified.mjs";
37
- import "../_libs/bail.mjs";
38
- import "../_libs/extend.mjs";
39
- import "../_libs/is-plain-obj.mjs";
40
- import "../_libs/trough.mjs";
41
- import "../_libs/vfile.mjs";
42
- import "../_libs/vfile-message.mjs";
43
- import "../_libs/unist-util-stringify-position.mjs";
44
- import "node:process";
45
- import "node:path";
46
- import "node:url";
47
- import "../_libs/remark-parse.mjs";
48
- import "../_libs/mdast-util-from-markdown.mjs";
49
- import "../_libs/micromark-util-decode-numeric-character-reference+[...].mjs";
50
- import "../_libs/micromark-util-decode-string.mjs";
51
- import "../_libs/decode-named-character-reference+[...].mjs";
52
- import "../_libs/character-entities.mjs";
53
- import "../_libs/micromark-util-normalize-identifier+[...].mjs";
54
- import "../_libs/micromark.mjs";
55
- import "../_libs/micromark-util-combine-extensions+[...].mjs";
56
- import "../_libs/micromark-util-chunked.mjs";
57
- import "../_libs/micromark-factory-space.mjs";
58
- import "../_libs/micromark-util-character.mjs";
59
- import "../_libs/micromark-core-commonmark.mjs";
60
- import "../_libs/micromark-util-classify-character+[...].mjs";
61
- import "../_libs/micromark-util-resolve-all.mjs";
62
- import "../_libs/micromark-util-subtokenize.mjs";
63
- import "../_libs/micromark-factory-destination.mjs";
64
- import "../_libs/micromark-factory-label.mjs";
65
- import "../_libs/micromark-factory-title.mjs";
66
- import "../_libs/micromark-factory-whitespace.mjs";
67
- import "../_libs/micromark-util-html-tag-name.mjs";
68
- import "../_libs/mdast-util-to-string.mjs";
69
- import "../_libs/remark-rehype.mjs";
70
- import "../_libs/mdast-util-to-hast.mjs";
71
- import "../_libs/ungap__structured-clone.mjs";
72
- import "../_libs/micromark-util-sanitize-uri.mjs";
73
- import "../_libs/unist-util-position.mjs";
74
- import "../_libs/trim-lines.mjs";
75
- import "../_libs/unist-util-visit.mjs";
76
- import "../_libs/unist-util-visit-parents.mjs";
77
- import "../_libs/unist-util-is.mjs";
78
- import "../_libs/hast-util-to-jsx-runtime.mjs";
79
- import "../_libs/comma-separated-tokens.mjs";
80
- import "../_libs/property-information.mjs";
81
- import "../_libs/space-separated-tokens.mjs";
82
- import "../_libs/style-to-js.mjs";
83
- import "../_libs/style-to-object.mjs";
84
- import "../_libs/inline-style-parser.mjs";
85
- import "../_libs/hast-util-whitespace.mjs";
86
- import "../_libs/estree-util-is-identifier-name.mjs";
87
- import "../_libs/html-url-attributes.mjs";
88
- import "../_libs/micromark-extension-gfm.mjs";
89
- import "../_libs/micromark-extension-gfm-autolink-literal+[...].mjs";
90
- import "../_libs/micromark-extension-gfm-footnote+[...].mjs";
91
- import "../_libs/micromark-extension-gfm-strikethrough+[...].mjs";
92
- import "../_libs/micromark-extension-gfm-table.mjs";
93
- import "../_libs/micromark-extension-gfm-task-list-item+[...].mjs";
94
- import "../_libs/mdast-util-gfm.mjs";
95
- import "../_libs/mdast-util-gfm-autolink-literal+[...].mjs";
96
- import "../_libs/ccount.mjs";
97
- import "../_libs/mdast-util-find-and-replace.mjs";
98
- import "../_libs/escape-string-regexp.mjs";
99
- import "../_libs/mdast-util-gfm-footnote.mjs";
100
- import "../_libs/mdast-util-gfm-strikethrough.mjs";
101
- import "../_libs/mdast-util-gfm-table.mjs";
102
- import "../_libs/markdown-table.mjs";
103
- import "../_libs/mdast-util-to-markdown.mjs";
104
- import "../_libs/longest-streak.mjs";
105
- import "../_libs/mdast-util-phrasing.mjs";
106
- import "../_libs/mdast-util-gfm-task-list-item.mjs";
107
- function isImeComposing(e) {
108
- return e.nativeEvent?.isComposing === true;
109
- }
110
- const SLASH_COMMANDS = [
111
- // Chat control (local)
112
- { name: "/new", description: "Start a new chat", category: "chat", local: true },
113
- { name: "/clear", description: "Clear conversation history", category: "chat", local: true },
114
- // Agent commands (sent to backend)
115
- { name: "/btw", description: "Ask a side question without affecting context", category: "agent" },
116
- { name: "/approve", description: "Approve a pending action", category: "agent" },
117
- { name: "/deny", description: "Deny a pending action", category: "agent" },
118
- { name: "/status", description: "Show current agent status", category: "agent" },
119
- { name: "/reset", description: "Reset conversation context", category: "agent" },
120
- { name: "/compact", description: "Compact and summarize the conversation", category: "agent" },
121
- { name: "/undo", description: "Undo the last action", category: "agent" },
122
- { name: "/retry", description: "Retry the last failed action", category: "agent" },
123
- { name: "/compress", description: "Compress conversation with optional focus topic", category: "agent" },
124
- { name: "/debug", description: "Show diagnostics and debug info", category: "agent" },
125
- { name: "/goal", description: "Lock the agent onto a persistent cross-turn goal", category: "agent" },
126
- { name: "/steer", description: "Steer the in-flight agent without interrupting it", category: "agent" },
127
- { name: "/queue", description: "Queue a follow-up to run after the current turn", category: "agent" },
128
- // Tools
129
- { name: "/web", description: "Search the web", category: "tools" },
130
- { name: "/image", description: "Generate an image", category: "tools" },
131
- { name: "/browse", description: "Browse a URL", category: "tools" },
132
- { name: "/code", description: "Write or execute code", category: "tools" },
133
- { name: "/file", description: "Read or write files", category: "tools" },
134
- { name: "/shell", description: "Run a shell command", category: "tools" },
135
- // Info (local)
136
- { name: "/help", description: "Show available commands and help", category: "info", local: true },
137
- { name: "/model", description: "Show or switch the current model", category: "info", local: true },
138
- { name: "/memory", description: "Show agent memory", category: "info", local: true },
139
- { name: "/persona", description: "Show current persona", category: "info", local: true },
140
- { name: "/version", description: "Show Hermes version", category: "info", local: true },
141
- { name: "/usage", description: "Show token usage and cost", category: "info", local: true },
142
- { name: "/tools", description: "List available tools", category: "info", local: true },
143
- { name: "/skills", description: "List installed skills", category: "info", local: true }
144
- ];
145
- function isLocalCommand(text) {
146
- if (!text.startsWith("/")) return false;
147
- const cmd = text.split(/\s+/)[0].toLowerCase();
148
- return SLASH_COMMANDS.some((c) => c.name === cmd && c.local);
149
- }
150
- function useInputHistory({
151
- currentInput,
152
- applyText
153
- }) {
154
- const [history, setHistory] = reactExports.useState([]);
155
- const indexRef = reactExports.useRef(-1);
156
- const draftRef = reactExports.useRef("");
157
- const push = reactExports.useCallback((text) => {
158
- setHistory((prev) => [...prev, text]);
159
- indexRef.current = -1;
160
- draftRef.current = "";
161
- }, []);
162
- const recallPrev = reactExports.useCallback(() => {
163
- if (history.length === 0) return false;
164
- const cur = indexRef.current;
165
- const next = cur === -1 ? history.length - 1 : Math.max(0, cur - 1);
166
- if (cur === -1) draftRef.current = currentInput;
167
- indexRef.current = next;
168
- applyText(history[next]);
169
- return true;
170
- }, [history, currentInput, applyText]);
171
- const recallNext = reactExports.useCallback(() => {
172
- const cur = indexRef.current;
173
- if (cur === -1) return false;
174
- if (cur < history.length - 1) {
175
- indexRef.current = cur + 1;
176
- applyText(history[cur + 1]);
177
- } else {
178
- indexRef.current = -1;
179
- applyText(draftRef.current);
180
- }
181
- return true;
182
- }, [history, applyText]);
183
- const isNavigating = reactExports.useCallback(() => indexRef.current !== -1, []);
184
- const size = reactExports.useCallback(() => history.length, [history.length]);
185
- return { push, recallPrev, recallNext, isNavigating, size };
186
- }
187
- function formatSize(bytes) {
188
- if (bytes < 1024) return `${bytes} B`;
189
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
190
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
191
- }
192
- function AttachmentThumb({ attachment, onRemove }) {
193
- const isImage = attachment.type.startsWith("image/");
194
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "group relative flex shrink-0 items-center gap-2 rounded-lg border border-border bg-muted/40 px-2 py-1.5 text-xs", children: [
195
- isImage && attachment.previewUrl ? /* @__PURE__ */ jsxRuntimeExports.jsx(
196
- "img",
197
- {
198
- src: attachment.previewUrl,
199
- alt: attachment.name,
200
- className: "h-10 w-10 rounded object-cover"
201
- }
202
- ) : isImage ? /* @__PURE__ */ jsxRuntimeExports.jsx(IconPhoto, { className: "size-5 text-muted-foreground" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(IconFile, { className: "size-5 text-muted-foreground" }),
203
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col min-w-0", children: [
204
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate max-w-[120px] font-medium text-foreground", children: attachment.name }),
205
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-muted-foreground", children: formatSize(attachment.size) })
206
- ] }),
207
- /* @__PURE__ */ jsxRuntimeExports.jsx(
208
- "button",
209
- {
210
- onClick: (e) => {
211
- e.stopPropagation();
212
- onRemove();
213
- },
214
- className: "absolute -top-1.5 -right-1.5 flex size-4 items-center justify-center rounded-full bg-muted-foreground/20 text-muted-foreground opacity-0 group-hover:opacity-100 hover:bg-destructive hover:text-destructive-foreground transition-all",
215
- "aria-label": `Remove ${attachment.name}`,
216
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(IconX, { className: "size-2.5" })
217
- }
218
- )
219
- ] });
220
- }
221
- function AttachmentPreview({ attachments, onRemove, compact = false }) {
222
- if (attachments.length === 0) return null;
223
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
224
- "div",
225
- {
226
- className: cn(
227
- "flex gap-2 overflow-x-auto",
228
- compact ? "px-3 pt-3" : "px-3 pt-3"
229
- ),
230
- children: attachments.map((a) => /* @__PURE__ */ jsxRuntimeExports.jsx(
231
- AttachmentThumb,
232
- {
233
- attachment: a,
234
- onRemove: () => onRemove(a.id)
235
- },
236
- a.id
237
- ))
238
- }
239
- );
240
- }
241
- function displayName(model) {
242
- if (model.name && model.name !== model.id) return model.name;
243
- const short = model.id.split("/").pop() || model.id;
244
- return short.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
245
- }
246
- function uid() {
247
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
248
- }
249
- const ChatInput = reactExports.forwardRef(
250
- function ChatInput2({ sessionId, model, isLoading, onSubmit, onAbort }, ref) {
251
- const [input, setInput] = reactExports.useState("");
252
- const [isComposing, setIsComposing] = reactExports.useState(false);
253
- const [slashMenuOpen, setSlashMenuOpen] = reactExports.useState(false);
254
- const [slashFilter, setSlashFilter] = reactExports.useState("");
255
- const [slashSelectedIndex, setSlashSelectedIndex] = reactExports.useState(0);
256
- const [attachments, setAttachments] = reactExports.useState([]);
257
- const [isDragOver, setIsDragOver] = reactExports.useState(false);
258
- const inputRef = reactExports.useRef(null);
259
- const slashMenuRef = reactExports.useRef(null);
260
- const fileInputRef = reactExports.useRef(null);
261
- const { data: modelsData, isLoading: modelsLoading } = useModels();
262
- const models = modelsData?.models ?? [];
263
- const [selectedModelId, setSelectedModelId] = reactExports.useState("");
264
- reactExports.useEffect(() => {
265
- if (!models.length) return;
266
- const sessionModelValid = model && models.some((m) => m.id === model);
267
- const selectedValid = selectedModelId && models.some((m) => m.id === selectedModelId);
268
- if (sessionModelValid) {
269
- if (selectedModelId !== model) {
270
- setSelectedModelId(model);
271
- }
272
- return;
273
- }
274
- if (!selectedValid) {
275
- const target = modelsData?.default_model || models[0]?.id;
276
- if (target) setSelectedModelId(target);
277
- }
278
- }, [models, modelsData?.default_model, model]);
279
- const selectedModel = models.find((m) => m.id === selectedModelId);
280
- const handleModelChange = reactExports.useCallback(
281
- async (nextModelId) => {
282
- setSelectedModelId(nextModelId);
283
- try {
284
- await updateModelConfig({ model: nextModelId, session_id: sessionId });
285
- } catch {
286
- }
287
- },
288
- [sessionId]
289
- );
290
- const autoResize = reactExports.useCallback(() => {
291
- const el = inputRef.current;
292
- if (!el) return;
293
- el.style.height = "auto";
294
- el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
295
- }, []);
296
- const applyHistoryText = reactExports.useCallback(
297
- (text) => {
298
- setInput(text);
299
- requestAnimationFrame(() => {
300
- autoResize();
301
- inputRef.current?.setSelectionRange(text.length, text.length);
302
- });
303
- },
304
- [autoResize]
305
- );
306
- const history = useInputHistory({ currentInput: input, applyText: applyHistoryText });
307
- reactExports.useImperativeHandle(
308
- ref,
309
- () => ({
310
- setText(text) {
311
- setInput(text);
312
- requestAnimationFrame(() => {
313
- autoResize();
314
- inputRef.current?.focus();
315
- });
316
- },
317
- clear() {
318
- setInput("");
319
- if (inputRef.current) inputRef.current.style.height = "auto";
320
- },
321
- focus() {
322
- inputRef.current?.focus();
323
- }
324
- }),
325
- [autoResize]
326
- );
327
- reactExports.useEffect(() => {
328
- if (!isLoading) inputRef.current?.focus();
329
- }, [isLoading]);
330
- reactExports.useEffect(() => {
331
- if (!slashMenuOpen) return;
332
- function handleClick(e) {
333
- if (slashMenuRef.current && !slashMenuRef.current.contains(e.target)) {
334
- setSlashMenuOpen(false);
335
- }
336
- }
337
- document.addEventListener("mousedown", handleClick);
338
- return () => document.removeEventListener("mousedown", handleClick);
339
- }, [slashMenuOpen]);
340
- reactExports.useEffect(() => {
341
- if (!slashMenuOpen) return;
342
- const active = slashMenuRef.current?.querySelector(".slash-item-active");
343
- active?.scrollIntoView({ block: "nearest" });
344
- }, [slashSelectedIndex, slashMenuOpen]);
345
- const filteredCommands = reactExports.useMemo(
346
- () => slashMenuOpen ? SLASH_COMMANDS.filter(
347
- (cmd) => cmd.name.toLowerCase().startsWith(slashFilter.toLowerCase())
348
- ) : [],
349
- [slashMenuOpen, slashFilter]
350
- );
351
- const addFiles = reactExports.useCallback((files) => {
352
- const newAttachments = Array.from(files).map((file) => ({
353
- id: uid(),
354
- name: file.name,
355
- type: file.type || "application/octet-stream",
356
- size: file.size,
357
- file,
358
- previewUrl: file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0
359
- }));
360
- setAttachments((prev) => [...prev, ...newAttachments]);
361
- }, []);
362
- const removeAttachment = reactExports.useCallback((id) => {
363
- setAttachments((prev) => {
364
- const removed = prev.find((a) => a.id === id);
365
- if (removed?.previewUrl) URL.revokeObjectURL(removed.previewUrl);
366
- return prev.filter((a) => a.id !== id);
367
- });
368
- }, []);
369
- reactExports.useEffect(() => {
370
- return () => {
371
- attachments.forEach((a) => {
372
- if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
373
- });
374
- };
375
- }, []);
376
- const handleDragOver = reactExports.useCallback((e) => {
377
- e.preventDefault();
378
- e.stopPropagation();
379
- setIsDragOver(true);
380
- }, []);
381
- const handleDragLeave = reactExports.useCallback((e) => {
382
- e.preventDefault();
383
- e.stopPropagation();
384
- if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget)) {
385
- setIsDragOver(false);
386
- }
387
- }, []);
388
- const handleDrop = reactExports.useCallback(
389
- (e) => {
390
- e.preventDefault();
391
- e.stopPropagation();
392
- setIsDragOver(false);
393
- if (e.dataTransfer.files.length > 0) addFiles(e.dataTransfer.files);
394
- },
395
- [addFiles]
396
- );
397
- const handlePaste = reactExports.useCallback(
398
- (e) => {
399
- const items = e.clipboardData?.items;
400
- if (!items) return;
401
- const imageFiles = [];
402
- for (let i = 0; i < items.length; i++) {
403
- const item = items[i];
404
- if (item.type.startsWith("image/")) {
405
- const file = item.getAsFile();
406
- if (file) {
407
- const ext = item.type.split("/")[1] || "png";
408
- const renamed = new File([file], `pasted-image-${Date.now()}.${ext}`, {
409
- type: item.type
410
- });
411
- imageFiles.push(renamed);
412
- }
413
- }
414
- }
415
- if (imageFiles.length > 0) {
416
- e.preventDefault();
417
- addFiles(imageFiles);
418
- }
419
- },
420
- [addFiles]
421
- );
422
- function clearAfterSend(text) {
423
- history.push(text);
424
- setInput("");
425
- if (inputRef.current) inputRef.current.style.height = "auto";
426
- }
427
- function handleSend() {
428
- const text = input.trim();
429
- if (!text || isLoading) return;
430
- setSlashMenuOpen(false);
431
- clearAfterSend(text);
432
- onSubmit(text, selectedModelId, attachments);
433
- setAttachments([]);
434
- }
435
- function handleSlashSelect(cmd) {
436
- setSlashMenuOpen(false);
437
- if (cmd.local || cmd.category === "info") {
438
- setInput("");
439
- if (inputRef.current) inputRef.current.style.height = "auto";
440
- onSubmit(cmd.name, selectedModelId);
441
- return;
442
- }
443
- setInput(cmd.name + " ");
444
- inputRef.current?.focus();
445
- }
446
- function handleInputChange(e) {
447
- const value = e.target.value;
448
- setInput(value);
449
- autoResize();
450
- if (value.startsWith("/") && !value.includes(" ")) {
451
- setSlashMenuOpen(true);
452
- setSlashFilter(value);
453
- setSlashSelectedIndex(0);
454
- } else if (slashMenuOpen) {
455
- setSlashMenuOpen(false);
456
- }
457
- }
458
- function handleKeyDown(e) {
459
- if (isImeComposing(e)) return;
460
- if (slashMenuOpen && filteredCommands.length > 0) {
461
- if (e.key === "ArrowDown") {
462
- e.preventDefault();
463
- setSlashSelectedIndex(
464
- (i) => i < filteredCommands.length - 1 ? i + 1 : 0
465
- );
466
- return;
467
- }
468
- if (e.key === "ArrowUp") {
469
- e.preventDefault();
470
- setSlashSelectedIndex(
471
- (i) => i > 0 ? i - 1 : filteredCommands.length - 1
472
- );
473
- return;
474
- }
475
- if (e.key === "Enter" || e.key === "Tab") {
476
- e.preventDefault();
477
- handleSlashSelect(filteredCommands[slashSelectedIndex]);
478
- return;
479
- }
480
- if (e.key === "Escape") {
481
- e.preventDefault();
482
- setSlashMenuOpen(false);
483
- return;
484
- }
485
- }
486
- if (!slashMenuOpen && (history.isNavigating() || !input.includes("\n"))) {
487
- if (e.key === "ArrowUp" && history.size() > 0) {
488
- if (history.recallPrev()) {
489
- e.preventDefault();
490
- return;
491
- }
492
- }
493
- if (e.key === "ArrowDown" && history.isNavigating()) {
494
- if (history.recallNext()) {
495
- e.preventDefault();
496
- return;
497
- }
498
- }
499
- }
500
- if (e.key === "Enter" && !e.shiftKey) {
501
- e.preventDefault();
502
- handleSend();
503
- }
504
- }
505
- const canSend = input.trim().length > 0;
506
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-3xl mx-auto flex flex-col gap-2 relative", children: [
507
- slashMenuOpen && filteredCommands.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(
508
- "div",
509
- {
510
- ref: slashMenuRef,
511
- className: "absolute bottom-full left-0 right-0 mb-1 rounded-xl border border-border bg-card shadow-lg z-50 overflow-hidden",
512
- children: [
513
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 px-3 py-2 text-[11px] font-medium text-muted-foreground border-b border-border/60 bg-muted/30", children: [
514
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconCommand, { className: "size-3" }),
515
- "Commands"
516
- ] }),
517
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-h-[240px] overflow-y-auto py-1", children: filteredCommands.map((cmd, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
518
- "button",
519
- {
520
- className: cn(
521
- "w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors text-left",
522
- i === slashSelectedIndex ? "bg-accent text-accent-foreground slash-item-active" : "hover:bg-accent/50"
523
- ),
524
- onMouseEnter: () => setSlashSelectedIndex(i),
525
- onClick: () => handleSlashSelect(cmd),
526
- children: [
527
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs text-primary", children: cmd.name }),
528
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground text-xs", children: cmd.description })
529
- ]
530
- },
531
- cmd.name
532
- )) })
533
- ]
534
- }
535
- ),
536
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
537
- "div",
538
- {
539
- className: cn(
540
- "flex min-h-[80px] flex-col rounded-2xl cursor-text bg-card border shadow-lg transition-colors",
541
- isDragOver ? "border-primary border-dashed" : "border-border"
542
- ),
543
- onDragOver: handleDragOver,
544
- onDragLeave: handleDragLeave,
545
- onDrop: handleDrop,
546
- children: [
547
- /* @__PURE__ */ jsxRuntimeExports.jsx(
548
- AttachmentPreview,
549
- {
550
- attachments,
551
- onRemove: removeAttachment,
552
- compact: true
553
- }
554
- ),
555
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 relative overflow-y-auto max-h-[200px]", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
556
- Textarea,
557
- {
558
- ref: inputRef,
559
- value: input,
560
- onChange: handleInputChange,
561
- onKeyDown: handleKeyDown,
562
- onPaste: handlePaste,
563
- onCompositionStart: () => setIsComposing(true),
564
- onCompositionEnd: () => setIsComposing(false),
565
- placeholder: "Ask anything…",
566
- rows: 1,
567
- className: cn(
568
- "w-full !rounded-none border-0 p-3 outline-none text-[15px] text-foreground resize-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-transparent bg-transparent whitespace-pre-wrap break-words",
569
- "min-h-[48px] max-h-[200px]"
570
- )
571
- }
572
- ) }),
573
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex min-h-[40px] items-center gap-2 p-2 pb-1", children: [
574
- /* @__PURE__ */ jsxRuntimeExports.jsx(
575
- Button,
576
- {
577
- variant: "ghost",
578
- size: "icon-sm",
579
- className: "text-muted-foreground hover:text-foreground transition-colors",
580
- title: "Attach images",
581
- onClick: () => fileInputRef.current?.click(),
582
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(IconPaperclip, { className: "h-5 w-5" })
583
- }
584
- ),
585
- /* @__PURE__ */ jsxRuntimeExports.jsxs(DropdownMenu, { children: [
586
- /* @__PURE__ */ jsxRuntimeExports.jsxs(DropdownMenuTrigger, { className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer outline-none ml-1", children: [
587
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconCloud, { className: "h-4 w-4 shrink-0" }),
588
- modelsLoading && /* @__PURE__ */ jsxRuntimeExports.jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin shrink-0" }),
589
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: cn(modelsLoading && "opacity-60"), children: modelsLoading ? "Loading…" : selectedModel ? displayName(selectedModel) : "Select model" })
590
- ] }),
591
- /* @__PURE__ */ jsxRuntimeExports.jsx(DropdownMenuContent, { align: "start", className: "max-h-[300px] overflow-y-auto min-w-48", children: models.map((model2) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
592
- DropdownMenuItem,
593
- {
594
- onClick: () => handleModelChange(model2.id),
595
- className: cn(
596
- "flex-col items-start py-2",
597
- model2.id === selectedModelId && "bg-muted"
598
- ),
599
- children: [
600
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm", children: displayName(model2) }),
601
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground text-xs", children: model2.provider_name })
602
- ]
603
- },
604
- model2.id
605
- )) })
606
- ] }),
607
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ml-auto flex items-center gap-2", children: isLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx(
608
- Button,
609
- {
610
- variant: "ghost",
611
- size: "icon-sm",
612
- className: "rounded-full bg-destructive hover:bg-destructive/90 text-destructive-foreground transition-colors cursor-pointer",
613
- onClick: onAbort,
614
- title: "Stop generating",
615
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(IconPlayerStop, { className: "h-4 w-4" })
616
- }
617
- ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
618
- Button,
619
- {
620
- variant: "ghost",
621
- size: "icon-sm",
622
- className: cn(
623
- "rounded-full transition-colors cursor-pointer",
624
- canSend ? "bg-primary hover:bg-primary/90 text-primary-foreground" : "bg-primary text-primary-foreground opacity-40"
625
- ),
626
- disabled: !canSend,
627
- onClick: handleSend,
628
- title: "Send message",
629
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(IconArrowUp, { className: "h-4 w-4" })
630
- }
631
- ) })
632
- ] })
633
- ]
634
- }
635
- ),
636
- /* @__PURE__ */ jsxRuntimeExports.jsx(
637
- "input",
638
- {
639
- ref: fileInputRef,
640
- type: "file",
641
- multiple: true,
642
- accept: "image/*",
643
- className: "hidden",
644
- onChange: (e) => {
645
- if (e.target.files?.length) {
646
- addFiles(e.target.files);
647
- e.target.value = "";
648
- }
649
- }
650
- }
651
- )
652
- ] });
653
- }
654
- );
655
- const ChatHeader = reactExports.memo(function ChatHeader2({
656
- sessionId,
657
- title,
658
- hasMessages,
659
- onNewChat,
660
- onClear
661
- }) {
662
- const navigate = useNavigate();
663
- const deleteStoreSession = useChatStore((s) => s.deleteSession);
664
- const handleDelete = async () => {
665
- try {
666
- await deleteSessionApi(sessionId);
667
- deleteStoreSession(sessionId);
668
- navigate({ to: "/chat" });
669
- } catch {
670
- }
671
- };
672
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "group/header shrink-0 px-2 py-1 flex items-center gap-1.5 min-w-0", children: [
673
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-medium text-muted-foreground truncate flex-1 select-none", children: title || "Conversation" }),
674
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity", children: [
675
- /* @__PURE__ */ jsxRuntimeExports.jsx(
676
- "button",
677
- {
678
- onClick: onNewChat,
679
- className: "flex items-center justify-center rounded-lg p-1 text-muted-foreground hover:bg-sidebar-muted hover:text-foreground transition-colors",
680
- title: "New chat",
681
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(IconPlus, { className: "size-3.5" })
682
- }
683
- ),
684
- hasMessages && /* @__PURE__ */ jsxRuntimeExports.jsx(
685
- "button",
686
- {
687
- onClick: onClear,
688
- className: "flex items-center justify-center rounded-lg p-1 text-muted-foreground hover:bg-sidebar-muted hover:text-foreground transition-colors",
689
- title: "Clear chat",
690
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(IconTrash, { className: "size-3.5" })
691
- }
692
- ),
693
- /* @__PURE__ */ jsxRuntimeExports.jsxs(DropdownMenu, { children: [
694
- /* @__PURE__ */ jsxRuntimeExports.jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "flex items-center justify-center rounded-lg p-1 text-muted-foreground hover:bg-sidebar-muted hover:text-foreground transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(IconDots, { className: "size-3.5" }) }) }),
695
- /* @__PURE__ */ jsxRuntimeExports.jsxs(DropdownMenuContent, { align: "end", side: "bottom", sideOffset: 4, children: [
696
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
697
- DropdownMenuItem,
698
- {
699
- className: "gap-2 text-xs",
700
- onClick: () => {
701
- const newTitle = prompt("Rename conversation", title || "");
702
- if (newTitle?.trim()) {
703
- renameSession(sessionId, newTitle.trim());
704
- useChatStore.getState().updateSession(sessionId, { title: newTitle.trim() });
705
- }
706
- },
707
- children: [
708
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconPencil, { className: "size-3" }),
709
- "Rename"
710
- ]
711
- }
712
- ),
713
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
714
- DropdownMenuItem,
715
- {
716
- className: "gap-2 text-xs text-destructive",
717
- onClick: handleDelete,
718
- children: [
719
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconTrash, { className: "size-3" }),
720
- "Delete"
721
- ]
722
- }
723
- )
724
- ] })
725
- ] })
726
- ] })
727
- ] });
728
- });
729
- const SUGGESTIONS = [
730
- {
731
- label: "Search the web",
732
- text: "Search the web for today's top tech news",
733
- Icon: IconSearch
734
- },
735
- {
736
- label: "Set a reminder",
737
- text: "Set a reminder to check emails every day at 9 AM",
738
- Icon: IconBell
739
- },
740
- {
741
- label: "Read emails",
742
- text: "Read my latest emails and summarize them",
743
- Icon: IconMail
744
- },
745
- {
746
- label: "Write a script",
747
- text: "Write a Python script to rename all files in a folder",
748
- Icon: IconCode
749
- },
750
- {
751
- label: "Schedule a job",
752
- text: "Schedule a cron job to back up my database every night",
753
- Icon: IconClock
754
- },
755
- {
756
- label: "Analyze data",
757
- text: "Analyze this CSV file and show key insights",
758
- Icon: IconChartLine
759
- }
760
- ];
761
- const ChatEmptyState = reactExports.memo(function ChatEmptyState2({
762
- onSelectSuggestion
763
- }) {
764
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full px-6 py-12", children: [
765
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-6 flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/20 to-primary/5 text-primary", children: /* @__PURE__ */ jsxRuntimeExports.jsx(IconSearch, { className: "w-8 h-8", strokeWidth: 1.5 }) }),
766
- /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "How can I help?" }),
767
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-muted-foreground mb-8 max-w-sm text-center", children: "Ask anything or pick a suggestion to get started." }),
768
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2 w-full max-w-lg", children: SUGGESTIONS.map(({ text, label, Icon }) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
769
- "button",
770
- {
771
- className: "flex items-center gap-3 rounded-xl border border-border/60 bg-card px-4 py-3 text-left text-sm text-foreground hover:bg-accent hover:border-accent transition-colors group",
772
- onClick: () => onSelectSuggestion(text),
773
- children: [
774
- /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { className: "w-4 h-4 shrink-0 text-muted-foreground group-hover:text-foreground transition-colors" }),
775
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate", children: label })
776
- ]
777
- },
778
- text
779
- )) })
780
- ] });
781
- });
782
- const SyncHighlighter = reactExports.lazy(() => import("./syntax-highlighter-5vezNTce.mjs").then((m) => ({ default: m.SyncHighlighter })));
783
- function CopyButton({ text }) {
784
- const [copied, setCopied] = reactExports.useState(false);
785
- const handleCopy = async () => {
786
- await navigator.clipboard.writeText(text);
787
- setCopied(true);
788
- setTimeout(() => setCopied(false), 2e3);
789
- };
790
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
791
- "button",
792
- {
793
- onClick: handleCopy,
794
- className: "absolute right-2 top-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors",
795
- "aria-label": "Copy code",
796
- children: copied ? /* @__PURE__ */ jsxRuntimeExports.jsx(IconCheck, { className: "size-3" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(IconCopy, { className: "size-3" })
797
- }
798
- );
799
- }
800
- function BubbleCopyButton({ text }) {
801
- const [copied, setCopied] = reactExports.useState(false);
802
- const handleCopy = async () => {
803
- await navigator.clipboard.writeText(text);
804
- setCopied(true);
805
- setTimeout(() => setCopied(false), 2e3);
806
- };
807
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
808
- "button",
809
- {
810
- onClick: handleCopy,
811
- className: "inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] text-muted-foreground/50 hover:text-foreground hover:bg-muted transition-colors",
812
- "aria-label": "Copy message",
813
- children: [
814
- copied ? /* @__PURE__ */ jsxRuntimeExports.jsx(IconCheck, { className: "size-3" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(IconCopy, { className: "size-3" }),
815
- copied ? "Copied" : "Copy"
816
- ]
817
- }
818
- );
819
- }
820
- function formatThinkingDuration(ms) {
821
- const s = Math.floor(ms / 1e3);
822
- if (s < 60) return `${s}s`;
823
- const m = Math.floor(s / 60);
824
- const r = s % 60;
825
- return r === 0 ? `${m}m` : `${m}m ${r}s`;
826
- }
827
- function ThinkingCard({
828
- reasoning,
829
- isStreaming,
830
- startedAt,
831
- endedAt
832
- }) {
833
- const [open, setOpen] = reactExports.useState(isStreaming ?? false);
834
- reactExports.useEffect(() => {
835
- setOpen(isStreaming ?? false);
836
- }, [isStreaming]);
837
- const [now, setNow] = reactExports.useState(Date.now());
838
- const tickRef = reactExports.useRef(null);
839
- reactExports.useEffect(() => {
840
- if (isStreaming && startedAt && !endedAt) {
841
- tickRef.current = setInterval(() => setNow(Date.now()), 1e3);
842
- return () => {
843
- if (tickRef.current) clearInterval(tickRef.current);
844
- };
845
- }
846
- if (tickRef.current) {
847
- clearInterval(tickRef.current);
848
- tickRef.current = null;
849
- }
850
- return void 0;
851
- }, [isStreaming, startedAt, endedAt]);
852
- if (!reasoning.trim()) return null;
853
- const durationMs = startedAt ? Math.max(0, (endedAt ?? (isStreaming ? now : startedAt)) - startedAt) : null;
854
- const charCount = [...reasoning].length;
855
- const hasThinking = isStreaming && startedAt && !endedAt;
856
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-1", children: [
857
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
858
- "button",
859
- {
860
- onClick: () => setOpen(!open),
861
- className: cn(
862
- "inline-flex items-center gap-1.5 text-[11px] transition-colors",
863
- hasThinking ? "text-amber-500 dark:text-amber-400 font-medium" : "text-muted-foreground/70 hover:text-foreground"
864
- ),
865
- children: [
866
- /* @__PURE__ */ jsxRuntimeExports.jsx(
867
- IconChevronRight,
868
- {
869
- className: cn(
870
- "size-3 shrink-0 transition-transform",
871
- open && "rotate-90"
872
- )
873
- }
874
- ),
875
- hasThinking ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1", children: [
876
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconLoader2, { className: "size-2.5 animate-spin" }),
877
- "thinking…"
878
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "thought" }),
879
- durationMs !== null && durationMs > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground/60 tabular-nums", children: [
880
- "· ",
881
- formatThinkingDuration(durationMs)
882
- ] }),
883
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground/60", children: [
884
- "· ",
885
- charCount.toLocaleString(),
886
- " chars"
887
- ] })
888
- ]
889
- }
890
- ),
891
- open && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 rounded-md bg-amber-50 dark:bg-amber-950/20 px-2.5 py-1.5 text-[11px] text-muted-foreground leading-relaxed whitespace-pre-wrap font-mono border border-amber-200 dark:border-amber-800/30", children: reasoning })
892
- ] });
893
- }
894
- function ToolCard({ name, content }) {
895
- const [open, setOpen] = reactExports.useState(false);
896
- const preview = content.length > 60 ? content.slice(0, 60) + "..." : content;
897
- if (!content) return null;
898
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-1", children: [
899
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
900
- "button",
901
- {
902
- onClick: () => setOpen(!open),
903
- className: "inline-flex items-center gap-1 text-[11px] text-muted-foreground/70 hover:text-foreground transition-colors truncate max-w-full",
904
- children: [
905
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconChevronRight, { className: cn("size-3 shrink-0 transition-transform", open && "rotate-90") }),
906
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "truncate", children: [
907
- name || "tool",
908
- ': "',
909
- preview,
910
- '"'
911
- ] })
912
- ]
913
- }
914
- ),
915
- open && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 rounded-md bg-muted/40 px-2.5 py-1.5 text-[11px] text-muted-foreground leading-relaxed whitespace-pre-wrap font-mono border border-border/50 max-h-[300px] overflow-auto", children: content })
916
- ] });
917
- }
918
- function formatToolPayload(raw) {
919
- if (!raw) return null;
920
- try {
921
- const parsed = JSON.parse(raw);
922
- return { text: JSON.stringify(parsed, null, 2), isJson: true };
923
- } catch {
924
- return { text: raw, isJson: false };
925
- }
926
- }
927
- function ToolInvocationCard({ tools }) {
928
- const [open, setOpen] = reactExports.useState(false);
929
- if (!tools?.length) return null;
930
- const runningCount = tools.filter((t) => t.status === "running").length;
931
- const doneCount = tools.filter((t) => t.status === "done").length;
932
- const errorCount = tools.filter((t) => t.status === "error").length;
933
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-1", children: [
934
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
935
- "button",
936
- {
937
- onClick: () => setOpen(!open),
938
- className: "inline-flex items-center gap-1 text-[11px] text-muted-foreground/70 hover:text-foreground transition-colors",
939
- children: [
940
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconChevronRight, { className: cn("size-3 shrink-0 transition-transform", open && "rotate-90") }),
941
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
942
- "used ",
943
- tools.length,
944
- " tool",
945
- tools.length > 1 ? "s" : ""
946
- ] }),
947
- runningCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-0.5 text-amber-500", children: [
948
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconLoader2, { className: "size-2.5 animate-spin" }),
949
- runningCount
950
- ] }),
951
- doneCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-emerald-500", children: [
952
- doneCount,
953
- " done"
954
- ] }),
955
- errorCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-red-500", children: [
956
- errorCount,
957
- " error"
958
- ] }),
959
- tools.length > 0 && runningCount === 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground/50 truncate max-w-[200px]", children: [
960
- "— ",
961
- tools.map((t) => t.name).join(", ")
962
- ] })
963
- ]
964
- }
965
- ),
966
- open && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 flex flex-col gap-1.5", children: tools.map((t, i) => {
967
- const formattedArgs = formatToolPayload(t.preview);
968
- const formattedOutput = formatToolPayload(t.output);
969
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
970
- "div",
971
- {
972
- className: "rounded-md bg-muted/40 border border-border/50 overflow-hidden",
973
- children: [
974
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 px-2.5 py-1.5", children: [
975
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] font-semibold text-foreground/80 font-mono", children: t.name }),
976
- t.status === "running" && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-amber-500", children: [
977
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconLoader2, { className: "size-2.5 animate-spin" }),
978
- "running"
979
- ] }),
980
- t.status === "done" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-emerald-500", children: "✓ done" }),
981
- t.status === "error" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-red-500", children: "✗ error" })
982
- ] }),
983
- formattedArgs && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-t border-border/30", children: [
984
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between px-2.5 py-0.5", children: [
985
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] uppercase tracking-wider text-muted-foreground/60", children: "Arguments" }),
986
- /* @__PURE__ */ jsxRuntimeExports.jsx(CopyButton, { text: formattedArgs.text })
987
- ] }),
988
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-2.5 pb-1.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: cn(
989
- "text-[10px] leading-relaxed overflow-x-auto font-mono whitespace-pre-wrap max-h-[200px] overflow-y-auto rounded",
990
- formattedArgs.isJson ? "bg-slate-900/10 dark:bg-slate-900/40 text-foreground/80 px-2 py-1" : "text-muted-foreground/70"
991
- ), children: formattedArgs.text }) })
992
- ] }),
993
- formattedOutput && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-t border-border/30", children: [
994
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between px-2.5 py-0.5", children: [
995
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] uppercase tracking-wider text-muted-foreground/60", children: "Result" }),
996
- /* @__PURE__ */ jsxRuntimeExports.jsx(CopyButton, { text: formattedOutput.text })
997
- ] }),
998
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-2.5 pb-1.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: cn(
999
- "text-[10px] leading-relaxed overflow-x-auto font-mono whitespace-pre-wrap max-h-[200px] overflow-y-auto rounded",
1000
- formattedOutput.isJson ? "bg-slate-900/10 dark:bg-slate-900/40 text-foreground/80 px-2 py-1" : "text-muted-foreground/70"
1001
- ), children: formattedOutput.text }) })
1002
- ] })
1003
- ]
1004
- },
1005
- t.callId || `${t.name}-${i}`
1006
- );
1007
- }) })
1008
- ] });
1009
- }
1010
- function CodeBlock({ language, value }) {
1011
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative group/code my-2", children: [
1012
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between rounded-t-lg bg-muted/80 px-3 py-1.5 border border-border border-b-0", children: [
1013
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-medium text-muted-foreground uppercase tracking-wide", children: language || "text" }),
1014
- /* @__PURE__ */ jsxRuntimeExports.jsx(CopyButton, { text: value })
1015
- ] }),
1016
- /* @__PURE__ */ jsxRuntimeExports.jsx(
1017
- reactExports.Suspense,
1018
- {
1019
- fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "bg-[#282c34] text-gray-300 rounded-b-lg px-4 py-3 text-[13px] leading-relaxed overflow-x-auto font-mono", children: /* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: value }) }),
1020
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(SyncHighlighter, { language, value })
1021
- }
1022
- )
1023
- ] });
1024
- }
1025
- const markdownComponents = {
1026
- pre({ children }) {
1027
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto max-w-full", children });
1028
- },
1029
- code({ className, children, ...props }) {
1030
- const match = /language-(\w+)/.exec(className || "");
1031
- const value = String(children).replace(/\n$/, "");
1032
- if (!match) {
1033
- return /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "rounded bg-muted px-1 py-0.5 text-[13px] font-mono text-foreground/90", ...props, children });
1034
- }
1035
- if (match[1] === "plaintext") {
1036
- return /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "rounded-lg bg-muted/50 border border-border/50 px-4 py-3 text-[13px] leading-relaxed overflow-x-auto font-mono whitespace-pre-wrap", children: value });
1037
- }
1038
- return /* @__PURE__ */ jsxRuntimeExports.jsx(CodeBlock, { language: match[1], value });
1039
- },
1040
- a({ href, children }) {
1041
- return /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href, target: "_blank", rel: "noopener noreferrer", className: "text-blue-600 dark:text-blue-400 underline underline-offset-2", children });
1042
- },
1043
- img({ src, alt }) {
1044
- return /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src, alt, className: "rounded-lg max-w-full my-2", loading: "lazy" });
1045
- },
1046
- table({ children }) {
1047
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto my-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx("table", { className: "w-full text-sm border-collapse border border-border rounded-lg overflow-hidden", children }) });
1048
- },
1049
- th({ children }) {
1050
- return /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "border border-border bg-muted px-3 py-1.5 text-left font-medium", children });
1051
- },
1052
- td({ children }) {
1053
- return /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "border border-border px-3 py-1.5", children });
1054
- },
1055
- blockquote({ children }) {
1056
- return /* @__PURE__ */ jsxRuntimeExports.jsx("blockquote", { className: "border-l-2 border-amber-400 dark:border-amber-600 pl-3 my-2 text-muted-foreground italic", children });
1057
- }
1058
- };
1059
- const BOX_DRAWING_RE = /[\u2500-\u257F]/;
1060
- function hasBoxDrawing(text) {
1061
- return BOX_DRAWING_RE.test(text);
1062
- }
1063
- function wrapAsciiArtCodeBlocks(content) {
1064
- const lines = content.split("\n");
1065
- const result = [];
1066
- let inArt = false;
1067
- for (let i = 0; i < lines.length; i++) {
1068
- const line = lines[i];
1069
- const isArtLine = hasBoxDrawing(line);
1070
- if (isArtLine && !inArt) {
1071
- if (result.length > 0 && result[result.length - 1] !== "") {
1072
- result.push("");
1073
- }
1074
- result.push("```plaintext");
1075
- inArt = true;
1076
- } else if (!isArtLine && inArt) {
1077
- result.push("```");
1078
- if (line !== "") {
1079
- result.push("");
1080
- }
1081
- inArt = false;
1082
- }
1083
- result.push(line);
1084
- }
1085
- if (inArt) {
1086
- result.push("```");
1087
- }
1088
- return result.join("\n");
1089
- }
1090
- function MessageBody({
1091
- content,
1092
- isStreaming
1093
- }) {
1094
- const processed = wrapAsciiArtCodeBlocks(content);
1095
- if (isStreaming && content) {
1096
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-w-none leading-relaxed", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "whitespace-pre-wrap text-base leading-relaxed", children: [
1097
- content,
1098
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-block w-0.5 h-4 bg-foreground/70 ml-0.5 align-text-bottom animate-pulse" })
1099
- ] }) });
1100
- }
1101
- if (isStreaming && !content) {
1102
- return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-block w-0.5 h-4 bg-foreground/70 ml-0.5 align-text-bottom animate-pulse" });
1103
- }
1104
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-w-none leading-relaxed [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_p]:my-3 [&_p]:leading-relaxed [&_ul]:my-3 [&_ul]:pl-5 [&_ul]:list-disc [&_ol]:my-3 [&_ol]:pl-5 [&_ol]:list-decimal [&_li]:my-1 [&_li]:leading-relaxed [&_h1]:mt-6 [&_h1]:mb-3 [&_h2]:mt-5 [&_h2]:mb-2 [&_h3]:mt-4 [&_h3]:mb-1.5 [&_h4]:mt-3 [&_h4]:mb-1 [&_blockquote]:my-3 [&_hr]:my-6 [&_table]:my-4 [&_pre]:my-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { remarkPlugins: [remarkGfm], components: markdownComponents, children: processed }) });
1105
- }
1106
- function formatTime(ts) {
1107
- if (!ts || isNaN(ts)) return null;
1108
- try {
1109
- return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1110
- } catch {
1111
- return null;
1112
- }
1113
- }
1114
- function ChatMessageRaw({ message }) {
1115
- const isUser = message.role === "user";
1116
- const isTool = message.role === "tool";
1117
- const hasContent = !!message.content?.trim();
1118
- const hasReasoning = !!message.reasoning?.trim();
1119
- const hasToolCalls = !!message.toolCalls?.length;
1120
- const timeLabel = formatTime(message.timestamp);
1121
- const isMinimal = isTool || !isUser && !hasContent && (hasReasoning || hasToolCalls);
1122
- if (isMinimal) {
1123
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-1", children: [
1124
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [
1125
- hasReasoning && /* @__PURE__ */ jsxRuntimeExports.jsx(ThinkingCard, { reasoning: message.reasoning, isStreaming: message.isStreaming, startedAt: message.reasoningStartedAt, endedAt: message.reasoningEndedAt }),
1126
- !isTool && hasToolCalls && /* @__PURE__ */ jsxRuntimeExports.jsx(ToolInvocationCard, { tools: message.toolCalls }),
1127
- isTool && /* @__PURE__ */ jsxRuntimeExports.jsx(ToolCard, { name: message.toolName || "", content: message.content })
1128
- ] }),
1129
- timeLabel && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mt-0.5 text-[10px] text-muted-foreground/40", children: timeLabel })
1130
- ] });
1131
- }
1132
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: cn("flex px-4", isUser ? "py-2 justify-end" : "py-4 justify-start"), children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: cn("flex min-w-0 flex-col", isUser ? "max-w-[85%] items-end" : "w-full"), children: [
1133
- message.isStreaming && !hasContent && !hasReasoning ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1.5 text-sm text-muted-foreground py-1", children: [
1134
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconLoader2, { className: "h-3.5 w-3.5 animate-spin" }),
1135
- "Thinking..."
1136
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 w-full", children: [
1137
- hasReasoning && /* @__PURE__ */ jsxRuntimeExports.jsx(ThinkingCard, { reasoning: message.reasoning, isStreaming: message.isStreaming, startedAt: message.reasoningStartedAt, endedAt: message.reasoningEndedAt }),
1138
- !isTool && !isUser && hasToolCalls && /* @__PURE__ */ jsxRuntimeExports.jsx(ToolInvocationCard, { tools: message.toolCalls }),
1139
- hasContent && (isUser ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-2xl px-4 py-2.5 text-sm leading-relaxed overflow-x-auto bg-primary text-primary-foreground", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1140
- MessageBody,
1141
- {
1142
- content: message.content,
1143
- isStreaming: message.isStreaming
1144
- }
1145
- ) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-base leading-relaxed", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1146
- MessageBody,
1147
- {
1148
- content: message.content,
1149
- isStreaming: message.isStreaming
1150
- }
1151
- ) }))
1152
- ] }),
1153
- hasContent && !isUser && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 flex items-center gap-2 px-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(BubbleCopyButton, { text: message.content }) }),
1154
- timeLabel && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mt-1 text-[10px] text-muted-foreground/50 px-1", children: timeLabel })
1155
- ] }) });
1156
- }
1157
- const ChatMessage = reactExports.memo(ChatMessageRaw);
1158
- const MessageList = reactExports.memo(function MessageList2({
1159
- messages,
1160
- isLoading
1161
- }) {
1162
- const mapped = reactExports.useMemo(
1163
- () => messages.map(
1164
- (msg) => ({
1165
- id: msg.id,
1166
- role: msg.role === "user" ? "user" : msg.role === "tool" ? "tool" : "assistant",
1167
- content: msg.content,
1168
- timestamp: msg.timestamp,
1169
- isStreaming: msg.isStreaming,
1170
- reasoning: msg.reasoning,
1171
- reasoningStartedAt: msg.reasoningStartedAt,
1172
- reasoningEndedAt: msg.reasoningEndedAt,
1173
- toolCalls: msg.toolCalls
1174
- })
1175
- ),
1176
- [messages]
1177
- );
1178
- return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: mapped.map((msg) => /* @__PURE__ */ jsxRuntimeExports.jsx(ChatMessage, { message: msg }, msg.id)) });
1179
- });
1180
- function useChatScroll(messages) {
1181
- const containerRef = reactExports.useRef(null);
1182
- const bottomRef = reactExports.useRef(null);
1183
- const userScrolledUpRef = reactExports.useRef(false);
1184
- const prevCountRef = reactExports.useRef(messages.length);
1185
- const scrollToBottom = reactExports.useCallback((force) => {
1186
- if (!force && userScrolledUpRef.current) return;
1187
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1188
- }, []);
1189
- const onScroll = reactExports.useCallback(() => {
1190
- const el = containerRef.current;
1191
- if (!el) return;
1192
- const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
1193
- userScrolledUpRef.current = !atBottom;
1194
- }, []);
1195
- reactExports.useEffect(() => {
1196
- const prevCount = prevCountRef.current;
1197
- prevCountRef.current = messages.length;
1198
- const userJustSent = messages.length > prevCount && messages[messages.length - 1]?.role === "user";
1199
- if (userJustSent) {
1200
- userScrolledUpRef.current = false;
1201
- scrollToBottom(true);
1202
- } else {
1203
- scrollToBottom();
1204
- }
1205
- }, [messages, scrollToBottom]);
1206
- return { containerRef, bottomRef, onScroll };
1207
- }
1208
- const BASE_URL = "http://localhost:47474";
1209
- async function* postChatStream(options) {
1210
- const { input, conversation, model, signal } = options;
1211
- const body = { input };
1212
- if (conversation) body.conversation = conversation;
1213
- if (model) body.model = model;
1214
- if (options.messages && options.messages.length > 0) {
1215
- body.messages = options.messages;
1216
- }
1217
- if (options.attachments && options.attachments.length > 0) {
1218
- body.attachments = options.attachments;
1219
- }
1220
- const res = await fetch(`${BASE_URL}/api/chat/stream`, {
1221
- method: "POST",
1222
- headers: { "Content-Type": "application/json" },
1223
- body: JSON.stringify(body),
1224
- signal
1225
- });
1226
- if (!res.ok) {
1227
- const text = await res.text().catch(() => "Stream request failed");
1228
- throw new Error(text);
1229
- }
1230
- if (!res.body) {
1231
- throw new Error("No response body");
1232
- }
1233
- const reader = res.body.getReader();
1234
- const decoder = new TextDecoder();
1235
- let buffer = "";
1236
- try {
1237
- while (true) {
1238
- const { done, value } = await reader.read();
1239
- if (done) break;
1240
- buffer += decoder.decode(value, { stream: true });
1241
- let boundary = buffer.indexOf("\n\n");
1242
- while (boundary >= 0) {
1243
- const frame = buffer.slice(0, boundary);
1244
- buffer = buffer.slice(boundary + 2);
1245
- let eventType = "";
1246
- let dataStr = "";
1247
- for (const line of frame.split("\n")) {
1248
- if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1249
- else if (line.startsWith("data: ")) {
1250
- dataStr += (dataStr ? "\n" : "") + line.slice(6);
1251
- }
1252
- }
1253
- if (dataStr) {
1254
- let parsed = {};
1255
- try {
1256
- parsed = JSON.parse(dataStr);
1257
- } catch {
1258
- parsed = { raw: dataStr };
1259
- }
1260
- yield { type: eventType || "unknown", data: parsed };
1261
- }
1262
- boundary = buffer.indexOf("\n\n");
1263
- }
1264
- }
1265
- } finally {
1266
- reader.releaseLock();
1267
- }
1268
- }
1269
- let msgCounter = 0;
1270
- function nextMsgId() {
1271
- return `msg-${++msgCounter}-${Math.random().toString(36).slice(2, 6)}`;
1272
- }
1273
- function useChatStream() {
1274
- const abortRef = reactExports.useRef(null);
1275
- const setStreaming = reactExports.useCallback((sessionId, loading) => {
1276
- useChatStore.setState((s) => ({
1277
- streamingSessions: loading ? /* @__PURE__ */ new Set([...s.streamingSessions, sessionId]) : new Set([...s.streamingSessions].filter((id) => id !== sessionId))
1278
- }));
1279
- }, []);
1280
- const abortChat = reactExports.useCallback(() => {
1281
- if (abortRef.current) {
1282
- abortRef.current.abort();
1283
- abortRef.current = null;
1284
- }
1285
- }, []);
1286
- const sendMessage = reactExports.useCallback(
1287
- async (text, sessionId, model, options) => {
1288
- if (useChatStore.getState().streamingSessions.has(sessionId)) return;
1289
- const userMsg = {
1290
- id: nextMsgId(),
1291
- role: "user",
1292
- content: text,
1293
- timestamp: Date.now()
1294
- };
1295
- useChatStore.setState((s) => {
1296
- const existing = s.sessions.find((sx) => sx.id === sessionId);
1297
- if (!existing) {
1298
- const now = Date.now();
1299
- const seeded = {
1300
- id: sessionId,
1301
- title: text.slice(0, 64),
1302
- model: model || "",
1303
- messages: [userMsg],
1304
- createdAt: now,
1305
- updatedAt: now
1306
- };
1307
- return { sessions: [seeded, ...s.sessions] };
1308
- }
1309
- return {
1310
- sessions: s.sessions.map(
1311
- (sx) => sx.id === sessionId ? {
1312
- ...sx,
1313
- messages: [...sx.messages, userMsg],
1314
- model: model || sx.model || "",
1315
- title: !sx.title || sx.title === "New Conversation" || sx.title === "Untitled" ? text.slice(0, 64) : sx.title
1316
- } : sx
1317
- )
1318
- };
1319
- });
1320
- const history = useChatStore.getState().sessions.find((sx) => sx.id === sessionId)?.messages.slice(0, -1).filter((m) => !m.isStreaming && m.content).map((m) => ({ role: m.role, content: m.content })) || [];
1321
- const asstId = nextMsgId();
1322
- const placeholder = {
1323
- id: asstId,
1324
- role: "assistant",
1325
- content: "",
1326
- timestamp: Date.now(),
1327
- isStreaming: true
1328
- };
1329
- useChatStore.setState((s) => ({
1330
- sessions: s.sessions.map(
1331
- (sx) => sx.id === sessionId ? { ...sx, messages: [...sx.messages, placeholder] } : sx
1332
- )
1333
- }));
1334
- setStreaming(sessionId, true);
1335
- const abortController = new AbortController();
1336
- abortRef.current = abortController;
1337
- let lastEventMs = Date.now();
1338
- const hangTimeout = setInterval(() => {
1339
- if (Date.now() - lastEventMs > 3e5) {
1340
- console.warn("[stream] hang timeout — aborting", sessionId);
1341
- abortController.abort();
1342
- }
1343
- }, 3e4);
1344
- try {
1345
- const stream = postChatStream({
1346
- input: text,
1347
- conversation: sessionId,
1348
- model,
1349
- signal: abortController.signal,
1350
- messages: history,
1351
- attachments: options?.attachments?.map((a) => ({
1352
- id: a.id,
1353
- name: a.name,
1354
- type: a.type,
1355
- size: a.size
1356
- }))
1357
- });
1358
- for await (const ev of stream) {
1359
- lastEventMs = Date.now();
1360
- switch (ev.type) {
1361
- case "token": {
1362
- const delta = ev.data.delta || "";
1363
- if (!delta) continue;
1364
- useChatStore.setState((s) => ({
1365
- sessions: s.sessions.map((sx) => {
1366
- if (sx.id !== sessionId) return sx;
1367
- const msgs = sx.messages.slice();
1368
- const last = msgs[msgs.length - 1];
1369
- if (last && last.role === "assistant" && last.isStreaming) {
1370
- msgs[msgs.length - 1] = { ...last, content: last.content + delta };
1371
- }
1372
- return { ...sx, messages: msgs };
1373
- })
1374
- }));
1375
- break;
1376
- }
1377
- case "reasoning": {
1378
- const text2 = ev.data.text || "";
1379
- if (!text2) continue;
1380
- useChatStore.setState((s) => ({
1381
- sessions: s.sessions.map((sx) => {
1382
- if (sx.id !== sessionId) return sx;
1383
- const msgs = sx.messages.slice();
1384
- const last = msgs[msgs.length - 1];
1385
- if (last && last.role === "assistant" && last.isStreaming) {
1386
- const prevReasoning = last.reasoning || "";
1387
- msgs[msgs.length - 1] = {
1388
- ...last,
1389
- reasoning: prevReasoning + text2,
1390
- reasoningStartedAt: last.reasoningStartedAt || Date.now()
1391
- };
1392
- }
1393
- return { ...sx, messages: msgs };
1394
- })
1395
- }));
1396
- break;
1397
- }
1398
- case "tool_start": {
1399
- const name = ev.data.name || "tool";
1400
- const preview = ev.data.preview || "";
1401
- const callId = ev.data.tool_call_id || void 0;
1402
- useChatStore.setState((s) => ({
1403
- sessions: s.sessions.map((sx) => {
1404
- if (sx.id !== sessionId) return sx;
1405
- const msgs = sx.messages.slice();
1406
- const last = msgs[msgs.length - 1];
1407
- if (last && last.role === "assistant" && last.isStreaming) {
1408
- const calls = [
1409
- ...last.toolCalls || [],
1410
- { name, preview, callId, status: "running" }
1411
- ];
1412
- msgs[msgs.length - 1] = { ...last, toolCalls: calls };
1413
- }
1414
- return { ...sx, messages: msgs };
1415
- })
1416
- }));
1417
- break;
1418
- }
1419
- case "tool_end": {
1420
- const callId = ev.data.tool_call_id;
1421
- const toolName = ev.data.tool;
1422
- const output = ev.data.output;
1423
- useChatStore.setState((s) => ({
1424
- sessions: s.sessions.map((sx) => {
1425
- if (sx.id !== sessionId) return sx;
1426
- const msgs = sx.messages.slice();
1427
- const last = msgs[msgs.length - 1];
1428
- if (!last?.toolCalls) return sx;
1429
- const calls = last.toolCalls.map((tc) => {
1430
- if (callId && tc.callId === callId) {
1431
- return { ...tc, status: "done", output: output || tc.output };
1432
- }
1433
- if (!callId && tc.name === toolName && tc.status === "running") {
1434
- return { ...tc, status: "done", output: output || tc.output };
1435
- }
1436
- return tc;
1437
- });
1438
- msgs[msgs.length - 1] = { ...last, toolCalls: calls };
1439
- return { ...sx, messages: msgs };
1440
- })
1441
- }));
1442
- break;
1443
- }
1444
- case "done": {
1445
- useChatStore.setState((s) => ({
1446
- sessions: s.sessions.map((sx) => {
1447
- if (sx.id !== sessionId) return sx;
1448
- const msgs = sx.messages.map(
1449
- (m) => m.role === "assistant" && m.isStreaming ? { ...m, isStreaming: false, reasoningEndedAt: m.reasoning ? Date.now() : void 0 } : m
1450
- );
1451
- return { ...sx, messages: msgs };
1452
- })
1453
- }));
1454
- setStreaming(sessionId, false);
1455
- break;
1456
- }
1457
- case "error": {
1458
- const err = ev.data.error;
1459
- useChatStore.setState((s) => ({
1460
- sessions: s.sessions.map((sx) => {
1461
- if (sx.id !== sessionId) return sx;
1462
- const msgs = sx.messages.map(
1463
- (m) => m.role === "assistant" && m.isStreaming ? { ...m, isStreaming: false, content: m.content || `**Error:** ${err || "Run failed"}` } : m
1464
- );
1465
- return { ...sx, messages: msgs };
1466
- })
1467
- }));
1468
- setStreaming(sessionId, false);
1469
- break;
1470
- }
1471
- case "approval": {
1472
- useChatStore.setState((s) => ({
1473
- sessions: s.sessions.map(
1474
- (sx) => sx.id === sessionId ? { ...sx, pendingApproval: ev.data } : sx
1475
- )
1476
- }));
1477
- break;
1478
- }
1479
- case "title": {
1480
- const newTitle = ev.data.title;
1481
- if (newTitle) {
1482
- useChatStore.setState((s) => ({
1483
- sessions: s.sessions.map(
1484
- (sx) => sx.id === sessionId ? { ...sx, title: newTitle } : sx
1485
- )
1486
- }));
1487
- }
1488
- break;
1489
- }
1490
- }
1491
- }
1492
- } catch (err) {
1493
- const errMsg = err instanceof Error ? err.message : "Connection error";
1494
- useChatStore.setState((s) => ({
1495
- sessions: s.sessions.map((sx) => {
1496
- if (sx.id !== sessionId) return sx;
1497
- const msgs = sx.messages.map(
1498
- (m) => m.role === "assistant" && m.isStreaming ? { ...m, isStreaming: false, content: m.content || `**Error:** ${errMsg}` } : m
1499
- );
1500
- return { ...sx, messages: msgs };
1501
- })
1502
- }));
1503
- setStreaming(sessionId, false);
1504
- } finally {
1505
- clearInterval(hangTimeout);
1506
- abortRef.current = null;
1507
- setStreaming(sessionId, false);
1508
- useChatStore.setState((s) => ({
1509
- sessions: s.sessions.map((sx) => {
1510
- if (sx.id !== sessionId) return sx;
1511
- const hasStreaming = sx.messages.some((m) => m.role === "assistant" && m.isStreaming);
1512
- if (!hasStreaming) return sx;
1513
- return {
1514
- ...sx,
1515
- messages: sx.messages.map(
1516
- (m) => m.role === "assistant" && m.isStreaming ? { ...m, isStreaming: false } : m
1517
- )
1518
- };
1519
- })
1520
- }));
1521
- }
1522
- },
1523
- [setStreaming]
1524
- );
1525
- return { sendMessage, abortChat };
1526
- }
1527
- function useChatActions({
1528
- sessionId,
1529
- model,
1530
- isLoading,
1531
- sendMessage,
1532
- abortChat
1533
- }) {
1534
- const navigate = useNavigate();
1535
- const isLoadingRef = reactExports.useRef(isLoading);
1536
- isLoadingRef.current = isLoading;
1537
- const handleSend = reactExports.useCallback(
1538
- async (text, modelId, attachments) => {
1539
- const trimmed = text.trim();
1540
- if (!trimmed || isLoadingRef.current) return;
1541
- if (isLocalCommand(trimmed)) {
1542
- const cmd = trimmed.split(/\s+/)[0].toLowerCase();
1543
- if (cmd === "/new") {
1544
- const id = await createSession({ model });
1545
- navigate({ to: "/chat/$sessionId", params: { sessionId: id.id } });
1546
- return;
1547
- }
1548
- if (cmd === "/clear") {
1549
- useChatStore.setState((s) => ({
1550
- sessions: s.sessions.map(
1551
- (sx) => sx.id === sessionId ? { ...sx, messages: [] } : sx
1552
- )
1553
- }));
1554
- return;
1555
- }
1556
- if (cmd === "/help") {
1557
- const lines = SLASH_COMMANDS.map(
1558
- (c) => `\`${c.name}\` — ${c.description}`
1559
- ).join("\n");
1560
- useChatStore.setState((s) => ({
1561
- sessions: s.sessions.map(
1562
- (sx) => sx.id === sessionId ? {
1563
- ...sx,
1564
- messages: [
1565
- ...sx.messages,
1566
- {
1567
- id: `agent-${Date.now()}`,
1568
- role: "assistant",
1569
- content: `**Available Commands**
1570
-
1571
- ${lines}`,
1572
- timestamp: Date.now()
1573
- }
1574
- ]
1575
- } : sx
1576
- )
1577
- }));
1578
- return;
1579
- }
1580
- useChatStore.setState((s) => ({
1581
- sessions: s.sessions.map(
1582
- (sx) => sx.id === sessionId ? {
1583
- ...sx,
1584
- messages: [
1585
- ...sx.messages,
1586
- {
1587
- id: `agent-${Date.now()}`,
1588
- role: "assistant",
1589
- content: `Command \`${cmd}\` is not yet implemented in the web UI.`,
1590
- timestamp: Date.now()
1591
- }
1592
- ]
1593
- } : sx
1594
- )
1595
- }));
1596
- return;
1597
- }
1598
- await sendMessage(trimmed, sessionId, modelId || model, { attachments });
1599
- },
1600
- [sessionId, model, sendMessage, navigate]
1601
- );
1602
- const handleAbort = reactExports.useCallback(() => {
1603
- abortChat();
1604
- }, [abortChat]);
1605
- const handleApprove = reactExports.useCallback(async () => {
1606
- if (isLoadingRef.current) return;
1607
- await sendMessage("/approve", sessionId, model);
1608
- }, [sessionId, model, sendMessage]);
1609
- const handleDeny = reactExports.useCallback(async () => {
1610
- if (isLoadingRef.current) return;
1611
- await sendMessage("/deny", sessionId, model);
1612
- }, [sessionId, model, sendMessage]);
1613
- const handleNewChat = reactExports.useCallback(async () => {
1614
- const id = await createSession({ model });
1615
- navigate({ to: "/chat/$sessionId", params: { sessionId: id.id } });
1616
- }, [model, navigate]);
1617
- const handleClear = reactExports.useCallback(() => {
1618
- if (isLoadingRef.current) {
1619
- abortChat();
1620
- }
1621
- useChatStore.setState((s) => ({
1622
- sessions: s.sessions.map(
1623
- (sx) => sx.id === sessionId ? { ...sx, messages: [] } : sx
1624
- )
1625
- }));
1626
- }, [sessionId, abortChat]);
1627
- return { handleSend, handleAbort, handleApprove, handleDeny, handleNewChat, handleClear };
1628
- }
1629
- function ApprovalRequest({
1630
- sessionId,
1631
- request,
1632
- onResolved
1633
- }) {
1634
- const [responding, setResponding] = reactExports.useState(false);
1635
- const handleChoice = async (choice) => {
1636
- setResponding(true);
1637
- try {
1638
- await respondApproval({ session_id: sessionId, choice });
1639
- onResolved();
1640
- } catch {
1641
- } finally {
1642
- setResponding(false);
1643
- }
1644
- };
1645
- const toolName = request.tool_name || "unknown tool";
1646
- const command = typeof request.command === "string" ? request.command : typeof request.arguments === "string" ? request.arguments : null;
1647
- const description = typeof request.description === "string" ? request.description : typeof request.preview === "string" ? request.preview : null;
1648
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-auto max-w-3xl px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4", children: [
1649
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
1650
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconShieldCheck, { className: "h-4 w-4 text-amber-500" }),
1651
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-amber-600 dark:text-amber-400", children: "Approval Required" })
1652
- ] }),
1653
- /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-sm text-foreground mb-1", children: [
1654
- "The agent wants to use ",
1655
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium", children: toolName })
1656
- ] }),
1657
- command && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 rounded-lg bg-muted/60 border border-border px-3 py-2 text-xs font-mono text-muted-foreground whitespace-pre-wrap break-all max-h-32 overflow-y-auto", children: command }),
1658
- description && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-2 text-xs text-muted-foreground leading-relaxed", children: description }),
1659
- request.risk === "high" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 flex items-center gap-1.5 text-xs text-red-500", children: [
1660
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconAlertTriangle, { className: "h-3.5 w-3.5" }),
1661
- "High risk operation"
1662
- ] }),
1663
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 flex flex-wrap gap-2", children: [
1664
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
1665
- Button,
1666
- {
1667
- size: "sm",
1668
- variant: "default",
1669
- className: "gap-1.5 bg-emerald-600 hover:bg-emerald-700 text-white",
1670
- disabled: responding,
1671
- onClick: () => handleChoice("once"),
1672
- children: [
1673
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconCheck, { className: "h-3.5 w-3.5" }),
1674
- "Approve once"
1675
- ]
1676
- }
1677
- ),
1678
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
1679
- Button,
1680
- {
1681
- size: "sm",
1682
- variant: "secondary",
1683
- className: "gap-1.5",
1684
- disabled: responding,
1685
- onClick: () => handleChoice("session"),
1686
- children: [
1687
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconCheck, { className: "h-3.5 w-3.5" }),
1688
- "Approve all"
1689
- ]
1690
- }
1691
- ),
1692
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
1693
- Button,
1694
- {
1695
- size: "sm",
1696
- variant: "outline",
1697
- className: "gap-1.5",
1698
- disabled: responding,
1699
- onClick: () => handleChoice("always"),
1700
- children: [
1701
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconShieldCheck, { className: "h-3.5 w-3.5" }),
1702
- "Always allow"
1703
- ]
1704
- }
1705
- ),
1706
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
1707
- Button,
1708
- {
1709
- size: "sm",
1710
- variant: "ghost",
1711
- className: "gap-1.5 text-destructive hover:text-destructive",
1712
- disabled: responding,
1713
- onClick: () => handleChoice("deny"),
1714
- children: [
1715
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconX, { className: "h-3.5 w-3.5" }),
1716
- "Deny"
1717
- ]
1718
- }
1719
- )
1720
- ] })
1721
- ] }) });
1722
- }
1723
- function Chat({
1724
- sessionId,
1725
- messages,
1726
- title,
1727
- model,
1728
- pendingApproval
1729
- }) {
1730
- const chatInputRef = reactExports.useRef(null);
1731
- const pendingSentRef = reactExports.useRef(false);
1732
- const { containerRef, bottomRef, onScroll } = useChatScroll(messages);
1733
- const { sendMessage, abortChat } = useChatStream();
1734
- const isLoading = useChatStore((s) => s.streamingSessions.has(sessionId));
1735
- const actions = useChatActions({
1736
- sessionId,
1737
- model,
1738
- isLoading,
1739
- sendMessage,
1740
- abortChat
1741
- });
1742
- reactExports.useEffect(() => {
1743
- if (!sessionId || pendingSentRef.current) return;
1744
- const msgKey = `hermium_pending_msg_${sessionId}`;
1745
- const modelKey = `hermium_pending_model_${sessionId}`;
1746
- const pending = sessionStorage.getItem(msgKey);
1747
- if (!pending) return;
1748
- pendingSentRef.current = true;
1749
- const pendingModel = sessionStorage.getItem(modelKey) || model;
1750
- sessionStorage.removeItem(msgKey);
1751
- sessionStorage.removeItem(modelKey);
1752
- sendMessage(pending, sessionId, pendingModel);
1753
- }, [sessionId, model, sendMessage]);
1754
- reactExports.useEffect(() => {
1755
- function onKey(e) {
1756
- if ((e.metaKey || e.ctrlKey) && e.key === "n") {
1757
- e.preventDefault();
1758
- actions.handleNewChat();
1759
- }
1760
- }
1761
- window.addEventListener("keydown", onKey);
1762
- return () => window.removeEventListener("keydown", onKey);
1763
- }, [actions]);
1764
- const handleSuggestion = reactExports.useCallback((text) => {
1765
- chatInputRef.current?.setText(text);
1766
- }, []);
1767
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [
1768
- /* @__PURE__ */ jsxRuntimeExports.jsx(
1769
- ChatHeader,
1770
- {
1771
- sessionId,
1772
- title,
1773
- hasMessages: messages.length > 0,
1774
- onNewChat: actions.handleNewChat,
1775
- onClear: actions.handleClear
1776
- }
1777
- ),
1778
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
1779
- "div",
1780
- {
1781
- className: "flex-1 min-w-0 overflow-y-auto",
1782
- ref: containerRef,
1783
- onScroll,
1784
- children: [
1785
- messages.length === 0 && !isLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChatEmptyState, { onSelectSuggestion: handleSuggestion }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-auto max-w-3xl", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1786
- MessageList,
1787
- {
1788
- messages,
1789
- isLoading,
1790
- onApprove: actions.handleApprove,
1791
- onDeny: actions.handleDeny
1792
- }
1793
- ) }),
1794
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: bottomRef }),
1795
- pendingApproval && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-auto max-w-3xl", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1796
- ApprovalRequest,
1797
- {
1798
- sessionId,
1799
- request: pendingApproval,
1800
- onResolved: () => {
1801
- useChatStore.getState().updateSession(sessionId, {
1802
- pendingApproval: null
1803
- });
1804
- }
1805
- }
1806
- ) })
1807
- ]
1808
- }
1809
- ),
1810
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 px-4 py-3 border-t bg-card/50 backdrop-blur-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1811
- ChatInput,
1812
- {
1813
- ref: chatInputRef,
1814
- sessionId,
1815
- model,
1816
- isLoading,
1817
- onSubmit: actions.handleSend,
1818
- onAbort: actions.handleAbort
1819
- }
1820
- ) })
1821
- ] });
1822
- }
1823
- function SessionChatPage() {
1824
- const {
1825
- sessionId
1826
- } = Route.useParams();
1827
- const switchSession = useChatStore((s) => s.switchSession);
1828
- const loadSessions = useChatStore((s) => s.loadSessions);
1829
- const loadSessionMessages = useChatStore((s) => s.loadSessionMessages);
1830
- const sessionsLoaded = useChatStore((s) => s.sessionsLoaded);
1831
- const isLoadingMessages = useChatStore((s) => s.isLoadingMessages);
1832
- const sessionMeta = useChatStore(useShallow((s) => {
1833
- const sess = s.sessions.find((sx) => sx.id === sessionId);
1834
- return {
1835
- title: sess?.title,
1836
- model: sess?.model || "",
1837
- pendingApproval: sess?.pendingApproval ?? null
1838
- };
1839
- }));
1840
- const messages = useChatStore(useShallow((s) => {
1841
- const sess = s.sessions.find((sx) => sx.id === sessionId);
1842
- return sess?.messages ?? [];
1843
- }));
1844
- reactExports.useEffect(() => {
1845
- if (!sessionsLoaded) {
1846
- loadSessions();
1847
- }
1848
- }, [sessionsLoaded, loadSessions]);
1849
- reactExports.useEffect(() => {
1850
- if (!sessionId) return;
1851
- switchSession(sessionId);
1852
- const sess = useChatStore.getState().getSession(sessionId);
1853
- if (!sess || sess.messages.length === 0) {
1854
- loadSessionMessages(sessionId);
1855
- }
1856
- }, [sessionId, switchSession, loadSessionMessages]);
1857
- if (isLoadingMessages && messages.length === 0) {
1858
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-1 flex-col h-full overflow-hidden", children: [
1859
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 border-b px-4 py-2.5 flex items-center gap-2 min-w-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-muted-foreground truncate flex-1", children: "Loading…" }) }),
1860
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-w-0 overflow-y-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-4 p-4 mx-auto max-w-3xl", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "animate-pulse space-y-2", children: [
1861
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `h-4 rounded bg-muted ${i % 2 === 0 ? "w-2/3" : "w-1/2 ml-auto"}` }),
1862
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `h-3 rounded bg-muted ${i % 2 === 0 ? "w-1/2" : "w-1/3 ml-auto"}` })
1863
- ] }, i)) }) })
1864
- ] });
1865
- }
1866
- return /* @__PURE__ */ jsxRuntimeExports.jsx(Chat, { sessionId, messages, title: sessionMeta.title, model: sessionMeta.model, pendingApproval: sessionMeta.pendingApproval });
1867
- }
1868
- export {
1869
- SessionChatPage as component
1870
- };