@nextclaw/ui 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/CHANGELOG.md +15 -0
  3. package/dist/assets/{ChannelsList-DACqpUYZ.js → ChannelsList-C49JQ-Zt.js} +1 -1
  4. package/dist/assets/ChatPage-DIx05c6s.js +36 -0
  5. package/dist/assets/{DocBrowser-D7mjKkGe.js → DocBrowser-CpOosDEI.js} +1 -1
  6. package/dist/assets/{LogoBadge-BlDT-g9R.js → LogoBadge-CL_8ZPXU.js} +1 -1
  7. package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
  8. package/dist/assets/{ModelConfig-DwRU5qrw.js → ModelConfig-BZ4ZfaQB.js} +1 -1
  9. package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
  10. package/dist/assets/{RuntimeConfig-C7BRLGSC.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
  11. package/dist/assets/{SecretsConfig-D5xZh7VF.js → SecretsConfig-C1PU0Yy8.js} +2 -2
  12. package/dist/assets/{SessionsConfig-ovpj_otA.js → SessionsConfig-EskBOofQ.js} +2 -2
  13. package/dist/assets/{card-Bf4CtrW8.js → card-C7Gtw2Vs.js} +1 -1
  14. package/dist/assets/index-Cn6_2To7.js +8 -0
  15. package/dist/assets/index-nEYGCJTC.css +1 -0
  16. package/dist/assets/{input-CaKJyoWZ.js → input-oBvxsnV9.js} +1 -1
  17. package/dist/assets/{label-BaXSWTKI.js → label-C7F8lMpQ.js} +1 -1
  18. package/dist/assets/{page-layout-DA6PFRtQ.js → page-layout-DO8BlScF.js} +1 -1
  19. package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
  20. package/dist/assets/{switch-Cvd5wZs-.js → switch-C6a5GyZB.js} +1 -1
  21. package/dist/assets/{tabs-custom-0PybLkXs.js → tabs-custom-BatFap5k.js} +1 -1
  22. package/dist/assets/{useConfirmDialog-DdtpSju1.js → useConfirmDialog-zJzVKMdu.js} +2 -2
  23. package/dist/assets/{vendor-C--HHaLf.js → vendor-TlME1INH.js} +84 -84
  24. package/dist/index.html +3 -3
  25. package/package.json +4 -2
  26. package/src/App.tsx +1 -2
  27. package/src/api/config.ts +205 -202
  28. package/src/api/types.ts +54 -24
  29. package/src/components/chat/ChatConversationPanel.tsx +102 -121
  30. package/src/components/chat/ChatPage.tsx +165 -437
  31. package/src/components/chat/ChatSidebar.tsx +30 -36
  32. package/src/components/chat/ChatThread.tsx +73 -131
  33. package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
  34. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
  35. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
  36. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
  37. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
  38. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
  39. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
  40. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
  41. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
  42. package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
  43. package/src/components/chat/chat-input.types.ts +15 -0
  44. package/src/components/chat/chat-page-data.ts +121 -0
  45. package/src/components/chat/chat-page-runtime.ts +221 -0
  46. package/src/components/chat/chat-session-route.ts +59 -0
  47. package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
  48. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
  49. package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
  50. package/src/components/chat/chat-stream/transport.ts +159 -0
  51. package/src/components/chat/chat-stream/types.ts +76 -0
  52. package/src/components/chat/managers/chat-input.manager.ts +142 -0
  53. package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
  54. package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
  55. package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
  56. package/src/components/chat/managers/chat-thread.manager.ts +86 -0
  57. package/src/components/chat/managers/chat-ui.manager.ts +65 -0
  58. package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
  59. package/src/components/chat/presenter/chat.presenter.ts +32 -0
  60. package/src/components/chat/stores/chat-input.store.ts +62 -0
  61. package/src/components/chat/stores/chat-run-status.store.ts +30 -0
  62. package/src/components/chat/stores/chat-session-list.store.ts +34 -0
  63. package/src/components/chat/stores/chat-thread.store.ts +52 -0
  64. package/src/components/chat/useChatRuntimeController.ts +134 -0
  65. package/src/components/chat/useChatSessionTypeState.ts +148 -0
  66. package/src/components/common/MaskedInput.tsx +1 -1
  67. package/src/components/config/ProviderForm.tsx +221 -14
  68. package/src/hooks/useConfig.ts +33 -2
  69. package/src/hooks/useObservable.ts +20 -0
  70. package/src/hooks/useWebSocket.ts +23 -1
  71. package/src/lib/chat-message.ts +2 -202
  72. package/src/lib/chat-runtime-utils.ts +250 -0
  73. package/src/lib/i18n.ts +11 -0
  74. package/tsconfig.json +2 -1
  75. package/vite.config.ts +2 -1
  76. package/dist/assets/ChatPage-iji0RkTR.js +0 -34
  77. package/dist/assets/MarketplacePage-CZq3jVgg.js +0 -49
  78. package/dist/assets/ProvidersList-DFxN3pjx.js +0 -1
  79. package/dist/assets/index-C_DhisNo.css +0 -1
  80. package/dist/assets/index-dKTqKCJo.js +0 -7
  81. package/dist/assets/session-run-status-CllIZxNf.js +0 -5
  82. package/src/components/chat/ChatInputBar.tsx +0 -590
  83. package/src/components/chat/useChatStreamController.ts +0 -591
@@ -0,0 +1,76 @@
1
+ import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
2
+ import type {
3
+ ChatRunView,
4
+ ChatTurnStreamDeltaEvent,
5
+ ChatTurnStreamReadyEvent,
6
+ ChatTurnStreamSessionEvent,
7
+ SessionMessageView
8
+ } from '@/api/types';
9
+
10
+ export type SendMessageParams = {
11
+ runId?: string;
12
+ message: string;
13
+ sessionKey: string;
14
+ agentId: string;
15
+ sessionType?: string;
16
+ model?: string;
17
+ requestedSkills?: string[];
18
+ stopSupported?: boolean;
19
+ stopReason?: string;
20
+ restoreDraftOnError?: boolean;
21
+ };
22
+
23
+ export type ActiveRunState = {
24
+ localRunId: number;
25
+ sessionKey: string;
26
+ agentId?: string;
27
+ backendRunId?: string;
28
+ backendStopSupported: boolean;
29
+ backendStopReason?: string;
30
+ };
31
+
32
+ export type StreamReadyPayload = {
33
+ sessionKey: string;
34
+ runId?: string;
35
+ stopSupported?: boolean;
36
+ stopReason?: string;
37
+ requestedAt?: string;
38
+ };
39
+
40
+ export type StreamReadyEvent = ChatTurnStreamReadyEvent;
41
+ export type StreamDeltaEvent = ChatTurnStreamDeltaEvent;
42
+ export type StreamSessionEvent = ChatTurnStreamSessionEvent;
43
+
44
+ export type NextbotAgentRunMetadata =
45
+ | {
46
+ driver: 'nextbot-stream';
47
+ mode: 'send';
48
+ payload: SendMessageParams;
49
+ requestedSkills: string[];
50
+ }
51
+ | {
52
+ driver: 'nextbot-stream';
53
+ mode: 'resume';
54
+ runId: string;
55
+ fromEventIndex?: number;
56
+ sessionKey?: string;
57
+ agentId?: string;
58
+ stopSupported?: boolean;
59
+ stopReason?: string;
60
+ };
61
+
62
+ export type UseChatStreamControllerParams = {
63
+ selectedSessionKeyRef: MutableRefObject<string | null>;
64
+ setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
65
+ setDraft: Dispatch<SetStateAction<string>>;
66
+ refetchSessions: () => Promise<unknown>;
67
+ refetchHistory: () => Promise<unknown>;
68
+ };
69
+
70
+ export type ChatStreamActions = {
71
+ sendMessage: (payload: SendMessageParams) => Promise<void>;
72
+ stopCurrentRun: () => Promise<void>;
73
+ resumeRun: (run: ChatRunView) => Promise<void>;
74
+ resetStreamState: () => void;
75
+ applyHistoryMessages: (messages: SessionMessageView[], options?: { isLoading?: boolean }) => void;
76
+ };
@@ -0,0 +1,142 @@
1
+ import { updateSession } from '@/api/config';
2
+ import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
3
+ import { buildNewSessionKey } from '@/components/chat/chat-session-route';
4
+ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
5
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
6
+ import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
7
+ import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
8
+ import type { SetStateAction } from 'react';
9
+ import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
10
+
11
+ export class ChatInputManager {
12
+ constructor(
13
+ private uiManager: ChatUiManager,
14
+ private streamActionsManager: ChatStreamActionsManager
15
+ ) {}
16
+
17
+ private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
18
+ const current = useChatInputStore.getState().snapshot;
19
+ for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
20
+ if (!Object.is(current[key], value)) {
21
+ return true;
22
+ }
23
+ }
24
+ return false;
25
+ };
26
+
27
+ private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
28
+ if (typeof next === 'function') {
29
+ return (next as (value: T) => T)(prev);
30
+ }
31
+ return next;
32
+ };
33
+
34
+ syncSnapshot = (patch: Partial<ChatInputSnapshot>) => {
35
+ if (!this.hasSnapshotChanges(patch)) {
36
+ return;
37
+ }
38
+ useChatInputStore.getState().setSnapshot(patch);
39
+ };
40
+
41
+ setDraft = (next: SetStateAction<string>) => {
42
+ const prev = useChatInputStore.getState().snapshot.draft;
43
+ const value = this.resolveUpdateValue(prev, next);
44
+ if (value === prev) {
45
+ return;
46
+ }
47
+ useChatInputStore.getState().setSnapshot({ draft: value });
48
+ };
49
+
50
+ setPendingSessionType = (next: SetStateAction<string>) => {
51
+ const prev = useChatInputStore.getState().snapshot.pendingSessionType;
52
+ const value = this.resolveUpdateValue(prev, next);
53
+ if (value === prev) {
54
+ return;
55
+ }
56
+ useChatInputStore.getState().setSnapshot({ pendingSessionType: value });
57
+ };
58
+
59
+ send = async () => {
60
+ const inputSnapshot = useChatInputStore.getState().snapshot;
61
+ const sessionSnapshot = useChatSessionListStore.getState().snapshot;
62
+ const message = inputSnapshot.draft.trim();
63
+ if (!message) {
64
+ return;
65
+ }
66
+ const requestedSkills = inputSnapshot.selectedSkills;
67
+ const hasSelectedSession = Boolean(sessionSnapshot.selectedSessionKey);
68
+ const sessionKey = sessionSnapshot.selectedSessionKey ?? buildNewSessionKey(sessionSnapshot.selectedAgentId);
69
+ if (!hasSelectedSession) {
70
+ this.uiManager.goToSession(sessionKey, { replace: true });
71
+ }
72
+ this.setDraft('');
73
+ this.setSelectedSkills([]);
74
+ await this.streamActionsManager.sendMessage({
75
+ message,
76
+ sessionKey,
77
+ agentId: sessionSnapshot.selectedAgentId,
78
+ sessionType: inputSnapshot.selectedSessionType,
79
+ model: inputSnapshot.selectedModel || undefined,
80
+ stopSupported: inputSnapshot.stopSupported,
81
+ stopReason: inputSnapshot.stopReason,
82
+ requestedSkills,
83
+ restoreDraftOnError: true
84
+ });
85
+ };
86
+
87
+ stop = async () => {
88
+ await this.streamActionsManager.stopCurrentRun();
89
+ };
90
+
91
+ goToProviders = () => {
92
+ this.uiManager.goToProviders();
93
+ };
94
+
95
+ setSelectedModel = (next: SetStateAction<string>) => {
96
+ const prev = useChatInputStore.getState().snapshot.selectedModel;
97
+ const value = this.resolveUpdateValue(prev, next);
98
+ if (value === prev) {
99
+ return;
100
+ }
101
+ useChatInputStore.getState().setSnapshot({ selectedModel: value });
102
+ };
103
+
104
+ selectSessionType = (value: string) => {
105
+ const normalized = normalizeSessionType(value);
106
+ useChatInputStore.getState().setSnapshot({ selectedSessionType: normalized, pendingSessionType: normalized });
107
+ void this.syncRemoteSessionType(normalized);
108
+ };
109
+
110
+ setSelectedSkills = (next: SetStateAction<string[]>) => {
111
+ const prev = useChatInputStore.getState().snapshot.selectedSkills;
112
+ const value = this.resolveUpdateValue(prev, next);
113
+ if (Object.is(value, prev)) {
114
+ return;
115
+ }
116
+ useChatInputStore.getState().setSnapshot({ selectedSkills: value });
117
+ };
118
+
119
+ selectModel = (value: string) => {
120
+ this.setSelectedModel(value);
121
+ };
122
+
123
+ selectSkills = (next: string[]) => {
124
+ this.setSelectedSkills(next);
125
+ };
126
+
127
+ private syncRemoteSessionType = async (normalizedType: string) => {
128
+ const sessionSnapshot = useChatSessionListStore.getState().snapshot;
129
+ const selectedSessionKey = sessionSnapshot.selectedSessionKey;
130
+ if (!selectedSessionKey) {
131
+ return;
132
+ }
133
+ const selectedSession = sessionSnapshot.sessions.find((session) => session.key === selectedSessionKey);
134
+ if (!selectedSession?.sessionTypeMutable) {
135
+ return;
136
+ }
137
+ if (normalizeSessionType(selectedSession.sessionType) === normalizedType) {
138
+ return;
139
+ }
140
+ await updateSession(selectedSessionKey, { sessionType: normalizedType });
141
+ };
142
+ }
@@ -0,0 +1,32 @@
1
+ import { useChatRunStatusStore } from '@/components/chat/stores/chat-run-status.store';
2
+ import type { ChatRunStatusSnapshot } from '@/components/chat/stores/chat-run-status.store';
3
+
4
+ function isMapEqual<T>(left: ReadonlyMap<string, T>, right: ReadonlyMap<string, T>): boolean {
5
+ if (left === right) {
6
+ return true;
7
+ }
8
+ if (left.size !== right.size) {
9
+ return false;
10
+ }
11
+ for (const [key, value] of left.entries()) {
12
+ if (!Object.is(value, right.get(key))) {
13
+ return false;
14
+ }
15
+ }
16
+ return true;
17
+ }
18
+
19
+ export class ChatRunStatusManager {
20
+ syncSnapshot = (patch: Partial<ChatRunStatusSnapshot>) => {
21
+ const current = useChatRunStatusStore.getState().snapshot;
22
+ const nextMap = patch.sessionRunStatusByKey;
23
+ if (
24
+ (nextMap ? isMapEqual(current.sessionRunStatusByKey, nextMap) : true) &&
25
+ (patch.isLocallyRunning === undefined || Object.is(current.isLocallyRunning, patch.isLocallyRunning)) &&
26
+ (patch.activeBackendRunId === undefined || Object.is(current.activeBackendRunId, patch.activeBackendRunId))
27
+ ) {
28
+ return;
29
+ }
30
+ useChatRunStatusStore.getState().setSnapshot(patch);
31
+ };
32
+ }
@@ -0,0 +1,77 @@
1
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
2
+ import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
3
+ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
4
+ import type { ChatSessionListSnapshot } from '@/components/chat/stores/chat-session-list.store';
5
+ import type { SetStateAction } from 'react';
6
+ import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
7
+
8
+ export class ChatSessionListManager {
9
+ constructor(
10
+ private uiManager: ChatUiManager,
11
+ private streamActionsManager: ChatStreamActionsManager
12
+ ) {}
13
+
14
+ private hasSnapshotChanges = (patch: Partial<ChatSessionListSnapshot>): boolean => {
15
+ const current = useChatSessionListStore.getState().snapshot;
16
+ for (const [key, value] of Object.entries(patch) as Array<[keyof ChatSessionListSnapshot, ChatSessionListSnapshot[keyof ChatSessionListSnapshot]]>) {
17
+ if (!Object.is(current[key], value)) {
18
+ return true;
19
+ }
20
+ }
21
+ return false;
22
+ };
23
+
24
+ private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
25
+ if (typeof next === 'function') {
26
+ return (next as (value: T) => T)(prev);
27
+ }
28
+ return next;
29
+ };
30
+
31
+ syncSnapshot = (patch: Partial<ChatSessionListSnapshot>) => {
32
+ if (!this.hasSnapshotChanges(patch)) {
33
+ return;
34
+ }
35
+ useChatSessionListStore.getState().setSnapshot(patch);
36
+ };
37
+
38
+ setSelectedAgentId = (next: SetStateAction<string>) => {
39
+ const prev = useChatSessionListStore.getState().snapshot.selectedAgentId;
40
+ const value = this.resolveUpdateValue(prev, next);
41
+ if (value === prev) {
42
+ return;
43
+ }
44
+ useChatSessionListStore.getState().setSnapshot({ selectedAgentId: value });
45
+ };
46
+
47
+ setSelectedSessionKey = (next: SetStateAction<string | null>) => {
48
+ const prev = useChatSessionListStore.getState().snapshot.selectedSessionKey;
49
+ const value = this.resolveUpdateValue(prev, next);
50
+ if (value === prev) {
51
+ return;
52
+ }
53
+ useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: value });
54
+ };
55
+
56
+ createSession = () => {
57
+ const defaultSessionType = useChatInputStore.getState().snapshot.defaultSessionType || 'native';
58
+ this.streamActionsManager.resetStreamState();
59
+ this.setSelectedSessionKey(null);
60
+ useChatInputStore.getState().setSnapshot({ pendingSessionType: defaultSessionType });
61
+ this.uiManager.goToChatRoot();
62
+ };
63
+
64
+ selectSession = (sessionKey: string) => {
65
+ this.setSelectedSessionKey(sessionKey);
66
+ this.uiManager.goToSession(sessionKey);
67
+ };
68
+
69
+ setQuery = (next: SetStateAction<string>) => {
70
+ const prev = useChatSessionListStore.getState().snapshot.query;
71
+ const value = this.resolveUpdateValue(prev, next);
72
+ if (value === prev) {
73
+ return;
74
+ }
75
+ useChatSessionListStore.getState().setSnapshot({ query: value });
76
+ };
77
+ }
@@ -0,0 +1,34 @@
1
+ import type { ChatStreamActions } from '@/components/chat/chat-stream/types';
2
+
3
+ const noopAsync = async () => {};
4
+ const noop = () => {};
5
+
6
+ export class ChatStreamActionsManager {
7
+ private actions: ChatStreamActions = {
8
+ sendMessage: noopAsync,
9
+ stopCurrentRun: noopAsync,
10
+ resumeRun: noopAsync,
11
+ resetStreamState: noop,
12
+ applyHistoryMessages: noop
13
+ };
14
+
15
+ bind = (patch: Partial<ChatStreamActions>) => {
16
+ this.actions = {
17
+ ...this.actions,
18
+ ...patch
19
+ };
20
+ };
21
+
22
+ sendMessage = (payload: Parameters<ChatStreamActions['sendMessage']>[0]) => this.actions.sendMessage(payload);
23
+
24
+ stopCurrentRun = () => this.actions.stopCurrentRun();
25
+
26
+ resumeRun = (run: Parameters<ChatStreamActions['resumeRun']>[0]) => this.actions.resumeRun(run);
27
+
28
+ resetStreamState = () => this.actions.resetStreamState();
29
+
30
+ applyHistoryMessages = (
31
+ messages: Parameters<ChatStreamActions['applyHistoryMessages']>[0],
32
+ options?: Parameters<ChatStreamActions['applyHistoryMessages']>[1]
33
+ ) => this.actions.applyHistoryMessages(messages, options);
34
+ }
@@ -0,0 +1,86 @@
1
+ import { deleteSession as deleteSessionApi } from '@/api/config';
2
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
3
+ import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
4
+ import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
5
+ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
6
+ import type { ChatThreadSnapshot } from '@/components/chat/stores/chat-thread.store';
7
+ import { t } from '@/lib/i18n';
8
+ import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
9
+
10
+ export type ChatThreadManagerActions = {
11
+ refetchSessions: () => Promise<unknown>;
12
+ };
13
+
14
+ const noopAsync = async () => {};
15
+ export class ChatThreadManager {
16
+ private actions: ChatThreadManagerActions = {
17
+ refetchSessions: noopAsync
18
+ };
19
+
20
+ constructor(
21
+ private uiManager: ChatUiManager,
22
+ private sessionListManager: ChatSessionListManager,
23
+ private streamActionsManager: ChatStreamActionsManager
24
+ ) {}
25
+
26
+ bindActions = (patch: Partial<ChatThreadManagerActions>) => {
27
+ this.actions = {
28
+ ...this.actions,
29
+ ...patch
30
+ };
31
+ };
32
+
33
+ private hasSnapshotChanges = (patch: Partial<ChatThreadSnapshot>): boolean => {
34
+ const current = useChatThreadStore.getState().snapshot;
35
+ for (const [key, value] of Object.entries(patch) as Array<[keyof ChatThreadSnapshot, ChatThreadSnapshot[keyof ChatThreadSnapshot]]>) {
36
+ if (!Object.is(current[key], value)) {
37
+ return true;
38
+ }
39
+ }
40
+ return false;
41
+ };
42
+
43
+ syncSnapshot = (patch: Partial<ChatThreadSnapshot>) => {
44
+ if (!this.hasSnapshotChanges(patch)) {
45
+ return;
46
+ }
47
+ useChatThreadStore.getState().setSnapshot(patch);
48
+ };
49
+
50
+ deleteSession = () => {
51
+ void this.deleteCurrentSession();
52
+ };
53
+
54
+ createSession = () => {
55
+ this.sessionListManager.createSession();
56
+ };
57
+
58
+ goToProviders = () => {
59
+ this.uiManager.goToProviders();
60
+ };
61
+
62
+ private deleteCurrentSession = async () => {
63
+ const selectedSessionKey = useChatSessionListStore.getState().snapshot.selectedSessionKey;
64
+ if (!selectedSessionKey) {
65
+ return;
66
+ }
67
+ const confirmed = await this.uiManager.confirm({
68
+ title: t('chatDeleteSessionConfirm'),
69
+ variant: 'destructive',
70
+ confirmLabel: t('delete')
71
+ });
72
+ if (!confirmed) {
73
+ return;
74
+ }
75
+ useChatThreadStore.getState().setSnapshot({ isDeletePending: true });
76
+ try {
77
+ await deleteSessionApi(selectedSessionKey);
78
+ this.streamActionsManager.resetStreamState();
79
+ useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: null });
80
+ this.uiManager.goToChatRoot({ replace: true });
81
+ await this.actions.refetchSessions();
82
+ } finally {
83
+ useChatThreadStore.getState().setSnapshot({ isDeletePending: false });
84
+ }
85
+ };
86
+ }
@@ -0,0 +1,65 @@
1
+ import { buildSessionPath } from '@/components/chat/chat-session-route';
2
+ import type { NavigateFunction, NavigateOptions } from 'react-router-dom';
3
+
4
+ type ChatUiState = {
5
+ pathname: string;
6
+ };
7
+
8
+ type ChatUiActions = {
9
+ navigate: NavigateFunction | null;
10
+ confirm: (params: { title: string; variant: 'destructive'; confirmLabel: string }) => Promise<boolean>;
11
+ };
12
+
13
+ const noopConfirm = async (_params: { title: string; variant: 'destructive'; confirmLabel: string }) => false;
14
+
15
+ export class ChatUiManager {
16
+ private state: ChatUiState = {
17
+ pathname: ''
18
+ };
19
+
20
+ private actions: ChatUiActions = {
21
+ navigate: null,
22
+ confirm: noopConfirm
23
+ };
24
+
25
+ syncState = (patch: Partial<ChatUiState>) => {
26
+ this.state = {
27
+ ...this.state,
28
+ ...patch
29
+ };
30
+ };
31
+
32
+ bindActions = (patch: Partial<ChatUiActions>) => {
33
+ this.actions = {
34
+ ...this.actions,
35
+ ...patch
36
+ };
37
+ };
38
+
39
+ confirm = async (params: { title: string; variant: 'destructive'; confirmLabel: string }) => {
40
+ return this.actions.confirm(params);
41
+ };
42
+
43
+ navigateTo = (to: string, options?: NavigateOptions) => {
44
+ if (!this.actions.navigate) {
45
+ return;
46
+ }
47
+ if (this.state.pathname === to && !options?.replace) {
48
+ return;
49
+ }
50
+ this.actions.navigate(to, options);
51
+ this.state.pathname = to;
52
+ };
53
+
54
+ goToProviders = () => {
55
+ this.navigateTo('/providers');
56
+ };
57
+
58
+ goToChatRoot = (options?: NavigateOptions) => {
59
+ this.navigateTo('/chat', options);
60
+ };
61
+
62
+ goToSession = (sessionKey: string, options?: NavigateOptions) => {
63
+ this.navigateTo(buildSessionPath(sessionKey), options);
64
+ };
65
+ }
@@ -0,0 +1,25 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import type { ChatPresenter } from '@/components/chat/presenter/chat.presenter';
4
+
5
+ const ChatPresenterContext = createContext<ChatPresenter | null>(null);
6
+
7
+ type ChatPresenterProviderProps = {
8
+ presenter: ChatPresenter;
9
+ children: ReactNode;
10
+ };
11
+
12
+ export function ChatPresenterProvider({ presenter, children }: ChatPresenterProviderProps) {
13
+ return <ChatPresenterContext.Provider value={presenter}>{children}</ChatPresenterContext.Provider>;
14
+ }
15
+
16
+ export function usePresenter(): ChatPresenter {
17
+ const presenter = useContext(ChatPresenterContext);
18
+ if (!presenter) {
19
+ throw new Error('usePresenter must be used inside ChatPresenterProvider');
20
+ }
21
+ return presenter;
22
+ }
23
+
24
+ // Backward-compatible alias with the name from project notes.
25
+ export const usePresneter = usePresenter;
@@ -0,0 +1,32 @@
1
+ import { ChatInputManager } from '@/components/chat/managers/chat-input.manager';
2
+ import { nextbotParsers } from '@/components/chat/chat-stream/nextbot-parsers';
3
+ import { ChatRunStatusManager } from '@/components/chat/managers/chat-run-status.manager';
4
+ import { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
5
+ import { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
6
+ import { ChatThreadManager } from '@/components/chat/managers/chat-thread.manager';
7
+ import { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
8
+ import { NextbotRuntimeAgent } from '@/components/chat/chat-stream/nextbot-runtime-agent';
9
+ import { AgentChatController } from '@nextclaw/agent-chat';
10
+
11
+ export class ChatPresenter {
12
+ chatUiManager = new ChatUiManager();
13
+ runtimeAgent = new NextbotRuntimeAgent();
14
+ chatController = new AgentChatController(
15
+ {
16
+ agent: this.runtimeAgent,
17
+ getToolDefs: () => [],
18
+ getContexts: () => [],
19
+ getToolExecutor: () => undefined
20
+ },
21
+ { metadataParsers: nextbotParsers }
22
+ );
23
+ chatStreamActionsManager = new ChatStreamActionsManager();
24
+ chatInputManager = new ChatInputManager(this.chatUiManager, this.chatStreamActionsManager);
25
+ chatSessionListManager = new ChatSessionListManager(this.chatUiManager, this.chatStreamActionsManager);
26
+ chatRunStatusManager = new ChatRunStatusManager();
27
+ chatThreadManager = new ChatThreadManager(
28
+ this.chatUiManager,
29
+ this.chatSessionListManager,
30
+ this.chatStreamActionsManager
31
+ );
32
+ }
@@ -0,0 +1,62 @@
1
+ import { create } from 'zustand';
2
+ import type { MarketplaceInstalledRecord } from '@/api/types';
3
+
4
+ export type ChatInputSnapshot = {
5
+ isProviderStateResolved: boolean;
6
+ draft: string;
7
+ pendingSessionType: string;
8
+ defaultSessionType: string;
9
+ canStopGeneration: boolean;
10
+ stopDisabledReason: string | null;
11
+ sendError: string | null;
12
+ isSending: boolean;
13
+ modelOptions: Array<{ value: string; modelLabel: string; providerLabel: string }>;
14
+ selectedModel: string;
15
+ sessionTypeOptions: Array<{ value: string; label: string }>;
16
+ selectedSessionType?: string;
17
+ stopSupported: boolean;
18
+ stopReason?: string;
19
+ canEditSessionType: boolean;
20
+ sessionTypeUnavailable: boolean;
21
+ skillRecords: MarketplaceInstalledRecord[];
22
+ isSkillsLoading: boolean;
23
+ selectedSkills: string[];
24
+ };
25
+
26
+ type ChatInputStore = {
27
+ snapshot: ChatInputSnapshot;
28
+ setSnapshot: (patch: Partial<ChatInputSnapshot>) => void;
29
+ };
30
+
31
+ const initialSnapshot: ChatInputSnapshot = {
32
+ isProviderStateResolved: false,
33
+ draft: '',
34
+ pendingSessionType: 'native',
35
+ defaultSessionType: 'native',
36
+ canStopGeneration: false,
37
+ stopDisabledReason: null,
38
+ sendError: null,
39
+ isSending: false,
40
+ modelOptions: [],
41
+ selectedModel: '',
42
+ sessionTypeOptions: [],
43
+ selectedSessionType: undefined,
44
+ stopSupported: false,
45
+ stopReason: undefined,
46
+ canEditSessionType: false,
47
+ sessionTypeUnavailable: false,
48
+ skillRecords: [],
49
+ isSkillsLoading: false,
50
+ selectedSkills: []
51
+ };
52
+
53
+ export const useChatInputStore = create<ChatInputStore>((set) => ({
54
+ snapshot: initialSnapshot,
55
+ setSnapshot: (patch) =>
56
+ set((state) => ({
57
+ snapshot: {
58
+ ...state.snapshot,
59
+ ...patch
60
+ }
61
+ }))
62
+ }));
@@ -0,0 +1,30 @@
1
+ import { create } from 'zustand';
2
+ import type { SessionRunStatus } from '@/lib/session-run-status';
3
+
4
+ export type ChatRunStatusSnapshot = {
5
+ sessionRunStatusByKey: ReadonlyMap<string, SessionRunStatus>;
6
+ isLocallyRunning: boolean;
7
+ activeBackendRunId: string | null;
8
+ };
9
+
10
+ type ChatRunStatusStore = {
11
+ snapshot: ChatRunStatusSnapshot;
12
+ setSnapshot: (patch: Partial<ChatRunStatusSnapshot>) => void;
13
+ };
14
+
15
+ const initialSnapshot: ChatRunStatusSnapshot = {
16
+ sessionRunStatusByKey: new Map(),
17
+ isLocallyRunning: false,
18
+ activeBackendRunId: null
19
+ };
20
+
21
+ export const useChatRunStatusStore = create<ChatRunStatusStore>((set) => ({
22
+ snapshot: initialSnapshot,
23
+ setSnapshot: (patch) =>
24
+ set((state) => ({
25
+ snapshot: {
26
+ ...state.snapshot,
27
+ ...patch
28
+ }
29
+ }))
30
+ }));