@nextclaw/ui 0.15.26 → 0.15.27
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.
- package/CHANGELOG.md +16 -0
- package/dist/assets/{api-CK3Fro7z.js → api-a_GaOFc7.js} +4 -4
- package/dist/assets/{appearance-settings-page-zqwWO8Dy.js → appearance-settings-page-DlGXLgff.js} +1 -1
- package/dist/assets/{channels-list-page-CVCZBlCL.js → channels-list-page-BEEk4Uc9.js} +2 -2
- package/dist/assets/{chat-page-D4wehRTz.js → chat-page-DUQXXqQm.js} +1 -1
- package/dist/assets/{config-split-page-CceNFvlY.js → config-split-page-ps1Aod3g.js} +1 -1
- package/dist/assets/{confirm-dialog-CWeNYWZY.js → confirm-dialog-DJSUNG33.js} +2 -2
- package/dist/assets/desktop-update-config--zagjHic.js +1 -0
- package/dist/assets/{dist-D9xtGgdL.js → dist-BkTYNzm_.js} +1 -1
- package/dist/assets/{dist-AYxkFx2B.js → dist-DkR7_Dpl.js} +1 -1
- package/dist/assets/doc-browser-DahzJEWj.js +1 -0
- package/dist/assets/doc-browser-Q67TOGVS.js +1 -0
- package/dist/assets/{doc-browser-context-Cb7Z0j90.js → doc-browser-context-BQim8SqD.js} +1 -1
- package/dist/assets/form-actions-B2-bpFie.js +1 -0
- package/dist/assets/index-Ba_o5fu9.js +109 -0
- package/dist/assets/index-DSLK7zgR.css +1 -0
- package/dist/assets/loader-circle-DyXZU92l.js +1 -0
- package/dist/assets/mcp-marketplace-page-B0dBE5CV.js +1 -0
- package/dist/assets/mcp-marketplace-page-w8panjZG.js +9 -0
- package/dist/assets/{model-config-page-DfOLyQqR.js → model-config-page-rrhFaque.js} +1 -1
- package/dist/assets/{navigation-link-MiiyoIjH.js → navigation-link-mpHFXwRr.js} +2 -2
- package/dist/assets/plus-Dhb4KuMH.js +1 -0
- package/dist/assets/{provider-scoped-model-input-DXzjr5l0.js → provider-scoped-model-input-CFsfWEt9.js} +1 -1
- package/dist/assets/{providers-config-page-CaO8WfZH.js → providers-config-page-D-TYFZsF.js} +1 -1
- package/dist/assets/react-BlXwvOLo.js +8 -0
- package/dist/assets/{refresh-cw-a2VVeeAF.js → refresh-cw-JFuyDj_P.js} +1 -1
- package/dist/assets/remote-BwVv_Qq-.js +1 -0
- package/dist/assets/rotate-cw-DjbbiIW_.js +1 -0
- package/dist/assets/{runtime-config-page-BuA4A02z.js → runtime-config-page-Br9j9Vxe.js} +1 -1
- package/dist/assets/{save-CGm23Yww.js → save-BL8P8CLX.js} +1 -1
- package/dist/assets/{search-CH2t--sH.js → search-bZL75Pgc.js} +1 -1
- package/dist/assets/{search-config-page-BAmYqoAz.js → search-config-page-B3fvzMd1.js} +1 -1
- package/dist/assets/{secrets-config-page-BlPOuql_.js → secrets-config-page-ClwTxtXd.js} +2 -2
- package/dist/assets/security-config-DmJw9QZA.js +1 -0
- package/dist/assets/{select-DnxxDpcV.js → select-rUZ8nIZu.js} +2 -2
- package/dist/assets/{settings-2-agnMYc8a.js → settings-2-BI4SCkxQ.js} +1 -1
- package/dist/assets/skeleton-CVmEPG43.js +1 -0
- package/dist/assets/{tag-chip-CPOc8b2N.js → tag-chip-BdI_yIOA.js} +1 -1
- package/dist/assets/x-BcEayhV6.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +7 -7
- package/src/app/components/layout/app-layout.tsx +38 -2
- package/src/app/hooks/__tests__/use-app-event-consumers.test.ts +20 -0
- package/src/app/hooks/use-app-event-consumers.ts +21 -0
- package/src/app/presenters/app.presenter.ts +6 -0
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +92 -14
- package/src/features/apps/components/__tests__/apps-panel.test.tsx +58 -0
- package/src/features/apps/components/app-artwork.tsx +49 -0
- package/src/features/apps/components/app-marketplace-catalog.tsx +332 -0
- package/src/features/apps/components/app-marketplace-cover.tsx +64 -0
- package/src/features/apps/components/app-marketplace-detail.tsx +161 -0
- package/src/features/apps/components/app-marketplace-dialog.tsx +107 -217
- package/src/features/apps/components/app-marketplace-operation.tsx +84 -0
- package/src/features/apps/components/app-package-card.tsx +201 -52
- package/src/features/apps/components/app-packages-panel.tsx +98 -26
- package/src/features/apps/components/apps-panel.tsx +28 -27
- package/src/features/apps/hooks/use-app-marketplace.test.ts +144 -0
- package/src/features/apps/hooks/use-app-marketplace.ts +55 -85
- package/src/features/apps/hooks/use-app-packages.ts +59 -4
- package/src/features/apps/index.ts +1 -0
- package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +81 -0
- package/src/features/apps/managers/app-package-operation-settlement.manager.ts +80 -0
- package/src/features/apps/services/app-marketplace.service.ts +223 -0
- package/src/features/apps/types/app-marketplace.types.ts +75 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +64 -3
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +24 -14
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +53 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +18 -0
- package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +4 -1
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +126 -32
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +12 -2
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +31 -0
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
- package/src/features/chat/features/input/utils/chat-inline-token-composer.utils.ts +14 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +25 -0
- package/src/features/chat/features/message/hooks/use-chat-inline-token-actions.ts +8 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +77 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +56 -50
- package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +33 -11
- package/src/features/chat/features/workspace/components/project-files/workspace-project-tree-view.tsx +93 -0
- package/src/features/chat/features/workspace/stores/__tests__/workspace-project-tree.store.test.ts +38 -0
- package/src/features/chat/features/workspace/stores/workspace-project-tree.store.ts +160 -0
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +29 -0
- package/src/features/chat/managers/chat-composer-intent.manager.ts +29 -2
- package/src/features/chat/presenters/chat.presenter.ts +2 -2
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +1 -1
- package/src/features/panel-apps/components/panel-apps-list.tsx +1 -6
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +34 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +29 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +3 -17
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -0
- package/src/platforms/mobile/components/mobile-app-shell.tsx +9 -1
- package/src/shared/components/actions/__tests__/navigation-link.test.tsx +12 -0
- package/src/shared/components/actions/navigation-link.tsx +6 -3
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +5 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +11 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +13 -0
- package/src/shared/components/ui/tabs.tsx +24 -1
- package/src/shared/hooks/use-server-path-watch.ts +42 -0
- package/src/shared/lib/api/server-path/server-path.types.ts +10 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +10 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +49 -7
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +49 -7
- package/vite.config.ts +32 -1
- package/dist/assets/desktop-update-config-Dirc3bpt.js +0 -1
- package/dist/assets/doc-browser-4Sud6dWw.js +0 -1
- package/dist/assets/doc-browser-D4x_yH5F.js +0 -1
- package/dist/assets/form-actions-B2q4jgjE.js +0 -1
- package/dist/assets/index-CdyTsSBu.css +0 -1
- package/dist/assets/index-Das_bw2h.js +0 -107
- package/dist/assets/loader-circle-CpmBdaon.js +0 -1
- package/dist/assets/mcp-marketplace-page-CdOY3Sop.js +0 -1
- package/dist/assets/mcp-marketplace-page-DvjrfqEt.js +0 -9
- package/dist/assets/plus-C8FkjpSZ.js +0 -1
- package/dist/assets/react-C8XFFvuz.js +0 -8
- package/dist/assets/remote-BJHXa4mB.js +0 -1
- package/dist/assets/rotate-cw-DJ7UF3F2.js +0 -1
- package/dist/assets/security-config-CbDiY2xr.js +0 -1
- package/dist/assets/skeleton-XpqeWL62.js +0 -1
- package/dist/assets/x-odTHAtLZ.js +0 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppMarketplaceCatalogParams,
|
|
3
|
+
AppMarketplaceCatalogView,
|
|
4
|
+
AppMarketplaceDetailView,
|
|
5
|
+
AppMarketplaceItemView,
|
|
6
|
+
} from '@/features/apps/types/app-marketplace.types';
|
|
7
|
+
|
|
8
|
+
const APP_MARKETPLACE_BASE_URL = 'https://apps-registry.nextclaw.io';
|
|
9
|
+
const UNSUPPORTED_CATALOG_STATUSES = new Set([404, 405, 501]);
|
|
10
|
+
const EXPLICIT_DEV_PREVIEW_COVER_BASE_URL = import.meta.env
|
|
11
|
+
.VITE_APP_MARKETPLACE_PREVIEW_COVER_BASE_URL?.replace(/\/$/, '');
|
|
12
|
+
const DEFAULT_DEV_PREVIEW_COVER_BASE_URL = import.meta.env.DEV && !import.meta.env.VITEST
|
|
13
|
+
? '/__app-marketplace-preview'
|
|
14
|
+
: undefined;
|
|
15
|
+
const DEV_PREVIEW_COVERS = new Set([
|
|
16
|
+
'hello-notes',
|
|
17
|
+
'personal-organizer',
|
|
18
|
+
'starter-card',
|
|
19
|
+
'workspace-glance',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
type AppMarketplaceRequestParams = AppMarketplaceCatalogParams & {
|
|
23
|
+
cursor?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function fetchAppMarketplace(
|
|
28
|
+
params: AppMarketplaceRequestParams = {},
|
|
29
|
+
signal?: AbortSignal,
|
|
30
|
+
): Promise<AppMarketplaceCatalogView> {
|
|
31
|
+
const response = await fetchAppMarketplaceV2(params, signal);
|
|
32
|
+
if (UNSUPPORTED_CATALOG_STATUSES.has(response.status)) {
|
|
33
|
+
return await fetchLegacyAppMarketplace(params, signal);
|
|
34
|
+
}
|
|
35
|
+
return await readAppMarketplaceCatalogResponse(response);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function fetchAppMarketplaceDetail(
|
|
39
|
+
slug: string,
|
|
40
|
+
signal?: AbortSignal,
|
|
41
|
+
): Promise<AppMarketplaceDetailView> {
|
|
42
|
+
const response = await fetch(
|
|
43
|
+
`${APP_MARKETPLACE_BASE_URL}/api/v1/apps/items/${encodeURIComponent(slug)}`,
|
|
44
|
+
{ headers: { accept: 'application/json' }, signal },
|
|
45
|
+
);
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`Marketplace detail request failed (${response.status})`);
|
|
48
|
+
}
|
|
49
|
+
const payload: unknown = await response.json();
|
|
50
|
+
if (!isRecord(payload) || payload.ok !== true || !isRecord(payload.data)) {
|
|
51
|
+
throw new Error('Marketplace returned an invalid app detail');
|
|
52
|
+
}
|
|
53
|
+
const { data: item } = payload;
|
|
54
|
+
const rawDetail = item as Record<string, unknown>;
|
|
55
|
+
if (!isAppMarketplaceItem(item) || !isRecord(rawDetail.manifest) || !isRecord(rawDetail.permissions)) {
|
|
56
|
+
throw new Error('Marketplace returned an invalid app detail');
|
|
57
|
+
}
|
|
58
|
+
const { manifest, permissions } = rawDetail;
|
|
59
|
+
const components = Array.isArray(manifest.components)
|
|
60
|
+
? manifest.components.filter((component: unknown): component is { kind: 'panel' | 'service'; path: string } =>
|
|
61
|
+
isRecord(component)
|
|
62
|
+
&& (component.kind === 'panel' || component.kind === 'service')
|
|
63
|
+
&& typeof component.path === 'string')
|
|
64
|
+
: undefined;
|
|
65
|
+
return {
|
|
66
|
+
...withDevPreviewCover(item),
|
|
67
|
+
description: typeof rawDetail.description === 'string' ? rawDetail.description : undefined,
|
|
68
|
+
descriptionI18n: isStringRecord(rawDetail.descriptionI18n) ? rawDetail.descriptionI18n : undefined,
|
|
69
|
+
manifest: {
|
|
70
|
+
schemaVersion: manifest.schemaVersion === 2 ? 2 : 1,
|
|
71
|
+
icon: typeof manifest.icon === 'string' ? manifest.icon : undefined,
|
|
72
|
+
engines: isRecord(manifest.engines) && typeof manifest.engines.nextclaw === 'string'
|
|
73
|
+
? { nextclaw: manifest.engines.nextclaw }
|
|
74
|
+
: undefined,
|
|
75
|
+
components,
|
|
76
|
+
},
|
|
77
|
+
permissions: permissions as AppMarketplaceDetailView['permissions'],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function withDevPreviewCover<T extends AppMarketplaceItemView>(item: T): T {
|
|
82
|
+
if (item.coverUrl) {
|
|
83
|
+
return item;
|
|
84
|
+
}
|
|
85
|
+
const previewCoverUrl = readDevPreviewCoverUrl(item.slug);
|
|
86
|
+
if (item.publisher.id !== 'nextclaw' || !previewCoverUrl) {
|
|
87
|
+
return item;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
...item,
|
|
91
|
+
coverUrl: previewCoverUrl,
|
|
92
|
+
coverPreview: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function fetchAppMarketplaceV2(
|
|
97
|
+
params: AppMarketplaceRequestParams,
|
|
98
|
+
signal?: AbortSignal,
|
|
99
|
+
): Promise<Response> {
|
|
100
|
+
const { cursor, featured, limit, publisher, q, sort, tag, tags } = params;
|
|
101
|
+
const search = new URLSearchParams();
|
|
102
|
+
if (q) search.set('q', q);
|
|
103
|
+
if (tag) search.set('tag', tag);
|
|
104
|
+
if (tags?.length) search.set('tags', tags.join(','));
|
|
105
|
+
if (publisher) search.set('publisher', publisher);
|
|
106
|
+
if (featured !== undefined) search.set('featured', String(featured));
|
|
107
|
+
if (sort) search.set('sort', sort);
|
|
108
|
+
if (cursor) search.set('cursor', cursor);
|
|
109
|
+
search.set('limit', String(limit ?? 24));
|
|
110
|
+
return await fetch(`${APP_MARKETPLACE_BASE_URL}/api/v2/apps/items?${search}`, {
|
|
111
|
+
headers: { accept: 'application/json' },
|
|
112
|
+
signal,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function readAppMarketplaceCatalogResponse(
|
|
117
|
+
response: Response,
|
|
118
|
+
): Promise<AppMarketplaceCatalogView> {
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(`Marketplace request failed (${response.status})`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const payload: unknown = await response.json();
|
|
124
|
+
if (!isRecord(payload) || payload.ok !== true || !isRecord(payload.data)) {
|
|
125
|
+
throw new Error('Marketplace returned an invalid response');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const items = Array.isArray(payload.data.items)
|
|
129
|
+
? payload.data.items.filter(isAppMarketplaceItem)
|
|
130
|
+
: [];
|
|
131
|
+
return {
|
|
132
|
+
items: items.map(withDevPreviewCover),
|
|
133
|
+
nextCursor: typeof payload.data.nextCursor === 'string' ? payload.data.nextCursor : undefined,
|
|
134
|
+
hasMore: payload.data.hasMore === true,
|
|
135
|
+
query: typeof payload.data.query === 'string' ? payload.data.query : undefined,
|
|
136
|
+
tag: typeof payload.data.tag === 'string' ? payload.data.tag : undefined,
|
|
137
|
+
featured: typeof payload.data.featured === 'boolean' ? payload.data.featured : undefined,
|
|
138
|
+
sort: payload.data.sort === 'updated' || payload.data.sort === 'featured'
|
|
139
|
+
? payload.data.sort
|
|
140
|
+
: 'relevance',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function fetchLegacyAppMarketplace(
|
|
145
|
+
params: AppMarketplaceRequestParams,
|
|
146
|
+
signal?: AbortSignal,
|
|
147
|
+
): Promise<AppMarketplaceCatalogView> {
|
|
148
|
+
const { featured, publisher, q, sort, tag, tags } = params;
|
|
149
|
+
const search = new URLSearchParams({ page: '1', pageSize: '100' });
|
|
150
|
+
if (q) search.set('q', q);
|
|
151
|
+
if (tag) search.set('tag', tag);
|
|
152
|
+
const response = await fetch(`${APP_MARKETPLACE_BASE_URL}/api/v1/apps/items?${search}`, {
|
|
153
|
+
headers: { accept: 'application/json' },
|
|
154
|
+
signal,
|
|
155
|
+
});
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
throw new Error(`Marketplace compatibility request failed (${response.status})`);
|
|
158
|
+
}
|
|
159
|
+
const payload: unknown = await response.json();
|
|
160
|
+
if (!isRecord(payload) || payload.ok !== true || !isRecord(payload.data)) {
|
|
161
|
+
throw new Error('Marketplace returned an invalid compatibility response');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const items = Array.isArray(payload.data.items)
|
|
165
|
+
? payload.data.items.filter(isAppMarketplaceItem)
|
|
166
|
+
: [];
|
|
167
|
+
const requiredTags = new Set(tags ?? []);
|
|
168
|
+
return {
|
|
169
|
+
items: items
|
|
170
|
+
.filter((item) => !publisher || item.publisher.id === publisher)
|
|
171
|
+
.filter((item) => featured === undefined || item.featured === featured)
|
|
172
|
+
.filter((item) => requiredTags.size === 0 || item.tags.some((tag) => requiredTags.has(tag)))
|
|
173
|
+
.map(withDevPreviewCover),
|
|
174
|
+
hasMore: false,
|
|
175
|
+
query: q,
|
|
176
|
+
tag,
|
|
177
|
+
tags,
|
|
178
|
+
featured,
|
|
179
|
+
sort: sort ?? (q ? 'relevance' : 'featured'),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readDevPreviewCoverUrl(slug: string): string | undefined {
|
|
184
|
+
const baseUrl = EXPLICIT_DEV_PREVIEW_COVER_BASE_URL ?? DEFAULT_DEV_PREVIEW_COVER_BASE_URL;
|
|
185
|
+
if (baseUrl && DEV_PREVIEW_COVERS.has(slug)) {
|
|
186
|
+
return `${baseUrl}/${encodeURIComponent(slug)}.webp`;
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isAppMarketplaceItem(value: unknown): value is AppMarketplaceItemView {
|
|
192
|
+
if (!isRecord(value) || !isRecord(value.install) || !isRecord(value.publisher)) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return typeof value.id === 'string'
|
|
196
|
+
&& typeof value.slug === 'string'
|
|
197
|
+
&& typeof value.appId === 'string'
|
|
198
|
+
&& typeof value.name === 'string'
|
|
199
|
+
&& (value.iconUrl === undefined || typeof value.iconUrl === 'string')
|
|
200
|
+
&& (value.coverUrl === undefined || typeof value.coverUrl === 'string')
|
|
201
|
+
&& (value.coverPreview === undefined || typeof value.coverPreview === 'boolean')
|
|
202
|
+
&& (value.accentColor === undefined || typeof value.accentColor === 'string')
|
|
203
|
+
&& typeof value.summary === 'string'
|
|
204
|
+
&& isStringRecord(value.summaryI18n)
|
|
205
|
+
&& Array.isArray(value.tags)
|
|
206
|
+
&& value.tags.every((tag) => typeof tag === 'string')
|
|
207
|
+
&& typeof value.latestVersion === 'string'
|
|
208
|
+
&& typeof value.featured === 'boolean'
|
|
209
|
+
&& typeof value.publisher.id === 'string'
|
|
210
|
+
&& typeof value.publisher.name === 'string'
|
|
211
|
+
&& value.install.kind === 'registry'
|
|
212
|
+
&& typeof value.install.spec === 'string'
|
|
213
|
+
&& typeof value.install.registry === 'string'
|
|
214
|
+
&& typeof value.webUrl === 'string';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
218
|
+
return isRecord(value) && Object.values(value).every((entry) => typeof entry === 'string');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
222
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
223
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type AppMarketplaceItemView = {
|
|
2
|
+
id: string;
|
|
3
|
+
slug: string;
|
|
4
|
+
appId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
iconUrl?: string;
|
|
7
|
+
coverUrl?: string;
|
|
8
|
+
coverPreview?: boolean;
|
|
9
|
+
accentColor?: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
summaryI18n: Record<string, string>;
|
|
12
|
+
tags: string[];
|
|
13
|
+
latestVersion: string;
|
|
14
|
+
featured: boolean;
|
|
15
|
+
publisher: {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
};
|
|
20
|
+
install: {
|
|
21
|
+
kind: 'registry';
|
|
22
|
+
spec: string;
|
|
23
|
+
registry: string;
|
|
24
|
+
};
|
|
25
|
+
webUrl: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type AppMarketplaceCatalogView = {
|
|
29
|
+
items: AppMarketplaceItemView[];
|
|
30
|
+
nextCursor?: string;
|
|
31
|
+
hasMore: boolean;
|
|
32
|
+
query?: string;
|
|
33
|
+
tag?: string;
|
|
34
|
+
tags?: string[];
|
|
35
|
+
featured?: boolean;
|
|
36
|
+
sort: 'relevance' | 'featured' | 'updated';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type AppMarketplaceListView = {
|
|
40
|
+
items: AppMarketplaceItemView[];
|
|
41
|
+
hasMore: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type AppMarketplaceDetailView = AppMarketplaceItemView & {
|
|
45
|
+
description?: string;
|
|
46
|
+
descriptionI18n?: Record<string, string>;
|
|
47
|
+
manifest: {
|
|
48
|
+
schemaVersion: 1 | 2;
|
|
49
|
+
icon?: string;
|
|
50
|
+
engines?: { nextclaw?: string };
|
|
51
|
+
components?: Array<{
|
|
52
|
+
kind: 'panel' | 'service';
|
|
53
|
+
path: string;
|
|
54
|
+
}>;
|
|
55
|
+
};
|
|
56
|
+
permissions: {
|
|
57
|
+
documentAccess?: Array<{
|
|
58
|
+
id: string;
|
|
59
|
+
mode: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
}>;
|
|
62
|
+
allowedDomains?: string[];
|
|
63
|
+
storage?: { namespace?: string };
|
|
64
|
+
capabilities?: { hostBridge?: boolean };
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type AppMarketplaceCatalogParams = {
|
|
69
|
+
q?: string;
|
|
70
|
+
tag?: string;
|
|
71
|
+
tags?: string[];
|
|
72
|
+
featured?: boolean;
|
|
73
|
+
publisher?: string;
|
|
74
|
+
sort?: 'relevance' | 'featured' | 'updated';
|
|
75
|
+
};
|
|
@@ -427,6 +427,26 @@ describe('SessionConversationInput streaming stability', () => {
|
|
|
427
427
|
expect(deleteQueuedInput).toHaveBeenCalledWith('queued-2');
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
+
it('renders a submitting queue row immediately without premature edit actions', () => {
|
|
431
|
+
const controlRef: MutableRefObject<StreamingInputControl | null> = { current: null };
|
|
432
|
+
renderInput(
|
|
433
|
+
<StreamingSessionConversationInputHarness
|
|
434
|
+
controlRef={controlRef}
|
|
435
|
+
controllerOverride={{
|
|
436
|
+
...controller,
|
|
437
|
+
queuedInputs: [
|
|
438
|
+
{ id: 'submitting-1', isSubmitting: true, preview: '正在提交的任务' },
|
|
439
|
+
],
|
|
440
|
+
}}
|
|
441
|
+
/>,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
expect(screen.getByText('正在提交的任务')).toBeTruthy();
|
|
445
|
+
expect(screen.getByLabelText('Adding to queue…')).toBeTruthy();
|
|
446
|
+
expect(screen.queryByRole('button', { name: 'Edit queued input' })).toBeNull();
|
|
447
|
+
expect(screen.queryByRole('button', { name: 'Delete queued input' })).toBeNull();
|
|
448
|
+
});
|
|
449
|
+
|
|
430
450
|
it('leaves send-error rendering to the conversation surface', () => {
|
|
431
451
|
const controlRef: MutableRefObject<StreamingInputControl | null> = { current: null };
|
|
432
452
|
const providerError = 'Chat Completions API failed (402): raw provider error';
|
|
@@ -456,9 +476,13 @@ function createAttachmentRunHandle(): NcpRunHandle {
|
|
|
456
476
|
|
|
457
477
|
function AttachmentSubmitHarness({
|
|
458
478
|
initialPrompt,
|
|
479
|
+
isRunning = false,
|
|
480
|
+
refreshQueuedInputs = async () => [],
|
|
459
481
|
send,
|
|
460
482
|
}: {
|
|
461
483
|
readonly initialPrompt?: string;
|
|
484
|
+
readonly isRunning?: boolean;
|
|
485
|
+
readonly refreshQueuedInputs?: () => Promise<readonly never[]>;
|
|
462
486
|
readonly send: AttachmentSubmitAgentSend;
|
|
463
487
|
}) {
|
|
464
488
|
const { inputActions, inputSnapshot } = useSessionConversationInputState(
|
|
@@ -503,14 +527,14 @@ function AttachmentSubmitHarness({
|
|
|
503
527
|
continueRun: vi.fn(),
|
|
504
528
|
editMessage: vi.fn(),
|
|
505
529
|
isHydrating: false,
|
|
506
|
-
isRunning
|
|
530
|
+
isRunning,
|
|
507
531
|
isSending: false,
|
|
508
532
|
send,
|
|
509
533
|
snapshot: {
|
|
510
|
-
activeRun: null,
|
|
534
|
+
activeRun: isRunning ? { sessionId: 'session-attachment' } : null,
|
|
511
535
|
},
|
|
512
536
|
visibleMessages: [],
|
|
513
|
-
}), [send]);
|
|
537
|
+
}), [isRunning, send]);
|
|
514
538
|
const controller = useSessionConversationController({
|
|
515
539
|
agent,
|
|
516
540
|
inputSnapshot,
|
|
@@ -518,6 +542,7 @@ function AttachmentSubmitHarness({
|
|
|
518
542
|
isRuntimeBlocked: false,
|
|
519
543
|
runQueue: {
|
|
520
544
|
inputs: [],
|
|
545
|
+
refreshQueuedInputs,
|
|
521
546
|
removeQueuedInput: async () => null,
|
|
522
547
|
},
|
|
523
548
|
selectedAgentId: 'main',
|
|
@@ -543,6 +568,42 @@ function AttachmentSubmitHarness({
|
|
|
543
568
|
}
|
|
544
569
|
|
|
545
570
|
describe('SessionConversationInput attachment submit', () => {
|
|
571
|
+
it('shows immediate queue feedback from the real send button before backend acknowledgement', async () => {
|
|
572
|
+
let resolveSend!: (handle: NcpRunHandle) => void;
|
|
573
|
+
const send = vi.fn<AttachmentSubmitAgentSend>(() => new Promise((resolve) => {
|
|
574
|
+
resolveSend = resolve;
|
|
575
|
+
}));
|
|
576
|
+
const refreshQueuedInputs = vi.fn(async () => [] as const);
|
|
577
|
+
|
|
578
|
+
renderInput(
|
|
579
|
+
<AttachmentSubmitHarness
|
|
580
|
+
initialPrompt="稍后继续这个任务"
|
|
581
|
+
isRunning
|
|
582
|
+
refreshQueuedInputs={refreshQueuedInputs}
|
|
583
|
+
send={send}
|
|
584
|
+
/>,
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
await act(async () => {
|
|
588
|
+
fireEvent.click(screen.getByRole('button', { name: /Queue|排队发送/ }));
|
|
589
|
+
await Promise.resolve();
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
593
|
+
const submittingStatus = screen.getByLabelText('Adding to queue…');
|
|
594
|
+
expect(submittingStatus.parentElement?.textContent).toContain('稍后继续这个任务');
|
|
595
|
+
await waitFor(() => expect(screen.getByRole('textbox').textContent).toBe(''));
|
|
596
|
+
expect(refreshQueuedInputs).not.toHaveBeenCalled();
|
|
597
|
+
|
|
598
|
+
await act(async () => {
|
|
599
|
+
resolveSend({ ...createAttachmentRunHandle(), runId: null });
|
|
600
|
+
await Promise.resolve();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await waitFor(() => expect(refreshQueuedInputs).toHaveBeenCalledTimes(1));
|
|
604
|
+
await waitFor(() => expect(screen.queryByLabelText('Adding to queue…')).toBeNull());
|
|
605
|
+
});
|
|
606
|
+
|
|
546
607
|
it('preserves a project file reference in the outgoing user message and AI context', async () => {
|
|
547
608
|
const send = vi.fn<AttachmentSubmitAgentSend>(async () => createAttachmentRunHandle());
|
|
548
609
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { CornerDownRight, Pencil, Trash2 } from 'lucide-react';
|
|
2
|
+
import { CornerDownRight, LoaderCircle, Pencil, Trash2 } from 'lucide-react';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
Tooltip,
|
|
@@ -74,20 +74,30 @@ export function SessionQueuedInputRows({
|
|
|
74
74
|
<span className="min-w-0 flex-1 truncate font-medium text-foreground/80">
|
|
75
75
|
{item.preview || t('chatQueuedBannerAttachmentFallback')}
|
|
76
76
|
</span>
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
label={t('
|
|
80
|
-
|
|
77
|
+
{item.isSubmitting ? (
|
|
78
|
+
<span
|
|
79
|
+
aria-label={t('chatQueuedSubmitting')}
|
|
80
|
+
className="inline-flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground"
|
|
81
81
|
>
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
<LoaderCircle className="h-3.5 w-3.5 animate-spin" />
|
|
83
|
+
{t('chatQueuedSubmitting')}
|
|
84
|
+
</span>
|
|
85
|
+
) : (
|
|
86
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
87
|
+
<QueuedInputIconButton
|
|
88
|
+
label={t('chatQueuedEdit')}
|
|
89
|
+
onClick={() => controller.editQueuedInput(item.id)}
|
|
90
|
+
>
|
|
91
|
+
<Pencil className="h-4 w-4" />
|
|
92
|
+
</QueuedInputIconButton>
|
|
93
|
+
<QueuedInputIconButton
|
|
94
|
+
label={t('chatQueuedDelete')}
|
|
95
|
+
onClick={() => controller.deleteQueuedInput(item.id)}
|
|
96
|
+
>
|
|
97
|
+
<Trash2 className="h-4 w-4" />
|
|
98
|
+
</QueuedInputIconButton>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
91
101
|
</div>
|
|
92
102
|
))}
|
|
93
103
|
</div>
|
|
@@ -59,6 +59,7 @@ function createControllerParams(params: {
|
|
|
59
59
|
const removeQueuedInput = vi.fn(async (id: string) =>
|
|
60
60
|
queuedInputs.find((item) => item.id === id) ?? null,
|
|
61
61
|
);
|
|
62
|
+
const refreshQueuedInputs = vi.fn(async () => queuedInputs);
|
|
62
63
|
return {
|
|
63
64
|
agent: {
|
|
64
65
|
abort: vi.fn(),
|
|
@@ -116,6 +117,7 @@ function createControllerParams(params: {
|
|
|
116
117
|
isRuntimeBlocked: false,
|
|
117
118
|
runQueue: {
|
|
118
119
|
inputs: queuedInputs,
|
|
120
|
+
refreshQueuedInputs,
|
|
119
121
|
removeQueuedInput,
|
|
120
122
|
},
|
|
121
123
|
selectedAgentId: 'main',
|
|
@@ -178,6 +180,57 @@ describe('useSessionConversationController backend run queue', () => {
|
|
|
178
180
|
expect(params.resetComposer).toHaveBeenCalledTimes(1);
|
|
179
181
|
});
|
|
180
182
|
|
|
183
|
+
it('projects a submitting queue row before the backend acknowledges the message', async () => {
|
|
184
|
+
let resolveSend!: (handle: NcpRunHandle) => void;
|
|
185
|
+
const send = vi.fn<TestAgentSend>(() => new Promise((resolve) => {
|
|
186
|
+
resolveSend = resolve;
|
|
187
|
+
}));
|
|
188
|
+
const params = createControllerParams({ isRunning: true, send });
|
|
189
|
+
const { result } = renderHook(() => useSessionConversationController(params));
|
|
190
|
+
let submission!: Promise<void>;
|
|
191
|
+
|
|
192
|
+
act(() => {
|
|
193
|
+
submission = result.current.send();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(params.resetComposer).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(result.current.queuedInputs).toEqual([
|
|
198
|
+
expect.objectContaining({
|
|
199
|
+
isSubmitting: true,
|
|
200
|
+
preview: 'next task',
|
|
201
|
+
}),
|
|
202
|
+
]);
|
|
203
|
+
expect(params.runQueue.refreshQueuedInputs).not.toHaveBeenCalled();
|
|
204
|
+
|
|
205
|
+
await act(async () => {
|
|
206
|
+
resolveSend(createRunHandle({ runId: null }));
|
|
207
|
+
await submission;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(params.runQueue.refreshQueuedInputs).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(result.current.queuedInputs).toEqual([]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('removes the submitting queue row and restores the draft when submission fails', async () => {
|
|
215
|
+
const sendError = new Error('queue submission failed');
|
|
216
|
+
const send = vi.fn<TestAgentSend>(async () => {
|
|
217
|
+
throw sendError;
|
|
218
|
+
});
|
|
219
|
+
const params = createControllerParams({ isRunning: true, send });
|
|
220
|
+
const { result } = renderHook(() => useSessionConversationController(params));
|
|
221
|
+
|
|
222
|
+
await act(async () => {
|
|
223
|
+
await expect(result.current.send()).rejects.toThrow(sendError);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(result.current.queuedInputs).toEqual([]);
|
|
227
|
+
expect(params.restoreComposer).toHaveBeenCalledWith(expect.objectContaining({
|
|
228
|
+
text: 'next task',
|
|
229
|
+
}));
|
|
230
|
+
expect(params.setSendError).toHaveBeenLastCalledWith(sendError.message);
|
|
231
|
+
expect(params.runQueue.refreshQueuedInputs).not.toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
181
234
|
it('keeps an existing session bound to its agent instead of the global draft selection', async () => {
|
|
182
235
|
const send = createSendMock();
|
|
183
236
|
const params = createControllerParams({ isRunning: false, send });
|
package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx
CHANGED
|
@@ -83,6 +83,24 @@ describe('useSessionRunQueue', () => {
|
|
|
83
83
|
unmount();
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
it('refreshes the authoritative queue on demand after a submission is accepted', async () => {
|
|
87
|
+
const listQueuedInputs = vi.spyOn(nextclawClient.sessions, 'listQueuedInputs')
|
|
88
|
+
.mockResolvedValue(createQueue('session-1', 'queued'));
|
|
89
|
+
const { result } = renderHook(
|
|
90
|
+
() => useSessionRunQueue('session-1'),
|
|
91
|
+
{ wrapper: createWrapper() },
|
|
92
|
+
);
|
|
93
|
+
await waitFor(() => expect(listQueuedInputs).toHaveBeenCalledTimes(1));
|
|
94
|
+
|
|
95
|
+
let refreshed: UiNcpSessionQueuedInputsView['inputs'] = [];
|
|
96
|
+
await act(async () => {
|
|
97
|
+
refreshed = [...await result.current.refreshQueuedInputs()];
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(listQueuedInputs).toHaveBeenCalledTimes(2);
|
|
101
|
+
expect(refreshed[0]?.message.parts[0]).toMatchObject({ text: 'queued' });
|
|
102
|
+
});
|
|
103
|
+
|
|
86
104
|
it('replaces an in-flight stale queue read after the queue changes', async () => {
|
|
87
105
|
let resolveInitialRead!: (queue: UiNcpSessionQueuedInputsView) => void;
|
|
88
106
|
const initialRead = new Promise<UiNcpSessionQueuedInputsView>((resolve) => {
|
|
@@ -2,6 +2,7 @@ import { useEffect, type RefObject } from 'react';
|
|
|
2
2
|
import { type ChatInputBarHandle } from '@nextclaw/agent-chat-ui';
|
|
3
3
|
import {
|
|
4
4
|
CHAT_CONVERSATION_EXCERPT_TOKEN_KIND,
|
|
5
|
+
CHAT_UI_RESOURCE_TOKEN_KIND,
|
|
5
6
|
CHAT_WORKSPACE_EXCERPT_TOKEN_KIND,
|
|
6
7
|
} from '@nextclaw/shared';
|
|
7
8
|
|
|
@@ -41,7 +42,9 @@ export function useChatComposerReferenceIntent(params: {
|
|
|
41
42
|
role: intent.role,
|
|
42
43
|
excerpt: intent.excerpt,
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
: intent.kind === CHAT_UI_RESOURCE_TOKEN_KIND
|
|
46
|
+
? { reference: intent.reference }
|
|
47
|
+
: undefined,
|
|
45
48
|
});
|
|
46
49
|
intentManager.markConsumed(intent.id);
|
|
47
50
|
};
|