@nextclaw/ui 0.10.5 → 0.11.1

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 (89) hide show
  1. package/CHANGELOG.md +23 -2
  2. package/dist/assets/{ChannelsList-Nu7Ig6_-.js → ChannelsList-CVPqrxns.js} +4 -4
  3. package/dist/assets/ChatPage-BO1VUrAY.js +37 -0
  4. package/dist/assets/{DocBrowser-3CfKmJA6.js → DocBrowser-FBwg8iji.js} +1 -1
  5. package/dist/assets/{LogoBadge-DdthDJOp.js → LogoBadge-BCmJfRT8.js} +1 -1
  6. package/dist/assets/MarketplacePage-DWxXUOCx.js +49 -0
  7. package/dist/assets/{McpMarketplacePage-Dg8GSZh6.js → McpMarketplacePage-Bth9X_hu.js} +2 -2
  8. package/dist/assets/{ModelConfig-DyQ6cC92.js → ModelConfig-PkSp_ioc.js} +1 -1
  9. package/dist/assets/ProvidersList-DVDge8wa.js +1 -0
  10. package/dist/assets/RemoteAccessPage-BVkzfEaL.js +1 -0
  11. package/dist/assets/RuntimeConfig-ByJs3khh.js +1 -0
  12. package/dist/assets/{SearchConfig-R1BcCLWO.js → SearchConfig-KZUAqYJN.js} +1 -1
  13. package/dist/assets/{SecretsConfig-D-jZMHeY.js → SecretsConfig-qwB_Y_Ka.js} +2 -2
  14. package/dist/assets/SessionsConfig-CGCl4UTr.js +2 -0
  15. package/dist/assets/index-CrilScMo.css +1 -0
  16. package/dist/assets/{index-BulnQWr6.js → index-D41ntvb7.js} +6 -6
  17. package/dist/assets/{label-C7yzBvzK.js → label-7JEFhkur.js} +1 -1
  18. package/dist/assets/ncp-session-adapter-BOqhkrc-.js +1 -0
  19. package/dist/assets/{page-layout-DF0xpax2.js → page-layout-B7q511TE.js} +1 -1
  20. package/dist/assets/popover-CywJGmPr.js +1 -0
  21. package/dist/assets/security-config-zi2UxN5r.js +1 -0
  22. package/dist/assets/skeleton-qUJZQ03S.js +1 -0
  23. package/dist/assets/{status-dot-B9opOZ22.js → status-dot-BilwNdTT.js} +1 -1
  24. package/dist/assets/{switch-l1P0ev4D.js → switch-BLp2Pno1.js} +1 -1
  25. package/dist/assets/tabs-custom-CgIdQMGC.js +1 -0
  26. package/dist/assets/useConfirmDialog-BitswAkv.js +1 -0
  27. package/dist/assets/{vendor-CNhxtHCf.js → vendor-D_JxmsLV.js} +87 -87
  28. package/dist/index.html +3 -3
  29. package/package.json +4 -4
  30. package/src/App.test.tsx +42 -10
  31. package/src/App.tsx +5 -40
  32. package/src/api/api-base.test.ts +37 -0
  33. package/src/api/api-base.ts +0 -4
  34. package/src/api/config.ts +2 -270
  35. package/src/api/ncp-attachments.ts +12 -12
  36. package/src/api/types.ts +4 -121
  37. package/src/components/chat/ChatPage.tsx +1 -11
  38. package/src/components/chat/ChatSidebar.test.tsx +1 -50
  39. package/src/components/chat/ChatSidebar.tsx +0 -5
  40. package/src/components/chat/README.md +2 -0
  41. package/src/components/chat/adapters/chat-message.adapter.test.ts +39 -0
  42. package/src/components/chat/adapters/chat-message.adapter.ts +56 -0
  43. package/src/components/chat/chat-attachment-upload-limit.test.ts +41 -0
  44. package/src/components/chat/chat-composer-state.test.ts +4 -4
  45. package/src/components/chat/chat-composer-state.ts +1 -1
  46. package/src/components/chat/chat-session-display.ts +9 -0
  47. package/src/components/chat/chat-session-label.service.ts +3 -12
  48. package/src/components/chat/chat-session-preference-sync.test.ts +10 -13
  49. package/src/components/chat/chat-stream/types.ts +4 -57
  50. package/src/components/chat/containers/chat-input-bar.container.tsx +2 -2
  51. package/src/components/chat/ncp/NcpChatPage.tsx +3 -3
  52. package/src/components/chat/ncp/ncp-chat-input.manager.ts +1 -1
  53. package/src/components/chat/useHydratedNcpAgent.test.tsx +77 -0
  54. package/src/components/config/README.md +2 -0
  55. package/src/components/config/SessionsConfig.tsx +152 -132
  56. package/src/hooks/use-auth.test.ts +3 -3
  57. package/src/hooks/use-auth.ts +16 -4
  58. package/src/hooks/use-realtime-query-bridge.ts +0 -24
  59. package/src/hooks/useConfig.ts +10 -137
  60. package/src/lib/session-run-status.ts +1 -63
  61. package/src/vite-env.d.ts +1 -0
  62. package/vite.config.ts +4 -4
  63. package/dist/assets/ChatPage-CBCFSk4e.js +0 -38
  64. package/dist/assets/MarketplacePage-inGGiv1T.js +0 -49
  65. package/dist/assets/ProvidersList-B2T8Lc_i.js +0 -1
  66. package/dist/assets/RemoteAccessPage-C9LxgK-C.js +0 -1
  67. package/dist/assets/RuntimeConfig-Ey4VIqTW.js +0 -1
  68. package/dist/assets/SessionsConfig-Cawoh4_2.js +0 -2
  69. package/dist/assets/chat-message-BbuIK4dQ.js +0 -3
  70. package/dist/assets/index-kaPUhd-8.css +0 -1
  71. package/dist/assets/popover-DjaScZDJ.js +0 -1
  72. package/dist/assets/security-config-Bg2eriNx.js +0 -1
  73. package/dist/assets/skeleton-DycBJAJF.js +0 -1
  74. package/dist/assets/tabs-custom-BG9y2JhC.js +0 -1
  75. package/dist/assets/useConfirmDialog-DTducNfn.js +0 -1
  76. package/src/api/config.stream.test.ts +0 -115
  77. package/src/components/chat/chat-chain.test.ts +0 -22
  78. package/src/components/chat/chat-chain.ts +0 -23
  79. package/src/components/chat/chat-page-data.ts +0 -171
  80. package/src/components/chat/chat-page-runtime.ts +0 -190
  81. package/src/components/chat/chat-stream/nextbot-parsers.ts +0 -52
  82. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +0 -413
  83. package/src/components/chat/chat-stream/stream-event-adapter.ts +0 -98
  84. package/src/components/chat/chat-stream/transport.ts +0 -253
  85. package/src/components/chat/legacy/LegacyChatPage.tsx +0 -223
  86. package/src/components/chat/managers/chat-input.manager.ts +0 -228
  87. package/src/components/chat/managers/chat-thread.manager.ts +0 -87
  88. package/src/components/chat/presenter/chat.presenter.ts +0 -32
  89. package/src/components/chat/useChatRuntimeController.ts +0 -134
@@ -1,115 +0,0 @@
1
- import { sendChatTurnStream, streamChatRun } from '@/api/config';
2
-
3
- const mocks = vi.hoisted(() => ({
4
- request: vi.fn(),
5
- openStream: vi.fn()
6
- }));
7
-
8
- vi.mock('@/transport', () => ({
9
- appClient: {
10
- request: mocks.request,
11
- openStream: mocks.openStream
12
- }
13
- }));
14
-
15
- describe('api/config stream routing', () => {
16
- beforeEach(() => {
17
- mocks.request.mockReset();
18
- mocks.openStream.mockReset();
19
- });
20
-
21
- it('routes sendChatTurnStream through appClient.openStream', async () => {
22
- const onReady = vi.fn();
23
- const onDelta = vi.fn();
24
- const onSessionEvent = vi.fn();
25
-
26
- mocks.openStream.mockImplementation(({ onEvent }) => {
27
- onEvent({ name: 'ready', payload: { sessionKey: 's1' } });
28
- onEvent({ name: 'delta', payload: { delta: 'hello' } });
29
- onEvent({ name: 'session_event', payload: { type: 'session.updated' } });
30
- onEvent({ name: 'final', payload: { sessionKey: 's1', reply: 'hello world' } });
31
- return {
32
- finished: Promise.resolve({ sessionKey: 's1', reply: 'hello world' }),
33
- cancel: vi.fn()
34
- };
35
- });
36
-
37
- const result = await sendChatTurnStream(
38
- { message: 'hi' } as never,
39
- { onReady, onDelta, onSessionEvent }
40
- );
41
-
42
- expect(mocks.openStream).toHaveBeenCalledWith({
43
- method: 'POST',
44
- path: '/api/chat/turn/stream',
45
- body: { message: 'hi' },
46
- signal: undefined,
47
- onEvent: expect.any(Function)
48
- });
49
- expect(onReady).toHaveBeenCalledWith({ sessionKey: 's1' });
50
- expect(onDelta).toHaveBeenCalledWith({ delta: 'hello' });
51
- expect(onSessionEvent).toHaveBeenCalledWith({ data: { type: 'session.updated' } });
52
- expect(result).toEqual({ sessionKey: 's1', reply: 'hello world' });
53
- });
54
-
55
- it('routes streamChatRun through appClient.openStream and preserves query params', async () => {
56
- const onReady = vi.fn();
57
- const onDelta = vi.fn();
58
- const onSessionEvent = vi.fn();
59
-
60
- mocks.openStream.mockImplementation(() => ({
61
- finished: Promise.resolve({ sessionKey: 's1', reply: 'resumed' }),
62
- cancel: vi.fn()
63
- }));
64
-
65
- const result = await streamChatRun(
66
- { runId: 'run-1', fromEventIndex: 42 },
67
- { onReady, onDelta, onSessionEvent }
68
- );
69
-
70
- expect(mocks.openStream).toHaveBeenCalledWith({
71
- method: 'GET',
72
- path: '/api/chat/runs/run-1/stream?fromEventIndex=42',
73
- signal: undefined,
74
- onEvent: expect.any(Function)
75
- });
76
- expect(onReady).not.toHaveBeenCalled();
77
- expect(onDelta).not.toHaveBeenCalled();
78
- expect(onSessionEvent).not.toHaveBeenCalled();
79
- expect(result).toEqual({ sessionKey: 's1', reply: 'resumed' });
80
- });
81
-
82
- it('surfaces transport error events as rejected stream promises', async () => {
83
- mocks.openStream.mockImplementation(({ onEvent }) => {
84
- let resolveFinished!: () => void;
85
- let rejectFinished!: (error: Error) => void;
86
- const finished = new Promise<void>((resolve, reject) => {
87
- resolveFinished = resolve;
88
- rejectFinished = reject;
89
- });
90
- queueMicrotask(() => {
91
- try {
92
- onEvent({ name: 'error', payload: { message: 'chat stream failed' } });
93
- resolveFinished();
94
- } catch (error) {
95
- rejectFinished(error instanceof Error ? error : new Error(String(error)));
96
- }
97
- });
98
- return {
99
- finished,
100
- cancel: vi.fn()
101
- };
102
- });
103
-
104
- await expect(
105
- sendChatTurnStream(
106
- { message: 'hi' } as never,
107
- {
108
- onReady: vi.fn(),
109
- onDelta: vi.fn(),
110
- onSessionEvent: vi.fn()
111
- }
112
- )
113
- ).rejects.toThrow('chat stream failed');
114
- });
115
- });
@@ -1,22 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { resolveChatChain } from '@/components/chat/chat-chain';
3
-
4
- describe('resolveChatChain', () => {
5
- it('defaults to ncp when no query or env override is provided', () => {
6
- vi.stubEnv('VITE_CHAT_CHAIN', '');
7
-
8
- expect(resolveChatChain('')).toBe('ncp');
9
- });
10
-
11
- it('allows explicit legacy rollback from query string', () => {
12
- vi.stubEnv('VITE_CHAT_CHAIN', 'ncp');
13
-
14
- expect(resolveChatChain('?chatChain=legacy')).toBe('legacy');
15
- });
16
-
17
- it('accepts env override when query string is absent', () => {
18
- vi.stubEnv('VITE_CHAT_CHAIN', 'legacy');
19
-
20
- expect(resolveChatChain('')).toBe('legacy');
21
- });
22
- });
@@ -1,23 +0,0 @@
1
- export type ChatChain = 'legacy' | 'ncp';
2
-
3
- const DEFAULT_CHAT_CHAIN: ChatChain = 'ncp';
4
-
5
- function normalizeChatChain(value: string | null | undefined): ChatChain | null {
6
- if (typeof value !== 'string') {
7
- return null;
8
- }
9
- const normalized = value.trim().toLowerCase();
10
- if (normalized === 'legacy' || normalized === 'ncp') {
11
- return normalized;
12
- }
13
- return null;
14
- }
15
-
16
- export function resolveChatChain(search: string): ChatChain {
17
- const fromSearch = normalizeChatChain(new URLSearchParams(search).get('chatChain'));
18
- if (fromSearch) {
19
- return fromSearch;
20
- }
21
- const fromEnv = normalizeChatChain(import.meta.env.VITE_CHAT_CHAIN);
22
- return fromEnv ?? DEFAULT_CHAT_CHAIN;
23
- }
@@ -1,171 +0,0 @@
1
- import { useMemo } from 'react';
2
- import type { Dispatch, SetStateAction } from 'react';
3
- import type { SessionEntryView, ThinkingLevel } from '@/api/types';
4
- import type { ChatModelOption } from '@/components/chat/chat-input.types';
5
- import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
6
- import {
7
- resolveRecentSessionPreferredThinking,
8
- resolveRecentSessionPreferredModel,
9
- useSyncSelectedModel,
10
- useSyncSelectedThinking
11
- } from '@/components/chat/chat-session-preference-governance';
12
- import {
13
- useChatCapabilities,
14
- useChatSessionTypes,
15
- useConfig,
16
- useConfigMeta,
17
- useSessionHistory,
18
- useSessions,
19
- } from '@/hooks/useConfig';
20
- import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
21
- import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCapability } from '@/lib/provider-models';
22
-
23
- type UseChatPageDataParams = {
24
- query: string;
25
- selectedSessionKey: string | null;
26
- selectedAgentId: string;
27
- currentSelectedModel: string;
28
- pendingSessionType: string;
29
- setPendingSessionType: Dispatch<SetStateAction<string>>;
30
- setSelectedModel: Dispatch<SetStateAction<string>>;
31
- setSelectedThinkingLevel: Dispatch<SetStateAction<ThinkingLevel | null>>;
32
- };
33
-
34
- export function useChatPageData(params: UseChatPageDataParams) {
35
- const configQuery = useConfig();
36
- const configMetaQuery = useConfigMeta();
37
- const sessionsQuery = useSessions({ q: params.query.trim() || undefined, limit: 120, activeMinutes: 0 });
38
- const installedSkillsQuery = useMarketplaceInstalled('skill');
39
- const chatCapabilitiesQuery = useChatCapabilities({
40
- sessionKey: params.selectedSessionKey,
41
- agentId: params.selectedAgentId
42
- });
43
- const historyQuery = useSessionHistory(params.selectedSessionKey, 300);
44
- const sessionTypesQuery = useChatSessionTypes();
45
- const isProviderStateResolved =
46
- (configQuery.isFetched || configQuery.isSuccess) &&
47
- (configMetaQuery.isFetched || configMetaQuery.isSuccess);
48
-
49
- const modelOptions = useMemo<ChatModelOption[]>(() => {
50
- const providers = buildProviderModelCatalog({
51
- meta: configMetaQuery.data,
52
- config: configQuery.data,
53
- onlyConfigured: true
54
- });
55
- const seen = new Set<string>();
56
- const options: ChatModelOption[] = [];
57
- for (const provider of providers) {
58
- for (const localModel of provider.models) {
59
- const value = composeProviderModel(provider.prefix, localModel);
60
- if (!value || seen.has(value)) {
61
- continue;
62
- }
63
- seen.add(value);
64
- options.push({
65
- value,
66
- modelLabel: localModel,
67
- providerLabel: provider.displayName,
68
- thinkingCapability: resolveModelThinkingCapability(provider.modelThinking, localModel, provider.aliases)
69
- });
70
- }
71
- }
72
- return options.sort((left, right) => {
73
- const providerCompare = left.providerLabel.localeCompare(right.providerLabel);
74
- if (providerCompare !== 0) {
75
- return providerCompare;
76
- }
77
- return left.modelLabel.localeCompare(right.modelLabel);
78
- });
79
- }, [configMetaQuery.data, configQuery.data]);
80
-
81
- const sessions = useMemo(() => sessionsQuery.data?.sessions ?? [], [sessionsQuery.data?.sessions]);
82
- const skillRecords = useMemo(() => installedSkillsQuery.data?.records ?? [], [installedSkillsQuery.data?.records]);
83
- const selectedSession = useMemo(
84
- () => sessions.find((session) => session.key === params.selectedSessionKey) ?? null,
85
- [params.selectedSessionKey, sessions]
86
- );
87
-
88
- const sessionTypeState = useChatSessionTypeState({
89
- selectedSession,
90
- selectedSessionKey: params.selectedSessionKey,
91
- pendingSessionType: params.pendingSessionType,
92
- setPendingSessionType: params.setPendingSessionType,
93
- sessionTypesData: sessionTypesQuery.data
94
- });
95
- const recentSessionPreferredModel = useMemo(
96
- () =>
97
- resolveRecentSessionPreferredModel({
98
- sessions,
99
- selectedSessionKey: params.selectedSessionKey,
100
- sessionType: sessionTypeState.selectedSessionType
101
- }),
102
- [params.selectedSessionKey, sessionTypeState.selectedSessionType, sessions]
103
- );
104
- const currentModelOption = useMemo(
105
- () => modelOptions.find((option) => option.value === params.currentSelectedModel),
106
- [modelOptions, params.currentSelectedModel]
107
- );
108
- const supportedThinkingLevels = useMemo(
109
- () => (currentModelOption?.thinkingCapability?.supported as ThinkingLevel[] | undefined) ?? [],
110
- [currentModelOption?.thinkingCapability?.supported]
111
- );
112
- const defaultThinkingLevel = useMemo(
113
- () => (currentModelOption?.thinkingCapability?.default as ThinkingLevel | null | undefined) ?? null,
114
- [currentModelOption?.thinkingCapability?.default]
115
- );
116
- const recentSessionPreferredThinking = useMemo(
117
- () =>
118
- resolveRecentSessionPreferredThinking({
119
- sessions,
120
- selectedSessionKey: params.selectedSessionKey,
121
- sessionType: sessionTypeState.selectedSessionType
122
- }),
123
- [params.selectedSessionKey, sessionTypeState.selectedSessionType, sessions]
124
- );
125
-
126
- useSyncSelectedModel({
127
- modelOptions,
128
- selectedSessionKey: params.selectedSessionKey,
129
- selectedSessionExists: Boolean(selectedSession),
130
- selectedSessionPreferredModel: selectedSession?.preferredModel,
131
- fallbackPreferredModel: recentSessionPreferredModel,
132
- defaultModel: configQuery.data?.agents.defaults.model,
133
- setSelectedModel: params.setSelectedModel
134
- });
135
- useSyncSelectedThinking({
136
- supportedThinkingLevels,
137
- selectedSessionKey: params.selectedSessionKey,
138
- selectedSessionExists: Boolean(selectedSession),
139
- selectedSessionPreferredThinking: selectedSession?.preferredThinking ?? null,
140
- fallbackPreferredThinking: recentSessionPreferredThinking ?? null,
141
- defaultThinkingLevel,
142
- setSelectedThinkingLevel: params.setSelectedThinkingLevel
143
- });
144
-
145
- const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
146
-
147
- return {
148
- configQuery,
149
- configMetaQuery,
150
- sessionsQuery,
151
- installedSkillsQuery,
152
- chatCapabilitiesQuery,
153
- historyQuery,
154
- sessionTypesQuery,
155
- isProviderStateResolved,
156
- modelOptions,
157
- sessions,
158
- skillRecords,
159
- selectedSession,
160
- historyMessages,
161
- ...sessionTypeState
162
- };
163
- }
164
-
165
- export function sessionDisplayName(session: SessionEntryView): string {
166
- if (session.label && session.label.trim()) {
167
- return session.label.trim();
168
- }
169
- const chunks = session.key.split(':');
170
- return chunks[chunks.length - 1] || session.key;
171
- }
@@ -1,190 +0,0 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react';
2
- import type { ChatRunView } from '@/api/types';
3
- import { useChatRuns } from '@/hooks/useConfig';
4
- import { buildActiveRunBySessionKey, buildSessionRunStatusByKey } from '@/lib/session-run-status';
5
-
6
- export type ChatMainPanelView = 'chat' | 'cron' | 'skills';
7
-
8
- export function useSessionRunStatus(params: {
9
- view: ChatMainPanelView;
10
- selectedSessionKey: string | null;
11
- activeBackendRunId: string | null;
12
- isLocallyRunning: boolean;
13
- resumeRun: (run: ChatRunView) => Promise<void>;
14
- }) {
15
- const { view, selectedSessionKey, activeBackendRunId, isLocallyRunning, resumeRun } = params;
16
- const [suppressedSessionState, setSuppressedSessionState] = useState<{
17
- sessionKey: string;
18
- runId?: string;
19
- } | null>(null);
20
- const wasLocallyRunningRef = useRef(false);
21
- const resumedRunBySessionRef = useRef(new Map<string, string>());
22
- const completedRunBySessionRef = useRef(new Map<string, string>());
23
- const locallySettledAtBySessionRef = useRef(new Map<string, number>());
24
- const latestBackendRunIdRef = useRef<string | null>(activeBackendRunId);
25
- const autoResumeEligibleSessionsRef = useRef(new Set<string>());
26
-
27
- useEffect(() => {
28
- if (!selectedSessionKey) {
29
- return;
30
- }
31
- autoResumeEligibleSessionsRef.current.add(selectedSessionKey);
32
- }, [selectedSessionKey]);
33
-
34
- useEffect(() => {
35
- if (!selectedSessionKey) {
36
- return;
37
- }
38
- if (isLocallyRunning) {
39
- autoResumeEligibleSessionsRef.current.delete(selectedSessionKey);
40
- }
41
- }, [isLocallyRunning, selectedSessionKey]);
42
-
43
- const sessionStatusRunsQuery = useChatRuns(
44
- view === 'chat'
45
- ? {
46
- states: ['queued', 'running'],
47
- limit: 200,
48
- syncActiveStates: true,
49
- isLocallyRunning
50
- }
51
- : undefined
52
- );
53
- const activeRunBySessionKey = useMemo(
54
- () => buildActiveRunBySessionKey(sessionStatusRunsQuery.data?.runs ?? []),
55
- [sessionStatusRunsQuery.data?.runs]
56
- );
57
- const sessionRunStatusByKey = useMemo(() => {
58
- const next = buildSessionRunStatusByKey(activeRunBySessionKey);
59
- if (suppressedSessionState) {
60
- const activeRun = activeRunBySessionKey.get(suppressedSessionState.sessionKey) ?? null;
61
- if (activeRun && (!suppressedSessionState.runId || activeRun.runId === suppressedSessionState.runId)) {
62
- next.delete(suppressedSessionState.sessionKey);
63
- }
64
- }
65
- return next;
66
- }, [activeRunBySessionKey, suppressedSessionState]);
67
- const activeRun = useMemo(() => {
68
- if (!selectedSessionKey) {
69
- return null;
70
- }
71
- const run = activeRunBySessionKey.get(selectedSessionKey) ?? null;
72
- const shouldSuppress = (() => {
73
- if (!run || !suppressedSessionState) {
74
- return false;
75
- }
76
- if (suppressedSessionState.sessionKey !== selectedSessionKey) {
77
- return false;
78
- }
79
- return !suppressedSessionState.runId || run.runId === suppressedSessionState.runId;
80
- })();
81
- if (shouldSuppress) {
82
- return null;
83
- }
84
- return run;
85
- }, [activeRunBySessionKey, selectedSessionKey, suppressedSessionState]);
86
-
87
- useEffect(() => {
88
- if (!activeBackendRunId) {
89
- return;
90
- }
91
- latestBackendRunIdRef.current = activeBackendRunId;
92
- }, [activeBackendRunId]);
93
-
94
- useEffect(() => {
95
- if (view !== 'chat' || !selectedSessionKey || !activeRun) {
96
- return;
97
- }
98
- if (!autoResumeEligibleSessionsRef.current.has(selectedSessionKey)) {
99
- return;
100
- }
101
- if (isLocallyRunning) {
102
- return;
103
- }
104
- if (activeBackendRunId === activeRun.runId) {
105
- return;
106
- }
107
- const resumedRunId = resumedRunBySessionRef.current.get(selectedSessionKey);
108
- if (resumedRunId === activeRun.runId) {
109
- return;
110
- }
111
- const completedRunId = completedRunBySessionRef.current.get(selectedSessionKey);
112
- if (completedRunId && completedRunId === activeRun.runId) {
113
- return;
114
- }
115
- const locallySettledAt = locallySettledAtBySessionRef.current.get(selectedSessionKey);
116
- if (typeof locallySettledAt === 'number') {
117
- const requestedAt = Date.parse(activeRun.requestedAt ?? '');
118
- if (Number.isFinite(requestedAt)) {
119
- if (requestedAt <= locallySettledAt + 2_000) {
120
- return;
121
- }
122
- } else if (Date.now() - locallySettledAt <= 8_000) {
123
- return;
124
- }
125
- }
126
- resumedRunBySessionRef.current.set(selectedSessionKey, activeRun.runId);
127
- autoResumeEligibleSessionsRef.current.delete(selectedSessionKey);
128
- void resumeRun(activeRun);
129
- }, [activeBackendRunId, activeRun, isLocallyRunning, resumeRun, selectedSessionKey, view]);
130
-
131
- useEffect(() => {
132
- if (!selectedSessionKey) {
133
- resumedRunBySessionRef.current.clear();
134
- completedRunBySessionRef.current.clear();
135
- locallySettledAtBySessionRef.current.clear();
136
- autoResumeEligibleSessionsRef.current.clear();
137
- return;
138
- }
139
- if (!activeRunBySessionKey.has(selectedSessionKey)) {
140
- resumedRunBySessionRef.current.delete(selectedSessionKey);
141
- completedRunBySessionRef.current.delete(selectedSessionKey);
142
- locallySettledAtBySessionRef.current.delete(selectedSessionKey);
143
- }
144
- }, [activeRunBySessionKey, selectedSessionKey]);
145
-
146
- useEffect(() => {
147
- const wasRunning = wasLocallyRunningRef.current;
148
- wasLocallyRunningRef.current = isLocallyRunning;
149
- if (isLocallyRunning) {
150
- return;
151
- }
152
- if (wasRunning && selectedSessionKey) {
153
- const completedRunId = latestBackendRunIdRef.current?.trim() || activeRunBySessionKey.get(selectedSessionKey)?.runId?.trim();
154
- if (completedRunId) {
155
- completedRunBySessionRef.current.set(selectedSessionKey, completedRunId);
156
- }
157
- locallySettledAtBySessionRef.current.set(selectedSessionKey, Date.now());
158
- setSuppressedSessionState({
159
- sessionKey: selectedSessionKey,
160
- ...(completedRunId ? { runId: completedRunId } : {})
161
- });
162
- void sessionStatusRunsQuery.refetch();
163
- }
164
- }, [activeRunBySessionKey, isLocallyRunning, selectedSessionKey, sessionStatusRunsQuery]);
165
-
166
- useEffect(() => {
167
- if (!suppressedSessionState) {
168
- return;
169
- }
170
- const activeRun = activeRunBySessionKey.get(suppressedSessionState.sessionKey) ?? null;
171
- if (!activeRun) {
172
- setSuppressedSessionState(null);
173
- return;
174
- }
175
- if (suppressedSessionState.runId && activeRun.runId !== suppressedSessionState.runId) {
176
- setSuppressedSessionState(null);
177
- }
178
- }, [activeRunBySessionKey, suppressedSessionState]);
179
-
180
- useEffect(() => {
181
- if (!isLocallyRunning) {
182
- return;
183
- }
184
- if (suppressedSessionState?.sessionKey === selectedSessionKey) {
185
- setSuppressedSessionState(null);
186
- }
187
- }, [isLocallyRunning, selectedSessionKey, suppressedSessionState]);
188
-
189
- return { sessionRunStatusByKey };
190
- }
@@ -1,52 +0,0 @@
1
- import type { RunMetadataParsers } from '@nextclaw/agent-chat';
2
- import type { ChatRunView } from '@/api/types';
3
- import type { NextbotAgentRunMetadata, SendMessageParams } from '@/components/chat/chat-stream/types';
4
-
5
- export const nextbotParsers: RunMetadataParsers = {
6
- parseReady: (metadata) => {
7
- if (metadata.driver !== 'nextbot-stream' || metadata.kind !== 'ready') {
8
- return null;
9
- }
10
- return {
11
- remoteRunId: typeof metadata.backendRunId === 'string' ? metadata.backendRunId : undefined,
12
- sessionId: typeof metadata.sessionKey === 'string' ? metadata.sessionKey : undefined,
13
- stopCapable: typeof metadata.stopSupported === 'boolean' ? metadata.stopSupported : undefined,
14
- stopReason: typeof metadata.stopReason === 'string' ? metadata.stopReason : undefined
15
- };
16
- },
17
- parseFinal: (metadata) => {
18
- if (metadata.driver !== 'nextbot-stream' || metadata.kind !== 'final') {
19
- return null;
20
- }
21
- return {
22
- sessionId: typeof metadata.sessionKey === 'string' ? metadata.sessionKey : undefined,
23
- hasOutput: Boolean(metadata.hasAssistantSessionEvent)
24
- };
25
- }
26
- };
27
-
28
- export function buildSendMetadata(payload: SendMessageParams, requestedSkills: string[]): NextbotAgentRunMetadata {
29
- return {
30
- driver: 'nextbot-stream',
31
- mode: 'send',
32
- payload,
33
- requestedSkills
34
- };
35
- }
36
-
37
- export function buildResumeMetadata(run: ChatRunView): NextbotAgentRunMetadata {
38
- const fromEventIndex =
39
- Number.isFinite(run.eventCount) && run.eventCount > 0
40
- ? Math.max(0, Math.trunc(run.eventCount))
41
- : undefined;
42
- return {
43
- driver: 'nextbot-stream',
44
- mode: 'resume',
45
- runId: run.runId!,
46
- ...(typeof fromEventIndex === 'number' ? { fromEventIndex } : {}),
47
- sessionKey: run.sessionKey,
48
- ...(run.agentId ? { agentId: run.agentId } : {}),
49
- stopSupported: run.stopSupported,
50
- ...(run.stopReason ? { stopReason: run.stopReason } : {})
51
- };
52
- }