@nextclaw/ui 0.12.22 → 0.12.24
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 +103 -0
- package/dist/assets/{api-lwyw9j7i.js → api-D2xRKmZd.js} +5 -5
- package/dist/assets/app-manager-provider-CNaZboG4.js +1 -0
- package/dist/assets/{app-navigation.config-DgiR0c5_.js → app-navigation.config-Ihhrrt--.js} +1 -1
- package/dist/assets/{book-open-DgLqYpNY.js → book-open-DDlN5MvX.js} +1 -1
- package/dist/assets/{channels-list-page-Dl839n02.js → channels-list-page-p26lgxLk.js} +2 -2
- package/dist/assets/{chat-DwUf7AKR.js → chat-Dkh2qtuz.js} +8 -8
- package/dist/assets/{chat-page-B-FvPmA7.js → chat-page-DoTmE2wx.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-u4uYphxM.js → chunk-JZWAC4HX-Kydj4yEz.js} +1 -1
- package/dist/assets/{config-split-page-BMRGuCJQ.js → config-split-page-DIOCjj2Q.js} +1 -1
- package/dist/assets/{createLucideIcon-BZkY6emz.js → createLucideIcon-BLMK3QUd.js} +1 -1
- package/dist/assets/{desktop-update-config-D5g_gPak.js → desktop-update-config-DlpzDfKM.js} +1 -1
- package/dist/assets/{dialog-CdtCU2xX.js → dialog-C3D7Be0p.js} +1 -1
- package/dist/assets/{dist-CuqvE--P.js → dist-CPlbUgwU.js} +1 -1
- package/dist/assets/{doc-browser-BUlCkZo2.js → doc-browser-C8FM5fC0.js} +1 -1
- package/dist/assets/doc-browser-RJUOL_GO.js +1 -0
- package/dist/assets/{doc-browser-context-DfLHAWbG.js → doc-browser-context-BJuMaI3o.js} +1 -1
- package/dist/assets/{doc-browser-CzCV73NJ.js → doc-browser-p82AdNO-.js} +1 -1
- package/dist/assets/{es2015-yYU5Ad5w.js → es2015-xqN1slyW.js} +1 -1
- package/dist/assets/{external-link-Sw3ah_JD.js → external-link-DwfSfTLB.js} +1 -1
- package/dist/assets/{folder-D7-VTnkz.js → folder-CeJKPx5P.js} +1 -1
- package/dist/assets/{hash-zajSTDXZ.js → hash-BqxRTZW5.js} +1 -1
- package/dist/assets/i18n-DnTGDIRw.js +1 -0
- package/dist/assets/{index-Doxyk7L2.js → index-pBvbJ5Mt.js} +2 -2
- package/dist/assets/{key-round-CnI1mc9F.js → key-round-CJ5gDAAG.js} +1 -1
- package/dist/assets/loader-circle-fd-vQKtW.js +1 -0
- package/dist/assets/{logo-badge-BQgKnVtz.js → logo-badge-KAe-7d8c.js} +1 -1
- package/dist/assets/{logos-CqVm0q0W.js → logos-C4sYP1Vl.js} +1 -1
- package/dist/assets/marketplace-page-Cql0kDi-.js +1 -0
- package/dist/assets/{marketplace-page-CawcdL6Y.js → marketplace-page-m4P5g_Ht.js} +1 -1
- package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +1 -0
- package/dist/assets/{mcp-marketplace-page-DEGfJ_70.js → mcp-marketplace-page-ByzBQZcx.js} +1 -1
- package/dist/assets/message-square-z_osm9c0.js +1 -0
- package/dist/assets/{model-config-r-1RPSrZ.js → model-config-Dbr_0APb.js} +1 -1
- package/dist/assets/{notice-card-BPtCVEKW.js → notice-card-BFDbKQDA.js} +1 -1
- package/dist/assets/play-Dv6Nr1Ew.js +1 -0
- package/dist/assets/plus-D8eKFY7h.js +1 -0
- package/dist/assets/{popover-jbfQhYQh.js → popover-B86Dbfhf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-gdk2lmRi.js → provider-scoped-model-input-DFm6N2f7.js} +1 -1
- package/dist/assets/{providers-list-DpISIr3M.js → providers-list-BJcLOjun.js} +1 -1
- package/dist/assets/{refresh-ccw-Bii4w8aB.js → refresh-ccw-ByVwmnN_.js} +1 -1
- package/dist/assets/{refresh-cw-BxojR62w.js → refresh-cw-PcqoYB3K.js} +1 -1
- package/dist/assets/remote-BOxo9iwd.js +1 -0
- package/dist/assets/{rotate-cw-1Xqa7LZ8.js → rotate-cw-BZ2JObNs.js} +1 -1
- package/dist/assets/runtime-config-page-CjLhnbSl.js +1 -0
- package/dist/assets/{save--BVI5wZX.js → save-euRxl8pI.js} +1 -1
- package/dist/assets/{search-vChioOoe.js → search-CLd7m0M7.js} +1 -1
- package/dist/assets/{search-config-BWqz8nqY.js → search-config-J4Htco-P.js} +1 -1
- package/dist/assets/{secrets-config-CjzSNg0Y.js → secrets-config-CUdERjco.js} +1 -1
- package/dist/assets/{select-Cw5Zkb1w.js → select-CJ0wbo3D.js} +1 -1
- package/dist/assets/{sessions-config-page-beoDPtII.js → sessions-config-page-DpK991fs.js} +2 -2
- package/dist/assets/{setting-row-Cjl2d40s.js → setting-row-D1Yygqp7.js} +1 -1
- package/dist/assets/{settings-CiRChctQ.js → settings-drbWqzA4.js} +1 -1
- package/dist/assets/skeleton-BK1SOSRA.js +1 -0
- package/dist/assets/{sparkles-D1ZKWdm4.js → sparkles-DVfeSVJQ.js} +1 -1
- package/dist/assets/{status-dot-Dv_hiUVa.js → status-dot-ChvPCib9.js} +1 -1
- package/dist/assets/{tabs-custom-CsACkVji.js → tabs-custom-Hia_ong0.js} +1 -1
- package/dist/assets/{tag-chip-CoWHxYJj.js → tag-chip-FrkmkT8r.js} +1 -1
- package/dist/assets/theme-provider-0hxjiPc_.js +2 -0
- package/dist/assets/{tooltip-GYzH-Hfq.js → tooltip-Cj4yA0gH.js} +1 -1
- package/dist/assets/{trash-2-rY9ZteZX.js → trash-2-CBsHCfqq.js} +1 -1
- package/dist/assets/{use-config-BhJHD3-G.js → use-config-38Ur-89i.js} +1 -1
- package/dist/assets/{use-confirm-dialog-Bqgy3Gi-.js → use-confirm-dialog-DPQThaeU.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-BfexitoF.js → use-infinite-scroll-loader-5Gf1xQi7.js} +1 -1
- package/dist/assets/{use-viewport-layout-D33zVbr5.js → use-viewport-layout-D1XzKeip.js} +1 -1
- package/dist/assets/x-CM-XDMpk.js +1 -0
- package/dist/index.html +39 -39
- package/package.json +9 -9
- package/src/features/account/hooks/use-auth.test.ts +7 -5
- package/src/features/account/hooks/use-auth.ts +23 -20
- package/src/features/chat/components/chat-sidebar-session-item.tsx +1 -1
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +2 -2
- package/src/features/chat/components/layout/chat-sidebar.test.tsx +74 -0
- package/src/features/chat/components/layout/chat-sidebar.tsx +28 -29
- package/src/features/chat/hooks/use-hydrated-ncp-agent.test.tsx +6 -0
- package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +158 -69
- package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
- package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
- package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
- package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
- package/src/features/chat/managers/chat-session-list.manager.test.ts +19 -16
- package/src/features/chat/managers/chat-session-list.manager.ts +20 -24
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +23 -12
- package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
- package/src/features/chat/pages/ncp-chat-page.tsx +23 -13
- package/src/features/chat/stores/chat-session-list.store.ts +2 -3
- package/src/features/chat/types/chat-stream.types.ts +1 -1
- package/src/features/chat/utils/ncp-session-adapter.utils.ts +1 -1
- package/src/features/system-status/hooks/use-system-status.ts +6 -28
- package/src/features/system-status/index.ts +2 -1
- package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +14 -4
- package/src/features/system-status/managers/system-status.manager.test.ts +2 -8
- package/src/features/system-status/managers/system-status.manager.ts +20 -30
- package/src/shared/components/common/brand-header.test.tsx +84 -3
- package/src/shared/components/common/brand-header.tsx +37 -39
- package/src/shared/lib/api/managers/client.manager.ts +30 -2
- package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
- package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
- package/src/shared/lib/api/utils/config.utils.ts +6 -4
- package/src/shared/lib/i18n/desktop-update-labels.utils.ts +3 -1
- package/src/shared/lib/transport/index.ts +1 -0
- package/src/shared/lib/transport/transport.types.ts +20 -0
- package/dist/assets/app-manager-provider-C0ONQxUg.js +0 -1
- package/dist/assets/doc-browser-Doh2541x.js +0 -1
- package/dist/assets/i18n-C5Mibli1.js +0 -1
- package/dist/assets/loader-circle-B5i8oMMY.js +0 -1
- package/dist/assets/marketplace-page-BRHkZaO5.js +0 -1
- package/dist/assets/mcp-marketplace-page-CL7BF4dD.js +0 -1
- package/dist/assets/message-square-D6Z4NwpG.js +0 -1
- package/dist/assets/play-D8WJLnJe.js +0 -1
- package/dist/assets/plus-Di0KAkiO.js +0 -1
- package/dist/assets/remote-BnRNqMlb.js +0 -1
- package/dist/assets/runtime-config-page-DQ8YY8Lc.js +0 -1
- package/dist/assets/skeleton-CFQRIUzt.js +0 -1
- package/dist/assets/theme-provider-B5XReW_-.js +0 -1
- package/dist/assets/x-DpTzXQcX.js +0 -1
|
@@ -1,90 +1,179 @@
|
|
|
1
|
-
import { act, renderHook } from "@testing-library/react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
2
|
+
import {
|
|
3
|
+
type NcpAgentClientEndpoint,
|
|
4
|
+
type NcpAgentSendEnvelope,
|
|
5
|
+
type NcpEndpointEvent,
|
|
6
|
+
type NcpEndpointManifest,
|
|
7
|
+
type NcpEndpointSubscriber,
|
|
8
|
+
NcpEventType,
|
|
9
|
+
} from "@nextclaw/ncp";
|
|
10
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { DefaultNcpAgentConversationStateManager } from "../../../../../ncp-packages/nextclaw-ncp-toolkit/src/agent/agent-conversation-state-manager.ts";
|
|
4
12
|
import { useNcpAgentRuntime } from "../../../../../ncp-packages/nextclaw-ncp-react/src/hooks/use-ncp-agent-runtime.ts";
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const now = "2026-05-14T00:00:00.000Z";
|
|
15
|
+
|
|
16
|
+
class DeferredSendClient implements NcpAgentClientEndpoint {
|
|
17
|
+
readonly manifest: NcpEndpointManifest = {
|
|
18
|
+
endpointKind: "agent",
|
|
19
|
+
endpointId: "deferred-send-client",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
supportsStreaming: true,
|
|
22
|
+
supportsAbort: true,
|
|
23
|
+
supportsProactiveMessages: false,
|
|
24
|
+
supportsLiveSessionStream: true,
|
|
25
|
+
supportedPartTypes: ["text"],
|
|
26
|
+
expectedLatency: "seconds",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
readonly stop = vi.fn(async () => {});
|
|
30
|
+
readonly start = vi.fn(async () => {});
|
|
31
|
+
readonly stream = vi.fn(async () => {});
|
|
32
|
+
readonly abort = vi.fn(async () => {});
|
|
33
|
+
private listeners = new Set<NcpEndpointSubscriber>();
|
|
34
|
+
private releaseCompletion: (() => void) | null = null;
|
|
35
|
+
private completionGate = new Promise<void>((resolve) => {
|
|
36
|
+
this.releaseCompletion = resolve;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
emit = async (event: NcpEndpointEvent): Promise<void> => {
|
|
40
|
+
this.publish(event);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
subscribe = (listener: NcpEndpointSubscriber): (() => void) => {
|
|
44
|
+
this.listeners.add(listener);
|
|
45
|
+
return () => {
|
|
46
|
+
this.listeners.delete(listener);
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
send = vi.fn(async (_envelope: NcpAgentSendEnvelope): Promise<void> => {
|
|
51
|
+
this.publish({
|
|
52
|
+
type: NcpEventType.MessageSent,
|
|
53
|
+
payload: {
|
|
54
|
+
sessionId: "session-created",
|
|
55
|
+
message: {
|
|
56
|
+
id: "user-1",
|
|
57
|
+
sessionId: "session-created",
|
|
58
|
+
role: "user",
|
|
59
|
+
status: "final",
|
|
60
|
+
parts: [{ type: "text", text: "hello" }],
|
|
61
|
+
timestamp: now,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
this.publish({
|
|
66
|
+
type: NcpEventType.RunStarted,
|
|
67
|
+
payload: {
|
|
68
|
+
sessionId: "session-created",
|
|
69
|
+
messageId: "assistant-1",
|
|
70
|
+
runId: "run-1",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
await this.completionGate;
|
|
74
|
+
this.publish({
|
|
75
|
+
type: NcpEventType.MessageTextStart,
|
|
76
|
+
payload: {
|
|
77
|
+
sessionId: "session-created",
|
|
78
|
+
messageId: "assistant-1",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
this.publish({
|
|
82
|
+
type: NcpEventType.MessageTextDelta,
|
|
83
|
+
payload: {
|
|
84
|
+
sessionId: "session-created",
|
|
85
|
+
messageId: "assistant-1",
|
|
86
|
+
delta: "done",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
this.publish({
|
|
90
|
+
type: NcpEventType.MessageTextEnd,
|
|
91
|
+
payload: {
|
|
92
|
+
sessionId: "session-created",
|
|
93
|
+
messageId: "assistant-1",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
this.publish({
|
|
97
|
+
type: NcpEventType.MessageCompleted,
|
|
98
|
+
payload: {
|
|
99
|
+
sessionId: "session-created",
|
|
100
|
+
message: {
|
|
101
|
+
id: "assistant-1",
|
|
102
|
+
sessionId: "session-created",
|
|
103
|
+
role: "assistant",
|
|
104
|
+
status: "final",
|
|
105
|
+
parts: [{ type: "text", text: "done" }],
|
|
106
|
+
timestamp: now,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
this.publish({
|
|
111
|
+
type: NcpEventType.RunFinished,
|
|
112
|
+
payload: {
|
|
113
|
+
sessionId: "session-created",
|
|
114
|
+
runId: "run-1",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
release = (): void => {
|
|
120
|
+
this.releaseCompletion?.();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
private publish = (event: NcpEndpointEvent): void => {
|
|
124
|
+
for (const listener of this.listeners) {
|
|
125
|
+
listener(event);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
16
128
|
}
|
|
17
129
|
|
|
18
130
|
describe("useNcpAgentRuntime", () => {
|
|
19
131
|
beforeEach(() => {
|
|
20
|
-
vi.
|
|
132
|
+
vi.clearAllMocks();
|
|
21
133
|
});
|
|
22
134
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
const client = {
|
|
36
|
-
subscribe: vi.fn((callback: (event: NcpEndpointEvent) => void) => {
|
|
37
|
-
subscriber = callback;
|
|
38
|
-
return () => {
|
|
39
|
-
subscriber = null;
|
|
40
|
-
};
|
|
41
|
-
}),
|
|
42
|
-
stop: vi.fn().mockResolvedValue(undefined),
|
|
43
|
-
send: vi.fn().mockResolvedValue(undefined),
|
|
44
|
-
abort: vi.fn().mockResolvedValue(undefined),
|
|
45
|
-
stream: vi.fn().mockResolvedValue(undefined),
|
|
46
|
-
};
|
|
47
|
-
const manager = {
|
|
48
|
-
getSnapshot: vi.fn(() => snapshot),
|
|
49
|
-
subscribe: vi.fn(() => () => {}),
|
|
50
|
-
dispatch: vi.fn().mockResolvedValue(undefined),
|
|
51
|
-
dispatchBatch: vi.fn().mockResolvedValue(undefined),
|
|
135
|
+
it("keeps the active send stream alive when a new root chat materializes a session id", async () => {
|
|
136
|
+
const client = new DeferredSendClient();
|
|
137
|
+
const manager = new DefaultNcpAgentConversationStateManager();
|
|
138
|
+
const envelope: NcpAgentSendEnvelope = {
|
|
139
|
+
message: {
|
|
140
|
+
id: "user-1",
|
|
141
|
+
role: "user",
|
|
142
|
+
status: "final",
|
|
143
|
+
parts: [{ type: "text", text: "hello" }],
|
|
144
|
+
timestamp: now,
|
|
145
|
+
},
|
|
52
146
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
client: client as never,
|
|
58
|
-
manager: manager as never,
|
|
59
|
-
}),
|
|
147
|
+
const { result, rerender } = renderHook(
|
|
148
|
+
({ sessionId }: { sessionId?: string }) =>
|
|
149
|
+
useNcpAgentRuntime({ sessionId, client, manager: manager as never }),
|
|
150
|
+
{ initialProps: { sessionId: undefined as string | undefined } },
|
|
60
151
|
);
|
|
61
152
|
|
|
62
|
-
|
|
63
|
-
|
|
153
|
+
let sendPromise: Promise<void>;
|
|
64
154
|
act(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
),
|
|
71
|
-
);
|
|
155
|
+
sendPromise = result.current.send(envelope);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(result.current.snapshot.activeRun?.sessionId).toBe("session-created");
|
|
72
160
|
});
|
|
73
161
|
|
|
74
|
-
|
|
162
|
+
rerender({ sessionId: "session-created" });
|
|
163
|
+
|
|
164
|
+
expect(client.stop).not.toHaveBeenCalled();
|
|
75
165
|
|
|
76
166
|
await act(async () => {
|
|
77
|
-
|
|
78
|
-
await
|
|
167
|
+
client.release();
|
|
168
|
+
await sendPromise;
|
|
79
169
|
});
|
|
80
170
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
),
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
expect(result.current.snapshot.activeRun).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
expect(result.current.visibleMessages.map((message) => message.id)).toEqual([
|
|
175
|
+
"user-1",
|
|
176
|
+
"assistant-1",
|
|
88
177
|
]);
|
|
89
178
|
});
|
|
90
179
|
});
|
|
@@ -113,7 +113,7 @@ export function useNcpChatSnapshotSync(params: {
|
|
|
113
113
|
sessionTypeUnavailableMessage: string | null;
|
|
114
114
|
currentSessionTypeLabel: string;
|
|
115
115
|
currentSessionTypeIcon: ChatSessionTypeOption['icon'];
|
|
116
|
-
sessionKey: string;
|
|
116
|
+
sessionKey: string | null | undefined;
|
|
117
117
|
currentAgentId: string;
|
|
118
118
|
currentAgent: AgentProfileView | null;
|
|
119
119
|
availableAgents: AgentProfileView[];
|
|
@@ -152,7 +152,7 @@ export function useNcpChatSnapshotSync(params: {
|
|
|
152
152
|
sessionTypeUnavailableMessage: params.sessionTypeUnavailableMessage,
|
|
153
153
|
sessionTypeLabel: params.currentSessionTypeLabel,
|
|
154
154
|
sessionTypeIcon: params.currentSessionTypeIcon,
|
|
155
|
-
sessionKey: params.sessionKey,
|
|
155
|
+
sessionKey: params.sessionKey ?? null,
|
|
156
156
|
agentId: params.currentAgentId,
|
|
157
157
|
agentDisplayName: params.currentAgent?.displayName ?? null,
|
|
158
158
|
agentAvatarUrl: params.currentAgent?.avatarUrl ?? null,
|
|
@@ -23,7 +23,7 @@ export type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
|
23
23
|
|
|
24
24
|
type UseNcpChatPageDataParams = {
|
|
25
25
|
query: string;
|
|
26
|
-
sessionKey: string;
|
|
26
|
+
sessionKey: string | null;
|
|
27
27
|
projectRootOverride?: string | null;
|
|
28
28
|
currentSelectedModel: string;
|
|
29
29
|
pendingSessionType: string;
|
|
@@ -76,7 +76,7 @@ function useNcpChatModelOptions(params: {
|
|
|
76
76
|
|
|
77
77
|
function useRecentSessionPreferences(params: {
|
|
78
78
|
sessions: SessionEntryView[];
|
|
79
|
-
sessionKey: string;
|
|
79
|
+
sessionKey: string | null;
|
|
80
80
|
sessionType: string;
|
|
81
81
|
}) {
|
|
82
82
|
const { sessions, sessionKey, sessionType } = params;
|
|
@@ -84,7 +84,7 @@ function useRecentSessionPreferences(params: {
|
|
|
84
84
|
() =>
|
|
85
85
|
resolveRecentSessionPreferredValue<string>({
|
|
86
86
|
sessions,
|
|
87
|
-
selectedSessionKey: sessionKey,
|
|
87
|
+
selectedSessionKey: sessionKey ?? '',
|
|
88
88
|
sessionType,
|
|
89
89
|
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
90
90
|
}),
|
|
@@ -94,7 +94,7 @@ function useRecentSessionPreferences(params: {
|
|
|
94
94
|
() =>
|
|
95
95
|
resolveRecentSessionPreferredValue<ThinkingLevel>({
|
|
96
96
|
sessions,
|
|
97
|
-
selectedSessionKey: sessionKey,
|
|
97
|
+
selectedSessionKey: sessionKey ?? '',
|
|
98
98
|
sessionType,
|
|
99
99
|
readPreference: (session) => session.preferredThinking ?? undefined
|
|
100
100
|
}),
|
|
@@ -158,7 +158,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
158
158
|
const sessionsQuery = useNcpSessions({ limit: 200 });
|
|
159
159
|
const sessionTypesQuery = useNcpChatSessionTypes();
|
|
160
160
|
const sessionSkillsQuery = useNcpSessionSkills({
|
|
161
|
-
sessionId: sessionKey,
|
|
161
|
+
sessionId: sessionKey ?? null,
|
|
162
162
|
...(Object.prototype.hasOwnProperty.call(params, 'projectRootOverride')
|
|
163
163
|
? { projectRoot: projectRootOverride ?? null }
|
|
164
164
|
: {})
|
|
@@ -219,7 +219,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
219
219
|
|
|
220
220
|
useSyncSelectedModel({
|
|
221
221
|
modelOptions: filteredModelOptions,
|
|
222
|
-
selectedSessionKey: sessionKey,
|
|
222
|
+
selectedSessionKey: sessionKey ?? '',
|
|
223
223
|
selectedSessionExists: Boolean(selectedSession),
|
|
224
224
|
selectedSessionPreferredModel: selectedSession?.preferredModel,
|
|
225
225
|
fallbackPreferredModel: sessionTypeState.selectedSessionTypeOption?.recommendedModel ?? recentSessionPreferredModel,
|
|
@@ -228,7 +228,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
228
228
|
});
|
|
229
229
|
useSyncSelectedThinking({
|
|
230
230
|
supportedThinkingLevels,
|
|
231
|
-
selectedSessionKey: sessionKey,
|
|
231
|
+
selectedSessionKey: sessionKey ?? '',
|
|
232
232
|
selectedSessionExists: Boolean(selectedSession),
|
|
233
233
|
selectedSessionPreferredThinking: selectedSession?.preferredThinking ?? null,
|
|
234
234
|
fallbackPreferredThinking: recentSessionPreferredThinking ?? null,
|
|
@@ -146,6 +146,16 @@ describe("useNcpSessionConversation", () => {
|
|
|
146
146
|
expect(mocks.hydratedCalls[1]?.client).toBe(mocks.clientInstances[1]);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
it("passes an empty session through without requesting a draft history seed", () => {
|
|
150
|
+
renderHook(() => useNcpSessionConversation(undefined));
|
|
151
|
+
|
|
152
|
+
expect(mocks.useHydratedNcpAgent).toHaveBeenCalledTimes(1);
|
|
153
|
+
expect(mocks.hydratedCalls[0]).toMatchObject({
|
|
154
|
+
sessionId: undefined,
|
|
155
|
+
});
|
|
156
|
+
expect(mocks.fetchNcpSessionMessages).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
149
159
|
it("exposes the hydrated session context window without changing the generic ncp agent seed", async () => {
|
|
150
160
|
const contextWindow = {
|
|
151
161
|
usedContextTokens: 42,
|
|
@@ -93,7 +93,7 @@ function useSyncReadyRetryVersion(
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export function useNcpSessionConversation(
|
|
96
|
-
sessionId: string,
|
|
96
|
+
sessionId: string | undefined,
|
|
97
97
|
options: UseNcpSessionConversationOptions = {},
|
|
98
98
|
) {
|
|
99
99
|
const [client] = useState(() => createNcpSessionConversationClient());
|
|
@@ -126,6 +126,7 @@ export function useNcpSessionConversation(
|
|
|
126
126
|
const currentAgentError =
|
|
127
127
|
agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
|
|
128
128
|
const readyRetrySignature =
|
|
129
|
+
sessionId &&
|
|
129
130
|
systemStatus.phase === "ready" &&
|
|
130
131
|
isNcpAgentStartupUnavailableErrorMessage(currentAgentError)
|
|
131
132
|
? `${sessionId}:${systemStatus.lastReadyAt ?? 0}`
|
|
@@ -6,15 +6,13 @@ import { buildChatContextWindowIndicator } from '@/features/chat/utils/chat-cont
|
|
|
6
6
|
|
|
7
7
|
export function useSelectedSessionContextWindowIndicator(): ChatContextWindowIndicator | null {
|
|
8
8
|
const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
|
|
9
|
-
const draftSessionKey = useChatSessionListStore((state) => state.snapshot.draftSessionKey);
|
|
10
9
|
const liveSessionKey = useChatThreadStore((state) => state.snapshot.sessionKey);
|
|
11
10
|
const liveContextWindow = useChatThreadStore((state) => state.snapshot.contextWindow);
|
|
12
|
-
const currentSessionKey = selectedSessionKey ?? draftSessionKey;
|
|
13
11
|
|
|
14
12
|
return useMemo(() => {
|
|
15
|
-
if (liveSessionKey ===
|
|
13
|
+
if (selectedSessionKey && liveSessionKey === selectedSessionKey && liveContextWindow) {
|
|
16
14
|
return buildChatContextWindowIndicator(liveContextWindow);
|
|
17
15
|
}
|
|
18
16
|
return null;
|
|
19
|
-
}, [
|
|
17
|
+
}, [liveContextWindow, liveSessionKey, selectedSessionKey]);
|
|
20
18
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type * as SharedApi from '@/shared/lib/api';
|
|
2
3
|
import { ChatSessionListManager } from '@/features/chat/managers/chat-session-list.manager';
|
|
3
4
|
import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
|
|
4
5
|
import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
|
|
@@ -9,7 +10,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
9
10
|
}));
|
|
10
11
|
|
|
11
12
|
vi.mock('@/shared/lib/api', async (importOriginal) => {
|
|
12
|
-
const actual = await importOriginal<typeof
|
|
13
|
+
const actual = await importOriginal<typeof SharedApi>();
|
|
13
14
|
return {
|
|
14
15
|
...actual,
|
|
15
16
|
updateNcpSession: mocks.updateNcpSession,
|
|
@@ -59,13 +60,13 @@ describe('ChatSessionListManager', () => {
|
|
|
59
60
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
60
61
|
|
|
61
62
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
62
|
-
|
|
63
|
+
manager.createSession('codex');
|
|
63
64
|
|
|
64
65
|
expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
|
|
65
66
|
expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
66
67
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
67
|
-
expect(useChatSessionListStore.getState().snapshot.draftSessionKey).
|
|
68
|
-
expect(
|
|
68
|
+
expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
|
|
69
|
+
expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
|
|
69
70
|
expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
|
|
70
71
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
|
|
71
72
|
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
@@ -82,14 +83,14 @@ describe('ChatSessionListManager', () => {
|
|
|
82
83
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
83
84
|
|
|
84
85
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
85
|
-
|
|
86
|
+
manager.startAgentDraftChat('researcher', 'codex');
|
|
86
87
|
|
|
87
88
|
expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
|
|
88
89
|
expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
89
90
|
expect(useChatSessionListStore.getState().snapshot.selectedAgentId).toBe('researcher');
|
|
90
91
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
91
|
-
expect(useChatSessionListStore.getState().snapshot.draftSessionKey).
|
|
92
|
-
expect(useChatThreadStore.getState().snapshot.sessionKey).
|
|
92
|
+
expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
|
|
93
|
+
expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
|
|
93
94
|
expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
|
|
94
95
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
|
|
95
96
|
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
@@ -106,13 +107,13 @@ describe('ChatSessionListManager', () => {
|
|
|
106
107
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
107
108
|
|
|
108
109
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
109
|
-
|
|
110
|
+
manager.createSession('native', '/tmp/project-alpha');
|
|
110
111
|
|
|
111
112
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBe('/tmp/project-alpha');
|
|
112
|
-
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).
|
|
113
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
113
114
|
});
|
|
114
115
|
|
|
115
|
-
it('
|
|
116
|
+
it('keeps the root draft key empty when send flow has no concrete session yet', () => {
|
|
116
117
|
useChatSessionListStore.setState({
|
|
117
118
|
snapshot: {
|
|
118
119
|
...useChatSessionListStore.getState().snapshot,
|
|
@@ -132,7 +133,7 @@ describe('ChatSessionListManager', () => {
|
|
|
132
133
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
133
134
|
const sessionKey = manager.ensureDraftSession('native');
|
|
134
135
|
|
|
135
|
-
expect(sessionKey).
|
|
136
|
+
expect(sessionKey).toBeNull();
|
|
136
137
|
expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
|
|
137
138
|
expect(uiManager.goToSession).not.toHaveBeenCalled();
|
|
138
139
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
@@ -149,10 +150,10 @@ describe('ChatSessionListManager', () => {
|
|
|
149
150
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
150
151
|
|
|
151
152
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
152
|
-
|
|
153
|
+
manager.createSession('native', '/tmp/project-alpha');
|
|
153
154
|
|
|
154
155
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
155
|
-
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).
|
|
156
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
156
157
|
});
|
|
157
158
|
|
|
158
159
|
it('delegates existing-session selection to routing without eagerly mutating the selected session state', () => {
|
|
@@ -224,7 +225,7 @@ describe('ChatSessionListManager', () => {
|
|
|
224
225
|
expect(mocks.updateNcpSession).not.toHaveBeenCalled();
|
|
225
226
|
});
|
|
226
227
|
|
|
227
|
-
it('
|
|
228
|
+
it('routes to the backend-materialized root session after the first send starts', () => {
|
|
228
229
|
useChatSessionListStore.setState({
|
|
229
230
|
snapshot: {
|
|
230
231
|
...useChatSessionListStore.getState().snapshot,
|
|
@@ -243,8 +244,10 @@ describe('ChatSessionListManager', () => {
|
|
|
243
244
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
244
245
|
|
|
245
246
|
manager.ensureDraftSession('native');
|
|
246
|
-
manager.
|
|
247
|
+
manager.materializeRootSessionRoute('ncp-materialized-session');
|
|
247
248
|
|
|
248
|
-
expect(
|
|
249
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('ncp-materialized-session');
|
|
250
|
+
expect(useChatThreadStore.getState().snapshot.sessionKey).toBe('ncp-materialized-session');
|
|
251
|
+
expect(uiManager.goToSession).toHaveBeenCalledWith('ncp-materialized-session', { replace: true });
|
|
249
252
|
});
|
|
250
253
|
});
|
|
@@ -5,7 +5,6 @@ import type { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
|
|
|
5
5
|
import type { SetStateAction } from 'react';
|
|
6
6
|
import type { ChatStreamActionsManager } from '@/features/chat/managers/chat-stream-actions.manager';
|
|
7
7
|
import { normalizeSessionProjectRootValue } from '@/shared/lib/session-project';
|
|
8
|
-
import { createNcpSessionId } from '@/features/chat/utils/ncp-session-adapter.utils';
|
|
9
8
|
import { updateNcpSession } from '@/shared/lib/api';
|
|
10
9
|
export class ChatSessionListManager {
|
|
11
10
|
constructor(
|
|
@@ -13,9 +12,9 @@ export class ChatSessionListManager {
|
|
|
13
12
|
private streamActionsManager: ChatStreamActionsManager
|
|
14
13
|
) {}
|
|
15
14
|
|
|
16
|
-
private syncDraftThreadState = (
|
|
15
|
+
private syncDraftThreadState = () => {
|
|
17
16
|
useChatThreadStore.getState().setSnapshot({
|
|
18
|
-
sessionKey,
|
|
17
|
+
sessionKey: null,
|
|
19
18
|
sessionDisplayName: undefined,
|
|
20
19
|
canDeleteSession: false,
|
|
21
20
|
isHistoryLoading: false,
|
|
@@ -97,7 +96,7 @@ export class ChatSessionListManager {
|
|
|
97
96
|
void updateNcpSession(normalizedSessionKey, { uiReadAt: normalizedReadAt }).catch(() => undefined);
|
|
98
97
|
};
|
|
99
98
|
|
|
100
|
-
createSession = (sessionType?: string, projectRoot?: string | null):
|
|
99
|
+
createSession = (sessionType?: string, projectRoot?: string | null): void => {
|
|
101
100
|
const { snapshot } = useChatInputStore.getState();
|
|
102
101
|
const { defaultSessionType: configuredDefaultSessionType } = snapshot;
|
|
103
102
|
const defaultSessionType = configuredDefaultSessionType || 'native';
|
|
@@ -106,30 +105,27 @@ export class ChatSessionListManager {
|
|
|
106
105
|
? sessionType.trim()
|
|
107
106
|
: defaultSessionType;
|
|
108
107
|
const normalizedProjectRoot = normalizeSessionProjectRootValue(projectRoot);
|
|
109
|
-
const nextSessionKey = createNcpSessionId();
|
|
110
108
|
this.streamActionsManager.resetStreamState();
|
|
111
109
|
useChatSessionListStore.getState().setSnapshot({
|
|
112
110
|
selectedSessionKey: null,
|
|
113
|
-
draftSessionKey:
|
|
111
|
+
draftSessionKey: null
|
|
114
112
|
});
|
|
115
|
-
this.syncDraftThreadState(
|
|
113
|
+
this.syncDraftThreadState();
|
|
116
114
|
useChatInputStore.getState().setSnapshot({
|
|
117
115
|
pendingSessionType: nextSessionType,
|
|
118
116
|
pendingProjectRoot: normalizedProjectRoot,
|
|
119
|
-
pendingProjectRootSessionKey:
|
|
117
|
+
pendingProjectRootSessionKey: null
|
|
120
118
|
});
|
|
121
119
|
this.uiManager.goToChatRoot();
|
|
122
|
-
return nextSessionKey;
|
|
123
120
|
};
|
|
124
121
|
|
|
125
|
-
startAgentDraftChat = (agentId: string, sessionType: string):
|
|
122
|
+
startAgentDraftChat = (agentId: string, sessionType: string): void => {
|
|
126
123
|
const normalizedAgentId = agentId.trim() || 'main';
|
|
127
|
-
|
|
124
|
+
this.createSession(sessionType);
|
|
128
125
|
this.setSelectedAgentId(normalizedAgentId);
|
|
129
|
-
return nextSessionKey;
|
|
130
126
|
};
|
|
131
127
|
|
|
132
|
-
ensureDraftSession = (sessionType?: string): string => {
|
|
128
|
+
ensureDraftSession = (sessionType?: string): string | null => {
|
|
133
129
|
const { snapshot } = useChatSessionListStore.getState();
|
|
134
130
|
if (snapshot.selectedSessionKey) {
|
|
135
131
|
return snapshot.selectedSessionKey;
|
|
@@ -138,28 +134,28 @@ export class ChatSessionListManager {
|
|
|
138
134
|
typeof sessionType === 'string' && sessionType.trim().length > 0
|
|
139
135
|
? sessionType.trim()
|
|
140
136
|
: null;
|
|
141
|
-
this.syncDraftThreadState(
|
|
137
|
+
this.syncDraftThreadState();
|
|
142
138
|
if (normalizedSessionType) {
|
|
143
139
|
useChatInputStore.getState().setSnapshot({ pendingSessionType: normalizedSessionType });
|
|
144
140
|
}
|
|
145
|
-
return
|
|
141
|
+
return null;
|
|
146
142
|
};
|
|
147
143
|
|
|
148
|
-
|
|
144
|
+
materializeRootSessionRoute = (sessionKey: string) => {
|
|
149
145
|
const normalizedSessionKey = sessionKey.trim();
|
|
150
146
|
if (!normalizedSessionKey) {
|
|
151
147
|
return;
|
|
152
148
|
}
|
|
153
|
-
|
|
154
|
-
const { sessionKey: currentThreadSessionKey } = useChatThreadStore.getState().snapshot;
|
|
155
|
-
if (
|
|
156
|
-
snapshot.selectedSessionKey !== null ||
|
|
157
|
-
snapshot.draftSessionKey !== normalizedSessionKey ||
|
|
158
|
-
currentThreadSessionKey !== normalizedSessionKey ||
|
|
159
|
-
!this.uiManager.isAtChatRoot()
|
|
160
|
-
) {
|
|
149
|
+
if (!this.uiManager.isAtChatRoot()) {
|
|
161
150
|
return;
|
|
162
151
|
}
|
|
152
|
+
useChatSessionListStore.getState().setSnapshot({
|
|
153
|
+
selectedSessionKey: normalizedSessionKey,
|
|
154
|
+
draftSessionKey: null,
|
|
155
|
+
});
|
|
156
|
+
useChatThreadStore.getState().setSnapshot({
|
|
157
|
+
sessionKey: normalizedSessionKey,
|
|
158
|
+
});
|
|
163
159
|
this.uiManager.goToSession(normalizedSessionKey, { replace: true });
|
|
164
160
|
};
|
|
165
161
|
|