@nextclaw/ui 0.24.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/.vite/manifest.json +538 -525
- package/dist/assets/{account-panel-BxMlLXYH.js → account-panel-UvHFa8Kl.js} +1 -1
- package/dist/assets/{api-Utt-xzrT.js → api-Dffeij78.js} +5 -5
- package/dist/assets/{app-data-DYXDL6X2.js → app-data-DzaIMy4D.js} +1 -1
- package/dist/assets/app-presenter-provider-O2tUKKGf.js +109 -0
- package/dist/assets/{appearance-settings-page-BfYznAK5.js → appearance-settings-page-D8cWniw-.js} +1 -1
- package/dist/assets/{book-open-DDW_3XpM.js → book-open-xqEj1OLE.js} +1 -1
- package/dist/assets/{boxes-DwJeuK_h.js → boxes-D6uPnkGo.js} +1 -1
- package/dist/assets/{card-IevY2nm-.js → card-CS8su2Zh.js} +1 -1
- package/dist/assets/{channels-list-page-DitnqbMR.js → channels-list-page-B_fYP1Pn.js} +1 -1
- package/dist/assets/{chevron-down-An7Km-t4.js → chevron-down-BL-yMYcX.js} +1 -1
- package/dist/assets/{circle-alert-CDXqELuo.js → circle-alert-lSa3LRvS.js} +1 -1
- package/dist/assets/{circle-dashed-CapXEpiV.js → circle-dashed-CU3yApvP.js} +1 -1
- package/dist/assets/{client-B0krol7A.js → client-BtGBwLTs.js} +1 -1
- package/dist/assets/{config-split-page-BtnkqWVp.js → config-split-page-6hHiiapl.js} +1 -1
- package/dist/assets/{confirm-dialog-DFt4jpN7.js → confirm-dialog-ClT6iaj8.js} +1 -1
- package/dist/assets/{createLucideIcon-BTHK0Ot9.js → createLucideIcon-BNKf0_kc.js} +1 -1
- package/dist/assets/{desktop-capabilities-page-CCf14WId.js → desktop-capabilities-page-CB5pniTx.js} +1 -1
- package/dist/assets/{desktop-update-config-DVoVgfFi.js → desktop-update-config-OuQV73gg.js} +1 -1
- package/dist/assets/{dialog-BFN_1DBH.js → dialog-Xe73hpQz.js} +1 -1
- package/dist/assets/{dist-Bc9hOVNN.js → dist-BiIcnf9j.js} +1 -1
- package/dist/assets/{dist-CEhIDAnr.js → dist-C5I0v8Qn.js} +1 -1
- package/dist/assets/{dist-hkVPP1th.js → dist-czPa8xFO.js} +1 -1
- package/dist/assets/doc-browser-BFkslEzY.js +1 -0
- package/dist/assets/{doc-browser-BNUxmwD6.js → doc-browser-Cu58uiEg.js} +1 -1
- package/dist/assets/{doc-browser-context-C4yQrKrH.js → doc-browser-context-CekJxJFm.js} +1 -1
- package/dist/assets/{ellipsis-BRI6ROeu.js → ellipsis-C5YXdOUx.js} +1 -1
- package/dist/assets/{es2015-MOCbM0Eu.js → es2015-cuwbYI-l.js} +1 -1
- package/dist/assets/{extensions-DxUBMtHW.js → extensions-B5r-0jCq.js} +1 -1
- package/dist/assets/{eye-cnynUDGt.js → eye-C84inPcV.js} +1 -1
- package/dist/assets/{form-actions-gQ0JEUwB.js → form-actions-AIdXOoCL.js} +1 -1
- package/dist/assets/{i18n-B9yXA8KO.js → i18n-CPjq_hQX.js} +2 -2
- package/dist/assets/{i18n-provider-BYBJua91.js → i18n-provider-BYO9AhFy.js} +1 -1
- package/dist/assets/keyboard-shortcuts-settings-page-1efEkGwq.js +1 -0
- package/dist/assets/loader-circle-K1BDdHR5.js +1 -0
- package/dist/assets/{login-page-CqrHhzaN.js → login-page-DBqkVTVD.js} +1 -1
- package/dist/assets/login-page-DjcRQLQe.js +1 -0
- package/dist/assets/{logo-badge-Cr03YceJ.js → logo-badge-C-5-zim6.js} +1 -1
- package/dist/assets/main-CLwCjK9U.css +1 -0
- package/dist/assets/main-CQ6GUYdX.js +2 -0
- package/dist/assets/main-i18n-catalog.config-QB4DfJRJ.js +1 -0
- package/dist/assets/{mcp-marketplace-page-CifiVgFD.js → mcp-marketplace-page-CWAxBNaS.js} +1 -1
- package/dist/assets/mcp-marketplace-page-Ct090ceA.js +1 -0
- package/dist/assets/{model-config-page-C6jVNbSj.js → model-config-page-DBcuZyRC.js} +1 -1
- package/dist/assets/{navigation-link-kbCLRSr_.js → navigation-link-BAQjgx2F.js} +1 -1
- package/dist/assets/panel-app-i18n-catalog.config-CQFujXqt.js +1 -0
- package/dist/assets/panel-app-maumVhbl.js +1 -0
- package/dist/assets/{panelStandalone-DMBnYyTw.js → panelStandalone-BGA1s7JG.js} +2 -2
- package/dist/assets/plus-LA8-jpxP.js +1 -0
- package/dist/assets/{popover-XVDgjxcl.js → popover-NkrFlWHp.js} +1 -1
- package/dist/assets/{privacy-settings-page-BN27MBVA.js → privacy-settings-page-DVC-aq1v.js} +1 -1
- package/dist/assets/projects-Cq0E3c7N.js +1 -0
- package/dist/assets/{provider-scoped-model-input-BGZMCThn.js → provider-scoped-model-input-BGdZMWWa.js} +1 -1
- package/dist/assets/{providers-config-page-D5aBwbOD.js → providers-config-page-B-IhHl6x.js} +1 -1
- package/dist/assets/{puzzle-D5Dhacmb.js → puzzle-D6gulz7u.js} +1 -1
- package/dist/assets/{pwa-ClQK56Qe.js → pwa-C3kR9mdy.js} +1 -1
- package/dist/assets/{refresh-cw-Bw5BY7vp.js → refresh-cw-h9Xq3LY3.js} +1 -1
- package/dist/assets/remote-Cnawibbg.js +1 -0
- package/dist/assets/{rotate-cw-BksCLAeE.js → rotate-cw-B6xm41Wn.js} +1 -1
- package/dist/assets/runtime-config-page-LmrB2q65.js +1 -0
- package/dist/assets/{save-CVi8VWzb.js → save-fff_WpY2.js} +1 -1
- package/dist/assets/{search-DN-f7IIC.js → search-CUe41wjm.js} +1 -1
- package/dist/assets/{search-config-page-BmuZTVxs.js → search-config-page-BIXmv5FY.js} +1 -1
- package/dist/assets/{secrets-config-page-NPHpg9sb.js → secrets-config-page-DU3fadrq.js} +1 -1
- package/dist/assets/security-config-9s0i0I4N.js +1 -0
- package/dist/assets/{select-CmDTqk2C.js → select-CG5YWnwe.js} +1 -1
- package/dist/assets/{service-apps-panel-BYGJ9_pO.js → service-apps-panel-Bshf3Wmq.js} +1 -1
- package/dist/assets/{settings-2-B62YtZon.js → settings-2-CE3ELQyk.js} +1 -1
- package/dist/assets/skeleton-BL9h32W7.js +1 -0
- package/dist/assets/{tabs-Bk_mG7ql.js → tabs-BuE0qKMh.js} +1 -1
- package/dist/assets/{tag-chip-DgqKlI5D.js → tag-chip-gAH1IOsZ.js} +1 -1
- package/dist/assets/{theme-provider-CeBY2pyu.js → theme-provider-DE7m9jvf.js} +1 -1
- package/dist/assets/{tooltip-CgGbBdYf.js → tooltip-DYcuWhuP.js} +1 -1
- package/dist/assets/{trash-2-CVOev8yc.js → trash-2-BPPYt2NV.js} +1 -1
- package/dist/assets/{use-config-b2DN67gs.js → use-config-Cyiw06-j.js} +1 -1
- package/dist/assets/{use-desktop-capabilities-B3tLU1yl.js → use-desktop-capabilities-OKG96v4y.js} +1 -1
- package/dist/assets/{use-panel-app-scroll-restoration-BekzoX6z.js → use-panel-app-scroll-restoration-DsV1IJx-.js} +1 -1
- package/dist/assets/{use-viewport-layout-CUA3jQsA.js → use-viewport-layout-BS18drMT.js} +1 -1
- package/dist/assets/{utils-UBHTdG5Q.js → utils-CBZPzC_b.js} +1 -1
- package/dist/assets/x-DdXOqLnX.js +1 -0
- package/dist/index.html +50 -50
- package/dist/panel-standalone.html +15 -15
- package/package.json +7 -7
- package/src/app/configs/app-navigation.config.ts +10 -3
- package/src/app/index.tsx +7 -0
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +12 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +13 -1
- package/src/features/chat/features/conversation/components/__tests__/session-voice-input.test-utils.ts +52 -0
- package/src/features/chat/features/conversation/components/chat-voice-input-panel.tsx +70 -0
- package/src/features/chat/features/conversation/components/chat-voice-shortcut-control.tsx +33 -0
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +1 -11
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +29 -5
- package/src/features/chat/features/conversation/hooks/__tests__/use-chat-voice-input.test.tsx +185 -0
- package/src/features/chat/features/conversation/hooks/use-chat-voice-input.ts +52 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +13 -6
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +28 -12
- package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +19 -1
- package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +10 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +7 -6
- package/src/features/chat/features/message/utils/chat-message-part.utils.ts +4 -1
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +20 -0
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +90 -6
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +64 -2
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/chat-voice-input.manager.ts +174 -0
- package/src/features/chat/presenters/chat.presenter.ts +2 -0
- package/src/features/chat/stores/chat-voice-shortcut.store.ts +31 -0
- package/src/features/desktop-capabilities/index.ts +1 -0
- package/src/features/panel-apps/components/__tests__/panel-app-open-standalone-menu-item.test.tsx +3 -2
- package/src/features/panel-apps/components/panel-app-open-standalone-menu-item.tsx +4 -1
- package/src/features/projects/AGENTS.md +1 -2
- package/src/features/projects/components/project-agreement.tsx +36 -77
- package/src/features/projects/components/project-artifacts.test.tsx +90 -62
- package/src/features/projects/components/project-artifacts.tsx +81 -160
- package/src/features/projects/components/project-materials.test.tsx +80 -0
- package/src/features/projects/components/project-overview.test.tsx +50 -40
- package/src/features/projects/components/project-overview.tsx +5 -4
- package/src/features/projects/components/project-skills.tsx +24 -14
- package/src/features/projects/components/work/project-work-item-drawer.test.tsx +3 -2
- package/src/features/projects/components/work/project-work-items.test.tsx +13 -1
- package/src/features/projects/components/work/project-work-items.tsx +7 -7
- package/src/features/projects/hooks/use-project-materials.test.tsx +65 -0
- package/src/features/projects/hooks/use-project-materials.ts +24 -0
- package/src/features/projects/hooks/use-project-work.test.tsx +57 -1
- package/src/features/projects/hooks/use-project-work.ts +11 -5
- package/src/features/projects/pages/project-home-page.tsx +51 -127
- package/src/features/projects/pages/projects-page.test.tsx +68 -75
- package/src/features/projects/presenters/project-home.presenter.test.ts +1 -64
- package/src/features/projects/presenters/project-home.presenter.ts +17 -54
- package/src/features/projects/utils/project-artifact-view.utils.test.ts +0 -46
- package/src/features/projects/utils/project-artifact-view.utils.ts +0 -47
- package/src/features/settings/pages/keyboard-shortcuts-settings-page.tsx +15 -0
- package/src/platforms/mobile/components/mobile-settings-shell.tsx +2 -1
- package/src/shared/lib/api/__tests__/ncp-session-query-cache.utils.test.ts +109 -1
- package/src/shared/lib/api/ncp-session-query-cache.utils.ts +71 -2
- package/src/shared/lib/i18n/locales/en-US/chat.json +32 -1
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
- package/src/shared/lib/i18n/locales/en-US/projects.json +8 -59
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +32 -1
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
- package/src/shared/lib/i18n/locales/zh-CN/projects.json +8 -59
- package/src/shared/lib/transport/__tests__/browser-realtime-recovery.service.test.ts +42 -0
- package/src/shared/lib/transport/__tests__/local-transport.service.test.ts +85 -0
- package/src/shared/lib/transport/__tests__/remote.transport.test.ts +54 -1
- package/src/shared/lib/transport/browser-realtime-recovery.service.ts +35 -0
- package/src/shared/lib/transport/local-transport.service.ts +32 -0
- package/src/shared/lib/transport/remote-transport.service.ts +30 -0
- package/dist/assets/app-presenter-provider-CLsFiXvV.js +0 -109
- package/dist/assets/doc-browser-w4eGZXLu.js +0 -1
- package/dist/assets/loader-circle-rWr5Yo0E.js +0 -1
- package/dist/assets/login-page-CfMSkOsg.js +0 -1
- package/dist/assets/main-8cVkMfgE.css +0 -1
- package/dist/assets/main-Bys58GxY.js +0 -2
- package/dist/assets/main-i18n-catalog.config-BSq9yiUQ.js +0 -1
- package/dist/assets/mcp-marketplace-page-DX5VE3mU.js +0 -1
- package/dist/assets/panel-app-BUUle0gP.js +0 -1
- package/dist/assets/panel-app-i18n-catalog.config-C2St67SP.js +0 -1
- package/dist/assets/plus-BHnKwuPq.js +0 -1
- package/dist/assets/projects-DV3UuJMS.js +0 -1
- package/dist/assets/remote-Bl005fzu.js +0 -1
- package/dist/assets/runtime-config-page-CmktQcMW.js +0 -1
- package/dist/assets/security-config-Djptuyf2.js +0 -1
- package/dist/assets/skeleton-Eh64l4T2.js +0 -1
- package/dist/assets/x-C0iaOsRW.js +0 -1
- package/src/features/projects/components/project-requests.tsx +0 -57
- package/src/features/projects/components/project-runs.tsx +0 -60
- package/src/features/projects/hooks/use-project-observation.test.tsx +0 -93
- package/src/features/projects/hooks/use-project-observation.ts +0 -115
- package/src/features/projects/utils/project-request-response.utils.test.ts +0 -44
- package/src/features/projects/utils/project-request-response.utils.ts +0 -39
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useMemo, useState, type ReactNode } from "react";
|
|
2
|
-
import { Link, useLocation } from "react-router-dom";
|
|
2
|
+
import { Link, useLocation, useNavigate } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
4
|
AlarmClock,
|
|
5
5
|
ChevronDown,
|
|
6
6
|
ChevronRight,
|
|
7
7
|
Folder,
|
|
8
|
+
FolderMinus,
|
|
8
9
|
MessageSquareText,
|
|
10
|
+
MoreVertical,
|
|
9
11
|
Pin,
|
|
10
12
|
Plus,
|
|
11
13
|
} from "lucide-react";
|
|
@@ -22,6 +24,9 @@ import { t } from "@/shared/lib/i18n";
|
|
|
22
24
|
import { ChatSidebarContextCard } from "@/features/chat/features/session/components/chat-sidebar-context-card";
|
|
23
25
|
import { SessionRunBadge } from "@/features/chat/features/session/components/session-run-badge";
|
|
24
26
|
import { cn } from "@/shared/lib/utils";
|
|
27
|
+
import { useRemoveProject } from "@/shared/hooks/use-projects";
|
|
28
|
+
import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
|
|
29
|
+
import { ChatSessionHeaderMenuItem } from "@/features/chat/features/session/components/session-header/chat-session-header-menu-item";
|
|
25
30
|
|
|
26
31
|
export type { ChatSidebarProjectGroup };
|
|
27
32
|
|
|
@@ -57,10 +62,16 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
57
62
|
} = props;
|
|
58
63
|
const presenter = usePresenter();
|
|
59
64
|
const location = useLocation();
|
|
65
|
+
const navigate = useNavigate();
|
|
66
|
+
const projectRemoval = useRemoveProject();
|
|
67
|
+
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
60
68
|
const collapsedProjectRoots = useChatSessionListStore(
|
|
61
69
|
(state) => state.snapshot.collapsedProjectRoots,
|
|
62
70
|
);
|
|
63
71
|
const [openProjectRoot, setOpenProjectRoot] = useState<string | null>(null);
|
|
72
|
+
const [openProjectMenuRoot, setOpenProjectMenuRoot] = useState<string | null>(
|
|
73
|
+
null,
|
|
74
|
+
);
|
|
64
75
|
const preferredSessionType = useMemo(
|
|
65
76
|
() =>
|
|
66
77
|
resolveProjectGroupDefaultSessionType(
|
|
@@ -70,6 +81,25 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
70
81
|
[defaultSessionType, sessionTypeOptions],
|
|
71
82
|
);
|
|
72
83
|
const supportsSessionTypeChoice = sessionTypeOptions.length > 1;
|
|
84
|
+
const removeProject = async (group: ChatSidebarProjectGroup) => {
|
|
85
|
+
if (!group.projectId) return;
|
|
86
|
+
setOpenProjectMenuRoot(null);
|
|
87
|
+
const confirmed = await confirm({
|
|
88
|
+
title: `${t("projectsRemoveConfirmTitle")} “${group.projectName}”`,
|
|
89
|
+
description: t("projectsRemoveConfirmDescription"),
|
|
90
|
+
confirmLabel: t("projectsRemoveConfirmAction"),
|
|
91
|
+
variant: "default",
|
|
92
|
+
});
|
|
93
|
+
if (!confirmed) return;
|
|
94
|
+
try {
|
|
95
|
+
await projectRemoval.mutateAsync(group.projectId);
|
|
96
|
+
if (location.pathname.startsWith(`/projects/${group.projectId}/`)) {
|
|
97
|
+
navigate("/projects");
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// The mutation owns the visible error and the project stays available.
|
|
101
|
+
}
|
|
102
|
+
};
|
|
73
103
|
|
|
74
104
|
return (
|
|
75
105
|
<div className="space-y-0.5">
|
|
@@ -116,7 +146,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
116
146
|
>
|
|
117
147
|
<div
|
|
118
148
|
className={cn(
|
|
119
|
-
"flex h-full w-full min-w-0 items-center group-hover/project:pr-
|
|
149
|
+
"flex h-full w-full min-w-0 items-center group-hover/project:pr-20 group-has-[[data-project-actions]:focus-within]/project:pr-20",
|
|
120
150
|
hasRunningSession && "pr-7",
|
|
121
151
|
)}
|
|
122
152
|
>
|
|
@@ -243,6 +273,37 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
243
273
|
)
|
|
244
274
|
}
|
|
245
275
|
/>
|
|
276
|
+
{group.projectId ? (
|
|
277
|
+
<Popover
|
|
278
|
+
open={openProjectMenuRoot === group.projectRoot}
|
|
279
|
+
onOpenChange={(nextOpen) =>
|
|
280
|
+
setOpenProjectMenuRoot(
|
|
281
|
+
nextOpen ? group.projectRoot : null,
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
>
|
|
285
|
+
<PopoverTrigger asChild>
|
|
286
|
+
<IconActionButton
|
|
287
|
+
icon={<MoreVertical className="h-3.5 w-3.5" />}
|
|
288
|
+
label={t("projectsMoreActions").replace(
|
|
289
|
+
"{name}",
|
|
290
|
+
group.projectName,
|
|
291
|
+
)}
|
|
292
|
+
tooltip={false}
|
|
293
|
+
disabled={projectRemoval.isPending}
|
|
294
|
+
onClick={(event) => event.stopPropagation()}
|
|
295
|
+
/>
|
|
296
|
+
</PopoverTrigger>
|
|
297
|
+
<ChatPopoverContent align="end" className="w-56 p-2">
|
|
298
|
+
<ChatSessionHeaderMenuItem
|
|
299
|
+
icon={FolderMinus}
|
|
300
|
+
label={t("projectsRemoveFromList")}
|
|
301
|
+
disabled={projectRemoval.isPending}
|
|
302
|
+
onClick={() => void removeProject(group)}
|
|
303
|
+
/>
|
|
304
|
+
</ChatPopoverContent>
|
|
305
|
+
</Popover>
|
|
306
|
+
) : null}
|
|
246
307
|
</div>
|
|
247
308
|
</div>
|
|
248
309
|
{isCollapsed ? null : (
|
|
@@ -253,6 +314,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
253
314
|
</div>
|
|
254
315
|
);
|
|
255
316
|
})}
|
|
317
|
+
<ConfirmDialog key="project-remove-confirm-dialog" />
|
|
256
318
|
</div>
|
|
257
319
|
);
|
|
258
320
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { ChatComposerDictationSession } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
|
|
3
|
+
export type VoicePhase = 'idle' | 'ready' | 'starting' | 'recording' | 'stopping' | 'error';
|
|
4
|
+
export type VoiceError = 'unsupported' | 'permission' | 'network' | 'audio-capture' | 'no-speech' | 'interrupted' | 'timeout' | 'failed';
|
|
5
|
+
export type VoiceSnapshot = { phase: VoicePhase; text: string; interim: string; seconds: number; error: VoiceError | null };
|
|
6
|
+
export type SpeechRecognitionLike = {
|
|
7
|
+
lang: string;
|
|
8
|
+
continuous: boolean;
|
|
9
|
+
interimResults: boolean;
|
|
10
|
+
start: () => void;
|
|
11
|
+
stop: () => void;
|
|
12
|
+
abort: () => void;
|
|
13
|
+
onstart: (() => void) | null;
|
|
14
|
+
onresult: ((event: { results: ArrayLike<{ isFinal: boolean; 0: { transcript: string } }> }) => void) | null;
|
|
15
|
+
onend: (() => void) | null;
|
|
16
|
+
onerror: ((event: { error: string }) => void) | null;
|
|
17
|
+
};
|
|
18
|
+
type RecognitionConstructor = new () => SpeechRecognitionLike;
|
|
19
|
+
const createBrowserRecognition = (): SpeechRecognitionLike | null => {
|
|
20
|
+
const host = window as Window & { SpeechRecognition?: RecognitionConstructor; webkitSpeechRecognition?: RecognitionConstructor };
|
|
21
|
+
const Constructor = host.SpeechRecognition ?? host.webkitSpeechRecognition;
|
|
22
|
+
return Constructor ? new Constructor() : null;
|
|
23
|
+
};
|
|
24
|
+
const initialSnapshot = (): VoiceSnapshot => ({ phase: 'idle', text: '', interim: '', seconds: 0, error: null });
|
|
25
|
+
const isActive = (phase: VoicePhase) => ['starting', 'recording', 'stopping'].includes(phase);
|
|
26
|
+
|
|
27
|
+
/** Owns one explicit dictation session; never writes to the chat draft or sends messages. */
|
|
28
|
+
export class ChatVoiceInputManager {
|
|
29
|
+
private snapshot = initialSnapshot();
|
|
30
|
+
private listeners = new Set<() => void>();
|
|
31
|
+
private recognition: SpeechRecognitionLike | null = null;
|
|
32
|
+
private deadline: ReturnType<typeof setTimeout> | undefined;
|
|
33
|
+
private ticker: ReturnType<typeof setInterval> | undefined;
|
|
34
|
+
private context = '';
|
|
35
|
+
private settingsRequested = false;
|
|
36
|
+
private draft: ChatComposerDictationSession | null = null;
|
|
37
|
+
|
|
38
|
+
constructor(private readonly createRecognition = createBrowserRecognition) {}
|
|
39
|
+
|
|
40
|
+
getSnapshot = (): VoiceSnapshot => this.snapshot;
|
|
41
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
42
|
+
this.listeners.add(listener);
|
|
43
|
+
return () => { this.listeners.delete(listener); };
|
|
44
|
+
};
|
|
45
|
+
private update = (patch: Partial<VoiceSnapshot>): void => {
|
|
46
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
47
|
+
this.listeners.forEach((listener) => listener());
|
|
48
|
+
};
|
|
49
|
+
open = (context: string): void => {
|
|
50
|
+
if (this.snapshot.phase !== 'idle') return;
|
|
51
|
+
this.context = context;
|
|
52
|
+
this.update({ ...initialSnapshot(), phase: 'ready' });
|
|
53
|
+
};
|
|
54
|
+
openSettings = (): void => {
|
|
55
|
+
this.settingsRequested = true;
|
|
56
|
+
if (this.snapshot.phase === 'recording') { this.finish(); return; }
|
|
57
|
+
if (this.snapshot.phase === 'stopping') return;
|
|
58
|
+
const { context } = this;
|
|
59
|
+
this.acceptDraft();
|
|
60
|
+
this.open(context);
|
|
61
|
+
};
|
|
62
|
+
start = (language: string, context: string, draft: ChatComposerDictationSession | null): void => {
|
|
63
|
+
if (isActive(this.snapshot.phase)) return;
|
|
64
|
+
if (!draft) return;
|
|
65
|
+
this.release();
|
|
66
|
+
this.context = context;
|
|
67
|
+
this.draft = draft;
|
|
68
|
+
this.update({ ...initialSnapshot(), phase: 'starting' });
|
|
69
|
+
try {
|
|
70
|
+
const recognition = this.createRecognition();
|
|
71
|
+
if (!recognition) { this.fail('unsupported'); return; }
|
|
72
|
+
this.recognition = recognition;
|
|
73
|
+
recognition.lang = language === 'zh' ? 'zh-CN' : 'en-US';
|
|
74
|
+
recognition.continuous = true;
|
|
75
|
+
recognition.interimResults = true;
|
|
76
|
+
this.connect(recognition);
|
|
77
|
+
this.setDeadline(15000);
|
|
78
|
+
recognition.start();
|
|
79
|
+
} catch { this.fail('failed'); }
|
|
80
|
+
};
|
|
81
|
+
private connect = (recognition: SpeechRecognitionLike): void => {
|
|
82
|
+
recognition.onstart = () => {
|
|
83
|
+
if (this.recognition !== recognition || this.snapshot.phase !== 'starting') return;
|
|
84
|
+
clearTimeout(this.deadline);
|
|
85
|
+
this.update({ phase: 'recording' });
|
|
86
|
+
const started = Date.now();
|
|
87
|
+
this.ticker = setInterval(() => {
|
|
88
|
+
const seconds = Math.floor((Date.now() - started) / 1000);
|
|
89
|
+
this.update({ seconds });
|
|
90
|
+
if (seconds >= 60) this.finish();
|
|
91
|
+
}, 1000);
|
|
92
|
+
};
|
|
93
|
+
recognition.onresult = (event) => {
|
|
94
|
+
if (this.recognition !== recognition) return;
|
|
95
|
+
const finals: string[] = [];
|
|
96
|
+
const interim: string[] = [];
|
|
97
|
+
for (let i = 0; i < event.results.length; i += 1) {
|
|
98
|
+
const result = event.results[i];
|
|
99
|
+
const text = result[0]?.transcript?.trim();
|
|
100
|
+
if (text) (result.isFinal ? finals : interim).push(text);
|
|
101
|
+
}
|
|
102
|
+
this.update({ text: finals.join(' '), interim: interim.join(' ') });
|
|
103
|
+
this.draft?.update(this.snapshot.text, this.snapshot.interim);
|
|
104
|
+
};
|
|
105
|
+
recognition.onerror = ({ error }) => {
|
|
106
|
+
if (this.recognition !== recognition) return;
|
|
107
|
+
const reason: VoiceError = error === 'not-allowed' || error === 'service-not-allowed'
|
|
108
|
+
? 'permission'
|
|
109
|
+
: error === 'network' || error === 'audio-capture' || error === 'no-speech' ? error : 'failed';
|
|
110
|
+
this.fail(reason);
|
|
111
|
+
};
|
|
112
|
+
recognition.onend = () => {
|
|
113
|
+
if (this.recognition !== recognition) return;
|
|
114
|
+
const text = this.snapshot.text.trim();
|
|
115
|
+
this.release(false);
|
|
116
|
+
if (!text) { this.fail('no-speech'); return; }
|
|
117
|
+
this.draft?.commit(text);
|
|
118
|
+
this.draft = null;
|
|
119
|
+
const { settingsRequested, context } = this;
|
|
120
|
+
this.cancel();
|
|
121
|
+
if (settingsRequested) this.open(context);
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
finish = (): void => {
|
|
125
|
+
if (this.snapshot.phase === 'starting') { this.cancel(); return; }
|
|
126
|
+
if (this.snapshot.phase !== 'recording' || !this.recognition) return;
|
|
127
|
+
clearInterval(this.ticker);
|
|
128
|
+
this.update({ phase: 'stopping' });
|
|
129
|
+
this.setDeadline(8000);
|
|
130
|
+
try { this.recognition.stop(); } catch { this.fail('failed'); }
|
|
131
|
+
};
|
|
132
|
+
interrupt = (): void => {
|
|
133
|
+
if (isActive(this.snapshot.phase)) this.fail('interrupted');
|
|
134
|
+
};
|
|
135
|
+
acceptDraft = (): void => {
|
|
136
|
+
this.draft?.commit();
|
|
137
|
+
this.draft = null;
|
|
138
|
+
this.cancel();
|
|
139
|
+
};
|
|
140
|
+
cancel = (): void => {
|
|
141
|
+
this.settingsRequested = false;
|
|
142
|
+
this.release();
|
|
143
|
+
this.context = '';
|
|
144
|
+
this.draft?.cancel();
|
|
145
|
+
this.draft = null;
|
|
146
|
+
this.update(initialSnapshot());
|
|
147
|
+
};
|
|
148
|
+
private fail = (error: VoiceError): void => {
|
|
149
|
+
this.release();
|
|
150
|
+
if (this.snapshot.text) this.draft?.commit(this.snapshot.text);
|
|
151
|
+
else this.draft?.cancel();
|
|
152
|
+
this.draft = null;
|
|
153
|
+
this.update(this.settingsRequested ? { ...initialSnapshot(), phase: 'ready' } : { phase: 'error', error, interim: '' });
|
|
154
|
+
this.settingsRequested = false;
|
|
155
|
+
};
|
|
156
|
+
private setDeadline = (milliseconds: number): void => {
|
|
157
|
+
clearTimeout(this.deadline);
|
|
158
|
+
this.deadline = setTimeout(() => this.fail('timeout'), milliseconds);
|
|
159
|
+
};
|
|
160
|
+
private release = (abort = true): void => {
|
|
161
|
+
clearTimeout(this.deadline);
|
|
162
|
+
clearInterval(this.ticker);
|
|
163
|
+
const { recognition } = this;
|
|
164
|
+
this.recognition = null;
|
|
165
|
+
if (!recognition) return;
|
|
166
|
+
recognition.onstart = null;
|
|
167
|
+
recognition.onend = null;
|
|
168
|
+
recognition.onerror = null;
|
|
169
|
+
recognition.onresult = null;
|
|
170
|
+
if (abort) {
|
|
171
|
+
try { recognition.abort(); } catch { /* Already released by the browser. */ }
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -4,8 +4,10 @@ import { ChatQueryManager } from '@/features/chat/managers/chat-query.manager';
|
|
|
4
4
|
import { ChatThreadManager } from '@/features/chat/managers/chat-thread.manager';
|
|
5
5
|
import type { ChatComposerIntentManager } from '@/features/chat/managers/chat-composer-intent.manager';
|
|
6
6
|
import type { AppPresenter } from '@/app/presenters/app.presenter';
|
|
7
|
+
import { ChatVoiceInputManager } from '@/features/chat/managers/chat-voice-input.manager';
|
|
7
8
|
|
|
8
9
|
export class ChatPresenter {
|
|
10
|
+
readonly chatVoiceInputManager = new ChatVoiceInputManager();
|
|
9
11
|
readonly chatUiManager: ChatUiManager;
|
|
10
12
|
readonly chatSessionListManager: ChatSessionListManager;
|
|
11
13
|
readonly chatQueryManager: ChatQueryManager;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
export type VoiceShortcut = { code: string; ctrl: boolean; meta: boolean; alt: boolean; shift: boolean };
|
|
5
|
+
export function isVoiceShortcut(value: unknown): value is VoiceShortcut {
|
|
6
|
+
if (!value || typeof value !== 'object') return false;
|
|
7
|
+
const key = value as VoiceShortcut;
|
|
8
|
+
const unmodified = key.ctrl === false && key.meta === false && key.alt === false && key.shift === false;
|
|
9
|
+
return typeof key.code === 'string' && (/^Key[A-Z]$/.test(key.code) || /^F(?:[6-9]|10)$/.test(key.code)) &&
|
|
10
|
+
typeof key.ctrl === 'boolean' && typeof key.meta === 'boolean' &&
|
|
11
|
+
typeof key.alt === 'boolean' && typeof key.shift === 'boolean' &&
|
|
12
|
+
(unmodified || (key.alt && key.ctrl !== key.meta));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type ShortcutState = { shortcut: VoiceShortcut | null; setShortcut: (shortcut: VoiceShortcut | null) => void };
|
|
16
|
+
export const useChatVoiceShortcutStore = create<ShortcutState>()(persist(
|
|
17
|
+
(set) => ({ shortcut: null, setShortcut: (shortcut) => set({ shortcut }) }),
|
|
18
|
+
{
|
|
19
|
+
name: 'nextclaw.chat.voice-shortcut', version: 1,
|
|
20
|
+
storage: createJSONStorage(() => window.localStorage),
|
|
21
|
+
partialize: (state) => ({ shortcut: state.shortcut }),
|
|
22
|
+
merge: (persisted, current) => {
|
|
23
|
+
const value = (persisted as { shortcut?: unknown } | null)?.shortcut;
|
|
24
|
+
return { ...current, shortcut: isVoiceShortcut(value) ? value : null };
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
));
|
|
28
|
+
|
|
29
|
+
export function voiceShortcutLabel(shortcut: VoiceShortcut): string {
|
|
30
|
+
return [shortcut.ctrl && 'Ctrl', shortcut.meta && '⌘', shortcut.alt && 'Alt/⌥', shortcut.shift && 'Shift', shortcut.code.replace(/^Key/, '')].filter(Boolean).join(' + ');
|
|
31
|
+
}
|
package/src/features/panel-apps/components/__tests__/panel-app-open-standalone-menu-item.test.tsx
CHANGED
|
@@ -32,7 +32,7 @@ describe("PanelAppOpenStandaloneMenuItem", () => {
|
|
|
32
32
|
setDesktopHost(undefined);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
it("uses
|
|
35
|
+
it("uses an auxiliary new-tab link that installed PWA navigation cannot capture", () => {
|
|
36
36
|
const onSelect = vi.fn();
|
|
37
37
|
setDesktopHost(undefined);
|
|
38
38
|
render(
|
|
@@ -44,7 +44,8 @@ describe("PanelAppOpenStandaloneMenuItem", () => {
|
|
|
44
44
|
"/apps/panel/publisher.todo%20board/standalone",
|
|
45
45
|
);
|
|
46
46
|
expect(link.getAttribute("target")).toBe("_blank");
|
|
47
|
-
expect(link.getAttribute("rel")).toBe("
|
|
47
|
+
expect(link.getAttribute("rel")).toBe("opener");
|
|
48
|
+
expect(link.getAttribute("referrerpolicy")).toBe("no-referrer");
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
it("describes the same link as opening in the browser inside Desktop", () => {
|
|
@@ -18,7 +18,10 @@ export function PanelAppOpenStandaloneMenuItem({
|
|
|
18
18
|
<a
|
|
19
19
|
href={href}
|
|
20
20
|
target="_blank"
|
|
21
|
-
|
|
21
|
+
// Chrome captures non-auxiliary in-scope links into an installed PWA.
|
|
22
|
+
// `opener` keeps this trusted same-origin target auxiliary, so the current browser owns the new tab.
|
|
23
|
+
rel="opener"
|
|
24
|
+
referrerPolicy="no-referrer"
|
|
22
25
|
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground"
|
|
23
26
|
onClick={onSelect}
|
|
24
27
|
>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Projects UI boundary
|
|
2
2
|
|
|
3
|
-
- 本目录拥有 Projects 页面、presenter
|
|
4
|
-
- 页面默认只读;确认/拒绝只复用现有 agent-run 消息入口,并携带可追踪、可幂等的响应元数据,不创建第二套 response bridge。
|
|
3
|
+
- 本目录拥有 Projects 页面、presenter 和查询状态;工作项与产物来自 Project Work API,Skills 与工作约定来自独立的有界材料 API,组件不得读取项目配置、扫描文件或从会话正文重建项目事实。
|
|
5
4
|
- Overview、列表和看板消费同一份工作项查询状态;所有工作项入口统一打开右侧详情抽屉,不得在页面内追加平铺详情。
|
|
6
5
|
- `project.work.changed` 只用于失效查询;UI 不重放事件历史,也不以轮询或目录扫描代替业务存储。
|
|
7
6
|
- 组件负责连接与展示,跨组件状态归 hook/presenter;用户文案必须走 Projects i18n 资源。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProjectAgreementMaterial } from "@nextclaw/client-sdk";
|
|
2
2
|
import { t } from "@/shared/lib/i18n";
|
|
3
3
|
import {
|
|
4
4
|
ProjectEmptyState,
|
|
@@ -7,86 +7,45 @@ import {
|
|
|
7
7
|
} from "./project-section";
|
|
8
8
|
|
|
9
9
|
export function ProjectAgreement({
|
|
10
|
-
|
|
10
|
+
agreement,
|
|
11
|
+
isLoading,
|
|
12
|
+
isError,
|
|
11
13
|
onOpenFile,
|
|
12
14
|
}: {
|
|
13
|
-
|
|
15
|
+
agreement?: ProjectAgreementMaterial;
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
isError: boolean;
|
|
14
18
|
onOpenFile: (path: string, label: string) => void;
|
|
15
19
|
}) {
|
|
16
20
|
return (
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</span>
|
|
47
|
-
</div>
|
|
48
|
-
))}
|
|
49
|
-
</div>
|
|
50
|
-
{snapshot.diagnostics.length ? (
|
|
51
|
-
<div className="mt-4 space-y-2 border-t border-border/60 pt-4">
|
|
52
|
-
{snapshot.diagnostics.map((diagnostic) => (
|
|
53
|
-
<div
|
|
54
|
-
key={diagnostic.id}
|
|
55
|
-
className="rounded-xl bg-muted/35 p-3 text-sm"
|
|
56
|
-
>
|
|
57
|
-
<p className="font-medium">{diagnostic.code}</p>
|
|
58
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
59
|
-
{diagnostic.message}
|
|
60
|
-
</p>
|
|
61
|
-
</div>
|
|
62
|
-
))}
|
|
63
|
-
</div>
|
|
64
|
-
) : null}
|
|
65
|
-
</ProjectSection>
|
|
66
|
-
</div>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function ProjectContextReference({
|
|
71
|
-
context,
|
|
72
|
-
onOpenFile,
|
|
73
|
-
}: {
|
|
74
|
-
context: ProjectObservationSnapshot["project"]["context"][number];
|
|
75
|
-
onOpenFile: (path: string, label: string) => void;
|
|
76
|
-
}) {
|
|
77
|
-
const { accessible, role, source } = context;
|
|
78
|
-
return (
|
|
79
|
-
<ProjectFilePreviewButton
|
|
80
|
-
available={accessible}
|
|
81
|
-
path={source}
|
|
82
|
-
label={role}
|
|
83
|
-
onOpen={onOpenFile}
|
|
84
|
-
className="block w-full py-3 text-left first:pt-0 enabled:hover:text-foreground enabled:focus-visible:outline-none enabled:focus-visible:ring-1 enabled:focus-visible:ring-border"
|
|
85
|
-
>
|
|
86
|
-
<span className="block text-sm font-medium">{role}</span>
|
|
87
|
-
<span className="mt-1 block truncate text-xs text-muted-foreground">
|
|
88
|
-
{source}{!accessible ? ` · ${t("projectsUnavailable")}` : ""}
|
|
89
|
-
</span>
|
|
90
|
-
</ProjectFilePreviewButton>
|
|
21
|
+
<ProjectSection title={t("projectsAgreementFile")}>
|
|
22
|
+
<p className="mb-4 text-sm text-muted-foreground">
|
|
23
|
+
{t("projectsAgreementDescription")}
|
|
24
|
+
</p>
|
|
25
|
+
{isLoading ? (
|
|
26
|
+
<p className="py-5 text-sm text-muted-foreground">
|
|
27
|
+
{t("projectsLoading")}
|
|
28
|
+
</p>
|
|
29
|
+
) : isError ? (
|
|
30
|
+
<p className="py-5 text-sm text-destructive">
|
|
31
|
+
{t("projectsLoadFailed")}
|
|
32
|
+
</p>
|
|
33
|
+
) : agreement?.available ? (
|
|
34
|
+
<ProjectFilePreviewButton
|
|
35
|
+
available
|
|
36
|
+
path={agreement.path}
|
|
37
|
+
label={agreement.path}
|
|
38
|
+
onOpen={onOpenFile}
|
|
39
|
+
className="block w-full rounded-xl bg-muted/45 p-3 text-left transition-colors enabled:hover:bg-[var(--interaction-hover)] enabled:focus-visible:outline-none enabled:focus-visible:ring-1 enabled:focus-visible:ring-border"
|
|
40
|
+
>
|
|
41
|
+
<span className="block text-sm font-medium">{agreement.path}</span>
|
|
42
|
+
<span className="mt-1 block text-xs text-muted-foreground">
|
|
43
|
+
{t("projectsAgreementOpen")}
|
|
44
|
+
</span>
|
|
45
|
+
</ProjectFilePreviewButton>
|
|
46
|
+
) : (
|
|
47
|
+
<ProjectEmptyState>{t("projectsNoAgreement")}</ProjectEmptyState>
|
|
48
|
+
)}
|
|
49
|
+
</ProjectSection>
|
|
91
50
|
);
|
|
92
51
|
}
|