@nextclaw/ui 0.12.20-beta.0 → 0.12.20-beta.2

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 (90) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/assets/api-BcqDx0tm.js +15 -0
  3. package/dist/assets/app-manager-provider-DVYBjif-.js +1 -0
  4. package/dist/assets/{app-navigation.config-BORqHkbN.js → app-navigation.config-BOVDFMnp.js} +1 -1
  5. package/dist/assets/{channels-list-page-sISO_4Yj.js → channels-list-page-CsoI4OJm.js} +2 -2
  6. package/dist/assets/{chat-ChCu7LQD.js → chat-ahMH_i_K.js} +6 -6
  7. package/dist/assets/chat-page-BxlXY-MB.js +1 -0
  8. package/dist/assets/chunk-JZWAC4HX-u4uYphxM.js +3 -0
  9. package/dist/assets/{desktop-update-config-BfJ5iSeY.js → desktop-update-config-CD6-2PfI.js} +1 -1
  10. package/dist/assets/{dialog-B-CXiFPZ.js → dialog-csshWetU.js} +1 -1
  11. package/dist/assets/{dist-DYVfg3q5.js → dist-Bl94Ahwx.js} +1 -1
  12. package/dist/assets/{es2015-BXroVnPi.js → es2015-JCM5-KtW.js} +1 -1
  13. package/dist/assets/index-CM-57d8J.js +2 -0
  14. package/dist/assets/index-D8MKmXtO.css +1 -0
  15. package/dist/assets/marketplace-page-DJGDpTAo.js +1 -0
  16. package/dist/assets/{marketplace-page-C9oZ01rM.js → marketplace-page-DxlxHCFm.js} +2 -2
  17. package/dist/assets/{mcp-marketplace-page-DuEixgSs.js → mcp-marketplace-page-5UjYRWOR.js} +2 -2
  18. package/dist/assets/mcp-marketplace-page-C1XaHZZO.js +1 -0
  19. package/dist/assets/{model-config-mfhqEZBG.js → model-config-PccJ9XyH.js} +1 -1
  20. package/dist/assets/{notice-card-CozHB03G.js → notice-card-CCgk6FvF.js} +1 -1
  21. package/dist/assets/{popover-CPUPma-w.js → popover-YAsxDBhY.js} +1 -1
  22. package/dist/assets/{provider-scoped-model-input-CL9sti2I.js → provider-scoped-model-input-CzpF7cug.js} +1 -1
  23. package/dist/assets/{providers-list-HPmL2akJ.js → providers-list-8qDMER8o.js} +1 -1
  24. package/dist/assets/remote-D4TtLPAp.js +1 -0
  25. package/dist/assets/runtime-config-page-DWJHrV7H.js +1 -0
  26. package/dist/assets/{search-config-Bcnk9VlL.js → search-config-D3a65l3r.js} +1 -1
  27. package/dist/assets/{secrets-config-Dde-5Y1w.js → secrets-config-CoMlR_7i.js} +2 -2
  28. package/dist/assets/{select-BELPuXLW.js → select-DIZrwsKU.js} +1 -1
  29. package/dist/assets/{sessions-config-page-CG49_0Z6.js → sessions-config-page-QjH5tgjr.js} +2 -2
  30. package/dist/assets/{setting-row-D5DtT6Ny.js → setting-row-DiQyrE81.js} +1 -1
  31. package/dist/assets/{tag-chip-D9BWWgYg.js → tag-chip-C3wDBe_-.js} +1 -1
  32. package/dist/assets/theme-provider-W704JWF8.js +1 -0
  33. package/dist/assets/{tooltip-CI0rpNee.js → tooltip-Dq5Xehpk.js} +1 -1
  34. package/dist/assets/use-config-BQJjq1mP.js +1 -0
  35. package/dist/assets/{use-confirm-dialog-hbynwWf2.js → use-confirm-dialog-DBoV5n5P.js} +1 -1
  36. package/dist/assets/{use-infinite-scroll-loader-Cw5qQr3-.js → use-infinite-scroll-loader-JAicqVC5.js} +1 -1
  37. package/dist/assets/{use-viewport-layout-CWHVDC6z.js → use-viewport-layout-BX3XqzJ4.js} +1 -1
  38. package/dist/index.html +17 -17
  39. package/package.json +8 -6
  40. package/src/app/hooks/use-realtime-query-bridge.ts +5 -5
  41. package/src/features/channels/components/config/channel-form.tsx +3 -3
  42. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -6
  43. package/src/features/chat/pages/ncp-chat-page.test.ts +22 -8
  44. package/src/features/chat/utils/chat-session-preference-governance.utils.test.tsx +114 -0
  45. package/src/features/chat/utils/chat-session-preference-governance.utils.ts +30 -36
  46. package/src/shared/components/common/brand-header.test.tsx +7 -2
  47. package/src/shared/components/common/brand-header.tsx +34 -4
  48. package/src/shared/lib/api/index.ts +12 -12
  49. package/src/shared/lib/api/ncp-session.test.ts +17 -18
  50. package/src/shared/lib/api/raw-client.utils.ts +3 -126
  51. package/src/shared/lib/api/services/agents.service.ts +18 -0
  52. package/src/shared/lib/api/services/channel-auth.service.ts +21 -0
  53. package/src/shared/lib/api/{client.ts → services/client.service.ts} +45 -1
  54. package/src/shared/lib/api/services/config.service.ts +171 -0
  55. package/src/shared/lib/api/services/marketplace.service.ts +66 -0
  56. package/src/shared/lib/api/services/mcp-marketplace.service.ts +70 -0
  57. package/src/shared/lib/api/services/ncp-attachments.service.ts +14 -0
  58. package/src/shared/lib/api/services/ncp-session.service.ts +39 -0
  59. package/src/shared/lib/api/services/remote.service.ts +50 -0
  60. package/src/shared/lib/api/services/runtime-control.service.ts +18 -0
  61. package/src/shared/lib/api/services/runtime-update.service.ts +26 -0
  62. package/src/shared/lib/api/services/server-path.service.ts +16 -0
  63. package/src/shared/lib/transport/index.ts +1 -0
  64. package/src/shared/lib/transport/local-transport.service.ts +24 -4
  65. package/src/shared/lib/transport/remote-transport.service.ts +1 -1
  66. package/src/shared/lib/transport/request-raw-api-response.utils.ts +133 -0
  67. package/src/shared/lib/transport/transport.types.ts +8 -2
  68. package/dist/assets/api-C412zuay.js +0 -15
  69. package/dist/assets/app-manager-provider-Cm-KiZZG.js +0 -1
  70. package/dist/assets/chat-page-BCaNZJGT.js +0 -1
  71. package/dist/assets/chunk-JZWAC4HX-DvbcIVPf.js +0 -3
  72. package/dist/assets/index-CUmk8xFK.css +0 -1
  73. package/dist/assets/index-CqPDhosM.js +0 -2
  74. package/dist/assets/marketplace-page-C8uaWkfd.js +0 -1
  75. package/dist/assets/mcp-marketplace-page-rNqr6ZpD.js +0 -1
  76. package/dist/assets/remote-oDlAdgVA.js +0 -1
  77. package/dist/assets/runtime-config-page-BCshTAAE.js +0 -1
  78. package/dist/assets/theme-provider-DeBrTglS.js +0 -1
  79. package/dist/assets/use-config-CrWZ_TSF.js +0 -1
  80. package/src/shared/lib/api/agents.ts +0 -34
  81. package/src/shared/lib/api/channel-auth.ts +0 -35
  82. package/src/shared/lib/api/config.ts +0 -362
  83. package/src/shared/lib/api/marketplace.ts +0 -156
  84. package/src/shared/lib/api/mcp-marketplace.ts +0 -138
  85. package/src/shared/lib/api/ncp-attachments.ts +0 -41
  86. package/src/shared/lib/api/ncp-session.ts +0 -78
  87. package/src/shared/lib/api/remote.ts +0 -86
  88. package/src/shared/lib/api/runtime-control.ts +0 -34
  89. package/src/shared/lib/api/runtime-update.service.ts +0 -50
  90. package/src/shared/lib/api/server-path.ts +0 -46
@@ -160,37 +160,14 @@ export function resolveRecentSessionPreferredValue<T>(params: {
160
160
  return bestValue;
161
161
  }
162
162
 
163
- export function resolveRecentSessionPreferredModel(params: {
164
- sessions: readonly SessionEntryView[];
165
- selectedSessionKey?: string | null;
166
- sessionType?: string | null;
167
- }): string | undefined {
168
- const { sessions, selectedSessionKey, sessionType } = params;
169
- return resolveRecentSessionPreferredValue<string>({
170
- sessions,
171
- selectedSessionKey,
172
- sessionType,
173
- readPreference: (session) => session.preferredModel?.trim() || undefined
174
- });
175
- }
176
-
177
- export function resolveRecentSessionPreferredThinking(params: {
178
- sessions: readonly SessionEntryView[];
179
- selectedSessionKey?: string | null;
180
- sessionType?: string | null;
181
- }): ThinkingLevel | undefined {
182
- const { sessions, selectedSessionKey, sessionType } = params;
183
- return resolveRecentSessionPreferredValue<ThinkingLevel>({
184
- sessions,
185
- selectedSessionKey,
186
- sessionType,
187
- readPreference: (session) => session.preferredThinking ?? undefined
188
- });
163
+ function buildSyncKey(parts: unknown[]): string {
164
+ return parts.map((part) => (part == null ? '' : String(part))).join('\u0002');
189
165
  }
190
166
 
191
167
  type UseSyncSessionPreferenceParams<T> = {
192
168
  isPreferenceAvailable: boolean;
193
169
  emptyValue: T;
170
+ syncKey: string;
194
171
  selectedSessionKey?: string | null;
195
172
  selectedSessionExists?: boolean;
196
173
  setValue: Dispatch<SetStateAction<T>>;
@@ -201,6 +178,7 @@ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>)
201
178
  const {
202
179
  isPreferenceAvailable,
203
180
  emptyValue,
181
+ syncKey,
204
182
  selectedSessionKey,
205
183
  selectedSessionExists = false,
206
184
  setValue,
@@ -208,27 +186,31 @@ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>)
208
186
  } = params;
209
187
  const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
210
188
  const resolveValueRef = useRef(resolveValue);
211
-
212
- useEffect(() => {
213
- resolveValueRef.current = resolveValue;
214
- }, [resolveValue]);
189
+ const lastSyncedValueRef = useRef<T>(emptyValue);
190
+ resolveValueRef.current = resolveValue;
215
191
 
216
192
  useEffect(() => {
217
193
  const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
218
194
  if (!isPreferenceAvailable) {
219
195
  setValue(emptyValue);
196
+ lastSyncedValueRef.current = emptyValue;
220
197
  previousSessionKeyRef.current = selectedSessionKey;
221
198
  return;
222
199
  }
223
- setValue((prev) =>
224
- resolveValueRef.current({
225
- currentValue: prev,
200
+ setValue((prev) => {
201
+ const next = resolveValueRef.current({
202
+ currentValue:
203
+ !sessionChanged && Object.is(prev, lastSyncedValueRef.current)
204
+ ? emptyValue
205
+ : prev,
226
206
  sessionChanged,
227
207
  preserveCurrentValueOnSessionChange: sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists
228
- })
229
- );
208
+ });
209
+ lastSyncedValueRef.current = next;
210
+ return next;
211
+ });
230
212
  previousSessionKeyRef.current = selectedSessionKey;
231
- }, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue]);
213
+ }, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue, syncKey]);
232
214
  }
233
215
 
234
216
  export function useSyncSelectedModel(params: {
@@ -244,6 +226,12 @@ export function useSyncSelectedModel(params: {
244
226
  useSyncSessionPreference<string>({
245
227
  isPreferenceAvailable: modelOptions.length > 0,
246
228
  emptyValue: '',
229
+ syncKey: buildSyncKey([
230
+ modelOptions.map((option) => option.value).join('\u0001'),
231
+ selectedSessionPreferredModel,
232
+ fallbackPreferredModel,
233
+ defaultModel
234
+ ]),
247
235
  selectedSessionKey,
248
236
  selectedSessionExists,
249
237
  setValue: setSelectedModel,
@@ -273,6 +261,12 @@ export function useSyncSelectedThinking(params: {
273
261
  useSyncSessionPreference<ThinkingLevel | null>({
274
262
  isPreferenceAvailable: supportedThinkingLevels.length > 0,
275
263
  emptyValue: null,
264
+ syncKey: buildSyncKey([
265
+ supportedThinkingLevels.join('\u0001'),
266
+ selectedSessionPreferredThinking,
267
+ fallbackPreferredThinking,
268
+ defaultThinkingLevel
269
+ ]),
276
270
  selectedSessionKey,
277
271
  selectedSessionExists,
278
272
  setValue: setSelectedThinkingLevel,
@@ -60,7 +60,8 @@ describe('BrandHeader', () => {
60
60
  mocks.downloadUpdate.mockReset();
61
61
  });
62
62
 
63
- it('shows update progress next to the product version', () => {
63
+ it('shows update progress next to the product version', async () => {
64
+ const user = userEvent.setup();
64
65
  useRuntimeUpdateStore.setState({
65
66
  supported: true,
66
67
  initialized: true,
@@ -96,7 +97,11 @@ describe('BrandHeader', () => {
96
97
 
97
98
  renderBrandHeader();
98
99
 
99
- expect(screen.getByText('v0.18.11')).toBeTruthy();
100
+ const version = screen.getByText('v0.18.11');
101
+ expect(version).toBeTruthy();
102
+ expect(screen.getAllByText('v0.18.11')).toHaveLength(1);
103
+ await user.hover(version);
104
+ expect(await screen.findAllByText('v0.18.11')).toHaveLength(2);
100
105
  expect(screen.getByText('下载 50%')).toBeTruthy();
101
106
  expect(screen.queryByRole('button', { name: '更新' })).toBeNull();
102
107
  });
@@ -1,7 +1,7 @@
1
1
  import type { UpdateSnapshot } from '@nextclaw/kernel';
2
2
  import { runtimeUpdateManager, useRuntimeUpdateStore } from '@/features/system-status';
3
3
  import { useAppMeta } from '@/shared/hooks/use-config';
4
- import type { ReactNode } from 'react';
4
+ import { type ReactNode, useState } from 'react';
5
5
  import { RuntimeStatusEntry } from '@/app/components/layout/runtime-status-entry';
6
6
  import { cn } from '@/shared/lib/utils';
7
7
  import { t } from '@/shared/lib/i18n';
@@ -15,6 +15,7 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
15
15
  const { data } = useAppMeta();
16
16
  const productName = data?.name ?? 'NextClaw';
17
17
  const productVersion = data?.productVersion?.trim();
18
+ const versionLabel = productVersion ? `v${productVersion}` : null;
18
19
  const resolvedSuffix = suffix ?? <RuntimeStatusEntry />;
19
20
 
20
21
  return (
@@ -22,9 +23,11 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
22
23
  <div className="h-7 w-7 rounded-lg overflow-hidden flex items-center justify-center">
23
24
  <img src="/logo.svg" alt={productName} className="h-full w-full object-contain" />
24
25
  </div>
25
- <div className="flex items-baseline gap-2 min-w-0">
26
- <span className="truncate text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{productName}</span>
27
- {productVersion ? <span className="text-[13px] font-medium text-gray-500">v{productVersion}</span> : null}
26
+ <div className="flex min-w-0 items-baseline gap-2">
27
+ <div className="flex min-w-0 flex-1 items-baseline gap-2">
28
+ <span className="shrink-0 text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{productName}</span>
29
+ {versionLabel ? <BrandVersionLabel versionLabel={versionLabel} /> : null}
30
+ </div>
28
31
  <RuntimeUpdateInlineStatus />
29
32
  {resolvedSuffix ? <span className="inline-flex items-center shrink-0">{resolvedSuffix}</span> : null}
30
33
  </div>
@@ -32,6 +35,33 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
32
35
  );
33
36
  }
34
37
 
38
+ function BrandVersionLabel({ versionLabel }: { versionLabel: string }) {
39
+ const [isTooltipOpen, setIsTooltipOpen] = useState(false);
40
+
41
+ return (
42
+ <span
43
+ className="relative min-w-0 flex-1"
44
+ onMouseEnter={() => setIsTooltipOpen(true)}
45
+ onMouseLeave={() => setIsTooltipOpen(false)}
46
+ onFocus={() => setIsTooltipOpen(true)}
47
+ onBlur={() => setIsTooltipOpen(false)}
48
+ >
49
+ <span
50
+ tabIndex={0}
51
+ aria-label={versionLabel}
52
+ className="block min-w-0 truncate text-[13px] font-medium text-gray-500 outline-none"
53
+ >
54
+ {versionLabel}
55
+ </span>
56
+ {isTooltipOpen ? (
57
+ <span className="pointer-events-none absolute left-0 top-full z-[var(--z-tooltip)] mt-1 w-max max-w-none whitespace-nowrap rounded-md border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground shadow-md">
58
+ {versionLabel}
59
+ </span>
60
+ ) : null}
61
+ </span>
62
+ );
63
+ }
64
+
35
65
  function RuntimeUpdateInlineStatus() {
36
66
  const { supported, busyAction, snapshot } = useRuntimeUpdateStore();
37
67
  if (!supported || !snapshot) {
@@ -1,22 +1,22 @@
1
- export * from './agents';
1
+ export * from './services/agents.service';
2
2
  export * from './api-base';
3
3
  export * from './auth.types';
4
- export * from './channel-auth';
4
+ export * from './services/channel-auth.service';
5
5
  export * from './channel-auth.types';
6
6
  export * from './chat-session-type.types';
7
- export * from './client';
8
- export * from './config';
9
- export * from './marketplace';
10
- export * from './mcp-marketplace';
11
- export * from './ncp-attachments';
12
- export * from './ncp-session';
7
+ export * from './services/client.service';
8
+ export * from './services/config.service';
9
+ export * from './services/marketplace.service';
10
+ export * from './services/mcp-marketplace.service';
11
+ export * from './services/ncp-attachments.service';
12
+ export * from './services/ncp-session.service';
13
13
  export * from './ncp-session.types';
14
14
  export * from './ncp-session-query-cache';
15
15
  export * from './raw-client.utils';
16
- export * from './remote';
16
+ export * from './services/remote.service';
17
17
  export * from './remote.types';
18
- export * from './runtime-control';
18
+ export * from './services/runtime-control.service';
19
19
  export * from './runtime-control.types';
20
- export * from './runtime-update.service';
21
- export * from './server-path';
20
+ export * from './services/runtime-update.service';
21
+ export * from './services/server-path.service';
22
22
  export * from './types';
@@ -1,37 +1,36 @@
1
- import { fetchNcpSessionSkills } from './ncp-session';
2
- import { api } from './client';
1
+ import { fetchNcpSessionSkills } from './services/ncp-session.service';
2
+ import { nextclawClient } from './services/client.service';
3
3
 
4
- vi.mock('./client', () => ({
5
- api: {
6
- get: vi.fn()
4
+ vi.mock('./services/client.service', () => ({
5
+ nextclawClient: {
6
+ sessions: {
7
+ listSkills: vi.fn()
8
+ }
7
9
  }
8
10
  }));
9
11
 
10
12
  describe('api/ncp-session', () => {
11
13
  beforeEach(() => {
12
- vi.mocked(api.get).mockReset();
13
- vi.mocked(api.get).mockResolvedValue({
14
- ok: true,
15
- data: {
16
- sessionId: 'session-1',
17
- total: 0,
18
- refs: [],
19
- records: []
20
- }
14
+ vi.mocked(nextclawClient.sessions.listSkills).mockReset();
15
+ vi.mocked(nextclawClient.sessions.listSkills).mockResolvedValue({
16
+ sessionId: 'session-1',
17
+ total: 0,
18
+ refs: [],
19
+ records: []
21
20
  });
22
21
  });
23
22
 
24
23
  it('does not send an empty projectRoot query when no override is provided', async () => {
25
24
  await fetchNcpSessionSkills('session-1', { projectRoot: null });
26
25
 
27
- expect(api.get).toHaveBeenCalledWith('/api/ncp/sessions/session-1/skills');
26
+ expect(nextclawClient.sessions.listSkills).toHaveBeenCalledWith('session-1', { projectRoot: null });
28
27
  });
29
28
 
30
29
  it('sends projectRoot only when the override is non-empty', async () => {
31
30
  await fetchNcpSessionSkills('session-1', { projectRoot: ' /tmp/project-alpha ' });
32
31
 
33
- expect(api.get).toHaveBeenCalledWith(
34
- '/api/ncp/sessions/session-1/skills?projectRoot=%2Ftmp%2Fproject-alpha'
35
- );
32
+ expect(nextclawClient.sessions.listSkills).toHaveBeenCalledWith('session-1', {
33
+ projectRoot: ' /tmp/project-alpha '
34
+ });
36
35
  });
37
36
  });
@@ -1,132 +1,9 @@
1
1
  import { API_BASE } from './api-base';
2
- import type { ApiResponse } from './types';
3
- import { systemStatusManager } from '@/features/system-status';
4
-
5
- function compactSnippet(text: string) {
6
- return text.replace(/\s+/g, ' ').trim().slice(0, 200);
7
- }
8
-
9
- function inferNonJsonHint(endpoint: string, status: number): string | undefined {
10
- if (
11
- status === 404 &&
12
- endpoint.startsWith('/api/config/providers/') &&
13
- endpoint.endsWith('/test')
14
- ) {
15
- return 'Provider test endpoint is missing. This usually means nextclaw runtime version is outdated.';
16
- }
17
- if (status === 401 || status === 403) {
18
- return 'Authentication failed. Check apiKey and custom headers.';
19
- }
20
- if (status === 429) {
21
- return 'Rate limited by upstream provider. Retry later or switch model/provider.';
22
- }
23
- if (status >= 500) {
24
- return 'Upstream service error. Retry later and inspect server logs if it persists.';
25
- }
26
- return undefined;
27
- }
28
-
29
- function formatUnknownFetchError(error: unknown): {
30
- summary: string;
31
- details: Record<string, unknown>;
32
- } {
33
- if (error instanceof Error) {
34
- const name = error.name?.trim() || 'Error';
35
- const message = error.message?.trim() || 'Unknown error';
36
- return {
37
- summary: `${name}: ${message}`,
38
- details: {
39
- errorName: name,
40
- errorMessage: message,
41
- ...(error.stack?.trim() ? { errorStack: error.stack.trim() } : {})
42
- }
43
- };
44
- }
45
- return {
46
- summary: String(error ?? 'Unknown error'),
47
- details: {
48
- errorName: 'NonError',
49
- errorMessage: String(error ?? 'Unknown error')
50
- }
51
- };
52
- }
2
+ import { requestRawApiResponse as requestRawTransportApiResponse } from '@/shared/lib/transport';
53
3
 
54
4
  export async function requestRawApiResponse<T>(
55
5
  endpoint: string,
56
6
  options: RequestInit = {}
57
- ): Promise<ApiResponse<T>> {
58
- const url = `${API_BASE}${endpoint}`;
59
- const method = (options.method || 'GET').toUpperCase();
60
-
61
- let response: Response;
62
- try {
63
- response = await fetch(url, {
64
- credentials: 'include',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- ...options.headers
68
- },
69
- ...options
70
- });
71
- } catch (error) {
72
- const formatted = formatUnknownFetchError(error);
73
- systemStatusManager.reportTransportFailure(formatted.summary);
74
- return {
75
- ok: false,
76
- error: {
77
- code: 'NETWORK_ERROR',
78
- message: `Fetch failed on ${method} ${endpoint} | ${formatted.summary}`,
79
- details: {
80
- method,
81
- endpoint,
82
- url,
83
- ...formatted.details
84
- }
85
- }
86
- };
87
- }
88
-
89
- const text = await response.text();
90
- let data: ApiResponse<T> | null = null;
91
- if (text) {
92
- try {
93
- data = JSON.parse(text) as ApiResponse<T>;
94
- } catch {
95
- // fall through to build a synthetic error response
96
- }
97
- }
98
-
99
- if (!data) {
100
- const snippet = text ? compactSnippet(text) : '';
101
- const hint = inferNonJsonHint(endpoint, response.status);
102
- const parts = [`Non-JSON response (${response.status} ${response.statusText}) on ${method} ${endpoint}`];
103
- if (snippet) {
104
- parts.push(`body=${snippet}`);
105
- }
106
- if (hint) {
107
- parts.push(`hint=${hint}`);
108
- }
109
- return {
110
- ok: false,
111
- error: {
112
- code: 'INVALID_RESPONSE',
113
- message: parts.join(' | '),
114
- details: {
115
- status: response.status,
116
- statusText: response.statusText,
117
- method,
118
- endpoint,
119
- url,
120
- bodySnippet: snippet || undefined,
121
- hint
122
- }
123
- }
124
- };
125
- }
126
-
127
- if (!response.ok) {
128
- return data as ApiResponse<T>;
129
- }
130
-
131
- return data as ApiResponse<T>;
7
+ ) {
8
+ return await requestRawTransportApiResponse<T>(API_BASE, endpoint, options);
132
9
  }
@@ -0,0 +1,18 @@
1
+ import { nextclawClient } from "./client.service";
2
+ import type { AgentCreateRequest, AgentDeleteResult, AgentProfileView, AgentUpdateRequest } from "@/shared/lib/api/types";
3
+
4
+ export async function fetchAgents(): Promise<{ agents: AgentProfileView[] }> {
5
+ return { agents: await nextclawClient.agents.list() };
6
+ }
7
+
8
+ export async function createAgent(data: AgentCreateRequest): Promise<AgentProfileView> {
9
+ return await nextclawClient.agents.create(data);
10
+ }
11
+
12
+ export async function updateAgent(agentId: string, data: AgentUpdateRequest): Promise<AgentProfileView> {
13
+ return await nextclawClient.agents.update(agentId, data);
14
+ }
15
+
16
+ export async function deleteAgent(agentId: string): Promise<AgentDeleteResult> {
17
+ return await nextclawClient.agents.delete(agentId);
18
+ }
@@ -0,0 +1,21 @@
1
+ import { nextclawClient } from './client.service';
2
+ import type {
3
+ ChannelAuthPollRequest,
4
+ ChannelAuthPollResult,
5
+ ChannelAuthStartRequest,
6
+ ChannelAuthStartResult
7
+ } from '@/shared/lib/api/channel-auth.types';
8
+
9
+ export async function startChannelAuth(
10
+ channel: string,
11
+ data: ChannelAuthStartRequest = {}
12
+ ): Promise<ChannelAuthStartResult> {
13
+ return await nextclawClient.channelAuth.start(channel, data);
14
+ }
15
+
16
+ export async function pollChannelAuth(
17
+ channel: string,
18
+ data: ChannelAuthPollRequest
19
+ ): Promise<ChannelAuthPollResult> {
20
+ return await nextclawClient.channelAuth.poll(channel, data);
21
+ }
@@ -1,5 +1,45 @@
1
+ import {
2
+ createNextClawClient,
3
+ type NextClawRealtimeEvent,
4
+ type NextClawTransport,
5
+ type NextClawTransportRequestInput,
6
+ type NextClawTransportUploadInput
7
+ } from '@nextclaw/client-sdk';
8
+ import { API_BASE } from '@/shared/lib/api/api-base';
1
9
  import { appClient } from '@/shared/lib/transport';
2
- import type { ApiResponse } from './types';
10
+ import type { ApiResponse } from '@/shared/lib/api/types';
11
+
12
+ const nextclawUiTransport: NextClawTransport = {
13
+ request: async <T>({ body, method, path, signal, timeoutMs }: NextClawTransportRequestInput): Promise<T> => {
14
+ return await appClient.request({
15
+ method,
16
+ path,
17
+ ...(body !== undefined ? { body } : {}),
18
+ ...(signal ? { signal } : {}),
19
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
20
+ });
21
+ },
22
+ upload: async <T>({ formData, headers, path, signal }: NextClawTransportUploadInput): Promise<T> => {
23
+ const response = await fetch(`${API_BASE}${path}`, {
24
+ method: 'POST',
25
+ body: formData,
26
+ credentials: 'include',
27
+ ...(headers ? { headers } : {}),
28
+ ...(signal ? { signal } : {})
29
+ });
30
+ const payload = (await response.json()) as ApiResponse<unknown>;
31
+ if (!response.ok || !payload.ok) {
32
+ throw new Error(readApiErrorMessage(payload, `Upload failed for ${path}`));
33
+ }
34
+ return payload.data as T;
35
+ },
36
+ subscribe: (handler: (event: NextClawRealtimeEvent) => void) => appClient.subscribe(handler)
37
+ };
38
+
39
+ export const nextclawClient = createNextClawClient({
40
+ baseUrl: API_BASE,
41
+ transport: nextclawUiTransport
42
+ });
3
43
 
4
44
  export async function requestApiResponse<T>(
5
45
  endpoint: string,
@@ -64,3 +104,7 @@ function parseRequestBody(body: BodyInit | null | undefined): unknown {
64
104
  }
65
105
  return body;
66
106
  }
107
+
108
+ function readApiErrorMessage(payload: ApiResponse<unknown>, fallback: string): string {
109
+ return !payload.ok ? payload.error.message : fallback;
110
+ }