@nextclaw/ui 0.13.11 → 0.13.13
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.
- package/CHANGELOG.md +34 -0
- package/dist/assets/{api-DmxQnm30.js → api-D0hARyyl.js} +1 -1
- package/dist/assets/app-presenter-provider-C7EsUuEJ.js +3 -0
- package/dist/assets/arrow-left-CzSkR5L6.js +1 -0
- package/dist/assets/book-open-DmeQPqkd.js +1 -0
- package/dist/assets/{channels-list-page-bkKDqaO5.js → channels-list-page-B0yiiH01.js} +2 -2
- package/dist/assets/chat-page-ChPjx_X9.js +105 -0
- package/dist/assets/{config-split-page-CdJEkWxv.js → config-split-page-BzfZSyAr.js} +1 -1
- package/dist/assets/{confirm-dialog-BWRNsRgb.js → confirm-dialog-CTcaBp-8.js} +1 -1
- package/dist/assets/desktop-update-config-CExpdkqD.js +1 -0
- package/dist/assets/{dist-CyWdaJvr.js → dist-C59BKqbX.js} +1 -1
- package/dist/assets/{dist-DzM2ZEVN.js → dist-Di6EHp2x.js} +1 -1
- package/dist/assets/{doc-browser-DewXa6oX.js → doc-browser-EZbnEcdk.js} +1 -1
- package/dist/assets/doc-browser-XIwe1Tg-.js +1 -0
- package/dist/assets/{doc-browser-context-CpL2m4Sp.js → doc-browser-context-BzEA1lm1.js} +1 -1
- package/dist/assets/{ellipsis-BDgTYNDX.js → ellipsis-hXUNwi63.js} +1 -1
- package/dist/assets/{es2015-WqekV0OK.js → es2015-CJlVSTh-.js} +1 -1
- package/dist/assets/{external-link-BLjo8z2a.js → external-link-J0fCK-vu.js} +1 -1
- package/dist/assets/host-capabilities-DjiRQ0Q9.js +1 -0
- package/dist/assets/index-DfZTO6Pi.js +101 -0
- package/dist/assets/index-mIb_YMrc.css +1 -0
- package/dist/assets/{key-round-Du8x3sWa.js → key-round-BzYZ0Nno.js} +1 -1
- package/dist/assets/loader-circle-BbdIlBbQ.js +1 -0
- package/dist/assets/mcp-marketplace-page-L22aCzHX.js +40 -0
- package/dist/assets/mcp-marketplace-page-f80Jmv37.js +1 -0
- package/dist/assets/model-config-DkD7vSQT.js +1 -0
- package/dist/assets/{notice-card-3a7FmAz-.js → notice-card-CD9Ok62G.js} +1 -1
- package/dist/assets/play-DHuSeKs8.js +1 -0
- package/dist/assets/plus-DrV685KC.js +1 -0
- package/dist/assets/{popover-BlJLguiZ.js → popover-CoBu3mbV.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-BgPTQa8H.js → provider-scoped-model-input-DZlmTCJU.js} +1 -1
- package/dist/assets/providers-list-CVkWiqIy.js +1 -0
- package/dist/assets/{react-BVjl1ZaK.js → react-C7HsxuKH.js} +1 -1
- package/dist/assets/{refresh-cw-DFR87zei.js → refresh-cw-CVo4nWpq.js} +1 -1
- package/dist/assets/remote-Bznw9iji.js +1 -0
- package/dist/assets/rotate-cw-CXDpRO6D.js +1 -0
- package/dist/assets/{runtime-config-page-DYH2ejUI.js → runtime-config-page-5IycbIxs.js} +1 -1
- package/dist/assets/{save-BDmIlHvN.js → save-Z1QTovIt.js} +1 -1
- package/dist/assets/search-8nY6PGm0.js +1 -0
- package/dist/assets/{search-config-BRsrQ4zK.js → search-config-DEH80WkC.js} +1 -1
- package/dist/assets/{secrets-config-eYdx5_AW.js → secrets-config-Gzo5nDJG.js} +1 -1
- package/dist/assets/{status-dot-SVPjIyG5.js → status-dot-C1-FKgvv.js} +1 -1
- package/dist/assets/{tabs-Czd2mj6z.js → tabs-BqkKwx1R.js} +1 -1
- package/dist/assets/{tabs-custom-Ix4nkp2h.js → tabs-custom-DOkaJZmt.js} +1 -1
- package/dist/assets/{tag-chip-Cdwb3JSH.js → tag-chip-3kpiMTST.js} +1 -1
- package/dist/assets/{tooltip-CKGxLGnG.js → tooltip-Bw-mXQAM.js} +1 -1
- package/dist/assets/{trash-2-yLA8BEcE.js → trash-2-CmIwAJIT.js} +1 -1
- package/dist/assets/x-CSRTOZnM.js +1 -0
- package/dist/index.html +27 -25
- package/package.json +8 -8
- package/src/features/account/components/account-panel.tsx +2 -2
- package/src/features/account/managers/account.manager.ts +6 -5
- package/src/features/chat/components/chat-session-workspace-file-preview.test.tsx +39 -0
- package/src/features/chat/components/chat-session-workspace-file-preview.tsx +11 -4
- package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +45 -13
- package/src/features/chat/components/chat-session-workspace-panel.tsx +20 -8
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +73 -0
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +4 -0
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
- package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -0
- package/src/features/chat/managers/chat-session-list.manager.test.ts +4 -0
- package/src/features/chat/managers/chat-session-list.manager.ts +2 -0
- package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +153 -25
- package/src/features/chat/managers/ncp-chat-thread.manager.ts +153 -7
- package/src/features/chat/pages/ncp-chat-page.tsx +4 -0
- package/src/features/chat/stores/chat-thread.store.test.ts +148 -0
- package/src/features/chat/stores/chat-thread.store.ts +289 -9
- package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +21 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.ts +6 -0
- package/src/features/marketplace/components/marketplace-catalog-grid.tsx +19 -1
- package/src/features/marketplace/components/marketplace-external-skill-source-action.tsx +14 -0
- package/src/features/marketplace/components/marketplace-page-parts.tsx +10 -2
- package/src/features/marketplace/components/marketplace-page.test.tsx +37 -1
- package/src/features/marketplace/components/marketplace-page.tsx +38 -16
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +1 -0
- package/src/features/marketplace/hooks/use-marketplace.test.tsx +109 -0
- package/src/features/marketplace/hooks/use-marketplace.ts +3 -5
- package/src/features/side-dock/components/side-dock.test.tsx +25 -0
- package/src/features/side-dock/components/side-dock.tsx +13 -0
- package/src/features/system-status/components/desktop-update-config.tsx +2 -1
- package/src/platforms/desktop/types/desktop-update.types.ts +9 -0
- package/src/shared/components/config/provider-form.tsx +2 -2
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +39 -32
- package/src/shared/components/ui/action-link.tsx +23 -1
- package/src/shared/lib/api/ncp-session.types.ts +1 -0
- package/src/shared/lib/host-capabilities/host-capability.manager.test.ts +50 -0
- package/src/shared/lib/host-capabilities/host-capability.manager.ts +62 -0
- package/src/shared/lib/host-capabilities/index.ts +1 -0
- package/src/shared/lib/i18n/README.md +16 -3
- package/src/shared/lib/i18n/index.ts +93 -426
- package/src/shared/lib/i18n/locales/en-US/agents.json +49 -0
- package/src/shared/lib/i18n/locales/en-US/channel-auth.json +51 -0
- package/src/shared/lib/i18n/locales/en-US/channels.json +50 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +181 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +310 -0
- package/src/shared/lib/i18n/locales/en-US/cron.json +32 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +90 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +87 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +72 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +12 -0
- package/src/shared/lib/i18n/locales/en-US/pwa.json +26 -0
- package/src/shared/lib/i18n/locales/en-US/remote.json +168 -0
- package/src/shared/lib/i18n/locales/en-US/runtime-control.json +105 -0
- package/src/shared/lib/i18n/locales/en-US/search.json +28 -0
- package/src/shared/lib/i18n/locales/zh-CN/agents.json +49 -0
- package/src/shared/lib/i18n/locales/zh-CN/channel-auth.json +51 -0
- package/src/shared/lib/i18n/locales/zh-CN/channels.json +50 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +181 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +310 -0
- package/src/shared/lib/i18n/locales/zh-CN/cron.json +32 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +90 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +87 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +72 -0
- package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +12 -0
- package/src/shared/lib/i18n/locales/zh-CN/pwa.json +26 -0
- package/src/shared/lib/i18n/locales/zh-CN/remote.json +168 -0
- package/src/shared/lib/i18n/locales/zh-CN/runtime-control.json +105 -0
- package/src/shared/lib/i18n/locales/zh-CN/search.json +28 -0
- package/src/shared/lib/navigation-history/index.ts +1 -0
- package/src/shared/lib/navigation-history/navigation-history.utils.test.ts +60 -0
- package/src/shared/lib/navigation-history/navigation-history.utils.ts +105 -0
- package/dist/assets/app-presenter-provider-CDSjiqfE.js +0 -3
- package/dist/assets/arrow-left-Dd45Mvr1.js +0 -1
- package/dist/assets/chat-page-YKdeGoVx.js +0 -105
- package/dist/assets/desktop-update-config-CLXdVz9r.js +0 -1
- package/dist/assets/doc-browser-D5FJcAyz.js +0 -1
- package/dist/assets/index-DKflQCOL.js +0 -101
- package/dist/assets/index-Q891JiF_.css +0 -1
- package/dist/assets/loader-circle-Dz4FssYC.js +0 -1
- package/dist/assets/mcp-marketplace-page-BaBuHB2L.js +0 -40
- package/dist/assets/mcp-marketplace-page-ooEvG_bB.js +0 -1
- package/dist/assets/model-config-B-qg70S4.js +0 -1
- package/dist/assets/play-BziM-Y34.js +0 -1
- package/dist/assets/plus-DrpyArC8.js +0 -1
- package/dist/assets/providers-list-cBeeslbc.js +0 -1
- package/dist/assets/remote-DjhVMpVd.js +0 -1
- package/dist/assets/rotate-cw-DMBnsYKm.js +0 -1
- package/dist/assets/search-CjHGYvTI.js +0 -1
- package/dist/assets/x-dpDISboO.js +0 -1
- package/src/shared/lib/i18n/agents.ts +0 -85
- package/src/shared/lib/i18n/channel-auth.constants.ts +0 -87
- package/src/shared/lib/i18n/channels.ts +0 -53
- package/src/shared/lib/i18n/chat-labels.utils.ts +0 -206
- package/src/shared/lib/i18n/cron-labels.utils.ts +0 -32
- package/src/shared/lib/i18n/desktop-update-labels.utils.ts +0 -165
- package/src/shared/lib/i18n/marketplace-labels.utils.ts +0 -78
- package/src/shared/lib/i18n/pwa.ts +0 -62
- package/src/shared/lib/i18n/remote.ts +0 -273
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +0 -120
- package/src/shared/lib/i18n/runtime/i18n.path-picker.ts +0 -12
- package/src/shared/lib/i18n/runtime-control-labels.utils.ts +0 -180
- package/src/shared/lib/i18n/search.ts +0 -37
- /package/dist/assets/{config-hints-Ceiol9x4.js → config-hints-fGnUjDe9.js} +0 -0
- /package/dist/assets/{provider-models-C_yOh6DE.js → provider-models-C9VRroK3.js} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
|
+
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
2
3
|
import type { MutableRefObject } from 'react';
|
|
3
4
|
import type { NcpMessage } from '@nextclaw/ncp';
|
|
4
5
|
import type { ChatFileOperationLineViewModel } from '@nextclaw/agent-chat-ui';
|
|
@@ -33,6 +34,19 @@ export type ChatWorkspaceFileTab = {
|
|
|
33
34
|
fullLines?: ChatFileOperationLineViewModel[];
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
export type ChatWorkspaceNavigationEntry =
|
|
38
|
+
| {
|
|
39
|
+
kind: 'child-session';
|
|
40
|
+
key: string;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
kind: 'file';
|
|
44
|
+
key: string;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
kind: 'cron';
|
|
48
|
+
};
|
|
49
|
+
|
|
36
50
|
export type ChatThreadSnapshot = {
|
|
37
51
|
isProviderStateResolved: boolean;
|
|
38
52
|
modelOptions: ChatModelOption[];
|
|
@@ -47,6 +61,7 @@ export type ChatThreadSnapshot = {
|
|
|
47
61
|
availableAgents?: AgentProfileView[];
|
|
48
62
|
sessionDisplayName?: string;
|
|
49
63
|
sessionProjectRoot?: string | null;
|
|
64
|
+
sessionWorkingDir?: string | null;
|
|
50
65
|
sessionProjectName?: string | null;
|
|
51
66
|
canDeleteSession: boolean;
|
|
52
67
|
isDeletePending: boolean;
|
|
@@ -64,14 +79,43 @@ export type ChatThreadSnapshot = {
|
|
|
64
79
|
activeChildSessionKey?: string | null;
|
|
65
80
|
workspaceFileTabs: ChatWorkspaceFileTab[];
|
|
66
81
|
activeWorkspaceFileKey?: string | null;
|
|
82
|
+
workspaceNavigationHistory: ChatWorkspaceNavigationEntry[];
|
|
83
|
+
workspaceNavigationHistoryIndex: number;
|
|
67
84
|
contextWindow?: SessionContextWindowView | null;
|
|
68
85
|
};
|
|
69
86
|
|
|
87
|
+
const CHAT_THREAD_WORKSPACE_STORAGE_KEY = 'nextclaw.chat.workspace-panel.state';
|
|
88
|
+
const CHAT_THREAD_WORKSPACE_STORAGE_VERSION = 2;
|
|
89
|
+
const CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS = 8;
|
|
90
|
+
|
|
70
91
|
type ChatThreadStore = {
|
|
71
92
|
snapshot: ChatThreadSnapshot;
|
|
72
93
|
setSnapshot: (patch: Partial<ChatThreadSnapshot>) => void;
|
|
73
94
|
};
|
|
74
95
|
|
|
96
|
+
type PersistedChatThreadStore = {
|
|
97
|
+
snapshot?: {
|
|
98
|
+
workspacePanelParentKey?: unknown;
|
|
99
|
+
activeWorkspacePanelKind?: unknown;
|
|
100
|
+
activeChildSessionKey?: unknown;
|
|
101
|
+
workspaceFileTabs?: unknown;
|
|
102
|
+
activeWorkspaceFileKey?: unknown;
|
|
103
|
+
workspaceNavigationHistory?: unknown;
|
|
104
|
+
workspaceNavigationHistoryIndex?: unknown;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type PersistedChatWorkspaceSnapshot = Pick<
|
|
109
|
+
ChatThreadSnapshot,
|
|
110
|
+
| 'workspacePanelParentKey'
|
|
111
|
+
| 'activeWorkspacePanelKind'
|
|
112
|
+
| 'activeChildSessionKey'
|
|
113
|
+
| 'workspaceFileTabs'
|
|
114
|
+
| 'activeWorkspaceFileKey'
|
|
115
|
+
| 'workspaceNavigationHistory'
|
|
116
|
+
| 'workspaceNavigationHistoryIndex'
|
|
117
|
+
>;
|
|
118
|
+
|
|
75
119
|
const initialSnapshot: ChatThreadSnapshot = {
|
|
76
120
|
isProviderStateResolved: false,
|
|
77
121
|
modelOptions: [],
|
|
@@ -86,6 +130,7 @@ const initialSnapshot: ChatThreadSnapshot = {
|
|
|
86
130
|
availableAgents: [],
|
|
87
131
|
sessionDisplayName: undefined,
|
|
88
132
|
sessionProjectRoot: null,
|
|
133
|
+
sessionWorkingDir: null,
|
|
89
134
|
sessionProjectName: null,
|
|
90
135
|
canDeleteSession: false,
|
|
91
136
|
isDeletePending: false,
|
|
@@ -103,16 +148,251 @@ const initialSnapshot: ChatThreadSnapshot = {
|
|
|
103
148
|
activeChildSessionKey: null,
|
|
104
149
|
workspaceFileTabs: [],
|
|
105
150
|
activeWorkspaceFileKey: null,
|
|
151
|
+
workspaceNavigationHistory: [],
|
|
152
|
+
workspaceNavigationHistoryIndex: 0,
|
|
106
153
|
contextWindow: null
|
|
107
154
|
};
|
|
108
155
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
156
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
157
|
+
return !!value && typeof value === 'object';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeOptionalString(value: unknown): string | null {
|
|
161
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
162
|
+
? value.trim()
|
|
163
|
+
: null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeOptionalNumber(value: unknown): number | null {
|
|
167
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
168
|
+
? value
|
|
169
|
+
: null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeHistoryIndex(value: unknown, maxIndex: number): number {
|
|
173
|
+
return typeof value === 'number' && Number.isInteger(value)
|
|
174
|
+
? Math.min(Math.max(0, value), maxIndex)
|
|
175
|
+
: maxIndex;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isWorkspacePanelKind(
|
|
179
|
+
value: unknown,
|
|
180
|
+
): value is NonNullable<ChatThreadSnapshot['activeWorkspacePanelKind']> {
|
|
181
|
+
return value === 'child-session' || value === 'file' || value === 'cron';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizePersistedWorkspaceNavigationEntry(
|
|
185
|
+
value: unknown,
|
|
186
|
+
): ChatWorkspaceNavigationEntry | null {
|
|
187
|
+
if (!isRecord(value)) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (value.kind === 'cron') {
|
|
191
|
+
return { kind: 'cron' };
|
|
192
|
+
}
|
|
193
|
+
const key = normalizeOptionalString(value.key);
|
|
194
|
+
if (!key || (value.kind !== 'child-session' && value.kind !== 'file')) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
kind: value.kind,
|
|
199
|
+
key,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function createWorkspaceNavigationEntryFromSnapshot({
|
|
204
|
+
activeChildSessionKey,
|
|
205
|
+
activeWorkspaceFileKey,
|
|
206
|
+
activeWorkspacePanelKind,
|
|
207
|
+
}: {
|
|
208
|
+
activeWorkspacePanelKind: ChatThreadSnapshot['activeWorkspacePanelKind'];
|
|
209
|
+
activeChildSessionKey: string | null;
|
|
210
|
+
activeWorkspaceFileKey: string | null;
|
|
211
|
+
}): ChatWorkspaceNavigationEntry | null {
|
|
212
|
+
if (activeWorkspacePanelKind === 'cron') {
|
|
213
|
+
return { kind: 'cron' };
|
|
214
|
+
}
|
|
215
|
+
if (activeWorkspacePanelKind === 'child-session' && activeChildSessionKey) {
|
|
216
|
+
return {
|
|
217
|
+
kind: 'child-session',
|
|
218
|
+
key: activeChildSessionKey,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (activeWorkspacePanelKind === 'file' && activeWorkspaceFileKey) {
|
|
222
|
+
return {
|
|
223
|
+
kind: 'file',
|
|
224
|
+
key: activeWorkspaceFileKey,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalizeOptionalText(value: unknown): string | null {
|
|
231
|
+
return typeof value === 'string' ? value : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizePersistedWorkspaceFileTab(
|
|
235
|
+
value: unknown,
|
|
236
|
+
): ChatWorkspaceFileTab | null {
|
|
237
|
+
if (
|
|
238
|
+
!isRecord(value)
|
|
239
|
+
|| typeof value.key !== 'string'
|
|
240
|
+
|| typeof value.path !== 'string'
|
|
241
|
+
|| (value.viewMode !== 'preview' && value.viewMode !== 'diff')
|
|
242
|
+
) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const key = value.key.trim();
|
|
246
|
+
const path = value.path.trim();
|
|
247
|
+
if (!key || !path) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
key,
|
|
252
|
+
parentSessionKey: normalizeOptionalString(value.parentSessionKey),
|
|
253
|
+
path,
|
|
254
|
+
label: normalizeOptionalString(value.label),
|
|
255
|
+
viewMode: value.viewMode,
|
|
256
|
+
line: normalizeOptionalNumber(value.line),
|
|
257
|
+
column: normalizeOptionalNumber(value.column),
|
|
258
|
+
rawText: normalizeOptionalText(value.rawText),
|
|
259
|
+
beforeText: normalizeOptionalText(value.beforeText),
|
|
260
|
+
afterText: normalizeOptionalText(value.afterText),
|
|
261
|
+
patchText: normalizeOptionalText(value.patchText),
|
|
262
|
+
oldStartLine: normalizeOptionalNumber(value.oldStartLine),
|
|
263
|
+
newStartLine: normalizeOptionalNumber(value.newStartLine),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function toPersistedWorkspaceFileTab(
|
|
268
|
+
tab: ChatWorkspaceFileTab,
|
|
269
|
+
): ChatWorkspaceFileTab {
|
|
270
|
+
return {
|
|
271
|
+
key: tab.key,
|
|
272
|
+
parentSessionKey: tab.parentSessionKey,
|
|
273
|
+
path: tab.path,
|
|
274
|
+
label: tab.label,
|
|
275
|
+
viewMode: tab.viewMode,
|
|
276
|
+
line: tab.line,
|
|
277
|
+
column: tab.column,
|
|
278
|
+
rawText: tab.rawText,
|
|
279
|
+
beforeText: tab.beforeText,
|
|
280
|
+
afterText: tab.afterText,
|
|
281
|
+
patchText: tab.patchText,
|
|
282
|
+
oldStartLine: tab.oldStartLine,
|
|
283
|
+
newStartLine: tab.newStartLine,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizePersistedWorkspaceSnapshot(
|
|
288
|
+
value: unknown,
|
|
289
|
+
): PersistedChatWorkspaceSnapshot | null {
|
|
290
|
+
if (!isRecord(value)) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const workspaceFileTabs = Array.isArray(value.workspaceFileTabs)
|
|
294
|
+
? value.workspaceFileTabs
|
|
295
|
+
.map(normalizePersistedWorkspaceFileTab)
|
|
296
|
+
.filter((tab): tab is ChatWorkspaceFileTab => tab !== null)
|
|
297
|
+
.slice(-CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS)
|
|
298
|
+
: [];
|
|
299
|
+
const activeWorkspaceFileKey = normalizeOptionalString(value.activeWorkspaceFileKey);
|
|
300
|
+
const resolvedActiveWorkspaceFileKey =
|
|
301
|
+
activeWorkspaceFileKey && workspaceFileTabs.some((tab) => tab.key === activeWorkspaceFileKey)
|
|
302
|
+
? activeWorkspaceFileKey
|
|
303
|
+
: workspaceFileTabs[0]?.key ?? null;
|
|
304
|
+
const activeWorkspacePanelKind = isWorkspacePanelKind(value.activeWorkspacePanelKind)
|
|
305
|
+
? value.activeWorkspacePanelKind
|
|
306
|
+
: null;
|
|
307
|
+
const resolvedActiveWorkspacePanelKind =
|
|
308
|
+
activeWorkspacePanelKind === 'file' && !resolvedActiveWorkspaceFileKey
|
|
309
|
+
? null
|
|
310
|
+
: activeWorkspacePanelKind;
|
|
311
|
+
const activeChildSessionKey = normalizeOptionalString(value.activeChildSessionKey);
|
|
312
|
+
const fallbackHistoryEntry = createWorkspaceNavigationEntryFromSnapshot({
|
|
313
|
+
activeWorkspacePanelKind: resolvedActiveWorkspacePanelKind,
|
|
314
|
+
activeChildSessionKey,
|
|
315
|
+
activeWorkspaceFileKey: resolvedActiveWorkspaceFileKey,
|
|
316
|
+
});
|
|
317
|
+
const normalizedNavigationHistory = Array.isArray(value.workspaceNavigationHistory)
|
|
318
|
+
? value.workspaceNavigationHistory
|
|
319
|
+
.map(normalizePersistedWorkspaceNavigationEntry)
|
|
320
|
+
.filter((entry): entry is ChatWorkspaceNavigationEntry => entry !== null)
|
|
321
|
+
.filter((entry) =>
|
|
322
|
+
entry.kind !== 'file' || workspaceFileTabs.some((tab) => tab.key === entry.key),
|
|
323
|
+
)
|
|
324
|
+
: [];
|
|
325
|
+
const workspaceNavigationHistory =
|
|
326
|
+
normalizedNavigationHistory.length > 0
|
|
327
|
+
? normalizedNavigationHistory
|
|
328
|
+
: fallbackHistoryEntry
|
|
329
|
+
? [fallbackHistoryEntry]
|
|
330
|
+
: [];
|
|
331
|
+
const workspaceNavigationHistoryIndex =
|
|
332
|
+
workspaceNavigationHistory.length > 0
|
|
333
|
+
? normalizeHistoryIndex(
|
|
334
|
+
value.workspaceNavigationHistoryIndex,
|
|
335
|
+
workspaceNavigationHistory.length - 1,
|
|
336
|
+
)
|
|
337
|
+
: 0;
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
workspacePanelParentKey: normalizeOptionalString(value.workspacePanelParentKey),
|
|
341
|
+
activeWorkspacePanelKind: resolvedActiveWorkspacePanelKind,
|
|
342
|
+
activeChildSessionKey,
|
|
343
|
+
workspaceFileTabs,
|
|
344
|
+
activeWorkspaceFileKey: resolvedActiveWorkspaceFileKey,
|
|
345
|
+
workspaceNavigationHistory,
|
|
346
|
+
workspaceNavigationHistoryIndex,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const useChatThreadStore = create<ChatThreadStore>()(
|
|
351
|
+
persist(
|
|
352
|
+
(set) => ({
|
|
353
|
+
snapshot: initialSnapshot,
|
|
354
|
+
setSnapshot: (patch) =>
|
|
355
|
+
set((state) => ({
|
|
356
|
+
snapshot: {
|
|
357
|
+
...state.snapshot,
|
|
358
|
+
...patch
|
|
359
|
+
}
|
|
360
|
+
}))
|
|
361
|
+
}),
|
|
362
|
+
{
|
|
363
|
+
name: CHAT_THREAD_WORKSPACE_STORAGE_KEY,
|
|
364
|
+
version: CHAT_THREAD_WORKSPACE_STORAGE_VERSION,
|
|
365
|
+
storage: createJSONStorage(() => window.localStorage),
|
|
366
|
+
partialize: (state): PersistedChatThreadStore => ({
|
|
367
|
+
snapshot: {
|
|
368
|
+
workspacePanelParentKey: state.snapshot.workspacePanelParentKey,
|
|
369
|
+
activeWorkspacePanelKind: state.snapshot.activeWorkspacePanelKind,
|
|
370
|
+
activeChildSessionKey: state.snapshot.activeChildSessionKey,
|
|
371
|
+
workspaceFileTabs: state.snapshot.workspaceFileTabs
|
|
372
|
+
.slice(-CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS)
|
|
373
|
+
.map(toPersistedWorkspaceFileTab),
|
|
374
|
+
activeWorkspaceFileKey: state.snapshot.activeWorkspaceFileKey,
|
|
375
|
+
workspaceNavigationHistory: state.snapshot.workspaceNavigationHistory,
|
|
376
|
+
workspaceNavigationHistoryIndex: state.snapshot.workspaceNavigationHistoryIndex,
|
|
377
|
+
}
|
|
378
|
+
}),
|
|
379
|
+
merge: (persistedState, currentState) => {
|
|
380
|
+
const persistedSnapshot = isRecord(persistedState)
|
|
381
|
+
? persistedState.snapshot
|
|
382
|
+
: null;
|
|
383
|
+
const workspaceSnapshot =
|
|
384
|
+
normalizePersistedWorkspaceSnapshot(persistedSnapshot);
|
|
385
|
+
if (!workspaceSnapshot) {
|
|
386
|
+
return currentState;
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
...currentState,
|
|
390
|
+
snapshot: {
|
|
391
|
+
...currentState.snapshot,
|
|
392
|
+
...workspaceSnapshot
|
|
393
|
+
}
|
|
394
|
+
};
|
|
116
395
|
}
|
|
117
|
-
}
|
|
118
|
-
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
);
|
|
@@ -22,6 +22,7 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
22
22
|
createSummary({
|
|
23
23
|
agentId: 'engineer',
|
|
24
24
|
lastMessageAt: '2026-03-18T00:00:00.000Z',
|
|
25
|
+
workingDir: '/Users/demo/workspace/project-alpha',
|
|
25
26
|
metadata: {
|
|
26
27
|
label: 'NCP Planning Thread',
|
|
27
28
|
model: 'openai/gpt-5',
|
|
@@ -40,6 +41,7 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
40
41
|
preferredModel: 'openai/gpt-5',
|
|
41
42
|
preferredThinking: 'medium',
|
|
42
43
|
projectRoot: '/Users/demo/workspace/project-alpha',
|
|
44
|
+
workingDir: '/Users/demo/workspace/project-alpha',
|
|
43
45
|
projectName: 'project-alpha',
|
|
44
46
|
lastMessageAt: '2026-03-18T00:00:00.000Z',
|
|
45
47
|
readAt: '2026-03-17T23:59:00.000Z',
|
|
@@ -50,6 +52,25 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
50
52
|
});
|
|
51
53
|
});
|
|
52
54
|
|
|
55
|
+
it('uses workingDir as the local file base even when projectRoot is absent', () => {
|
|
56
|
+
const adapted = adaptNcpSessionSummary(
|
|
57
|
+
createSummary({
|
|
58
|
+
workingDir: '/Users/demo/workspace',
|
|
59
|
+
metadata: {
|
|
60
|
+
label: 'Workspace Thread',
|
|
61
|
+
session_type: 'native',
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(adapted).toMatchObject({
|
|
67
|
+
key: 'ncp-session-1',
|
|
68
|
+
label: 'Workspace Thread',
|
|
69
|
+
workingDir: '/Users/demo/workspace',
|
|
70
|
+
});
|
|
71
|
+
expect(adapted.projectRoot).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
53
74
|
it('marks child sessions from parent_session_id metadata and keeps the request link', () => {
|
|
54
75
|
const adapted = adaptNcpSessionSummary(
|
|
55
76
|
createSummary({
|
|
@@ -120,6 +120,10 @@ function readNcpSessionProjectRoot(summary: NcpSessionSummaryView): string | nul
|
|
|
120
120
|
return normalizeSessionProjectRootValue(metadata.project_root ?? metadata.projectRoot);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function readNcpSessionWorkingDir(summary: NcpSessionSummaryView): string | null {
|
|
124
|
+
return readOptionalString(summary.workingDir);
|
|
125
|
+
}
|
|
126
|
+
|
|
123
127
|
function readNcpSessionReadAt(summary: NcpSessionSummaryView): string | null {
|
|
124
128
|
const metadata = readMetadata(summary);
|
|
125
129
|
if (!metadata) {
|
|
@@ -308,6 +312,7 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
308
312
|
const preferredModel = readNcpSessionPreferredModel(summary);
|
|
309
313
|
const preferredThinking = readNcpSessionPreferredThinking(summary);
|
|
310
314
|
const projectRoot = readNcpSessionProjectRoot(summary);
|
|
315
|
+
const workingDir = readNcpSessionWorkingDir(summary) ?? projectRoot;
|
|
311
316
|
const readAt = readNcpSessionReadAt(summary);
|
|
312
317
|
const lastMessageAt = readOptionalString(summary.lastMessageAt);
|
|
313
318
|
const projectName = getSessionProjectName(projectRoot);
|
|
@@ -328,6 +333,7 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
328
333
|
...(preferredModel ? { preferredModel } : {}),
|
|
329
334
|
...(preferredThinking ? { preferredThinking } : {}),
|
|
330
335
|
...(projectRoot ? { projectRoot } : {}),
|
|
336
|
+
...(workingDir ? { workingDir } : {}),
|
|
331
337
|
...(projectName ? { projectName } : {}),
|
|
332
338
|
sessionType: readNcpSessionType(summary),
|
|
333
339
|
sessionTypeMutable: false,
|
|
@@ -19,8 +19,10 @@ type MarketplaceCatalogGridProps = {
|
|
|
19
19
|
scope: "all" | "installed";
|
|
20
20
|
title: string;
|
|
21
21
|
summary: string;
|
|
22
|
+
refreshingLabel: string;
|
|
22
23
|
showTitle: boolean;
|
|
23
24
|
showListSkeleton: boolean;
|
|
25
|
+
isRefreshing: boolean;
|
|
24
26
|
skeletonCardCount: number;
|
|
25
27
|
allItems: MarketplaceItemSummary[];
|
|
26
28
|
installedEntries: InstalledRenderEntry[];
|
|
@@ -41,8 +43,10 @@ export function MarketplaceCatalogGrid(props: MarketplaceCatalogGridProps) {
|
|
|
41
43
|
scope,
|
|
42
44
|
title,
|
|
43
45
|
summary,
|
|
46
|
+
refreshingLabel,
|
|
44
47
|
showTitle,
|
|
45
48
|
showListSkeleton,
|
|
49
|
+
isRefreshing,
|
|
46
50
|
skeletonCardCount,
|
|
47
51
|
allItems,
|
|
48
52
|
installedEntries,
|
|
@@ -60,7 +64,21 @@ export function MarketplaceCatalogGrid(props: MarketplaceCatalogGridProps) {
|
|
|
60
64
|
{showTitle && (
|
|
61
65
|
<div className="flex items-center justify-between gap-3">
|
|
62
66
|
<h3 className="text-[14px] font-semibold text-gray-950">{title}</h3>
|
|
63
|
-
<
|
|
67
|
+
<div className="flex items-center gap-2 text-[12px] text-gray-500">
|
|
68
|
+
{isRefreshing && (
|
|
69
|
+
<span className="font-medium text-primary">{refreshingLabel}</span>
|
|
70
|
+
)}
|
|
71
|
+
<span>{summary}</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{isRefreshing && (
|
|
77
|
+
<div
|
|
78
|
+
data-testid="marketplace-list-refreshing"
|
|
79
|
+
className="h-0.5 overflow-hidden rounded-full bg-primary/10"
|
|
80
|
+
>
|
|
81
|
+
<div className="h-full w-1/3 animate-pulse rounded-full bg-primary" />
|
|
64
82
|
</div>
|
|
65
83
|
)}
|
|
66
84
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ExternalActionLink } from "@/shared/components/ui/action-link";
|
|
2
|
+
import { hostCapabilityManager } from "@/shared/lib/host-capabilities";
|
|
3
|
+
import { t } from "@/shared/lib/i18n";
|
|
4
|
+
|
|
5
|
+
const SKILLHUB_URL = "https://skillhub.cn/";
|
|
6
|
+
|
|
7
|
+
export function MarketplaceExternalSkillSourceAction() {
|
|
8
|
+
return (
|
|
9
|
+
<ExternalActionLink
|
|
10
|
+
label={t("marketplaceExternalSkillSourceTitle")}
|
|
11
|
+
onClick={() => void hostCapabilityManager.openExternalUrl(SKILLHUB_URL)}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
} from "@/shared/components/ui/select";
|
|
9
9
|
import { Skeleton } from "@/shared/components/ui/skeleton";
|
|
10
10
|
import { t } from "@/shared/lib/i18n";
|
|
11
|
-
import { PackageSearch } from "lucide-react";
|
|
11
|
+
import { Loader2, PackageSearch } from "lucide-react";
|
|
12
12
|
import type { Ref } from "react";
|
|
13
13
|
|
|
14
14
|
export function FilterPanel({
|
|
15
15
|
scope,
|
|
16
|
+
isRefreshing,
|
|
16
17
|
searchText,
|
|
17
18
|
searchPlaceholder,
|
|
18
19
|
sort,
|
|
@@ -20,6 +21,7 @@ export function FilterPanel({
|
|
|
20
21
|
onSortChange,
|
|
21
22
|
}: {
|
|
22
23
|
scope: "all" | "installed";
|
|
24
|
+
isRefreshing: boolean;
|
|
23
25
|
searchText: string;
|
|
24
26
|
searchPlaceholder: string;
|
|
25
27
|
sort: MarketplaceSort;
|
|
@@ -35,8 +37,14 @@ export function FilterPanel({
|
|
|
35
37
|
value={searchText}
|
|
36
38
|
onChange={(event) => onSearchTextChange(event.target.value)}
|
|
37
39
|
placeholder={searchPlaceholder}
|
|
38
|
-
className="h-9 w-full rounded-xl border border-gray-200/80 pl-9 pr-
|
|
40
|
+
className="h-9 w-full rounded-xl border border-gray-200/80 pl-9 pr-9 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40"
|
|
39
41
|
/>
|
|
42
|
+
{isRefreshing && (
|
|
43
|
+
<Loader2
|
|
44
|
+
className="absolute right-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 animate-spin text-primary"
|
|
45
|
+
aria-label={t("marketplaceRefreshingResults")}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
40
48
|
</div>
|
|
41
49
|
|
|
42
50
|
{scope === "all" && (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
2
|
import { MarketplacePage } from "@/features/marketplace";
|
|
3
3
|
import type {
|
|
4
4
|
MarketplaceInstalledView,
|
|
@@ -10,8 +10,10 @@ type ItemsQueryState = {
|
|
|
10
10
|
data?: MarketplaceListView;
|
|
11
11
|
isLoading: boolean;
|
|
12
12
|
isFetching: boolean;
|
|
13
|
+
isFetchingNextPage: boolean;
|
|
13
14
|
isError: boolean;
|
|
14
15
|
error: Error | null;
|
|
16
|
+
hasNextPage: boolean;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
type InstalledQueryState = {
|
|
@@ -26,6 +28,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
26
28
|
navigate: vi.fn(),
|
|
27
29
|
docOpen: vi.fn(),
|
|
28
30
|
confirm: vi.fn(),
|
|
31
|
+
openExternalUrl: vi.fn(),
|
|
29
32
|
routeParams: {} as { scene?: string },
|
|
30
33
|
itemsQuery: null as unknown as ItemsQueryState,
|
|
31
34
|
installedQuery: null as unknown as InstalledQueryState,
|
|
@@ -70,6 +73,12 @@ vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
|
|
|
70
73
|
}),
|
|
71
74
|
}));
|
|
72
75
|
|
|
76
|
+
vi.mock("@/shared/lib/host-capabilities", () => ({
|
|
77
|
+
hostCapabilityManager: {
|
|
78
|
+
openExternalUrl: mocks.openExternalUrl,
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
|
|
73
82
|
vi.mock("@/features/marketplace/hooks/use-marketplace", () => ({
|
|
74
83
|
useMarketplaceItems: () => mocks.itemsQuery,
|
|
75
84
|
useMarketplaceSkillScenes: () => ({
|
|
@@ -121,8 +130,10 @@ function createItemsQuery(overrides: Partial<Record<string, unknown>> = {}) {
|
|
|
121
130
|
data: undefined as MarketplaceListView | undefined,
|
|
122
131
|
isLoading: false,
|
|
123
132
|
isFetching: false,
|
|
133
|
+
isFetchingNextPage: false,
|
|
124
134
|
isError: false,
|
|
125
135
|
error: null,
|
|
136
|
+
hasNextPage: false,
|
|
126
137
|
...overrides,
|
|
127
138
|
};
|
|
128
139
|
}
|
|
@@ -150,6 +161,7 @@ describe("MarketplacePage", () => {
|
|
|
150
161
|
mocks.navigate.mockReset();
|
|
151
162
|
mocks.docOpen.mockReset();
|
|
152
163
|
mocks.confirm.mockReset();
|
|
164
|
+
mocks.openExternalUrl.mockReset();
|
|
153
165
|
mocks.routeParams = {};
|
|
154
166
|
mocks.installMutation.mutateAsync.mockReset();
|
|
155
167
|
mocks.manageMutation.mutate.mockReset();
|
|
@@ -198,6 +210,8 @@ describe("MarketplacePage", () => {
|
|
|
198
210
|
render(<MarketplacePage forcedType="skills" />);
|
|
199
211
|
|
|
200
212
|
expect(screen.queryByTestId("marketplace-list-skeleton")).toBeNull();
|
|
213
|
+
expect(screen.getByTestId("marketplace-list-refreshing")).toBeTruthy();
|
|
214
|
+
expect(screen.getAllByText("Updating...").length).toBeGreaterThan(0);
|
|
201
215
|
expect(screen.getByText("Web Search")).toBeTruthy();
|
|
202
216
|
});
|
|
203
217
|
|
|
@@ -240,4 +254,26 @@ describe("MarketplacePage", () => {
|
|
|
240
254
|
expect(screen.getByText("Web Search")).toBeTruthy();
|
|
241
255
|
expect(container.querySelector(".opacity-70")).toBeNull();
|
|
242
256
|
});
|
|
257
|
+
|
|
258
|
+
it("opens SkillHub from the marketplace tab row", () => {
|
|
259
|
+
mocks.itemsQuery = createItemsQuery({
|
|
260
|
+
data: {
|
|
261
|
+
total: 1,
|
|
262
|
+
page: 1,
|
|
263
|
+
pageSize: 20,
|
|
264
|
+
totalPages: 1,
|
|
265
|
+
sort: "relevance",
|
|
266
|
+
items: [createMarketplaceItem()],
|
|
267
|
+
} satisfies MarketplaceListView,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
render(<MarketplacePage forcedType="skills" />);
|
|
271
|
+
|
|
272
|
+
const openButton = screen.getByRole("button", {
|
|
273
|
+
name: /SkillHub/i,
|
|
274
|
+
});
|
|
275
|
+
fireEvent.click(openButton);
|
|
276
|
+
|
|
277
|
+
expect(mocks.openExternalUrl).toHaveBeenCalledWith("https://skillhub.cn/");
|
|
278
|
+
});
|
|
243
279
|
});
|