@rexvane/piweb 0.3.24 → 0.3.26

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 (285) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +11 -11
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +5 -5
  5. package/.next/react-loadable-manifest.json +13 -20
  6. package/.next/required-server-files.js +2 -1
  7. package/.next/required-server-files.json +2 -1
  8. package/.next/routes-manifest.json +6 -8
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/resources/route.js +1 -0
  32. package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
  33. package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
  34. package/.next/server/app/api/config/route.js +1 -1
  35. package/.next/server/app/api/config/route.js.nft.json +1 -1
  36. package/.next/server/app/api/files/route.js +1 -1
  37. package/.next/server/app/api/files/route.js.nft.json +1 -1
  38. package/.next/server/app/api/fs/route.js +1 -1
  39. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  40. package/.next/server/app/api/git/route.js +4 -4
  41. package/.next/server/app/api/git/route.js.nft.json +1 -1
  42. package/.next/server/app/api/github/route.js +4 -4
  43. package/.next/server/app/api/github/route.js.nft.json +1 -1
  44. package/.next/server/app/api/growth/route.js +1 -1
  45. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  46. package/.next/server/app/api/health/route.js +1 -1
  47. package/.next/server/app/api/models/defaults/route.js +1 -1
  48. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
  49. package/.next/server/app/api/models/route.js +1 -1
  50. package/.next/server/app/api/models/route.js.nft.json +1 -1
  51. package/.next/server/app/api/pi-settings/route.js +1 -1
  52. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  53. package/.next/server/app/api/plugins/route.js +1 -1
  54. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  55. package/.next/server/app/api/prompts/route.js +1 -1
  56. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  57. package/.next/server/app/api/remote/route.js +4 -3
  58. package/.next/server/app/api/security/route.js +1 -1
  59. package/.next/server/app/api/security/route.js.nft.json +1 -1
  60. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  61. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  63. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sessions/import/route.js +2 -2
  65. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  66. package/.next/server/app/api/sessions/route.js +1 -1
  67. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  68. package/.next/server/app/api/skills/route.js +2 -2
  69. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  70. package/.next/server/app/api/update/route.js +2 -2
  71. package/.next/server/app/api/update/route.js.nft.json +1 -1
  72. package/.next/server/app/api/version/route.js +1 -1
  73. package/.next/server/app/api/version/route.js.nft.json +1 -1
  74. package/.next/server/app/api/web-auth/route.js +2 -2
  75. package/.next/server/app/api/workspaces/route.js +1 -1
  76. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  77. package/.next/server/app/icon.svg/route.js +1 -1
  78. package/.next/server/app/index.html +1 -1
  79. package/.next/server/app/index.rsc +6 -7
  80. package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -7
  81. package/.next/server/app/index.segments/_full.segment.rsc +6 -7
  82. package/.next/server/app/index.segments/_tree.segment.rsc +2 -3
  83. package/.next/server/app/login/page.js +1 -1
  84. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/login.html +1 -1
  86. package/.next/server/app/login.rsc +3 -3
  87. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  88. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  89. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
  90. package/.next/server/app/page.js +1 -1
  91. package/.next/server/app/page.js.nft.json +1 -1
  92. package/.next/server/app/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app-paths-manifest.json +11 -11
  94. package/.next/server/chunks/3924.js +3 -3
  95. package/.next/server/chunks/538.js +1 -1
  96. package/.next/server/chunks/620.js +1 -0
  97. package/.next/server/chunks/6229.js +1 -1
  98. package/.next/server/chunks/7496.js +1 -1
  99. package/.next/server/chunks/7662.js +2 -2
  100. package/.next/server/chunks/8686.js +1 -1
  101. package/.next/server/functions-config-manifest.json +1 -2
  102. package/.next/server/instrumentation.js.nft.json +1 -1
  103. package/.next/server/middleware-build-manifest.js +1 -1
  104. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  105. package/.next/server/pages/404.html +1 -1
  106. package/.next/server/pages/500.html +1 -1
  107. package/.next/server/preview-props.json +3 -3
  108. package/.next/server/server-reference-manifest.json +1 -1
  109. package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
  110. package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
  111. package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
  112. package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
  113. package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
  114. package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
  115. package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
  116. package/.next/static/chunks/{618.949981da99be7182.js → 618.9a22656b863f242b.js} +2 -2
  117. package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
  118. package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
  119. package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
  120. package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
  121. package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
  122. package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
  123. package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
  124. package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
  125. package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
  126. package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
  127. package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
  128. package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
  129. package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
  130. package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
  131. package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
  132. package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
  133. package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
  134. package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
  135. package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
  136. package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
  137. package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
  138. package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
  139. package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
  140. package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
  141. package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
  142. package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
  143. package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
  144. package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
  145. package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
  146. package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
  147. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
  148. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
  149. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
  150. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
  151. package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
  152. package/.next/static/css/014eb1f21414e615.css +1 -0
  153. package/.next/static/css/56b129a13637fcfd.css +1 -0
  154. package/.next/static/css/7e3caf797e4cb19a.css +1 -0
  155. package/.next/types/app/api/agent/{[id]/side-chat → resources}/route.ts +3 -3
  156. package/.next/types/routes.d.ts +2 -2
  157. package/.next/types/validator.ts +9 -9
  158. package/README.md +1 -1
  159. package/next.config.ts +1 -1
  160. package/package.json +9 -9
  161. package/scripts/apply-global.mjs +87 -0
  162. package/scripts/launcher.mjs +21 -0
  163. package/scripts/testing/prepare-sdk-tools.mjs +19 -0
  164. package/src/app/api/agent/new/route.ts +2 -1
  165. package/src/app/api/agent/resources/route.ts +18 -0
  166. package/src/app/api/models/route.ts +22 -1
  167. package/src/app/api/plugins/route.ts +3 -2
  168. package/src/app/api/prompts/route.ts +3 -2
  169. package/src/app/api/remote/route.ts +22 -4
  170. package/src/app/api/sessions/[id]/export/route.ts +3 -1
  171. package/src/app/api/skills/route.ts +3 -2
  172. package/src/app/globals.css +1002 -1765
  173. package/src/components/AppShell.tsx +128 -93
  174. package/src/components/ChatInput.tsx +177 -98
  175. package/src/components/ChatWindow.tsx +189 -56
  176. package/src/components/ContextMeter.tsx +33 -9
  177. package/src/components/CustomSelect.module.css +142 -0
  178. package/src/components/CustomSelect.tsx +157 -0
  179. package/src/components/DrawerPanel.tsx +1 -1
  180. package/src/components/ExtensionUI.tsx +9 -3
  181. package/src/components/FileViewer.tsx +24 -21
  182. package/src/components/GitPanel.tsx +35 -13
  183. package/src/components/GrowthTree.tsx +6 -2
  184. package/src/components/ImageViewer.tsx +72 -0
  185. package/src/components/MarkdownBlock.tsx +67 -96
  186. package/src/components/ModelSelector.tsx +36 -39
  187. package/src/components/PiMark.tsx +76 -41
  188. package/src/components/PreviewImage.tsx +33 -0
  189. package/src/components/ProjectPanel.tsx +8 -5
  190. package/src/components/PromptPanel.tsx +85 -20
  191. package/src/components/ProviderSetupModal.module.css +12 -11
  192. package/src/components/ProviderSetupModal.tsx +107 -36
  193. package/src/components/RemoteSection.tsx +53 -2
  194. package/src/components/SessionImportSection.tsx +30 -15
  195. package/src/components/SessionSidebar.tsx +72 -62
  196. package/src/components/SettingsPanel.tsx +49 -151
  197. package/src/components/TrajectoryAnalyzer.module.css +26 -11
  198. package/src/components/TrajectoryView.tsx +11 -3
  199. package/src/components/chat/markdown-actions.tsx +23 -0
  200. package/src/components/composer/ComposerFiles.tsx +224 -0
  201. package/src/components/composer/ComposerImages.tsx +59 -127
  202. package/src/hooks/usePiWeb.ts +47 -2
  203. package/src/i18n.tsx +62 -80
  204. package/src/lib/agent-manager.ts +365 -33
  205. package/src/lib/chat/file-path.ts +54 -0
  206. package/src/lib/chat/skill-expansion.ts +51 -0
  207. package/src/lib/extension-ui.ts +25 -1
  208. package/src/lib/font.ts +37 -39
  209. package/src/lib/git-service.ts +4 -2
  210. package/src/lib/growth-turns.ts +3 -1
  211. package/src/lib/model-draft.ts +69 -2
  212. package/src/lib/models-service.ts +53 -3
  213. package/src/lib/pi.ts +2 -2
  214. package/src/lib/remote/assets.json +1 -1
  215. package/src/lib/remote/supervisor.ts +48 -5
  216. package/src/lib/security-service.ts +2 -1
  217. package/src/lib/session-reader.ts +25 -13
  218. package/src/lib/session-retention.ts +11 -8
  219. package/src/lib/skills-service.ts +2 -1
  220. package/src/lib/slash-command-token.ts +14 -0
  221. package/src/lib/trajectory.ts +111 -15
  222. package/src/lib/types.ts +28 -3
  223. package/src/lib/update-service.ts +29 -9
  224. package/src/lib/workspace-store.ts +36 -1
  225. package/.next/server/app/api/agent/[id]/side-chat/route.js +0 -5
  226. package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +0 -1
  227. package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +0 -1
  228. package/.next/server/chunks/3421.js +0 -1
  229. package/.next/static/chunks/2716.44fdf1b21454b961.js +0 -1
  230. package/.next/static/chunks/4673.e261707dce391be7.js +0 -1
  231. package/.next/static/chunks/4686-0cd87513f9ea3538.js +0 -1
  232. package/.next/static/chunks/4778.eddd3a5813f85824.js +0 -1
  233. package/.next/static/chunks/7411.5f79671208209b13.js +0 -1
  234. package/.next/static/chunks/8552.25c39ef4c995eba0.js +0 -1
  235. package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +0 -1
  236. package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +0 -1
  237. package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +0 -1
  238. package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +0 -1
  239. package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +0 -1
  240. package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +0 -1
  241. package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +0 -1
  242. package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +0 -1
  243. package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +0 -1
  244. package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +0 -1
  245. package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +0 -1
  246. package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +0 -1
  247. package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +0 -1
  248. package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +0 -1
  249. package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +0 -1
  250. package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +0 -1
  251. package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +0 -1
  252. package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +0 -1
  253. package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +0 -1
  254. package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +0 -1
  255. package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +0 -1
  256. package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +0 -1
  257. package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +0 -1
  258. package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +0 -1
  259. package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +0 -1
  260. package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +0 -1
  261. package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +0 -1
  262. package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +0 -1
  263. package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +0 -1
  264. package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +0 -1
  265. package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +0 -1
  266. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +0 -1
  267. package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +0 -1
  268. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +0 -1
  269. package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +0 -1
  270. package/.next/static/css/08815507594e8b8d.css +0 -1
  271. package/.next/static/css/9fc559c2c4ad1d0f.css +0 -1
  272. package/.next/static/css/c6e2227c36ec2fd6.css +0 -1
  273. package/.next/static/css/f15a853af8b5127f.css +0 -1
  274. package/.next/static/lvKCJmjFj8LuqSojRVLRF/_buildManifest.js +0 -1
  275. package/src/app/api/agent/[id]/side-chat/route.ts +0 -77
  276. package/src/components/side-chat/SideChatPanel.module.css +0 -131
  277. package/src/components/side-chat/SideChatPanel.tsx +0 -189
  278. package/src/components/side-chat/icons.tsx +0 -8
  279. package/src/components/side-chat/request-history.ts +0 -29
  280. package/src/components/side-chat/store.ts +0 -248
  281. package/src/lib/chat/stream-settings.ts +0 -192
  282. package/src/lib/side-chat/context.ts +0 -123
  283. package/src/lib/side-chat/service.ts +0 -191
  284. package/src/lib/side-chat/types.ts +0 -61
  285. /package/.next/static/{lvKCJmjFj8LuqSojRVLRF → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
@@ -9,6 +9,7 @@
9
9
  import { useEffect, useMemo, useRef, useState } from "react";
10
10
  import {
11
11
  IconAtOutline16,
12
+ IconCloseOutline14,
12
13
  IconFileOutline16,
13
14
  IconFolderClose16,
14
15
  IconSendArrowUp14,
@@ -17,12 +18,14 @@ import {
17
18
  } from "@/components/icons";
18
19
  import { ContextMeter } from "@/components/ContextMeter";
19
20
  import { COLLAPSE_AT, ComposerImages } from "@/components/composer/ComposerImages";
21
+ import { ComposerFiles } from "@/components/composer/ComposerFiles";
20
22
  import { ModelSelector, type ModelChoice } from "@/components/ModelSelector";
21
23
  import { useI18n } from "@/i18n";
22
24
  import { useWorkspaceFiles } from "@/hooks/useWorkspaceFiles";
23
25
  import { fmtBytes } from "@/lib/format";
24
26
  import { otherBehavior, useEnterBehavior } from "@/lib/enter-behavior";
25
27
  import { atToken, candidateText, suggestPaths, type PathCandidate } from "@/lib/workspace-index";
28
+ import { slashCommandToken } from "@/lib/slash-command-token";
26
29
  import type { ImageAttachment } from "@/lib/types";
27
30
 
28
31
  export interface SlashCommand {
@@ -86,6 +89,7 @@ export function ChatInput({
86
89
  pendingUploadCount = 0,
87
90
  pendingSend = false,
88
91
  onSendPendingChange,
92
+ attachmentLayout = "overlay",
89
93
  }: {
90
94
  disabled?: boolean;
91
95
  isStreaming: boolean;
@@ -131,6 +135,8 @@ export function ChatInput({
131
135
  pendingUploadCount?: number;
132
136
  pendingSend?: boolean;
133
137
  onSendPendingChange?: (pending: boolean) => void;
138
+ /** Hero 在输入卡上方预留空间;聊天中仍让附件悬浮、不占布局高度 */
139
+ attachmentLayout?: "overlay" | "flow";
134
140
  }) {
135
141
  const [localDraft, setLocalDraft] = useState<ChatDraft>(initialDraft ?? EMPTY_CHAT_DRAFT);
136
142
  const currentDraft = draft ?? localDraft;
@@ -159,12 +165,11 @@ export function ChatInput({
159
165
  const [dragActive, setDragActive] = useState(false);
160
166
  /** 引用 chip 超过 COLLAPSE_AT 就先折叠:手机上折行会把输入框顶掉(图片的折叠在 ComposerImages 里) */
161
167
  const [refsExpanded, setRefsExpanded] = useState(false);
162
- /** 上传文件超过 4 个时的展开状态(与引用同一套折叠规则) */
163
- const [uploadsExpanded, setUploadsExpanded] = useState(false);
164
168
  const dragDepth = useRef(0);
165
169
  const [modelOpen, setModelOpen] = useState(false);
166
170
  const [cmdIdx, setCmdIdx] = useState(0);
167
171
  const [cmdDismissed, setCmdDismissed] = useState(false);
172
+ const [commandCaret, setCommandCaret] = useState<number | null>(currentDraft.text.length);
168
173
  // @ 引用:打字触发候选,选中后落成引用 chip(发送时统一拼成 `@路径` 行)
169
174
  const [mention, setMention] = useState<{ query: string; start: number; tokenEnd: number } | null>(null);
170
175
  const [mentionIdx, setMentionIdx] = useState(0);
@@ -190,11 +195,11 @@ export function ChatInput({
190
195
  autoSize();
191
196
  }, [text]);
192
197
 
193
- // 斜杠命令模式:文本以 / 开头且未输入参数空格
194
- const slash = text.startsWith("/") ? text.slice(1) : "";
195
- const hasArgs = slash.includes(" ");
196
- const typedCommandMode = !!commands?.length && text.startsWith("/") && !cmdDismissed && !hasArgs;
197
- const filteredCommands = (commands ?? []).filter((c) => c.name.toLowerCase().startsWith(slash.split(" ")[0].toLowerCase()));
198
+ // 补全光标处的 token,而不是只看草稿的第一个字符
199
+ const commandToken = slashCommandToken(text, commandCaret);
200
+ const slash = commandToken?.query ?? "";
201
+ const typedCommandMode = !adopted && !!commands?.length && !!commandToken && !cmdDismissed;
202
+ const filteredCommands = (commands ?? []).filter((c) => c.name.toLowerCase().startsWith(slash.toLowerCase()));
198
203
  const visibleCommands = filteredCommands;
199
204
  const commandMode = typedCommandMode;
200
205
 
@@ -278,7 +283,67 @@ export function ChatInput({
278
283
  }, [cmdIdx, visibleCommands.length]);
279
284
  const effectiveIdx = Math.min(cmdIdx, Math.max(0, visibleCommands.length - 1));
280
285
 
286
+ const withoutCommandToken = () => commandToken ? text.slice(0, commandToken.start) + text.slice(commandToken.tokenEnd) : text;
287
+ const adoptCommand = (entry: SlashCommand) => {
288
+ // SDK 只展开开头的一个命令:已经采纳时不再覆盖(先 Backspace 摘掉)
289
+ if (draftRef.current.adopted) return;
290
+ const remaining = withoutCommandToken();
291
+ const caret = commandToken?.start ?? remaining.length;
292
+ setAdopted(entry);
293
+ setText(remaining);
294
+ setCommandCaret(caret);
295
+ setCmdDismissed(true);
296
+ setCmdIdx(0);
297
+ requestAnimationFrame(() => {
298
+ autoSize();
299
+ taRef.current?.focus();
300
+ taRef.current?.setSelectionRange(caret, caret);
301
+ });
302
+ };
303
+
304
+ // 行内扩展命令(如 /fast)自己执行:不能为了切换它把周围的草稿、图片或引用一起发出去
305
+ const runInlineExtension = async (entry: SlashCommand) => {
306
+ if (disabled || isBlocked || sendingRef.current || pendingSend || uploadBusy) return;
307
+ const original = text;
308
+ const remaining = withoutCommandToken();
309
+ sendingRef.current = true;
310
+ setSending(true);
311
+ onSendPendingChange?.(true);
312
+ setText(remaining);
313
+ setCommandCaret(remaining.length);
314
+ setCmdDismissed(true);
315
+ try {
316
+ const result = await onSend(`/${entry.name}`, []);
317
+ if (!result?.success) throw new Error(result?.error || "Command failed");
318
+ } catch (error) {
319
+ setText((previous) => previous === remaining ? original : previous);
320
+ if (mountedRef.current) {
321
+ setCommandCaret(commandToken?.tokenEnd ?? original.length);
322
+ setCmdDismissed(false);
323
+ setAttachmentError(error instanceof Error ? error.message : String(error));
324
+ }
325
+ } finally {
326
+ sendingRef.current = false;
327
+ if (mountedRef.current) setSending(false);
328
+ onSendPendingChange?.(false);
329
+ }
330
+ };
331
+
281
332
  const runEntry = (entry: SlashCommand) => {
333
+ if (draftRef.current.adopted) return;
334
+ // 命令出现在正文中间:只替换这一段 token,前后文字原样保留
335
+ if (commandToken && (commandToken.start > 0 || commandToken.tokenEnd < text.length)) {
336
+ if (entry.kind === "builtin") {
337
+ if (entry.name === "model") setModelOpen(true);
338
+ else onCommand?.(entry.name, "");
339
+ const remaining = withoutCommandToken();
340
+ setText(remaining);
341
+ setCommandCaret(remaining.length);
342
+ setCmdDismissed(true);
343
+ } else if (entry.kind === "extension") void runInlineExtension(entry);
344
+ else adoptCommand(entry);
345
+ return;
346
+ }
282
347
  const args = slash.slice(entry.name.length).trim();
283
348
  if (entry.kind === "builtin" && entry.name === "model") {
284
349
  setModelOpen(true);
@@ -506,28 +571,50 @@ export function ChatInput({
506
571
  // eslint-disable-next-line react-hooks/exhaustive-deps
507
572
  }, []);
508
573
 
509
- const hasDraft = text.trim().length > 0 || images.length > 0 || uploads.length > 0 || references.length > 0;
574
+ const hasDraft = adopted !== null || text.trim().length > 0 || images.length > 0 || uploads.length > 0 || references.length > 0;
510
575
 
511
576
  return (
512
577
  <div ref={wrapRef} className="relative w-full" data-testid="composer">
513
- {/* 排队中的消息(Claude Code 同款:列在输入卡上方;↑ 取回编辑,或直接丢弃) */}
578
+ {/* 排队中的消息(高质感浮动拟态胶囊 dock:左侧消息与彩色药丸徽标,右侧快捷键微胶囊按钮) */}
514
579
  {queuedCount > 0 && (
515
- <div className="pw-queue">
516
- {queue.steering.map((q, i) => (
517
- <div key={`s${i}`} className="pw-queue-row" title={q}>
518
- <span className="pw-queue-tag">{t.modeSteer}</span>
519
- <span className="pw-queue-text">{q}</span>
520
- </div>
521
- ))}
522
- {queue.followUp.map((q, i) => (
523
- <div key={`f${i}`} className="pw-queue-row" title={q}>
524
- <span className="pw-queue-tag">{t.modeQueue}</span>
525
- <span className="pw-queue-text">{q}</span>
526
- </div>
527
- ))}
580
+ <div className="pw-queue" role="status" aria-live="polite">
581
+ <div className="pw-queue-list">
582
+ {queue.steering.map((q, i) => (
583
+ <div key={`s${i}`} className="pw-queue-row" title={q}>
584
+ <span className="pw-queue-tag pw-queue-tag-steer">
585
+ <span className="pw-queue-dot" />
586
+ {t.modeSteer}
587
+ </span>
588
+ <span className="pw-queue-text">{q}</span>
589
+ </div>
590
+ ))}
591
+ {queue.followUp.map((q, i) => (
592
+ <div key={`f${i}`} className="pw-queue-row" title={q}>
593
+ <span className="pw-queue-tag pw-queue-tag-queue">
594
+ <span className="pw-queue-dot" />
595
+ {t.modeQueue}
596
+ </span>
597
+ <span className="pw-queue-text">{q}</span>
598
+ </div>
599
+ ))}
600
+ </div>
528
601
  <div className="pw-queue-foot">
529
- <button type="button" onClick={() => void pullQueue()}>{t.queueEdit}</button>
530
- <button type="button" onClick={() => void onClearQueue?.()}>{t.queueDrop}</button>
602
+ <button
603
+ type="button"
604
+ className="pw-queue-btn pw-queue-btn-pull"
605
+ onClick={() => void pullQueue()}
606
+ title="↑"
607
+ >
608
+ <span className="pw-queue-kbd">↑</span>
609
+ <span>{t.queueEdit.replace(/^[↑\s]+/, "") || t.queueEdit}</span>
610
+ </button>
611
+ <button
612
+ type="button"
613
+ className="pw-queue-btn pw-queue-btn-drop"
614
+ onClick={() => void onClearQueue?.()}
615
+ >
616
+ {t.queueDrop}
617
+ </button>
531
618
  </div>
532
619
  </div>
533
620
  )}
@@ -541,7 +628,7 @@ export function ChatInput({
541
628
  <div
542
629
  ref={mentionMenuRef}
543
630
  data-testid="mention-menu"
544
- className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-y-auto py-1.5"
631
+ className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-x-hidden overflow-y-auto py-1.5"
545
632
  role="listbox"
546
633
  aria-label={t.mentionFiles}
547
634
  style={{ overflowY: "auto" }}
@@ -568,23 +655,25 @@ export function ChatInput({
568
655
  type="button"
569
656
  role="option"
570
657
  aria-selected={i === effectiveMentionIdx}
571
- className="flex w-full items-center gap-2 px-4 py-1.5 text-left transition-colors"
572
- style={{ background: i === effectiveMentionIdx ? "var(--dsw-hover)" : "transparent" }}
658
+ data-active={i === effectiveMentionIdx ? "true" : undefined}
659
+ className="isx-menu-item mx-1.5 flex w-[calc(100%-12px)] items-center gap-2 px-3 py-1.5 text-left"
573
660
  onMouseEnter={() => setMentionIdx(i)}
574
661
  onClick={() => applyMention(candidate)}
575
662
  title={candidateText(candidate)}
576
663
  >
577
- {candidate.dir ? (
578
- <IconFolderClose16 size={13} style={{ flex: "none", color: "var(--dsw-warn)" }} />
579
- ) : (
580
- <IconFileOutline16 size={13} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
581
- )}
664
+ <span className="isx-menu-icon" style={{ display: "inline-flex", flex: "none" }}>
665
+ {candidate.dir ? (
666
+ <IconFolderClose16 size={13} style={{ flex: "none", color: "var(--dsw-warn)" }} />
667
+ ) : (
668
+ <IconFileOutline16 size={13} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
669
+ )}
670
+ </span>
582
671
  {/* 和 pi 终端一样:名字在前,路径在后;根目录下的条目两者相同,不重复显示 */}
583
- <span className="flex-none truncate" style={{ maxWidth: "55%", fontSize: 12.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-primary)" }}>
672
+ <span className="isx-menu-shift flex-none truncate" style={{ maxWidth: "55%", fontSize: 12.5, fontFamily: "var(--font-mono)" }}>
584
673
  {label}
585
674
  </span>
586
675
  {description.replace(/\/$/, "") !== label.replace(/\/$/, "") && (
587
- <span className="min-w-0 flex-1 truncate" style={{ fontSize: 11.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)" }}>
676
+ <span className="isx-menu-sub min-w-0 flex-1 truncate pr-2" style={{ fontSize: 11.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)" }}>
588
677
  {description}
589
678
  </span>
590
679
  )}
@@ -595,7 +684,7 @@ export function ChatInput({
595
684
 
596
685
  {/* 斜杠命令菜单(dsh:悬浮于输入卡上方,同宽) */}
597
686
  {commandMode && visibleCommands.length > 0 && (
598
- <div ref={cmdMenuRef} className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-y-auto py-1.5" role="listbox" aria-label={t.slashCatalog} style={{ overflowY: "auto" }}>
687
+ <div ref={cmdMenuRef} className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-x-hidden overflow-y-auto py-1.5" role="listbox" aria-label={t.slashCatalog} style={{ overflowY: "auto" }}>
599
688
  <div className="px-4 pb-1 pt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
600
689
  {t.slashCatalog}
601
690
  </div>
@@ -604,21 +693,21 @@ export function ChatInput({
604
693
  key={`${c.kind}:${c.name}`}
605
694
  role="option"
606
695
  aria-selected={i === effectiveIdx}
607
- className="flex w-full items-baseline gap-3 px-4 py-2 text-left transition-colors"
608
- style={{ background: i === effectiveIdx ? "var(--dsw-hover)" : "transparent" }}
696
+ data-active={i === effectiveIdx ? "true" : undefined}
697
+ className="isx-menu-item mx-1.5 flex w-[calc(100%-12px)] items-baseline gap-3 px-3 py-2 text-left"
609
698
  onMouseEnter={() => setCmdIdx(i)}
610
699
  onClick={() => runEntry(c)}
611
700
  >
612
- <span style={{ fontSize: 14, fontWeight: 500, color: "var(--dsw-label-primary)", flex: "none" }}>{c.name}</span>
701
+ <span className="isx-menu-shift" style={{ fontSize: 14, fontWeight: 500, flex: "none" }}>{c.name}</span>
613
702
  {c.kind !== "builtin" && (
614
- <span className="rounded px-1.5 py-0.5" style={{ fontSize: 10.5, color: "var(--dsw-label-caption)", background: "var(--dsw-selector)", flex: "none" }}>
703
+ <span className="isx-menu-sub rounded px-1.5 py-0.5" style={{ fontSize: 10.5, color: "var(--dsw-label-caption)", background: "var(--dsw-selector)", flex: "none" }}>
615
704
  {c.kind === "skill" ? t.skillType : c.kind === "template" ? t.promptType : t.extensionCmdType}
616
705
  </span>
617
706
  )}
618
707
  {c.argumentHint && (
619
- <span style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)", flex: "none" }}>{c.argumentHint}</span>
708
+ <span className="isx-menu-sub" style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)", flex: "none" }}>{c.argumentHint}</span>
620
709
  )}
621
- <span className="truncate" style={{ fontSize: 13, color: "var(--dsw-label-tertiary)" }}>
710
+ <span className="isx-menu-sub truncate pr-2" style={{ fontSize: 13, color: "var(--dsw-label-tertiary)" }}>
622
711
  {c.desc}
623
712
  </span>
624
713
  </button>
@@ -627,11 +716,12 @@ export function ChatInput({
627
716
  )}
628
717
 
629
718
  {/*
630
- * 附件轨(图片 / 引用 / 上传文件):绝对定位浮在输入卡上方,**不占布局高度** ——
719
+ * 附件轨(图片 / 引用 / 上传文件):聊天中悬浮于输入卡上方,不占布局高度 ——
631
720
  * 输入框位置不动,多出来的附件直接盖住后面的聊天记录(用户要的就是这个)。
721
+ * 新对话页用 flow 占位,避免 @ 引用和图片盖住上方的工作区选择器。
632
722
  * 外层 pointer-events-none,只有附件本身接收点击,免得挡住聊天区的交互。
633
723
  */}
634
- <div className="pointer-events-none absolute inset-x-0 bottom-full z-20 flex flex-col gap-1.5 pb-2" data-testid="composer-attachments">
724
+ {(images.length > 0 || references.length > 0 || uploads.length > 0) && <div className={`pointer-events-none z-20 flex flex-col gap-1.5 pb-2 ${attachmentLayout === "flow" ? "relative" : "absolute inset-x-0 bottom-full"}`} data-testid="composer-attachments">
635
725
  {/* 附件轨:图片(超过 4 张折叠成牌堆,展开沿弧线扇开;点开可放大、切换、删除) */}
636
726
  {images.length > 0 && (
637
727
  <ComposerImages
@@ -681,51 +771,28 @@ export function ChatInput({
681
771
  </div>
682
772
  )}
683
773
 
684
- {/* 附件轨:上传的普通文件(压缩包/文档等,路径引用);超过 4 个折叠 */}
774
+ {/* 附件轨:上传的普通文件(超过 4 个折叠成牌堆,展开沿竖直向上弧度展开) */}
685
775
  {uploads.length > 0 && (
686
- <div className="pointer-events-auto flex flex-wrap items-center gap-1 px-1" data-testid="composer-uploads">
687
- {(uploadsExpanded ? uploads : uploads.slice(0, COLLAPSE_AT)).map((f, i) => (
688
- <span key={f.path} className="pw-ref-chip group/ref" title={f.path}>
689
- <IconFileOutline16 size={12} style={{ flex: "none", color: "var(--dsw-label-tertiary)" }} />
690
- <span className="max-w-[180px] truncate">{f.name}</span>
691
- <span style={{ flex: "none", fontSize: 11, color: "var(--dsw-label-caption)" }}>{fmtUploadSize(f.size)}</span>
692
- <button
693
- type="button"
694
- className="pw-ref-close"
695
- disabled={sendBusy}
696
- onClick={() => changeDraft((previous) => ({ ...previous, uploads: previous.uploads.filter((_, index) => index !== i) }))}
697
- aria-label={`${t.delete} ${f.name}`}
698
- >
699
- ✕
700
- </button>
701
- </span>
702
- ))}
703
- {uploads.length > COLLAPSE_AT ? (
704
- <button type="button" className="pw-chip" data-testid="uploads-toggle" onClick={() => setUploadsExpanded((open) => !open)}>
705
- {uploadsExpanded ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(uploads.length - COLLAPSE_AT))}
706
- </button>
707
- ) : null}
708
- </div>
776
+ <ComposerFiles
777
+ files={uploads}
778
+ disabled={sendBusy}
779
+ onRemove={(index) => changeDraft((previous) => ({ ...previous, uploads: previous.uploads.filter((_, at) => at !== index) }))}
780
+ onClear={() => changeDraft((previous) => ({ ...previous, uploads: [] }))}
781
+ />
709
782
  )}
710
783
 
711
- </div>
784
+ </div>}
712
785
 
713
- {/* 卡片(玻璃态)—— 拖放由 document 级监听统一接管 */}
714
- <div
715
- className="relative w-full"
716
- style={{
717
- borderRadius: 22,
718
- background: "var(--dsw-input-major)",
719
- boxShadow: "var(--dsw-elevation-soft)",
720
- border: "0.5px solid var(--dsw-border-l1)",
721
- }}
722
- >
723
- <div className="px-4 pt-3 flex items-start gap-1.5">
786
+ {/* 卡片(Iconsax .isx-composer:24px 圆角 + 聚焦高对比描边 + 底部流光短横条)—— 拖放由 document 级监听统一接管 */}
787
+ <div className="isx-composer relative w-full">
788
+ <div className="px-4 pt-3 flex flex-col items-stretch gap-1.5">
724
789
  {adopted && (
725
790
  <button
726
791
  type="button"
727
- className="flex-none flex items-center gap-1 self-start"
728
- title={`${t.delete} (Backspace)`}
792
+ className="flex-none flex max-w-full items-center gap-1 self-start"
793
+ disabled={disabled || sendBusy}
794
+ aria-label={`${t.delete} /${adopted.name}`}
795
+ title={`${t.delete} /${adopted.name} (Backspace)`}
729
796
  style={{
730
797
  fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))",
731
798
  lineHeight: "var(--piweb-chat-line-height, 1.55)",
@@ -740,7 +807,8 @@ export function ChatInput({
740
807
  }}
741
808
  >
742
809
  <IconTerminalOutline14 size={14} style={{ color: "var(--dsw-accent)", flex: "none" }} />
743
- <span style={{ color: "var(--dsw-accent)", fontWeight: 600 }}>/{adopted.name}</span>
810
+ <span className="min-w-0 truncate" style={{ color: "var(--dsw-accent)", fontWeight: 600 }}>/{adopted.name}</span>
811
+ <IconCloseOutline14 size={12} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
744
812
  </button>
745
813
  )}
746
814
  <textarea
@@ -750,11 +818,12 @@ export function ChatInput({
750
818
  rows={1}
751
819
  placeholder={adopted ? (adopted.argumentHint ?? t.inputPlaceholder) : !disabled && isBlocked ? t.blockedComposer : t.inputPlaceholder}
752
820
  suppressHydrationWarning
753
- className="min-w-0 flex-1 resize-none"
821
+ className="min-w-0 w-full resize-none"
754
822
  style={{ fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))", lineHeight: 1.55 }}
755
823
  onChange={(e) => {
756
824
  const value = e.target.value;
757
825
  setText(value);
826
+ setCommandCaret(e.target.selectionStart);
758
827
  setCmdDismissed(false);
759
828
  setCmdIdx(0);
760
829
  syncMention(value, e.target.selectionStart);
@@ -763,7 +832,10 @@ export function ChatInput({
763
832
  onSelect={(e) => {
764
833
  // 选中了一段文字时不弹:那时 Enter / Tab 应该照常工作
765
834
  const ta = e.target as HTMLTextAreaElement;
766
- syncMention(ta.value, ta.selectionStart === ta.selectionEnd ? ta.selectionStart : null);
835
+ const caret = ta.selectionStart === ta.selectionEnd ? ta.selectionStart : null;
836
+ setCommandCaret(caret);
837
+ if (slashCommandToken(ta.value, caret)?.start !== commandToken?.start) setCmdDismissed(false);
838
+ syncMention(ta.value, caret);
767
839
  }}
768
840
  onPaste={(e) => {
769
841
  const files = Array.from(e.clipboardData.files);
@@ -801,13 +873,16 @@ export function ChatInput({
801
873
  }
802
874
  }
803
875
  }
804
- if (adopted && text === "" && (e.key === "Backspace" || e.key === "Escape")) {
805
- // 空参数时退格/Esc:摘掉命令胶囊
876
+ if (adopted && !e.nativeEvent.isComposing && (
877
+ (e.key === "Backspace" && e.currentTarget.selectionStart === 0 && e.currentTarget.selectionEnd === 0) ||
878
+ (e.key === "Escape" && text === "")
879
+ )) {
880
+ // 光标在开头时退格 / 空参数时 Esc:只摘掉命令标记,不碰任何正文
806
881
  e.preventDefault();
807
882
  setAdopted(null);
808
883
  return;
809
884
  }
810
- if (commandMode && visibleCommands.length > 0) {
885
+ if (commandMode && visibleCommands.length > 0 && !e.nativeEvent.isComposing) {
811
886
  if (e.key === "ArrowDown") {
812
887
  e.preventDefault();
813
888
  setCmdIdx((i) => Math.min(i + 1, visibleCommands.length - 1));
@@ -820,10 +895,10 @@ export function ChatInput({
820
895
  }
821
896
  if (e.key === "Escape") {
822
897
  e.preventDefault();
823
- setCmdDismissed(true);
898
+ setCmdDismissed(true);
824
899
  return;
825
900
  }
826
- if (e.key === "Enter" && !e.nativeEvent.isComposing) {
901
+ if (e.key === "Enter") {
827
902
  e.preventDefault();
828
903
  runEntry(visibleCommands[effectiveIdx]);
829
904
  return;
@@ -833,13 +908,7 @@ export function ChatInput({
833
908
  e.preventDefault();
834
909
  const entry = visibleCommands[effectiveIdx];
835
910
  if (!entry) return;
836
- setAdopted(entry);
837
- setText("");
838
- setCmdIdx(0);
839
- requestAnimationFrame(() => {
840
- autoSize();
841
- taRef.current?.focus();
842
- });
911
+ adoptCommand(entry);
843
912
  return;
844
913
  }
845
914
  }
@@ -900,13 +969,23 @@ export function ChatInput({
900
969
 
901
970
  {/* 停止 / 发送圆钮:运行中没草稿 → 停止;运行中有草稿 → 幽灵停止钮 + 发送(发送即按设置插话 / 排队) */}
902
971
  {isStreaming && !hasDraft ? (
903
- <button className="btn-primary-circle" title={t.stop} onClick={() => void stopAndRestore()}>
972
+ <button
973
+ className="btn-primary-circle isx-btn-stop"
974
+ data-streaming="true"
975
+ title={t.stop}
976
+ onClick={() => void stopAndRestore()}
977
+ >
904
978
  <IconStopFill16 size={16} />
905
979
  </button>
906
980
  ) : (
907
981
  <>
908
982
  {isStreaming && (
909
- <button className="btn-ghost-circle" title={t.stop} onClick={() => void stopAndRestore()}>
983
+ <button
984
+ className="btn-ghost-circle isx-btn-stop"
985
+ data-streaming="true"
986
+ title={t.stop}
987
+ onClick={() => void stopAndRestore()}
988
+ >
910
989
  <IconStopFill16 size={14} />
911
990
  </button>
912
991
  )}