@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
@@ -8,8 +8,13 @@ import type {
8
8
  } from '@/shared/lib/api';
9
9
  import {
10
10
  normalizePersistedWorkspaceFileTab,
11
+ retainWorkspaceFileTabs,
11
12
  toPersistedWorkspaceFileTab,
12
13
  } from '@/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils';
14
+ import {
15
+ CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH,
16
+ normalizeChatWorkspacePanelWidth,
17
+ } from '@/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils';
13
18
 
14
19
  export type ChatChildSessionTab = {
15
20
  sessionKey: string;
@@ -33,6 +38,9 @@ export type ChatWorkspaceFileTab = {
33
38
  line?: number | null;
34
39
  column?: number | null;
35
40
  rawText?: string | null;
41
+ /** Attachment/binary content URL for workspace-native media preview. */
42
+ contentUrl?: string | null;
43
+ mimeType?: string | null;
36
44
  beforeText?: string | null;
37
45
  afterText?: string | null;
38
46
  patchText?: string | null;
@@ -41,9 +49,21 @@ export type ChatWorkspaceFileTab = {
41
49
  fullLines?: ChatFileOperationLineViewModel[];
42
50
  };
43
51
 
52
+ export type ChatWorkspacePanelKind =
53
+ | 'overview'
54
+ | 'child-sessions'
55
+ | 'child-session'
56
+ | 'side-chat-draft'
57
+ | 'project-files'
58
+ | 'file'
59
+ | 'cron';
60
+
44
61
  export type ChatWorkspaceNavigationEntry =
62
+ | { kind: 'overview' }
63
+ | { kind: 'child-sessions' }
45
64
  | { kind: 'child-session'; key: string }
46
65
  | { kind: 'side-chat-draft'; key: string }
66
+ | { kind: 'project-files' }
47
67
  | { kind: 'file'; key: string }
48
68
  | { kind: 'cron' };
49
69
 
@@ -66,7 +86,7 @@ export type ChatThreadSnapshot = {
66
86
  parentSessionKey?: string | null;
67
87
  parentSessionLabel?: string | null;
68
88
  workspacePanelParentKey?: string | null;
69
- activeWorkspacePanelKind?: "child-session" | "side-chat-draft" | "file" | "cron" | null;
89
+ activeWorkspacePanelKind?: ChatWorkspacePanelKind | null;
70
90
  childSessionTabs: ChatChildSessionTab[];
71
91
  activeChildSessionKey?: string | null;
72
92
  activeSideChatDraft?: ChatWorkspaceSideChatDraft | null;
@@ -74,6 +94,7 @@ export type ChatThreadSnapshot = {
74
94
  activeWorkspaceFileKey?: string | null;
75
95
  workspaceNavigationHistory: ChatWorkspaceNavigationEntry[];
76
96
  workspaceNavigationHistoryIndex: number;
97
+ workspacePanelWidth: number;
77
98
  contextWindow?: SessionContextWindowView | null;
78
99
  };
79
100
 
@@ -95,6 +116,7 @@ type PersistedChatThreadStore = {
95
116
  activeWorkspaceFileKey?: unknown;
96
117
  workspaceNavigationHistory?: unknown;
97
118
  workspaceNavigationHistoryIndex?: unknown;
119
+ workspacePanelWidth?: unknown;
98
120
  };
99
121
  };
100
122
 
@@ -107,9 +129,10 @@ type PersistedChatWorkspaceSnapshot = Pick<
107
129
  | 'activeWorkspaceFileKey'
108
130
  | 'workspaceNavigationHistory'
109
131
  | 'workspaceNavigationHistoryIndex'
132
+ | 'workspacePanelWidth'
110
133
  >;
111
134
 
112
- type PersistedWorkspacePanelKind = 'child-session' | 'file' | 'cron';
135
+ type PersistedWorkspacePanelKind = Exclude<ChatWorkspacePanelKind, 'side-chat-draft'>;
113
136
 
114
137
  const initialSnapshot: ChatThreadSnapshot = {
115
138
  sessionTypeLabel: null,
@@ -138,6 +161,7 @@ const initialSnapshot: ChatThreadSnapshot = {
138
161
  activeWorkspaceFileKey: null,
139
162
  workspaceNavigationHistory: [],
140
163
  workspaceNavigationHistoryIndex: 0,
164
+ workspacePanelWidth: CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH,
141
165
  contextWindow: null
142
166
  };
143
167
 
@@ -160,7 +184,14 @@ function normalizeHistoryIndex(value: unknown, maxIndex: number): number {
160
184
  function isWorkspacePanelKind(
161
185
  value: unknown,
162
186
  ): value is PersistedWorkspacePanelKind {
163
- return value === 'child-session' || value === 'file' || value === 'cron';
187
+ return (
188
+ value === 'overview' ||
189
+ value === 'child-sessions' ||
190
+ value === 'child-session' ||
191
+ value === 'project-files' ||
192
+ value === 'file' ||
193
+ value === 'cron'
194
+ );
164
195
  }
165
196
 
166
197
  function normalizePersistedWorkspaceNavigationEntry(
@@ -169,8 +200,13 @@ function normalizePersistedWorkspaceNavigationEntry(
169
200
  if (!isRecord(value)) {
170
201
  return null;
171
202
  }
172
- if (value.kind === 'cron') {
173
- return { kind: 'cron' };
203
+ if (
204
+ value.kind === 'overview' ||
205
+ value.kind === 'child-sessions' ||
206
+ value.kind === 'project-files' ||
207
+ value.kind === 'cron'
208
+ ) {
209
+ return { kind: value.kind };
174
210
  }
175
211
  const key = normalizeOptionalString(value.key);
176
212
  if (!key || (value.kind !== 'child-session' && value.kind !== 'file')) {
@@ -191,6 +227,15 @@ function createWorkspaceNavigationEntryFromSnapshot({
191
227
  activeChildSessionKey: string | null;
192
228
  activeWorkspaceFileKey: string | null;
193
229
  }): ChatWorkspaceNavigationEntry | null {
230
+ if (activeWorkspacePanelKind === 'overview') {
231
+ return { kind: 'overview' };
232
+ }
233
+ if (activeWorkspacePanelKind === 'child-sessions') {
234
+ return { kind: 'child-sessions' };
235
+ }
236
+ if (activeWorkspacePanelKind === 'project-files') {
237
+ return { kind: 'project-files' };
238
+ }
194
239
  if (activeWorkspacePanelKind === 'cron') {
195
240
  return { kind: 'cron' };
196
241
  }
@@ -215,17 +260,25 @@ function normalizePersistedWorkspaceSnapshot(
215
260
  if (!isRecord(value)) {
216
261
  return null;
217
262
  }
218
- const workspaceFileTabs = Array.isArray(value.workspaceFileTabs)
263
+ const workspacePanelParentKey = normalizeOptionalString(value.workspacePanelParentKey);
264
+ const activeWorkspaceFileKey = normalizeOptionalString(value.activeWorkspaceFileKey);
265
+ const normalizedWorkspaceFileTabs = Array.isArray(value.workspaceFileTabs)
219
266
  ? value.workspaceFileTabs
220
267
  .map(normalizePersistedWorkspaceFileTab)
221
268
  .filter((tab): tab is ChatWorkspaceFileTab => tab !== null)
222
- .slice(-CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS)
223
269
  : [];
224
- const activeWorkspaceFileKey = normalizeOptionalString(value.activeWorkspaceFileKey);
270
+ const workspaceFileTabs = retainWorkspaceFileTabs(
271
+ normalizedWorkspaceFileTabs,
272
+ activeWorkspaceFileKey,
273
+ CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS,
274
+ );
275
+ const sessionWorkspaceFileTabs = workspaceFileTabs.filter(
276
+ (tab) => tab.parentSessionKey === workspacePanelParentKey,
277
+ );
225
278
  const resolvedActiveWorkspaceFileKey =
226
- activeWorkspaceFileKey && workspaceFileTabs.some((tab) => tab.key === activeWorkspaceFileKey)
279
+ activeWorkspaceFileKey && sessionWorkspaceFileTabs.some((tab) => tab.key === activeWorkspaceFileKey)
227
280
  ? activeWorkspaceFileKey
228
- : workspaceFileTabs[0]?.key ?? null;
281
+ : sessionWorkspaceFileTabs[0]?.key ?? null;
229
282
  const activeWorkspacePanelKind = isWorkspacePanelKind(value.activeWorkspacePanelKind)
230
283
  ? value.activeWorkspacePanelKind
231
284
  : null;
@@ -262,13 +315,16 @@ function normalizePersistedWorkspaceSnapshot(
262
315
  : 0;
263
316
 
264
317
  return {
265
- workspacePanelParentKey: normalizeOptionalString(value.workspacePanelParentKey),
318
+ workspacePanelParentKey,
266
319
  activeWorkspacePanelKind: resolvedActiveWorkspacePanelKind,
267
320
  activeChildSessionKey,
268
321
  workspaceFileTabs,
269
322
  activeWorkspaceFileKey: resolvedActiveWorkspaceFileKey,
270
323
  workspaceNavigationHistory,
271
324
  workspaceNavigationHistoryIndex,
325
+ workspacePanelWidth: normalizeChatWorkspacePanelWidth(
326
+ value.workspacePanelWidth,
327
+ ),
272
328
  };
273
329
  }
274
330
 
@@ -306,12 +362,16 @@ export const useChatThreadStore = create<ChatThreadStore>()(
306
362
  ? null
307
363
  : state.snapshot.activeWorkspacePanelKind,
308
364
  activeChildSessionKey: state.snapshot.activeChildSessionKey,
309
- workspaceFileTabs: state.snapshot.workspaceFileTabs
310
- .slice(-CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS)
365
+ workspaceFileTabs: retainWorkspaceFileTabs(
366
+ state.snapshot.workspaceFileTabs,
367
+ state.snapshot.activeWorkspaceFileKey,
368
+ CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS,
369
+ )
311
370
  .map(toPersistedWorkspaceFileTab),
312
371
  activeWorkspaceFileKey: state.snapshot.activeWorkspaceFileKey,
313
372
  workspaceNavigationHistory,
314
373
  workspaceNavigationHistoryIndex,
374
+ workspacePanelWidth: state.snapshot.workspacePanelWidth,
315
375
  }
316
376
  };
317
377
  },
@@ -38,7 +38,7 @@ import { cn } from "@/shared/lib/utils";
38
38
  import { t } from "@/shared/lib/i18n";
39
39
  import { PageLayout, PageHeader } from "@/app/components/layout/page-layout";
40
40
  import {
41
- AlarmClock,
41
+ Search,
42
42
  RefreshCw,
43
43
  Trash2,
44
44
  Play,
@@ -77,16 +77,16 @@ function StatusBadge({ job }: { job: CronJobView }) {
77
77
  return (
78
78
  <span
79
79
  className={cn(
80
- "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold",
80
+ "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium",
81
81
  job.enabled
82
82
  ? "bg-emerald-50 text-emerald-700"
83
- : "bg-gray-100 text-gray-500",
83
+ : "bg-muted text-muted-foreground",
84
84
  )}
85
85
  >
86
86
  <span
87
87
  className={cn(
88
88
  "h-1.5 w-1.5 rounded-full",
89
- job.enabled ? "bg-emerald-400" : "bg-gray-400",
89
+ job.enabled ? "bg-emerald-500" : "bg-muted-foreground/50",
90
90
  )}
91
91
  />
92
92
  {job.enabled ? t("enabled") : t("disabled")}
@@ -104,10 +104,7 @@ function CronJobCard(props: {
104
104
  const [expanded, setExpanded] = useState(false);
105
105
  const [menuOpen, setMenuOpen] = useState(false);
106
106
 
107
- const barColor = job.enabled ? "bg-emerald-400" : "bg-gray-300";
108
-
109
107
  const handleCardClick = (e: React.MouseEvent) => {
110
- // Don't expand when clicking switch or menu
111
108
  const target = e.target as HTMLElement;
112
109
  if (target.closest("[data-no-expand]")) return;
113
110
  setExpanded(!expanded);
@@ -116,44 +113,33 @@ function CronJobCard(props: {
116
113
  return (
117
114
  <div
118
115
  className={cn(
119
- "group relative overflow-hidden rounded-xl border bg-white transition-all duration-150 cursor-pointer",
120
- expanded
121
- ? "border-gray-300 shadow-sm"
122
- : "border-gray-200 hover:border-gray-300 hover:shadow-sm",
116
+ "group overflow-hidden rounded-xl border border-border/70 bg-card transition-colors cursor-pointer",
117
+ expanded ? "bg-muted/20" : "hover:bg-muted/30",
123
118
  )}
124
119
  onClick={handleCardClick}
125
120
  >
126
- {/* Left color bar */}
127
- <div
128
- className={cn(
129
- "absolute left-0 top-0 bottom-0 w-1 rounded-l-xl",
130
- barColor,
131
- )}
132
- />
133
-
134
- <div className="pl-4 pr-3 py-3"> {/* Row 1: name + badges + actions */}
135
- <div className="flex items-center gap-3">
121
+ <div className="px-3 py-2.5">
122
+ <div className="flex items-center gap-2.5">
136
123
  <div className="min-w-0 flex-1">
137
124
  <div className="flex items-center gap-2">
138
125
  {expanded ? (
139
- <ChevronDown className="h-3.5 w-3.5 text-gray-400 shrink-0" />
126
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
140
127
  ) : (
141
- <ChevronRight className="h-3.5 w-3.5 text-gray-300 shrink-0 group-hover:text-gray-500 transition-colors" />
128
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground/50 shrink-0 group-hover:text-muted-foreground transition-colors" />
142
129
  )}
143
- <span className="text-sm font-semibold text-gray-900 truncate">
130
+ <span className="text-sm font-medium text-foreground truncate">
144
131
  {job.name || job.id}
145
132
  </span>
146
133
  <StatusBadge job={job} />
147
- {job.deleteAfterRun && (
148
- <span className="rounded-full bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-600">
134
+ {job.deleteAfterRun ? (
135
+ <span className="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
149
136
  {t("cronOneShot")}
150
137
  </span>
151
- )}
138
+ ) : null}
152
139
  </div>
153
140
  </div>
154
141
 
155
- {/* Actions - stop propagation so they don't toggle expand */}
156
- <div className="flex items-center gap-1.5 shrink-0" data-no-expand>
142
+ <div className="flex items-center gap-1 shrink-0" data-no-expand>
157
143
  <Switch
158
144
  checked={job.enabled}
159
145
  onCheckedChange={(checked) => onToggle(job, checked)}
@@ -163,7 +149,7 @@ function CronJobCard(props: {
163
149
  <Button
164
150
  variant="ghost"
165
151
  size="icon"
166
- className="h-8 w-8 text-gray-400 hover:text-gray-600"
152
+ className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-muted"
167
153
  aria-label={t("cronMoreActions")}
168
154
  >
169
155
  <MoreVertical className="h-4 w-4" />
@@ -176,9 +162,9 @@ function CronJobCard(props: {
176
162
  setMenuOpen(false);
177
163
  onRun(job);
178
164
  }}
179
- className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100"
165
+ className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-foreground transition-colors hover:bg-muted"
180
166
  >
181
- <Play className="h-3.5 w-3.5 text-gray-500" />
167
+ <Play className="h-3.5 w-3.5 text-muted-foreground" />
182
168
  {t("cronRunNow")}
183
169
  </button>
184
170
  <button
@@ -197,28 +183,27 @@ function CronJobCard(props: {
197
183
  </div>
198
184
  </div>
199
185
 
200
- {/* Row 2: schedule + next run + last run (always visible) */}
201
- <div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500 pl-6">
186
+ <div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground pl-6">
202
187
  <span className="flex items-center gap-1">
203
- <CalendarClock className="h-3 w-3 text-gray-400" />
204
- <span className="font-mono text-gray-600">
188
+ <CalendarClock className="h-3 w-3 text-muted-foreground/70" />
189
+ <span className="font-mono text-foreground/80">
205
190
  {describeCronSchedule(job)}
206
191
  </span>
207
192
  </span>
208
- {job.state.nextRunAt && (
193
+ {job.state.nextRunAt ? (
209
194
  <span className="flex items-center gap-1">
210
- <Clock className="h-3 w-3 text-gray-400" />
195
+ <Clock className="h-3 w-3 text-muted-foreground/70" />
211
196
  <span>{formatRelativeTime(job.state.nextRunAt)}</span>
212
197
  </span>
213
- )}
214
- {job.state.lastRunAt && (
198
+ ) : null}
199
+ {job.state.lastRunAt ? (
215
200
  <span className="flex items-center gap-1" data-no-expand>
216
- <span className="text-gray-300">•</span>
201
+ <span className="text-border">•</span>
217
202
  {job.state.lastStatus === "error" && job.state.lastError ? (
218
203
  <TooltipProvider delayDuration={200}>
219
204
  <Tooltip>
220
205
  <TooltipTrigger asChild>
221
- <span className="inline-flex items-center gap-0.5 text-orange-400 cursor-help">
206
+ <span className="inline-flex items-center gap-0.5 text-orange-500 cursor-help">
222
207
  <AlertCircle className="h-3 w-3" />
223
208
  </span>
224
209
  </TooltipTrigger>
@@ -235,8 +220,8 @@ function CronJobCard(props: {
235
220
  <span
236
221
  className={cn(
237
222
  job.state.lastStatus === "ok"
238
- ? "text-emerald-400"
239
- : "text-gray-400",
223
+ ? "text-emerald-500"
224
+ : "text-muted-foreground/70",
240
225
  )}
241
226
  >
242
227
  {job.state.lastStatus === "ok" ? "✓" : ""}
@@ -244,14 +229,13 @@ function CronJobCard(props: {
244
229
  )}
245
230
  <span>{formatRelativeTime(job.state.lastRunAt)}</span>
246
231
  </span>
247
- )}
232
+ ) : null}
248
233
  </div>
249
234
 
250
- {/* Row 3: message preview (always visible, 2 lines) */}
251
235
  <div className="mt-2 pl-6">
252
236
  <div
253
237
  className={cn(
254
- "text-sm text-gray-600 break-words",
238
+ "text-sm text-muted-foreground break-words",
255
239
  !expanded && "line-clamp-2",
256
240
  )}
257
241
  >
@@ -259,40 +243,38 @@ function CronJobCard(props: {
259
243
  </div>
260
244
  </div>
261
245
 
262
- {/* Expanded details */}
263
- {expanded && (
264
- <div className="mt-3 ml-6 space-y-3 animate-in fade-in-0 slide-in-from-top-1 duration-150">
265
- {/* Metadata grid */}
246
+ {expanded ? (
247
+ <div className="mt-3 ml-6 space-y-3 border-t border-border/60 pt-3">
266
248
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-2 text-xs">
267
249
  <div>
268
- <div className="text-[11px] text-gray-400 mb-0.5">
250
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
269
251
  {t("cronSessionLabel")}
270
252
  </div>
271
253
  <div
272
- className="text-gray-700 font-mono text-[11px] truncate"
254
+ className="text-foreground/80 font-mono text-[11px] truncate"
273
255
  title={describeCronSession(job)}
274
256
  >
275
257
  {describeCronSession(job)}
276
258
  </div>
277
259
  </div>
278
260
  <div>
279
- <div className="text-[11px] text-gray-400 mb-0.5">
261
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
280
262
  {t("cronNextRun")}
281
263
  </div>
282
- <div className="text-gray-700">
264
+ <div className="text-foreground/80">
283
265
  {formatCronDate(job.state.nextRunAt)}
284
266
  </div>
285
267
  </div>
286
268
  <div>
287
- <div className="text-[11px] text-gray-400 mb-0.5">
269
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
288
270
  {t("cronLastRun")}
289
271
  </div>
290
- <div className="text-gray-700">
272
+ <div className="text-foreground/80">
291
273
  {formatCronDate(job.state.lastRunAt)}
292
274
  </div>
293
275
  </div>
294
276
  <div>
295
- <div className="text-[11px] text-gray-400 mb-0.5">
277
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
296
278
  {t("cronLastStatus")}
297
279
  </div>
298
280
  <div
@@ -304,26 +286,26 @@ function CronJobCard(props: {
304
286
  ? "text-red-600"
305
287
  : job.state.lastStatus === "skipped"
306
288
  ? "text-amber-600"
307
- : "text-gray-500",
289
+ : "text-muted-foreground",
308
290
  )}
309
291
  >
310
292
  {job.state.lastStatus ?? "-"}
311
293
  </div>
312
294
  </div>
313
295
  <div>
314
- <div className="text-[11px] text-gray-400 mb-0.5">
296
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
315
297
  {t("cronCreatedAt")}
316
298
  </div>
317
- <div className="text-gray-700">
299
+ <div className="text-foreground/80">
318
300
  {formatCronDate(job.createdAt)}
319
301
  </div>
320
302
  </div>
321
303
  <div>
322
- <div className="text-[11px] text-gray-400 mb-0.5">
304
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
323
305
  {t("cronId")}
324
306
  </div>
325
307
  <div
326
- className="text-gray-700 font-mono text-[11px] truncate"
308
+ className="text-foreground/80 font-mono text-[11px] truncate"
327
309
  title={job.id}
328
310
  >
329
311
  {job.id}
@@ -331,14 +313,13 @@ function CronJobCard(props: {
331
313
  </div>
332
314
  </div>
333
315
 
334
- {/* Error */}
335
- {job.state.lastError && (
336
- <div className="rounded-lg bg-red-50 border border-red-100 px-3 py-2 text-xs text-red-600 break-words">
316
+ {job.state.lastError ? (
317
+ <div className="rounded-lg bg-red-50/80 border border-red-100 px-3 py-2 text-xs text-red-600 break-words">
337
318
  {job.state.lastError}
338
319
  </div>
339
- )}
320
+ ) : null}
340
321
  </div>
341
- )}
322
+ ) : null}
342
323
  </div>
343
324
  </div>
344
325
  );
@@ -397,7 +378,7 @@ export function CronConfig() {
397
378
  <Button
398
379
  variant="ghost"
399
380
  size="icon"
400
- className="h-9 w-9 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100"
381
+ className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted"
401
382
  onClick={() => cronQuery.refetch()}
402
383
  >
403
384
  <RefreshCw
@@ -407,16 +388,16 @@ export function CronConfig() {
407
388
  }
408
389
  />
409
390
 
410
- <div className="mb-5">
411
- <div className="flex flex-wrap gap-3 items-center">
391
+ <div className="mb-4">
392
+ <div className="flex flex-wrap gap-2.5 items-center">
412
393
  <div className="relative flex-1 min-w-[240px]">
394
+ <Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/70" />
413
395
  <Input
414
396
  value={query}
415
397
  onChange={(e) => setQuery(e.target.value)}
416
398
  placeholder={t("cronSearchPlaceholder")}
417
399
  className="pl-9"
418
400
  />
419
- <AlarmClock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
420
401
  </div>
421
402
  <div className="min-w-[140px]">
422
403
  <Select
@@ -437,7 +418,7 @@ export function CronConfig() {
437
418
  </SelectContent>
438
419
  </Select>
439
420
  </div>
440
- <div className="text-xs text-gray-400 ml-auto tabular-nums">
421
+ <div className="text-xs text-muted-foreground ml-auto tabular-nums">
441
422
  {jobs.length} / {cronQuery.data?.total ?? 0}
442
423
  </div>
443
424
  </div>
@@ -445,22 +426,24 @@ export function CronConfig() {
445
426
 
446
427
  <div className="flex-1 overflow-auto custom-scrollbar">
447
428
  {cronQuery.isLoading ? (
448
- <div className="flex flex-col items-center justify-center py-16 text-gray-400">
449
- <RefreshCw className="h-6 w-6 animate-spin mb-3" />
429
+ <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
430
+ <RefreshCw className="h-5 w-5 animate-spin mb-3" />
450
431
  <span className="text-sm">{t("cronLoading")}</span>
451
432
  </div>
452
433
  ) : jobs.length === 0 ? (
453
- <div className="flex flex-col items-center justify-center py-16 text-gray-400">
454
- <ListTodo className="h-10 w-10 mb-3 text-gray-300" />
455
- <span className="text-sm font-medium text-gray-500">
434
+ <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
435
+ <div className="mb-3 flex h-10 w-10 items-center justify-center rounded-xl bg-muted">
436
+ <ListTodo className="h-5 w-5 text-muted-foreground/70" />
437
+ </div>
438
+ <span className="text-sm font-medium text-foreground">
456
439
  {t("cronEmpty")}
457
440
  </span>
458
- <span className="text-xs text-gray-400 mt-1">
441
+ <span className="text-xs text-muted-foreground mt-1">
459
442
  {t("cronEmptyGuide")}
460
443
  </span>
461
444
  </div>
462
445
  ) : (
463
- <div className="space-y-2.5">
446
+ <div className="space-y-2">
464
447
  {jobs.map((job) => (
465
448
  <CronJobCard
466
449
  key={job.id}
@@ -1,11 +1,32 @@
1
- import { render, screen } from "@testing-library/react";
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type * as MarketplaceApi from "@/shared/lib/api";
2
4
  import {
3
5
  createMarketplaceDetailDocId,
4
6
  createMarketplaceDetailDocUrl,
5
7
  MARKETPLACE_DETAIL_DOC_BROWSER_RENDERERS,
6
8
  MARKETPLACE_DETAIL_TAB_KIND,
7
9
  } from "@/features/marketplace/components/marketplace-detail-doc";
8
- import { setMarketplaceDetailDocEntry } from "@/features/marketplace/stores/marketplace-detail-doc.store";
10
+ import {
11
+ setMarketplaceDetailDocEntry,
12
+ useMarketplaceDetailDocStore,
13
+ } from "@/features/marketplace/stores/marketplace-detail-doc.store";
14
+
15
+ const mocks = vi.hoisted(() => ({
16
+ fetchMarketplaceSkillContent: vi.fn(),
17
+ }));
18
+
19
+ vi.mock("@/shared/lib/api", async (importOriginal) => {
20
+ const actual = await importOriginal<typeof MarketplaceApi>();
21
+ return {
22
+ ...actual,
23
+ fetchMarketplaceSkillContent: mocks.fetchMarketplaceSkillContent,
24
+ };
25
+ });
26
+
27
+ vi.mock("@/shared/lib/i18n", () => ({
28
+ t: (key: string) => key,
29
+ }));
9
30
 
10
31
  function renderDetail(detailId: string) {
11
32
  const renderer = MARKETPLACE_DETAIL_DOC_BROWSER_RENDERERS[MARKETPLACE_DETAIL_TAB_KIND];
@@ -31,6 +52,11 @@ function renderDetail(detailId: string) {
31
52
  }
32
53
 
33
54
  describe("Marketplace detail doc renderer", () => {
55
+ beforeEach(() => {
56
+ mocks.fetchMarketplaceSkillContent.mockReset();
57
+ useMarketplaceDetailDocStore.setState({ entries: {} });
58
+ });
59
+
34
60
  it("renders metadata and markdown content as React content", () => {
35
61
  const detailId = createMarketplaceDetailDocId("skill", "weather");
36
62
  setMarketplaceDetailDocEntry({
@@ -75,7 +101,7 @@ describe("Marketplace detail doc renderer", () => {
75
101
  expect(screen.getAllByText(/<script>alert\(1\)<\/script>/)).toHaveLength(1);
76
102
  });
77
103
 
78
- it("renders loading and missing states inside the React panel", () => {
104
+ it("renders loading state and rehydrates missing skill details after refresh", async () => {
79
105
  const loadingId = createMarketplaceDetailDocId("skill", "loading");
80
106
  setMarketplaceDetailDocEntry({
81
107
  id: loadingId,
@@ -88,7 +114,15 @@ describe("Marketplace detail doc renderer", () => {
88
114
  const { rerender } = renderDetail(loadingId);
89
115
  expect(document.querySelector(".animate-pulse")).toBeTruthy();
90
116
 
91
- const missingId = createMarketplaceDetailDocId("skill", "missing");
117
+ mocks.fetchMarketplaceSkillContent.mockResolvedValue({
118
+ metadataRaw: "name: weather",
119
+ bodyRaw: "# Weather Skill\n\nLocal forecast",
120
+ raw: "# Weather Skill\n\nLocal forecast",
121
+ source: "catalog",
122
+ sourceUrl: "https://example.com/weather",
123
+ });
124
+
125
+ const missingId = createMarketplaceDetailDocId("skill-preview", "weather");
92
126
  const renderer = MARKETPLACE_DETAIL_DOC_BROWSER_RENDERERS[MARKETPLACE_DETAIL_TAB_KIND];
93
127
  rerender(
94
128
  <>
@@ -100,7 +134,7 @@ describe("Marketplace detail doc renderer", () => {
100
134
  tab: {
101
135
  id: "tab-1",
102
136
  kind: MARKETPLACE_DETAIL_TAB_KIND,
103
- title: "Missing",
137
+ title: "weather",
104
138
  currentUrl: createMarketplaceDetailDocUrl(missingId),
105
139
  history: [createMarketplaceDetailDocUrl(missingId)],
106
140
  historyIndex: 0,
@@ -110,6 +144,10 @@ describe("Marketplace detail doc renderer", () => {
110
144
  </>,
111
145
  );
112
146
 
113
- expect(screen.getByText("Detail unavailable")).toBeTruthy();
147
+ await waitFor(() => {
148
+ expect(screen.getByRole("heading", { name: "weather", level: 1 })).toBeTruthy();
149
+ });
150
+ expect(mocks.fetchMarketplaceSkillContent).toHaveBeenCalledWith("weather");
151
+ expect(screen.getByText("Local forecast")).toBeTruthy();
114
152
  });
115
153
  });