@nextclaw/ui 0.15.5 → 0.15.6
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 +24 -0
- package/dist/assets/api-Dyb5rgh0.js +13 -0
- package/dist/assets/app-presenter-provider-7jV0nPiL.js +106 -0
- package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DFm3BbEb.js} +1 -1
- package/dist/assets/{book-open-CN7Shpij.js → book-open-L6LCuqOr.js} +1 -1
- package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-DOaPKNJA.js} +2 -2
- package/dist/assets/{chat-page-152YuwfU.js → chat-page-Cw9uDhD_.js} +1 -1
- package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-CqlqjnCK.js} +1 -1
- package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-bAp8NU93.js} +1 -1
- package/dist/assets/desktop-update-config-2SjMfCMl.js +1 -0
- package/dist/assets/{dist-D5ycbwCo.js → dist-CrtbGLlh.js} +1 -1
- package/dist/assets/doc-browser-A5IRyDXQ2.js +1 -0
- package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-BmnQa30-.js} +1 -1
- package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-BlpUUFrR.js} +1 -1
- package/dist/assets/{download-CyPgglKr.js → download-BhZZNXD8.js} +1 -1
- package/dist/assets/{external-link-BahW1sl3.js → external-link-B9Uoq5J7.js} +1 -1
- package/dist/assets/{eye-D8gGXMkD.js → eye-K5knauEM.js} +1 -1
- package/dist/assets/{folder-DGWqFDvE.js → folder-BIYZZWJv.js} +1 -1
- package/dist/assets/{form-actions-D2imZLnU.js → form-actions-CKprWFeM.js} +1 -1
- package/dist/assets/index-BVn702m4.css +1 -0
- package/dist/assets/{index-CT0iYlxV.js → index-CFxJ1Fkg.js} +2 -2
- package/dist/assets/loader-circle-Bj2RMrCR.js +1 -0
- package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-BQ-XFZod.js} +1 -1
- package/dist/assets/mcp-marketplace-page-BfA7A3n4.js +9 -0
- package/dist/assets/mcp-marketplace-page-CXHYO95j.js +1 -0
- package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-BbWbW_qM.js} +1 -1
- package/dist/assets/plus-TmFdwO3P.js +1 -0
- package/dist/assets/{popover-Btp7KVc3.js → popover-CX9zFsuc.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-B-9_DtIP.js} +1 -1
- package/dist/assets/providers-config-page-xwIjGur2.js +1 -0
- package/dist/assets/{react-BY-U3h3t.js → react-DsgudwpT.js} +1 -1
- package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-kXdvbhFf.js} +1 -1
- package/dist/assets/remote-B9F2uCk9.js +1 -0
- package/dist/assets/rotate-cw-DEc6aihr.js +1 -0
- package/dist/assets/runtime-config-page-DTsi7czJ.js +1 -0
- package/dist/assets/{save-DNg2yJTS.js → save-C9qpz2_2.js} +1 -1
- package/dist/assets/{search-3ykfrAFt.js → search-_kEi29qo.js} +1 -1
- package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-H7ug8f4D.js} +1 -1
- package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-C1_6rINu.js} +2 -2
- package/dist/assets/security-config-Cr2ERKLu.js +1 -0
- package/dist/assets/{select-B5EecXJt.js → select-MJKhKAWR.js} +1 -1
- package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-DA3tJuew.js} +1 -1
- package/dist/assets/{settings-2-D-3um9Az.js → settings-2-D_y7zXJz.js} +1 -1
- package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-0uTLUhm9.js} +1 -1
- package/dist/assets/{tabs-BVD8gp3y.js → tabs-h44XPocd.js} +1 -1
- package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-bRZ04q3C.js} +1 -1
- package/dist/assets/{trash-2-cnsMppRC.js → trash-2-BRpSE-Wt.js} +1 -1
- package/dist/assets/use-config-CwDEAl6f.js +1 -0
- package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-BbnWw-8c.js} +1 -1
- package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-CqgIMDQL.js} +1 -1
- package/dist/assets/x-DdEDL6ju.js +1 -0
- package/dist/index.html +29 -29
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +84 -1
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
- package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
- package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +52 -2
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
- package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +37 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +140 -63
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +175 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +8 -1
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +274 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +50 -4
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +66 -8
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +41 -3
- package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
- package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +81 -127
- package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
- package/src/features/settings/components/config/provider-form.tsx +0 -4
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
- package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
- package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
- package/src/features/system-status/index.ts +9 -0
- package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
- package/src/index.css +10 -16
- package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
- package/src/shared/components/common/brand-header.tsx +121 -35
- package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
- package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
- package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
- package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
- package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
- package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
- package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
- package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
- package/src/shared/hooks/use-projects.ts +28 -0
- package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
- package/src/shared/hooks/use-server-path-search.ts +40 -0
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/projects/project.utils.ts +14 -0
- package/src/shared/lib/api/types.ts +27 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +35 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +4 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +35 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +4 -0
- package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
- package/dist/assets/api-IlX_CbwU.js +0 -13
- package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
- package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
- package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
- package/dist/assets/index-0av6WO9-.css +0 -1
- package/dist/assets/loader-circle-DtKek-qG.js +0 -1
- package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
- package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
- package/dist/assets/plus-BwgE-Zwf.js +0 -1
- package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
- package/dist/assets/remote-BaVECEZw.js +0 -1
- package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
- package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
- package/dist/assets/security-config-BPzet76z.js +0 -1
- package/dist/assets/use-config-D6yx0lb0.js +0 -1
- package/dist/assets/x-B3o1dWR5.js +0 -1
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { UpdateSnapshot } from '@nextclaw/shared';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
runtimeUpdateManager,
|
|
4
|
+
resolveUpdateReleaseNotesLink,
|
|
5
|
+
useCurrentVersionReleaseNotesLink,
|
|
6
|
+
useRuntimeUpdateStore,
|
|
7
|
+
type ReleaseNotesLink,
|
|
8
|
+
type RuntimeUpdateBusyAction
|
|
9
|
+
} from '@/features/system-status';
|
|
3
10
|
import { useAppMeta } from '@/shared/hooks/use-app-meta';
|
|
4
11
|
import type { ReactNode } from 'react';
|
|
5
12
|
import { RuntimeStatusEntry } from '@/app/components/layout/runtime-status-entry';
|
|
6
13
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shared/components/ui/tooltip';
|
|
14
|
+
import { hostCapabilityManager } from '@/shared/lib/host-capabilities';
|
|
7
15
|
import { t } from '@/shared/lib/i18n';
|
|
8
16
|
import { cn } from '@/shared/lib/utils';
|
|
17
|
+
import { ExternalLink } from 'lucide-react';
|
|
9
18
|
|
|
10
19
|
type BrandHeaderProps = {
|
|
11
20
|
className?: string;
|
|
@@ -15,9 +24,11 @@ type BrandHeaderProps = {
|
|
|
15
24
|
|
|
16
25
|
export function BrandHeader({ className, density = 'sidebar', suffix }: BrandHeaderProps) {
|
|
17
26
|
const { data } = useAppMeta();
|
|
27
|
+
const { supported, busyAction, snapshot } = useRuntimeUpdateStore();
|
|
18
28
|
const productName = data?.name ?? 'NextClaw';
|
|
19
29
|
const productVersion = data?.productVersion?.trim();
|
|
20
30
|
const versionLabel = productVersion ? `v${productVersion}` : null;
|
|
31
|
+
const releaseNotesLink = useCurrentVersionReleaseNotesLink(productVersion);
|
|
21
32
|
const resolvedSuffix = suffix ?? <RuntimeStatusEntry />;
|
|
22
33
|
const shouldReserveMacWindowControls = typeof window !== 'undefined' && window.nextclawDesktop?.platform === 'darwin';
|
|
23
34
|
const isChromeDensity = density === 'chrome';
|
|
@@ -42,9 +53,9 @@ export function BrandHeader({ className, density = 'sidebar', suffix }: BrandHea
|
|
|
42
53
|
>
|
|
43
54
|
{productName}
|
|
44
55
|
</span>
|
|
45
|
-
{versionLabel ? <BrandVersionLabel versionLabel={versionLabel} density={density} /> : null}
|
|
56
|
+
{versionLabel ? <BrandVersionLabel versionLabel={versionLabel} density={density} releaseNotesLink={releaseNotesLink} /> : null}
|
|
46
57
|
</div>
|
|
47
|
-
<RuntimeUpdateInlineStatus />
|
|
58
|
+
<RuntimeUpdateInlineStatus supported={supported} busyAction={busyAction} snapshot={snapshot} />
|
|
48
59
|
{resolvedSuffix ? <span className="inline-flex items-center shrink-0">{resolvedSuffix}</span> : null}
|
|
49
60
|
</div>
|
|
50
61
|
</div>
|
|
@@ -54,68 +65,143 @@ export function BrandHeader({ className, density = 'sidebar', suffix }: BrandHea
|
|
|
54
65
|
function BrandVersionLabel({
|
|
55
66
|
versionLabel,
|
|
56
67
|
density,
|
|
68
|
+
releaseNotesLink,
|
|
57
69
|
}: {
|
|
58
70
|
versionLabel: string;
|
|
59
71
|
density: BrandHeaderProps['density'];
|
|
72
|
+
releaseNotesLink: ReleaseNotesLink | null;
|
|
60
73
|
}) {
|
|
61
74
|
const isChromeDensity = density === 'chrome';
|
|
75
|
+
const triggerClassName = cn(
|
|
76
|
+
'block min-w-0 flex-1 truncate font-medium text-gray-500 outline-none',
|
|
77
|
+
isChromeDensity ? 'text-[12px]' : 'text-[12px]',
|
|
78
|
+
releaseNotesLink && 'cursor-pointer rounded-sm transition-colors hover:text-gray-700 hover:underline focus-visible:ring-2 focus-visible:ring-gray-300'
|
|
79
|
+
);
|
|
80
|
+
const tooltip = releaseNotesLink
|
|
81
|
+
? [
|
|
82
|
+
t('desktopUpdatesVersionTooltipCurrent').replace('{version}', versionLabel),
|
|
83
|
+
t('desktopUpdatesVersionTooltipReleaseNotes').replace('{version}', releaseNotesLink.versionLabel)
|
|
84
|
+
].join('\n')
|
|
85
|
+
: versionLabel;
|
|
86
|
+
const trigger = releaseNotesLink ? (
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
aria-label={t('desktopUpdatesVersionReleaseNotesLabel').replace('{currentVersion}', versionLabel).replace('{releaseVersion}', releaseNotesLink.versionLabel)}
|
|
90
|
+
className={cn(triggerClassName, 'border-0 bg-transparent p-0 text-left')}
|
|
91
|
+
onClick={() => void hostCapabilityManager.openExternalUrl(releaseNotesLink.url)}
|
|
92
|
+
>
|
|
93
|
+
{versionLabel}
|
|
94
|
+
</button>
|
|
95
|
+
) : (
|
|
96
|
+
<span tabIndex={0} aria-label={versionLabel} className={triggerClassName}>
|
|
97
|
+
{versionLabel}
|
|
98
|
+
</span>
|
|
99
|
+
);
|
|
62
100
|
|
|
63
101
|
return (
|
|
64
102
|
<TooltipProvider delayDuration={250}>
|
|
65
103
|
<Tooltip>
|
|
66
|
-
<TooltipTrigger asChild>
|
|
67
|
-
|
|
68
|
-
tabIndex={0}
|
|
69
|
-
aria-label={versionLabel}
|
|
70
|
-
className={cn(
|
|
71
|
-
'block min-w-0 flex-1 truncate font-medium text-gray-500 outline-none',
|
|
72
|
-
isChromeDensity ? 'text-[12px]' : 'text-[12px]',
|
|
73
|
-
)}
|
|
74
|
-
>
|
|
75
|
-
{versionLabel}
|
|
76
|
-
</span>
|
|
77
|
-
</TooltipTrigger>
|
|
78
|
-
<TooltipContent side="bottom" className="text-xs">{versionLabel}</TooltipContent>
|
|
104
|
+
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
|
105
|
+
<TooltipContent side="bottom" className="max-w-72 whitespace-pre-line text-xs leading-relaxed">{tooltip}</TooltipContent>
|
|
79
106
|
</Tooltip>
|
|
80
107
|
</TooltipProvider>
|
|
81
108
|
);
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
function RuntimeUpdateInlineStatus(
|
|
85
|
-
|
|
111
|
+
function RuntimeUpdateInlineStatus({
|
|
112
|
+
supported,
|
|
113
|
+
busyAction,
|
|
114
|
+
snapshot,
|
|
115
|
+
}: {
|
|
116
|
+
supported: boolean;
|
|
117
|
+
busyAction: RuntimeUpdateBusyAction;
|
|
118
|
+
snapshot: UpdateSnapshot | null;
|
|
119
|
+
}) {
|
|
86
120
|
if (!supported || !snapshot) {
|
|
87
121
|
return null;
|
|
88
122
|
}
|
|
89
|
-
|
|
123
|
+
const releaseNotesLink = resolveUpdateReleaseNotesLink(snapshot);
|
|
124
|
+
if (snapshot.status === 'downloading') {
|
|
125
|
+
return (
|
|
126
|
+
<RuntimeUpdateInlineGroup>
|
|
127
|
+
<RuntimeUpdateInlineBadge snapshot={snapshot} />
|
|
128
|
+
{releaseNotesLink ? <RuntimeUpdateReleaseNotesButton link={releaseNotesLink} tone="amber" /> : null}
|
|
129
|
+
</RuntimeUpdateInlineGroup>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (snapshot.status === 'blocked' || snapshot.status === 'failed') {
|
|
90
133
|
return <RuntimeUpdateInlineBadge snapshot={snapshot} />;
|
|
91
134
|
}
|
|
92
135
|
if (snapshot.status === 'downloaded') {
|
|
93
136
|
return (
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
<RuntimeUpdateInlineGroup>
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
className="inline-flex h-5 shrink-0 items-center rounded-full bg-emerald-50 px-2 text-[11px] font-semibold leading-none text-emerald-700 ring-1 ring-emerald-100 transition-colors hover:bg-emerald-100 disabled:opacity-70"
|
|
141
|
+
disabled={busyAction === 'applying'}
|
|
142
|
+
onClick={() => void runtimeUpdateManager.applyDownloadedUpdate()}
|
|
143
|
+
>
|
|
144
|
+
{busyAction === 'applying' ? t('desktopUpdatesInlineApplying') : t('desktopUpdatesInlineReady')}
|
|
145
|
+
</button>
|
|
146
|
+
{releaseNotesLink ? <RuntimeUpdateReleaseNotesButton link={releaseNotesLink} tone="emerald" /> : null}
|
|
147
|
+
</RuntimeUpdateInlineGroup>
|
|
102
148
|
);
|
|
103
149
|
}
|
|
104
150
|
if (snapshot.status === 'update-available') {
|
|
105
151
|
return (
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
152
|
+
<RuntimeUpdateInlineGroup>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className="inline-flex h-5 shrink-0 items-center rounded-full bg-amber-50 px-2 text-[11px] font-semibold leading-none text-amber-700 ring-1 ring-amber-100 transition-colors hover:bg-amber-100 disabled:opacity-70"
|
|
156
|
+
disabled={busyAction === 'downloading'}
|
|
157
|
+
onClick={() => void runtimeUpdateManager.downloadUpdate()}
|
|
158
|
+
>
|
|
159
|
+
{busyAction === 'downloading' ? t('desktopUpdatesInlineDownloading') : t('desktopUpdatesInlineDownload')}
|
|
160
|
+
</button>
|
|
161
|
+
{releaseNotesLink ? <RuntimeUpdateReleaseNotesButton link={releaseNotesLink} tone="amber" /> : null}
|
|
162
|
+
</RuntimeUpdateInlineGroup>
|
|
114
163
|
);
|
|
115
164
|
}
|
|
116
165
|
return null;
|
|
117
166
|
}
|
|
118
167
|
|
|
168
|
+
function RuntimeUpdateInlineGroup({ children }: { children: ReactNode }) {
|
|
169
|
+
return <span className="inline-flex shrink-0 items-center gap-1">{children}</span>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function RuntimeUpdateReleaseNotesButton({
|
|
173
|
+
link,
|
|
174
|
+
tone,
|
|
175
|
+
}: {
|
|
176
|
+
link: ReleaseNotesLink;
|
|
177
|
+
tone: 'amber' | 'emerald';
|
|
178
|
+
}) {
|
|
179
|
+
const label = t('desktopUpdatesUpdateReleaseNotesLabel').replace('{version}', link.versionLabel);
|
|
180
|
+
const className = cn(
|
|
181
|
+
'inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[11px] ring-1 transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
182
|
+
tone === 'emerald'
|
|
183
|
+
? 'bg-emerald-50 text-emerald-700 ring-emerald-100 hover:bg-emerald-100 focus-visible:ring-emerald-300'
|
|
184
|
+
: 'bg-amber-50 text-amber-700 ring-amber-100 hover:bg-amber-100 focus-visible:ring-amber-300'
|
|
185
|
+
);
|
|
186
|
+
return (
|
|
187
|
+
<TooltipProvider delayDuration={250}>
|
|
188
|
+
<Tooltip>
|
|
189
|
+
<TooltipTrigger asChild>
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
aria-label={label}
|
|
193
|
+
className={className}
|
|
194
|
+
onClick={() => void hostCapabilityManager.openExternalUrl(link.url)}
|
|
195
|
+
>
|
|
196
|
+
<ExternalLink className="h-3 w-3" aria-hidden="true" />
|
|
197
|
+
</button>
|
|
198
|
+
</TooltipTrigger>
|
|
199
|
+
<TooltipContent side="bottom" className="text-xs">{label}</TooltipContent>
|
|
200
|
+
</Tooltip>
|
|
201
|
+
</TooltipProvider>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
119
205
|
function RuntimeUpdateInlineBadge({ snapshot }: { snapshot: UpdateSnapshot }) {
|
|
120
206
|
if (snapshot.status === 'blocked' || snapshot.status === 'failed') {
|
|
121
207
|
return <RuntimeUpdateIssueIcon snapshot={snapshot} />;
|
|
@@ -5,6 +5,9 @@ import { setLanguage } from '@/shared/lib/i18n';
|
|
|
5
5
|
|
|
6
6
|
const mocks = vi.hoisted(() => ({
|
|
7
7
|
useServerPathBrowse: vi.fn(),
|
|
8
|
+
useServerPathCreateDirectory: vi.fn(),
|
|
9
|
+
createDirectory: vi.fn(),
|
|
10
|
+
resetCreateDirectory: vi.fn(),
|
|
8
11
|
refetch: vi.fn(),
|
|
9
12
|
}));
|
|
10
13
|
|
|
@@ -12,21 +15,75 @@ vi.mock('@/shared/hooks/use-server-path-browse', () => ({
|
|
|
12
15
|
useServerPathBrowse: mocks.useServerPathBrowse,
|
|
13
16
|
}));
|
|
14
17
|
|
|
18
|
+
vi.mock('@/shared/hooks/use-server-path-create-directory', () => ({
|
|
19
|
+
useServerPathCreateDirectory: mocks.useServerPathCreateDirectory,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
type PickerOverrides = {
|
|
23
|
+
currentPath?: string;
|
|
24
|
+
defaultWorkspacePath?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
open?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const createPickerElement = (overrides?: PickerOverrides) => (
|
|
30
|
+
<ServerPathPickerDialog
|
|
31
|
+
open={overrides?.open ?? true}
|
|
32
|
+
currentPath={overrides?.currentPath ?? '/workspace'}
|
|
33
|
+
defaultWorkspacePath={overrides?.defaultWorkspacePath}
|
|
34
|
+
isSaving={false}
|
|
35
|
+
onOpenChange={() => {}}
|
|
36
|
+
onConfirm={() => {}}
|
|
37
|
+
title="选择目录"
|
|
38
|
+
description={overrides?.description ?? '选择一个目录'}
|
|
39
|
+
pathLabel="目录"
|
|
40
|
+
confirmLabel="确定"
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const renderPicker = (overrides?: PickerOverrides) => render(createPickerElement(overrides));
|
|
45
|
+
|
|
15
46
|
describe('ServerPathPickerDialog', () => {
|
|
16
47
|
beforeEach(() => {
|
|
17
48
|
setLanguage('zh');
|
|
18
49
|
mocks.refetch.mockReset();
|
|
50
|
+
mocks.refetch.mockResolvedValue(undefined);
|
|
51
|
+
mocks.createDirectory.mockReset();
|
|
52
|
+
mocks.createDirectory.mockResolvedValue({ path: '/workspace/new-folder' });
|
|
53
|
+
mocks.resetCreateDirectory.mockReset();
|
|
54
|
+
mocks.useServerPathCreateDirectory.mockReturnValue({
|
|
55
|
+
mutateAsync: mocks.createDirectory,
|
|
56
|
+
reset: mocks.resetCreateDirectory,
|
|
57
|
+
isPending: false,
|
|
58
|
+
error: null,
|
|
59
|
+
});
|
|
19
60
|
mocks.useServerPathBrowse.mockReturnValue({
|
|
20
61
|
data: {
|
|
21
62
|
currentPath: '/workspace',
|
|
22
63
|
homePath: '/Users/peiwang',
|
|
23
64
|
parentPath: '/',
|
|
24
|
-
breadcrumbs: [
|
|
25
|
-
{ label: 'workspace', path: '/workspace' },
|
|
26
|
-
],
|
|
65
|
+
breadcrumbs: [{ label: 'workspace', path: '/workspace' }],
|
|
27
66
|
entries: [
|
|
28
|
-
{
|
|
29
|
-
|
|
67
|
+
{
|
|
68
|
+
name: 'playground',
|
|
69
|
+
path: '/workspace/playground',
|
|
70
|
+
kind: 'directory',
|
|
71
|
+
hidden: false,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'nextbot',
|
|
75
|
+
path: '/workspace/nextbot',
|
|
76
|
+
kind: 'directory',
|
|
77
|
+
hidden: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
locations: [
|
|
81
|
+
{ kind: 'desktop', path: '/Users/peiwang/Desktop' },
|
|
82
|
+
{ kind: 'documents', path: '/Users/peiwang/Documents' },
|
|
83
|
+
{ kind: 'downloads', path: '/Users/peiwang/Downloads' },
|
|
84
|
+
{ kind: 'icloud-drive', path: '/Users/peiwang/Library/Mobile Documents/com~apple~CloudDocs' },
|
|
85
|
+
{ kind: 'applications', path: '/Applications' },
|
|
86
|
+
{ kind: 'volumes', path: '/Volumes' },
|
|
30
87
|
],
|
|
31
88
|
},
|
|
32
89
|
isLoading: false,
|
|
@@ -37,55 +94,248 @@ describe('ServerPathPickerDialog', () => {
|
|
|
37
94
|
|
|
38
95
|
it('filters entries inside the current directory', async () => {
|
|
39
96
|
const user = userEvent.setup();
|
|
40
|
-
|
|
41
|
-
render(
|
|
42
|
-
<ServerPathPickerDialog
|
|
43
|
-
open
|
|
44
|
-
currentPath="/workspace"
|
|
45
|
-
isSaving={false}
|
|
46
|
-
onOpenChange={() => {}}
|
|
47
|
-
onConfirm={() => {}}
|
|
48
|
-
title="选择目录"
|
|
49
|
-
description="选择一个目录"
|
|
50
|
-
pathLabel="目录"
|
|
51
|
-
confirmLabel="确定"
|
|
52
|
-
/>
|
|
53
|
-
);
|
|
97
|
+
renderPicker();
|
|
54
98
|
|
|
55
99
|
expect(screen.getByText('playground')).toBeTruthy();
|
|
56
100
|
expect(screen.getByText('nextbot')).toBeTruthy();
|
|
57
101
|
|
|
58
|
-
await user.type(screen.
|
|
102
|
+
await user.type(screen.getAllByPlaceholderText('搜索当前目录')[0], 'play');
|
|
59
103
|
|
|
60
104
|
expect(screen.getByText('playground')).toBeTruthy();
|
|
61
105
|
expect(screen.queryByText('nextbot')).toBeNull();
|
|
62
106
|
});
|
|
63
107
|
|
|
64
|
-
it('shows
|
|
108
|
+
it('shows server-backed Mac locations and opens sidebar locations with one click', async () => {
|
|
65
109
|
const user = userEvent.setup();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
renderPicker();
|
|
111
|
+
|
|
112
|
+
expect(screen.getByRole('button', { name: '桌面' })).toBeTruthy();
|
|
113
|
+
expect(screen.getByRole('button', { name: '文稿' })).toBeTruthy();
|
|
114
|
+
expect(screen.getByRole('button', { name: '下载' })).toBeTruthy();
|
|
115
|
+
expect(screen.getByRole('button', { name: 'iCloud 云盘' })).toBeTruthy();
|
|
116
|
+
expect(screen.getByRole('button', { name: '应用程序' })).toBeTruthy();
|
|
117
|
+
expect(screen.getByRole('button', { name: '磁盘与卷' })).toBeTruthy();
|
|
118
|
+
|
|
119
|
+
await user.click(screen.getByRole('button', { name: '下载' }));
|
|
120
|
+
|
|
121
|
+
expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
|
|
122
|
+
path: '/Users/peiwang/Downloads',
|
|
123
|
+
enabled: true,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('keeps the explicit current path when an older home browse result is still visible', () => {
|
|
128
|
+
mocks.useServerPathBrowse.mockReturnValue({
|
|
129
|
+
data: {
|
|
130
|
+
currentPath: '/Users/peiwang',
|
|
131
|
+
homePath: '/Users/peiwang',
|
|
132
|
+
parentPath: '/Users',
|
|
133
|
+
breadcrumbs: [
|
|
134
|
+
{ label: 'Users', path: '/Users' },
|
|
135
|
+
{ label: 'peiwang', path: '/Users/peiwang' },
|
|
136
|
+
],
|
|
137
|
+
entries: [],
|
|
138
|
+
},
|
|
139
|
+
isLoading: false,
|
|
140
|
+
error: null,
|
|
141
|
+
refetch: mocks.refetch,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
renderPicker({ currentPath: '/Users/peiwang/.nextclaw/workspace' });
|
|
145
|
+
|
|
146
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
|
|
147
|
+
'/Users/peiwang/.nextclaw/workspace',
|
|
78
148
|
);
|
|
149
|
+
expect(screen.getByText('/Users/peiwang/.nextclaw/workspace')).toBeTruthy();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('starts at the workdir and keeps the canonical address and selected folder aligned', () => {
|
|
153
|
+
mocks.useServerPathBrowse.mockReturnValue({
|
|
154
|
+
data: {
|
|
155
|
+
currentPath: '/Users/peiwang/.nextclaw/workspace',
|
|
156
|
+
homePath: '/Users/peiwang',
|
|
157
|
+
parentPath: '/Users/peiwang/.nextclaw',
|
|
158
|
+
breadcrumbs: [
|
|
159
|
+
{ label: '/', path: '/' },
|
|
160
|
+
{ label: 'Users', path: '/Users' },
|
|
161
|
+
{ label: 'peiwang', path: '/Users/peiwang' },
|
|
162
|
+
{ label: '.nextclaw', path: '/Users/peiwang/.nextclaw' },
|
|
163
|
+
{ label: 'workspace', path: '/Users/peiwang/.nextclaw/workspace' },
|
|
164
|
+
],
|
|
165
|
+
entries: [],
|
|
166
|
+
},
|
|
167
|
+
isLoading: false,
|
|
168
|
+
error: null,
|
|
169
|
+
refetch: mocks.refetch,
|
|
170
|
+
});
|
|
79
171
|
|
|
80
|
-
|
|
172
|
+
renderPicker({
|
|
173
|
+
currentPath: '',
|
|
174
|
+
defaultWorkspacePath: '~/.nextclaw/workspace',
|
|
175
|
+
description: undefined,
|
|
176
|
+
});
|
|
81
177
|
|
|
82
|
-
expect(
|
|
178
|
+
expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
|
|
179
|
+
path: '~/.nextclaw/workspace',
|
|
180
|
+
enabled: true,
|
|
181
|
+
});
|
|
182
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
|
|
183
|
+
'/Users/peiwang/.nextclaw/workspace',
|
|
184
|
+
);
|
|
185
|
+
expect(screen.getByRole('button', { name: 'NextClaw 工作区' })).toBeTruthy();
|
|
186
|
+
expect(screen.getByText('workspace')).toBeTruthy();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('normalizes a home-relative workdir with Windows separators', () => {
|
|
190
|
+
mocks.useServerPathBrowse.mockReturnValue({
|
|
191
|
+
data: {
|
|
192
|
+
currentPath: 'C:\\Users\\alice\\.nextclaw\\workspace',
|
|
193
|
+
homePath: 'C:\\Users\\alice',
|
|
194
|
+
parentPath: 'C:\\Users\\alice\\.nextclaw',
|
|
195
|
+
breadcrumbs: [
|
|
196
|
+
{ label: 'C:', path: 'C:\\' },
|
|
197
|
+
{ label: 'Users', path: 'C:\\Users' },
|
|
198
|
+
{ label: 'alice', path: 'C:\\Users\\alice' },
|
|
199
|
+
{ label: '.nextclaw', path: 'C:\\Users\\alice\\.nextclaw' },
|
|
200
|
+
{ label: 'workspace', path: 'C:\\Users\\alice\\.nextclaw\\workspace' },
|
|
201
|
+
],
|
|
202
|
+
entries: [],
|
|
203
|
+
locations: [],
|
|
204
|
+
},
|
|
205
|
+
isLoading: false,
|
|
206
|
+
error: null,
|
|
207
|
+
refetch: mocks.refetch,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
renderPicker({ currentPath: '', defaultWorkspacePath: '~/.nextclaw/workspace' });
|
|
211
|
+
|
|
212
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
|
|
213
|
+
'C:\\Users\\alice\\.nextclaw\\workspace',
|
|
214
|
+
);
|
|
215
|
+
expect(screen.getByText('workspace')).toBeTruthy();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('creates a folder in the current directory and selects it without entering it', async () => {
|
|
219
|
+
const user = userEvent.setup();
|
|
220
|
+
renderPicker();
|
|
221
|
+
|
|
222
|
+
await user.click(screen.getByRole('button', { name: '新建文件夹' }));
|
|
223
|
+
await user.type(screen.getByLabelText('文件夹名称'), 'new-folder');
|
|
224
|
+
await user.click(screen.getByRole('button', { name: '创建' }));
|
|
225
|
+
|
|
226
|
+
expect(mocks.createDirectory).toHaveBeenCalledWith({
|
|
227
|
+
parentPath: '/workspace',
|
|
228
|
+
name: 'new-folder',
|
|
229
|
+
});
|
|
230
|
+
expect(mocks.refetch).toHaveBeenCalledOnce();
|
|
231
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
|
|
232
|
+
'/workspace/new-folder',
|
|
233
|
+
);
|
|
234
|
+
expect((screen.getByRole('button', { name: '后退' }) as HTMLButtonElement).disabled).toBe(
|
|
235
|
+
true,
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('uses single click to select and double click to enter a folder', async () => {
|
|
240
|
+
const user = userEvent.setup();
|
|
241
|
+
renderPicker();
|
|
242
|
+
const playground = screen.getByRole('option', { name: /playground/ });
|
|
243
|
+
|
|
244
|
+
expect(screen.getByText('单击选择,双击进入')).toBeTruthy();
|
|
245
|
+
|
|
246
|
+
await user.click(playground);
|
|
247
|
+
|
|
248
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
|
|
249
|
+
'/workspace/playground',
|
|
250
|
+
);
|
|
251
|
+
expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
|
|
252
|
+
path: '/workspace',
|
|
253
|
+
enabled: true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await user.dblClick(playground);
|
|
257
|
+
|
|
258
|
+
expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
|
|
259
|
+
path: '/workspace/playground',
|
|
260
|
+
enabled: true,
|
|
261
|
+
});
|
|
262
|
+
expect((screen.getByRole('button', { name: '后退' }) as HTMLButtonElement).disabled).toBe(
|
|
263
|
+
false,
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('starts a fresh selection session when the dialog is reopened', async () => {
|
|
268
|
+
const user = userEvent.setup();
|
|
269
|
+
const view = renderPicker();
|
|
83
270
|
|
|
271
|
+
await user.click(screen.getByRole('option', { name: /playground/ }));
|
|
272
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
|
|
273
|
+
'/workspace/playground',
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
view.rerender(createPickerElement({ open: false }));
|
|
277
|
+
view.rerender(createPickerElement());
|
|
278
|
+
|
|
279
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe('/workspace');
|
|
280
|
+
expect((screen.getByRole('button', { name: '后退' }) as HTMLButtonElement).disabled).toBe(
|
|
281
|
+
true,
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('opens an editable address and records navigation history', async () => {
|
|
286
|
+
const user = userEvent.setup();
|
|
287
|
+
renderPicker();
|
|
288
|
+
|
|
289
|
+
await user.click(screen.getByRole('button', { name: '编辑地址' }));
|
|
290
|
+
const addressInput = screen.getByLabelText('地址');
|
|
291
|
+
await user.clear(addressInput);
|
|
292
|
+
await user.type(addressInput, '/workspace/playground{Enter}');
|
|
293
|
+
|
|
294
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
|
|
295
|
+
'/workspace/playground',
|
|
296
|
+
);
|
|
297
|
+
expect((screen.getByRole('button', { name: '后退' }) as HTMLButtonElement).disabled).toBe(
|
|
298
|
+
false,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
await user.click(screen.getByRole('button', { name: '后退' }));
|
|
302
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe('/workspace');
|
|
303
|
+
expect((screen.getByRole('button', { name: '前进' }) as HTMLButtonElement).disabled).toBe(
|
|
304
|
+
false,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('supports arrow selection and Enter navigation from the directory list', async () => {
|
|
309
|
+
const user = userEvent.setup();
|
|
310
|
+
renderPicker();
|
|
311
|
+
const playground = screen.getByRole('option', { name: /playground/ });
|
|
312
|
+
|
|
313
|
+
await user.click(playground);
|
|
314
|
+
await user.keyboard('{ArrowDown}');
|
|
315
|
+
|
|
316
|
+
expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
|
|
317
|
+
'/workspace/nextbot',
|
|
318
|
+
);
|
|
319
|
+
await user.keyboard('{Enter}');
|
|
320
|
+
expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
|
|
321
|
+
path: '/workspace/nextbot',
|
|
322
|
+
enabled: true,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('shows the empty-search hint and keeps the list region scroll-contained', async () => {
|
|
327
|
+
const user = userEvent.setup();
|
|
328
|
+
renderPicker();
|
|
329
|
+
|
|
330
|
+
await user.type(screen.getAllByPlaceholderText('搜索当前目录')[0], 'missing');
|
|
331
|
+
|
|
332
|
+
expect(screen.getByText('当前目录下没有匹配结果。')).toBeTruthy();
|
|
84
333
|
const dialog = screen.getByRole('dialog');
|
|
85
334
|
expect(dialog.className).toContain('overflow-hidden');
|
|
86
|
-
expect(dialog.className).toContain('sm:h-[
|
|
87
|
-
|
|
88
|
-
|
|
335
|
+
expect(dialog.className).toContain('sm:h-[44rem]');
|
|
336
|
+
const scrollArea = screen
|
|
337
|
+
.getByText('当前目录下没有匹配结果。')
|
|
338
|
+
.closest('.overflow-auto');
|
|
89
339
|
expect(scrollArea?.className).toContain('flex-1');
|
|
90
340
|
expect(scrollArea?.className).toContain('min-h-0');
|
|
91
341
|
});
|