@nextclaw/ui 0.7.0 → 0.9.0

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 (80) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/assets/ChannelsList-C7F_As4r.js +1 -0
  3. package/dist/assets/ChatPage-Oo7-OUsx.js +37 -0
  4. package/dist/assets/{DocBrowser-B9ws5JL7.js → DocBrowser-Dsd8Dlq8.js} +1 -1
  5. package/dist/assets/{LogoBadge-DvGAzkZ3.js → LogoBadge-2ChEc_oz.js} +1 -1
  6. package/dist/assets/MarketplacePage-BXck6-X3.js +49 -0
  7. package/dist/assets/{ModelConfig-BL_HsOsm.js → ModelConfig-CgHRSD0b.js} +1 -1
  8. package/dist/assets/ProvidersList-PPfZucvS.js +1 -0
  9. package/dist/assets/RuntimeConfig-ClLEKNTN.js +1 -0
  10. package/dist/assets/{SearchConfig-BhaI0fUf.js → SearchConfig-CuXVCbrf.js} +1 -1
  11. package/dist/assets/{SecretsConfig-CFoimOh9.js → SecretsConfig-udJz6Ake.js} +2 -2
  12. package/dist/assets/SessionsConfig-C1XnFfiC.js +2 -0
  13. package/dist/assets/{session-run-status-TkIuGbVw.js → chat-message-BETwXLD4.js} +3 -3
  14. package/dist/assets/{index-uMsNsQX6.js → index-COJdlL0e.js} +1 -1
  15. package/dist/assets/index-CsvP4CER.js +8 -0
  16. package/dist/assets/index-D-bXl7qL.css +1 -0
  17. package/dist/assets/{label-D8ly4a2P.js → label-BGL-ztxh.js} +1 -1
  18. package/dist/assets/{page-layout-BSYfvwbp.js → page-layout-aw88k7tG.js} +1 -1
  19. package/dist/assets/popover-DyEvzhmV.js +1 -0
  20. package/dist/assets/security-config-BuPAQn82.js +1 -0
  21. package/dist/assets/skeleton-drzO_tdU.js +1 -0
  22. package/dist/assets/{switch-Ce_g9lpN.js → switch-BK8jIzto.js} +1 -1
  23. package/dist/assets/{tabs-custom-Cf5azvT5.js → tabs-custom-Da3cEOji.js} +1 -1
  24. package/dist/assets/{useConfirmDialog-A8Ek8Wu7.js → useConfirmDialog-z0CE92iS.js} +2 -2
  25. package/dist/assets/{vendor-B7ozqnFC.js → vendor-CkJHmX1g.js} +65 -70
  26. package/dist/index.html +3 -3
  27. package/package.json +5 -2
  28. package/src/api/config.ts +9 -0
  29. package/src/api/ncp-session.ts +50 -0
  30. package/src/api/types.ts +20 -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/ChatPage.tsx +10 -324
  34. package/src/components/chat/ChatSidebar.test.tsx +203 -0
  35. package/src/components/chat/ChatSidebar.tsx +97 -7
  36. package/src/components/chat/adapters/chat-message.adapter.test.ts +132 -81
  37. package/src/components/chat/adapters/chat-message.adapter.ts +27 -9
  38. package/src/components/chat/chat-chain.test.ts +22 -0
  39. package/src/components/chat/chat-chain.ts +23 -0
  40. package/src/components/chat/chat-page-data.ts +30 -1
  41. package/src/components/chat/chat-page-runtime.test.ts +181 -0
  42. package/src/components/chat/chat-page-runtime.ts +101 -15
  43. package/src/components/chat/chat-page-shell.tsx +103 -0
  44. package/src/components/chat/chat-session-preference-sync.test.ts +62 -0
  45. package/src/components/chat/chat-session-preference-sync.ts +75 -0
  46. package/src/components/chat/containers/chat-input-bar.container.tsx +0 -22
  47. package/src/components/chat/containers/chat-message-list.container.tsx +34 -26
  48. package/src/components/chat/legacy/LegacyChatPage.tsx +252 -0
  49. package/src/components/chat/managers/chat-input.manager.ts +5 -0
  50. package/src/components/chat/managers/chat-session-list.manager.test.ts +39 -0
  51. package/src/components/chat/managers/chat-session-list.manager.ts +9 -3
  52. package/src/components/chat/ncp/NcpChatPage.tsx +381 -0
  53. package/src/components/chat/ncp/ncp-chat-input.manager.ts +179 -0
  54. package/src/components/chat/ncp/ncp-chat-page-data.ts +166 -0
  55. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
  56. package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
  57. package/src/components/chat/ncp/ncp-session-adapter.test.ts +75 -0
  58. package/src/components/chat/ncp/ncp-session-adapter.ts +214 -0
  59. package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
  60. package/src/components/chat/stores/chat-thread.store.ts +2 -0
  61. package/src/components/chat/useChatSessionTypeState.test.tsx +58 -0
  62. package/src/components/chat/useChatSessionTypeState.ts +25 -8
  63. package/src/hooks/use-ncp-chat-session-types.ts +11 -0
  64. package/src/hooks/useConfig.ts +41 -1
  65. package/src/hooks/useMarketplace.ts +7 -4
  66. package/src/hooks/useWebSocket.ts +23 -2
  67. package/src/lib/i18n.ts +1 -1
  68. package/tailwind.config.js +8 -3
  69. package/tsconfig.json +4 -1
  70. package/dist/assets/ChannelsList-DF2U-LY1.js +0 -1
  71. package/dist/assets/ChatPage-BX39y0U5.js +0 -36
  72. package/dist/assets/MarketplacePage-DG5mHWJ8.js +0 -49
  73. package/dist/assets/ProvidersList-CH5z00YT.js +0 -1
  74. package/dist/assets/RuntimeConfig-BplBgkwo.js +0 -1
  75. package/dist/assets/SessionsConfig-BHTAYn9T.js +0 -2
  76. package/dist/assets/index-BLeJkJ0o.css +0 -1
  77. package/dist/assets/index-DK4TS5ev.js +0 -8
  78. package/dist/assets/index-X5J6Mm--.js +0 -1
  79. package/dist/assets/security-config-DlKEYHNN.js +0 -1
  80. package/dist/assets/skeleton-CWbsNx2h.js +0 -1
@@ -0,0 +1,166 @@
1
+ import { useMemo } from 'react';
2
+ import type { Dispatch, SetStateAction } from 'react';
3
+ import type { SessionEntryView } from '@/api/types';
4
+ import type { ChatModelOption } from '@/components/chat/chat-input.types';
5
+ import {
6
+ adaptNcpSessionSummaries,
7
+ readNcpSessionPreferredThinking
8
+ } from '@/components/chat/ncp/ncp-session-adapter';
9
+ import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
10
+ import {
11
+ resolveSelectedModelValue,
12
+ resolveRecentSessionPreferredModel,
13
+ useSyncSelectedModel
14
+ } from '@/components/chat/chat-page-runtime';
15
+ import {
16
+ useConfig,
17
+ useConfigMeta,
18
+ useNcpSessions
19
+ } from '@/hooks/useConfig';
20
+ import { useNcpChatSessionTypes } from '@/hooks/use-ncp-chat-session-types';
21
+ import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
22
+ import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCapability } from '@/lib/provider-models';
23
+
24
+ type UseNcpChatPageDataParams = {
25
+ query: string;
26
+ selectedSessionKey: string | null;
27
+ pendingSessionType: string;
28
+ setPendingSessionType: Dispatch<SetStateAction<string>>;
29
+ setSelectedModel: Dispatch<SetStateAction<string>>;
30
+ };
31
+
32
+ function filterSessionsByQuery(sessions: SessionEntryView[], query: string): SessionEntryView[] {
33
+ const normalizedQuery = query.trim().toLowerCase();
34
+ if (!normalizedQuery) {
35
+ return sessions;
36
+ }
37
+ return sessions.filter((session) => session.key.toLowerCase().includes(normalizedQuery));
38
+ }
39
+
40
+ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
41
+ const configQuery = useConfig();
42
+ const configMetaQuery = useConfigMeta();
43
+ const sessionsQuery = useNcpSessions({ limit: 200 });
44
+ const sessionTypesQuery = useNcpChatSessionTypes();
45
+ const installedSkillsQuery = useMarketplaceInstalled('skill');
46
+ const isProviderStateResolved =
47
+ (configQuery.isFetched || configQuery.isSuccess) &&
48
+ (configMetaQuery.isFetched || configMetaQuery.isSuccess);
49
+
50
+ const modelOptions = useMemo<ChatModelOption[]>(() => {
51
+ const providers = buildProviderModelCatalog({
52
+ meta: configMetaQuery.data,
53
+ config: configQuery.data,
54
+ onlyConfigured: true
55
+ });
56
+ const seen = new Set<string>();
57
+ const options: ChatModelOption[] = [];
58
+ for (const provider of providers) {
59
+ for (const localModel of provider.models) {
60
+ const value = composeProviderModel(provider.prefix, localModel);
61
+ if (!value || seen.has(value)) {
62
+ continue;
63
+ }
64
+ seen.add(value);
65
+ options.push({
66
+ value,
67
+ modelLabel: localModel,
68
+ providerLabel: provider.displayName,
69
+ thinkingCapability: resolveModelThinkingCapability(provider.modelThinking, localModel, provider.aliases)
70
+ });
71
+ }
72
+ }
73
+ return options.sort((left, right) => {
74
+ const providerCompare = left.providerLabel.localeCompare(right.providerLabel);
75
+ if (providerCompare !== 0) {
76
+ return providerCompare;
77
+ }
78
+ return left.modelLabel.localeCompare(right.modelLabel);
79
+ });
80
+ }, [configMetaQuery.data, configQuery.data]);
81
+
82
+ const sessionSummaries = useMemo(
83
+ () => sessionsQuery.data?.sessions ?? [],
84
+ [sessionsQuery.data?.sessions]
85
+ );
86
+ const allSessions = useMemo(
87
+ () => adaptNcpSessionSummaries(sessionSummaries),
88
+ [sessionSummaries]
89
+ );
90
+ const sessions = useMemo(
91
+ () => filterSessionsByQuery(allSessions, params.query),
92
+ [allSessions, params.query]
93
+ );
94
+ const selectedSession = useMemo(
95
+ () => allSessions.find((session) => session.key === params.selectedSessionKey) ?? null,
96
+ [allSessions, params.selectedSessionKey]
97
+ );
98
+ const selectedSessionSummary = useMemo(
99
+ () => sessionSummaries.find((session) => session.sessionId === params.selectedSessionKey) ?? null,
100
+ [params.selectedSessionKey, sessionSummaries]
101
+ );
102
+ const skillRecords = useMemo(
103
+ () => installedSkillsQuery.data?.records ?? [],
104
+ [installedSkillsQuery.data?.records]
105
+ );
106
+ const selectedSessionThinkingLevel = useMemo(
107
+ () => (selectedSessionSummary ? readNcpSessionPreferredThinking(selectedSessionSummary) : null),
108
+ [selectedSessionSummary]
109
+ );
110
+
111
+ const sessionTypeState = useChatSessionTypeState({
112
+ selectedSession,
113
+ selectedSessionKey: params.selectedSessionKey,
114
+ pendingSessionType: params.pendingSessionType,
115
+ setPendingSessionType: params.setPendingSessionType,
116
+ sessionTypesData: sessionTypesQuery.data
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
+ );
127
+
128
+ useSyncSelectedModel({
129
+ modelOptions,
130
+ selectedSessionKey: params.selectedSessionKey,
131
+ selectedSessionPreferredModel: selectedSession?.preferredModel,
132
+ fallbackPreferredModel: recentSessionPreferredModel,
133
+ defaultModel: configQuery.data?.agents.defaults.model,
134
+ setSelectedModel: params.setSelectedModel
135
+ });
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
+
150
+ return {
151
+ configQuery,
152
+ configMetaQuery,
153
+ sessionsQuery,
154
+ sessionTypesQuery,
155
+ installedSkillsQuery,
156
+ isProviderStateResolved,
157
+ modelOptions,
158
+ sessionSummaries,
159
+ sessions,
160
+ skillRecords,
161
+ selectedSession,
162
+ hydratedSessionModel,
163
+ selectedSessionThinkingLevel,
164
+ ...sessionTypeState
165
+ };
166
+ }
@@ -0,0 +1,89 @@
1
+ import { deleteNcpSession as deleteNcpSessionApi } from '@/api/ncp-session';
2
+ import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
3
+ import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
4
+ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
5
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
6
+ import type { ChatThreadSnapshot } from '@/components/chat/stores/chat-thread.store';
7
+ import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
8
+ import { t } from '@/lib/i18n';
9
+
10
+ export type NcpChatThreadManagerActions = {
11
+ refetchSessions: () => Promise<unknown>;
12
+ };
13
+
14
+ const noopAsync = async () => {};
15
+
16
+ export class NcpChatThreadManager {
17
+ private actions: NcpChatThreadManagerActions = {
18
+ refetchSessions: noopAsync
19
+ };
20
+
21
+ constructor(
22
+ private uiManager: ChatUiManager,
23
+ private sessionListManager: ChatSessionListManager,
24
+ private streamActionsManager: ChatStreamActionsManager
25
+ ) {}
26
+
27
+ bindActions = (patch: Partial<NcpChatThreadManagerActions>) => {
28
+ this.actions = {
29
+ ...this.actions,
30
+ ...patch
31
+ };
32
+ };
33
+
34
+ private hasSnapshotChanges = (patch: Partial<ChatThreadSnapshot>): boolean => {
35
+ const current = useChatThreadStore.getState().snapshot;
36
+ for (const [key, value] of Object.entries(patch) as Array<[keyof ChatThreadSnapshot, ChatThreadSnapshot[keyof ChatThreadSnapshot]]>) {
37
+ if (!Object.is(current[key], value)) {
38
+ return true;
39
+ }
40
+ }
41
+ return false;
42
+ };
43
+
44
+ syncSnapshot = (patch: Partial<ChatThreadSnapshot>) => {
45
+ if (!this.hasSnapshotChanges(patch)) {
46
+ return;
47
+ }
48
+ useChatThreadStore.getState().setSnapshot(patch);
49
+ };
50
+
51
+ deleteSession = () => {
52
+ void this.deleteCurrentSession();
53
+ };
54
+
55
+ createSession = () => {
56
+ this.sessionListManager.createSession();
57
+ };
58
+
59
+ goToProviders = () => {
60
+ this.uiManager.goToProviders();
61
+ };
62
+
63
+ private deleteCurrentSession = async () => {
64
+ const {
65
+ snapshot: { selectedSessionKey }
66
+ } = useChatSessionListStore.getState();
67
+ if (!selectedSessionKey) {
68
+ return;
69
+ }
70
+ const confirmed = await this.uiManager.confirm({
71
+ title: t('chatDeleteSessionConfirm'),
72
+ variant: 'destructive',
73
+ confirmLabel: t('delete')
74
+ });
75
+ if (!confirmed) {
76
+ return;
77
+ }
78
+ useChatThreadStore.getState().setSnapshot({ isDeletePending: true });
79
+ try {
80
+ await deleteNcpSessionApi(selectedSessionKey);
81
+ this.streamActionsManager.resetStreamState();
82
+ useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: null });
83
+ this.uiManager.goToChatRoot({ replace: true });
84
+ await this.actions.refetchSessions();
85
+ } finally {
86
+ useChatThreadStore.getState().setSnapshot({ isDeletePending: false });
87
+ }
88
+ };
89
+ }
@@ -0,0 +1,33 @@
1
+ import { ChatRunStatusManager } from '@/components/chat/managers/chat-run-status.manager';
2
+ import { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
3
+ import { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
4
+ import { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
5
+ import { NcpChatInputManager } from '@/components/chat/ncp/ncp-chat-input.manager';
6
+ import { NcpChatThreadManager } from '@/components/chat/ncp/ncp-chat-thread.manager';
7
+
8
+ export class NcpChatPresenter {
9
+ chatUiManager = new ChatUiManager();
10
+ chatStreamActionsManager = new ChatStreamActionsManager();
11
+ chatSessionListManager = new ChatSessionListManager(this.chatUiManager, this.chatStreamActionsManager);
12
+ chatInputManager = new NcpChatInputManager(
13
+ this.chatUiManager,
14
+ this.chatStreamActionsManager,
15
+ () => this.getDraftSessionId()
16
+ );
17
+ chatRunStatusManager = new ChatRunStatusManager();
18
+ chatThreadManager = new NcpChatThreadManager(
19
+ this.chatUiManager,
20
+ this.chatSessionListManager,
21
+ this.chatStreamActionsManager
22
+ );
23
+
24
+ private draftSessionId = '';
25
+
26
+ setDraftSessionId = (sessionId: string) => {
27
+ this.draftSessionId = sessionId;
28
+ };
29
+
30
+ private getDraftSessionId(): string {
31
+ return this.draftSessionId;
32
+ }
33
+ }
@@ -0,0 +1,75 @@
1
+ import {
2
+ adaptNcpSessionSummary,
3
+ buildNcpSessionRunStatusByKey,
4
+ readNcpSessionPreferredThinking
5
+ } from '@/components/chat/ncp/ncp-session-adapter';
6
+ import type { NcpSessionSummaryView } from '@/api/types';
7
+
8
+ function createSummary(partial: Partial<NcpSessionSummaryView> = {}): NcpSessionSummaryView {
9
+ return {
10
+ sessionId: 'ncp-session-1',
11
+ messageCount: 3,
12
+ updatedAt: '2026-03-18T00:00:00.000Z',
13
+ status: 'idle',
14
+ ...partial
15
+ };
16
+ }
17
+
18
+ describe('adaptNcpSessionSummary', () => {
19
+ it('maps session metadata into shared session entry fields', () => {
20
+ const adapted = adaptNcpSessionSummary(
21
+ createSummary({
22
+ metadata: {
23
+ label: 'NCP Planning Thread',
24
+ model: 'openai/gpt-5',
25
+ session_type: 'native'
26
+ }
27
+ })
28
+ );
29
+
30
+ expect(adapted).toMatchObject({
31
+ key: 'ncp-session-1',
32
+ label: 'NCP Planning Thread',
33
+ preferredModel: 'openai/gpt-5',
34
+ sessionType: 'native',
35
+ sessionTypeMutable: false,
36
+ messageCount: 3
37
+ });
38
+ });
39
+ });
40
+
41
+ describe('readNcpSessionPreferredThinking', () => {
42
+ it('normalizes persisted thinking metadata for UI hydration', () => {
43
+ const thinking = readNcpSessionPreferredThinking(
44
+ createSummary({
45
+ metadata: {
46
+ preferred_thinking: 'HIGH'
47
+ }
48
+ })
49
+ );
50
+
51
+ expect(thinking).toBe('high');
52
+ });
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
+ });
@@ -0,0 +1,214 @@
1
+ import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
2
+ import type { NcpMessagePart } from '@nextclaw/ncp';
3
+ import type { NcpMessageView, NcpSessionSummaryView, SessionEntryView, ThinkingLevel } from '@/api/types';
4
+ import type { SessionRunStatus } from '@/lib/session-run-status';
5
+
6
+ const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
7
+
8
+ function stringifyUnknown(value: unknown): string {
9
+ if (typeof value === 'string') {
10
+ return value;
11
+ }
12
+ try {
13
+ return JSON.stringify(value ?? {});
14
+ } catch {
15
+ return String(value ?? '');
16
+ }
17
+ }
18
+
19
+ function readOptionalString(value: unknown): string | null {
20
+ if (typeof value !== 'string') {
21
+ return null;
22
+ }
23
+ const trimmed = value.trim();
24
+ return trimmed.length > 0 ? trimmed : null;
25
+ }
26
+
27
+ function readMetadata(summary: NcpSessionSummaryView): Record<string, unknown> | null {
28
+ const { metadata } = summary;
29
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
30
+ return null;
31
+ }
32
+ return metadata as Record<string, unknown>;
33
+ }
34
+
35
+ export function readNcpSessionPreferredModel(summary: NcpSessionSummaryView): string | null {
36
+ const metadata = readMetadata(summary);
37
+ if (!metadata) {
38
+ return null;
39
+ }
40
+ return (
41
+ readOptionalString(metadata.preferred_model) ??
42
+ readOptionalString(metadata.preferredModel) ??
43
+ readOptionalString(metadata.model)
44
+ );
45
+ }
46
+
47
+ export function readNcpSessionPreferredThinking(summary: NcpSessionSummaryView): ThinkingLevel | null {
48
+ const metadata = readMetadata(summary);
49
+ if (!metadata) {
50
+ return null;
51
+ }
52
+ const rawValue =
53
+ readOptionalString(metadata.preferred_thinking) ??
54
+ readOptionalString(metadata.thinking) ??
55
+ readOptionalString(metadata.thinking_level) ??
56
+ readOptionalString(metadata.thinkingLevel);
57
+ if (!rawValue) {
58
+ return null;
59
+ }
60
+ const normalized = rawValue.toLowerCase();
61
+ return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
62
+ }
63
+
64
+ function readNcpSessionLabel(summary: NcpSessionSummaryView): string | null {
65
+ const metadata = readMetadata(summary);
66
+ if (!metadata) {
67
+ return null;
68
+ }
69
+ return readOptionalString(metadata.label);
70
+ }
71
+
72
+ function readNcpSessionType(summary: NcpSessionSummaryView): string {
73
+ const metadata = readMetadata(summary);
74
+ if (!metadata) {
75
+ return 'native';
76
+ }
77
+ return readOptionalString(metadata.session_type) ?? readOptionalString(metadata.sessionType) ?? 'native';
78
+ }
79
+
80
+ function mapToolStatus(part: Extract<NcpMessagePart, { type: 'tool-invocation' }>): ToolInvocationStatus {
81
+ if (part.state === 'result') {
82
+ return ToolInvocationStatus.RESULT;
83
+ }
84
+ if (part.state === 'partial-call') {
85
+ return ToolInvocationStatus.PARTIAL_CALL;
86
+ }
87
+ return ToolInvocationStatus.CALL;
88
+ }
89
+
90
+ function toUiParts(parts: NcpMessagePart[]): UIMessage['parts'] {
91
+ const uiParts: UIMessage['parts'] = [];
92
+ for (const part of parts) {
93
+ if (part.type === 'text') {
94
+ uiParts.push({ type: 'text', text: part.text });
95
+ continue;
96
+ }
97
+ if (part.type === 'rich-text') {
98
+ uiParts.push({ type: 'text', text: part.text });
99
+ continue;
100
+ }
101
+ if (part.type === 'reasoning') {
102
+ uiParts.push({
103
+ type: 'reasoning',
104
+ reasoning: part.text,
105
+ details: []
106
+ });
107
+ continue;
108
+ }
109
+ if (part.type === 'source') {
110
+ uiParts.push({
111
+ type: 'source',
112
+ source: {
113
+ sourceType: 'url',
114
+ id: part.url ?? part.title ?? Math.random().toString(36).slice(2, 8),
115
+ url: part.url ?? '',
116
+ ...(part.title ? { title: part.title } : {})
117
+ }
118
+ });
119
+ continue;
120
+ }
121
+ if (part.type === 'file' && part.contentBase64) {
122
+ uiParts.push({
123
+ type: 'file',
124
+ mimeType: part.mimeType ?? 'application/octet-stream',
125
+ data: part.contentBase64
126
+ });
127
+ continue;
128
+ }
129
+ if (part.type === 'step-start') {
130
+ uiParts.push({ type: 'step-start' });
131
+ continue;
132
+ }
133
+ if (part.type === 'tool-invocation') {
134
+ uiParts.push({
135
+ type: 'tool-invocation',
136
+ toolInvocation: {
137
+ status: mapToolStatus(part),
138
+ toolCallId: part.toolCallId ?? `${part.toolName}-${Math.random().toString(36).slice(2, 8)}`,
139
+ toolName: part.toolName,
140
+ args: stringifyUnknown(part.args),
141
+ result: part.result
142
+ }
143
+ });
144
+ }
145
+ }
146
+ return uiParts;
147
+ }
148
+
149
+ function normalizeRole(role: NcpMessageView['role']): UIMessage['role'] {
150
+ if (role === 'service') {
151
+ return 'system';
152
+ }
153
+ return role === 'tool' ? 'assistant' : role;
154
+ }
155
+
156
+ export function adaptNcpMessageToUiMessage(message: NcpMessageView): UIMessage {
157
+ return {
158
+ id: message.id,
159
+ role: normalizeRole(message.role),
160
+ parts: toUiParts(message.parts),
161
+ meta: {
162
+ source: 'stream',
163
+ status: message.status,
164
+ sessionKey: message.sessionId,
165
+ timestamp: message.timestamp
166
+ }
167
+ };
168
+ }
169
+
170
+ export function adaptNcpMessagesToUiMessages(messages: readonly NcpMessageView[]): UIMessage[] {
171
+ console.log('[adaptNcpMessagesToUiMessages]', { messages });
172
+ return messages.map(adaptNcpMessageToUiMessage);
173
+ }
174
+
175
+ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionEntryView {
176
+ const label = readNcpSessionLabel(summary);
177
+ const preferredModel = readNcpSessionPreferredModel(summary);
178
+ return {
179
+ key: summary.sessionId,
180
+ createdAt: summary.updatedAt,
181
+ updatedAt: summary.updatedAt,
182
+ ...(label ? { label } : {}),
183
+ ...(preferredModel ? { preferredModel } : {}),
184
+ sessionType: readNcpSessionType(summary),
185
+ sessionTypeMutable: false,
186
+ messageCount: summary.messageCount
187
+ };
188
+ }
189
+
190
+ export function adaptNcpSessionSummaries(summaries: NcpSessionSummaryView[]): SessionEntryView[] {
191
+ return summaries.map(adaptNcpSessionSummary);
192
+ }
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
+
212
+ export function createNcpSessionId(): string {
213
+ return `ncp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
214
+ }
@@ -1,11 +1,50 @@
1
1
  import { createContext, useContext } from 'react';
2
2
  import type { ReactNode } from 'react';
3
- import type { ChatPresenter } from '@/components/chat/presenter/chat.presenter';
3
+ import type { SetStateAction } from 'react';
4
+ import type { ChatRunStatusManager } from '@/components/chat/managers/chat-run-status.manager';
5
+ import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
6
+ import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
7
+ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
8
+ import type { ChatThreadSnapshot } from '@/components/chat/stores/chat-thread.store';
9
+ import type { ThinkingLevel } from '@/api/types';
4
10
 
5
- const ChatPresenterContext = createContext<ChatPresenter | null>(null);
11
+ export type ChatInputManagerLike = {
12
+ syncSnapshot: (patch: Record<string, unknown>) => void;
13
+ setDraft: (next: SetStateAction<string>) => void;
14
+ setPendingSessionType: (next: SetStateAction<string>) => void;
15
+ send: () => Promise<void>;
16
+ stop: () => Promise<void>;
17
+ goToProviders: () => void;
18
+ setSelectedModel: (next: SetStateAction<string>) => void;
19
+ setSelectedThinkingLevel: (next: SetStateAction<ThinkingLevel | null>) => void;
20
+ setSelectedSkills: (next: SetStateAction<string[]>) => void;
21
+ selectSessionType: (value: string) => void;
22
+ selectModel: (value: string) => void;
23
+ selectThinkingLevel: (value: ThinkingLevel) => void;
24
+ selectSkills: (next: string[]) => void;
25
+ };
26
+
27
+ export type ChatThreadManagerLike = {
28
+ bindActions: (patch: { refetchSessions?: () => Promise<unknown> }) => void;
29
+ syncSnapshot: (patch: Partial<ChatThreadSnapshot>) => void;
30
+ deleteSession: () => void;
31
+ createSession: () => void;
32
+ goToProviders: () => void;
33
+ };
34
+
35
+ export type ChatPresenterLike = {
36
+ chatUiManager: ChatUiManager;
37
+ chatStreamActionsManager: ChatStreamActionsManager;
38
+ chatInputManager: ChatInputManagerLike;
39
+ chatSessionListManager: ChatSessionListManager;
40
+ chatRunStatusManager: ChatRunStatusManager;
41
+ chatThreadManager: ChatThreadManagerLike;
42
+ };
43
+
44
+ const ChatPresenterContext = createContext<ChatPresenterLike | null>(null);
6
45
 
7
46
  type ChatPresenterProviderProps = {
8
- presenter: ChatPresenter;
47
+ presenter: ChatPresenterLike;
9
48
  children: ReactNode;
10
49
  };
11
50
 
@@ -13,7 +52,7 @@ export function ChatPresenterProvider({ presenter, children }: ChatPresenterProv
13
52
  return <ChatPresenterContext.Provider value={presenter}>{children}</ChatPresenterContext.Provider>;
14
53
  }
15
54
 
16
- export function usePresenter(): ChatPresenter {
55
+ export function usePresenter(): ChatPresenterLike {
17
56
  const presenter = useContext(ChatPresenterContext);
18
57
  if (!presenter) {
19
58
  throw new Error('usePresenter must be used inside ChatPresenterProvider');
@@ -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,