@nextclaw/ui 0.15.26 → 0.15.28
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 +23 -0
- package/dist/assets/{api-CK3Fro7z.js → api-a_GaOFc7.js} +4 -4
- package/dist/assets/{appearance-settings-page-zqwWO8Dy.js → appearance-settings-page-KEYKkxHu.js} +1 -1
- package/dist/assets/{channels-list-page-CVCZBlCL.js → channels-list-page-eXLXPgKT.js} +2 -2
- package/dist/assets/{chat-page-D4wehRTz.js → chat-page-jfImLdsq.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-C20U71k1.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-BP0Hd2jI.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-CJCnaKOh.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-Dqu-ZoWG.js} +1 -1
- package/dist/assets/{providers-config-page-CaO8WfZH.js → providers-config-page-C6fRN2rJ.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-Xyv2c_W9.js +1 -0
- package/dist/assets/rotate-cw-DjbbiIW_.js +1 -0
- package/dist/assets/{runtime-config-page-BuA4A02z.js → runtime-config-page-CtypMgLd.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-DhSFjYj4.js} +1 -1
- package/dist/assets/{secrets-config-page-BlPOuql_.js → secrets-config-page-BLGcyb1D.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 +6 -6
- 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 +116 -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 +120 -42
- 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
package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useMemo } from 'react';
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
2
|
import { buildNcpRequestEnvelope } from '@nextclaw/ncp-react';
|
|
3
3
|
import type { UiNcpSessionQueuedInputView } from '@nextclaw/client-sdk';
|
|
4
4
|
import type { NcpAgentSendEnvelope, NcpRunHandle } from '@nextclaw/ncp';
|
|
@@ -39,11 +39,13 @@ type SessionConversationAgent = SessionConversationRecoveryAgent & {
|
|
|
39
39
|
|
|
40
40
|
export type SessionConversationQueuedInput = {
|
|
41
41
|
readonly id: string;
|
|
42
|
+
readonly isSubmitting?: boolean;
|
|
42
43
|
readonly preview: string;
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
type SessionRunQueue = {
|
|
46
47
|
readonly inputs: readonly UiNcpSessionQueuedInputView[];
|
|
48
|
+
readonly refreshQueuedInputs: () => Promise<readonly UiNcpSessionQueuedInputView[]>;
|
|
47
49
|
readonly removeQueuedInput: (
|
|
48
50
|
queuedInputId: string,
|
|
49
51
|
) => Promise<UiNcpSessionQueuedInputView | null>;
|
|
@@ -85,6 +87,13 @@ type SubmissionDraft = {
|
|
|
85
87
|
readonly metadata: Record<string, unknown>;
|
|
86
88
|
};
|
|
87
89
|
|
|
90
|
+
type SubmittingQueuedInput = {
|
|
91
|
+
readonly id: string;
|
|
92
|
+
readonly preview: string;
|
|
93
|
+
readonly sessionId: string;
|
|
94
|
+
readonly userMessageId: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
88
97
|
const hasSendableMessagePart = (parts: ReturnType<typeof deriveNcpMessagePartsFromComposer>): boolean =>
|
|
89
98
|
parts.some((part) => part.type !== 'text' || part.text.trim().length > 0);
|
|
90
99
|
|
|
@@ -188,6 +197,97 @@ function buildSubmissionEnvelope(
|
|
|
188
197
|
});
|
|
189
198
|
}
|
|
190
199
|
|
|
200
|
+
function buildSubmittingQueuedInput(
|
|
201
|
+
envelope: NcpAgentSendEnvelope,
|
|
202
|
+
): SubmittingQueuedInput | null {
|
|
203
|
+
const sessionId = envelope.sessionId?.trim();
|
|
204
|
+
if (!sessionId) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const message = {
|
|
208
|
+
...envelope.message,
|
|
209
|
+
sessionId,
|
|
210
|
+
};
|
|
211
|
+
const enqueuedAt = new Date().toISOString();
|
|
212
|
+
const input: UiNcpSessionQueuedInputView = {
|
|
213
|
+
id: `submitting-${message.id}`,
|
|
214
|
+
sessionId,
|
|
215
|
+
enqueuedAt,
|
|
216
|
+
message,
|
|
217
|
+
metadata: structuredClone(envelope.metadata ?? {}),
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
id: input.id,
|
|
221
|
+
preview: buildSessionQueuedInputPreview(input),
|
|
222
|
+
sessionId,
|
|
223
|
+
userMessageId: message.id,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function useSubmittingQueuedInputProjection(
|
|
228
|
+
serverInputs: readonly UiNcpSessionQueuedInputView[],
|
|
229
|
+
sessionKey: string | null,
|
|
230
|
+
) {
|
|
231
|
+
const [submittingInput, setSubmittingInput] =
|
|
232
|
+
useState<SubmittingQueuedInput | null>(null);
|
|
233
|
+
const stageSubmittingInput = useCallback((envelope: NcpAgentSendEnvelope) => {
|
|
234
|
+
const input = buildSubmittingQueuedInput(envelope);
|
|
235
|
+
if (input) {
|
|
236
|
+
setSubmittingInput(input);
|
|
237
|
+
}
|
|
238
|
+
return input;
|
|
239
|
+
}, []);
|
|
240
|
+
const clearSubmittingInput = useCallback((input: SubmittingQueuedInput) => {
|
|
241
|
+
setSubmittingInput((current) =>
|
|
242
|
+
current?.userMessageId === input.userMessageId ? null : current
|
|
243
|
+
);
|
|
244
|
+
}, []);
|
|
245
|
+
const queuedInputs = useMemo<SessionConversationQueuedInput[]>(() => {
|
|
246
|
+
const projected: SessionConversationQueuedInput[] = serverInputs.map((input) => ({
|
|
247
|
+
id: input.id,
|
|
248
|
+
preview: buildSessionQueuedInputPreview(input),
|
|
249
|
+
}));
|
|
250
|
+
if (
|
|
251
|
+
submittingInput?.sessionId === sessionKey &&
|
|
252
|
+
!serverInputs.some((input) => input.message.id === submittingInput.userMessageId)
|
|
253
|
+
) {
|
|
254
|
+
projected.push({
|
|
255
|
+
id: submittingInput.id,
|
|
256
|
+
isSubmitting: true,
|
|
257
|
+
preview: submittingInput.preview,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return projected;
|
|
261
|
+
}, [serverInputs, sessionKey, submittingInput]);
|
|
262
|
+
return { clearSubmittingInput, queuedInputs, stageSubmittingInput };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function useQueuedInputActions(params: {
|
|
266
|
+
readonly availableSkills: readonly { ref: string; name: string }[];
|
|
267
|
+
readonly restoreComposer: (snapshot: ComposerDraftSnapshot) => void;
|
|
268
|
+
readonly runQueue: SessionRunQueue;
|
|
269
|
+
readonly setSendError: (message: string | null) => void;
|
|
270
|
+
}) {
|
|
271
|
+
const { availableSkills, restoreComposer, runQueue, setSendError } = params;
|
|
272
|
+
const editQueuedInput = useCallback((id: string) => {
|
|
273
|
+
const input = runQueue.inputs.find((item) => item.id === id);
|
|
274
|
+
if (!input) return;
|
|
275
|
+
void runQueue.removeQueuedInput(id).then((removed) => {
|
|
276
|
+
if (!removed) return;
|
|
277
|
+
restoreComposer(buildSessionQueuedInputComposerSnapshot(removed, availableSkills));
|
|
278
|
+
setSendError(null);
|
|
279
|
+
}).catch((error) => {
|
|
280
|
+
setSendError(error instanceof Error ? error.message : String(error));
|
|
281
|
+
});
|
|
282
|
+
}, [availableSkills, restoreComposer, runQueue, setSendError]);
|
|
283
|
+
const deleteQueuedInput = useCallback((id: string) => {
|
|
284
|
+
void runQueue.removeQueuedInput(id).then(() => setSendError(null)).catch((error) => {
|
|
285
|
+
setSendError(error instanceof Error ? error.message : String(error));
|
|
286
|
+
});
|
|
287
|
+
}, [runQueue, setSendError]);
|
|
288
|
+
return { deleteQueuedInput, editQueuedInput };
|
|
289
|
+
}
|
|
290
|
+
|
|
191
291
|
export function useSessionConversationController(params: UseSessionConversationControllerParams) {
|
|
192
292
|
const {
|
|
193
293
|
agent,
|
|
@@ -203,6 +303,17 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
203
303
|
restoreComposer,
|
|
204
304
|
setSendError,
|
|
205
305
|
} = params;
|
|
306
|
+
const {
|
|
307
|
+
clearSubmittingInput,
|
|
308
|
+
queuedInputs,
|
|
309
|
+
stageSubmittingInput,
|
|
310
|
+
} = useSubmittingQueuedInputProjection(runQueue.inputs, sessionKey);
|
|
311
|
+
const { deleteQueuedInput, editQueuedInput } = useQueuedInputActions({
|
|
312
|
+
availableSkills: inputQuery.skillRecords,
|
|
313
|
+
restoreComposer,
|
|
314
|
+
runQueue,
|
|
315
|
+
setSendError,
|
|
316
|
+
});
|
|
206
317
|
const parts = useMemo(
|
|
207
318
|
() => deriveNcpMessagePartsFromComposer([...inputSnapshot.nodes], inputSnapshot.attachments),
|
|
208
319
|
[inputSnapshot.attachments, inputSnapshot.nodes],
|
|
@@ -255,10 +366,17 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
255
366
|
if (!envelope) {
|
|
256
367
|
return;
|
|
257
368
|
}
|
|
369
|
+
const queuedSubmission = agent.isRunning ? stageSubmittingInput(envelope) : null;
|
|
258
370
|
resetComposer();
|
|
259
371
|
setSendError(null);
|
|
260
372
|
try {
|
|
261
373
|
const handle = await agent.send(envelope);
|
|
374
|
+
if (queuedSubmission && handle?.runId === null) {
|
|
375
|
+
await runQueue.refreshQueuedInputs().catch(() => undefined);
|
|
376
|
+
}
|
|
377
|
+
if (queuedSubmission) {
|
|
378
|
+
clearSubmittingInput(queuedSubmission);
|
|
379
|
+
}
|
|
262
380
|
const materializedSessionKey =
|
|
263
381
|
handle?.sessionId?.trim() ||
|
|
264
382
|
agent.snapshot.activeRun?.sessionId?.trim() ||
|
|
@@ -267,12 +385,16 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
267
385
|
onSessionMaterialized?.(materializedSessionKey);
|
|
268
386
|
}
|
|
269
387
|
} catch (error) {
|
|
388
|
+
if (queuedSubmission) {
|
|
389
|
+
clearSubmittingInput(queuedSubmission);
|
|
390
|
+
}
|
|
270
391
|
restoreComposer(draft.composerSnapshot);
|
|
271
392
|
setSendError(error instanceof Error ? error.message : String(error));
|
|
272
393
|
throw error;
|
|
273
394
|
}
|
|
274
395
|
}, [
|
|
275
396
|
agent,
|
|
397
|
+
clearSubmittingInput,
|
|
276
398
|
continueRun,
|
|
277
399
|
inputQuery,
|
|
278
400
|
inputSnapshot,
|
|
@@ -284,36 +406,11 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
284
406
|
selectedAgentId,
|
|
285
407
|
sessionKey,
|
|
286
408
|
setSendError,
|
|
409
|
+
stageSubmittingInput,
|
|
287
410
|
primaryAction,
|
|
411
|
+
runQueue,
|
|
288
412
|
]);
|
|
289
413
|
|
|
290
|
-
const editQueuedInput = useCallback((id: string) => {
|
|
291
|
-
const input = runQueue.inputs.find((item) => item.id === id);
|
|
292
|
-
if (!input) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
void runQueue.removeQueuedInput(id).then((removed) => {
|
|
296
|
-
if (!removed) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
restoreComposer(buildSessionQueuedInputComposerSnapshot(
|
|
300
|
-
removed,
|
|
301
|
-
inputQuery.skillRecords,
|
|
302
|
-
));
|
|
303
|
-
setSendError(null);
|
|
304
|
-
}).catch((error) => {
|
|
305
|
-
setSendError(error instanceof Error ? error.message : String(error));
|
|
306
|
-
});
|
|
307
|
-
}, [inputQuery.skillRecords, restoreComposer, runQueue, setSendError]);
|
|
308
|
-
|
|
309
|
-
const deleteQueuedInput = useCallback((id: string) => {
|
|
310
|
-
void runQueue.removeQueuedInput(id).then(() => {
|
|
311
|
-
setSendError(null);
|
|
312
|
-
}).catch((error) => {
|
|
313
|
-
setSendError(error instanceof Error ? error.message : String(error));
|
|
314
|
-
});
|
|
315
|
-
}, [runQueue, setSendError]);
|
|
316
|
-
|
|
317
414
|
const stop = useCallback(async () => {
|
|
318
415
|
await agent.abort();
|
|
319
416
|
}, [agent]);
|
|
@@ -327,10 +424,7 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
327
424
|
editQueuedInput,
|
|
328
425
|
hasSendableDraft,
|
|
329
426
|
isSending,
|
|
330
|
-
queuedInputs
|
|
331
|
-
id: input.id,
|
|
332
|
-
preview: buildSessionQueuedInputPreview(input),
|
|
333
|
-
})),
|
|
427
|
+
queuedInputs,
|
|
334
428
|
send,
|
|
335
429
|
sendDisabled,
|
|
336
430
|
primaryAction,
|
|
@@ -16,6 +16,7 @@ export function useSessionRunQueue(sessionKey: string | null) {
|
|
|
16
16
|
retry: false,
|
|
17
17
|
staleTime: 5_000,
|
|
18
18
|
});
|
|
19
|
+
const { data, isLoading, refetch } = query;
|
|
19
20
|
|
|
20
21
|
useEffect(() => {
|
|
21
22
|
if (!normalizedSessionKey) {
|
|
@@ -43,9 +44,18 @@ export function useSessionRunQueue(sessionKey: string | null) {
|
|
|
43
44
|
);
|
|
44
45
|
}, [normalizedSessionKey]);
|
|
45
46
|
|
|
47
|
+
const refreshQueuedInputs = useCallback(async (): Promise<readonly UiNcpSessionQueuedInputView[]> => {
|
|
48
|
+
if (!normalizedSessionKey) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const result = await refetch();
|
|
52
|
+
return result.data?.inputs ?? [];
|
|
53
|
+
}, [normalizedSessionKey, refetch]);
|
|
54
|
+
|
|
46
55
|
return {
|
|
47
|
-
inputs:
|
|
48
|
-
isLoading
|
|
56
|
+
inputs: data?.inputs ?? [],
|
|
57
|
+
isLoading,
|
|
58
|
+
refreshQueuedInputs,
|
|
49
59
|
removeQueuedInput,
|
|
50
60
|
};
|
|
51
61
|
}
|
|
@@ -374,3 +374,34 @@ describe('chat inline token workspace references', () => {
|
|
|
374
374
|
})).toBeNull();
|
|
375
375
|
});
|
|
376
376
|
});
|
|
377
|
+
|
|
378
|
+
describe('UI resource inline tokens', () => {
|
|
379
|
+
it('round-trips the visible token with its selection-time snapshot', () => {
|
|
380
|
+
const reference = {
|
|
381
|
+
uri: 'nextclaw://apps?tab=panel-apps',
|
|
382
|
+
resourceKind: 'apps',
|
|
383
|
+
title: 'Panel Apps',
|
|
384
|
+
currentUrl: 'nextclaw://apps?tab=panel-apps',
|
|
385
|
+
contentParams: { filter: 'installed' },
|
|
386
|
+
};
|
|
387
|
+
const tokens = buildInlineTokensFromComposer([
|
|
388
|
+
createChatComposerTokenNode({
|
|
389
|
+
tokenKind: 'ui_resource',
|
|
390
|
+
tokenKey: reference.uri,
|
|
391
|
+
label: reference.title,
|
|
392
|
+
data: { reference },
|
|
393
|
+
}),
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
expect(tokens).toEqual([{
|
|
397
|
+
kind: 'ui_resource',
|
|
398
|
+
key: reference.uri,
|
|
399
|
+
label: reference.title,
|
|
400
|
+
rawText: `@resource:${encodeURIComponent(reference.uri)}`,
|
|
401
|
+
reference,
|
|
402
|
+
}]);
|
|
403
|
+
expect(readInlineTokensFromMetadata({
|
|
404
|
+
ui_inline_tokens: createInlineTokensMetadata(tokens),
|
|
405
|
+
})).toEqual(tokens);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
CHAT_CONVERSATION_EXCERPT_TOKEN_KIND,
|
|
3
3
|
CHAT_PROJECT_TOKEN_KIND,
|
|
4
4
|
CHAT_SYSTEM_OBJECT_TOKEN_KIND,
|
|
5
|
+
CHAT_UI_RESOURCE_TOKEN_KIND,
|
|
5
6
|
CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
|
|
6
7
|
CHAT_WORKSPACE_EXCERPT_TOKEN_KIND,
|
|
7
8
|
CHAT_WORKSPACE_FILE_TOKEN_KIND,
|
|
@@ -15,6 +16,7 @@ const CHAT_WORKSPACE_DIRECTORY_TOKEN_PREFIX = '@folder:';
|
|
|
15
16
|
const CHAT_WORKSPACE_EXCERPT_TOKEN_PREFIX = '@excerpt:';
|
|
16
17
|
const CHAT_CONVERSATION_EXCERPT_TOKEN_PREFIX = '@message-excerpt:';
|
|
17
18
|
const CHAT_SYSTEM_OBJECT_TOKEN_PREFIX = '@object:';
|
|
19
|
+
const CHAT_UI_RESOURCE_TOKEN_PREFIX = '@resource:';
|
|
18
20
|
|
|
19
21
|
export function serializeChatComposerTokenText(params: {
|
|
20
22
|
label?: string;
|
|
@@ -46,5 +48,8 @@ export function serializeChatComposerTokenText(params: {
|
|
|
46
48
|
if (tokenKind === CHAT_SYSTEM_OBJECT_TOKEN_KIND) {
|
|
47
49
|
return `${CHAT_SYSTEM_OBJECT_TOKEN_PREFIX}${encodeURIComponent(tokenKey)}`;
|
|
48
50
|
}
|
|
51
|
+
if (tokenKind === CHAT_UI_RESOURCE_TOKEN_KIND) {
|
|
52
|
+
return `${CHAT_UI_RESOURCE_TOKEN_PREFIX}${encodeURIComponent(tokenKey)}`;
|
|
53
|
+
}
|
|
49
54
|
return null;
|
|
50
55
|
}
|
|
@@ -3,11 +3,13 @@ import {
|
|
|
3
3
|
CHAT_CONVERSATION_EXCERPT_TOKEN_KIND,
|
|
4
4
|
CHAT_PROJECT_TOKEN_KIND,
|
|
5
5
|
CHAT_SYSTEM_OBJECT_TOKEN_KIND,
|
|
6
|
+
CHAT_UI_RESOURCE_TOKEN_KIND,
|
|
6
7
|
CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
|
|
7
8
|
CHAT_WORKSPACE_EXCERPT_TOKEN_KIND,
|
|
8
9
|
CHAT_WORKSPACE_FILE_TOKEN_KIND,
|
|
9
10
|
type ChatInlineTokenMetadata,
|
|
10
11
|
type ChatSkillSource,
|
|
12
|
+
readChatUiResourceReference,
|
|
11
13
|
} from '@nextclaw/shared';
|
|
12
14
|
|
|
13
15
|
import { serializeChatComposerTokenText } from './chat-composer-token-protocol.utils';
|
|
@@ -83,6 +85,18 @@ function buildStructuredReferenceToken(
|
|
|
83
85
|
}
|
|
84
86
|
: null;
|
|
85
87
|
}
|
|
88
|
+
if (node.tokenKind === CHAT_UI_RESOURCE_TOKEN_KIND) {
|
|
89
|
+
const reference = readChatUiResourceReference(node.data?.reference);
|
|
90
|
+
return reference?.uri === node.tokenKey
|
|
91
|
+
? {
|
|
92
|
+
kind: CHAT_UI_RESOURCE_TOKEN_KIND,
|
|
93
|
+
key: node.tokenKey,
|
|
94
|
+
label: node.label,
|
|
95
|
+
rawText,
|
|
96
|
+
reference,
|
|
97
|
+
}
|
|
98
|
+
: null;
|
|
99
|
+
}
|
|
86
100
|
return undefined;
|
|
87
101
|
}
|
|
88
102
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
CHAT_INLINE_TOKENS_SCHEMA_VERSION,
|
|
5
5
|
CHAT_PROJECT_TOKEN_KIND,
|
|
6
6
|
CHAT_SYSTEM_OBJECT_TOKEN_KIND,
|
|
7
|
+
CHAT_UI_RESOURCE_TOKEN_KIND,
|
|
7
8
|
CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
|
|
8
9
|
CHAT_WORKSPACE_EXCERPT_TOKEN_KIND,
|
|
9
10
|
CHAT_WORKSPACE_FILE_TOKEN_KIND,
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
type ChatInlineTokensMetadata,
|
|
12
13
|
type ChatSkillSource,
|
|
13
14
|
type SystemObjectResolvedReference,
|
|
15
|
+
type ChatUiResourceReference,
|
|
16
|
+
readChatUiResourceReference,
|
|
14
17
|
} from '@nextclaw/shared';
|
|
15
18
|
import { readConversationExcerptInlineToken } from './chat-conversation-excerpt-token.utils';
|
|
16
19
|
import { readSystemObjectResolvedReference } from './chat-system-object-reference.utils';
|
|
@@ -26,6 +29,7 @@ const CHAT_PROJECT_TOKEN_PATTERN = /@project:([^\s]+)/g;
|
|
|
26
29
|
const CHAT_WORKSPACE_FILE_TOKEN_PATTERN = /@file:([^\s]+)/g;
|
|
27
30
|
const CHAT_WORKSPACE_DIRECTORY_TOKEN_PATTERN = /@folder:([^\s]+)/g;
|
|
28
31
|
const CHAT_SYSTEM_OBJECT_TOKEN_PATTERN = /@object:([^\s]+)/g;
|
|
32
|
+
const CHAT_UI_RESOURCE_TOKEN_PATTERN = /@resource:([^\s]+)/g;
|
|
29
33
|
|
|
30
34
|
export type ChatInlineTokenSource =
|
|
31
35
|
| {
|
|
@@ -68,6 +72,13 @@ export type ChatInlineTokenSource =
|
|
|
68
72
|
label: string;
|
|
69
73
|
rawText: string;
|
|
70
74
|
reference: SystemObjectResolvedReference;
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
kind: typeof CHAT_UI_RESOURCE_TOKEN_KIND;
|
|
78
|
+
key: string;
|
|
79
|
+
label: string;
|
|
80
|
+
rawText: string;
|
|
81
|
+
reference: ChatUiResourceReference;
|
|
71
82
|
};
|
|
72
83
|
|
|
73
84
|
export function resolveWorkspaceReferencePath(params: {
|
|
@@ -124,6 +135,7 @@ function appendEncodedKeyTokens(params: {
|
|
|
124
135
|
kind:
|
|
125
136
|
| typeof CHAT_PROJECT_TOKEN_KIND
|
|
126
137
|
| typeof CHAT_SYSTEM_OBJECT_TOKEN_KIND
|
|
138
|
+
| typeof CHAT_UI_RESOURCE_TOKEN_KIND
|
|
127
139
|
| typeof CHAT_WORKSPACE_FILE_TOKEN_KIND
|
|
128
140
|
| typeof CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND;
|
|
129
141
|
pattern: RegExp;
|
|
@@ -177,6 +189,12 @@ export function buildInlineTokensFromTextProtocol(text: string): ChatInlineToken
|
|
|
177
189
|
text,
|
|
178
190
|
tokens,
|
|
179
191
|
});
|
|
192
|
+
appendEncodedKeyTokens({
|
|
193
|
+
kind: CHAT_UI_RESOURCE_TOKEN_KIND,
|
|
194
|
+
pattern: CHAT_UI_RESOURCE_TOKEN_PATTERN,
|
|
195
|
+
text,
|
|
196
|
+
tokens,
|
|
197
|
+
});
|
|
180
198
|
appendEncodedKeyTokens({
|
|
181
199
|
kind: CHAT_WORKSPACE_FILE_TOKEN_KIND,
|
|
182
200
|
pattern: CHAT_WORKSPACE_FILE_TOKEN_PATTERN,
|
|
@@ -269,6 +287,13 @@ function readInlineTokenEntry(entry: unknown): ChatInlineTokenSource | null {
|
|
|
269
287
|
? { kind, key, label, rawText, reference }
|
|
270
288
|
: null;
|
|
271
289
|
}
|
|
290
|
+
if (kind === CHAT_UI_RESOURCE_TOKEN_KIND) {
|
|
291
|
+
const key = readOptionalString(entry.key);
|
|
292
|
+
const reference = readChatUiResourceReference(entry.reference);
|
|
293
|
+
return key && reference && reference.uri === key
|
|
294
|
+
? { kind, key, label, rawText, reference }
|
|
295
|
+
: null;
|
|
296
|
+
}
|
|
272
297
|
const key = readOptionalString(entry.key);
|
|
273
298
|
return key ? { kind, key, rawText, label } : null;
|
|
274
299
|
}
|
|
@@ -2,10 +2,12 @@ import { useCallback } from "react";
|
|
|
2
2
|
import type { ChatInlineTokenViewModel } from "@nextclaw/agent-chat-ui";
|
|
3
3
|
import {
|
|
4
4
|
CHAT_PROJECT_TOKEN_KIND,
|
|
5
|
+
CHAT_UI_RESOURCE_TOKEN_KIND,
|
|
5
6
|
CHAT_WORKSPACE_EXCERPT_TOKEN_KIND,
|
|
6
7
|
} from "@nextclaw/shared";
|
|
7
8
|
import { toast } from "sonner";
|
|
8
9
|
import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
|
|
10
|
+
import { useAppPresenter } from "@/app/components/app-presenter-provider";
|
|
9
11
|
import { resolveWorkspaceReferencePath } from "@/features/chat/features/input/utils/chat-inline-token.utils";
|
|
10
12
|
import { fetchNcpSessionSkills } from "@/shared/lib/api";
|
|
11
13
|
import { t } from "@/shared/lib/i18n";
|
|
@@ -21,9 +23,14 @@ export function useChatInlineTokenActions(params: {
|
|
|
21
23
|
}) {
|
|
22
24
|
const { selectedSession, sessionKey } = params;
|
|
23
25
|
const presenter = usePresenter();
|
|
26
|
+
const appPresenter = useAppPresenter();
|
|
24
27
|
const projectRoot = selectedSession?.projectRoot ?? selectedSession?.workingDir;
|
|
25
28
|
|
|
26
29
|
const handleInlineTokenClick = useCallback((token: ChatInlineTokenViewModel) => {
|
|
30
|
+
if (token.kind === CHAT_UI_RESOURCE_TOKEN_KIND && "key" in token) {
|
|
31
|
+
appPresenter.docBrowserManager.open(token.key);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
27
34
|
if (token.kind === "panel_app" && "key" in token) {
|
|
28
35
|
void presenter.chatUiManager.showContent({
|
|
29
36
|
target: { type: "panel_app", payload: { appId: token.key } },
|
|
@@ -98,7 +105,7 @@ export function useChatInlineTokenActions(params: {
|
|
|
98
105
|
previewViewer: "rendered",
|
|
99
106
|
});
|
|
100
107
|
}).catch(() => toast.error(t("chatSkillPreviewUnavailable")));
|
|
101
|
-
}, [presenter, projectRoot, selectedSession?.projectRoot, sessionKey]);
|
|
108
|
+
}, [appPresenter.docBrowserManager, presenter, projectRoot, selectedSession?.projectRoot, sessionKey]);
|
|
102
109
|
|
|
103
110
|
const handleAttachmentOpen = useCallback((file: {
|
|
104
111
|
label: string;
|
|
@@ -5,6 +5,7 @@ import { ChatSessionWorkspacePanelContent } from '@/features/chat/features/works
|
|
|
5
5
|
import type { ResolvedChildSessionTab } from '@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view';
|
|
6
6
|
import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
|
|
7
7
|
import { CHAT_WORKSPACE_EXPLORER_DEFAULT_WIDTH } from '@/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils';
|
|
8
|
+
import { useWorkspaceProjectTreeStore } from '@/features/chat/features/workspace/stores/workspace-project-tree.store';
|
|
8
9
|
|
|
9
10
|
const mocks = vi.hoisted(() => ({
|
|
10
11
|
browseEntries: [
|
|
@@ -27,7 +28,9 @@ const mocks = vi.hoisted(() => ({
|
|
|
27
28
|
deleteServerPathEntry: vi.fn(),
|
|
28
29
|
renameServerPathEntry: vi.fn(),
|
|
29
30
|
uploadServerPathFiles: vi.fn(),
|
|
31
|
+
watchDirectories: vi.fn(),
|
|
30
32
|
refetchBrowse: vi.fn(),
|
|
33
|
+
refetchQueries: vi.fn(),
|
|
31
34
|
requestDirectoryReference: vi.fn(),
|
|
32
35
|
requestFileReference: vi.fn(),
|
|
33
36
|
openChildSessions: vi.fn(),
|
|
@@ -41,6 +44,15 @@ const mocks = vi.hoisted(() => ({
|
|
|
41
44
|
scrollIntoView: vi.fn(),
|
|
42
45
|
}));
|
|
43
46
|
|
|
47
|
+
vi.mock('@/shared/hooks/use-server-path-watch', () => ({
|
|
48
|
+
useServerPathWatch: mocks.watchDirectories,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock('@tanstack/react-query', async (importOriginal) => ({
|
|
52
|
+
...(await importOriginal()),
|
|
53
|
+
useQueryClient: () => ({ refetchQueries: mocks.refetchQueries }),
|
|
54
|
+
}));
|
|
55
|
+
|
|
44
56
|
function firePointerEvent(
|
|
45
57
|
target: Window | Document | Node | Element,
|
|
46
58
|
type: string,
|
|
@@ -168,6 +180,8 @@ beforeEach(() => {
|
|
|
168
180
|
overwritten: false,
|
|
169
181
|
});
|
|
170
182
|
mocks.refetchBrowse.mockResolvedValue(undefined);
|
|
183
|
+
mocks.refetchQueries.mockResolvedValue(undefined);
|
|
184
|
+
useWorkspaceProjectTreeStore.setState({ trees: {} });
|
|
171
185
|
useChatThreadStore.getState().setSnapshot({
|
|
172
186
|
workspaceExplorerOpen: false,
|
|
173
187
|
workspaceExplorerWidth: CHAT_WORKSPACE_EXPLORER_DEFAULT_WIDTH,
|
|
@@ -475,7 +489,7 @@ it('creates a project folder from the root toolbar and refreshes the tree', asyn
|
|
|
475
489
|
parentPath: '/Users/peiwang/Projects/nextbot',
|
|
476
490
|
name: 'research',
|
|
477
491
|
});
|
|
478
|
-
expect(mocks.
|
|
492
|
+
expect(mocks.refetchQueries).toHaveBeenCalled();
|
|
479
493
|
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'research' }));
|
|
480
494
|
expect(mocks.scrollIntoView).toHaveBeenCalledTimes(2);
|
|
481
495
|
});
|
|
@@ -564,7 +578,7 @@ it('uploads multiple project files from the root actions menu', async () => {
|
|
|
564
578
|
files: [file],
|
|
565
579
|
overwrite: undefined,
|
|
566
580
|
});
|
|
567
|
-
expect(mocks.
|
|
581
|
+
expect(mocks.refetchQueries).toHaveBeenCalled();
|
|
568
582
|
});
|
|
569
583
|
});
|
|
570
584
|
|
|
@@ -594,13 +608,62 @@ it('requires destructive confirmation before deleting a project file', async ()
|
|
|
594
608
|
path: '/private/resolved-worktree/package.json',
|
|
595
609
|
});
|
|
596
610
|
expect(mocks.removeWorkspacePath).toHaveBeenCalledWith('/private/resolved-worktree/package.json');
|
|
597
|
-
expect(mocks.
|
|
611
|
+
expect(mocks.refetchQueries).toHaveBeenCalled();
|
|
598
612
|
});
|
|
599
613
|
});
|
|
600
614
|
|
|
601
|
-
it('
|
|
615
|
+
it('refreshes the root and every active expanded directory query', async () => {
|
|
602
616
|
const user = userEvent.setup();
|
|
603
|
-
|
|
617
|
+
render(
|
|
618
|
+
<ChatSessionWorkspacePanelContent
|
|
619
|
+
activeSelection={{ kind: 'project-files' }}
|
|
620
|
+
childSessionTabs={[]}
|
|
621
|
+
filePreviewRefreshVersion={0}
|
|
622
|
+
sessionKey="parent-1"
|
|
623
|
+
sessionCronJobs={[]}
|
|
624
|
+
sessionProjectRoot="/Users/peiwang/Projects/nextbot"
|
|
625
|
+
sessionWorkingDir="/Users/peiwang/Projects/nextbot"
|
|
626
|
+
/>,
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
await user.click(screen.getByRole('button', { name: 'src' }));
|
|
630
|
+
await user.click(screen.getByRole('button', { name: 'Refresh project files' }));
|
|
631
|
+
|
|
632
|
+
expect(mocks.refetchQueries).toHaveBeenCalledTimes(1);
|
|
633
|
+
const request = mocks.refetchQueries.mock.calls[0]?.[0] as {
|
|
634
|
+
predicate: (query: { queryKey: readonly unknown[] }) => boolean;
|
|
635
|
+
type: string;
|
|
636
|
+
};
|
|
637
|
+
expect(request.type).toBe('active');
|
|
638
|
+
expect(request.predicate({ queryKey: ['server-path-browse', '/Users/peiwang/Projects/nextbot', '', true] })).toBe(true);
|
|
639
|
+
expect(request.predicate({ queryKey: ['server-path-browse', '/Users/peiwang/Projects/nextbot/src', '', true] })).toBe(true);
|
|
640
|
+
expect(request.predicate({ queryKey: ['server-path-browse', '/tmp/another-project', '', true] })).toBe(false);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('collapses all persisted directories in one action', async () => {
|
|
644
|
+
const user = userEvent.setup();
|
|
645
|
+
render(
|
|
646
|
+
<ChatSessionWorkspacePanelContent
|
|
647
|
+
activeSelection={{ kind: 'project-files' }}
|
|
648
|
+
childSessionTabs={[]}
|
|
649
|
+
filePreviewRefreshVersion={0}
|
|
650
|
+
sessionKey="parent-1"
|
|
651
|
+
sessionCronJobs={[]}
|
|
652
|
+
sessionProjectRoot="/Users/peiwang/Projects/nextbot"
|
|
653
|
+
sessionWorkingDir="/Users/peiwang/Projects/nextbot"
|
|
654
|
+
/>,
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
const sourceDirectory = screen.getByRole('treeitem', { name: 'Open directory: src' });
|
|
658
|
+
await user.click(screen.getByRole('button', { name: 'src' }));
|
|
659
|
+
expect(sourceDirectory.getAttribute('aria-expanded')).toBe('true');
|
|
660
|
+
await user.click(screen.getByRole('button', { name: 'Collapse all' }));
|
|
661
|
+
expect(sourceDirectory.getAttribute('aria-expanded')).toBe('false');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('restores expansion and scroll from project view state after remounting', async () => {
|
|
665
|
+
const user = userEvent.setup();
|
|
666
|
+
const props = {
|
|
604
667
|
activeSelection: { kind: 'project-files' as const },
|
|
605
668
|
childSessionTabs: [],
|
|
606
669
|
filePreviewRefreshVersion: 0,
|
|
@@ -609,22 +672,16 @@ it('preserves project tree expansion and scroll while visiting another workspace
|
|
|
609
672
|
sessionProjectRoot: '/Users/peiwang/Projects/nextbot',
|
|
610
673
|
sessionWorkingDir: '/Users/peiwang/Projects/nextbot',
|
|
611
674
|
};
|
|
612
|
-
const
|
|
613
|
-
const tree = screen.getByRole('tree', { name: 'Project files' });
|
|
614
|
-
const sourceDirectory = screen.getByRole('treeitem', {
|
|
615
|
-
name: 'Open directory: src',
|
|
616
|
-
});
|
|
675
|
+
const first = render(<ChatSessionWorkspacePanelContent {...props} />);
|
|
617
676
|
await user.click(screen.getByRole('button', { name: 'src' }));
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
expect(screen.
|
|
625
|
-
expect(
|
|
626
|
-
expect(sourceDirectory.getAttribute('aria-expanded')).toBe('true');
|
|
627
|
-
expect(tree.scrollTop).toBe(128);
|
|
677
|
+
const firstTree = screen.getByRole('tree', { name: 'Project files' });
|
|
678
|
+
firstTree.scrollTop = 144;
|
|
679
|
+
fireEvent.scroll(firstTree);
|
|
680
|
+
first.unmount();
|
|
681
|
+
|
|
682
|
+
render(<ChatSessionWorkspacePanelContent {...props} />);
|
|
683
|
+
expect(screen.getAllByRole('treeitem', { name: 'Open directory: src' })[0]?.getAttribute('aria-expanded')).toBe('true');
|
|
684
|
+
expect(screen.getByRole('tree', { name: 'Project files' }).scrollTop).toBe(144);
|
|
628
685
|
});
|
|
629
686
|
|
|
630
687
|
it('selects and reveals the active file when it is opened outside the Explorer', async () => {
|