@nextclaw/ui 0.15.19 → 0.15.21
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 +40 -0
- package/dist/assets/api-CHo2gekJ.js +13 -0
- package/dist/assets/app-presenter-provider-2_DmWYT9.js +108 -0
- package/dist/assets/appearance-settings-page-BEy2UE7R.js +1 -0
- package/dist/assets/{book-open-HtUSPwoS.js → book-open-BrZL4o9L.js} +1 -1
- package/dist/assets/card-B5rRLu_6.js +1 -0
- package/dist/assets/{channels-list-page-Bspr1Jp4.js → channels-list-page-C1dnm68I.js} +2 -2
- package/dist/assets/chat-page-Dz6tvZ91.js +1 -0
- package/dist/assets/config-split-page-6y6W6dz4.js +1 -0
- package/dist/assets/confirm-dialog-q1CpEmR8.js +5 -0
- package/dist/assets/desktop-update-config-DrdlwP4j.js +1 -0
- package/dist/assets/{dist-CZx9Q6jc.js → dist-C9EX46wM.js} +1 -1
- package/dist/assets/doc-browser-BfttDSti.js +1 -0
- package/dist/assets/{doc-browser-B4l0Xu7u.js → doc-browser-DH-3hDnI.js} +1 -1
- package/dist/assets/{doc-browser-context-Dy7EhLI8.js → doc-browser-context-TU2heon0.js} +1 -1
- package/dist/assets/{eye-DU7p1-7X.js → eye-B-3dyZNA.js} +1 -1
- package/dist/assets/form-actions-DvvxCpL4.js +1 -0
- package/dist/assets/index-CXDPLDFD.css +1 -0
- package/dist/assets/index-k_eWScvN.js +2 -0
- package/dist/assets/loader-circle-B35NlHTI.js +1 -0
- package/dist/assets/logo-badge-DB51c0cS.js +1 -0
- package/dist/assets/mcp-marketplace-page-CkPtwPBk.js +1 -0
- package/dist/assets/{mcp-marketplace-page-Dg03SZX1.js → mcp-marketplace-page-DWtsIqml.js} +3 -3
- package/dist/assets/{model-config-page-DOBqLZoQ.js → model-config-page-D6cGNnMM.js} +1 -1
- package/dist/assets/{navigation-link-Ct6xtnVM.js → navigation-link-BSJEUiwD.js} +1 -1
- package/dist/assets/plus-BRVe5jSJ.js +1 -0
- package/dist/assets/{popover-C2oIpGBF.js → popover-Bp9IpE_N.js} +1 -1
- package/dist/assets/provider-scoped-model-input-dk7fz9NL.js +1 -0
- package/dist/assets/{providers-config-page-BAVPJKpx.js → providers-config-page-DTP2YfxA.js} +1 -1
- package/dist/assets/react-D6ShNq95.js +8 -0
- package/dist/assets/{refresh-cw-BZ8giusN.js → refresh-cw-C2e94pmp.js} +1 -1
- package/dist/assets/remote-FllhXW3o.js +1 -0
- package/dist/assets/rotate-cw-CTC-Qszi.js +1 -0
- package/dist/assets/runtime-config-page-C57OLdPm.js +1 -0
- package/dist/assets/{save-CSzFGRse.js → save-CkwDOgvt.js} +1 -1
- package/dist/assets/{search-BmSWsiG2.js → search-CLXM2reO.js} +1 -1
- package/dist/assets/{search-config-page-CrL1pUzy.js → search-config-page-tnJXaaSr.js} +1 -1
- package/dist/assets/{secrets-config-page-BlgsD0-L.js → secrets-config-page-DQ_Curaj.js} +1 -1
- package/dist/assets/security-config-S00DjFfO.js +1 -0
- package/dist/assets/{select-CklWb6Rs.js → select-CNFBf00l.js} +1 -1
- package/dist/assets/{settings-2-BSZf8327.js → settings-2-B9IT2HGd.js} +1 -1
- package/dist/assets/skeleton-Dc06gatK.js +1 -0
- package/dist/assets/{tabs-Cah5klko.js → tabs-BuNr8LSO.js} +1 -1
- package/dist/assets/{tag-chip-DcrirDgN.js → tag-chip-CDkB5smU.js} +1 -1
- package/dist/assets/{trash-2-BLQOULWD.js → trash-2-GchaJuEM.js} +1 -1
- package/dist/assets/{use-config-CuFIsUVG.js → use-config-C69rR15i.js} +1 -1
- package/dist/assets/use-confirm-dialog-DF_EgPYx.js +1 -0
- package/dist/assets/{use-viewport-layout-DU29bUJS.js → use-viewport-layout-BACNZyMY.js} +1 -1
- package/dist/assets/x-BXFquY9e.js +1 -0
- package/dist/index.html +28 -28
- package/package.json +9 -9
- package/src/app/components/__tests__/app-notification-runtime.test.tsx +32 -0
- package/src/app/components/app-notification-runtime.tsx +15 -0
- package/src/app/components/layout/sidebar-items.tsx +23 -9
- package/src/app/configs/app-navigation.config.ts +21 -0
- package/src/app/index.tsx +22 -2
- package/src/app/presenters/app.presenter.ts +8 -0
- package/src/features/chat/components/layout/chat-page-shell.tsx +7 -2
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +12 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +28 -0
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +4 -5
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +13 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +31 -8
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +79 -9
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +16 -4
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -2
- package/src/features/chat/features/input/input-surface-plugins/context-reference-items.utils.ts +240 -0
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +62 -173
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +29 -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.utils.ts +26 -5
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +12 -1
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +9 -5
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-completion-notification.manager.test.ts +177 -0
- package/src/features/chat/managers/chat-completion-notification.manager.ts +146 -0
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +26 -0
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +29 -2
- package/src/features/chat/pages/ncp-chat-page.tsx +9 -0
- package/src/features/inbox/components/inbox-delivery-content.test.tsx +57 -0
- package/src/features/inbox/components/inbox-delivery-content.tsx +118 -0
- package/src/features/inbox/components/inbox-reader-dialog.test.tsx +77 -0
- package/src/features/inbox/components/inbox-reader-dialog.tsx +224 -0
- package/src/features/inbox/components/inbox-runtime.tsx +14 -0
- package/src/features/inbox/hooks/use-inbox-deliveries.ts +14 -0
- package/src/features/inbox/index.ts +7 -0
- package/src/features/inbox/managers/inbox.manager.test.ts +86 -0
- package/src/features/inbox/managers/inbox.manager.ts +178 -0
- package/src/features/inbox/pages/inbox-page.test.ts +28 -0
- package/src/features/inbox/pages/inbox-page.tsx +385 -0
- package/src/features/inbox/stores/inbox.store.ts +23 -0
- package/src/features/notifications/components/__tests__/app-notification-toast.test.tsx +48 -0
- package/src/features/notifications/components/app-notification-toast.tsx +80 -0
- package/src/features/notifications/index.ts +8 -0
- package/src/features/notifications/managers/__tests__/app-notification.manager.test.ts +75 -0
- package/src/features/notifications/managers/app-notification.manager.ts +41 -0
- package/src/platforms/mobile/components/mobile-bottom-nav.tsx +14 -7
- package/src/shared/hooks/use-projects.ts +2 -1
- package/src/shared/lib/i18n/index.ts +4 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -4
- package/src/shared/lib/i18n/locales/en-US/inbox.json +30 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -4
- package/src/shared/lib/i18n/locales/zh-CN/inbox.json +30 -0
- package/dist/assets/api-O5yEgzBt.js +0 -13
- package/dist/assets/app-presenter-provider-DXTYtH78.js +0 -106
- package/dist/assets/appearance-settings-page-BuGxSvlJ.js +0 -1
- package/dist/assets/card-GkBc-UMw.js +0 -1
- package/dist/assets/chat-page-BKQ89f4s.js +0 -1
- package/dist/assets/config-split-page-CvLXSFSC.js +0 -1
- package/dist/assets/confirm-dialog-BVH7haN0.js +0 -5
- package/dist/assets/desktop-update-config-C9ZUI8UW.js +0 -1
- package/dist/assets/doc-browser-DsVF1DrS.js +0 -1
- package/dist/assets/form-actions-aM9_jSJC.js +0 -1
- package/dist/assets/index-6pHX0DZH.js +0 -2
- package/dist/assets/index-DN6KM7WD.css +0 -1
- package/dist/assets/loader-circle-Dd6jw700.js +0 -1
- package/dist/assets/logo-badge-2-iV1pkr.js +0 -1
- package/dist/assets/mcp-marketplace-page-C-d0RMfg.js +0 -1
- package/dist/assets/plus-CsgeRId8.js +0 -1
- package/dist/assets/provider-scoped-model-input-DDqCdnTU.js +0 -1
- package/dist/assets/react-CeMr9V52.js +0 -8
- package/dist/assets/remote-C-r0SHcz.js +0 -1
- package/dist/assets/rotate-cw-DVdRfMSO.js +0 -1
- package/dist/assets/runtime-config-page-D0244uxJ.js +0 -1
- package/dist/assets/security-config-cDrdpMJW.js +0 -1
- package/dist/assets/skeleton-DG9u1BmA.js +0 -1
- package/dist/assets/use-confirm-dialog-CcySmkWp.js +0 -1
- package/dist/assets/x-BVoeXny1.js +0 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
AlarmClock,
|
|
6
6
|
Cpu,
|
|
7
7
|
Download,
|
|
8
|
+
Inbox,
|
|
8
9
|
KeyRound,
|
|
9
10
|
MessageCircle,
|
|
10
11
|
MessageSquare,
|
|
@@ -44,6 +45,8 @@ export function isMainWorkspaceRoute(pathname: string): boolean {
|
|
|
44
45
|
return (
|
|
45
46
|
normalized === "/chat" ||
|
|
46
47
|
normalized.startsWith("/chat/") ||
|
|
48
|
+
normalized === "/inbox" ||
|
|
49
|
+
normalized.startsWith("/inbox/") ||
|
|
47
50
|
normalized === "/skills" ||
|
|
48
51
|
normalized.startsWith("/skills/") ||
|
|
49
52
|
normalized === "/cron" ||
|
|
@@ -67,6 +70,11 @@ export function getMobileBottomNavItems(
|
|
|
67
70
|
label: translate("chat"),
|
|
68
71
|
icon: MessageCircle,
|
|
69
72
|
},
|
|
73
|
+
{
|
|
74
|
+
target: "/inbox",
|
|
75
|
+
label: translate("inboxTitle"),
|
|
76
|
+
icon: Inbox,
|
|
77
|
+
},
|
|
70
78
|
{
|
|
71
79
|
target: "/skills",
|
|
72
80
|
label: translate("marketplaceFilterSkills"),
|
|
@@ -94,6 +102,11 @@ export function getMainSidebarNavItems(
|
|
|
94
102
|
label: translate("chat"),
|
|
95
103
|
icon: MessageCircle,
|
|
96
104
|
},
|
|
105
|
+
{
|
|
106
|
+
target: "/inbox",
|
|
107
|
+
label: translate("inboxTitle"),
|
|
108
|
+
icon: Inbox,
|
|
109
|
+
},
|
|
97
110
|
{
|
|
98
111
|
target: "/chat/cron",
|
|
99
112
|
label: translate("cron"),
|
|
@@ -227,6 +240,14 @@ export function resolveMobileRouteMeta(
|
|
|
227
240
|
};
|
|
228
241
|
}
|
|
229
242
|
|
|
243
|
+
if (normalized === "/inbox" || normalized.startsWith("/inbox/")) {
|
|
244
|
+
return {
|
|
245
|
+
title: translate("inboxTitle"),
|
|
246
|
+
backTarget: normalized === "/inbox" ? null : "/inbox",
|
|
247
|
+
backLabel: translate("inboxTitle"),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
230
251
|
if (normalized === "/skills" || normalized.startsWith("/skills/")) {
|
|
231
252
|
return {
|
|
232
253
|
title: translate("marketplaceFilterSkills"),
|
package/src/app/index.tsx
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { lazy, Suspense, useEffect } from "react";
|
|
2
|
-
import type { ReactElement } from "react";
|
|
2
|
+
import type { CSSProperties, ReactElement } from "react";
|
|
3
3
|
import { QueryClientProvider } from "@tanstack/react-query";
|
|
4
4
|
import { Navigate, Route, Routes } from "react-router-dom";
|
|
5
5
|
import { Toaster } from "sonner";
|
|
6
6
|
import { appQueryClient } from "@/app-query-client";
|
|
7
7
|
import { AppPresenterProvider } from "@/app/components/app-presenter-provider";
|
|
8
|
+
import { AppNotificationRuntime } from "@/app/components/app-notification-runtime";
|
|
8
9
|
import { AppLayout } from "@/app/components/layout/app-layout";
|
|
10
|
+
import { SIDEBAR_RAIL_WIDTH_PX } from "@/app/components/layout/sidebar-rail.styles";
|
|
9
11
|
import { SettingsEntryPage } from "@/app/components/layout/settings-entry-page";
|
|
10
12
|
import { LoginPage } from "@/components/auth/login-page";
|
|
11
13
|
import { AccountPanel } from "@/features/account";
|
|
14
|
+
import { InboxRuntime } from "@/features/inbox";
|
|
12
15
|
import { ServiceActionAuthorizationDialog } from "@/features/service-apps";
|
|
13
16
|
import { runtimeUpdateManager, useSystemStatusSources } from "@/features/system-status";
|
|
14
17
|
import {
|
|
@@ -21,6 +24,8 @@ import {
|
|
|
21
24
|
} from "@/pwa/components/pwa-install-entry";
|
|
22
25
|
import { startNextClawPwa } from "@/pwa/register-pwa";
|
|
23
26
|
|
|
27
|
+
const NOTIFICATION_TOASTER_STYLE = { "--width": "320px" } as CSSProperties;
|
|
28
|
+
|
|
24
29
|
const ModelConfigPage = lazy(async () => ({
|
|
25
30
|
default: (await import("@/features/settings/pages/model-config-page"))
|
|
26
31
|
.ModelConfigPage,
|
|
@@ -117,6 +122,10 @@ const protectedRouteDefinitions: ProtectedRouteDefinition[] = [
|
|
|
117
122
|
path: "/cron",
|
|
118
123
|
element: createLazyElement(<ChatPage view="cron" />),
|
|
119
124
|
},
|
|
125
|
+
{
|
|
126
|
+
path: "/inbox/:deliveryId?",
|
|
127
|
+
element: createLazyElement(<ChatPage view="inbox" />),
|
|
128
|
+
},
|
|
120
129
|
{
|
|
121
130
|
path: "/appearance",
|
|
122
131
|
element: createLazyElement(<AppearanceSettingsPage />),
|
|
@@ -219,6 +228,8 @@ function ProtectedApp() {
|
|
|
219
228
|
|
|
220
229
|
return (
|
|
221
230
|
<AppPresenterProvider>
|
|
231
|
+
<AppNotificationRuntime />
|
|
232
|
+
<InboxRuntime />
|
|
222
233
|
<AppLayout>
|
|
223
234
|
<ProtectedRoutes />
|
|
224
235
|
</AppLayout>
|
|
@@ -257,7 +268,16 @@ export default function AppContent() {
|
|
|
257
268
|
<QueryClientProvider client={appQueryClient}>
|
|
258
269
|
<AuthGate />
|
|
259
270
|
<PwaInstallBanner />
|
|
260
|
-
<Toaster
|
|
271
|
+
<Toaster
|
|
272
|
+
position="top-right"
|
|
273
|
+
richColors
|
|
274
|
+
offset={{
|
|
275
|
+
top: window.nextclawDesktop?.platform === "win32" ? 56 : 44,
|
|
276
|
+
right: SIDEBAR_RAIL_WIDTH_PX + 16,
|
|
277
|
+
}}
|
|
278
|
+
mobileOffset={window.nextclawDesktop?.platform === "win32" ? 56 : undefined}
|
|
279
|
+
style={NOTIFICATION_TOASTER_STYLE}
|
|
280
|
+
/>
|
|
261
281
|
</QueryClientProvider>
|
|
262
282
|
);
|
|
263
283
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { AccountManager } from '@/features/account';
|
|
2
2
|
import { viewportLayoutManager } from '@/app/managers/viewport-layout.manager';
|
|
3
3
|
import {
|
|
4
|
+
ChatCompletionNotificationManager,
|
|
4
5
|
ChatDraftIntentManager,
|
|
5
6
|
useChatThreadStore,
|
|
6
7
|
} from '@/features/chat';
|
|
8
|
+
import { AppNotificationManager } from '@/features/notifications';
|
|
7
9
|
import { PanelAppBridgeManager } from '@/features/panel-apps';
|
|
10
|
+
import { InboxManager } from '@/features/inbox';
|
|
8
11
|
import { RightPanelResourceRouteResolver } from '@/features/right-panel-resources';
|
|
9
12
|
import { RemoteAccessManager } from '@/features/remote';
|
|
10
13
|
import { ServiceActionAuthorizationManager } from '@/features/service-apps';
|
|
@@ -19,6 +22,11 @@ function isChatWorkspacePanelOpen(snapshot: ChatThreadSnapshot): boolean {
|
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export class AppPresenter {
|
|
25
|
+
notificationManager = new AppNotificationManager();
|
|
26
|
+
inboxManager = new InboxManager();
|
|
27
|
+
chatCompletionNotificationManager = new ChatCompletionNotificationManager(
|
|
28
|
+
this.notificationManager,
|
|
29
|
+
);
|
|
22
30
|
accountManager = new AccountManager();
|
|
23
31
|
rightPanelResourceRouteResolver = new RightPanelResourceRouteResolver();
|
|
24
32
|
notifyRightPanelOpened = () => {
|
|
@@ -6,7 +6,8 @@ import { CronConfig } from "@/features/cron";
|
|
|
6
6
|
import { MarketplacePage } from "@/features/marketplace";
|
|
7
7
|
import { useViewportLayout } from "@/app/hooks/use-viewport-layout";
|
|
8
8
|
import { ChatMobileShell } from "@/platforms/mobile";
|
|
9
|
-
|
|
9
|
+
import { InboxPage } from "@/features/inbox";
|
|
10
|
+
export type MainPanelView = "chat" | "cron" | "skills" | "agents" | "inbox";
|
|
10
11
|
export type ChatPageProps = {
|
|
11
12
|
view: MainPanelView;
|
|
12
13
|
};
|
|
@@ -51,7 +52,11 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
|
|
|
51
52
|
isMobile ? <ChatMobileShell /> : <ChatConversationPanel />
|
|
52
53
|
) : (
|
|
53
54
|
<section className="flex-1 min-h-0 overflow-hidden bg-background">
|
|
54
|
-
{view === "
|
|
55
|
+
{view === "inbox" ? (
|
|
56
|
+
<div className="mx-auto flex h-full min-h-0 w-full max-w-[min(1180px,100%)] flex-col py-4 sm:px-6 sm:py-5">
|
|
57
|
+
<InboxPage />
|
|
58
|
+
</div>
|
|
59
|
+
) : view === "cron" ? (
|
|
55
60
|
<div className="h-full overflow-auto custom-scrollbar">
|
|
56
61
|
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
|
|
57
62
|
<CronConfig />
|
|
@@ -16,10 +16,12 @@ import {
|
|
|
16
16
|
AlarmClock,
|
|
17
17
|
Bot,
|
|
18
18
|
BrainCircuit,
|
|
19
|
+
Inbox,
|
|
19
20
|
PanelLeftClose,
|
|
20
21
|
PanelLeftOpen,
|
|
21
22
|
Plus,
|
|
22
23
|
} from "lucide-react";
|
|
24
|
+
import { useInboxUnreadCount } from "@/features/inbox";
|
|
23
25
|
import { ChatSidebarSessionList } from "@/features/chat/features/session/components/chat-sidebar-session-list";
|
|
24
26
|
import { ChatSidebarUtilityMenu } from "@/features/chat/components/layout/chat-sidebar-utility-menu";
|
|
25
27
|
import { isWindowsDesktopHost } from "@/platforms/desktop";
|
|
@@ -34,6 +36,11 @@ import {
|
|
|
34
36
|
} from "@/app/components/layout/sidebar-rail.styles";
|
|
35
37
|
|
|
36
38
|
const navItems = [
|
|
39
|
+
{
|
|
40
|
+
target: "/inbox",
|
|
41
|
+
label: () => t("inboxTitle"),
|
|
42
|
+
icon: Inbox,
|
|
43
|
+
},
|
|
37
44
|
{
|
|
38
45
|
target: "/cron",
|
|
39
46
|
label: () => t("chatSidebarScheduledTasks"),
|
|
@@ -99,6 +106,7 @@ export function ChatSidebarDesktopNav({
|
|
|
99
106
|
}: {
|
|
100
107
|
isCollapsed: boolean;
|
|
101
108
|
}) {
|
|
109
|
+
const unreadCount = useInboxUnreadCount();
|
|
102
110
|
return (
|
|
103
111
|
<>
|
|
104
112
|
<div className={cn("pb-2", isCollapsed ? "px-0" : "px-3")}>
|
|
@@ -114,6 +122,10 @@ export function ChatSidebarDesktopNav({
|
|
|
114
122
|
icon={item.icon}
|
|
115
123
|
density="compact"
|
|
116
124
|
collapsed={isCollapsed}
|
|
125
|
+
indicator={item.target === "/inbox" && unreadCount > 0}
|
|
126
|
+
trailing={item.target === "/inbox" && unreadCount > 0
|
|
127
|
+
? unreadCount > 99 ? "99+" : unreadCount
|
|
128
|
+
: undefined}
|
|
117
129
|
/>
|
|
118
130
|
</li>
|
|
119
131
|
))}
|
package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx
CHANGED
|
@@ -82,4 +82,32 @@ describe('useSessionRunQueue', () => {
|
|
|
82
82
|
await waitFor(() => expect(listQueuedInputs).toHaveBeenCalledTimes(2));
|
|
83
83
|
unmount();
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
it('replaces an in-flight stale queue read after the queue changes', async () => {
|
|
87
|
+
let resolveInitialRead!: (queue: UiNcpSessionQueuedInputsView) => void;
|
|
88
|
+
const initialRead = new Promise<UiNcpSessionQueuedInputsView>((resolve) => {
|
|
89
|
+
resolveInitialRead = resolve;
|
|
90
|
+
});
|
|
91
|
+
const listQueuedInputs = vi.spyOn(nextclawClient.sessions, 'listQueuedInputs')
|
|
92
|
+
.mockImplementationOnce(async () => await initialRead)
|
|
93
|
+
.mockResolvedValue({ sessionId: 'session-1', inputs: [] });
|
|
94
|
+
const { result } = renderHook(
|
|
95
|
+
() => useSessionRunQueue('session-1'),
|
|
96
|
+
{ wrapper: createWrapper() },
|
|
97
|
+
);
|
|
98
|
+
await waitFor(() => expect(listQueuedInputs).toHaveBeenCalledTimes(1));
|
|
99
|
+
|
|
100
|
+
act(() => {
|
|
101
|
+
nextclawClient.eventBus.emit(eventKeys.sessionRunQueueUpdated, {
|
|
102
|
+
sessionKey: 'session-1',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
await waitFor(() => expect(listQueuedInputs).toHaveBeenCalledTimes(2));
|
|
106
|
+
|
|
107
|
+
await act(async () => {
|
|
108
|
+
resolveInitialRead(createQueue('session-1', 'stale queued input'));
|
|
109
|
+
await initialRead;
|
|
110
|
+
});
|
|
111
|
+
await waitFor(() => expect(result.current.inputs).toEqual([]));
|
|
112
|
+
});
|
|
85
113
|
});
|
|
@@ -21,14 +21,13 @@ export function useSessionRunQueue(sessionKey: string | null) {
|
|
|
21
21
|
if (!normalizedSessionKey) {
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
24
|
-
return nextclawClient.eventBus.on(eventKeys.sessionRunQueueUpdated, ({ sessionKey: updatedSessionKey }) => {
|
|
24
|
+
return nextclawClient.eventBus.on(eventKeys.sessionRunQueueUpdated, async ({ sessionKey: updatedSessionKey }) => {
|
|
25
25
|
if (updatedSessionKey !== normalizedSessionKey) {
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
28
|
+
const queryKey = [SESSION_RUN_QUEUE_QUERY_KEY, normalizedSessionKey];
|
|
29
|
+
await queryClient.cancelQueries({ queryKey, exact: true });
|
|
30
|
+
await queryClient.invalidateQueries({ queryKey, exact: true });
|
|
32
31
|
});
|
|
33
32
|
}, [normalizedSessionKey, queryClient]);
|
|
34
33
|
|
package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx
CHANGED
|
@@ -9,6 +9,14 @@ const usePanelAppsMock = vi.hoisted(() =>
|
|
|
9
9
|
isLoading: false,
|
|
10
10
|
})),
|
|
11
11
|
);
|
|
12
|
+
const useProjectsMock = vi.hoisted(() =>
|
|
13
|
+
vi.fn(() => ({
|
|
14
|
+
data: { projects: [] },
|
|
15
|
+
error: null,
|
|
16
|
+
isFetching: false,
|
|
17
|
+
isLoading: false,
|
|
18
|
+
})),
|
|
19
|
+
);
|
|
12
20
|
const useServerPathSearchMock = vi.hoisted(() =>
|
|
13
21
|
vi.fn(() => ({
|
|
14
22
|
data: { entries: [] },
|
|
@@ -38,6 +46,9 @@ const useServerPathBrowseMock = vi.hoisted(() =>
|
|
|
38
46
|
vi.mock('@/features/panel-apps', () => ({
|
|
39
47
|
usePanelApps: usePanelAppsMock,
|
|
40
48
|
}));
|
|
49
|
+
vi.mock('@/shared/hooks/use-projects', () => ({
|
|
50
|
+
useProjects: useProjectsMock,
|
|
51
|
+
}));
|
|
41
52
|
vi.mock('@/shared/hooks/use-server-path-search', () => ({
|
|
42
53
|
useServerPathSearch: useServerPathSearchMock,
|
|
43
54
|
}));
|
|
@@ -68,6 +79,7 @@ function createHookParams() {
|
|
|
68
79
|
|
|
69
80
|
beforeEach(() => {
|
|
70
81
|
usePanelAppsMock.mockClear();
|
|
82
|
+
useProjectsMock.mockClear();
|
|
71
83
|
useServerPathBrowseMock.mockClear();
|
|
72
84
|
useServerPathSearchMock.mockClear();
|
|
73
85
|
});
|
|
@@ -84,6 +96,7 @@ it('browses nested folders while reserving search for project-wide queries', ()
|
|
|
84
96
|
start: 0,
|
|
85
97
|
}),
|
|
86
98
|
);
|
|
99
|
+
expect(useProjectsMock).toHaveBeenLastCalledWith({ enabled: true });
|
|
87
100
|
const filesItem = result.current.inputSurfaceState.panel?.items.find(
|
|
88
101
|
(item) => item.selectionBehavior === 'navigate',
|
|
89
102
|
);
|
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
CONTEXT_REFERENCE_TRIGGER_SPEC,
|
|
11
11
|
createContextReferenceInputSurfacePlugin,
|
|
12
12
|
} from '@/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils';
|
|
13
|
+
import type { ContextReferenceLocation } from '@/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types';
|
|
13
14
|
import {
|
|
14
15
|
createSlashCommandInputSurfacePlugin,
|
|
15
16
|
type ChatSlashCommandDescriptor,
|
|
16
17
|
} from '@/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils';
|
|
17
18
|
import type { ChatInputBarAdapterTexts, ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
|
|
19
|
+
import { useProjects } from '@/shared/hooks/use-projects';
|
|
18
20
|
import { useServerPathBrowse } from '@/shared/hooks/use-server-path-browse';
|
|
19
21
|
import { useServerPathSearch } from '@/shared/hooks/use-server-path-search';
|
|
20
22
|
import type { ServerPathEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
|
|
@@ -39,7 +41,7 @@ type UseChatInputSurfaceStateParams = {
|
|
|
39
41
|
function buildChatInputSurfacePlugins(params: {
|
|
40
42
|
commands: readonly ChatSlashCommandDescriptor[];
|
|
41
43
|
language: I18nLanguage;
|
|
42
|
-
onNavigate: (
|
|
44
|
+
onNavigate: (location: ContextReferenceLocation) => void;
|
|
43
45
|
onSelectPanelApp: (appId: string) => void;
|
|
44
46
|
onSelectSkill: (skillRef: string) => void;
|
|
45
47
|
slashTexts: ChatInputSurfaceSlashTexts;
|
|
@@ -105,6 +107,15 @@ function buildChatInputSurfacePlugins(params: {
|
|
|
105
107
|
parentLabel: t('chatContextReferenceParent', language),
|
|
106
108
|
parentDescription: t('chatContextReferenceParentDescription', language),
|
|
107
109
|
parentHintLabel: t('chatContextReferenceParentHint', language),
|
|
110
|
+
projectDescription: t('chatContextReferenceProjectDescription', language),
|
|
111
|
+
projectPathLabel: t('chatContextReferenceProjectPath', language),
|
|
112
|
+
projectSectionLabel: t('chatContextReferenceProjectSection', language),
|
|
113
|
+
projectSubtitle: t('chatContextReferenceProjectType', language),
|
|
114
|
+
projectsDescription: t('chatContextReferenceProjectsDescription', language),
|
|
115
|
+
projectsHintLabel: t('chatContextReferenceProjectsHint', language),
|
|
116
|
+
projectsLabel: t('chatContextReferenceProjectsLabel', language),
|
|
117
|
+
projectsLoadFailedLabel: t('chatContextReferenceProjectsLoadFailed', language),
|
|
118
|
+
projectsSubtitle: t('chatContextReferenceProjectsType', language),
|
|
108
119
|
projectRootLabel: t('chatContextReferenceProjectRoot', language),
|
|
109
120
|
searchFailedLabel: t('chatContextReferenceSearchFailed', language),
|
|
110
121
|
workspaceSectionLabel: t('chatContextReferenceWorkspaceSection', language),
|
|
@@ -150,7 +161,9 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
|
|
|
150
161
|
skillRecords,
|
|
151
162
|
commands,
|
|
152
163
|
} = params;
|
|
153
|
-
const [
|
|
164
|
+
const [referenceLocation, setReferenceLocation] = useState<ContextReferenceLocation>({
|
|
165
|
+
view: 'root',
|
|
166
|
+
});
|
|
154
167
|
const [inputSurfaceTrigger, setInputSurfaceTriggerState] = useState<ChatInputSurfaceTrigger | null>(null);
|
|
155
168
|
const inputSurfaceTriggerSignatureRef = useRef('null');
|
|
156
169
|
const setInputSurfaceTrigger = useCallback((nextTrigger: ChatInputSurfaceTrigger | null): void => {
|
|
@@ -158,23 +171,25 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
|
|
|
158
171
|
if (inputSurfaceTriggerSignatureRef.current === nextSignature) return;
|
|
159
172
|
inputSurfaceTriggerSignatureRef.current = nextSignature;
|
|
160
173
|
if (!nextTrigger) {
|
|
161
|
-
|
|
174
|
+
setReferenceLocation({ view: 'root' });
|
|
162
175
|
}
|
|
163
176
|
setInputSurfaceTriggerState(nextTrigger);
|
|
164
177
|
}, []);
|
|
165
178
|
const isContextReferenceTrigger = inputSurfaceTrigger?.key === CONTEXT_REFERENCE_TRIGGER_SPEC.key;
|
|
179
|
+
const referencePath = referenceLocation.view === 'files' ? referenceLocation.path : '';
|
|
166
180
|
const deferredReferenceQuery = useDeferredValue(
|
|
167
181
|
isContextReferenceTrigger ? inputSurfaceTrigger.query : '',
|
|
168
182
|
);
|
|
169
183
|
const isBrowsingServerPaths = Boolean(
|
|
170
184
|
isContextReferenceTrigger &&
|
|
171
185
|
projectRoot &&
|
|
172
|
-
|
|
186
|
+
referenceLocation.view === 'files' &&
|
|
173
187
|
!deferredReferenceQuery.trim(),
|
|
174
188
|
);
|
|
175
189
|
const shouldSearchServerPaths = Boolean(
|
|
176
190
|
isContextReferenceTrigger &&
|
|
177
191
|
projectRoot &&
|
|
192
|
+
referenceLocation.view !== 'projects' &&
|
|
178
193
|
deferredReferenceQuery.trim(),
|
|
179
194
|
);
|
|
180
195
|
const serverPathBrowse = useServerPathBrowse({
|
|
@@ -193,7 +208,7 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
|
|
|
193
208
|
() => isBrowsingServerPaths
|
|
194
209
|
? buildBrowsedReferenceEntries({
|
|
195
210
|
entries: serverPathBrowse.data?.entries ?? [],
|
|
196
|
-
parentRelativePath: referencePath
|
|
211
|
+
parentRelativePath: referencePath,
|
|
197
212
|
})
|
|
198
213
|
: serverPathSearch.data?.entries ?? [],
|
|
199
214
|
[
|
|
@@ -207,12 +222,13 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
|
|
|
207
222
|
isContextReferenceTrigger ||
|
|
208
223
|
inputSurfaceTrigger?.key === CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC.key;
|
|
209
224
|
const panelApps = usePanelApps({ enabled: shouldLoadPanelApps });
|
|
225
|
+
const projects = useProjects({ enabled: isContextReferenceTrigger });
|
|
210
226
|
|
|
211
227
|
const inputSurfacePlugins = useMemo(
|
|
212
228
|
() => buildChatInputSurfacePlugins({
|
|
213
229
|
commands,
|
|
214
230
|
language,
|
|
215
|
-
onNavigate:
|
|
231
|
+
onNavigate: setReferenceLocation,
|
|
216
232
|
onSelectPanelApp,
|
|
217
233
|
onSelectSkill,
|
|
218
234
|
slashTexts: itemTexts.slashTexts,
|
|
@@ -229,12 +245,15 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
|
|
|
229
245
|
() => resolveChatInputSurfaceState({
|
|
230
246
|
data: {
|
|
231
247
|
isPanelAppsLoading: panelApps.isLoading || panelApps.isFetching,
|
|
248
|
+
isProjectsLoading: projects.isLoading || projects.isFetching,
|
|
232
249
|
isServerPathSearchLoading: activeServerPathQuery.isLoading || activeServerPathQuery.isFetching,
|
|
233
250
|
isSkillsLoading,
|
|
234
251
|
panelApps: panelApps.data?.entries ?? [],
|
|
235
252
|
projectRoot,
|
|
253
|
+
projects: projects.data?.projects ?? [],
|
|
254
|
+
projectsError: projects.error instanceof Error ? projects.error.message : null,
|
|
236
255
|
recentSkillValues,
|
|
237
|
-
|
|
256
|
+
referenceLocation,
|
|
238
257
|
serverPathEntries,
|
|
239
258
|
serverPathSearchError: activeServerPathQuery.error instanceof Error
|
|
240
259
|
? activeServerPathQuery.error.message
|
|
@@ -251,10 +270,14 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
|
|
|
251
270
|
panelApps.isFetching,
|
|
252
271
|
panelApps.isLoading,
|
|
253
272
|
projectRoot,
|
|
273
|
+
projects.data?.projects,
|
|
274
|
+
projects.error,
|
|
275
|
+
projects.isFetching,
|
|
276
|
+
projects.isLoading,
|
|
254
277
|
activeServerPathQuery.error,
|
|
255
278
|
activeServerPathQuery.isFetching,
|
|
256
279
|
activeServerPathQuery.isLoading,
|
|
257
|
-
|
|
280
|
+
referenceLocation,
|
|
258
281
|
serverPathEntries,
|
|
259
282
|
isSkillsLoading,
|
|
260
283
|
recentSkillValues,
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
CONTEXT_REFERENCE_TRIGGER_SPEC,
|
|
5
5
|
createContextReferenceInputSurfacePlugin,
|
|
6
6
|
} from '@/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils';
|
|
7
|
-
import type {
|
|
7
|
+
import type { ContextReferenceLocation } from '@/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types';
|
|
8
|
+
import type { PanelAppEntryView, ProjectView } from '@/shared/lib/api';
|
|
8
9
|
|
|
9
10
|
function createPanelApp(): PanelAppEntryView {
|
|
10
11
|
return {
|
|
@@ -25,6 +26,15 @@ function createPanelApp(): PanelAppEntryView {
|
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function createProject(): ProjectView {
|
|
30
|
+
return {
|
|
31
|
+
name: 'NextClaw',
|
|
32
|
+
rootPath: '/tmp/nextclaw',
|
|
33
|
+
createdAt: '2026-07-28T00:00:00.000Z',
|
|
34
|
+
updatedAt: '2026-07-28T00:00:00.000Z',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
function createPlugin(onNavigate = vi.fn()) {
|
|
29
39
|
return createContextReferenceInputSurfacePlugin({
|
|
30
40
|
itemTexts: {
|
|
@@ -43,6 +53,15 @@ function createPlugin(onNavigate = vi.fn()) {
|
|
|
43
53
|
parentLabel: 'Up one folder',
|
|
44
54
|
parentDescription: 'Browse parent',
|
|
45
55
|
parentHintLabel: 'Enter to browse parent',
|
|
56
|
+
projectDescription: 'Project context',
|
|
57
|
+
projectPathLabel: 'Project path',
|
|
58
|
+
projectSectionLabel: 'Projects',
|
|
59
|
+
projectSubtitle: 'Project',
|
|
60
|
+
projectsDescription: 'Browse registered projects',
|
|
61
|
+
projectsHintLabel: 'Enter to browse projects',
|
|
62
|
+
projectsLabel: 'Projects',
|
|
63
|
+
projectsLoadFailedLabel: 'Projects failed',
|
|
64
|
+
projectsSubtitle: 'Project Context',
|
|
46
65
|
projectRootLabel: 'Project directory',
|
|
47
66
|
searchFailedLabel: 'Search failed',
|
|
48
67
|
workspaceSectionLabel: 'Files & Folders',
|
|
@@ -65,15 +84,18 @@ function createPlugin(onNavigate = vi.fn()) {
|
|
|
65
84
|
});
|
|
66
85
|
}
|
|
67
86
|
|
|
68
|
-
function createData(
|
|
87
|
+
function createData(referenceLocation: ContextReferenceLocation = { view: 'root' }) {
|
|
69
88
|
return {
|
|
70
89
|
isPanelAppsLoading: false,
|
|
90
|
+
isProjectsLoading: false,
|
|
71
91
|
isServerPathSearchLoading: false,
|
|
72
92
|
isSkillsLoading: false,
|
|
73
93
|
panelApps: [createPanelApp()],
|
|
74
94
|
projectRoot: '/tmp/project',
|
|
95
|
+
projects: [createProject()],
|
|
96
|
+
projectsError: null,
|
|
75
97
|
recentSkillValues: [],
|
|
76
|
-
|
|
98
|
+
referenceLocation,
|
|
77
99
|
serverPathEntries: [
|
|
78
100
|
{
|
|
79
101
|
name: 'server-path.ts',
|
|
@@ -98,7 +120,7 @@ function createData(referencePath: string | null = null) {
|
|
|
98
120
|
}
|
|
99
121
|
|
|
100
122
|
describe('context reference input surface plugin', () => {
|
|
101
|
-
it('shows Files & Folders and panel apps in the root @ menu', () => {
|
|
123
|
+
it('shows Files & Folders, Projects, and panel apps in the root @ menu', () => {
|
|
102
124
|
const state = resolveChatInputSurfaceState({
|
|
103
125
|
plugins: [createPlugin()],
|
|
104
126
|
trigger: {
|
|
@@ -116,6 +138,11 @@ describe('context reference input surface plugin', () => {
|
|
|
116
138
|
icon: 'files',
|
|
117
139
|
selectionBehavior: 'navigate',
|
|
118
140
|
}),
|
|
141
|
+
expect.objectContaining({
|
|
142
|
+
title: 'Projects',
|
|
143
|
+
icon: 'project',
|
|
144
|
+
selectionBehavior: 'navigate',
|
|
145
|
+
}),
|
|
119
146
|
expect.objectContaining({
|
|
120
147
|
key: 'panel-app:task-board',
|
|
121
148
|
tokenKind: 'panel_app',
|
|
@@ -133,7 +160,7 @@ describe('context reference input surface plugin', () => {
|
|
|
133
160
|
start: 0,
|
|
134
161
|
end: 7,
|
|
135
162
|
},
|
|
136
|
-
data: createData(''),
|
|
163
|
+
data: createData({ view: 'files', path: '' }),
|
|
137
164
|
});
|
|
138
165
|
|
|
139
166
|
expect(state.panel?.items.slice(1)).toEqual([
|
|
@@ -173,7 +200,7 @@ describe('context reference input surface plugin', () => {
|
|
|
173
200
|
|
|
174
201
|
state.panel?.onSelectItem?.(item!);
|
|
175
202
|
|
|
176
|
-
expect(onNavigate).toHaveBeenCalledWith('');
|
|
203
|
+
expect(onNavigate).toHaveBeenCalledWith({ view: 'files', path: '' });
|
|
177
204
|
expect(item).not.toHaveProperty('tokenKind');
|
|
178
205
|
});
|
|
179
206
|
|
|
@@ -187,7 +214,7 @@ describe('context reference input surface plugin', () => {
|
|
|
187
214
|
start: 0,
|
|
188
215
|
end: 1,
|
|
189
216
|
},
|
|
190
|
-
data: createData(''),
|
|
217
|
+
data: createData({ view: 'files', path: '' }),
|
|
191
218
|
});
|
|
192
219
|
const directoryItem = rootState.panel?.items.find((item) => item.title === 'docs');
|
|
193
220
|
|
|
@@ -197,7 +224,7 @@ describe('context reference input surface plugin', () => {
|
|
|
197
224
|
});
|
|
198
225
|
expect(directoryItem?.tokenKind).toBeUndefined();
|
|
199
226
|
rootState.panel?.onSelectItem?.(directoryItem!);
|
|
200
|
-
expect(onNavigate).toHaveBeenCalledWith('docs');
|
|
227
|
+
expect(onNavigate).toHaveBeenCalledWith({ view: 'files', path: 'docs' });
|
|
201
228
|
|
|
202
229
|
const nestedState = resolveChatInputSurfaceState({
|
|
203
230
|
plugins: [createPlugin(onNavigate)],
|
|
@@ -207,7 +234,7 @@ describe('context reference input surface plugin', () => {
|
|
|
207
234
|
start: 0,
|
|
208
235
|
end: 1,
|
|
209
236
|
},
|
|
210
|
-
data: createData('docs'),
|
|
237
|
+
data: createData({ view: 'files', path: 'docs' }),
|
|
211
238
|
});
|
|
212
239
|
expect(nestedState.panel?.items).toEqual(expect.arrayContaining([
|
|
213
240
|
expect.objectContaining({
|
|
@@ -222,4 +249,47 @@ describe('context reference input surface plugin', () => {
|
|
|
222
249
|
}),
|
|
223
250
|
]));
|
|
224
251
|
});
|
|
252
|
+
|
|
253
|
+
it('opens the project view and builds semantic project tokens', () => {
|
|
254
|
+
const onNavigate = vi.fn();
|
|
255
|
+
const rootState = resolveChatInputSurfaceState({
|
|
256
|
+
plugins: [createPlugin(onNavigate)],
|
|
257
|
+
trigger: {
|
|
258
|
+
...CONTEXT_REFERENCE_TRIGGER_SPEC,
|
|
259
|
+
query: '',
|
|
260
|
+
start: 0,
|
|
261
|
+
end: 1,
|
|
262
|
+
},
|
|
263
|
+
data: createData(),
|
|
264
|
+
});
|
|
265
|
+
const projectsItem = rootState.panel?.items.find((item) => item.title === 'Projects');
|
|
266
|
+
|
|
267
|
+
rootState.panel?.onSelectItem?.(projectsItem!);
|
|
268
|
+
expect(onNavigate).toHaveBeenCalledWith({ view: 'projects' });
|
|
269
|
+
|
|
270
|
+
const projectsState = resolveChatInputSurfaceState({
|
|
271
|
+
plugins: [createPlugin(onNavigate)],
|
|
272
|
+
trigger: {
|
|
273
|
+
...CONTEXT_REFERENCE_TRIGGER_SPEC,
|
|
274
|
+
query: 'next',
|
|
275
|
+
start: 0,
|
|
276
|
+
end: 5,
|
|
277
|
+
},
|
|
278
|
+
data: createData({ view: 'projects' }),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(projectsState.panel?.items).toEqual([
|
|
282
|
+
expect.objectContaining({
|
|
283
|
+
title: 'Back',
|
|
284
|
+
selectionBehavior: 'navigate',
|
|
285
|
+
}),
|
|
286
|
+
expect.objectContaining({
|
|
287
|
+
title: 'NextClaw',
|
|
288
|
+
icon: 'project',
|
|
289
|
+
tokenKind: 'project',
|
|
290
|
+
tokenKey: '/tmp/nextclaw',
|
|
291
|
+
detailLines: ['Project path: /tmp/nextclaw'],
|
|
292
|
+
}),
|
|
293
|
+
]);
|
|
294
|
+
});
|
|
225
295
|
});
|