@nextclaw/ui 0.12.7 → 0.12.9

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 (171) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/assets/ChannelsList-Ita2Zm1_.js +8 -0
  3. package/dist/assets/{DocBrowser-Cse_F8Nn.js → DocBrowser-6ReNjvzF.js} +1 -1
  4. package/dist/assets/DocBrowser-BNwbPHf4.js +1 -0
  5. package/dist/assets/{DocBrowserContext-Bai1WU2H.js → DocBrowserContext-B6SpA7Qs.js} +1 -1
  6. package/dist/assets/{LogoBadge-BdxMPc9v.js → LogoBadge-ByNLYg65.js} +1 -1
  7. package/dist/assets/MarketplacePage-CjX2MWww.js +1 -0
  8. package/dist/assets/{MarketplacePage-BbpAkllU.js → MarketplacePage-D0sDlYX4.js} +1 -1
  9. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +40 -0
  10. package/dist/assets/{ModelConfig-3GLqQ5GY.js → ModelConfig-BzZenCH-.js} +1 -1
  11. package/dist/assets/{ProviderScopedModelInput-BYNouw-i.js → ProviderScopedModelInput-Da7khnBA.js} +1 -1
  12. package/dist/assets/{ProvidersList-BR1gJ4Dm.js → ProvidersList-BbVzRxjY.js} +1 -1
  13. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +1 -0
  14. package/dist/assets/RuntimeConfig-F_XKGgLm.js +1 -0
  15. package/dist/assets/{SearchConfig-DTeJvp8m.js → SearchConfig-BGkzXQP-.js} +1 -1
  16. package/dist/assets/{SecretsConfig-CCYO6NcV.js → SecretsConfig-D281Rotl.js} +2 -2
  17. package/dist/assets/{SessionsConfig-Du39vDgt.js → SessionsConfig-ChHQ7M5c.js} +2 -2
  18. package/dist/assets/{app-query-client-Dr5d-K8d.js → app-query-client-VnFElj4E.js} +1 -1
  19. package/dist/assets/{book-open-Da4OEPqB.js → book-open-BdcxxoQu.js} +1 -1
  20. package/dist/assets/chat-page-Doe0yTtB.js +58 -0
  21. package/dist/assets/chat-session-display-cw78aiI_.js +1 -0
  22. package/dist/assets/{chunk-JZWAC4HX-CoFVxHXV.js → chunk-JZWAC4HX-DK5HPmIK.js} +1 -1
  23. package/dist/assets/{client-CSk58DcF.js → client-_i4MU2bB.js} +1 -1
  24. package/dist/assets/{config-D8KzikVB.js → config-DtIQwrHF.js} +1 -1
  25. package/dist/assets/{createLucideIcon-83gaZMtv.js → createLucideIcon-BSeTgkZW.js} +1 -1
  26. package/dist/assets/desktop-update-config-Dpcf4BKG.js +1 -0
  27. package/dist/assets/{dist-toEYs-MZ.js → dist-6TrrnPCR.js} +1 -1
  28. package/dist/assets/{dist-aTmhMDVh.js → dist-ccBFUi-o.js} +1 -1
  29. package/dist/assets/download-BhDxnyvU.js +1 -0
  30. package/dist/assets/{external-link-QQ0TC6X4.js → external-link-BgErLCNT.js} +1 -1
  31. package/dist/assets/{hash-DaFBEkmi.js → hash-Bl7dr_UG.js} +1 -1
  32. package/dist/assets/i18n-eDHeDY0n.js +1 -0
  33. package/dist/assets/index-CF9xve0E.js +6 -0
  34. package/dist/assets/index-FgA52VBt.css +1 -0
  35. package/dist/assets/{infiniteQueryBehavior-BmHX_ayZ.js → infiniteQueryBehavior-ZDS92Qpp.js} +1 -1
  36. package/dist/assets/loader-circle-ACM1s51e.js +1 -0
  37. package/dist/assets/{logos-Dzlz30M3.js → logos-x89HbrZ4.js} +1 -1
  38. package/dist/assets/{page-layout-D2eRufRQ.js → page-layout-vZnghcFy.js} +1 -1
  39. package/dist/assets/play-CFUwCA2E.js +1 -0
  40. package/dist/assets/plus-rYsv72JG.js +1 -0
  41. package/dist/assets/{popover-BSXxm5bj.js → popover-Bg1VoTZ6.js} +1 -1
  42. package/dist/assets/{refresh-ccw-B3zMtN-_.js → refresh-ccw-DT98i__E.js} +1 -1
  43. package/dist/assets/{refresh-cw-DlZkIHnJ.js → refresh-cw-C47QSEwg.js} +1 -1
  44. package/dist/assets/rotate-cw-JtFzpNn6.js +1 -0
  45. package/dist/assets/{save-Us9fg4Sj.js → save-3S6-H3Xw.js} +1 -1
  46. package/dist/assets/search-3kFR_zh9.js +1 -0
  47. package/dist/assets/{security-config-BGWYwxNr.js → security-config-BWaiARNk.js} +1 -1
  48. package/dist/assets/{select-DLYqySQK.js → select-DJ2MUjBB.js} +1 -1
  49. package/dist/assets/skeleton-ByQepn0M.js +1 -0
  50. package/dist/assets/{status-dot-DGayudyB.js → status-dot-vbanNPFU.js} +1 -1
  51. package/dist/assets/{switch-Dz2ScsKx.js → switch-BsLtHOH-.js} +1 -1
  52. package/dist/assets/{tabs-custom-CdKyjiGk.js → tabs-custom-D3HYMt6k.js} +1 -1
  53. package/dist/assets/{trash-2-Db-mZOZs.js → trash-2-G48scll7.js} +1 -1
  54. package/dist/assets/{use-infinite-scroll-loader-DBJX5hj0.js → use-infinite-scroll-loader-DkNhD-42.js} +1 -1
  55. package/dist/assets/{useConfirmDialog-DL0a-oGC.js → useConfirmDialog-BkvTN-vd.js} +1 -1
  56. package/dist/assets/{useMutation-BdZm-9PL.js → useMutation-CBWjE2uj.js} +1 -1
  57. package/dist/assets/x-ByDbItbq.js +1 -0
  58. package/dist/index.html +95 -21
  59. package/dist/manifest.webmanifest +30 -0
  60. package/dist/offline.html +102 -0
  61. package/dist/pwa-192.png +0 -0
  62. package/dist/pwa-512.png +0 -0
  63. package/dist/sw.js +80 -0
  64. package/index.html +73 -1
  65. package/package.json +6 -6
  66. package/public/manifest.webmanifest +30 -0
  67. package/public/offline.html +102 -0
  68. package/public/pwa-192.png +0 -0
  69. package/public/pwa-512.png +0 -0
  70. package/public/sw.js +80 -0
  71. package/src/api/runtime-control.ts +34 -0
  72. package/src/api/runtime-control.types.ts +58 -0
  73. package/src/api/server-path.ts +27 -4
  74. package/src/api/types.ts +30 -10
  75. package/src/{App.test.tsx → app.test.tsx} +1 -1
  76. package/src/{App.tsx → app.tsx} +10 -1
  77. package/src/components/chat/ChatSidebar.test.tsx +79 -8
  78. package/src/components/chat/ChatSidebar.tsx +43 -26
  79. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  80. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  81. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  82. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +118 -155
  83. package/src/components/chat/chat-conversation-panel.tsx +412 -0
  84. package/src/components/chat/chat-page-runtime.test.ts +1 -1
  85. package/src/components/chat/chat-page-shell.tsx +1 -1
  86. package/src/components/chat/{ChatPage.tsx → chat-page.tsx} +1 -1
  87. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +91 -0
  88. package/src/components/chat/chat-session-workspace-file-preview.tsx +307 -0
  89. package/src/components/chat/chat-session-workspace-panel-nav.tsx +197 -0
  90. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  91. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  92. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  93. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  94. package/src/components/chat/managers/chat-session-list.manager.test.ts +94 -31
  95. package/src/components/chat/managers/chat-session-list.manager.ts +86 -14
  96. package/src/components/chat/managers/chat-ui.manager.ts +2 -0
  97. package/src/components/chat/ncp/README.md +1 -1
  98. package/src/components/chat/ncp/ncp-chat-input.manager.ts +7 -1
  99. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +1 -1
  100. package/src/components/chat/ncp/{NcpChatPage.tsx → ncp-chat-page.tsx} +7 -7
  101. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  102. package/src/components/chat/ncp/ncp-session-adapter.test.ts +40 -2
  103. package/src/components/chat/ncp/ncp-session-adapter.ts +29 -0
  104. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +54 -11
  105. package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +4 -0
  106. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +99 -0
  107. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  108. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  109. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  110. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  111. package/src/components/chat/stores/chat-session-list.store.ts +25 -54
  112. package/src/components/chat/stores/chat-thread.store.ts +24 -0
  113. package/src/components/common/ProviderScopedModelInput.tsx +12 -2
  114. package/src/components/config/ModelConfig.test.tsx +108 -2
  115. package/src/components/config/RuntimeConfig.tsx +154 -7
  116. package/src/components/config/desktop-update-config.test.tsx +85 -0
  117. package/src/components/config/desktop-update-config.tsx +44 -3
  118. package/src/components/config/runtime-control-card.test.tsx +255 -0
  119. package/src/components/config/runtime-control-card.tsx +301 -0
  120. package/src/components/config/runtime-presence-card.test.tsx +154 -0
  121. package/src/components/config/runtime-presence-card.tsx +163 -0
  122. package/src/components/layout/AppLayout.tsx +1 -1
  123. package/src/components/providers/ThemeProvider.tsx +5 -0
  124. package/src/desktop/desktop-update.types.ts +25 -0
  125. package/src/desktop/managers/desktop-presence.manager.ts +91 -0
  126. package/src/desktop/managers/desktop-update.manager.ts +37 -1
  127. package/src/desktop/stores/desktop-presence.store.ts +18 -0
  128. package/src/desktop/stores/desktop-update.store.ts +7 -1
  129. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  130. package/src/hooks/use-runtime-control.ts +24 -0
  131. package/src/lib/chat-message.ts +14 -3
  132. package/src/lib/desktop-update-labels.utils.ts +28 -2
  133. package/src/lib/i18n.chat.ts +12 -1
  134. package/src/lib/i18n.pwa.ts +62 -0
  135. package/src/lib/i18n.runtime-control.ts +120 -0
  136. package/src/lib/i18n.ts +4 -6
  137. package/src/main.tsx +1 -1
  138. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  139. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  140. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  141. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  142. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  143. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  144. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  145. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  146. package/src/pwa/pwa.types.ts +22 -0
  147. package/src/pwa/register-pwa.ts +14 -0
  148. package/src/pwa/stores/pwa.store.ts +17 -0
  149. package/src/runtime-control/runtime-control.manager.ts +118 -0
  150. package/src/vite-env.d.ts +9 -0
  151. package/dist/assets/ChannelsList-D8p4OlM6.js +0 -8
  152. package/dist/assets/ChatPage-A45t1Rmf.js +0 -58
  153. package/dist/assets/DocBrowser-B2MpsnU9.js +0 -1
  154. package/dist/assets/MarketplacePage-BNZ3Jx5d.js +0 -1
  155. package/dist/assets/McpMarketplacePage-CxPFOgxv.js +0 -40
  156. package/dist/assets/RemoteAccessPage-DyYVWsyK.js +0 -1
  157. package/dist/assets/RuntimeConfig-ChdfK4Y_.js +0 -1
  158. package/dist/assets/chat-session-display-CAlPrnlV.js +0 -1
  159. package/dist/assets/desktop-update-config-CfoVwf-w.js +0 -1
  160. package/dist/assets/i18n-C3jb83S6.js +0 -1
  161. package/dist/assets/index-CE4N7ItL.css +0 -1
  162. package/dist/assets/index-riX7Sg0_.js +0 -6
  163. package/dist/assets/loader-circle-BjMg63eu.js +0 -1
  164. package/dist/assets/plus-CIXME2pD.js +0 -1
  165. package/dist/assets/search-B_Qr0f6C.js +0 -1
  166. package/dist/assets/skeleton-CYQJazv6.js +0 -1
  167. package/dist/assets/x-B8Tho_xC.js +0 -1
  168. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  169. package/src/components/chat/chat-child-session-panel.tsx +0 -262
  170. /package/dist/assets/{config-hints-GSUMvmSo.js → config-hints-BhTmc9P1.js} +0 -0
  171. /package/dist/assets/{config-layout-CgBMG7OL.js → config-layout-CHs0mAaR.js} +0 -0
@@ -0,0 +1,307 @@
1
+ import { useMemo } from "react";
2
+ import type {
3
+ ChatFileOpenActionViewModel,
4
+ ChatFileOperationBlockViewModel,
5
+ } from "@nextclaw/agent-chat-ui";
6
+ import {
7
+ ChatMessageMarkdown,
8
+ FileOperationCodeSurface,
9
+ } from "@nextclaw/agent-chat-ui";
10
+ import type { ChatWorkspaceFileTab } from "@/components/chat/stores/chat-thread.store";
11
+ import { useServerPathRead } from "@/hooks/server-path/use-server-path-read";
12
+ import {
13
+ buildLineDiff,
14
+ buildPreviewLines,
15
+ } from "@/components/chat/adapters/file-operation/line-builder";
16
+ import { t } from "@/lib/i18n";
17
+ import { cn } from "@/lib/utils";
18
+
19
+ function inferPreviewKind(params: {
20
+ path: string;
21
+ serverKind?: "text" | "markdown" | "binary";
22
+ }): "text" | "markdown" | "binary" {
23
+ if (params.serverKind) {
24
+ return params.serverKind;
25
+ }
26
+ return /\.mdx?$/i.test(params.path) ? "markdown" : "text";
27
+ }
28
+
29
+ function buildPreviewBlock(params: {
30
+ path: string;
31
+ text: string;
32
+ line?: number | null;
33
+ }): ChatFileOperationBlockViewModel {
34
+ const { line, path, text } = params;
35
+ const startLine = line ?? 1;
36
+ return {
37
+ key: `preview:${path}`,
38
+ path,
39
+ display: "preview",
40
+ lines: buildPreviewLines({
41
+ text,
42
+ kind: "context",
43
+ oldStartLine: startLine,
44
+ newStartLine: startLine,
45
+ }),
46
+ rawText: text,
47
+ oldStartLine: startLine,
48
+ newStartLine: startLine,
49
+ };
50
+ }
51
+
52
+ function buildDiffBlock(
53
+ file: ChatWorkspaceFileTab,
54
+ ): ChatFileOperationBlockViewModel | null {
55
+ if (Array.isArray(file.fullLines) && file.fullLines.length > 0) {
56
+ return {
57
+ key: `diff:${file.key}`,
58
+ path: file.path,
59
+ display: "diff",
60
+ lines: file.fullLines,
61
+ fullLines: file.fullLines,
62
+ ...(file.beforeText ? { beforeText: file.beforeText } : {}),
63
+ ...(file.afterText ? { afterText: file.afterText } : {}),
64
+ ...(file.patchText ? { patchText: file.patchText } : {}),
65
+ ...(typeof file.oldStartLine === "number"
66
+ ? { oldStartLine: file.oldStartLine }
67
+ : {}),
68
+ ...(typeof file.newStartLine === "number"
69
+ ? { newStartLine: file.newStartLine }
70
+ : {}),
71
+ };
72
+ }
73
+
74
+ if (file.beforeText == null && file.afterText == null) {
75
+ return null;
76
+ }
77
+
78
+ const lines = buildLineDiff({
79
+ beforeText: file.beforeText ?? "",
80
+ afterText: file.afterText ?? "",
81
+ oldStartLine: file.oldStartLine ?? undefined,
82
+ newStartLine: file.newStartLine ?? undefined,
83
+ });
84
+
85
+ return {
86
+ key: `diff:${file.key}`,
87
+ path: file.path,
88
+ display: "diff",
89
+ lines,
90
+ fullLines: lines,
91
+ ...(file.beforeText ? { beforeText: file.beforeText } : {}),
92
+ ...(file.afterText ? { afterText: file.afterText } : {}),
93
+ ...(typeof file.oldStartLine === "number"
94
+ ? { oldStartLine: file.oldStartLine }
95
+ : {}),
96
+ ...(typeof file.newStartLine === "number"
97
+ ? { newStartLine: file.newStartLine }
98
+ : {}),
99
+ };
100
+ }
101
+
102
+ function WorkspaceFilePreviewStatus({
103
+ text,
104
+ tone = "muted",
105
+ }: {
106
+ text: string;
107
+ tone?: "muted" | "error";
108
+ }) {
109
+ return (
110
+ <div
111
+ className={cn(
112
+ "flex h-full items-center justify-center px-6 text-center text-sm",
113
+ tone === "error" ? "text-rose-600" : "text-gray-500",
114
+ )}
115
+ >
116
+ {text}
117
+ </div>
118
+ );
119
+ }
120
+
121
+ function WorkspaceFileHeader({
122
+ file,
123
+ resolvedPath,
124
+ truncated,
125
+ }: {
126
+ file: ChatWorkspaceFileTab;
127
+ resolvedPath: string;
128
+ truncated: boolean;
129
+ }) {
130
+ return (
131
+ <div className="border-b border-gray-200/80 px-4 py-3">
132
+ <div
133
+ title={resolvedPath}
134
+ className="truncate font-mono text-[12px] font-medium text-gray-700"
135
+ >
136
+ {resolvedPath}
137
+ </div>
138
+ <div className="mt-2 flex flex-wrap items-center gap-2">
139
+ <span className="rounded border border-gray-200 bg-gray-50 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.08em] text-gray-500">
140
+ {file.viewMode === "diff"
141
+ ? t("chatWorkspaceDiff")
142
+ : t("chatWorkspacePreview")}
143
+ </span>
144
+ {typeof file.line === "number" ? (
145
+ <span className="rounded border border-gray-200 bg-gray-50 px-2 py-0.5 text-[10px] font-medium text-gray-500">
146
+ {`L${file.line}${typeof file.column === "number" ? `:${file.column}` : ""}`}
147
+ </span>
148
+ ) : null}
149
+ {truncated ? (
150
+ <span className="rounded border border-amber-200 bg-amber-50 px-2 py-0.5 text-[10px] font-medium text-amber-700">
151
+ {t("chatWorkspacePreviewTruncated")}
152
+ </span>
153
+ ) : null}
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ function WorkspaceDiffBody({
160
+ diffBlock,
161
+ }: {
162
+ diffBlock: ChatFileOperationBlockViewModel | null;
163
+ }) {
164
+ if (!diffBlock) {
165
+ return <WorkspaceFilePreviewStatus text={t("chatWorkspaceDiffEmpty")} />;
166
+ }
167
+
168
+ return (
169
+ <WorkspaceCodeSurface block={diffBlock} />
170
+ );
171
+ }
172
+
173
+ function WorkspaceCodeSurface({
174
+ block,
175
+ }: {
176
+ block: ChatFileOperationBlockViewModel;
177
+ }) {
178
+ return (
179
+ <div className="h-full overflow-auto custom-scrollbar bg-white">
180
+ <FileOperationCodeSurface block={block} layout="workspace" />
181
+ </div>
182
+ );
183
+ }
184
+
185
+ function WorkspacePreviewBody({
186
+ onFileOpen,
187
+ previewBlock,
188
+ previewKind,
189
+ previewQuery,
190
+ previewText,
191
+ }: {
192
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
193
+ previewBlock: ChatFileOperationBlockViewModel | null;
194
+ previewKind: "text" | "markdown" | "binary";
195
+ previewQuery: ReturnType<typeof useServerPathRead>;
196
+ previewText: string | null;
197
+ }) {
198
+ if (previewQuery.isLoading && !previewBlock) {
199
+ return <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingFile")} />;
200
+ }
201
+
202
+ if (previewQuery.data?.kind === "binary") {
203
+ return (
204
+ <WorkspaceFilePreviewStatus text={t("chatWorkspacePreviewUnsupported")} />
205
+ );
206
+ }
207
+
208
+ if (previewQuery.error && !previewBlock) {
209
+ return (
210
+ <WorkspaceFilePreviewStatus
211
+ tone="error"
212
+ text={
213
+ previewQuery.error instanceof Error
214
+ ? previewQuery.error.message
215
+ : String(previewQuery.error)
216
+ }
217
+ />
218
+ );
219
+ }
220
+
221
+ if (previewKind === "markdown" && previewText) {
222
+ return (
223
+ <div className="h-full overflow-auto custom-scrollbar px-5 py-4">
224
+ <ChatMessageMarkdown
225
+ text={previewText}
226
+ role="assistant"
227
+ texts={{
228
+ copyCodeLabel: t("chatCodeCopy"),
229
+ copiedCodeLabel: t("chatCodeCopied"),
230
+ }}
231
+ onFileOpen={onFileOpen}
232
+ />
233
+ </div>
234
+ );
235
+ }
236
+
237
+ if (previewBlock) {
238
+ return <WorkspaceCodeSurface block={previewBlock} />;
239
+ }
240
+
241
+ return <WorkspaceFilePreviewStatus text={t("chatWorkspacePreviewEmpty")} />;
242
+ }
243
+
244
+ type ChatSessionWorkspaceFilePreviewProps = {
245
+ file: ChatWorkspaceFileTab;
246
+ sessionProjectRoot: string | null;
247
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
248
+ };
249
+
250
+ export function ChatSessionWorkspaceFilePreview({
251
+ file,
252
+ sessionProjectRoot,
253
+ onFileOpen,
254
+ }: ChatSessionWorkspaceFilePreviewProps) {
255
+ const isPreviewMode = file.viewMode === "preview";
256
+ const previewQuery = useServerPathRead({
257
+ path: file.path,
258
+ basePath: sessionProjectRoot,
259
+ enabled: isPreviewMode,
260
+ });
261
+ const diffBlock = useMemo(
262
+ () => (file.viewMode === "diff" ? buildDiffBlock(file) : null),
263
+ [file],
264
+ );
265
+ const previewText =
266
+ isPreviewMode ? previewQuery.data?.text ?? file.rawText ?? null : null;
267
+ const previewKind = inferPreviewKind({
268
+ path: previewQuery.data?.resolvedPath ?? file.path,
269
+ serverKind: previewQuery.data?.kind,
270
+ });
271
+ const previewBlock = useMemo(() => {
272
+ if (!isPreviewMode || !previewText) {
273
+ return null;
274
+ }
275
+ return buildPreviewBlock({
276
+ path: previewQuery.data?.resolvedPath ?? file.path,
277
+ text: previewText,
278
+ line: file.line,
279
+ });
280
+ }, [file.line, file.path, isPreviewMode, previewQuery.data?.resolvedPath, previewText]);
281
+ const resolvedPath = previewQuery.data?.resolvedPath ?? file.path;
282
+ const isTruncated = Boolean(previewQuery.data?.truncated);
283
+
284
+ return (
285
+ <div className="flex h-full min-h-0 flex-col bg-white">
286
+ <WorkspaceFileHeader
287
+ file={file}
288
+ resolvedPath={resolvedPath}
289
+ truncated={isTruncated}
290
+ />
291
+
292
+ <div className="flex-1 min-h-0 overflow-hidden">
293
+ {file.viewMode === "diff" ? (
294
+ <WorkspaceDiffBody diffBlock={diffBlock} />
295
+ ) : (
296
+ <WorkspacePreviewBody
297
+ onFileOpen={onFileOpen}
298
+ previewBlock={previewBlock}
299
+ previewKind={previewKind}
300
+ previewQuery={previewQuery}
301
+ previewText={previewText}
302
+ />
303
+ )}
304
+ </div>
305
+ </div>
306
+ );
307
+ }
@@ -0,0 +1,197 @@
1
+ import { FileCode2, MessageSquareText, X } from "lucide-react";
2
+ import type { ResolvedChildSessionTab } from "@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view";
3
+ import type { ChatWorkspaceFileTab } from "@/components/chat/stores/chat-thread.store";
4
+ import { AgentIdentityAvatar } from "@/components/common/agent-identity";
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipProvider,
9
+ TooltipTrigger,
10
+ } from "@/components/ui/tooltip";
11
+ import { t } from "@/lib/i18n";
12
+ import { cn } from "@/lib/utils";
13
+
14
+ export type WorkspaceSelection =
15
+ | {
16
+ kind: "child-session";
17
+ tab: ResolvedChildSessionTab;
18
+ }
19
+ | {
20
+ kind: "file";
21
+ file: ChatWorkspaceFileTab;
22
+ };
23
+
24
+ export type WorkspaceTabViewModel = {
25
+ key: string;
26
+ kind: "child-session" | "file";
27
+ title: string;
28
+ tooltip: string;
29
+ active: boolean;
30
+ agentId?: string | null;
31
+ showUnreadDot?: boolean;
32
+ viewMode?: "preview" | "diff";
33
+ onSelect: () => void;
34
+ onClose?: () => void;
35
+ };
36
+
37
+ export function readWorkspaceFileTitle(file: ChatWorkspaceFileTab): string {
38
+ const label = file.label?.trim();
39
+ if (label) {
40
+ return label;
41
+ }
42
+ return file.path.split("/").filter(Boolean).pop() || file.path;
43
+ }
44
+
45
+ export function resolveWorkspaceSelection(params: {
46
+ activeChildSessionKey: string | null;
47
+ activeWorkspaceFileKey: string | null;
48
+ childSessionTabs: ResolvedChildSessionTab[];
49
+ workspaceFileTabs: readonly ChatWorkspaceFileTab[];
50
+ }): WorkspaceSelection | null {
51
+ const {
52
+ activeChildSessionKey,
53
+ activeWorkspaceFileKey,
54
+ childSessionTabs,
55
+ workspaceFileTabs,
56
+ } = params;
57
+
58
+ if (activeWorkspaceFileKey) {
59
+ const activeFile = workspaceFileTabs.find(
60
+ (file) => file.key === activeWorkspaceFileKey,
61
+ );
62
+ if (activeFile) {
63
+ return {
64
+ kind: "file",
65
+ file: activeFile,
66
+ };
67
+ }
68
+ }
69
+
70
+ if (activeChildSessionKey) {
71
+ const activeChild = childSessionTabs.find(
72
+ (tab) => tab.sessionKey === activeChildSessionKey,
73
+ );
74
+ if (activeChild) {
75
+ return {
76
+ kind: "child-session",
77
+ tab: activeChild,
78
+ };
79
+ }
80
+ }
81
+
82
+ if (childSessionTabs[0]) {
83
+ return {
84
+ kind: "child-session",
85
+ tab: childSessionTabs[0],
86
+ };
87
+ }
88
+
89
+ if (workspaceFileTabs[0]) {
90
+ return {
91
+ kind: "file",
92
+ file: workspaceFileTabs[0],
93
+ };
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ function WorkspaceTabIcon({
100
+ agentId,
101
+ kind,
102
+ }: Pick<WorkspaceTabViewModel, "agentId" | "kind">) {
103
+ if (kind === "file") {
104
+ return <FileCode2 className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
105
+ }
106
+
107
+ if (agentId) {
108
+ return (
109
+ <AgentIdentityAvatar
110
+ agentId={agentId}
111
+ className="h-3.5 w-3.5 shrink-0"
112
+ />
113
+ );
114
+ }
115
+
116
+ return <MessageSquareText className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
117
+ }
118
+
119
+ function WorkspaceTabItem({
120
+ tab,
121
+ }: {
122
+ tab: WorkspaceTabViewModel;
123
+ }) {
124
+ return (
125
+ <TooltipProvider delayDuration={250}>
126
+ <Tooltip>
127
+ <TooltipTrigger asChild>
128
+ <div
129
+ className={cn(
130
+ "group flex max-w-[180px] min-w-0 items-center gap-1.5 border-r border-gray-200/70 border-b-2 px-2.5 py-2 transition-colors",
131
+ tab.active
132
+ ? "border-b-primary bg-white text-gray-900"
133
+ : "border-b-transparent bg-gray-50/85 text-gray-500 hover:bg-gray-100",
134
+ )}
135
+ >
136
+ <button
137
+ type="button"
138
+ onClick={tab.onSelect}
139
+ className="flex min-w-0 flex-1 items-center gap-1.5 text-left"
140
+ >
141
+ <WorkspaceTabIcon kind={tab.kind} agentId={tab.agentId} />
142
+ <span className="min-w-0 truncate text-[12px] font-medium">
143
+ {tab.title}
144
+ </span>
145
+ {tab.kind === "file" && tab.viewMode === "diff" ? (
146
+ <span className="shrink-0 rounded border border-amber-200 bg-amber-50 px-1 py-0 text-[9px] font-medium uppercase tracking-[0.08em] text-amber-700">
147
+ {t("chatWorkspaceDiff")}
148
+ </span>
149
+ ) : null}
150
+ {tab.showUnreadDot ? (
151
+ <span
152
+ aria-label={t("chatSessionUnread")}
153
+ className="h-2 w-2 shrink-0 rounded-full bg-primary"
154
+ />
155
+ ) : null}
156
+ </button>
157
+ {tab.onClose ? (
158
+ <button
159
+ type="button"
160
+ onClick={(event) => {
161
+ event.stopPropagation();
162
+ tab.onClose?.();
163
+ }}
164
+ className={cn(
165
+ "rounded p-0.5 text-gray-400 transition-colors hover:bg-gray-200 hover:text-gray-700",
166
+ tab.active ? "opacity-100" : "opacity-0 group-hover:opacity-100",
167
+ )}
168
+ aria-label={t("chatWorkspaceCloseFile")}
169
+ >
170
+ <X className="h-3 w-3" />
171
+ </button>
172
+ ) : null}
173
+ </div>
174
+ </TooltipTrigger>
175
+ <TooltipContent side="bottom" className="max-w-[320px] text-xs">
176
+ {tab.tooltip}
177
+ </TooltipContent>
178
+ </Tooltip>
179
+ </TooltipProvider>
180
+ );
181
+ }
182
+
183
+ export function WorkspaceTabsBar({
184
+ tabs,
185
+ }: {
186
+ tabs: readonly WorkspaceTabViewModel[];
187
+ }) {
188
+ return (
189
+ <div className="overflow-x-auto border-b border-gray-200/70 bg-gray-50/85 custom-scrollbar">
190
+ <div className="flex min-w-max items-stretch">
191
+ {tabs.map((tab) => (
192
+ <WorkspaceTabItem key={tab.key} tab={tab} />
193
+ ))}
194
+ </div>
195
+ </div>
196
+ );
197
+ }