@nextclaw/ui 0.12.8 → 0.12.10

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 (227) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/assets/ChannelsList-M9FTK1Ak.js +8 -0
  3. package/dist/assets/DocBrowser-CH7-GxlL.js +1 -0
  4. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-B3HWP4oz.js} +1 -1
  9. package/dist/assets/ProvidersList-CHjMnRhX.js +1 -0
  10. package/dist/assets/RuntimeConfig-psp8nMSG.js +1 -0
  11. package/dist/assets/SearchConfig-CSoKip1f.js +1 -0
  12. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-DocgeQtR.js → book-open-DzdUViDm.js} +1 -1
  16. package/dist/assets/chat-page-CLp0UV0Y.js +58 -0
  17. package/dist/assets/chat-session-display-DsYHx0RZ.js +1 -0
  18. package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-CVqPF5ie.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-Bop2oB18.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-DmAlInRu.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-Da5Gm_pO.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/download-BD0ETkB-.js +1 -0
  27. package/dist/assets/{external-link-DFjw3x1B.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-DJtaCejM.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/i18n-CpTZLchQ.js +1 -0
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-DEFUIR12.js → logos-B7gRObP8.js} +1 -1
  35. package/dist/assets/marketplace-page-3qVMnF3d.js +1 -0
  36. package/dist/assets/marketplace-page-BhFIeQzI.js +49 -0
  37. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +40 -0
  38. package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-0UcO9H9Z.js} +1 -1
  39. package/dist/assets/play-CKDjSQFL.js +1 -0
  40. package/dist/assets/plus-CG0QrVY_.js +1 -0
  41. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-Bcv40SXy.js} +1 -1
  43. package/dist/assets/remote-access-page-CWHG-sug.js +1 -0
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-EqJPOF0G.js} +1 -1
  46. package/dist/assets/search-BCAlB8nz.js +1 -0
  47. package/dist/assets/security-config-Slh0Mayz.js +1 -0
  48. package/dist/assets/select-CVz0t7MF.js +41 -0
  49. package/dist/assets/setting-row-CbVHAuQt.js +1 -0
  50. package/dist/assets/skeleton-D5rdKvzy.js +1 -0
  51. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-0rmPBRKI.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +95 -21
  61. package/dist/manifest.webmanifest +30 -0
  62. package/dist/offline.html +102 -0
  63. package/dist/pwa-192.png +0 -0
  64. package/dist/pwa-512.png +0 -0
  65. package/dist/runtime-icons/claude.ico +0 -0
  66. package/dist/runtime-icons/codex-openai.svg +6 -0
  67. package/dist/runtime-icons/hermes-agent.png +0 -0
  68. package/dist/sw.js +80 -0
  69. package/index.html +73 -1
  70. package/package.json +5 -5
  71. package/public/manifest.webmanifest +30 -0
  72. package/public/offline.html +102 -0
  73. package/public/pwa-192.png +0 -0
  74. package/public/pwa-512.png +0 -0
  75. package/public/runtime-icons/claude.ico +0 -0
  76. package/public/runtime-icons/codex-openai.svg +6 -0
  77. package/public/runtime-icons/hermes-agent.png +0 -0
  78. package/public/sw.js +80 -0
  79. package/src/account/components/account-panel.tsx +217 -97
  80. package/src/account/managers/account.manager.ts +3 -2
  81. package/src/api/chat-session-type.types.ts +7 -0
  82. package/src/api/runtime-control.types.ts +8 -0
  83. package/src/api/server-path.ts +27 -4
  84. package/src/api/types.ts +25 -10
  85. package/src/app.tsx +227 -54
  86. package/src/components/agents/agent-dialogs.tsx +499 -0
  87. package/src/components/agents/agents-page.test.tsx +238 -0
  88. package/src/components/agents/agents-page.tsx +435 -0
  89. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  90. package/src/components/chat/ChatSidebar.tsx +35 -35
  91. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  92. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  93. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  94. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +127 -206
  95. package/src/components/chat/chat-conversation-panel.tsx +482 -0
  96. package/src/components/chat/chat-page-shell.tsx +19 -13
  97. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  98. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  99. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +178 -0
  100. package/src/components/chat/chat-session-workspace-file-preview.tsx +278 -0
  101. package/src/components/chat/chat-session-workspace-panel-nav.tsx +203 -0
  102. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  103. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  104. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  105. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  106. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  107. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  108. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  109. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  110. package/src/components/chat/ncp/ncp-chat-page.tsx +9 -7
  111. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  112. package/src/components/chat/ncp/ncp-session-adapter.test.ts +36 -1
  113. package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
  114. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +62 -13
  115. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  116. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  117. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  118. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  119. package/src/components/chat/stores/chat-input.store.ts +2 -1
  120. package/src/components/chat/stores/chat-thread.store.ts +27 -1
  121. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  122. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  123. package/src/components/common/BrandHeader.tsx +3 -1
  124. package/src/components/common/session-context-icon.tsx +15 -2
  125. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  126. package/src/components/config/ChannelForm.test.tsx +89 -3
  127. package/src/components/config/ChannelForm.tsx +157 -188
  128. package/src/components/config/ChannelsList.test.tsx +163 -119
  129. package/src/components/config/ChannelsList.tsx +90 -101
  130. package/src/components/config/ProviderForm.tsx +108 -146
  131. package/src/components/config/ProvidersList.tsx +100 -123
  132. package/src/components/config/RuntimeConfig.tsx +141 -2
  133. package/src/components/config/SearchConfig.tsx +423 -393
  134. package/src/components/config/channel-form-fields-section.tsx +70 -37
  135. package/src/components/config/config-split-page.tsx +109 -0
  136. package/src/components/config/provider-enabled-field.tsx +17 -10
  137. package/src/components/config/runtime-control-card.test.tsx +56 -0
  138. package/src/components/config/runtime-control-card.tsx +25 -0
  139. package/src/components/config/runtime-presence-card.tsx +93 -79
  140. package/src/components/layout/AppLayout.tsx +25 -37
  141. package/src/components/layout/app-layout.test.tsx +46 -14
  142. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  143. package/src/components/layout/runtime-status-entry.tsx +143 -0
  144. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  145. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  146. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  147. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  148. package/src/components/marketplace/marketplace-page.tsx +596 -0
  149. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  150. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  151. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  152. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  153. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  154. package/src/components/providers/ThemeProvider.tsx +5 -0
  155. package/src/components/remote/remote-access-page.test.tsx +105 -0
  156. package/src/components/remote/remote-access-page.tsx +248 -0
  157. package/src/components/ui/notice-card.tsx +129 -0
  158. package/src/components/ui/setting-row.tsx +51 -0
  159. package/src/components/ui/tag-chip.tsx +39 -0
  160. package/src/components/ui/textarea.tsx +19 -0
  161. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  162. package/src/hooks/useConfig.ts +2 -1
  163. package/src/index.css +24 -0
  164. package/src/lib/app-resource-uri.test.ts +20 -0
  165. package/src/lib/app-resource-uri.ts +29 -0
  166. package/src/lib/chat-message.ts +14 -3
  167. package/src/lib/i18n.chat.ts +12 -1
  168. package/src/lib/i18n.pwa.ts +62 -0
  169. package/src/lib/i18n.remote.ts +1 -1
  170. package/src/lib/i18n.runtime-control.ts +31 -0
  171. package/src/lib/i18n.ts +7 -10
  172. package/src/lib/session-context.utils.test.ts +71 -0
  173. package/src/lib/session-context.utils.ts +28 -3
  174. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  175. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  176. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  177. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  178. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  179. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  180. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  181. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  182. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  183. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  184. package/src/pwa/pwa.types.ts +22 -0
  185. package/src/pwa/register-pwa.ts +14 -0
  186. package/src/pwa/stores/pwa.store.ts +17 -0
  187. package/src/vite-env.d.ts +9 -0
  188. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  189. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  190. package/dist/assets/MarketplacePage-BySqkYDh.js +0 -49
  191. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  192. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  193. package/dist/assets/ModelConfig-IrmzoslW.js +0 -1
  194. package/dist/assets/ProvidersList-8_Kalfwl.js +0 -1
  195. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  196. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  197. package/dist/assets/SearchConfig-DNBR-UbE.js +0 -1
  198. package/dist/assets/SessionsConfig-Doqp5ghH.js +0 -2
  199. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  200. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  201. package/dist/assets/config-layout-DmlGaay2.js +0 -1
  202. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  203. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  204. package/dist/assets/index-DafCdM4F.css +0 -1
  205. package/dist/assets/index-DdksE6U3.js +0 -6
  206. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  207. package/dist/assets/play-DBQbBxTA.js +0 -1
  208. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  209. package/dist/assets/popover-C_mWOFzI.js +0 -1
  210. package/dist/assets/search-MChQRYR1.js +0 -1
  211. package/dist/assets/security-config-CbXfPZzr.js +0 -1
  212. package/dist/assets/select-Caud8QvU.js +0 -41
  213. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  214. package/dist/assets/x-DuMhMATD.js +0 -1
  215. package/src/components/agents/AgentDialogs.tsx +0 -400
  216. package/src/components/agents/AgentsPage.test.tsx +0 -217
  217. package/src/components/agents/AgentsPage.tsx +0 -352
  218. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  219. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  220. package/src/components/config/config-layout.ts +0 -10
  221. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  222. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  223. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  224. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  225. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  226. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  227. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
@@ -0,0 +1,482 @@
1
+ import { type ComponentProps, useRef } from "react";
2
+ import { ArrowLeft } from "lucide-react";
3
+ import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
4
+ import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
5
+ import {
6
+ ChatInputBarContainer,
7
+ ChatMessageListContainer,
8
+ } from "@/components/chat/nextclaw";
9
+ import { ChatWelcome } from "@/components/chat/ChatWelcome";
10
+ import { ChatSessionWorkspacePanel } from "@/components/chat/chat-session-workspace-panel";
11
+ import { AgentAvatar } from "@/components/common/AgentAvatar";
12
+ import { SessionContextIconNode } from "@/components/common/session-context-icon";
13
+ import { usePresenter } from "@/components/chat/presenter/chat-presenter-context";
14
+ import { ChatSessionHeaderActions } from "@/components/chat/session-header/chat-session-header-actions";
15
+ import { ChatSessionProjectBadge } from "@/components/chat/session-header/chat-session-project-badge";
16
+ import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
17
+ import { useChatThreadStore } from "@/components/chat/stores/chat-thread.store";
18
+ import { Skeleton } from "@/components/ui/skeleton";
19
+ import { resolveAgentRuntimeSessionType } from "@/components/chat/useChatSessionTypeState";
20
+ import { t } from "@/lib/i18n";
21
+ import { cn } from "@/lib/utils";
22
+
23
+ const CHAT_CONVERSATION_SKELETON_BUBBLES = [
24
+ {
25
+ key: "hero",
26
+ alignmentClassName: "justify-start",
27
+ bubbleClassName: "max-w-[78%] h-32 rounded-[30px]",
28
+ },
29
+ {
30
+ key: "follow-up",
31
+ alignmentClassName: "justify-start",
32
+ bubbleClassName: "max-w-[62%] h-24 rounded-[28px]",
33
+ },
34
+ {
35
+ key: "reply",
36
+ alignmentClassName: "justify-end",
37
+ bubbleClassName: "max-w-[70%] h-24 rounded-[28px]",
38
+ },
39
+ {
40
+ key: "detail",
41
+ alignmentClassName: "justify-start",
42
+ bubbleClassName: "max-w-[88%] h-36 rounded-[30px]",
43
+ },
44
+ ] as const;
45
+
46
+ function ChatConversationSkeleton() {
47
+ return (
48
+ <section
49
+ data-testid="chat-conversation-skeleton"
50
+ className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white"
51
+ >
52
+ <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
53
+ <div className="mx-auto flex min-h-full w-full max-w-[min(1120px,100%)] flex-col px-6 py-5">
54
+ <div className="flex flex-1 flex-col gap-8">
55
+ <div className="space-y-6">
56
+ <Skeleton className="h-6 w-52 rounded-lg bg-gray-200/90" />
57
+ <div className="space-y-5">
58
+ {CHAT_CONVERSATION_SKELETON_BUBBLES.map((bubble) => (
59
+ <div
60
+ key={bubble.key}
61
+ className={cn("flex w-full", bubble.alignmentClassName)}
62
+ >
63
+ <Skeleton
64
+ data-testid="chat-conversation-skeleton-bubble"
65
+ className={cn(
66
+ "w-full bg-gray-200/80",
67
+ bubble.bubbleClassName,
68
+ )}
69
+ />
70
+ </div>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ <div className="mt-auto grid gap-4 pb-2 sm:grid-cols-[minmax(0,1fr)_minmax(180px,240px)] sm:items-end">
75
+ <div className="space-y-3">
76
+ <Skeleton className="h-4 w-40 rounded-full bg-gray-200/70" />
77
+ <Skeleton className="h-[112px] w-full rounded-[30px] bg-gray-200/70" />
78
+ </div>
79
+ <div className="hidden justify-end sm:flex">
80
+ <Skeleton className="h-10 w-36 rounded-full bg-gray-200/75" />
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ <div className="border-t border-gray-200/80 bg-white p-4">
87
+ <div className="mx-auto w-full max-w-[min(1120px,100%)]">
88
+ <div className="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-card">
89
+ <div className="px-4 py-2.5">
90
+ <Skeleton className="h-[84px] w-full rounded-[28px] bg-gray-200/80" />
91
+ </div>
92
+ <div className="flex items-center justify-between gap-3 px-3 pb-3">
93
+ <div className="flex items-center gap-2">
94
+ <Skeleton className="h-8 w-20 rounded-full bg-gray-200/75" />
95
+ <Skeleton className="h-8 w-28 rounded-full bg-gray-200/75" />
96
+ <Skeleton className="hidden h-8 w-24 rounded-full bg-gray-200/70 sm:block" />
97
+ </div>
98
+ <Skeleton className="h-8 w-8 rounded-full bg-gray-200/85" />
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </section>
104
+ );
105
+ }
106
+
107
+ type ChatThreadSnapshot = ReturnType<typeof useChatThreadStore.getState>["snapshot"];
108
+ type ChatHeaderDeleteHandler = ComponentProps<
109
+ typeof ChatSessionHeaderActions
110
+ >["onDeleteSession"];
111
+ type ChatToolActionHandler = ComponentProps<
112
+ typeof ChatMessageListContainer
113
+ >["onToolAction"];
114
+ type ChatFileOpenHandler = ComponentProps<
115
+ typeof ChatMessageListContainer
116
+ >["onFileOpen"];
117
+
118
+ type ChatParentSessionBannerProps = {
119
+ parentSessionLabel: string | null;
120
+ onGoToParentSession: () => void;
121
+ };
122
+
123
+ function ChatParentSessionBanner({
124
+ parentSessionLabel,
125
+ onGoToParentSession,
126
+ }: ChatParentSessionBannerProps) {
127
+ if (!parentSessionLabel) {
128
+ return null;
129
+ }
130
+ const trimmedLabel = parentSessionLabel.trim();
131
+ return (
132
+ <div className="border-b border-gray-200/60 bg-white/75 px-5 py-2 backdrop-blur-sm">
133
+ <button
134
+ type="button"
135
+ onClick={onGoToParentSession}
136
+ className="inline-flex items-center gap-2 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900"
137
+ >
138
+ <ArrowLeft className="h-3.5 w-3.5" />
139
+ <span>
140
+ {t("chatBackToParent")}
141
+ {trimmedLabel ? ` · ${trimmedLabel}` : ""}
142
+ </span>
143
+ </button>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ type ChatConversationHeaderProps = {
149
+ snapshot: ChatThreadSnapshot;
150
+ childSessionCount: number;
151
+ normalizedAgentId: string;
152
+ sessionHeaderTitle: string;
153
+ shouldShowHeaderAgentAvatar: boolean;
154
+ shouldShowSessionHeader: boolean;
155
+ onOpenChildSessions: () => void;
156
+ onDeleteSession: ChatHeaderDeleteHandler;
157
+ };
158
+
159
+ function ChatConversationHeader({
160
+ snapshot,
161
+ childSessionCount,
162
+ normalizedAgentId,
163
+ sessionHeaderTitle,
164
+ shouldShowHeaderAgentAvatar,
165
+ shouldShowSessionHeader,
166
+ onOpenChildSessions,
167
+ onDeleteSession,
168
+ }: ChatConversationHeaderProps) {
169
+ return (
170
+ <div
171
+ className={cn(
172
+ "px-5 border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-all duration-200",
173
+ shouldShowSessionHeader ? "py-3 opacity-100" : "h-0 py-0 opacity-0 border-b-0",
174
+ )}
175
+ >
176
+ <div className="min-w-0 flex-1 flex items-center gap-2">
177
+ {shouldShowHeaderAgentAvatar ? (
178
+ <div className="inline-flex shrink-0 items-center">
179
+ <AgentAvatar
180
+ agentId={normalizedAgentId}
181
+ displayName={snapshot.agentDisplayName}
182
+ avatarUrl={snapshot.agentAvatarUrl}
183
+ className="h-5 w-5"
184
+ />
185
+ </div>
186
+ ) : null}
187
+ <span className="text-sm font-medium text-gray-700 truncate">
188
+ {sessionHeaderTitle}
189
+ </span>
190
+ {snapshot.sessionTypeLabel ? (
191
+ <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-gray-200 bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">
192
+ {snapshot.sessionTypeIcon?.src ? (
193
+ <span className="inline-flex h-[1.125rem] w-[1.125rem] items-center justify-center">
194
+ <SessionContextIconNode
195
+ icon={{
196
+ kind: "runtime-image",
197
+ src: snapshot.sessionTypeIcon.src,
198
+ alt: snapshot.sessionTypeIcon.alt ?? null,
199
+ name: snapshot.sessionTypeLabel
200
+ }}
201
+ />
202
+ </span>
203
+ ) : null}
204
+ {snapshot.sessionTypeLabel}
205
+ </span>
206
+ ) : null}
207
+ {snapshot.sessionProjectName ? (
208
+ <ChatSessionProjectBadge
209
+ sessionKey={snapshot.sessionKey ?? "draft"}
210
+ projectName={snapshot.sessionProjectName}
211
+ projectRoot={snapshot.sessionProjectRoot}
212
+ persistToServer={snapshot.canDeleteSession}
213
+ />
214
+ ) : null}
215
+ </div>
216
+ {snapshot.sessionKey ? (
217
+ <ChatSessionHeaderActions
218
+ sessionKey={snapshot.sessionKey}
219
+ canDeleteSession={snapshot.canDeleteSession}
220
+ isDeletePending={snapshot.isDeletePending}
221
+ projectRoot={snapshot.sessionProjectRoot}
222
+ childSessionCount={childSessionCount}
223
+ onOpenChildSessions={onOpenChildSessions}
224
+ onDeleteSession={onDeleteSession}
225
+ />
226
+ ) : null}
227
+ </div>
228
+ );
229
+ }
230
+
231
+ type ChatConversationAlertsProps = {
232
+ shouldShowProviderHint: boolean;
233
+ sessionTypeUnavailable: boolean;
234
+ sessionTypeUnavailableMessage: string | null;
235
+ onGoToProviders: () => void;
236
+ };
237
+
238
+ function ChatConversationAlerts({
239
+ shouldShowProviderHint,
240
+ sessionTypeUnavailable,
241
+ sessionTypeUnavailableMessage,
242
+ onGoToProviders,
243
+ }: ChatConversationAlertsProps) {
244
+ return (
245
+ <>
246
+ {shouldShowProviderHint ? (
247
+ <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0">
248
+ <span className="text-xs text-amber-800">
249
+ {t("chatModelNoOptions")}
250
+ </span>
251
+ <button
252
+ type="button"
253
+ onClick={onGoToProviders}
254
+ className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
255
+ >
256
+ {t("chatGoConfigureProvider")}
257
+ </button>
258
+ </div>
259
+ ) : null}
260
+ {sessionTypeUnavailable && sessionTypeUnavailableMessage?.trim() ? (
261
+ <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0">
262
+ <span className="text-xs text-amber-800">
263
+ {sessionTypeUnavailableMessage}
264
+ </span>
265
+ </div>
266
+ ) : null}
267
+ </>
268
+ );
269
+ }
270
+
271
+ type ChatConversationContentProps = {
272
+ snapshot: ChatThreadSnapshot;
273
+ availableAgents: NonNullable<ChatThreadSnapshot["availableAgents"]>;
274
+ hideEmptyHint: boolean;
275
+ showWelcome: boolean;
276
+ threadRef: ComponentProps<"div">["ref"];
277
+ onScroll: ComponentProps<"div">["onScroll"];
278
+ onCreateSession: () => void;
279
+ onSelectAgent: (agentId: string) => void;
280
+ onToolAction: ChatToolActionHandler;
281
+ onFileOpen: ChatFileOpenHandler;
282
+ };
283
+
284
+ function ChatConversationContent({
285
+ snapshot,
286
+ availableAgents,
287
+ hideEmptyHint,
288
+ showWelcome,
289
+ threadRef,
290
+ onScroll,
291
+ onCreateSession,
292
+ onSelectAgent,
293
+ onToolAction,
294
+ onFileOpen,
295
+ }: ChatConversationContentProps) {
296
+ return (
297
+ <div
298
+ ref={threadRef}
299
+ onScroll={onScroll}
300
+ className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
301
+ >
302
+ {showWelcome ? (
303
+ <ChatWelcome
304
+ onCreateSession={onCreateSession}
305
+ agents={availableAgents}
306
+ selectedAgentId={snapshot.agentId ?? "main"}
307
+ onSelectAgent={onSelectAgent}
308
+ />
309
+ ) : hideEmptyHint ? null : snapshot.messages.length === 0 ? (
310
+ <div className="px-5 py-5 text-sm text-gray-500">
311
+ {t("chatNoMessages")}
312
+ </div>
313
+ ) : (
314
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
315
+ <ChatMessageListContainer
316
+ key={snapshot.sessionKey ?? "draft"}
317
+ messages={snapshot.messages}
318
+ isSending={snapshot.isSending && snapshot.isAwaitingAssistantOutput}
319
+ onToolAction={onToolAction}
320
+ onFileOpen={onFileOpen}
321
+ />
322
+ </div>
323
+ )}
324
+ </div>
325
+ );
326
+ }
327
+
328
+ function shouldShowWorkspacePanel(
329
+ snapshot: ChatThreadSnapshot,
330
+ childSessionTabs: ChatThreadSnapshot["childSessionTabs"],
331
+ workspaceFileTabs: ChatThreadSnapshot["workspaceFileTabs"],
332
+ ) {
333
+ if (snapshot.workspacePanelParentKey !== snapshot.sessionKey) {
334
+ return false;
335
+ }
336
+ return childSessionTabs.length > 0 || workspaceFileTabs.length > 0;
337
+ }
338
+
339
+ export function ChatConversationPanel() {
340
+ const presenter = usePresenter();
341
+ const defaultSessionType = useChatInputStore(
342
+ (state) => state.snapshot.defaultSessionType,
343
+ );
344
+ const snapshot = useChatThreadStore((state) => state.snapshot);
345
+ const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
346
+ const threadRef = snapshot.threadRef ?? fallbackThreadRef;
347
+ const childSessionTabs = snapshot.childSessionTabs.filter(
348
+ (tab) => tab.parentSessionKey === snapshot.sessionKey,
349
+ );
350
+ const workspaceFileTabs = snapshot.workspaceFileTabs.filter(
351
+ (tab) => tab.parentSessionKey === snapshot.sessionKey,
352
+ );
353
+ const showWorkspacePanel = shouldShowWorkspacePanel(
354
+ snapshot,
355
+ childSessionTabs,
356
+ workspaceFileTabs,
357
+ );
358
+ const shouldShowSessionHeader = Boolean(
359
+ snapshot.sessionKey || snapshot.sessionTypeLabel,
360
+ );
361
+ const sessionHeaderTitle =
362
+ snapshot.sessionDisplayName ||
363
+ (snapshot.canDeleteSession && snapshot.sessionKey ? snapshot.sessionKey : null) ||
364
+ t("chatSidebarNewTask");
365
+ const normalizedAgentId = snapshot.agentId?.trim() ?? "";
366
+ const shouldShowHeaderAgentAvatar =
367
+ normalizedAgentId.length > 0 &&
368
+ normalizedAgentId.toLowerCase() !== "main";
369
+
370
+ const showWelcome =
371
+ !snapshot.canDeleteSession &&
372
+ snapshot.messages.length === 0 &&
373
+ !snapshot.isSending;
374
+ const hasConfiguredModel = snapshot.modelOptions.length > 0;
375
+ const shouldShowProviderHint =
376
+ snapshot.isProviderStateResolved && !hasConfiguredModel;
377
+ const hideEmptyHint =
378
+ snapshot.isHistoryLoading &&
379
+ snapshot.messages.length === 0 &&
380
+ !snapshot.isSending &&
381
+ !snapshot.isAwaitingAssistantOutput;
382
+ const availableAgents = snapshot.availableAgents ?? [];
383
+ const resolveDraftAgent = (agentId: string) =>
384
+ availableAgents.find((agent) => agent.id === agentId) ?? null;
385
+ const createDraftSessionForAgent = () => {
386
+ const sessionType = resolveAgentRuntimeSessionType(
387
+ resolveDraftAgent(snapshot.agentId ?? "main"),
388
+ defaultSessionType,
389
+ );
390
+ presenter.chatSessionListManager.createSession(sessionType);
391
+ };
392
+ const selectDraftAgent = (agentId: string) => {
393
+ presenter.chatSessionListManager.setSelectedAgentId(agentId);
394
+ presenter.chatInputManager.setPendingSessionType(
395
+ resolveAgentRuntimeSessionType(resolveDraftAgent(agentId), defaultSessionType),
396
+ );
397
+ };
398
+ const openFilePreview = (action: ChatFileOpenActionViewModel) => {
399
+ presenter.chatThreadManager.openFilePreview(action);
400
+ };
401
+ const openChildSessions = () => {
402
+ if (!snapshot.sessionKey) {
403
+ return;
404
+ }
405
+ presenter.chatThreadManager.openChildSessionPanel({
406
+ parentSessionKey: snapshot.sessionKey,
407
+ activeChildSessionKey: childSessionTabs[0]?.sessionKey ?? null,
408
+ });
409
+ };
410
+
411
+ const { onScroll: handleScroll } = useStickyBottomScroll({
412
+ scrollRef: threadRef,
413
+ resetKey: snapshot.sessionKey,
414
+ isLoading: snapshot.isHistoryLoading,
415
+ hasContent: snapshot.messages.length > 0,
416
+ contentVersion: snapshot.messages[snapshot.messages.length - 1] ?? null,
417
+ });
418
+
419
+ if (!snapshot.isProviderStateResolved) {
420
+ return <ChatConversationSkeleton />;
421
+ }
422
+
423
+ return (
424
+ <section className="flex-1 min-h-0 flex overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
425
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
426
+ <ChatParentSessionBanner
427
+ parentSessionLabel={
428
+ snapshot.parentSessionKey ? (snapshot.parentSessionLabel ?? null) : null
429
+ }
430
+ onGoToParentSession={presenter.chatThreadManager.goToParentSession}
431
+ />
432
+ <ChatConversationHeader
433
+ snapshot={snapshot}
434
+ childSessionCount={childSessionTabs.length}
435
+ normalizedAgentId={normalizedAgentId}
436
+ sessionHeaderTitle={sessionHeaderTitle}
437
+ shouldShowHeaderAgentAvatar={shouldShowHeaderAgentAvatar}
438
+ shouldShowSessionHeader={shouldShowSessionHeader}
439
+ onOpenChildSessions={openChildSessions}
440
+ onDeleteSession={presenter.chatThreadManager.deleteSession}
441
+ />
442
+ <ChatConversationAlerts
443
+ shouldShowProviderHint={shouldShowProviderHint}
444
+ sessionTypeUnavailable={snapshot.sessionTypeUnavailable}
445
+ sessionTypeUnavailableMessage={snapshot.sessionTypeUnavailableMessage ?? null}
446
+ onGoToProviders={presenter.chatThreadManager.goToProviders}
447
+ />
448
+ <ChatConversationContent
449
+ snapshot={snapshot}
450
+ availableAgents={availableAgents}
451
+ hideEmptyHint={hideEmptyHint}
452
+ showWelcome={showWelcome}
453
+ threadRef={threadRef}
454
+ onScroll={handleScroll}
455
+ onCreateSession={createDraftSessionForAgent}
456
+ onSelectAgent={selectDraftAgent}
457
+ onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
458
+ onFileOpen={openFilePreview}
459
+ />
460
+
461
+ <ChatInputBarContainer />
462
+ </div>
463
+
464
+ {showWorkspacePanel ? (
465
+ <ChatSessionWorkspacePanel
466
+ childSessionTabs={childSessionTabs}
467
+ activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
468
+ workspaceFileTabs={workspaceFileTabs}
469
+ activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
470
+ sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
471
+ onSelectSession={presenter.chatThreadManager.selectChildSessionDetail}
472
+ onSelectFile={presenter.chatThreadManager.selectWorkspaceFile}
473
+ onCloseFile={presenter.chatThreadManager.closeWorkspaceFile}
474
+ onClose={presenter.chatThreadManager.closeWorkspacePanel}
475
+ onBackToParent={presenter.chatThreadManager.goToParentSession}
476
+ onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
477
+ onFileOpen={openFilePreview}
478
+ />
479
+ ) : null}
480
+ </section>
481
+ );
482
+ }
@@ -1,12 +1,12 @@
1
- import { useEffect } from 'react';
2
- import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
3
- import { ChatSidebar } from '@/components/chat/ChatSidebar';
4
- import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
5
- import { AgentsPage } from '@/components/agents/AgentsPage';
6
- import { CronConfig } from '@/components/config/CronConfig';
7
- import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
1
+ import { useEffect } from "react";
2
+ import type { Dispatch, MutableRefObject, SetStateAction } from "react";
3
+ import { ChatSidebar } from "@/components/chat/ChatSidebar";
4
+ import { ChatConversationPanel } from "@/components/chat/chat-conversation-panel";
5
+ import { AgentsPage } from "@/components/agents/agents-page";
6
+ import { CronConfig } from "@/components/config/CronConfig";
7
+ import { MarketplacePage } from "@/components/marketplace/marketplace-page";
8
8
 
9
- export type MainPanelView = 'chat' | 'cron' | 'skills' | 'agents';
9
+ export type MainPanelView = "chat" | "cron" | "skills" | "agents";
10
10
 
11
11
  export type ChatPageProps = {
12
12
  view: MainPanelView;
@@ -32,7 +32,7 @@ export function useChatSessionSync(params: UseChatSessionSyncParams): void {
32
32
  } = params;
33
33
 
34
34
  useEffect(() => {
35
- if (view !== 'chat') {
35
+ if (view !== "chat") {
36
36
  return;
37
37
  }
38
38
  if (routeSessionKey) {
@@ -45,7 +45,13 @@ export function useChatSessionSync(params: UseChatSessionSyncParams): void {
45
45
  setSelectedSessionKey(null);
46
46
  resetStreamState();
47
47
  }
48
- }, [resetStreamState, routeSessionKey, selectedSessionKey, setSelectedSessionKey, view]);
48
+ }, [
49
+ resetStreamState,
50
+ routeSessionKey,
51
+ selectedSessionKey,
52
+ setSelectedSessionKey,
53
+ view,
54
+ ]);
49
55
 
50
56
  useEffect(() => {
51
57
  selectedSessionKeyRef.current = selectedSessionKey;
@@ -62,17 +68,17 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
62
68
  <div className="h-full flex">
63
69
  <ChatSidebar />
64
70
 
65
- {view === 'chat' ? (
71
+ {view === "chat" ? (
66
72
  <ChatConversationPanel />
67
73
  ) : (
68
74
  <section className="flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
69
- {view === 'cron' ? (
75
+ {view === "cron" ? (
70
76
  <div className="h-full overflow-auto custom-scrollbar">
71
77
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
72
78
  <CronConfig />
73
79
  </div>
74
80
  </div>
75
- ) : view === 'agents' ? (
81
+ ) : view === "agents" ? (
76
82
  <div className="h-full overflow-auto custom-scrollbar">
77
83
  <div className="mx-auto w-full max-w-[min(1180px,100%)] px-6 py-5">
78
84
  <AgentsPage />
@@ -0,0 +1,46 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ChatSessionTypeOptionItem } from "@/components/chat/chat-session-type-option-item";
4
+
5
+ describe("ChatSessionTypeOptionItem", () => {
6
+ it("renders a runtime icon image when the session type option provides an app resource URI", () => {
7
+ render(
8
+ <ChatSessionTypeOptionItem
9
+ option={{
10
+ value: "codex",
11
+ label: "Codex",
12
+ icon: {
13
+ kind: "image",
14
+ src: "app://runtime-icons/codex-openai.svg",
15
+ alt: "Codex",
16
+ },
17
+ ready: true,
18
+ }}
19
+ onSelect={vi.fn()}
20
+ />,
21
+ );
22
+
23
+ const runtimeIcon = screen.getByRole("img", { name: "Codex logo" });
24
+ expect(runtimeIcon.getAttribute("src")).toBe("/runtime-icons/codex-openai.svg");
25
+ });
26
+
27
+ it("keeps ready options visually compact without repeating helper copy", () => {
28
+ render(
29
+ <ChatSessionTypeOptionItem
30
+ option={{
31
+ value: "claude",
32
+ label: "Claude",
33
+ icon: {
34
+ kind: "image",
35
+ src: "app://runtime-icons/claude.ico",
36
+ alt: "Claude",
37
+ },
38
+ ready: true,
39
+ }}
40
+ onSelect={vi.fn()}
41
+ />,
42
+ );
43
+
44
+ expect(screen.getAllByText("Ready")).toHaveLength(1);
45
+ });
46
+ });
@@ -0,0 +1,68 @@
1
+ import type { ChatInputSnapshot } from "@/components/chat/stores/chat-input.store";
2
+ import { SessionContextIconNode } from "@/components/common/session-context-icon";
3
+ import { t } from "@/lib/i18n";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ type SessionTypeOption = ChatInputSnapshot["sessionTypeOptions"][number];
7
+
8
+ export function ChatSessionTypeOptionItem(props: {
9
+ option: SessionTypeOption;
10
+ onSelect: () => void;
11
+ }) {
12
+ const { option, onSelect } = props;
13
+ const helperText =
14
+ option.ready === false
15
+ ? option.reasonMessage?.trim() || t("statusSetup")
16
+ : null;
17
+
18
+ return (
19
+ <button
20
+ type="button"
21
+ onClick={onSelect}
22
+ className="w-full rounded-2xl px-3 py-2.5 text-left transition-colors hover:bg-gray-50"
23
+ >
24
+ <div className="flex items-start gap-3">
25
+ <div className="flex min-w-0 flex-1 items-start gap-2.5">
26
+ {option.icon?.src ? (
27
+ <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center pt-0.5">
28
+ <SessionContextIconNode
29
+ icon={{
30
+ kind: "runtime-image",
31
+ src: option.icon.src,
32
+ alt: option.icon.alt ?? null,
33
+ name: option.label,
34
+ }}
35
+ />
36
+ </span>
37
+ ) : null}
38
+ <div className="min-w-0 flex-1">
39
+ <div className="flex items-center justify-between gap-3">
40
+ <div className="truncate text-[13px] font-semibold text-gray-900">
41
+ {option.label}
42
+ </div>
43
+ <span
44
+ className={cn(
45
+ "inline-flex shrink-0 items-center gap-1.5 text-[11px] font-medium",
46
+ option.ready === false ? "text-amber-700" : "text-emerald-600",
47
+ )}
48
+ >
49
+ <span
50
+ className={cn(
51
+ "h-1.5 w-1.5 rounded-full",
52
+ option.ready === false ? "bg-amber-500" : "bg-emerald-500",
53
+ )}
54
+ />
55
+ {option.ready === false ? t("statusSetup") : t("statusReady")}
56
+ </span>
57
+ </div>
58
+ {helperText ? (
59
+ <div className="mt-1 pr-4 text-[11px] leading-4 text-gray-500">
60
+ {helperText}
61
+ </div>
62
+ ) : null}
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </button>
67
+ );
68
+ }