@nextclaw/ui 0.15.2 → 0.15.4

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 (212) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/aiden0z-pptx-renderer.es-Bd99vut2.js +94 -0
  3. package/dist/assets/api-BOXOrpcJ.js +13 -0
  4. package/dist/assets/app-presenter-provider-CkHLbvZQ.js +106 -0
  5. package/dist/assets/app-presenter-provider-D3BuuOyZ.css +1 -0
  6. package/dist/assets/appearance-settings-page-fLIJbx1C.js +1 -0
  7. package/dist/assets/{book-open-Ba2f7YiG.js → book-open-DWUjS6aW.js} +1 -1
  8. package/dist/assets/channels-list-page-CvCTJraa.js +8 -0
  9. package/dist/assets/chat-page-B42UUIFR.js +1 -0
  10. package/dist/assets/chunk-DseTPa7n.js +1 -0
  11. package/dist/assets/config-split-page-DBgt9S8b.js +1 -0
  12. package/dist/assets/confirm-dialog-GSXIvoRU.js +5 -0
  13. package/dist/assets/desktop-update-config-7bmNeWIC.js +1 -0
  14. package/dist/assets/dist-DC7f4TJH.js +41 -0
  15. package/dist/assets/doc-browser-CgdoLUT92.js +1 -0
  16. package/dist/assets/doc-browser-D0monHjG.js +1 -0
  17. package/dist/assets/doc-browser-context-CGwEOhBy.js +1 -0
  18. package/dist/assets/docx-preview-eFH9DRsS.js +20 -0
  19. package/dist/assets/download-DiP9zx-J.js +1 -0
  20. package/dist/assets/{external-link-DxuPaDoK.js → external-link-C6ZeSPef.js} +1 -1
  21. package/dist/assets/eye-suvKmtwn.js +1 -0
  22. package/dist/assets/folder-CMELbQGG.js +1 -0
  23. package/dist/assets/{form-actions-BztL7P_T.js → form-actions-CUAzpNe_.js} +1 -1
  24. package/dist/assets/index-BFeNDQ4R.css +1 -0
  25. package/dist/assets/index-D_6U-M94.js +2 -0
  26. package/dist/assets/jszip.min-Das2IAnb.js +2 -0
  27. package/dist/assets/loader-circle-tYHStefV.js +1 -0
  28. package/dist/assets/logo-badge-Cu5UGH_4.js +1 -0
  29. package/dist/assets/mcp-marketplace-page-BUzHDwW6.js +9 -0
  30. package/dist/assets/mcp-marketplace-page-ohUAdfqJ.js +1 -0
  31. package/dist/assets/model-config-page-CWP6pyBm.js +1 -0
  32. package/dist/assets/plus-tywizkeY.js +1 -0
  33. package/dist/assets/popover-DImguRok.js +1 -0
  34. package/dist/assets/provider-models-C_yOh6DE.js +1 -0
  35. package/dist/assets/provider-scoped-model-input-CE7p4SbJ.js +1 -0
  36. package/dist/assets/providers-config-page-BKPo8SjH.js +1 -0
  37. package/dist/assets/react-5sjAT_6d.js +8 -0
  38. package/dist/assets/{refresh-cw-eT8BF-8B.js → refresh-cw-CHLBzWWj.js} +1 -1
  39. package/dist/assets/remote-DXJryUxB.js +1 -0
  40. package/dist/assets/rotate-cw-BefO5brU.js +1 -0
  41. package/dist/assets/runtime-config-page-1Axv4QGu.js +1 -0
  42. package/dist/assets/{save-Cb2g0BOi.js → save-BABzyrjB.js} +1 -1
  43. package/dist/assets/{search-BcHjwdsb.js → search-BTqxG3ZG.js} +1 -1
  44. package/dist/assets/search-config-page-BkKKmxmS.js +1 -0
  45. package/dist/assets/secrets-config-page-CWJMyu-k.js +3 -0
  46. package/dist/assets/security-config-DeQUIXfi.js +1 -0
  47. package/dist/assets/select-TYZDBNmq.js +1 -0
  48. package/dist/assets/setting-row-DR23Z_5I.js +1 -0
  49. package/dist/assets/{settings-2-2DWauOjY.js → settings-2-D8WWcBnw.js} +1 -1
  50. package/dist/assets/{skeleton-P5oM14E1.js → skeleton-CloHeg0S.js} +1 -1
  51. package/dist/assets/tabs-CX-ckWoo.js +1 -0
  52. package/dist/assets/tag-chip-mCc23Rhb.js +1 -0
  53. package/dist/assets/trash-2-SAW1Z4Po.js +1 -0
  54. package/dist/assets/use-config-BucdaK23.js +1 -0
  55. package/dist/assets/use-confirm-dialog-C95F6sT1.js +1 -0
  56. package/dist/assets/use-viewport-layout-C6m3Nte7.js +1 -0
  57. package/dist/assets/x-DtCPHwCu.js +1 -0
  58. package/dist/assets/xlsx-B6zVt1RK.js +64 -0
  59. package/dist/index.html +34 -28
  60. package/package.json +14 -9
  61. package/src/app/components/layout/sidebar-items.tsx +3 -3
  62. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  63. package/src/app/components/layout/sidebar.tsx +1 -1
  64. package/src/app/styles/design-system.css +17 -4
  65. package/src/features/chat/components/__tests__/chat-popover-content.test.tsx +41 -0
  66. package/src/features/chat/components/chat-popover-content.tsx +26 -0
  67. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -0
  68. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +21 -11
  69. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +2 -2
  70. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +28 -1
  71. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -4
  72. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +9 -18
  73. package/src/features/chat/components/conversation/chat-conversation-track.tsx +35 -0
  74. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +1 -0
  75. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +7 -7
  76. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +4 -15
  77. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +20 -1
  78. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +69 -54
  79. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  80. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  81. package/src/features/chat/features/message/components/chat-message-list.container.tsx +63 -2
  82. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +30 -0
  83. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  84. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  85. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  86. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  87. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +37 -0
  88. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -4
  89. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +3 -8
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +65 -20
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +26 -28
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  93. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +5 -5
  94. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +4 -4
  95. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +4 -4
  96. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +463 -99
  97. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +113 -2
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +197 -44
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +11 -5
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +8 -6
  102. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +99 -67
  103. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +239 -1
  104. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +56 -6
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +29 -7
  106. package/src/features/chat/features/workspace/components/file-content-preview/use-workspace-file-buffer.ts +42 -0
  107. package/src/features/chat/features/workspace/components/file-content-preview/workspace-document-preview-state.tsx +18 -0
  108. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.css +89 -0
  109. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.tsx +87 -0
  110. package/src/features/chat/features/workspace/components/file-content-preview/workspace-presentation-preview.tsx +82 -0
  111. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +180 -0
  112. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +191 -0
  113. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +1 -4
  114. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +53 -10
  115. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +68 -2
  116. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +21 -0
  117. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +63 -0
  118. package/src/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils.ts +49 -0
  119. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -0
  120. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +120 -38
  121. package/src/features/chat/index.ts +1 -0
  122. package/src/features/chat/managers/__tests__/chat-thread-workspace.manager.test.ts +76 -0
  123. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +44 -60
  124. package/src/features/chat/managers/chat-thread.manager.ts +123 -124
  125. package/src/features/chat/pages/ncp-chat-page.tsx +1 -2
  126. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +78 -1
  127. package/src/features/chat/stores/chat-message-layout.store.test.ts +37 -0
  128. package/src/features/chat/stores/chat-message-layout.store.ts +41 -0
  129. package/src/features/chat/stores/chat-thread.store.ts +73 -13
  130. package/src/features/cron/components/cron-config.tsx +63 -80
  131. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  132. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  133. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  134. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  135. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  136. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  137. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -8
  138. package/src/features/settings/pages/appearance-settings-page.tsx +92 -13
  139. package/src/features/side-dock/components/side-dock.tsx +3 -3
  140. package/src/index.css +275 -15
  141. package/src/shared/components/__tests__/file-type-icon.test.tsx +61 -0
  142. package/src/shared/components/actions/action-link.tsx +1 -1
  143. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +19 -0
  144. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -0
  145. package/src/shared/components/doc-browser/doc-browser-context.tsx +1 -0
  146. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  147. package/src/shared/components/doc-browser/doc-browser.tsx +11 -3
  148. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +5 -0
  149. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +6 -1
  150. package/src/shared/components/doc-browser/types/doc-browser.types.ts +2 -0
  151. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  152. package/src/shared/components/file-type-icon.tsx +272 -0
  153. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +22 -0
  154. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +17 -2
  155. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  156. package/src/shared/components/ui/button.tsx +1 -1
  157. package/src/shared/components/ui/dialog.tsx +1 -1
  158. package/src/shared/components/ui/input.tsx +1 -1
  159. package/src/shared/components/ui/select.tsx +1 -1
  160. package/src/shared/components/ui/switch.tsx +1 -1
  161. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +38 -0
  162. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +82 -2
  163. package/src/shared/components/ui/tabs.tsx +1 -1
  164. package/src/shared/components/ui/textarea.tsx +1 -1
  165. package/src/shared/lib/api/__tests__/server-path.utils.test.ts +28 -0
  166. package/src/shared/lib/api/utils/server-path.utils.ts +19 -20
  167. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  168. package/src/shared/lib/i18n/locales/en-US/core.json +6 -0
  169. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  170. package/src/shared/lib/i18n/locales/zh-CN/core.json +6 -0
  171. package/dist/assets/api-B7Qigvtm.js +0 -13
  172. package/dist/assets/appearance-settings-page-CuE0Iiqj.js +0 -1
  173. package/dist/assets/channels-list-page-CTsM6TcP.js +0 -8
  174. package/dist/assets/chat-page-C_5dcuE3.js +0 -1
  175. package/dist/assets/config-split-page-Fbb4PtTf.js +0 -1
  176. package/dist/assets/confirm-dialog-D2EG3nqE.js +0 -5
  177. package/dist/assets/desktop-update-config-BGR3HUi1.js +0 -1
  178. package/dist/assets/dist-7JTjycxE.js +0 -1
  179. package/dist/assets/dist-wXLhk4hl.js +0 -1
  180. package/dist/assets/doc-browser-bHyXnxlj.js +0 -1
  181. package/dist/assets/doc-browser-context-CYAGm5d5.js +0 -1
  182. package/dist/assets/doc-browser-gTrQ3jCF.js +0 -1
  183. package/dist/assets/es2015-j1SRCIX5.js +0 -41
  184. package/dist/assets/i18n-provider-B4oMxOFx.js +0 -1
  185. package/dist/assets/index-CvrvblgX.css +0 -1
  186. package/dist/assets/index-Obd4JYU2.js +0 -104
  187. package/dist/assets/loader-circle-CdzgK4eH.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-Bt7CuOoq.js +0 -9
  189. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +0 -1
  190. package/dist/assets/model-config-page-DeKk9gvE.js +0 -1
  191. package/dist/assets/plus-Bx7wQbhG.js +0 -1
  192. package/dist/assets/provider-scoped-model-input-DsH2LF1q.js +0 -1
  193. package/dist/assets/providers-config-page-BFnGp68F.js +0 -1
  194. package/dist/assets/react-UirzUuST.js +0 -8
  195. package/dist/assets/remote-CDkHZtA8.js +0 -1
  196. package/dist/assets/rotate-cw-1JqxHTuw.js +0 -1
  197. package/dist/assets/runtime-config-page-BelAqiNG.js +0 -1
  198. package/dist/assets/search-config-page-zPm6--Ig.js +0 -1
  199. package/dist/assets/secrets-config-page-Cxyp-KuQ.js +0 -3
  200. package/dist/assets/security-config-B_gCq9fn.js +0 -1
  201. package/dist/assets/select-CdSEOwVM.js +0 -1
  202. package/dist/assets/setting-row-CTAtGexr.js +0 -1
  203. package/dist/assets/side-dock-DPvakJ0a.js +0 -3
  204. package/dist/assets/switch-BU_UEULb.js +0 -1
  205. package/dist/assets/tag-chip-D-WsKoT1.js +0 -1
  206. package/dist/assets/tooltip-CjkOaHW7.js +0 -1
  207. package/dist/assets/use-config--hx5oxQn.js +0 -1
  208. package/dist/assets/use-confirm-dialog-CD9a_SK1.js +0 -1
  209. package/dist/assets/x-DT5x45JU.js +0 -1
  210. /package/dist/assets/{config-hints-C_iP1zl_.js → config-hints-fDrYfl0l.js} +0 -0
  211. /package/dist/assets/{host-capabilities-9bB_998Z.js → host-capabilities-zZvY9U63.js} +0 -0
  212. /package/dist/assets/{middleware-BILWbJE4.js → middleware-BLMHv9YF.js} +0 -0
@@ -0,0 +1,191 @@
1
+ import { WorkspaceDocxPreview } from "./file-content-preview/workspace-docx-preview";
2
+ import { WorkspacePresentationPreview } from "./file-content-preview/workspace-presentation-preview";
3
+ import { WorkspaceSpreadsheetPreview } from "./file-content-preview/workspace-spreadsheet-preview";
4
+ import { t } from "@/shared/lib/i18n";
5
+
6
+ export type WorkspaceFileContentKind =
7
+ | "image"
8
+ | "audio"
9
+ | "video"
10
+ | "pdf"
11
+ | "html"
12
+ | "docx"
13
+ | "spreadsheet"
14
+ | "presentation"
15
+ | "other";
16
+
17
+ export function resolveWorkspaceFileContentKind(params: {
18
+ path: string;
19
+ mimeType?: string | null;
20
+ }): WorkspaceFileContentKind {
21
+ const mime = params.mimeType?.trim().toLowerCase() ?? "";
22
+ const path = params.path.trim().toLowerCase();
23
+ if (
24
+ mime.startsWith("image/") ||
25
+ /\.(avif|bmp|gif|heic|heif|ico|jpe?g|png|svg|tiff?|webp)$/i.test(path)
26
+ ) {
27
+ return "image";
28
+ }
29
+ if (
30
+ mime.startsWith("audio/") ||
31
+ /\.(aac|flac|m4a|mp3|ogg|opus|wav|weba)$/i.test(path)
32
+ ) {
33
+ return "audio";
34
+ }
35
+ if (
36
+ mime.startsWith("video/") ||
37
+ /\.(avi|m4v|mkv|mov|mp4|webm|wmv)$/i.test(path)
38
+ ) {
39
+ return "video";
40
+ }
41
+ if (mime.includes("pdf") || path.endsWith(".pdf")) {
42
+ return "pdf";
43
+ }
44
+ if (
45
+ mime ===
46
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
47
+ path.endsWith(".docx")
48
+ ) {
49
+ return "docx";
50
+ }
51
+ if (
52
+ mime.includes("spreadsheet") ||
53
+ mime.includes("excel") ||
54
+ /\.(ods|xls|xlsb|xlsm|xlsx)$/i.test(path)
55
+ ) {
56
+ return "spreadsheet";
57
+ }
58
+ if (
59
+ mime ===
60
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation" ||
61
+ path.endsWith(".pptx")
62
+ ) {
63
+ return "presentation";
64
+ }
65
+ if (mime.includes("html") || /\.html?$/i.test(path)) {
66
+ return "html";
67
+ }
68
+ return "other";
69
+ }
70
+
71
+ function WorkspaceUnsupportedContent({
72
+ contentUrl,
73
+ label,
74
+ }: {
75
+ contentUrl: string;
76
+ label: string;
77
+ }) {
78
+ return (
79
+ <div
80
+ className="flex h-full flex-col items-center justify-center gap-4 px-6 text-center"
81
+ data-testid="workspace-content-unsupported"
82
+ >
83
+ <div className="max-w-sm space-y-1.5">
84
+ <p
85
+ className="truncate text-sm font-medium text-foreground"
86
+ title={label}
87
+ >
88
+ {label}
89
+ </p>
90
+ <p className="text-sm text-muted-foreground">
91
+ {t("chatWorkspacePreviewUnsupported")}
92
+ </p>
93
+ <p className="text-xs leading-5 text-muted-foreground/80">
94
+ {t("chatWorkspacePreviewUnsupportedHint")}
95
+ </p>
96
+ </div>
97
+ <div className="flex flex-wrap items-center justify-center gap-2">
98
+ <a
99
+ href={contentUrl}
100
+ download={label}
101
+ className="inline-flex h-8 items-center rounded-lg border border-border bg-card px-3 text-xs font-medium text-foreground transition-colors hover:bg-muted"
102
+ >
103
+ {t("chatWorkspacePreviewDownload")}
104
+ </a>
105
+ <a
106
+ href={contentUrl}
107
+ target="_blank"
108
+ rel="noreferrer"
109
+ className="inline-flex h-8 items-center rounded-lg bg-primary px-3 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
110
+ >
111
+ {t("chatWorkspacePreviewOpenExternally")}
112
+ </a>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ export function WorkspaceFileContentPreview({
119
+ contentUrl,
120
+ kind,
121
+ label,
122
+ }: {
123
+ contentUrl: string;
124
+ kind: WorkspaceFileContentKind;
125
+ label: string;
126
+ }) {
127
+ if (kind === "image") {
128
+ return (
129
+ <div className="flex h-full items-center justify-center overflow-auto bg-white p-4 custom-scrollbar">
130
+ <img
131
+ src={contentUrl}
132
+ alt={label}
133
+ className="max-h-full max-w-full object-contain"
134
+ data-testid="workspace-content-image"
135
+ />
136
+ </div>
137
+ );
138
+ }
139
+ if (kind === "audio") {
140
+ return (
141
+ <div className="flex h-full items-center justify-center bg-white px-6">
142
+ <audio
143
+ controls
144
+ preload="metadata"
145
+ aria-label={label}
146
+ className="w-full max-w-xl"
147
+ data-testid="workspace-content-audio"
148
+ src={contentUrl}
149
+ />
150
+ </div>
151
+ );
152
+ }
153
+ if (kind === "video") {
154
+ return (
155
+ <div className="flex h-full items-center justify-center bg-black">
156
+ <video
157
+ controls
158
+ playsInline
159
+ preload="metadata"
160
+ aria-label={label}
161
+ className="max-h-full max-w-full"
162
+ data-testid="workspace-content-video"
163
+ src={contentUrl}
164
+ />
165
+ </div>
166
+ );
167
+ }
168
+ if (kind === "pdf" || kind === "html") {
169
+ return (
170
+ <iframe
171
+ allowFullScreen
172
+ className="h-full w-full border-0 bg-white"
173
+ data-testid={
174
+ kind === "pdf" ? "workspace-content-pdf" : "workspace-html-preview"
175
+ }
176
+ src={contentUrl}
177
+ title={label}
178
+ />
179
+ );
180
+ }
181
+ if (kind === "docx") {
182
+ return <WorkspaceDocxPreview contentUrl={contentUrl} />;
183
+ }
184
+ if (kind === "spreadsheet") {
185
+ return <WorkspaceSpreadsheetPreview contentUrl={contentUrl} />;
186
+ }
187
+ if (kind === "presentation") {
188
+ return <WorkspacePresentationPreview contentUrl={contentUrl} />;
189
+ }
190
+ return <WorkspaceUnsupportedContent contentUrl={contentUrl} label={label} />;
191
+ }
@@ -64,9 +64,6 @@ export function useChatConversationWorkspaceState(
64
64
  sessionCronJobs,
65
65
  showWorkspacePanel:
66
66
  snapshot.workspacePanelParentKey === sessionKey &&
67
- (activeSideChatDraft !== null ||
68
- childSessionTabs.length > 0 ||
69
- workspaceFileTabs.length > 0 ||
70
- sessionCronJobs.length > 0),
67
+ snapshot.activeWorkspacePanelKind != null,
71
68
  };
72
69
  }
@@ -4,6 +4,7 @@ import {
4
4
  buildWorkspaceTabsViewModel,
5
5
  resolveWorkspaceSelection,
6
6
  } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
7
+ import { t } from "@/shared/lib/i18n";
7
8
 
8
9
  function createChildTab(
9
10
  overrides: Partial<ResolvedChildSessionTab> = {},
@@ -26,6 +27,19 @@ function createChildTab(
26
27
  }
27
28
 
28
29
  describe("resolveWorkspaceSelection", () => {
30
+ it("keeps the explicit workspace overview even when no resources exist", () => {
31
+ expect(
32
+ resolveWorkspaceSelection({
33
+ activePanelKind: "overview",
34
+ activeChildSessionKey: null,
35
+ activeSideChatDraft: null,
36
+ activeWorkspaceFileKey: null,
37
+ childSessionTabs: [],
38
+ workspaceFileTabs: [],
39
+ }),
40
+ ).toEqual({ kind: "overview" });
41
+ });
42
+
29
43
  it("honors the explicit active panel kind before fallback order", () => {
30
44
  const childTab = createChildTab();
31
45
  const fileTab = {
@@ -43,7 +57,6 @@ describe("resolveWorkspaceSelection", () => {
43
57
  activeWorkspaceFileKey: fileTab.key,
44
58
  childSessionTabs: [childTab],
45
59
  workspaceFileTabs: [fileTab],
46
- sessionCronJobCount: 1,
47
60
  }),
48
61
  ).toMatchObject({
49
62
  kind: "file",
@@ -62,7 +75,6 @@ describe("resolveWorkspaceSelection", () => {
62
75
  activeWorkspaceFileKey: null,
63
76
  childSessionTabs: [childTab],
64
77
  workspaceFileTabs: [],
65
- sessionCronJobCount: 1,
66
78
  }),
67
79
  ).toMatchObject({
68
80
  kind: "child-session",
@@ -84,7 +96,6 @@ describe("resolveWorkspaceSelection", () => {
84
96
  activeWorkspaceFileKey: null,
85
97
  childSessionTabs: [createChildTab()],
86
98
  workspaceFileTabs: [],
87
- sessionCronJobCount: 0,
88
99
  }),
89
100
  ).toMatchObject({
90
101
  kind: "side-chat-draft",
@@ -105,16 +116,19 @@ describe("buildWorkspaceTabsViewModel", () => {
105
116
  resolvedChildTabs: [childTab],
106
117
  activeSideChatDraft: null,
107
118
  workspaceFileTabs: [],
108
- sessionCronJobCount: 0,
109
119
  activeSelection: null,
110
120
  optimisticReadAtBySessionKey: {},
111
121
  onSelectSession,
112
122
  onSelectFile: vi.fn(),
123
+ onOpenFileViewer: vi.fn(),
113
124
  onCloseFile: vi.fn(),
125
+ onSelectOverview: vi.fn(),
126
+ onSelectChildSessions: vi.fn(),
127
+ onSelectProjectFiles: vi.fn(),
114
128
  onSelectCronJobs: vi.fn(),
115
129
  });
116
130
 
117
- expect(tabs[0]).toMatchObject({
131
+ expect(tabs[4]).toMatchObject({
118
132
  key: "child:child-1",
119
133
  kind: "child-session",
120
134
  title: "Child session",
@@ -122,7 +136,7 @@ describe("buildWorkspaceTabsViewModel", () => {
122
136
  showUnreadDot: true,
123
137
  });
124
138
 
125
- tabs[0]?.onSelect();
139
+ tabs[4]?.onSelect();
126
140
  expect(onSelectSession).toHaveBeenCalledWith("child-1");
127
141
  });
128
142
 
@@ -137,7 +151,6 @@ describe("buildWorkspaceTabsViewModel", () => {
137
151
  resolvedChildTabs: [childTab],
138
152
  activeSideChatDraft: draft,
139
153
  workspaceFileTabs: [],
140
- sessionCronJobCount: 0,
141
154
  activeSelection: {
142
155
  kind: "side-chat-draft",
143
156
  draft,
@@ -145,15 +158,23 @@ describe("buildWorkspaceTabsViewModel", () => {
145
158
  optimisticReadAtBySessionKey: {},
146
159
  onSelectSession: vi.fn(),
147
160
  onSelectFile: vi.fn(),
161
+ onOpenFileViewer: vi.fn(),
148
162
  onCloseFile: vi.fn(),
163
+ onSelectOverview: vi.fn(),
164
+ onSelectChildSessions: vi.fn(),
165
+ onSelectProjectFiles: vi.fn(),
149
166
  onSelectCronJobs: vi.fn(),
150
167
  });
151
168
 
152
169
  expect(tabs.map((tab) => tab.key)).toEqual([
170
+ "overview",
171
+ "child-sessions",
172
+ "cron:session",
173
+ "project-files",
153
174
  "side-chat-draft:draft-1",
154
175
  "child:child-1",
155
176
  ]);
156
- expect(tabs[0]).toMatchObject({
177
+ expect(tabs[4]).toMatchObject({
157
178
  kind: "side-chat-draft",
158
179
  active: true,
159
180
  });
@@ -177,11 +198,11 @@ describe("buildWorkspaceTabsViewModel", () => {
177
198
  previewViewer: "source" as const,
178
199
  };
179
200
 
201
+ const onOpenFileViewer = vi.fn();
180
202
  const tabs = buildWorkspaceTabsViewModel({
181
203
  resolvedChildTabs: [],
182
204
  activeSideChatDraft: null,
183
205
  workspaceFileTabs: [sourceTab, renderedTab],
184
- sessionCronJobCount: 0,
185
206
  activeSelection: {
186
207
  kind: "file",
187
208
  file: sourceTab,
@@ -189,19 +210,41 @@ describe("buildWorkspaceTabsViewModel", () => {
189
210
  optimisticReadAtBySessionKey: {},
190
211
  onSelectSession: vi.fn(),
191
212
  onSelectFile: vi.fn(),
213
+ onOpenFileViewer,
192
214
  onCloseFile: vi.fn(),
215
+ onSelectOverview: vi.fn(),
216
+ onSelectChildSessions: vi.fn(),
217
+ onSelectProjectFiles: vi.fn(),
193
218
  onSelectCronJobs: vi.fn(),
194
219
  });
195
220
 
196
- expect(tabs).toEqual([
221
+ expect(tabs.slice(4)).toEqual([
197
222
  expect.objectContaining({
198
223
  key: "file:parent::preview::demo.html",
199
224
  active: true,
225
+ fileName: "demo.html",
226
+ title: "demo.html",
227
+ isRenderedPreview: false,
228
+ alternateViewerAction: expect.objectContaining({
229
+ label: t("chatWorkspaceOpenPreview"),
230
+ viewer: "rendered",
231
+ }),
200
232
  }),
201
233
  expect.objectContaining({
202
234
  key: "file:parent::preview:rendered::demo.html",
203
235
  active: false,
236
+ title: `${t("chatWorkspacePreview")}: demo.html`,
237
+ isRenderedPreview: true,
238
+ alternateViewerAction: expect.objectContaining({
239
+ label: t("chatWorkspaceOpenSource"),
240
+ viewer: "source",
241
+ }),
204
242
  }),
205
243
  ]);
244
+ tabs[4]?.alternateViewerAction?.onSelect();
245
+ expect(onOpenFileViewer).toHaveBeenCalledWith(
246
+ "parent::preview::demo.html",
247
+ "rendered",
248
+ );
206
249
  });
207
250
  });
@@ -16,10 +16,76 @@ export function areWorkspaceNavigationEntriesEqual(
16
16
  if (current.kind !== next.kind) {
17
17
  return false;
18
18
  }
19
- if (current.kind === 'cron') {
19
+ if (
20
+ current.kind === 'overview' ||
21
+ current.kind === 'child-sessions' ||
22
+ current.kind === 'project-files' ||
23
+ current.kind === 'cron'
24
+ ) {
20
25
  return true;
21
26
  }
22
- return next.kind !== 'cron' && current.key === next.key;
27
+ return (
28
+ next.kind !== 'overview' &&
29
+ next.kind !== 'child-sessions' &&
30
+ next.kind !== 'project-files' &&
31
+ next.kind !== 'cron' &&
32
+ current.key === next.key
33
+ );
34
+ }
35
+
36
+ export function createWorkspaceSelectionPatch(
37
+ entry: ChatWorkspaceNavigationEntry,
38
+ snapshot: ChatThreadSnapshot,
39
+ ): Partial<ChatThreadSnapshot> | null {
40
+ if (
41
+ entry.kind === 'overview' ||
42
+ entry.kind === 'child-sessions' ||
43
+ entry.kind === 'project-files'
44
+ ) {
45
+ return {
46
+ activeWorkspacePanelKind: entry.kind,
47
+ activeChildSessionKey: null,
48
+ activeSideChatDraft: null,
49
+ activeWorkspaceFileKey: null,
50
+ };
51
+ }
52
+ if (entry.kind === 'cron') {
53
+ return {
54
+ activeWorkspacePanelKind: 'cron',
55
+ activeChildSessionKey: null,
56
+ activeSideChatDraft: null,
57
+ activeWorkspaceFileKey: null,
58
+ };
59
+ }
60
+ if (entry.kind === 'file') {
61
+ if (!snapshot.workspaceFileTabs.some((tab) => tab.key === entry.key)) {
62
+ return null;
63
+ }
64
+ return {
65
+ activeWorkspacePanelKind: 'file',
66
+ activeChildSessionKey: null,
67
+ activeSideChatDraft: null,
68
+ activeWorkspaceFileKey: entry.key,
69
+ };
70
+ }
71
+ if (entry.kind === 'side-chat-draft') {
72
+ const { activeSideChatDraft } = snapshot;
73
+ if (activeSideChatDraft?.draftKey !== entry.key) {
74
+ return null;
75
+ }
76
+ return {
77
+ activeWorkspacePanelKind: 'side-chat-draft',
78
+ activeChildSessionKey: null,
79
+ activeSideChatDraft,
80
+ activeWorkspaceFileKey: null,
81
+ };
82
+ }
83
+ return {
84
+ activeWorkspacePanelKind: 'child-session',
85
+ activeChildSessionKey: entry.key,
86
+ activeSideChatDraft: null,
87
+ activeWorkspaceFileKey: null,
88
+ };
23
89
  }
24
90
 
25
91
  export function createSideChatDraft(parentSessionKey: string): ChatWorkspaceSideChatDraft {
@@ -53,6 +53,8 @@ export function normalizePersistedWorkspaceFileTab(
53
53
  line: normalizeOptionalNumber(value.line),
54
54
  column: normalizeOptionalNumber(value.column),
55
55
  rawText: normalizeOptionalText(value.rawText),
56
+ contentUrl: normalizeOptionalString(value.contentUrl),
57
+ mimeType: normalizeOptionalString(value.mimeType),
56
58
  beforeText: normalizeOptionalText(value.beforeText),
57
59
  afterText: normalizeOptionalText(value.afterText),
58
60
  patchText: normalizeOptionalText(value.patchText),
@@ -74,6 +76,8 @@ export function toPersistedWorkspaceFileTab(
74
76
  line: tab.line,
75
77
  column: tab.column,
76
78
  rawText: tab.rawText,
79
+ contentUrl: tab.contentUrl,
80
+ mimeType: tab.mimeType,
77
81
  beforeText: tab.beforeText,
78
82
  afterText: tab.afterText,
79
83
  patchText: tab.patchText,
@@ -81,3 +85,20 @@ export function toPersistedWorkspaceFileTab(
81
85
  newStartLine: tab.newStartLine,
82
86
  };
83
87
  }
88
+
89
+ export function retainWorkspaceFileTabs(
90
+ tabs: readonly ChatWorkspaceFileTab[],
91
+ activeWorkspaceFileKey: string | null | undefined,
92
+ maxTabs: number,
93
+ ): ChatWorkspaceFileTab[] {
94
+ const retained = tabs.slice(0, maxTabs);
95
+ const activeTab = activeWorkspaceFileKey
96
+ ? tabs.find((tab) => tab.key === activeWorkspaceFileKey)
97
+ : null;
98
+ if (!activeTab || retained.some((tab) => tab.key === activeTab.key)) {
99
+ return retained;
100
+ }
101
+ return retained.length < maxTabs
102
+ ? [...retained, activeTab]
103
+ : [...retained.slice(0, -1), activeTab];
104
+ }
@@ -0,0 +1,63 @@
1
+ import type { ChatFileOpenActionViewModel } from '@nextclaw/agent-chat-ui';
2
+ import type { ChatWorkspaceFileTab } from '@/features/chat/stores/chat-thread.store';
3
+ import { normalizeWorkspaceFilePreviewViewer } from './chat-workspace-file-viewer.utils';
4
+
5
+ export function createWorkspaceFileTab(
6
+ action: ChatFileOpenActionViewModel,
7
+ parentSessionKey: string | null,
8
+ ): ChatWorkspaceFileTab | null {
9
+ const path = action.path.trim();
10
+ if (!path) {
11
+ return null;
12
+ }
13
+ const normalizedParentSessionKey = parentSessionKey?.trim() || null;
14
+ const previewViewer = action.viewMode === 'preview'
15
+ ? normalizeWorkspaceFilePreviewViewer(path, action.previewViewer)
16
+ : action.previewViewer ?? null;
17
+ const viewIdentity = action.viewMode === 'preview' && previewViewer === 'rendered'
18
+ ? 'preview:rendered'
19
+ : action.viewMode;
20
+ return {
21
+ key: `${normalizedParentSessionKey ?? 'draft'}::${viewIdentity}::${path}`,
22
+ parentSessionKey: normalizedParentSessionKey,
23
+ path,
24
+ label: action.label?.trim() || null,
25
+ viewMode: action.viewMode,
26
+ previewViewer,
27
+ line: action.line ?? null,
28
+ column: action.column ?? null,
29
+ rawText: action.rawText ?? null,
30
+ contentUrl: action.contentUrl?.trim() || null,
31
+ mimeType: action.mimeType?.trim() || null,
32
+ beforeText: action.beforeText ?? null,
33
+ afterText: action.afterText ?? null,
34
+ patchText: action.patchText ?? null,
35
+ oldStartLine: action.oldStartLine ?? null,
36
+ newStartLine: action.newStartLine ?? null,
37
+ fullLines: action.fullLines,
38
+ };
39
+ }
40
+
41
+ export function upsertWorkspaceFileTab(
42
+ tabs: readonly ChatWorkspaceFileTab[],
43
+ nextTab: ChatWorkspaceFileTab,
44
+ adjacentToKey?: string,
45
+ ): ChatWorkspaceFileTab[] {
46
+ const existingIndex = tabs.findIndex((tab) => tab.key === nextTab.key);
47
+ if (existingIndex !== -1) {
48
+ const nextTabs = [...tabs];
49
+ nextTabs[existingIndex] = { ...tabs[existingIndex], ...nextTab };
50
+ return nextTabs;
51
+ }
52
+ const adjacentIndex = adjacentToKey
53
+ ? tabs.findIndex((tab) => tab.key === adjacentToKey)
54
+ : -1;
55
+ if (adjacentIndex === -1) {
56
+ return [nextTab, ...tabs];
57
+ }
58
+ return [
59
+ ...tabs.slice(0, adjacentIndex + 1),
60
+ nextTab,
61
+ ...tabs.slice(adjacentIndex + 1),
62
+ ];
63
+ }
@@ -0,0 +1,49 @@
1
+ import type { ChatFilePreviewViewer } from "@nextclaw/agent-chat-ui";
2
+
3
+ export type ChatWorkspaceFileViewer = "source" | "rendered";
4
+
5
+ function resolveDualViewFileKind(path: string): "html" | "markdown" | null {
6
+ if (/\.html?$/i.test(path)) {
7
+ return "html";
8
+ }
9
+ if (/\.mdx?$/i.test(path)) {
10
+ return "markdown";
11
+ }
12
+ return null;
13
+ }
14
+
15
+ export function resolveWorkspaceFileViewer(
16
+ path: string,
17
+ viewer?: ChatFilePreviewViewer | null,
18
+ ): ChatWorkspaceFileViewer | null {
19
+ const kind = resolveDualViewFileKind(path);
20
+ if (kind === "html") {
21
+ return viewer === "rendered" ? "rendered" : "source";
22
+ }
23
+ if (kind === "markdown") {
24
+ return viewer === "source" ? "source" : "rendered";
25
+ }
26
+ if (viewer === "source" || viewer === "rendered") {
27
+ return viewer;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ export function normalizeWorkspaceFilePreviewViewer(
33
+ path: string,
34
+ viewer?: ChatFilePreviewViewer | null,
35
+ ): ChatFilePreviewViewer | null {
36
+ return resolveWorkspaceFileViewer(path, viewer) ?? viewer ?? null;
37
+ }
38
+
39
+ export function resolveAlternateWorkspaceFileViewer(
40
+ path: string,
41
+ viewer?: ChatFilePreviewViewer | null,
42
+ ): ChatWorkspaceFileViewer | null {
43
+ if (!resolveDualViewFileKind(path)) {
44
+ return null;
45
+ }
46
+ return resolveWorkspaceFileViewer(path, viewer) === "source"
47
+ ? "rendered"
48
+ : "source";
49
+ }
@@ -0,0 +1,13 @@
1
+ export const CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH = 480;
2
+ export const CHAT_WORKSPACE_PANEL_MIN_WIDTH = 360;
3
+ export const CHAT_WORKSPACE_PANEL_MAX_WIDTH = 860;
4
+
5
+ export function normalizeChatWorkspacePanelWidth(value: unknown): number {
6
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
7
+ return CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH;
8
+ }
9
+ return Math.max(
10
+ CHAT_WORKSPACE_PANEL_MIN_WIDTH,
11
+ Math.min(CHAT_WORKSPACE_PANEL_MAX_WIDTH, value),
12
+ );
13
+ }