@nextclaw/ui 0.8.0 → 0.9.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 (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/assets/ChannelsList-DhvjpZcs.js +1 -0
  3. package/dist/assets/ChatPage-B8VBaMQm.js +38 -0
  4. package/dist/assets/{DocBrowser-DDX2HMXW.js → DocBrowser-LpzGe8An.js} +1 -1
  5. package/dist/assets/{LogoBadge-J53F_3JA.js → LogoBadge-Be4lktJN.js} +1 -1
  6. package/dist/assets/{MarketplacePage-0BZ4bza0.js → MarketplacePage-Cx9AI3_h.js} +3 -3
  7. package/dist/assets/{ModelConfig-Wzq9wGHV.js → ModelConfig-DuImUHIX.js} +1 -1
  8. package/dist/assets/ProvidersList-Ccleg25k.js +1 -0
  9. package/dist/assets/{RuntimeConfig-N771_AM6.js → RuntimeConfig-C6iqpJR_.js} +1 -1
  10. package/dist/assets/{SearchConfig-DVt5QVa_.js → SearchConfig-Dvp1TAXu.js} +1 -1
  11. package/dist/assets/{SecretsConfig-CkwauPa8.js → SecretsConfig-D5Ymlvt9.js} +1 -1
  12. package/dist/assets/{SessionsConfig-C3mnHzkZ.js → SessionsConfig-CIA_jA1P.js} +2 -2
  13. package/dist/assets/{chat-message-pxr79GDs.js → chat-message-B60Fh9kI.js} +1 -1
  14. package/dist/assets/index-BiPDnzv0.js +8 -0
  15. package/dist/assets/index-C8GsgIUn.css +1 -0
  16. package/dist/assets/{index-GdpEEKnz.js → index-CPDASUXh.js} +1 -1
  17. package/dist/assets/{label-CmksBHgc.js → label-D4fGx6Wb.js} +1 -1
  18. package/dist/assets/{page-layout-Db0GbnhS.js → page-layout-twy8gmBE.js} +1 -1
  19. package/dist/assets/popover-DYbYpt1j.js +1 -0
  20. package/dist/assets/{security-config-CjLFME5Q.js → security-config-BcIZ4rpb.js} +1 -1
  21. package/dist/assets/skeleton-DypBy7jp.js +1 -0
  22. package/dist/assets/{switch-C24d-UJU.js → switch-DqA6r5XR.js} +1 -1
  23. package/dist/assets/tabs-custom-C6enKKs1.js +1 -0
  24. package/dist/assets/{useConfirmDialog-BeP35LcG.js → useConfirmDialog-CHBf5Of7.js} +1 -1
  25. package/dist/assets/{vendor-psXJBy9u.js → vendor-DKBNiC31.js} +1 -1
  26. package/dist/index.html +3 -3
  27. package/package.json +6 -6
  28. package/src/api/config.ts +9 -38
  29. package/src/api/ncp-session.ts +50 -0
  30. package/src/api/types.ts +1 -0
  31. package/src/components/chat/ChatConversationPanel.test.tsx +65 -0
  32. package/src/components/chat/ChatConversationPanel.tsx +21 -12
  33. package/src/components/chat/ChatSidebar.test.tsx +203 -0
  34. package/src/components/chat/ChatSidebar.tsx +97 -7
  35. package/src/components/chat/adapters/chat-message.adapter.test.ts +132 -82
  36. package/src/components/chat/adapters/chat-message.adapter.ts +27 -9
  37. package/src/components/chat/chat-composer-state.ts +53 -0
  38. package/src/components/chat/chat-page-data.ts +30 -1
  39. package/src/components/chat/chat-page-runtime.test.ts +181 -0
  40. package/src/components/chat/chat-page-runtime.ts +101 -15
  41. package/src/components/chat/chat-session-preference-sync.test.ts +62 -0
  42. package/src/components/chat/chat-session-preference-sync.ts +75 -0
  43. package/src/components/chat/chat-stream/types.ts +3 -0
  44. package/src/components/chat/containers/chat-input-bar.container.tsx +12 -63
  45. package/src/components/chat/containers/chat-message-list.container.tsx +31 -27
  46. package/src/components/chat/legacy/LegacyChatPage.tsx +25 -0
  47. package/src/components/chat/managers/chat-input.manager.ts +48 -13
  48. package/src/components/chat/managers/chat-session-list.manager.test.ts +39 -0
  49. package/src/components/chat/managers/chat-session-list.manager.ts +9 -3
  50. package/src/components/chat/ncp/NcpChatPage.tsx +53 -13
  51. package/src/components/chat/ncp/ncp-chat-input.manager.ts +48 -12
  52. package/src/components/chat/ncp/ncp-chat-page-data.ts +34 -2
  53. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +1 -1
  54. package/src/components/chat/ncp/ncp-session-adapter.test.ts +27 -1
  55. package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
  56. package/src/components/chat/presenter/chat-presenter-context.tsx +2 -0
  57. package/src/components/chat/stores/chat-input.store.ts +4 -0
  58. package/src/components/chat/stores/chat-thread.store.ts +2 -0
  59. package/src/components/chat/useChatSessionTypeState.test.tsx +58 -0
  60. package/src/components/chat/useChatSessionTypeState.ts +25 -8
  61. package/src/hooks/use-ncp-chat-session-types.ts +11 -0
  62. package/src/hooks/useConfig.ts +2 -4
  63. package/src/hooks/useMarketplace.ts +7 -4
  64. package/src/hooks/useWebSocket.ts +23 -2
  65. package/dist/assets/ChannelsList-DBcoVJRW.js +0 -1
  66. package/dist/assets/ChatPage-CD3cxyyM.js +0 -37
  67. package/dist/assets/ProvidersList-kwzRS8_M.js +0 -1
  68. package/dist/assets/index-BIvFMkN4.js +0 -1
  69. package/dist/assets/index-CzkY1reu.js +0 -8
  70. package/dist/assets/index-RZ0kHHRI.css +0 -1
  71. package/dist/assets/skeleton-CkpQeVWN.js +0 -1
  72. package/dist/assets/tabs-custom-D89bh-fc.js +0 -1
@@ -1,14 +1,26 @@
1
+ import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
1
2
  import type { SetStateAction } from 'react';
2
3
  import type { ThinkingLevel } from '@/api/types';
4
+ import { updateNcpSession } from '@/api/ncp-session';
5
+ import {
6
+ createChatComposerNodesFromDraft,
7
+ createInitialChatComposerNodes,
8
+ deriveChatComposerDraft,
9
+ deriveSelectedSkillsFromComposer,
10
+ syncComposerSkills
11
+ } from '@/components/chat/chat-composer-state';
3
12
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
4
13
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
5
14
  import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
6
15
  import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
7
16
  import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
17
+ import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
8
18
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
9
19
  import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
10
20
 
11
21
  export class NcpChatInputManager {
22
+ private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateNcpSession);
23
+
12
24
  constructor(
13
25
  private uiManager: ChatUiManager,
14
26
  private streamActionsManager: ChatStreamActionsManager,
@@ -32,6 +44,17 @@ export class NcpChatInputManager {
32
44
  return next;
33
45
  };
34
46
 
47
+ private isSameStringArray = (left: string[], right: string[]): boolean =>
48
+ left.length === right.length && left.every((value, index) => value === right[index]);
49
+
50
+ private syncComposerSnapshot = (nodes: ChatComposerNode[]) => {
51
+ useChatInputStore.getState().setSnapshot({
52
+ composerNodes: nodes,
53
+ draft: deriveChatComposerDraft(nodes),
54
+ selectedSkills: deriveSelectedSkillsFromComposer(nodes)
55
+ });
56
+ };
57
+
35
58
  syncSnapshot = (patch: Partial<ChatInputSnapshot>) => {
36
59
  if (!this.hasSnapshotChanges(patch)) {
37
60
  return;
@@ -42,8 +65,8 @@ export class NcpChatInputManager {
42
65
  Object.prototype.hasOwnProperty.call(patch, 'selectedModel') ||
43
66
  Object.prototype.hasOwnProperty.call(patch, 'selectedThinkingLevel')
44
67
  ) {
45
- const snapshot = useChatInputStore.getState().snapshot;
46
- this.reconcileThinkingForModel(snapshot.selectedModel);
68
+ const { selectedModel } = useChatInputStore.getState().snapshot;
69
+ this.reconcileThinkingForModel(selectedModel);
47
70
  }
48
71
  };
49
72
 
@@ -53,7 +76,16 @@ export class NcpChatInputManager {
53
76
  if (value === prev) {
54
77
  return;
55
78
  }
56
- useChatInputStore.getState().setSnapshot({ draft: value });
79
+ this.syncComposerSnapshot(createChatComposerNodesFromDraft(value));
80
+ };
81
+
82
+ setComposerNodes = (next: SetStateAction<ChatComposerNode[]>) => {
83
+ const prev = useChatInputStore.getState().snapshot.composerNodes;
84
+ const value = this.resolveUpdateValue(prev, next);
85
+ if (Object.is(value, prev)) {
86
+ return;
87
+ }
88
+ this.syncComposerSnapshot(value);
57
89
  };
58
90
 
59
91
  setPendingSessionType = (next: SetStateAction<string>) => {
@@ -72,13 +104,12 @@ export class NcpChatInputManager {
72
104
  if (!message) {
73
105
  return;
74
106
  }
75
- const requestedSkills = inputSnapshot.selectedSkills;
107
+ const { selectedSkills: requestedSkills, composerNodes } = inputSnapshot;
76
108
  const sessionKey = sessionSnapshot.selectedSessionKey ?? this.getDraftSessionId();
77
109
  if (!sessionSnapshot.selectedSessionKey) {
78
110
  this.uiManager.goToSession(sessionKey, { replace: true });
79
111
  }
80
- this.setDraft('');
81
- this.setSelectedSkills([]);
112
+ this.setComposerNodes(createInitialChatComposerNodes());
82
113
  await this.streamActionsManager.sendMessage({
83
114
  message,
84
115
  sessionKey,
@@ -88,7 +119,8 @@ export class NcpChatInputManager {
88
119
  thinkingLevel: inputSnapshot.selectedThinkingLevel ?? undefined,
89
120
  stopSupported: true,
90
121
  requestedSkills,
91
- restoreDraftOnError: true
122
+ restoreDraftOnError: true,
123
+ composerNodes
92
124
  });
93
125
  };
94
126
 
@@ -125,20 +157,23 @@ export class NcpChatInputManager {
125
157
  };
126
158
 
127
159
  setSelectedSkills = (next: SetStateAction<string[]>) => {
128
- const prev = useChatInputStore.getState().snapshot.selectedSkills;
160
+ const snapshot = useChatInputStore.getState().snapshot;
161
+ const { selectedSkills: prev } = snapshot;
129
162
  const value = this.resolveUpdateValue(prev, next);
130
- if (Object.is(value, prev)) {
163
+ if (this.isSameStringArray(value, prev)) {
131
164
  return;
132
165
  }
133
- useChatInputStore.getState().setSnapshot({ selectedSkills: value });
166
+ this.syncComposerSnapshot(syncComposerSkills(snapshot.composerNodes, value, snapshot.skillRecords));
134
167
  };
135
168
 
136
169
  selectModel = (value: string) => {
137
170
  this.setSelectedModel(value);
171
+ this.sessionPreferenceSync.syncSelectedSessionPreferences();
138
172
  };
139
173
 
140
174
  selectThinkingLevel = (value: ThinkingLevel) => {
141
175
  this.setSelectedThinkingLevel(value);
176
+ this.sessionPreferenceSync.syncSelectedSessionPreferences();
142
177
  };
143
178
 
144
179
  selectSkills = (next: string[]) => {
@@ -165,8 +200,9 @@ export class NcpChatInputManager {
165
200
  private reconcileThinkingForModel(model: string): void {
166
201
  const snapshot = useChatInputStore.getState().snapshot;
167
202
  const modelOption = snapshot.modelOptions.find((option) => option.value === model);
168
- const nextThinking = this.resolveThinkingForModel(modelOption, snapshot.selectedThinkingLevel);
169
- if (nextThinking !== snapshot.selectedThinkingLevel) {
203
+ const { selectedThinkingLevel } = snapshot;
204
+ const nextThinking = this.resolveThinkingForModel(modelOption, selectedThinkingLevel);
205
+ if (nextThinking !== selectedThinkingLevel) {
170
206
  useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: nextThinking });
171
207
  }
172
208
  }
@@ -7,12 +7,17 @@ import {
7
7
  readNcpSessionPreferredThinking
8
8
  } from '@/components/chat/ncp/ncp-session-adapter';
9
9
  import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
10
- import { useSyncSelectedModel } from '@/components/chat/chat-page-runtime';
10
+ import {
11
+ resolveSelectedModelValue,
12
+ resolveRecentSessionPreferredModel,
13
+ useSyncSelectedModel
14
+ } from '@/components/chat/chat-page-runtime';
11
15
  import {
12
16
  useConfig,
13
17
  useConfigMeta,
14
18
  useNcpSessions
15
19
  } from '@/hooks/useConfig';
20
+ import { useNcpChatSessionTypes } from '@/hooks/use-ncp-chat-session-types';
16
21
  import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
17
22
  import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCapability } from '@/lib/provider-models';
18
23
 
@@ -36,6 +41,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
36
41
  const configQuery = useConfig();
37
42
  const configMetaQuery = useConfigMeta();
38
43
  const sessionsQuery = useNcpSessions({ limit: 200 });
44
+ const sessionTypesQuery = useNcpChatSessionTypes();
39
45
  const installedSkillsQuery = useMarketplaceInstalled('skill');
40
46
  const isProviderStateResolved =
41
47
  (configQuery.isFetched || configQuery.isSuccess) &&
@@ -107,20 +113,45 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
107
113
  selectedSessionKey: params.selectedSessionKey,
108
114
  pendingSessionType: params.pendingSessionType,
109
115
  setPendingSessionType: params.setPendingSessionType,
110
- sessionTypesData: null
116
+ sessionTypesData: sessionTypesQuery.data
111
117
  });
118
+ const recentSessionPreferredModel = useMemo(
119
+ () =>
120
+ resolveRecentSessionPreferredModel({
121
+ sessions: allSessions,
122
+ selectedSessionKey: params.selectedSessionKey,
123
+ sessionType: sessionTypeState.selectedSessionType
124
+ }),
125
+ [allSessions, params.selectedSessionKey, sessionTypeState.selectedSessionType]
126
+ );
112
127
 
113
128
  useSyncSelectedModel({
114
129
  modelOptions,
130
+ selectedSessionKey: params.selectedSessionKey,
115
131
  selectedSessionPreferredModel: selectedSession?.preferredModel,
132
+ fallbackPreferredModel: recentSessionPreferredModel,
116
133
  defaultModel: configQuery.data?.agents.defaults.model,
117
134
  setSelectedModel: params.setSelectedModel
118
135
  });
119
136
 
137
+ const hydratedSessionModel = useMemo(
138
+ () =>
139
+ resolveSelectedModelValue({
140
+ currentSelectedModel: '',
141
+ modelOptions,
142
+ selectedSessionPreferredModel: selectedSession?.preferredModel,
143
+ fallbackPreferredModel: recentSessionPreferredModel,
144
+ defaultModel: configQuery.data?.agents.defaults.model,
145
+ preferSessionPreferredModel: true
146
+ }),
147
+ [configQuery.data?.agents.defaults.model, modelOptions, recentSessionPreferredModel, selectedSession?.preferredModel]
148
+ );
149
+
120
150
  return {
121
151
  configQuery,
122
152
  configMetaQuery,
123
153
  sessionsQuery,
154
+ sessionTypesQuery,
124
155
  installedSkillsQuery,
125
156
  isProviderStateResolved,
126
157
  modelOptions,
@@ -128,6 +159,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
128
159
  sessions,
129
160
  skillRecords,
130
161
  selectedSession,
162
+ hydratedSessionModel,
131
163
  selectedSessionThinkingLevel,
132
164
  ...sessionTypeState
133
165
  };
@@ -1,4 +1,4 @@
1
- import { deleteNcpSession as deleteNcpSessionApi } from '@/api/config';
1
+ import { deleteNcpSession as deleteNcpSessionApi } from '@/api/ncp-session';
2
2
  import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
3
3
  import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
4
4
  import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
@@ -1,4 +1,8 @@
1
- import { adaptNcpSessionSummary, readNcpSessionPreferredThinking } from '@/components/chat/ncp/ncp-session-adapter';
1
+ import {
2
+ adaptNcpSessionSummary,
3
+ buildNcpSessionRunStatusByKey,
4
+ readNcpSessionPreferredThinking
5
+ } from '@/components/chat/ncp/ncp-session-adapter';
2
6
  import type { NcpSessionSummaryView } from '@/api/types';
3
7
 
4
8
  function createSummary(partial: Partial<NcpSessionSummaryView> = {}): NcpSessionSummaryView {
@@ -47,3 +51,25 @@ describe('readNcpSessionPreferredThinking', () => {
47
51
  expect(thinking).toBe('high');
48
52
  });
49
53
  });
54
+
55
+ describe('buildNcpSessionRunStatusByKey', () => {
56
+ it('marks the active local session as running before the server summary catches up', () => {
57
+ const statuses = buildNcpSessionRunStatusByKey({
58
+ summaries: [createSummary({ sessionId: 'ncp-session-1', status: 'idle' })],
59
+ activeSessionId: 'ncp-session-1',
60
+ isLocallyRunning: true
61
+ });
62
+
63
+ expect(statuses.get('ncp-session-1')).toBe('running');
64
+ });
65
+
66
+ it('keeps persisted running sessions marked as running', () => {
67
+ const statuses = buildNcpSessionRunStatusByKey({
68
+ summaries: [createSummary({ sessionId: 'ncp-session-2', status: 'running' })],
69
+ activeSessionId: null,
70
+ isLocallyRunning: false
71
+ });
72
+
73
+ expect(statuses.get('ncp-session-2')).toBe('running');
74
+ });
75
+ });
@@ -1,6 +1,7 @@
1
1
  import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
2
2
  import type { NcpMessagePart } from '@nextclaw/ncp';
3
3
  import type { NcpMessageView, NcpSessionSummaryView, SessionEntryView, ThinkingLevel } from '@/api/types';
4
+ import type { SessionRunStatus } from '@/lib/session-run-status';
4
5
 
5
6
  const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
6
7
 
@@ -167,6 +168,7 @@ export function adaptNcpMessageToUiMessage(message: NcpMessageView): UIMessage {
167
168
  }
168
169
 
169
170
  export function adaptNcpMessagesToUiMessages(messages: readonly NcpMessageView[]): UIMessage[] {
171
+ console.log('[adaptNcpMessagesToUiMessages]', { messages });
170
172
  return messages.map(adaptNcpMessageToUiMessage);
171
173
  }
172
174
 
@@ -189,6 +191,24 @@ export function adaptNcpSessionSummaries(summaries: NcpSessionSummaryView[]): Se
189
191
  return summaries.map(adaptNcpSessionSummary);
190
192
  }
191
193
 
194
+ export function buildNcpSessionRunStatusByKey(params: {
195
+ summaries: readonly NcpSessionSummaryView[];
196
+ activeSessionId?: string | null;
197
+ isLocallyRunning?: boolean;
198
+ }): Map<string, SessionRunStatus> {
199
+ const map = new Map<string, SessionRunStatus>();
200
+ for (const summary of params.summaries) {
201
+ if (summary.status === 'running') {
202
+ map.set(summary.sessionId, 'running');
203
+ }
204
+ }
205
+ const activeSessionId = readOptionalString(params.activeSessionId);
206
+ if (params.isLocallyRunning && activeSessionId) {
207
+ map.set(activeSessionId, 'running');
208
+ }
209
+ return map;
210
+ }
211
+
192
212
  export function createNcpSessionId(): string {
193
213
  return `ncp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
194
214
  }
@@ -1,3 +1,4 @@
1
+ import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
1
2
  import { createContext, useContext } from 'react';
2
3
  import type { ReactNode } from 'react';
3
4
  import type { SetStateAction } from 'react';
@@ -11,6 +12,7 @@ import type { ThinkingLevel } from '@/api/types';
11
12
  export type ChatInputManagerLike = {
12
13
  syncSnapshot: (patch: Record<string, unknown>) => void;
13
14
  setDraft: (next: SetStateAction<string>) => void;
15
+ setComposerNodes: (next: SetStateAction<ChatComposerNode[]>) => void;
14
16
  setPendingSessionType: (next: SetStateAction<string>) => void;
15
17
  send: () => Promise<void>;
16
18
  stop: () => Promise<void>;
@@ -1,10 +1,13 @@
1
1
  import { create } from 'zustand';
2
+ import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
2
3
  import type { MarketplaceInstalledRecord } from '@/api/types';
3
4
  import type { ThinkingLevel } from '@/api/types';
4
5
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
6
+ import { createInitialChatComposerNodes } from '@/components/chat/chat-composer-state';
5
7
 
6
8
  export type ChatInputSnapshot = {
7
9
  isProviderStateResolved: boolean;
10
+ composerNodes: ChatComposerNode[];
8
11
  draft: string;
9
12
  pendingSessionType: string;
10
13
  defaultSessionType: string;
@@ -33,6 +36,7 @@ type ChatInputStore = {
33
36
 
34
37
  const initialSnapshot: ChatInputSnapshot = {
35
38
  isProviderStateResolved: false,
39
+ composerNodes: createInitialChatComposerNodes(),
36
40
  draft: '',
37
41
  pendingSessionType: 'native',
38
42
  defaultSessionType: 'native',
@@ -8,6 +8,7 @@ export type ChatThreadSnapshot = {
8
8
  modelOptions: ChatModelOption[];
9
9
  sessionTypeUnavailable: boolean;
10
10
  sessionTypeUnavailableMessage?: string | null;
11
+ sessionTypeLabel?: string | null;
11
12
  selectedSessionKey: string | null;
12
13
  sessionDisplayName?: string;
13
14
  canDeleteSession: boolean;
@@ -29,6 +30,7 @@ const initialSnapshot: ChatThreadSnapshot = {
29
30
  modelOptions: [],
30
31
  sessionTypeUnavailable: false,
31
32
  sessionTypeUnavailableMessage: null,
33
+ sessionTypeLabel: null,
32
34
  selectedSessionKey: null,
33
35
  sessionDisplayName: undefined,
34
36
  canDeleteSession: false,
@@ -0,0 +1,58 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { resolveSessionTypeLabel, useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
4
+
5
+ vi.mock('@/lib/i18n', () => ({
6
+ t: (key: string) => key
7
+ }));
8
+
9
+ describe('useChatSessionTypeState', () => {
10
+ it('formats non-native runtime labels generically when no explicit label is provided', () => {
11
+ expect(resolveSessionTypeLabel('workspace-agent')).toBe('Workspace Agent');
12
+ });
13
+
14
+ it('preserves an explicitly selected draft session type instead of resetting to the default', () => {
15
+ const setPendingSessionType = vi.fn();
16
+
17
+ const { result } = renderHook(() =>
18
+ useChatSessionTypeState({
19
+ selectedSession: null,
20
+ selectedSessionKey: null,
21
+ pendingSessionType: 'codex-sdk',
22
+ setPendingSessionType,
23
+ sessionTypesData: {
24
+ defaultType: 'native',
25
+ options: [
26
+ { value: 'native', label: 'Native' },
27
+ { value: 'codex-sdk', label: 'Codex' }
28
+ ]
29
+ }
30
+ })
31
+ );
32
+
33
+ expect(result.current.selectedSessionType).toBe('codex-sdk');
34
+ expect(setPendingSessionType).not.toHaveBeenCalled();
35
+ });
36
+
37
+ it('hydrates the draft session type from the runtime default when no explicit type exists', () => {
38
+ const setPendingSessionType = vi.fn();
39
+
40
+ renderHook(() =>
41
+ useChatSessionTypeState({
42
+ selectedSession: null,
43
+ selectedSessionKey: null,
44
+ pendingSessionType: '',
45
+ setPendingSessionType,
46
+ sessionTypesData: {
47
+ defaultType: 'codex-sdk',
48
+ options: [
49
+ { value: 'native', label: 'Native' },
50
+ { value: 'codex-sdk', label: 'Codex' }
51
+ ]
52
+ }
53
+ })
54
+ );
55
+
56
+ expect(setPendingSessionType).toHaveBeenCalledWith('codex-sdk');
57
+ });
58
+ });
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo } from 'react';
1
+ import { useEffect, useMemo, useRef } from 'react';
2
2
  import type { Dispatch, SetStateAction } from 'react';
3
3
  import type { SessionEntryView } from '@/api/types';
4
4
  import { t } from '@/lib/i18n';
@@ -33,13 +33,16 @@ export function resolveSessionTypeLabel(sessionType: string, fallbackLabel?: str
33
33
  if (sessionType === 'native') {
34
34
  return t('chatSessionTypeNative');
35
35
  }
36
- if (sessionType === 'codex-sdk') {
37
- return t('chatSessionTypeCodex');
36
+ const normalizedFallback = fallbackLabel?.trim();
37
+ if (normalizedFallback) {
38
+ return normalizedFallback;
38
39
  }
39
- if (sessionType === 'claude-agent-sdk') {
40
- return t('chatSessionTypeClaude');
41
- }
42
- return fallbackLabel?.trim() || sessionType;
40
+ return sessionType
41
+ .trim()
42
+ .split(/[-_]+/g)
43
+ .filter(Boolean)
44
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
45
+ .join(' ') || sessionType;
43
46
  }
44
47
 
45
48
  function buildSessionTypeOptions(
@@ -113,6 +116,7 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
113
116
  () => normalizeSessionType(sessionTypesData?.defaultType ?? DEFAULT_SESSION_TYPE),
114
117
  [sessionTypesData?.defaultType]
115
118
  );
119
+ const lastAutoPendingSessionTypeRef = useRef<string | null>(null);
116
120
  const selectedSessionType = useMemo(
117
121
  () => normalizeSessionType(selectedSession?.sessionType ?? pendingSessionType ?? defaultSessionType),
118
122
  [defaultSessionType, pendingSessionType, selectedSession?.sessionType]
@@ -122,8 +126,21 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
122
126
  if (selectedSessionKey) {
123
127
  return;
124
128
  }
129
+ const rawPending = typeof pendingSessionType === 'string' ? pendingSessionType.trim() : '';
130
+ const normalizedPending = normalizeSessionType(pendingSessionType);
131
+ const shouldFollowDefault =
132
+ rawPending.length === 0 ||
133
+ lastAutoPendingSessionTypeRef.current === normalizedPending ||
134
+ (lastAutoPendingSessionTypeRef.current === null && normalizedPending === DEFAULT_SESSION_TYPE);
135
+ if (!shouldFollowDefault) {
136
+ return;
137
+ }
138
+ lastAutoPendingSessionTypeRef.current = defaultSessionType;
139
+ if (normalizedPending === defaultSessionType) {
140
+ return;
141
+ }
125
142
  setPendingSessionType(defaultSessionType);
126
- }, [defaultSessionType, selectedSessionKey, setPendingSessionType]);
143
+ }, [defaultSessionType, pendingSessionType, selectedSessionKey, setPendingSessionType]);
127
144
 
128
145
  const canEditSessionType = !selectedSessionKey || Boolean(selectedSession?.sessionTypeMutable);
129
146
  const availableSessionTypeSet = useMemo(
@@ -0,0 +1,11 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { fetchNcpChatSessionTypes } from '@/api/config';
3
+
4
+ export function useNcpChatSessionTypes() {
5
+ return useQuery({
6
+ queryKey: ['ncp-session-types'],
7
+ queryFn: fetchNcpChatSessionTypes,
8
+ staleTime: 10_000,
9
+ retry: false
10
+ });
11
+ }
@@ -19,11 +19,8 @@ import {
19
19
  executeConfigAction,
20
20
  fetchSessions,
21
21
  fetchSessionHistory,
22
- fetchNcpSessions,
23
- fetchNcpSessionMessages,
24
22
  updateSession,
25
23
  deleteSession,
26
- deleteNcpSession,
27
24
  sendChatTurn,
28
25
  fetchChatRun,
29
26
  fetchChatRuns,
@@ -34,6 +31,7 @@ import {
34
31
  setCronJobEnabled,
35
32
  runCronJob
36
33
  } from '@/api/config';
34
+ import { deleteNcpSession, fetchNcpSessionMessages, fetchNcpSessions } from '@/api/ncp-session';
37
35
  import { toast } from 'sonner';
38
36
  import { t } from '@/lib/i18n';
39
37
 
@@ -390,7 +388,7 @@ export function useChatRuns(params?: {
390
388
  if (params?.isLocallyRunning) {
391
389
  return 800;
392
390
  }
393
- const data = query.state.data;
391
+ const { data } = query.state;
394
392
  const hasActiveRuns = Array.isArray(data?.runs) && data.runs.length > 0;
395
393
  return hasActiveRuns ? 800 : false;
396
394
  },
@@ -61,8 +61,10 @@ export function useInstallMarketplaceItem() {
61
61
  mutationFn: (request: MarketplaceInstallRequest) => installMarketplaceItem(request),
62
62
  onSuccess: (result) => {
63
63
  queryClient.invalidateQueries({ queryKey: ['marketplace-installed', result.type] });
64
- queryClient.refetchQueries({ queryKey: ['marketplace-installed', result.type], type: 'active' });
65
- queryClient.refetchQueries({ queryKey: ['marketplace-items'], type: 'active' });
64
+ queryClient.invalidateQueries({ queryKey: ['marketplace-items'] });
65
+ if (result.type === 'plugin') {
66
+ queryClient.invalidateQueries({ queryKey: ['ncp-session-types'] });
67
+ }
66
68
  const fallback = result.type === 'plugin'
67
69
  ? t('marketplaceInstallSuccessPlugin')
68
70
  : t('marketplaceInstallSuccessSkill');
@@ -82,8 +84,9 @@ export function useManageMarketplaceItem() {
82
84
  onSuccess: (result) => {
83
85
  queryClient.invalidateQueries({ queryKey: ['marketplace-installed', result.type] });
84
86
  queryClient.invalidateQueries({ queryKey: ['marketplace-items'] });
85
- queryClient.refetchQueries({ queryKey: ['marketplace-installed', result.type], type: 'active' });
86
- queryClient.refetchQueries({ queryKey: ['marketplace-items'], type: 'active' });
87
+ if (result.type === 'plugin') {
88
+ queryClient.invalidateQueries({ queryKey: ['ncp-session-types'] });
89
+ }
87
90
  const fallback = result.action === 'enable'
88
91
  ? t('marketplaceEnableSuccess')
89
92
  : result.action === 'disable'
@@ -73,11 +73,25 @@ export function useWebSocket(queryClient?: QueryClient) {
73
73
  return;
74
74
  }
75
75
  queryClient.invalidateQueries({ queryKey: ['sessions'] });
76
+ queryClient.invalidateQueries({ queryKey: ['ncp-sessions'] });
76
77
  if (sessionKey && sessionKey.trim().length > 0) {
77
78
  queryClient.invalidateQueries({ queryKey: ['session-history', sessionKey.trim()] });
79
+ queryClient.invalidateQueries({ queryKey: ['ncp-session-messages', sessionKey.trim()] });
78
80
  return;
79
81
  }
80
82
  queryClient.invalidateQueries({ queryKey: ['session-history'] });
83
+ queryClient.invalidateQueries({ queryKey: ['ncp-session-messages'] });
84
+ };
85
+
86
+ const shouldInvalidateConfigQuery = (configPath: string) => {
87
+ const normalized = configPath.trim().toLowerCase();
88
+ if (!normalized) {
89
+ return true;
90
+ }
91
+ if (normalized.startsWith('plugins') || normalized.startsWith('skills')) {
92
+ return false;
93
+ }
94
+ return true;
81
95
  };
82
96
 
83
97
  setConnectionStatus('connecting');
@@ -98,13 +112,20 @@ export function useWebSocket(queryClient?: QueryClient) {
98
112
  });
99
113
 
100
114
  client.on('config.updated', (event) => {
115
+ const payload = event.payload as { path?: unknown } | undefined;
116
+ const configPath = typeof payload?.path === 'string' ? payload.path : '';
101
117
  // Trigger refetch of config
102
- if (queryClient) {
118
+ if (queryClient && shouldInvalidateConfigQuery(configPath)) {
103
119
  queryClient.invalidateQueries({ queryKey: ['config'] });
104
120
  }
105
- if (event.type === 'config.updated' && event.payload.path.startsWith('session')) {
121
+ if (configPath.startsWith('session')) {
106
122
  invalidateSessionQueries();
107
123
  }
124
+ if (configPath.startsWith('plugins')) {
125
+ queryClient?.invalidateQueries({ queryKey: ['ncp-session-types'] });
126
+ queryClient?.invalidateQueries({ queryKey: ['marketplace-installed', 'plugin'] });
127
+ queryClient?.invalidateQueries({ queryKey: ['marketplace-items'] });
128
+ }
108
129
  });
109
130
 
110
131
  client.on('run.updated', (event) => {
@@ -1 +0,0 @@
1
- import{r as v,j as a,a5 as Z,F as ee,e as T,K as ae,ah as te,aR as se,aS as ne,aT as le,z as re,s as oe,a8 as ce,v as ie}from"./vendor-psXJBy9u.js";import{t as e,c as I,Y as me,u as q,a as $,b as H,_ as pe,$ as de,I as D,S as be,e as ue,f as xe,g as ye,h as ge,B as E}from"./index-CzkY1reu.js";import{L as he}from"./label-CmksBHgc.js";import{S as fe}from"./switch-C24d-UJU.js";import{L as K,S as J}from"./LogoBadge-J53F_3JA.js";import{h as _}from"./config-hints-CApS3K_7.js";import{c as we,b as ve,a as je,C as ke}from"./config-layout-BHnOoweL.js";import{T as Se}from"./tabs-custom-D89bh-fc.js";import{P as Ce,a as Ne}from"./page-layout-Db0GbnhS.js";function Pe({value:t,onChange:m,className:i,placeholder:r=""}){const[o,u]=v.useState(""),d=x=>{x.key==="Enter"&&o.trim()?(x.preventDefault(),m([...t,o.trim()]),u("")):x.key==="Backspace"&&!o&&t.length>0&&m(t.slice(0,-1))},g=x=>{m(t.filter((j,h)=>h!==x))};return a.jsxs("div",{className:I("flex flex-wrap gap-2 p-2 border rounded-md min-h-[42px]",i),children:[t.map((x,j)=>a.jsxs("span",{className:"inline-flex items-center gap-1 px-2 py-1 bg-primary text-primary-foreground rounded text-sm",children:[x,a.jsx("button",{type:"button",onClick:()=>g(j),className:"hover:text-red-300 transition-colors",children:a.jsx(Z,{className:"h-3 w-3"})})]},j)),a.jsx("input",{type:"text",value:o,onChange:x=>u(x.target.value),onKeyDown:d,className:"flex-1 outline-none min-w-[100px] bg-transparent text-sm",placeholder:r||e("enterTag")})]})}function z(t){var r,o;const m=me();return((r=t.tutorialUrls)==null?void 0:r[m])||((o=t.tutorialUrls)==null?void 0:o.default)||t.tutorialUrl}const Ie={telegram:"telegram.svg",slack:"slack.svg",discord:"discord.svg",whatsapp:"whatsapp.svg",qq:"qq.svg",feishu:"feishu.svg",dingtalk:"dingtalk.svg",wecom:"wecom.svg",mochat:"mochat.svg",email:"email.svg"};function Fe(t,m){const i=m.toLowerCase(),r=t[i];return r?`/logos/${r}`:null}function Y(t){return Fe(Ie,t)}const R=[{value:"pairing",label:"pairing"},{value:"allowlist",label:"allowlist"},{value:"open",label:"open"},{value:"disabled",label:"disabled"}],B=[{value:"open",label:"open"},{value:"allowlist",label:"allowlist"},{value:"disabled",label:"disabled"}],Te=[{value:"off",label:"off"},{value:"partial",label:"partial"},{value:"block",label:"block"},{value:"progress",label:"progress"}],De=t=>t.includes("token")||t.includes("secret")||t.includes("password")?a.jsx(ae,{className:"h-3.5 w-3.5 text-gray-500"}):t.includes("url")||t.includes("host")?a.jsx(te,{className:"h-3.5 w-3.5 text-gray-500"}):t.includes("email")||t.includes("mail")?a.jsx(se,{className:"h-3.5 w-3.5 text-gray-500"}):t.includes("id")||t.includes("from")?a.jsx(ne,{className:"h-3.5 w-3.5 text-gray-500"}):t==="enabled"||t==="consentGranted"?a.jsx(le,{className:"h-3.5 w-3.5 text-gray-500"}):a.jsx(re,{className:"h-3.5 w-3.5 text-gray-500"});function G(){return{telegram:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"proxy",type:"text",label:e("proxy")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:R},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:B},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],discord:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"gatewayUrl",type:"text",label:e("gatewayUrl")},{name:"intents",type:"number",label:e("intents")},{name:"proxy",type:"text",label:e("proxy")},{name:"mediaMaxMb",type:"number",label:e("attachmentMaxSizeMb")},{name:"streaming",type:"select",label:e("streamingMode"),options:Te},{name:"draftChunk",type:"json",label:e("draftChunkingJson")},{name:"textChunkLimit",type:"number",label:e("textChunkLimit")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:R},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:B},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],whatsapp:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"bridgeUrl",type:"text",label:e("bridgeUrl")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],feishu:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"appSecret",type:"password",label:e("appSecret")},{name:"encryptKey",type:"password",label:e("encryptKey")},{name:"verificationToken",type:"password",label:e("verificationToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],dingtalk:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"clientId",type:"text",label:e("clientId")},{name:"clientSecret",type:"password",label:e("clientSecret")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],wecom:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"corpId",type:"text",label:e("corpId")},{name:"agentId",type:"text",label:e("agentId")},{name:"secret",type:"password",label:e("secret")},{name:"token",type:"password",label:e("token")},{name:"callbackPort",type:"number",label:e("callbackPort")},{name:"callbackPath",type:"text",label:e("callbackPath")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],slack:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"mode",type:"text",label:e("mode")},{name:"webhookPath",type:"text",label:e("webhookPath")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"botToken",type:"password",label:e("botToken")},{name:"appToken",type:"password",label:e("appToken")}],email:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"consentGranted",type:"boolean",label:e("consentGranted")},{name:"imapHost",type:"text",label:e("imapHost")},{name:"imapPort",type:"number",label:e("imapPort")},{name:"imapUsername",type:"text",label:e("imapUsername")},{name:"imapPassword",type:"password",label:e("imapPassword")},{name:"fromAddress",type:"email",label:e("fromAddress")}],mochat:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"baseUrl",type:"text",label:e("baseUrl")},{name:"clawToken",type:"password",label:e("clawToken")},{name:"agentUserId",type:"text",label:e("agentUserId")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],qq:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"secret",type:"password",label:e("appSecret")},{name:"markdownSupport",type:"boolean",label:e("markdownSupport")},{name:"allowFrom",type:"tags",label:e("allowFrom")}]}}function A(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function V(t,m){const i={...t};for(const[r,o]of Object.entries(m)){const u=i[r];if(A(u)&&A(o)){i[r]=V(u,o);continue}i[r]=o}return i}function Ae(t,m){const i=t.split("."),r={};let o=r;for(let u=0;u<i.length-1;u+=1){const d=i[u];o[d]={},o=o[d]}return o[i[i.length-1]]=m,r}function Le({channelName:t}){var O,U;const{data:m}=q(),{data:i}=$(),{data:r}=H(),o=pe(),u=de(),[d,g]=v.useState({}),[x,j]=v.useState({}),[h,f]=v.useState(null),k=t?m==null?void 0:m.channels[t]:null,w=t?G()[t]??[]:[],c=r==null?void 0:r.uiHints,p=t?`channels.${t}`:null,S=((O=r==null?void 0:r.actions)==null?void 0:O.filter(s=>s.scope===p))??[],C=t&&(((U=_(`channels.${t}`,c))==null?void 0:U.label)??t),P=i==null?void 0:i.channels.find(s=>s.name===t),F=P?z(P):void 0;v.useEffect(()=>{if(k){g({...k});const s={};(t?G()[t]??[]:[]).filter(l=>l.type==="json").forEach(l=>{const y=k[l.name];s[l.name]=JSON.stringify(y??{},null,2)}),j(s)}else g({}),j({})},[k,t]);const N=(s,n)=>{g(l=>({...l,[s]:n}))},L=s=>{if(s.preventDefault(),!t)return;const n={...d};for(const l of w){if(l.type!=="password")continue;const y=n[l.name];(typeof y!="string"||y.length===0)&&delete n[l.name]}for(const l of w){if(l.type!=="json")continue;const y=x[l.name]??"";try{n[l.name]=y.trim()?JSON.parse(y):{}}catch{T.error(`${e("invalidJson")}: ${l.name}`);return}}o.mutate({channel:t,data:n})},Q=s=>{if(!s||!t)return;const n=s.channels;if(!A(n))return;const l=n[t];A(l)&&g(y=>V(y,l))},W=async s=>{if(!(!t||!p)){f(s.id);try{let n={...d};s.saveBeforeRun&&(n={...n,...s.savePatch??{}},g(n),await o.mutateAsync({channel:t,data:n}));const l=await u.mutateAsync({actionId:s.id,data:{scope:p,draftConfig:Ae(p,n)}});Q(l.patch),l.ok?T.success(l.message||e("success")):T.error(l.message||e("error"))}catch(n){const l=n instanceof Error?n.message:String(n);T.error(`${e("error")}: ${l}`)}finally{f(null)}}};if(!t||!P||!k)return a.jsx("div",{className:we,children:a.jsxs("div",{children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:e("channelsSelectTitle")}),a.jsx("p",{className:"mt-2 text-sm text-gray-500",children:e("channelsSelectDescription")})]})});const M=!!k.enabled;return a.jsxs("div",{className:ve,children:[a.jsx("div",{className:"border-b border-gray-100 px-6 py-5",children:a.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[a.jsxs("div",{className:"min-w-0",children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx(K,{name:t,src:Y(t),className:I("h-9 w-9 rounded-lg border",M?"border-primary/30 bg-white":"border-gray-200/70 bg-white"),imgClassName:"h-5 w-5 object-contain",fallback:a.jsx("span",{className:"text-sm font-semibold uppercase text-gray-500",children:t[0]})}),a.jsx("h3",{className:"truncate text-lg font-semibold text-gray-900 capitalize",children:C})]}),a.jsx("p",{className:"mt-2 text-sm text-gray-500",children:e("channelsFormDescription")}),F&&a.jsxs("a",{href:F,className:"mt-2 inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover",children:[a.jsx(ee,{className:"h-3.5 w-3.5"}),e("channelsGuideTitle")]})]}),a.jsx(J,{status:M?"active":"inactive",label:M?e("statusActive"):e("statusInactive")})]})}),a.jsxs("form",{onSubmit:L,className:"flex min-h-0 flex-1 flex-col",children:[a.jsx("div",{className:"min-h-0 flex-1 space-y-6 overflow-y-auto overscroll-contain px-6 py-5",children:w.map(s=>{const n=t?_(`channels.${t}.${s.name}`,c):void 0,l=(n==null?void 0:n.label)??s.label,y=n==null?void 0:n.placeholder;return a.jsxs("div",{className:"space-y-2.5",children:[a.jsxs(he,{htmlFor:s.name,className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[De(s.name),l]}),s.type==="boolean"&&a.jsxs("div",{className:"flex items-center justify-between rounded-xl bg-gray-50 p-3",children:[a.jsx("span",{className:"text-sm text-gray-500",children:d[s.name]?e("enabled"):e("disabled")}),a.jsx(fe,{id:s.name,checked:d[s.name]||!1,onCheckedChange:b=>N(s.name,b),className:"data-[state=checked]:bg-emerald-500"})]}),(s.type==="text"||s.type==="email")&&a.jsx(D,{id:s.name,type:s.type,value:d[s.name]||"",onChange:b=>N(s.name,b.target.value),placeholder:y,className:"rounded-xl"}),s.type==="password"&&a.jsx(D,{id:s.name,type:"password",value:d[s.name]||"",onChange:b=>N(s.name,b.target.value),placeholder:y??e("leaveBlankToKeepUnchanged"),className:"rounded-xl"}),s.type==="number"&&a.jsx(D,{id:s.name,type:"number",value:d[s.name]||0,onChange:b=>N(s.name,parseInt(b.target.value,10)||0),placeholder:y,className:"rounded-xl"}),s.type==="tags"&&a.jsx(Pe,{value:d[s.name]||[],onChange:b=>N(s.name,b)}),s.type==="select"&&a.jsxs(be,{value:d[s.name]||"",onValueChange:b=>N(s.name,b),children:[a.jsx(ue,{className:"rounded-xl",children:a.jsx(xe,{})}),a.jsx(ye,{children:(s.options??[]).map(b=>a.jsx(ge,{value:b.value,children:b.label},b.value))})]}),s.type==="json"&&a.jsx("textarea",{id:s.name,value:x[s.name]??"{}",onChange:b=>j(X=>({...X,[s.name]:b.target.value})),className:"min-h-[120px] w-full resize-none rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"})]},s.name)})}),a.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4",children:[a.jsx("div",{className:"flex flex-wrap items-center gap-2",children:S.filter(s=>s.trigger==="manual").map(s=>a.jsx(E,{type:"button",onClick:()=>W(s),disabled:o.isPending||!!h,variant:"secondary",children:h===s.id?e("connecting"):s.title},s.id))}),a.jsx(E,{type:"submit",disabled:o.isPending||!!h,children:o.isPending?e("saving"):e("save")})]})]})]})}const Me={telegram:"channelDescTelegram",slack:"channelDescSlack",email:"channelDescEmail",webhook:"channelDescWebhook",discord:"channelDescDiscord",feishu:"channelDescFeishu"};function He(){const{data:t}=q(),{data:m}=$(),{data:i}=H(),[r,o]=v.useState("enabled"),[u,d]=v.useState(),[g,x]=v.useState(""),j=i==null?void 0:i.uiHints,h=m==null?void 0:m.channels,f=t==null?void 0:t.channels,k=[{id:"enabled",label:e("channelsTabEnabled"),count:(h??[]).filter(c=>{var p;return(p=f==null?void 0:f[c.name])==null?void 0:p.enabled}).length},{id:"all",label:e("channelsTabAll"),count:(h??[]).length}],w=v.useMemo(()=>{const c=g.trim().toLowerCase();return(h??[]).filter(p=>{var C;const S=((C=f==null?void 0:f[p.name])==null?void 0:C.enabled)||!1;return r==="enabled"?S:!0}).filter(p=>c?(p.displayName||p.name).toLowerCase().includes(c)||p.name.toLowerCase().includes(c):!0)},[r,f,h,g]);return v.useEffect(()=>{if(w.length===0){d(void 0);return}w.some(p=>p.name===u)||d(w[0].name)},[w,u]),!t||!m?a.jsx("div",{className:"p-8 text-gray-400",children:e("channelsLoading")}):a.jsxs(Ce,{className:"xl:flex xl:h-full xl:min-h-0 xl:flex-col xl:pb-0",children:[a.jsx(Ne,{title:e("channelsPageTitle"),description:e("channelsPageDescription")}),a.jsxs("div",{className:I(ke,"xl:min-h-0 xl:flex-1"),children:[a.jsxs("section",{className:je,children:[a.jsx("div",{className:"border-b border-gray-100 px-4 pt-4",children:a.jsx(Se,{tabs:k,activeTab:r,onChange:o,className:"mb-0"})}),a.jsx("div",{className:"border-b border-gray-100 px-4 py-3",children:a.jsxs("div",{className:"relative",children:[a.jsx(oe,{className:"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"}),a.jsx(D,{value:g,onChange:c=>x(c.target.value),placeholder:e("channelsFilterPlaceholder"),className:"h-10 rounded-xl pl-9"})]})}),a.jsxs("div",{className:"min-h-0 flex-1 space-y-2 overflow-y-auto overscroll-contain p-3",children:[w.map(c=>{const p=t.channels[c.name],S=(p==null?void 0:p.enabled)||!1,C=_(`channels.${c.name}`,j),P=z(c),F=(C==null?void 0:C.help)||e(Me[c.name]||"channelDescriptionDefault"),N=u===c.name;return a.jsx("button",{type:"button",onClick:()=>d(c.name),className:I("w-full rounded-xl border p-2.5 text-left transition-all",N?"border-primary/30 bg-primary-50/40 shadow-sm":"border-gray-200/70 bg-white hover:border-gray-300 hover:bg-gray-50/70"),children:a.jsxs("div",{className:"flex items-start justify-between gap-3",children:[a.jsxs("div",{className:"flex min-w-0 items-center gap-3",children:[a.jsx(K,{name:c.name,src:Y(c.name),className:I("h-10 w-10 rounded-lg border",S?"border-primary/30 bg-white":"border-gray-200/70 bg-white"),imgClassName:"h-5 w-5 object-contain",fallback:a.jsx("span",{className:"text-sm font-semibold uppercase text-gray-500",children:c.name[0]})}),a.jsxs("div",{className:"min-w-0",children:[a.jsx("p",{className:"truncate text-sm font-semibold text-gray-900",children:c.displayName||c.name}),a.jsx("p",{className:"line-clamp-1 text-[11px] text-gray-500",children:F})]})]}),a.jsxs("div",{className:"flex items-center gap-2",children:[P&&a.jsx("a",{href:P,onClick:L=>L.stopPropagation(),className:"inline-flex h-7 w-7 items-center justify-center rounded-md text-gray-300 transition-colors hover:bg-gray-100/70 hover:text-gray-500",title:e("channelsGuideTitle"),children:a.jsx(ce,{className:"h-3.5 w-3.5"})}),a.jsx(J,{status:S?"active":"inactive",label:S?e("statusActive"):e("statusInactive"),className:"min-w-[56px] justify-center"})]})]})},c.name)}),w.length===0&&a.jsxs("div",{className:"flex h-full min-h-[220px] flex-col items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50/70 py-10 text-center",children:[a.jsx("div",{className:"mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-white",children:a.jsx(ie,{className:"h-5 w-5 text-gray-300"})}),a.jsx("p",{className:"text-sm font-medium text-gray-700",children:e("channelsNoMatch")})]})]})]}),a.jsx(Le,{channelName:u})]})]})}export{He as ChannelsList};