@nextclaw/ui 0.14.4 → 0.15.1

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 (222) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/assets/api-b1R9VdeH.js +13 -0
  3. package/dist/assets/appearance-settings-page-D2O9pch9.js +1 -0
  4. package/dist/assets/book-open-B77lQ2Tg.js +1 -0
  5. package/dist/assets/channels-list-page-DcsIQc6A.js +8 -0
  6. package/dist/assets/chat-page-CZ7WxPir.js +1 -0
  7. package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-BWQ6_Raq.js} +1 -1
  8. package/dist/assets/confirm-dialog-C-NXSioX.js +5 -0
  9. package/dist/assets/desktop-update-config-usHpXugP.js +1 -0
  10. package/dist/assets/{dist-CikkhOlq.js → dist-YtoqU-AT.js} +1 -1
  11. package/dist/assets/{dist-PVA_xL1D.js → dist-jdGmbb6w.js} +1 -1
  12. package/dist/assets/doc-browser-B_Tf9n70.js +1 -0
  13. package/dist/assets/doc-browser-BdZPFOVa.js +1 -0
  14. package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-DwokemUn.js} +1 -1
  15. package/dist/assets/es2015-bNpL2LKy.js +41 -0
  16. package/dist/assets/{external-link-DVJIQf_G.js → external-link-BM6fk8q-.js} +1 -1
  17. package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
  18. package/dist/assets/i18n-provider-CntWolRA.js +1 -0
  19. package/dist/assets/index-BIcEw90u.js +104 -0
  20. package/dist/assets/index-BiNSTKTU.css +1 -0
  21. package/dist/assets/loader-circle-MBgNSVIH.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +1 -0
  24. package/dist/assets/model-config-page-Di0ySJD2.js +1 -0
  25. package/dist/assets/plus-vDqW5Kxp.js +1 -0
  26. package/dist/assets/provider-scoped-model-input-Cm_a4GHf.js +1 -0
  27. package/dist/assets/providers-config-page-BAf2W0MV.js +1 -0
  28. package/dist/assets/{react-VNqT5Zp7.js → react-BN8uzPnp.js} +1 -1
  29. package/dist/assets/refresh-cw-BpVcFPmJ.js +1 -0
  30. package/dist/assets/remote-CLQ0IBM8.js +1 -0
  31. package/dist/assets/rotate-cw-CvNBv7_w.js +1 -0
  32. package/dist/assets/runtime-config-page-CflvvoDc.js +1 -0
  33. package/dist/assets/{save-BOJ477H-.js → save-BpFW5fpk.js} +1 -1
  34. package/dist/assets/{search-BEti5beA.js → search-D0uO7enf.js} +1 -1
  35. package/dist/assets/search-config-page-D0klFUXd.js +1 -0
  36. package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-dVZ631Wp.js} +2 -2
  37. package/dist/assets/security-config-C5gnlkuj.js +1 -0
  38. package/dist/assets/select-Bh-CrA4p.js +1 -0
  39. package/dist/assets/setting-row-BJfCHRcb.js +1 -0
  40. package/dist/assets/settings-2-Blzi1vr6.js +1 -0
  41. package/dist/assets/side-dock-ZdaSrQVB.js +3 -0
  42. package/dist/assets/skeleton-BLlEzebZ.js +1 -0
  43. package/dist/assets/switch-CFLJFNSd.js +1 -0
  44. package/dist/assets/tag-chip-CzGihiQx.js +1 -0
  45. package/dist/assets/tooltip-CdcrBjrC.js +1 -0
  46. package/dist/assets/use-config-CVPgsBmU.js +1 -0
  47. package/dist/assets/use-confirm-dialog-CSeBk9SF.js +1 -0
  48. package/dist/assets/x-BF2KQ--V.js +1 -0
  49. package/dist/index.html +53 -17
  50. package/index.html +25 -2
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
  53. package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
  54. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
  55. package/src/app/components/layout/app-layout.tsx +10 -3
  56. package/src/app/components/layout/sidebar-items.tsx +8 -8
  57. package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
  58. package/src/app/components/layout/sidebar.tsx +21 -28
  59. package/src/app/configs/app-navigation.config.ts +11 -5
  60. package/src/app/index.tsx +8 -0
  61. package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
  62. package/src/app/managers/viewport-layout.manager.ts +39 -0
  63. package/src/app/presenters/app.presenter.ts +25 -2
  64. package/src/app/styles/design-system.css +91 -4
  65. package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
  66. package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
  67. package/src/features/agents/components/agent-details-dialog.tsx +398 -0
  68. package/src/features/agents/components/agent-dialogs.tsx +20 -47
  69. package/src/features/agents/components/agents-page.tsx +76 -10
  70. package/src/features/agents/types/agent-form.types.ts +21 -0
  71. package/src/features/agents/utils/agent-form.utils.ts +39 -0
  72. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
  73. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +132 -1
  74. package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
  75. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +8 -0
  76. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
  77. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
  78. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
  79. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
  80. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +66 -2
  81. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
  82. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
  83. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
  84. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
  85. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
  86. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
  87. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
  88. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
  89. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
  90. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
  91. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
  92. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
  93. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +167 -1
  94. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
  95. package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
  96. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
  97. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
  98. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
  99. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
  100. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
  101. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
  102. package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
  103. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +62 -1
  104. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
  105. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
  106. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  107. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
  108. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
  109. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  110. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
  111. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
  112. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
  113. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
  114. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
  115. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +304 -115
  116. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
  117. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
  118. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +64 -5
  119. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
  120. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
  121. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +126 -1
  122. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
  123. package/src/features/chat/managers/chat-thread.manager.ts +15 -3
  124. package/src/features/chat/managers/chat-ui.manager.ts +5 -1
  125. package/src/features/chat/presenters/chat.presenter.ts +2 -1
  126. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  127. package/src/features/chat/stores/chat-thread.store.ts +6 -64
  128. package/src/features/chat/types/chat-message.types.ts +4 -0
  129. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
  130. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
  131. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  132. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
  133. package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
  134. package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
  135. package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
  136. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
  137. package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
  138. package/src/features/marketplace/index.ts +3 -0
  139. package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
  140. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
  141. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
  142. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
  143. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
  144. package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
  145. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
  146. package/src/features/side-dock/components/side-dock.tsx +100 -13
  147. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
  148. package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
  149. package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
  150. package/src/features/side-dock/stores/side-dock.store.ts +9 -0
  151. package/src/features/side-dock/types/side-dock.types.ts +10 -3
  152. package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
  153. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
  154. package/src/features/system-status/components/desktop-update-config.tsx +91 -2
  155. package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
  156. package/src/index.css +501 -27
  157. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  158. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
  159. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
  160. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
  161. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
  162. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
  163. package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
  164. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
  165. package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
  166. package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
  167. package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
  168. package/src/shared/components/ui/dialog.tsx +2 -2
  169. package/src/shared/components/ui/popover.tsx +1 -1
  170. package/src/shared/components/ui/select.tsx +1 -1
  171. package/src/shared/components/ui/switch.tsx +4 -3
  172. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
  173. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
  174. package/src/shared/components/ui/tooltip.tsx +1 -1
  175. package/src/shared/lib/api/ncp-session.types.ts +1 -1
  176. package/src/shared/lib/api/types.ts +12 -3
  177. package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
  178. package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
  179. package/src/shared/lib/i18n/locales/en-US/chat.json +18 -6
  180. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  181. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
  182. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
  183. package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
  184. package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
  185. package/src/shared/lib/i18n/locales/zh-CN/chat.json +18 -6
  186. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  187. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
  188. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
  189. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
  190. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
  191. package/src/shared/lib/session-project/index.ts +1 -1
  192. package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
  193. package/src/shared/lib/theme/index.ts +51 -13
  194. package/dist/assets/api-BP_8IaxB.js +0 -13
  195. package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
  196. package/dist/assets/chat-page-BRB0N5WW.js +0 -105
  197. package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
  198. package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
  199. package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
  200. package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
  201. package/dist/assets/ellipsis-y6vDtq4M.js +0 -1
  202. package/dist/assets/index-BiE-K8-q.css +0 -1
  203. package/dist/assets/index-D9BAE_L_.js +0 -105
  204. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
  205. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
  206. package/dist/assets/model-config-page-DotaTUHb.js +0 -1
  207. package/dist/assets/plus-BbZqquJe.js +0 -1
  208. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
  209. package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
  210. package/dist/assets/remote-B8TiUfwW.js +0 -1
  211. package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
  212. package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
  213. package/dist/assets/search-config-page-DGj5am03.js +0 -1
  214. package/dist/assets/select-hoj8dK4K.js +0 -41
  215. package/dist/assets/tag-chip-T8-h-die.js +0 -1
  216. package/dist/assets/use-config-CjRV2hwL.js +0 -1
  217. package/dist/assets/x-D9PXs_xP.js +0 -1
  218. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
  219. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
  220. package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
  221. /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
  222. /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
@@ -1,6 +1,12 @@
1
- import { Fragment } from "react";
1
+ import { Fragment, useState } from "react";
2
+ import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
2
3
  import { ChevronRight, FileCode2, FolderTree } from "lucide-react";
3
- import type { WorkspaceFileBreadcrumbViewModel } from "@/shared/lib/session-project";
4
+ import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
5
+ import { WorkspaceBreadcrumbBrowser } from "./chat-session-workspace-file-breadcrumb-browser";
6
+ import type {
7
+ WorkspaceFileBreadcrumbSegmentViewModel,
8
+ WorkspaceFileBreadcrumbViewModel,
9
+ } from "@/shared/lib/session-project";
4
10
  import { t } from "@/shared/lib/i18n";
5
11
  import { cn } from "@/shared/lib/utils";
6
12
 
@@ -19,7 +25,69 @@ function WorkspaceBreadcrumbMetaChip({ tone = "neutral", value }: { tone?: "neut
19
25
  );
20
26
  }
21
27
 
22
- export function ChatSessionWorkspaceFileBreadcrumbs({ breadcrumb }: { breadcrumb: WorkspaceFileBreadcrumbViewModel }) {
28
+ function WorkspaceBreadcrumbSegmentButton({
29
+ onFileOpen,
30
+ segment,
31
+ }: {
32
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
33
+ segment: WorkspaceFileBreadcrumbSegmentViewModel;
34
+ }) {
35
+ const [open, setOpen] = useState(false);
36
+ const [browsePath, setBrowsePath] = useState<string | null>(segment.browsePath);
37
+ const handleOpenChange = (nextOpen: boolean) => {
38
+ if (nextOpen) {
39
+ setBrowsePath(segment.browsePath);
40
+ }
41
+ setOpen(nextOpen);
42
+ };
43
+
44
+ return (
45
+ <Popover open={open} onOpenChange={handleOpenChange}>
46
+ <PopoverTrigger asChild>
47
+ <button
48
+ type="button"
49
+ aria-current={segment.isCurrent ? "page" : undefined}
50
+ className={cn(
51
+ "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none transition-colors hover:bg-gray-200/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
52
+ segment.kind === "workspace"
53
+ ? "bg-primary/8 text-primary hover:bg-primary/12"
54
+ : segment.isCurrent
55
+ ? "bg-gray-200/70 text-gray-900"
56
+ : "text-gray-500",
57
+ )}
58
+ disabled={!segment.browsePath}
59
+ >
60
+ {segment.kind === "workspace" ? (
61
+ <FolderTree className="h-3 w-3 shrink-0" />
62
+ ) : segment.isCurrent ? (
63
+ <FileCode2 className="h-3 w-3 shrink-0" />
64
+ ) : null}
65
+ <span>{segment.label}</span>
66
+ </button>
67
+ </PopoverTrigger>
68
+ <PopoverContent
69
+ data-testid="workspace-breadcrumb-popover"
70
+ className="w-[22rem] rounded-md p-0"
71
+ align="start"
72
+ >
73
+ <WorkspaceBreadcrumbBrowser
74
+ browsePath={browsePath}
75
+ onBrowsePathChange={setBrowsePath}
76
+ onClose={() => setOpen(false)}
77
+ onFileOpen={onFileOpen}
78
+ />
79
+ </PopoverContent>
80
+ </Popover>
81
+ );
82
+ }
83
+
84
+ export function ChatSessionWorkspaceFileBreadcrumbs({
85
+ breadcrumb,
86
+ onFileOpen,
87
+ }: {
88
+ breadcrumb: WorkspaceFileBreadcrumbViewModel;
89
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
90
+ }) {
23
91
  return (
24
92
  <div
25
93
  data-testid="workspace-file-breadcrumbs"
@@ -33,23 +101,10 @@ export function ChatSessionWorkspaceFileBreadcrumbs({ breadcrumb }: { breadcrumb
33
101
  <div className="flex min-w-0 flex-1 items-center gap-1 pr-1">
34
102
  {breadcrumb.segments.map((segment, index) => (
35
103
  <Fragment key={segment.key}>
36
- <span
37
- className={cn(
38
- "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none",
39
- segment.kind === "workspace"
40
- ? "bg-primary/8 text-primary"
41
- : segment.isCurrent
42
- ? "bg-gray-200/70 text-gray-900"
43
- : "text-gray-500",
44
- )}
45
- >
46
- {segment.kind === "workspace" ? (
47
- <FolderTree className="h-3 w-3 shrink-0" />
48
- ) : segment.isCurrent ? (
49
- <FileCode2 className="h-3 w-3 shrink-0" />
50
- ) : null}
51
- <span>{segment.label}</span>
52
- </span>
104
+ <WorkspaceBreadcrumbSegmentButton
105
+ segment={segment}
106
+ onFileOpen={onFileOpen}
107
+ />
53
108
  {index < breadcrumb.segments.length - 1 ? (
54
109
  <ChevronRight className="h-3 w-3 shrink-0 text-gray-300" />
55
110
  ) : null}
@@ -1,9 +1,14 @@
1
1
  import { useMemo } from "react";
2
- import type { ChatFileOpenActionViewModel, ChatFileOperationBlockViewModel } from "@nextclaw/agent-chat-ui";
2
+ import type {
3
+ ChatFileOpenActionViewModel,
4
+ ChatFileOperationBlockViewModel,
5
+ ChatFilePreviewViewer,
6
+ } from "@nextclaw/agent-chat-ui";
3
7
  import { ChatMessageMarkdown, FileOperationCodeSurface } from "@nextclaw/agent-chat-ui";
4
8
  import type { ChatWorkspaceFileTab } from "@/features/chat/stores/chat-thread.store";
5
9
  import { ChatSessionWorkspaceFileBreadcrumbs } from "./chat-session-workspace-file-breadcrumbs";
6
10
  import { useServerPathRead } from "@/shared/hooks/use-server-path-read";
11
+ import { buildServerPathContentUrl } from "@/shared/lib/api";
7
12
  import { buildLineDiff, buildPreviewLines } from "@/features/chat/features/message/utils/file-operation/line-builder.utils";
8
13
  import { t } from "@/shared/lib/i18n";
9
14
  import { buildWorkspaceFileBreadcrumb } from "@/shared/lib/session-project";
@@ -19,12 +24,20 @@ function inferPreviewKind(params: {
19
24
  return /\.mdx?$/i.test(params.path) ? "markdown" : "text";
20
25
  }
21
26
 
27
+ function resolveFilePreviewViewer(params: {
28
+ path: string;
29
+ viewer?: ChatFilePreviewViewer | null;
30
+ }): "source" | "rendered" {
31
+ return params.viewer === "rendered" && /\.html?$/i.test(params.path) ? "rendered" : "source";
32
+ }
33
+
22
34
  function buildPreviewBlock(params: {
23
35
  path: string;
24
36
  text: string;
37
+ languageHint?: string | null;
25
38
  line?: number | null;
26
39
  }): ChatFileOperationBlockViewModel {
27
- const { line, path, text } = params;
40
+ const { languageHint, line, path, text } = params;
28
41
  const startLine = line ?? 1;
29
42
  return {
30
43
  key: `preview:${path}`,
@@ -37,6 +50,7 @@ function buildPreviewBlock(params: {
37
50
  newStartLine: startLine,
38
51
  }),
39
52
  rawText: text,
53
+ languageHint: languageHint ?? null,
40
54
  oldStartLine: startLine,
41
55
  newStartLine: startLine,
42
56
  };
@@ -130,18 +144,38 @@ function WorkspaceCodeSurface({
130
144
  return <div className="h-full overflow-auto custom-scrollbar bg-white"><FileOperationCodeSurface block={block} layout="workspace" /></div>;
131
145
  }
132
146
 
147
+ function WorkspaceHtmlRenderedPreview({
148
+ src,
149
+ }: {
150
+ src: string;
151
+ }) {
152
+ return (
153
+ <iframe
154
+ allowFullScreen
155
+ className="h-full w-full border-0 bg-white"
156
+ data-testid="workspace-html-preview"
157
+ src={src}
158
+ title={t("chatWorkspaceHtmlPreviewTitle")}
159
+ />
160
+ );
161
+ }
162
+
133
163
  function WorkspacePreviewBody({
134
164
  onFileOpen,
135
165
  previewBlock,
136
166
  previewKind,
137
167
  previewQuery,
138
168
  previewText,
169
+ previewUrl,
170
+ previewViewer,
139
171
  }: {
140
172
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
141
173
  previewBlock: ChatFileOperationBlockViewModel | null;
142
174
  previewKind: "text" | "markdown" | "binary";
143
175
  previewQuery: ReturnType<typeof useServerPathRead>;
144
176
  previewText: string | null;
177
+ previewUrl: string | null;
178
+ previewViewer: "source" | "rendered";
145
179
  }) {
146
180
  if (previewQuery.isLoading && !previewBlock) {
147
181
  return <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingFile")} />;
@@ -166,6 +200,10 @@ function WorkspacePreviewBody({
166
200
  );
167
201
  }
168
202
 
203
+ if (previewViewer === "rendered" && previewUrl) {
204
+ return <WorkspaceHtmlRenderedPreview src={previewUrl} />;
205
+ }
206
+
169
207
  if (previewKind === "markdown" && previewText) {
170
208
  return (
171
209
  <div className="h-full overflow-auto custom-scrollbar px-5 py-4">
@@ -218,6 +256,15 @@ export function ChatSessionWorkspaceFilePreview({
218
256
  path: previewQuery.data?.resolvedPath ?? file.path,
219
257
  serverKind: previewQuery.data?.kind,
220
258
  });
259
+ const resolvedPath = previewQuery.data?.resolvedPath ?? file.path;
260
+ const previewViewer = resolveFilePreviewViewer({
261
+ path: resolvedPath,
262
+ viewer: file.previewViewer,
263
+ });
264
+ const previewUrl =
265
+ previewViewer === "rendered" && previewQuery.data?.resolvedPath
266
+ ? buildServerPathContentUrl(previewQuery.data.resolvedPath)
267
+ : null;
221
268
  const previewBlock = useMemo(() => {
222
269
  if (!isPreviewMode || !previewText) {
223
270
  return null;
@@ -225,10 +272,17 @@ export function ChatSessionWorkspaceFilePreview({
225
272
  return buildPreviewBlock({
226
273
  path: previewQuery.data?.resolvedPath ?? file.path,
227
274
  text: previewText,
275
+ languageHint: previewQuery.data?.languageHint ?? null,
228
276
  line: file.line,
229
277
  });
230
- }, [file.line, file.path, isPreviewMode, previewQuery.data?.resolvedPath, previewText]);
231
- const resolvedPath = previewQuery.data?.resolvedPath ?? file.path;
278
+ }, [
279
+ file.line,
280
+ file.path,
281
+ isPreviewMode,
282
+ previewQuery.data?.languageHint,
283
+ previewQuery.data?.resolvedPath,
284
+ previewText,
285
+ ]);
232
286
  const isTruncated = Boolean(previewQuery.data?.truncated);
233
287
  const breadcrumbBasePath = sessionProjectRoot ?? sessionWorkingDir;
234
288
  const breadcrumb = useMemo(
@@ -245,7 +299,10 @@ export function ChatSessionWorkspaceFilePreview({
245
299
 
246
300
  return (
247
301
  <div className="flex h-full min-h-0 flex-col bg-white">
248
- <ChatSessionWorkspaceFileBreadcrumbs breadcrumb={breadcrumb} />
302
+ <ChatSessionWorkspaceFileBreadcrumbs
303
+ breadcrumb={breadcrumb}
304
+ onFileOpen={onFileOpen}
305
+ />
249
306
 
250
307
  <div className="flex-1 min-h-0 overflow-hidden">
251
308
  {file.viewMode === "diff" ? (
@@ -257,6 +314,8 @@ export function ChatSessionWorkspaceFilePreview({
257
314
  previewKind={previewKind}
258
315
  previewQuery={previewQuery}
259
316
  previewText={previewText}
317
+ previewUrl={previewUrl}
318
+ previewViewer={previewViewer}
260
319
  />
261
320
  )}
262
321
  </div>
@@ -158,4 +158,50 @@ describe("buildWorkspaceTabsViewModel", () => {
158
158
  active: true,
159
159
  });
160
160
  });
161
+
162
+ it("keeps distinct source and rendered file tabs without collapsing by path", () => {
163
+ const renderedTab = {
164
+ key: "parent::preview:rendered::demo.html",
165
+ parentSessionKey: "parent-1",
166
+ path: "demo.html",
167
+ label: "demo.html",
168
+ viewMode: "preview" as const,
169
+ previewViewer: "rendered" as const,
170
+ };
171
+ const sourceTab = {
172
+ key: "parent::preview::demo.html",
173
+ parentSessionKey: "parent-1",
174
+ path: "demo.html",
175
+ label: "demo.html",
176
+ viewMode: "preview" as const,
177
+ previewViewer: "source" as const,
178
+ };
179
+
180
+ const tabs = buildWorkspaceTabsViewModel({
181
+ resolvedChildTabs: [],
182
+ activeSideChatDraft: null,
183
+ workspaceFileTabs: [sourceTab, renderedTab],
184
+ sessionCronJobCount: 0,
185
+ activeSelection: {
186
+ kind: "file",
187
+ file: sourceTab,
188
+ },
189
+ optimisticReadAtBySessionKey: {},
190
+ onSelectSession: vi.fn(),
191
+ onSelectFile: vi.fn(),
192
+ onCloseFile: vi.fn(),
193
+ onSelectCronJobs: vi.fn(),
194
+ });
195
+
196
+ expect(tabs).toEqual([
197
+ expect.objectContaining({
198
+ key: "file:parent::preview::demo.html",
199
+ active: true,
200
+ }),
201
+ expect.objectContaining({
202
+ key: "file:parent::preview:rendered::demo.html",
203
+ active: false,
204
+ }),
205
+ ]);
206
+ });
161
207
  });
@@ -0,0 +1,83 @@
1
+ import type { ChatFilePreviewViewer } from '@nextclaw/agent-chat-ui';
2
+ import type { ChatWorkspaceFileTab } from '@/features/chat/stores/chat-thread.store';
3
+
4
+ function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return !!value && typeof value === 'object';
6
+ }
7
+
8
+ function normalizeOptionalString(value: unknown): string | null {
9
+ return typeof value === 'string' && value.trim().length > 0
10
+ ? value.trim()
11
+ : null;
12
+ }
13
+
14
+ function normalizeOptionalNumber(value: unknown): number | null {
15
+ return typeof value === 'number' && Number.isFinite(value)
16
+ ? value
17
+ : null;
18
+ }
19
+
20
+ function normalizeOptionalText(value: unknown): string | null {
21
+ return typeof value === 'string' ? value : null;
22
+ }
23
+
24
+ function normalizeFilePreviewViewer(value: unknown): ChatFilePreviewViewer | null {
25
+ return value === 'auto' || value === 'source' || value === 'rendered'
26
+ ? value
27
+ : null;
28
+ }
29
+
30
+ export function normalizePersistedWorkspaceFileTab(
31
+ value: unknown,
32
+ ): ChatWorkspaceFileTab | null {
33
+ if (
34
+ !isRecord(value)
35
+ || typeof value.key !== 'string'
36
+ || typeof value.path !== 'string'
37
+ || (value.viewMode !== 'preview' && value.viewMode !== 'diff')
38
+ ) {
39
+ return null;
40
+ }
41
+ const key = value.key.trim();
42
+ const path = value.path.trim();
43
+ if (!key || !path) {
44
+ return null;
45
+ }
46
+ return {
47
+ key,
48
+ parentSessionKey: normalizeOptionalString(value.parentSessionKey),
49
+ path,
50
+ label: normalizeOptionalString(value.label),
51
+ viewMode: value.viewMode,
52
+ previewViewer: normalizeFilePreviewViewer(value.previewViewer),
53
+ line: normalizeOptionalNumber(value.line),
54
+ column: normalizeOptionalNumber(value.column),
55
+ rawText: normalizeOptionalText(value.rawText),
56
+ beforeText: normalizeOptionalText(value.beforeText),
57
+ afterText: normalizeOptionalText(value.afterText),
58
+ patchText: normalizeOptionalText(value.patchText),
59
+ oldStartLine: normalizeOptionalNumber(value.oldStartLine),
60
+ newStartLine: normalizeOptionalNumber(value.newStartLine),
61
+ };
62
+ }
63
+
64
+ export function toPersistedWorkspaceFileTab(
65
+ tab: ChatWorkspaceFileTab,
66
+ ): ChatWorkspaceFileTab {
67
+ return {
68
+ key: tab.key,
69
+ parentSessionKey: tab.parentSessionKey,
70
+ path: tab.path,
71
+ label: tab.label,
72
+ viewMode: tab.viewMode,
73
+ previewViewer: tab.previewViewer,
74
+ line: tab.line,
75
+ column: tab.column,
76
+ rawText: tab.rawText,
77
+ beforeText: tab.beforeText,
78
+ afterText: tab.afterText,
79
+ patchText: tab.patchText,
80
+ oldStartLine: tab.oldStartLine,
81
+ newStartLine: tab.newStartLine,
82
+ };
83
+ }
@@ -155,6 +155,40 @@ describe('ChatThreadManager', () => {
155
155
  expect(uiManager.goToSession).not.toHaveBeenCalled();
156
156
  });
157
157
 
158
+ it('notifies the app layout coordinator after opening the workspace panel', () => {
159
+ const onWorkspacePanelOpened = vi.fn();
160
+ const manager = new ChatThreadManager(
161
+ createUiManager(),
162
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
163
+ onWorkspacePanelOpened,
164
+ );
165
+
166
+ manager.openFilePreview({
167
+ path: 'README.md',
168
+ label: 'README.md',
169
+ viewMode: 'preview',
170
+ });
171
+
172
+ expect(onWorkspacePanelOpened).toHaveBeenCalledTimes(1);
173
+ });
174
+
175
+ it('does not notify the app layout coordinator when no workspace panel opens', () => {
176
+ const onWorkspacePanelOpened = vi.fn();
177
+ const manager = new ChatThreadManager(
178
+ createUiManager(),
179
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
180
+ onWorkspacePanelOpened,
181
+ );
182
+
183
+ manager.openFilePreview({
184
+ path: ' ',
185
+ label: 'README.md',
186
+ viewMode: 'preview',
187
+ });
188
+
189
+ expect(onWorkspacePanelOpened).not.toHaveBeenCalled();
190
+ });
191
+
158
192
  it('opens a side chat draft beside the selected parent session', () => {
159
193
  const uiManager = createUiManager({
160
194
  goToSession: vi.fn(),
@@ -293,6 +327,94 @@ describe('ChatThreadManager', () => {
293
327
  ]),
294
328
  );
295
329
  });
330
+
331
+ });
332
+
333
+ describe('ChatThreadManager workspace file tabs', () => {
334
+ it('opens HTML previews in source mode when the viewer is omitted or auto', () => {
335
+ const manager = new ChatThreadManager(
336
+ createUiManager(),
337
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
338
+ );
339
+
340
+ manager.openFilePreview({
341
+ path: 'demo.html',
342
+ label: 'demo.html',
343
+ viewMode: 'preview',
344
+ });
345
+
346
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
347
+ activeWorkspaceFileKey: 'parent-session-1::preview::demo.html',
348
+ workspaceFileTabs: [
349
+ {
350
+ key: 'parent-session-1::preview::demo.html',
351
+ path: 'demo.html',
352
+ viewMode: 'preview',
353
+ previewViewer: 'source',
354
+ },
355
+ ],
356
+ });
357
+
358
+ manager.openFilePreview({
359
+ path: 'auto.html',
360
+ label: 'auto.html',
361
+ viewMode: 'preview',
362
+ previewViewer: 'auto',
363
+ });
364
+
365
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
366
+ activeWorkspaceFileKey: 'parent-session-1::preview::auto.html',
367
+ workspaceFileTabs: [
368
+ {
369
+ key: 'parent-session-1::preview::auto.html',
370
+ path: 'auto.html',
371
+ viewMode: 'preview',
372
+ previewViewer: 'source',
373
+ },
374
+ expect.objectContaining({
375
+ key: 'parent-session-1::preview::demo.html',
376
+ }),
377
+ ],
378
+ });
379
+ });
380
+
381
+ it('keeps source and rendered previews for the same file as separate workspace tabs', () => {
382
+ const manager = new ChatThreadManager(
383
+ createUiManager(),
384
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
385
+ );
386
+
387
+ manager.openFilePreview({
388
+ path: 'demo.html',
389
+ label: 'demo.html',
390
+ viewMode: 'preview',
391
+ previewViewer: 'source',
392
+ });
393
+ manager.openFilePreview({
394
+ path: 'demo.html',
395
+ label: 'demo.html',
396
+ viewMode: 'preview',
397
+ previewViewer: 'rendered',
398
+ });
399
+
400
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
401
+ activeWorkspaceFileKey: 'parent-session-1::preview:rendered::demo.html',
402
+ workspaceFileTabs: [
403
+ {
404
+ key: 'parent-session-1::preview:rendered::demo.html',
405
+ path: 'demo.html',
406
+ viewMode: 'preview',
407
+ previewViewer: 'rendered',
408
+ },
409
+ {
410
+ key: 'parent-session-1::preview::demo.html',
411
+ path: 'demo.html',
412
+ viewMode: 'preview',
413
+ previewViewer: 'source',
414
+ },
415
+ ],
416
+ });
417
+ });
296
418
  });
297
419
 
298
420
  describe('ChatThreadManager workspace navigation', () => {
@@ -513,6 +635,7 @@ describe('ChatThreadManager showContent', () => {
513
635
  payload: {
514
636
  path: 'docs/example.md',
515
637
  line: 5,
638
+ viewer: 'rendered',
516
639
  },
517
640
  },
518
641
  title: 'Example',
@@ -522,12 +645,14 @@ describe('ChatThreadManager showContent', () => {
522
645
 
523
646
  expect(useChatThreadStore.getState().snapshot).toMatchObject({
524
647
  activeWorkspacePanelKind: 'file',
525
- activeWorkspaceFileKey: 'parent-session-1::preview::docs/example.md',
648
+ activeWorkspaceFileKey: 'parent-session-1::preview:rendered::docs/example.md',
526
649
  workspaceFileTabs: [
527
650
  expect.objectContaining({
651
+ key: 'parent-session-1::preview:rendered::docs/example.md',
528
652
  path: 'docs/example.md',
529
653
  label: 'Example',
530
654
  line: 5,
655
+ previewViewer: 'rendered',
531
656
  viewMode: 'preview',
532
657
  }),
533
658
  ],
@@ -68,6 +68,8 @@ describe('ChatUiManager content display', () => {
68
68
  });
69
69
 
70
70
  expect(docBrowserManager.open).toHaveBeenCalledWith('https://example.com/read', {
71
+ dedupeKey: 'browser:https://example.com/read',
72
+ kind: 'content',
71
73
  title: 'Example URL',
72
74
  });
73
75
  });
@@ -44,6 +44,7 @@ export class ChatThreadManager {
44
44
  constructor(
45
45
  private uiManager: ChatUiManager,
46
46
  private sessionListManager: ChatSessionListManager,
47
+ private readonly onWorkspacePanelOpened?: () => void,
47
48
  ) {}
48
49
 
49
50
  private hasSnapshotChanges = (patch: Partial<ChatThreadSnapshot>): boolean => {
@@ -110,14 +111,20 @@ export class ChatThreadManager {
110
111
  return null;
111
112
  }
112
113
  const normalizedParentSessionKey = parentSessionKey?.trim() || null;
113
- const key =
114
- `${normalizedParentSessionKey ?? 'draft'}::${action.viewMode}::${normalizedPath}`;
114
+ const previewViewer =
115
+ action.viewMode === 'preview' &&
116
+ action.previewViewer !== 'rendered' &&
117
+ (action.previewViewer === 'auto' || action.previewViewer === 'source' || /\.html?$/i.test(normalizedPath))
118
+ ? 'source'
119
+ : action.previewViewer ?? null;
120
+ const tabViewIdentity = action.viewMode === 'preview' && previewViewer === 'rendered' ? 'preview:rendered' : action.viewMode;
115
121
  return {
116
- key,
122
+ key: `${normalizedParentSessionKey ?? 'draft'}::${tabViewIdentity}::${normalizedPath}`,
117
123
  parentSessionKey: normalizedParentSessionKey,
118
124
  path: normalizedPath,
119
125
  label: action.label?.trim() || null,
120
126
  viewMode: action.viewMode,
127
+ previewViewer,
121
128
  line: action.line ?? null,
122
129
  column: action.column ?? null,
123
130
  rawText: action.rawText ?? null,
@@ -265,6 +272,7 @@ export class ChatThreadManager {
265
272
  useChatThreadStore.getState().setSnapshot(patch);
266
273
  }
267
274
  this.ensureWorkspaceParentRoute(parentSessionKey);
275
+ this.onWorkspacePanelOpened?.();
268
276
  };
269
277
 
270
278
  openSideChatDraft = (parentSessionKey?: string | null) => {
@@ -285,6 +293,7 @@ export class ChatThreadManager {
285
293
  key: activeSideChatDraft.draftKey,
286
294
  });
287
295
  this.ensureWorkspaceParentRoute(resolvedParentSessionKey);
296
+ this.onWorkspacePanelOpened?.();
288
297
  };
289
298
 
290
299
  materializeSideChatDraft = (params: {
@@ -322,6 +331,7 @@ export class ChatThreadManager {
322
331
  key: nextTab.key,
323
332
  });
324
333
  this.ensureWorkspaceParentRoute(parentSessionKey);
334
+ this.onWorkspacePanelOpened?.();
325
335
  };
326
336
 
327
337
  private openSessionFromToolAction = (action: ChatToolActionViewModel) => {
@@ -384,6 +394,7 @@ export class ChatThreadManager {
384
394
  path: request.target.payload.path,
385
395
  label: request.title,
386
396
  viewMode: 'preview',
397
+ previewViewer: request.target.payload.viewer,
387
398
  line: request.target.payload.line,
388
399
  column: request.target.payload.column,
389
400
  });
@@ -504,6 +515,7 @@ export class ChatThreadManager {
504
515
  kind: 'cron',
505
516
  });
506
517
  this.ensureWorkspaceParentRoute(parentSessionKey);
518
+ this.onWorkspacePanelOpened?.();
507
519
  };
508
520
 
509
521
  goBackWorkspacePanel = () => {
@@ -89,7 +89,11 @@ export class ChatUiManager {
89
89
  showContent = async (params: { target: ChatUiDisplayContentTarget; title?: string }) => {
90
90
  const { target, title } = params;
91
91
  if (target.type === 'url') {
92
- this.docBrowserManager.open(target.payload.url, { title });
92
+ this.docBrowserManager.open(target.payload.url, {
93
+ dedupeKey: `browser:${target.payload.url}`,
94
+ kind: 'content',
95
+ title,
96
+ });
93
97
  return;
94
98
  }
95
99
  const { appId } = target.payload;
@@ -16,7 +16,8 @@ export class ChatPresenter {
16
16
  this.chatQueryManager = new ChatQueryManager();
17
17
  this.chatThreadManager = new ChatThreadManager(
18
18
  this.chatUiManager,
19
- this.chatSessionListManager
19
+ this.chatSessionListManager,
20
+ appPresenter.notifyRightPanelOpened,
20
21
  );
21
22
  }
22
23
  }