@nextclaw/ui 0.13.12 → 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 +19 -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-D-WYFmcG.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-IMD4W0jD.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-DzbTVebD.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-CJ6OPxAf.js → search-config-DEH80WkC.js} +1 -1
- package/dist/assets/{secrets-config-CYOHWL7P.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-panel-nav.tsx +45 -13
- package/src/features/chat/components/chat-session-workspace-panel.tsx +17 -8
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +69 -0
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +3 -0
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +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/stores/chat-thread.store.test.ts +148 -0
- package/src/features/chat/stores/chat-thread.store.ts +287 -9
- 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/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-D1X8YNTC.js +0 -105
- package/dist/assets/desktop-update-config-DOi5R1fX.js +0 -1
- package/dist/assets/doc-browser-D5FJcAyz.js +0 -1
- package/dist/assets/index-Q891JiF_.css +0 -1
- package/dist/assets/index-ZDJP_uJc.js +0 -101
- package/dist/assets/loader-circle-Dz4FssYC.js +0 -1
- package/dist/assets/mcp-marketplace-page-LmMhKH5a.js +0 -1
- package/dist/assets/mcp-marketplace-page-RqISBSqO.js +0 -40
- package/dist/assets/model-config-JUsW1Khb.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-BsDpxNzy.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
|
@@ -16,6 +16,8 @@ const mocks = vi.hoisted(() => ({
|
|
|
16
16
|
goToChatRoot: vi.fn(),
|
|
17
17
|
goToSession: vi.fn(),
|
|
18
18
|
openSessionCronPanel: vi.fn(),
|
|
19
|
+
goBackWorkspacePanel: vi.fn(),
|
|
20
|
+
goForwardWorkspacePanel: vi.fn(),
|
|
19
21
|
deleteCronJob: vi.fn(),
|
|
20
22
|
cronJobs: [] as CronJobView[],
|
|
21
23
|
setSelectedAgentId: vi.fn(),
|
|
@@ -119,6 +121,8 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
119
121
|
selectWorkspaceFile: vi.fn(),
|
|
120
122
|
closeWorkspaceFile: vi.fn(),
|
|
121
123
|
closeWorkspacePanel: vi.fn(),
|
|
124
|
+
goBackWorkspacePanel: mocks.goBackWorkspacePanel,
|
|
125
|
+
goForwardWorkspacePanel: mocks.goForwardWorkspacePanel,
|
|
122
126
|
goToParentSession: vi.fn(),
|
|
123
127
|
},
|
|
124
128
|
chatSessionListManager: {
|
|
@@ -200,8 +204,25 @@ function resetChatConversationPanelTestState() {
|
|
|
200
204
|
mocks.goToChatRoot.mockReset();
|
|
201
205
|
mocks.goToSession.mockReset();
|
|
202
206
|
mocks.openSessionCronPanel.mockReset();
|
|
207
|
+
mocks.goBackWorkspacePanel.mockReset();
|
|
208
|
+
mocks.goForwardWorkspacePanel.mockReset();
|
|
203
209
|
mocks.deleteCronJob.mockReset();
|
|
204
210
|
mocks.cronJobs = [];
|
|
211
|
+
mocks.resolvedChildTabs = [
|
|
212
|
+
{
|
|
213
|
+
sessionKey: "child-session-1",
|
|
214
|
+
parentSessionKey: "parent-session-1",
|
|
215
|
+
title: "北京天气",
|
|
216
|
+
agentId: "weather",
|
|
217
|
+
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
218
|
+
lastMessageAt: "2026-04-10T09:00:00.000Z",
|
|
219
|
+
readAt: "2026-04-10T09:00:00.000Z",
|
|
220
|
+
sessionTypeLabel: "Codex",
|
|
221
|
+
preferredModel: "openai/gpt-5.3-codex",
|
|
222
|
+
projectName: "project-alpha",
|
|
223
|
+
projectRoot: "/Users/demo/project-alpha",
|
|
224
|
+
},
|
|
225
|
+
];
|
|
205
226
|
mocks.setSelectedAgentId.mockReset();
|
|
206
227
|
mocks.setPendingSessionType.mockReset();
|
|
207
228
|
mocks.stickyBottomScroll.mockClear();
|
|
@@ -247,6 +268,8 @@ function resetChatConversationPanelTestState() {
|
|
|
247
268
|
activeChildSessionKey: null,
|
|
248
269
|
workspaceFileTabs: [],
|
|
249
270
|
activeWorkspaceFileKey: null,
|
|
271
|
+
workspaceNavigationHistory: [],
|
|
272
|
+
workspaceNavigationHistoryIndex: 0,
|
|
250
273
|
},
|
|
251
274
|
});
|
|
252
275
|
useChatSessionListStore.setState({
|
|
@@ -406,6 +429,11 @@ describe("ChatConversationPanel", () => {
|
|
|
406
429
|
messageCount: "1",
|
|
407
430
|
sending: "true",
|
|
408
431
|
});
|
|
432
|
+
expect(
|
|
433
|
+
screen
|
|
434
|
+
.getByTestId("chat-message-list")
|
|
435
|
+
.closest('[data-chat-scroll-container="true"]'),
|
|
436
|
+
).toBeTruthy();
|
|
409
437
|
expect(
|
|
410
438
|
screen.queryByText("No messages yet. Send one to start."),
|
|
411
439
|
).toBeNull();
|
|
@@ -520,6 +548,8 @@ describe("ChatConversationPanel", () => {
|
|
|
520
548
|
});
|
|
521
549
|
|
|
522
550
|
describe("ChatSessionWorkspacePanel", () => {
|
|
551
|
+
beforeEach(resetChatConversationPanelTestState);
|
|
552
|
+
|
|
523
553
|
it("renders child session tabs and active child metadata in the workspace sidebar", () => {
|
|
524
554
|
mocks.resolvedChildTabs = [
|
|
525
555
|
{
|
|
@@ -661,6 +691,45 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
661
691
|
).toContain("workspace-horizontal-scrollbar");
|
|
662
692
|
});
|
|
663
693
|
|
|
694
|
+
it("uses workspace-local backward and forward history actions in the tab bar", async () => {
|
|
695
|
+
const user = userEvent.setup();
|
|
696
|
+
|
|
697
|
+
render(
|
|
698
|
+
<ChatSessionWorkspacePanel
|
|
699
|
+
sessionKey="parent-session-1"
|
|
700
|
+
childSessionTabs={[]}
|
|
701
|
+
activeChildSessionKey={null}
|
|
702
|
+
workspaceFileTabs={[
|
|
703
|
+
{
|
|
704
|
+
key: "parent-session-1::preview::README.md",
|
|
705
|
+
parentSessionKey: "parent-session-1",
|
|
706
|
+
path: "README.md",
|
|
707
|
+
label: "README.md",
|
|
708
|
+
viewMode: "preview",
|
|
709
|
+
},
|
|
710
|
+
]}
|
|
711
|
+
activeWorkspaceFileKey="parent-session-1::preview::README.md"
|
|
712
|
+
workspaceNavigationHistory={[
|
|
713
|
+
{ kind: "child-session", key: "child-session-1" },
|
|
714
|
+
{ kind: "file", key: "parent-session-1::preview::README.md" },
|
|
715
|
+
{ kind: "cron" },
|
|
716
|
+
]}
|
|
717
|
+
workspaceNavigationHistoryIndex={1}
|
|
718
|
+
sessionProjectRoot="/Users/demo/project-alpha"
|
|
719
|
+
sessionWorkingDir="/Users/demo/project-alpha"
|
|
720
|
+
/>,
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
await user.click(screen.getByRole("button", { name: "Back in workspace" }));
|
|
724
|
+
await user.click(
|
|
725
|
+
screen.getByRole("button", { name: "Forward in workspace" }),
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
expect(mocks.goBackWorkspacePanel).toHaveBeenCalledTimes(1);
|
|
729
|
+
expect(mocks.goForwardWorkspacePanel).toHaveBeenCalledTimes(1);
|
|
730
|
+
expect(screen.queryByRole("button", { name: "Back to parent" })).toBeNull();
|
|
731
|
+
});
|
|
732
|
+
|
|
664
733
|
it("renders session cron jobs in the workspace sidebar and deletes with a neutral confirmation", async () => {
|
|
665
734
|
const user = userEvent.setup();
|
|
666
735
|
const job: CronJobView = {
|
|
@@ -194,6 +194,7 @@ function ChatConversationContent() {
|
|
|
194
194
|
<div
|
|
195
195
|
ref={threadRef}
|
|
196
196
|
onScroll={onScroll}
|
|
197
|
+
data-chat-scroll-container="true"
|
|
197
198
|
className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
|
|
198
199
|
>
|
|
199
200
|
{showWelcome ? (
|
|
@@ -363,6 +364,8 @@ function ChatSessionWorkspacePanelContainer({
|
|
|
363
364
|
activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
|
|
364
365
|
workspaceFileTabs={workspaceFileTabs}
|
|
365
366
|
activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
|
|
367
|
+
workspaceNavigationHistory={snapshot.workspaceNavigationHistory}
|
|
368
|
+
workspaceNavigationHistoryIndex={snapshot.workspaceNavigationHistoryIndex}
|
|
366
369
|
activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
|
|
367
370
|
sessionCronJobs={sessionCronJobs}
|
|
368
371
|
sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
|
|
@@ -42,6 +42,8 @@ export type ChatThreadManagerLike = {
|
|
|
42
42
|
selectWorkspaceFile: (fileKey: string) => void;
|
|
43
43
|
closeWorkspaceFile: (fileKey: string) => void;
|
|
44
44
|
closeWorkspacePanel: () => void;
|
|
45
|
+
goBackWorkspacePanel: () => void;
|
|
46
|
+
goForwardWorkspacePanel: () => void;
|
|
45
47
|
goToParentSession: () => void;
|
|
46
48
|
};
|
|
47
49
|
|
|
@@ -57,6 +57,10 @@ describe('ChatSessionListManager', () => {
|
|
|
57
57
|
activeWorkspacePanelKind: 'child-session',
|
|
58
58
|
activeChildSessionKey: 'child-session-1',
|
|
59
59
|
activeWorkspaceFileKey: null,
|
|
60
|
+
workspaceNavigationHistory: [
|
|
61
|
+
{ kind: 'child-session', key: 'child-session-1' },
|
|
62
|
+
],
|
|
63
|
+
workspaceNavigationHistoryIndex: 0,
|
|
60
64
|
},
|
|
61
65
|
});
|
|
62
66
|
});
|
|
@@ -19,33 +19,37 @@ vi.mock('@/shared/lib/api', async (importOriginal) => {
|
|
|
19
19
|
};
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
snapshot
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
useChatThreadStore.setState({
|
|
32
|
-
snapshot: {
|
|
33
|
-
...useChatThreadStore.getState().snapshot,
|
|
34
|
-
sessionKey: 'parent-session-1',
|
|
35
|
-
workspacePanelParentKey: null,
|
|
36
|
-
childSessionTabs: [
|
|
37
|
-
{
|
|
38
|
-
sessionKey: 'child-session-1',
|
|
39
|
-
parentSessionKey: 'parent-session-1',
|
|
40
|
-
label: 'Child Session 1',
|
|
41
|
-
agentId: 'reviewer',
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
activeChildSessionKey: null,
|
|
45
|
-
},
|
|
46
|
-
});
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
useChatSessionListStore.setState({
|
|
24
|
+
optimisticReadAtBySessionKey: {},
|
|
25
|
+
snapshot: {
|
|
26
|
+
...useChatSessionListStore.getState().snapshot,
|
|
27
|
+
selectedSessionKey: 'parent-session-1',
|
|
28
|
+
},
|
|
47
29
|
});
|
|
30
|
+
useChatThreadStore.setState({
|
|
31
|
+
snapshot: {
|
|
32
|
+
...useChatThreadStore.getState().snapshot,
|
|
33
|
+
sessionKey: 'parent-session-1',
|
|
34
|
+
workspacePanelParentKey: null,
|
|
35
|
+
childSessionTabs: [
|
|
36
|
+
{
|
|
37
|
+
sessionKey: 'child-session-1',
|
|
38
|
+
parentSessionKey: 'parent-session-1',
|
|
39
|
+
label: 'Child Session 1',
|
|
40
|
+
agentId: 'reviewer',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
activeChildSessionKey: null,
|
|
44
|
+
workspaceFileTabs: [],
|
|
45
|
+
activeWorkspaceFileKey: null,
|
|
46
|
+
workspaceNavigationHistory: [],
|
|
47
|
+
workspaceNavigationHistoryIndex: 0,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
});
|
|
48
51
|
|
|
52
|
+
describe('NcpChatThreadManager', () => {
|
|
49
53
|
it('opens the child-session panel for the requested parent session and keeps focus on the chosen child', () => {
|
|
50
54
|
const uiManager = {
|
|
51
55
|
goToSession: vi.fn(),
|
|
@@ -190,7 +194,129 @@ describe('NcpChatThreadManager', () => {
|
|
|
190
194
|
]),
|
|
191
195
|
);
|
|
192
196
|
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('NcpChatThreadManager workspace navigation', () => {
|
|
200
|
+
it('navigates backward and forward through workspace selections', () => {
|
|
201
|
+
const uiManager = {
|
|
202
|
+
goToSession: vi.fn(),
|
|
203
|
+
goToChatRoot: vi.fn(),
|
|
204
|
+
goToProviders: vi.fn(),
|
|
205
|
+
confirm: vi.fn(),
|
|
206
|
+
} as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
|
|
207
|
+
const manager = new NcpChatThreadManager(
|
|
208
|
+
uiManager,
|
|
209
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[1],
|
|
210
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[2],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
manager.openChildSessionPanel({
|
|
214
|
+
parentSessionKey: 'parent-session-1',
|
|
215
|
+
activeChildSessionKey: 'child-session-1',
|
|
216
|
+
});
|
|
217
|
+
manager.openFilePreview({
|
|
218
|
+
path: 'README.md',
|
|
219
|
+
label: 'README.md',
|
|
220
|
+
viewMode: 'preview',
|
|
221
|
+
});
|
|
222
|
+
manager.openSessionCronPanel('parent-session-1');
|
|
223
|
+
|
|
224
|
+
expect(useChatThreadStore.getState().snapshot.workspaceNavigationHistory).toEqual([
|
|
225
|
+
{ kind: 'child-session', key: 'child-session-1' },
|
|
226
|
+
{ kind: 'file', key: 'parent-session-1::preview::README.md' },
|
|
227
|
+
{ kind: 'cron' },
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
manager.goBackWorkspacePanel();
|
|
231
|
+
expect(useChatThreadStore.getState().snapshot).toMatchObject({
|
|
232
|
+
activeWorkspacePanelKind: 'file',
|
|
233
|
+
activeWorkspaceFileKey: 'parent-session-1::preview::README.md',
|
|
234
|
+
workspaceNavigationHistoryIndex: 1,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
manager.goBackWorkspacePanel();
|
|
238
|
+
expect(useChatThreadStore.getState().snapshot).toMatchObject({
|
|
239
|
+
activeWorkspacePanelKind: 'child-session',
|
|
240
|
+
activeChildSessionKey: 'child-session-1',
|
|
241
|
+
workspaceNavigationHistoryIndex: 0,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
manager.goForwardWorkspacePanel();
|
|
245
|
+
expect(useChatThreadStore.getState().snapshot).toMatchObject({
|
|
246
|
+
activeWorkspacePanelKind: 'file',
|
|
247
|
+
activeWorkspaceFileKey: 'parent-session-1::preview::README.md',
|
|
248
|
+
workspaceNavigationHistoryIndex: 1,
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('truncates workspace forward history after selecting a new entry', () => {
|
|
253
|
+
useChatThreadStore.getState().setSnapshot({
|
|
254
|
+
childSessionTabs: [
|
|
255
|
+
...useChatThreadStore.getState().snapshot.childSessionTabs,
|
|
256
|
+
{
|
|
257
|
+
sessionKey: 'child-session-2',
|
|
258
|
+
parentSessionKey: 'parent-session-1',
|
|
259
|
+
label: 'Child Session 2',
|
|
260
|
+
agentId: 'writer',
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
const manager = new NcpChatThreadManager(
|
|
265
|
+
{ goToSession: vi.fn() } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0],
|
|
266
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[1],
|
|
267
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[2],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
manager.openChildSessionPanel({
|
|
271
|
+
parentSessionKey: 'parent-session-1',
|
|
272
|
+
activeChildSessionKey: 'child-session-1',
|
|
273
|
+
});
|
|
274
|
+
manager.openFilePreview({
|
|
275
|
+
path: 'README.md',
|
|
276
|
+
label: 'README.md',
|
|
277
|
+
viewMode: 'preview',
|
|
278
|
+
});
|
|
279
|
+
manager.goBackWorkspacePanel();
|
|
280
|
+
manager.selectChildSessionDetail('child-session-2');
|
|
281
|
+
|
|
282
|
+
expect(useChatThreadStore.getState().snapshot.workspaceNavigationHistory).toEqual([
|
|
283
|
+
{ kind: 'child-session', key: 'child-session-1' },
|
|
284
|
+
{ kind: 'child-session', key: 'child-session-2' },
|
|
285
|
+
]);
|
|
286
|
+
expect(useChatThreadStore.getState().snapshot.workspaceNavigationHistoryIndex).toBe(1);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('removes a closed active file from workspace history and restores the previous entry', () => {
|
|
290
|
+
const manager = new NcpChatThreadManager(
|
|
291
|
+
{ goToSession: vi.fn() } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0],
|
|
292
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[1],
|
|
293
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[2],
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
manager.openChildSessionPanel({
|
|
297
|
+
parentSessionKey: 'parent-session-1',
|
|
298
|
+
activeChildSessionKey: 'child-session-1',
|
|
299
|
+
});
|
|
300
|
+
manager.openFilePreview({
|
|
301
|
+
path: 'README.md',
|
|
302
|
+
label: 'README.md',
|
|
303
|
+
viewMode: 'preview',
|
|
304
|
+
});
|
|
305
|
+
manager.closeWorkspaceFile('parent-session-1::preview::README.md');
|
|
306
|
+
|
|
307
|
+
expect(useChatThreadStore.getState().snapshot).toMatchObject({
|
|
308
|
+
activeWorkspacePanelKind: 'child-session',
|
|
309
|
+
activeChildSessionKey: 'child-session-1',
|
|
310
|
+
activeWorkspaceFileKey: null,
|
|
311
|
+
workspaceNavigationHistory: [
|
|
312
|
+
{ kind: 'child-session', key: 'child-session-1' },
|
|
313
|
+
],
|
|
314
|
+
workspaceNavigationHistoryIndex: 0,
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
193
318
|
|
|
319
|
+
describe('NcpChatThreadManager deletion', () => {
|
|
194
320
|
it('clears the selected thread state after deleting the current session', async () => {
|
|
195
321
|
const removeQueries = vi.spyOn(appQueryClient, 'removeQueries').mockImplementation(async () => undefined);
|
|
196
322
|
const uiManager = {
|
|
@@ -228,6 +354,8 @@ describe('NcpChatThreadManager', () => {
|
|
|
228
354
|
activeChildSessionKey: null,
|
|
229
355
|
workspaceFileTabs: [],
|
|
230
356
|
activeWorkspaceFileKey: null,
|
|
357
|
+
workspaceNavigationHistory: [],
|
|
358
|
+
workspaceNavigationHistoryIndex: 0,
|
|
231
359
|
});
|
|
232
360
|
expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
|
|
233
361
|
expect(deleteSummaryMock).toHaveBeenCalledWith(appQueryClient, 'parent-session-1');
|
|
@@ -11,11 +11,30 @@ import type { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
|
|
|
11
11
|
import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
|
|
12
12
|
import type {
|
|
13
13
|
ChatThreadSnapshot,
|
|
14
|
+
ChatWorkspaceNavigationEntry,
|
|
14
15
|
ChatWorkspaceFileTab,
|
|
15
16
|
} from '@/features/chat/stores/chat-thread.store';
|
|
16
17
|
import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
|
|
17
18
|
import { viewportLayoutManager } from '@/app/managers/viewport-layout.manager';
|
|
18
19
|
import { t } from '@/shared/lib/i18n';
|
|
20
|
+
import {
|
|
21
|
+
filterNavigationHistoryEntries,
|
|
22
|
+
pushNavigationHistoryEntry,
|
|
23
|
+
stepNavigationHistory,
|
|
24
|
+
} from '@/shared/lib/navigation-history';
|
|
25
|
+
|
|
26
|
+
function areWorkspaceNavigationEntriesEqual(
|
|
27
|
+
current: ChatWorkspaceNavigationEntry,
|
|
28
|
+
next: ChatWorkspaceNavigationEntry,
|
|
29
|
+
): boolean {
|
|
30
|
+
if (current.kind !== next.kind) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (current.kind === 'cron') {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return next.kind !== 'cron' && current.key === next.key;
|
|
37
|
+
}
|
|
19
38
|
|
|
20
39
|
export class NcpChatThreadManager {
|
|
21
40
|
constructor(
|
|
@@ -67,6 +86,8 @@ export class NcpChatThreadManager {
|
|
|
67
86
|
activeChildSessionKey: null,
|
|
68
87
|
workspaceFileTabs: [],
|
|
69
88
|
activeWorkspaceFileKey: null,
|
|
89
|
+
workspaceNavigationHistory: [],
|
|
90
|
+
workspaceNavigationHistoryIndex: 0,
|
|
70
91
|
});
|
|
71
92
|
};
|
|
72
93
|
|
|
@@ -134,6 +155,57 @@ export class NcpChatThreadManager {
|
|
|
134
155
|
}
|
|
135
156
|
};
|
|
136
157
|
|
|
158
|
+
private createWorkspaceSelectionPatch = (
|
|
159
|
+
entry: ChatWorkspaceNavigationEntry,
|
|
160
|
+
snapshot: ChatThreadSnapshot,
|
|
161
|
+
): Partial<ChatThreadSnapshot> | null => {
|
|
162
|
+
if (entry.kind === 'cron') {
|
|
163
|
+
return {
|
|
164
|
+
activeWorkspacePanelKind: 'cron',
|
|
165
|
+
activeChildSessionKey: null,
|
|
166
|
+
activeWorkspaceFileKey: null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (entry.kind === 'file') {
|
|
170
|
+
if (!snapshot.workspaceFileTabs.some((tab) => tab.key === entry.key)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
activeWorkspacePanelKind: 'file',
|
|
175
|
+
activeChildSessionKey: null,
|
|
176
|
+
activeWorkspaceFileKey: entry.key,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (!entry.key) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
activeWorkspacePanelKind: 'child-session',
|
|
184
|
+
activeChildSessionKey: entry.key,
|
|
185
|
+
activeWorkspaceFileKey: null,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
private setWorkspaceSelection = (
|
|
190
|
+
patch: Partial<ChatThreadSnapshot>,
|
|
191
|
+
entry: ChatWorkspaceNavigationEntry,
|
|
192
|
+
) => {
|
|
193
|
+
const { snapshot } = useChatThreadStore.getState();
|
|
194
|
+
const history = pushNavigationHistoryEntry(
|
|
195
|
+
{
|
|
196
|
+
entries: snapshot.workspaceNavigationHistory,
|
|
197
|
+
index: snapshot.workspaceNavigationHistoryIndex,
|
|
198
|
+
},
|
|
199
|
+
entry,
|
|
200
|
+
areWorkspaceNavigationEntriesEqual,
|
|
201
|
+
);
|
|
202
|
+
useChatThreadStore.getState().setSnapshot({
|
|
203
|
+
...patch,
|
|
204
|
+
workspaceNavigationHistory: [...history.entries],
|
|
205
|
+
workspaceNavigationHistoryIndex: history.index,
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
137
209
|
deleteSession = () => {
|
|
138
210
|
void this.deleteCurrentSession();
|
|
139
211
|
};
|
|
@@ -155,12 +227,20 @@ export class NcpChatThreadManager {
|
|
|
155
227
|
return;
|
|
156
228
|
}
|
|
157
229
|
const activeChildSessionKey = params.activeChildSessionKey?.trim() || null;
|
|
158
|
-
|
|
230
|
+
const patch: Partial<ChatThreadSnapshot> = {
|
|
159
231
|
workspacePanelParentKey: parentSessionKey,
|
|
160
232
|
activeWorkspacePanelKind: 'child-session',
|
|
161
233
|
activeChildSessionKey,
|
|
162
234
|
activeWorkspaceFileKey: null,
|
|
163
|
-
}
|
|
235
|
+
};
|
|
236
|
+
if (activeChildSessionKey) {
|
|
237
|
+
this.setWorkspaceSelection(patch, {
|
|
238
|
+
kind: 'child-session',
|
|
239
|
+
key: activeChildSessionKey,
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
useChatThreadStore.getState().setSnapshot(patch);
|
|
243
|
+
}
|
|
164
244
|
this.ensureWorkspaceParentRoute(parentSessionKey);
|
|
165
245
|
};
|
|
166
246
|
|
|
@@ -170,12 +250,15 @@ export class NcpChatThreadManager {
|
|
|
170
250
|
if (!nextTab) {
|
|
171
251
|
return;
|
|
172
252
|
}
|
|
173
|
-
|
|
253
|
+
this.setWorkspaceSelection({
|
|
174
254
|
workspacePanelParentKey: parentSessionKey,
|
|
175
255
|
activeWorkspacePanelKind: 'file',
|
|
176
256
|
workspaceFileTabs: this.upsertWorkspaceFileTab(nextTab),
|
|
177
257
|
activeWorkspaceFileKey: nextTab.key,
|
|
178
258
|
activeChildSessionKey: null,
|
|
259
|
+
}, {
|
|
260
|
+
kind: 'file',
|
|
261
|
+
key: nextTab.key,
|
|
179
262
|
});
|
|
180
263
|
this.ensureWorkspaceParentRoute(parentSessionKey);
|
|
181
264
|
};
|
|
@@ -202,6 +285,8 @@ export class NcpChatThreadManager {
|
|
|
202
285
|
activeWorkspacePanelKind: null,
|
|
203
286
|
activeChildSessionKey: null,
|
|
204
287
|
activeWorkspaceFileKey: null,
|
|
288
|
+
workspaceNavigationHistory: [],
|
|
289
|
+
workspaceNavigationHistoryIndex: 0,
|
|
205
290
|
});
|
|
206
291
|
this.uiManager.goToSession(action.sessionId);
|
|
207
292
|
};
|
|
@@ -215,10 +300,13 @@ export class NcpChatThreadManager {
|
|
|
215
300
|
if (!childSessionTabs.some((tab) => tab.sessionKey === normalizedSessionKey)) {
|
|
216
301
|
return;
|
|
217
302
|
}
|
|
218
|
-
|
|
303
|
+
this.setWorkspaceSelection({
|
|
219
304
|
activeChildSessionKey: normalizedSessionKey,
|
|
220
305
|
activeWorkspaceFileKey: null,
|
|
221
306
|
activeWorkspacePanelKind: 'child-session',
|
|
307
|
+
}, {
|
|
308
|
+
kind: 'child-session',
|
|
309
|
+
key: normalizedSessionKey,
|
|
222
310
|
});
|
|
223
311
|
};
|
|
224
312
|
|
|
@@ -231,10 +319,13 @@ export class NcpChatThreadManager {
|
|
|
231
319
|
if (!workspaceFileTabs.some((tab) => tab.key === normalizedFileKey)) {
|
|
232
320
|
return;
|
|
233
321
|
}
|
|
234
|
-
|
|
322
|
+
this.setWorkspaceSelection({
|
|
235
323
|
activeWorkspaceFileKey: normalizedFileKey,
|
|
236
324
|
activeChildSessionKey: null,
|
|
237
325
|
activeWorkspacePanelKind: 'file',
|
|
326
|
+
}, {
|
|
327
|
+
kind: 'file',
|
|
328
|
+
key: normalizedFileKey,
|
|
238
329
|
});
|
|
239
330
|
};
|
|
240
331
|
|
|
@@ -248,11 +339,31 @@ export class NcpChatThreadManager {
|
|
|
248
339
|
const nextTabs = workspaceFileTabs.filter(
|
|
249
340
|
(tab) => tab.key !== normalizedFileKey,
|
|
250
341
|
);
|
|
342
|
+
const history = filterNavigationHistoryEntries(
|
|
343
|
+
{
|
|
344
|
+
entries: snapshot.workspaceNavigationHistory,
|
|
345
|
+
index: snapshot.workspaceNavigationHistoryIndex,
|
|
346
|
+
},
|
|
347
|
+
(entry) => entry.kind !== 'file' || entry.key !== normalizedFileKey,
|
|
348
|
+
);
|
|
251
349
|
const nextPatch: Partial<ChatThreadSnapshot> = {
|
|
252
350
|
workspaceFileTabs: nextTabs,
|
|
351
|
+
workspaceNavigationHistory: [...history.entries],
|
|
352
|
+
workspaceNavigationHistoryIndex: history.index,
|
|
253
353
|
};
|
|
254
354
|
if (activeWorkspaceFileKey === normalizedFileKey) {
|
|
255
|
-
|
|
355
|
+
const nextSnapshot = {
|
|
356
|
+
...snapshot,
|
|
357
|
+
workspaceFileTabs: nextTabs,
|
|
358
|
+
};
|
|
359
|
+
const restoreEntry = history.entries[history.index];
|
|
360
|
+
const restorePatch = restoreEntry
|
|
361
|
+
? this.createWorkspaceSelectionPatch(restoreEntry, nextSnapshot)
|
|
362
|
+
: null;
|
|
363
|
+
Object.assign(nextPatch, restorePatch ?? {
|
|
364
|
+
activeWorkspacePanelKind: null,
|
|
365
|
+
activeWorkspaceFileKey: null,
|
|
366
|
+
});
|
|
256
367
|
}
|
|
257
368
|
useChatThreadStore.getState().setSnapshot(nextPatch);
|
|
258
369
|
};
|
|
@@ -263,6 +374,8 @@ export class NcpChatThreadManager {
|
|
|
263
374
|
activeWorkspacePanelKind: null,
|
|
264
375
|
activeChildSessionKey: null,
|
|
265
376
|
activeWorkspaceFileKey: null,
|
|
377
|
+
workspaceNavigationHistory: [],
|
|
378
|
+
workspaceNavigationHistoryIndex: 0,
|
|
266
379
|
});
|
|
267
380
|
};
|
|
268
381
|
|
|
@@ -271,15 +384,48 @@ export class NcpChatThreadManager {
|
|
|
271
384
|
if (!parentSessionKey) {
|
|
272
385
|
return;
|
|
273
386
|
}
|
|
274
|
-
|
|
387
|
+
this.setWorkspaceSelection({
|
|
275
388
|
workspacePanelParentKey: parentSessionKey,
|
|
276
389
|
activeWorkspacePanelKind: 'cron',
|
|
277
390
|
activeChildSessionKey: null,
|
|
278
391
|
activeWorkspaceFileKey: null,
|
|
392
|
+
}, {
|
|
393
|
+
kind: 'cron',
|
|
279
394
|
});
|
|
280
395
|
this.ensureWorkspaceParentRoute(parentSessionKey);
|
|
281
396
|
};
|
|
282
397
|
|
|
398
|
+
goBackWorkspacePanel = () => {
|
|
399
|
+
this.restoreWorkspaceNavigationStep('back');
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
goForwardWorkspacePanel = () => {
|
|
403
|
+
this.restoreWorkspaceNavigationStep('forward');
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
private restoreWorkspaceNavigationStep = (direction: 'back' | 'forward') => {
|
|
407
|
+
const { snapshot } = useChatThreadStore.getState();
|
|
408
|
+
const step = stepNavigationHistory(
|
|
409
|
+
{
|
|
410
|
+
entries: snapshot.workspaceNavigationHistory,
|
|
411
|
+
index: snapshot.workspaceNavigationHistoryIndex,
|
|
412
|
+
},
|
|
413
|
+
direction,
|
|
414
|
+
);
|
|
415
|
+
if (!step) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const selectionPatch = this.createWorkspaceSelectionPatch(step.entry, snapshot);
|
|
419
|
+
if (!selectionPatch) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
useChatThreadStore.getState().setSnapshot({
|
|
423
|
+
...selectionPatch,
|
|
424
|
+
workspaceNavigationHistory: [...step.history.entries],
|
|
425
|
+
workspaceNavigationHistoryIndex: step.history.index,
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
|
|
283
429
|
closeChildSessionDetail = () => {
|
|
284
430
|
this.closeWorkspacePanel();
|
|
285
431
|
};
|