@nextclaw/ui 0.15.0 → 0.15.2
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 +50 -0
- package/dist/assets/{api-DLtsiBKm.js → api-B7Qigvtm.js} +2 -2
- package/dist/assets/{appearance-settings-page-CtkeYXux.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
- package/dist/assets/{book-open-BQwWBDKb.js → book-open-Ba2f7YiG.js} +1 -1
- package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
- package/dist/assets/chat-page-C_5dcuE3.js +1 -0
- package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
- package/dist/assets/{confirm-dialog-iIJFFp2T.js → confirm-dialog-D2EG3nqE.js} +1 -1
- package/dist/assets/desktop-update-config-BGR3HUi1.js +1 -0
- package/dist/assets/{dist-D2eZC3OR.js → dist-7JTjycxE.js} +1 -1
- package/dist/assets/{dist-CBxWOazz.js → dist-wXLhk4hl.js} +1 -1
- package/dist/assets/doc-browser-bHyXnxlj.js +1 -0
- package/dist/assets/doc-browser-context-CYAGm5d5.js +1 -0
- package/dist/assets/doc-browser-gTrQ3jCF.js +1 -0
- package/dist/assets/{es2015-DhQo_oS6.js → es2015-j1SRCIX5.js} +1 -1
- package/dist/assets/{external-link-CM5dnUI6.js → external-link-DxuPaDoK.js} +1 -1
- package/dist/assets/form-actions-BztL7P_T.js +1 -0
- package/dist/assets/{i18n-provider-di1waTGX.js → i18n-provider-B4oMxOFx.js} +1 -1
- package/dist/assets/index-CvrvblgX.css +1 -0
- package/dist/assets/index-Obd4JYU2.js +104 -0
- package/dist/assets/loader-circle-CdzgK4eH.js +1 -0
- package/dist/assets/{mcp-marketplace-page-C_MVQUbT.js → mcp-marketplace-page-Bt7CuOoq.js} +2 -2
- package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +1 -0
- package/dist/assets/model-config-page-DeKk9gvE.js +1 -0
- package/dist/assets/plus-Bx7wQbhG.js +1 -0
- package/dist/assets/{provider-scoped-model-input-g1WkGiJ8.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
- package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
- package/dist/assets/{react-DjpzoZXB.js → react-UirzUuST.js} +1 -1
- package/dist/assets/{refresh-cw-DaTv3zxE.js → refresh-cw-eT8BF-8B.js} +1 -1
- package/dist/assets/remote-CDkHZtA8.js +1 -0
- package/dist/assets/rotate-cw-1JqxHTuw.js +1 -0
- package/dist/assets/runtime-config-page-BelAqiNG.js +1 -0
- package/dist/assets/{save-BZof1uie.js → save-Cb2g0BOi.js} +1 -1
- package/dist/assets/{search-MxAarS_g.js → search-BcHjwdsb.js} +1 -1
- package/dist/assets/search-config-page-zPm6--Ig.js +1 -0
- package/dist/assets/secrets-config-page-Cxyp-KuQ.js +3 -0
- package/dist/assets/security-config-B_gCq9fn.js +1 -0
- package/dist/assets/{select-D7GcNP0z.js → select-CdSEOwVM.js} +1 -1
- package/dist/assets/{setting-row-CuozlnJd.js → setting-row-CTAtGexr.js} +1 -1
- package/dist/assets/settings-2-2DWauOjY.js +1 -0
- package/dist/assets/{side-dock-BcLau5S_.js → side-dock-DPvakJ0a.js} +2 -2
- package/dist/assets/{skeleton-shfmXuYp.js → skeleton-P5oM14E1.js} +1 -1
- package/dist/assets/{switch-C1BWMU5c.js → switch-BU_UEULb.js} +1 -1
- package/dist/assets/{tag-chip-DCUrQUCP.js → tag-chip-D-WsKoT1.js} +1 -1
- package/dist/assets/{tooltip-DfAHHA76.js → tooltip-CjkOaHW7.js} +1 -1
- package/dist/assets/{use-config-DAdbT5QJ.js → use-config--hx5oxQn.js} +1 -1
- package/dist/assets/{use-confirm-dialog-eWcJ6V-s.js → use-confirm-dialog-CD9a_SK1.js} +1 -1
- package/dist/assets/x-DT5x45JU.js +1 -0
- package/dist/index.html +38 -30
- package/index.html +12 -4
- package/package.json +9 -9
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
- package/src/app/components/layout/sidebar-items.tsx +43 -4
- package/src/app/components/layout/sidebar.tsx +83 -61
- package/src/app/configs/app-navigation.config.ts +49 -0
- package/src/app/styles/design-system.css +82 -0
- package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
- package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
- package/src/features/agents/components/agent-details-dialog.tsx +398 -0
- package/src/features/agents/components/agent-dialogs.tsx +20 -47
- package/src/features/agents/components/agents-page.tsx +76 -10
- package/src/features/agents/types/agent-form.types.ts +21 -0
- package/src/features/agents/utils/agent-form.utils.ts +39 -0
- package/src/features/channels/components/config/channel-form.tsx +14 -11
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +172 -1
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +11 -1
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
- package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +29 -30
- package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +18 -3
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +1 -1
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +23 -1
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
- package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
- package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
- package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +193 -56
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +67 -14
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
- package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +47 -0
- package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
- package/src/features/chat/managers/chat-thread.manager.ts +8 -2
- package/src/features/chat/stores/chat-session-list.store.ts +28 -5
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
- package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
- package/src/features/panel-apps/index.ts +1 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
- package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
- package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
- package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
- package/src/features/settings/components/config/provider-form.tsx +11 -8
- package/src/features/settings/components/config/provider-models-section.tsx +7 -7
- package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
- package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
- package/src/features/settings/pages/model-config-page.tsx +26 -25
- package/src/features/settings/pages/providers-config-page.tsx +10 -13
- package/src/features/settings/pages/search-config-page.tsx +8 -5
- package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
- package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
- package/src/features/system-status/components/desktop-update-config.tsx +91 -2
- package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
- package/src/index.css +18 -0
- package/src/shared/components/config-split-page.tsx +6 -6
- package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +5 -1
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
- package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
- package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
- package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
- package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
- package/src/shared/components/ui/actions/form-actions.tsx +25 -0
- package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
- package/src/shared/components/ui/card.tsx +12 -4
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
- package/src/shared/hooks/use-server-path-browse.ts +26 -4
- package/src/shared/hooks/use-server-path-read.ts +11 -1
- package/src/shared/lib/api/ncp-session.types.ts +1 -1
- package/src/shared/lib/api/types.ts +12 -3
- package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
- package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +19 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +19 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
- package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
- package/src/shared/lib/theme/index.test.ts +21 -0
- package/src/shared/lib/theme/index.ts +6 -4
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/channels-list-page-Cv8o9Hxg.js +0 -8
- package/dist/assets/chat-page-BtA7H1Ea.js +0 -1
- package/dist/assets/config-split-page-EidRBpP2.js +0 -1
- package/dist/assets/desktop-update-config-C6n_TB1r.js +0 -1
- package/dist/assets/doc-browser-B76XOXmW.js +0 -1
- package/dist/assets/doc-browser-DBnj9UMB.js +0 -1
- package/dist/assets/doc-browser-context-Sv-044Cl.js +0 -1
- package/dist/assets/ellipsis-2uzhypw5.js +0 -1
- package/dist/assets/index-CBRP-7-a.css +0 -1
- package/dist/assets/index-Z1fYpbNd.js +0 -104
- package/dist/assets/loader-circle-BjmN5DeJ.js +0 -1
- package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +0 -1
- package/dist/assets/model-config-page-Cdr9P6TQ.js +0 -1
- package/dist/assets/plus-3ztRZD_l.js +0 -1
- package/dist/assets/providers-config-page-CWlO7J4A.js +0 -1
- package/dist/assets/remote-CKWZfVVB.js +0 -1
- package/dist/assets/rotate-cw-CHcaRUDz.js +0 -1
- package/dist/assets/runtime-config-page-CfTqh1nv.js +0 -1
- package/dist/assets/search-config-page-Dm8daYny.js +0 -1
- package/dist/assets/secrets-config-page-D8AbcR05.js +0 -3
- package/dist/assets/security-config-Ds0LsJmo.js +0 -1
- package/dist/assets/x-DqL1mYLC.js +0 -1
- /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
|
@@ -13,6 +13,7 @@ export type ChatSidebarProjectGroup = {
|
|
|
13
13
|
projectName: string;
|
|
14
14
|
items: NcpSessionListItemView[];
|
|
15
15
|
latestUpdatedAt: number;
|
|
16
|
+
isPinned: boolean;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
export function getSessionActivityAtTimestamp(
|
|
@@ -33,8 +34,19 @@ export function sortSessionItemsByActivityAtDesc(
|
|
|
33
34
|
);
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function sortSessionItemsByPinnedActivityAtDesc(
|
|
38
|
+
items: NcpSessionListItemView[],
|
|
39
|
+
pinnedSessionKeys: ReadonlySet<string>,
|
|
40
|
+
): NcpSessionListItemView[] {
|
|
41
|
+
return [...items].sort((left, right) => {
|
|
42
|
+
const pinnedDifference = Number(pinnedSessionKeys.has(right.session.key)) - Number(pinnedSessionKeys.has(left.session.key));
|
|
43
|
+
return pinnedDifference || getSessionActivityAtTimestamp(right) - getSessionActivityAtTimestamp(left);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
export function groupSessionsByDate(
|
|
37
48
|
items: NcpSessionListItemView[],
|
|
49
|
+
pinnedSessionKeys: ReadonlySet<string> = new Set(),
|
|
38
50
|
): ChatSidebarDateGroup[] {
|
|
39
51
|
const now = new Date();
|
|
40
52
|
const todayStart = new Date(
|
|
@@ -50,7 +62,13 @@ export function groupSessionsByDate(
|
|
|
50
62
|
const previous7: NcpSessionListItemView[] = [];
|
|
51
63
|
const older: NcpSessionListItemView[] = [];
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
const unpinnedItems = items.filter((item) => !pinnedSessionKeys.has(item.session.key));
|
|
66
|
+
const pinnedItems = sortSessionItemsByPinnedActivityAtDesc(
|
|
67
|
+
items.filter((item) => pinnedSessionKeys.has(item.session.key)),
|
|
68
|
+
pinnedSessionKeys,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (const item of unpinnedItems) {
|
|
54
72
|
const ts = getSessionActivityAtTimestamp(item);
|
|
55
73
|
if (ts >= todayStart) {
|
|
56
74
|
today.push(item);
|
|
@@ -64,6 +82,7 @@ export function groupSessionsByDate(
|
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
const groups: ChatSidebarDateGroup[] = [];
|
|
85
|
+
if (pinnedItems.length > 0) groups.push({ label: t("chatSidebarPinned"), items: pinnedItems });
|
|
67
86
|
if (today.length > 0) groups.push({ label: t("chatSidebarToday"), items: today });
|
|
68
87
|
if (yesterday.length > 0)
|
|
69
88
|
groups.push({ label: t("chatSidebarYesterday"), items: yesterday });
|
|
@@ -75,6 +94,8 @@ export function groupSessionsByDate(
|
|
|
75
94
|
|
|
76
95
|
export function groupSessionsByProject(
|
|
77
96
|
items: NcpSessionListItemView[],
|
|
97
|
+
pinnedSessionKeys: ReadonlySet<string> = new Set(),
|
|
98
|
+
pinnedProjectRoots: ReadonlySet<string> = new Set(),
|
|
78
99
|
): ChatSidebarProjectGroup[] {
|
|
79
100
|
const grouped = new Map<string, ChatSidebarProjectGroup>();
|
|
80
101
|
|
|
@@ -101,15 +122,16 @@ export function groupSessionsByProject(
|
|
|
101
122
|
projectRoot,
|
|
102
123
|
items: [item],
|
|
103
124
|
latestUpdatedAt: updatedAt,
|
|
125
|
+
isPinned: pinnedProjectRoots.has(projectRoot),
|
|
104
126
|
});
|
|
105
127
|
}
|
|
106
128
|
|
|
107
129
|
return [...grouped.values()]
|
|
108
130
|
.map((group) => ({
|
|
109
131
|
...group,
|
|
110
|
-
items:
|
|
132
|
+
items: sortSessionItemsByPinnedActivityAtDesc(group.items, pinnedSessionKeys),
|
|
111
133
|
}))
|
|
112
|
-
.sort((left, right) => right.latestUpdatedAt - left.latestUpdatedAt);
|
|
134
|
+
.sort((left, right) => Number(right.isPinned) - Number(left.isPinned) || right.latestUpdatedAt - left.latestUpdatedAt);
|
|
113
135
|
}
|
|
114
136
|
|
|
115
137
|
export function groupChildSessionsByParentKey(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AlarmClock, BrainCircuit, MessageCircle } from 'lucide-react';
|
|
2
|
+
import { cn } from '@/shared/lib/utils';
|
|
2
3
|
import { t } from '@/shared/lib/i18n';
|
|
3
4
|
|
|
4
5
|
const WELCOME_CAPABILITIES = [
|
|
@@ -34,13 +35,17 @@ export function ChatWelcomeCapabilityGrid({
|
|
|
34
35
|
return (
|
|
35
36
|
<button
|
|
36
37
|
key={capability.titleKey}
|
|
38
|
+
type="button"
|
|
37
39
|
onClick={() => onSelectPrompt(t(capability.promptKey))}
|
|
38
|
-
className=
|
|
40
|
+
className={cn(
|
|
41
|
+
'min-w-0 rounded-xl border border-border/75 bg-card p-3 text-left text-card-foreground shadow-none transition-colors sm:p-3.5',
|
|
42
|
+
'hover:bg-muted/40',
|
|
43
|
+
)}
|
|
39
44
|
>
|
|
40
|
-
<div className="mb-
|
|
41
|
-
<Icon className="h-4
|
|
45
|
+
<div className="mb-2.5 flex h-8 w-8 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
|
46
|
+
<Icon className="h-4 w-4" />
|
|
42
47
|
</div>
|
|
43
|
-
<div className="mb-1 text-sm font-
|
|
48
|
+
<div className="mb-1 text-sm font-medium text-foreground">
|
|
44
49
|
{t(capability.titleKey)}
|
|
45
50
|
</div>
|
|
46
51
|
<div className="text-[11px] leading-relaxed text-muted-foreground">
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import { Bot } from 'lucide-react';
|
|
4
3
|
import { ChatSessionProjectDialog } from '@/features/chat/features/session/components/session-header/chat-session-project-dialog';
|
|
5
4
|
import { ChatWelcomeAgentPicker } from '@/features/chat/features/welcome/components/chat-welcome-agent-picker';
|
|
6
5
|
import { ChatWelcomeCapabilityGrid } from '@/features/chat/features/welcome/components/chat-welcome-capability-grid';
|
|
@@ -64,10 +63,6 @@ export function ChatWelcome({
|
|
|
64
63
|
return (
|
|
65
64
|
<div className="flex min-h-full items-center justify-center px-4 py-8 sm:p-8">
|
|
66
65
|
<div className="min-w-0 w-full max-w-[min(760px,100%)]">
|
|
67
|
-
<div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl border border-border bg-accent shadow-card">
|
|
68
|
-
<Bot className="h-8 w-8 text-primary" />
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
66
|
<div className="text-center">
|
|
72
67
|
<h2 className="text-2xl font-semibold text-foreground sm:text-3xl">
|
|
73
68
|
{t('chatWelcomeTitle')}
|
|
@@ -14,6 +14,7 @@ const serverPathBrowseMock = vi.fn();
|
|
|
14
14
|
type RenderWorkspaceFilePreviewOptions = {
|
|
15
15
|
file?: Partial<ChatWorkspaceFileTab>;
|
|
16
16
|
onFileOpen?: (action: ChatFileOpenActionViewModel) => void;
|
|
17
|
+
refreshVersion?: number;
|
|
17
18
|
sessionProjectRoot?: string | null;
|
|
18
19
|
sessionWorkingDir?: string | null;
|
|
19
20
|
};
|
|
@@ -82,6 +83,7 @@ function mockTextRead(overrides: TextReadDataOverrides = {}) {
|
|
|
82
83
|
function renderWorkspaceFilePreview({
|
|
83
84
|
file,
|
|
84
85
|
onFileOpen = vi.fn(),
|
|
86
|
+
refreshVersion,
|
|
85
87
|
sessionProjectRoot = "/tmp",
|
|
86
88
|
sessionWorkingDir = "/tmp",
|
|
87
89
|
}: RenderWorkspaceFilePreviewOptions = {}) {
|
|
@@ -90,6 +92,7 @@ function renderWorkspaceFilePreview({
|
|
|
90
92
|
file={buildWorkspaceFile(file ?? {})}
|
|
91
93
|
sessionProjectRoot={sessionProjectRoot}
|
|
92
94
|
sessionWorkingDir={sessionWorkingDir}
|
|
95
|
+
refreshVersion={refreshVersion}
|
|
93
96
|
onFileOpen={onFileOpen}
|
|
94
97
|
/>,
|
|
95
98
|
);
|
|
@@ -97,55 +100,68 @@ function renderWorkspaceFilePreview({
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
function mockWorkspaceBreadcrumbBrowseTree() {
|
|
100
|
-
serverPathBrowseMock.mockImplementation(
|
|
101
|
-
(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
hidden: false,
|
|
121
|
-
},
|
|
122
|
-
],
|
|
123
|
-
}
|
|
124
|
-
: {
|
|
125
|
-
currentPath: "/tmp/workspace/src",
|
|
126
|
-
parentPath: "/tmp/workspace",
|
|
127
|
-
homePath: "/Users/demo",
|
|
128
|
-
breadcrumbs: [
|
|
129
|
-
{ label: "workspace", path: "/tmp/workspace" },
|
|
130
|
-
{ label: "src", path: "/tmp/workspace/src" },
|
|
131
|
-
],
|
|
132
|
-
entries: [
|
|
133
|
-
{
|
|
134
|
-
name: "components",
|
|
135
|
-
path: "/tmp/workspace/src/components",
|
|
136
|
-
kind: "directory",
|
|
137
|
-
hidden: false,
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
name: "index.ts",
|
|
141
|
-
path: "/tmp/workspace/src/index.ts",
|
|
142
|
-
kind: "file",
|
|
143
|
-
hidden: false,
|
|
144
|
-
},
|
|
145
|
-
],
|
|
103
|
+
serverPathBrowseMock.mockImplementation(({ path }: { path?: string | null }) => {
|
|
104
|
+
if (path === "/tmp/workspace/src/components") {
|
|
105
|
+
return {
|
|
106
|
+
isLoading: false,
|
|
107
|
+
error: null,
|
|
108
|
+
data: {
|
|
109
|
+
currentPath: "/tmp/workspace/src/components",
|
|
110
|
+
parentPath: "/tmp/workspace/src",
|
|
111
|
+
homePath: "/Users/demo",
|
|
112
|
+
breadcrumbs: [
|
|
113
|
+
{ label: "workspace", path: "/tmp/workspace" },
|
|
114
|
+
{ label: "src", path: "/tmp/workspace/src" },
|
|
115
|
+
{ label: "components", path: "/tmp/workspace/src/components" },
|
|
116
|
+
],
|
|
117
|
+
entries: [
|
|
118
|
+
{
|
|
119
|
+
name: "button.tsx",
|
|
120
|
+
path: "/tmp/workspace/src/components/button.tsx",
|
|
121
|
+
kind: "file",
|
|
122
|
+
hidden: false,
|
|
146
123
|
},
|
|
147
|
-
|
|
148
|
-
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (path === "/tmp/workspace/src") {
|
|
130
|
+
return {
|
|
131
|
+
isLoading: false,
|
|
132
|
+
error: null,
|
|
133
|
+
data: {
|
|
134
|
+
currentPath: "/tmp/workspace/src",
|
|
135
|
+
parentPath: "/tmp/workspace",
|
|
136
|
+
homePath: "/Users/demo",
|
|
137
|
+
breadcrumbs: [
|
|
138
|
+
{ label: "workspace", path: "/tmp/workspace" },
|
|
139
|
+
{ label: "src", path: "/tmp/workspace/src" },
|
|
140
|
+
],
|
|
141
|
+
entries: [
|
|
142
|
+
{
|
|
143
|
+
name: "components",
|
|
144
|
+
path: "/tmp/workspace/src/components",
|
|
145
|
+
kind: "directory",
|
|
146
|
+
hidden: false,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "index.ts",
|
|
150
|
+
path: "/tmp/workspace/src/index.ts",
|
|
151
|
+
kind: "file",
|
|
152
|
+
hidden: false,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
isLoading: false,
|
|
161
|
+
error: new Error("server path must point to a directory"),
|
|
162
|
+
data: null,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
beforeEach(() => {
|
|
@@ -153,14 +169,8 @@ beforeEach(() => {
|
|
|
153
169
|
serverPathBrowseMock.mockReset();
|
|
154
170
|
serverPathBrowseMock.mockReturnValue({
|
|
155
171
|
isLoading: false,
|
|
156
|
-
error:
|
|
157
|
-
data:
|
|
158
|
-
currentPath: "/tmp",
|
|
159
|
-
parentPath: null,
|
|
160
|
-
homePath: "/Users/demo",
|
|
161
|
-
breadcrumbs: [],
|
|
162
|
-
entries: [],
|
|
163
|
-
},
|
|
172
|
+
error: new Error("server path must point to a directory"),
|
|
173
|
+
data: null,
|
|
164
174
|
});
|
|
165
175
|
});
|
|
166
176
|
|
|
@@ -189,6 +199,23 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
|
|
|
189
199
|
).toBe("js");
|
|
190
200
|
});
|
|
191
201
|
|
|
202
|
+
it("keeps HTML files in the source preview when preview viewer is automatic", () => {
|
|
203
|
+
mockTextRead({
|
|
204
|
+
resolvedPath: "/tmp/example.html",
|
|
205
|
+
text: "<!doctype html><h1>Hello</h1>",
|
|
206
|
+
});
|
|
207
|
+
renderWorkspaceFilePreview({
|
|
208
|
+
file: {
|
|
209
|
+
label: "example.html",
|
|
210
|
+
path: "/tmp/example.html",
|
|
211
|
+
viewMode: "preview",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(screen.queryByTestId("workspace-html-preview")).toBeNull();
|
|
216
|
+
expect(screen.getByTestId("file-code-surface")).toBeTruthy();
|
|
217
|
+
});
|
|
218
|
+
|
|
192
219
|
it("renders HTML files through an unrestricted server content iframe when rendered preview is requested", () => {
|
|
193
220
|
mockTextRead({
|
|
194
221
|
resolvedPath: "/tmp/example.html",
|
|
@@ -212,6 +239,43 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
|
|
|
212
239
|
expect(screen.queryByTestId("file-code-surface")).toBeNull();
|
|
213
240
|
});
|
|
214
241
|
|
|
242
|
+
it("adds the refresh version to rendered HTML iframe URLs", () => {
|
|
243
|
+
mockTextRead({
|
|
244
|
+
resolvedPath: "/tmp/example.html",
|
|
245
|
+
text: "<!doctype html><h1>Hello</h1>",
|
|
246
|
+
});
|
|
247
|
+
renderWorkspaceFilePreview({
|
|
248
|
+
file: {
|
|
249
|
+
label: "example.html",
|
|
250
|
+
path: "/tmp/example.html",
|
|
251
|
+
previewViewer: "rendered",
|
|
252
|
+
viewMode: "preview",
|
|
253
|
+
},
|
|
254
|
+
refreshVersion: 3,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(screen.getByTestId("workspace-html-preview").getAttribute("src")).toContain(
|
|
258
|
+
"refresh=3",
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("keeps HTML files in the source preview when source is requested", () => {
|
|
263
|
+
mockTextRead({
|
|
264
|
+
resolvedPath: "/tmp/example.html",
|
|
265
|
+
text: "<!doctype html><h1>Hello</h1>",
|
|
266
|
+
});
|
|
267
|
+
renderWorkspaceFilePreview({
|
|
268
|
+
file: {
|
|
269
|
+
path: "/tmp/example.html",
|
|
270
|
+
previewViewer: "source",
|
|
271
|
+
viewMode: "preview",
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(screen.queryByTestId("workspace-html-preview")).toBeNull();
|
|
276
|
+
expect(screen.getByTestId("file-code-surface")).toBeTruthy();
|
|
277
|
+
});
|
|
278
|
+
|
|
215
279
|
it("falls back to the source preview when rendered is requested for a non-HTML file", () => {
|
|
216
280
|
mockTextRead({ resolvedPath: "/tmp/example.txt", text: "plain text" });
|
|
217
281
|
renderWorkspaceFilePreview({
|
|
@@ -257,6 +321,79 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
|
|
|
257
321
|
expect(screen.getByText("tmp")).toBeTruthy();
|
|
258
322
|
expect(screen.getByText("example.ts")).toBeTruthy();
|
|
259
323
|
});
|
|
324
|
+
|
|
325
|
+
it("renders directory entries and opens child directories or files", () => {
|
|
326
|
+
serverPathReadMock.mockReturnValue({
|
|
327
|
+
isLoading: false,
|
|
328
|
+
error: new Error("server path must point to a file"),
|
|
329
|
+
data: null,
|
|
330
|
+
});
|
|
331
|
+
serverPathBrowseMock.mockReturnValue({
|
|
332
|
+
isLoading: false,
|
|
333
|
+
error: null,
|
|
334
|
+
data: {
|
|
335
|
+
currentPath: "/tmp/workspace/src",
|
|
336
|
+
parentPath: "/tmp/workspace",
|
|
337
|
+
homePath: "/Users/demo",
|
|
338
|
+
breadcrumbs: [
|
|
339
|
+
{ label: "workspace", path: "/tmp/workspace" },
|
|
340
|
+
{ label: "src", path: "/tmp/workspace/src" },
|
|
341
|
+
],
|
|
342
|
+
entries: [
|
|
343
|
+
{
|
|
344
|
+
name: "components",
|
|
345
|
+
path: "/tmp/workspace/src/components",
|
|
346
|
+
kind: "directory",
|
|
347
|
+
hidden: false,
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "index.ts",
|
|
351
|
+
path: "/tmp/workspace/src/index.ts",
|
|
352
|
+
kind: "file",
|
|
353
|
+
hidden: false,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
const onFileOpen = vi.fn();
|
|
359
|
+
|
|
360
|
+
renderWorkspaceFilePreview({
|
|
361
|
+
file: {
|
|
362
|
+
path: "src",
|
|
363
|
+
label: "src",
|
|
364
|
+
viewMode: "preview",
|
|
365
|
+
},
|
|
366
|
+
onFileOpen,
|
|
367
|
+
sessionProjectRoot: "/tmp/workspace",
|
|
368
|
+
sessionWorkingDir: "/tmp/workspace",
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(serverPathBrowseMock).toHaveBeenCalledWith({
|
|
372
|
+
path: "src",
|
|
373
|
+
basePath: "/tmp/workspace",
|
|
374
|
+
includeFiles: true,
|
|
375
|
+
enabled: true,
|
|
376
|
+
});
|
|
377
|
+
expect(screen.getByTestId("workspace-directory-browser")).toBeTruthy();
|
|
378
|
+
|
|
379
|
+
fireEvent.click(
|
|
380
|
+
screen.getByRole("button", { name: "Open directory: components" }),
|
|
381
|
+
);
|
|
382
|
+
expect(onFileOpen).toHaveBeenCalledWith({
|
|
383
|
+
path: "/tmp/workspace/src/components",
|
|
384
|
+
label: "components",
|
|
385
|
+
viewMode: "preview",
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
fireEvent.click(
|
|
389
|
+
screen.getByRole("button", { name: "Open file: index.ts" }),
|
|
390
|
+
);
|
|
391
|
+
expect(onFileOpen).toHaveBeenCalledWith({
|
|
392
|
+
path: "/tmp/workspace/src/index.ts",
|
|
393
|
+
label: "index.ts",
|
|
394
|
+
viewMode: "preview",
|
|
395
|
+
});
|
|
396
|
+
});
|
|
260
397
|
});
|
|
261
398
|
|
|
262
399
|
describe("ChatSessionWorkspaceFilePreview breadcrumbs", () => {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ChatSessionWorkspacePanel } from "@/features/chat/features/workspace/components/chat-session-workspace-panel";
|
|
5
|
+
import type { ChatWorkspaceFileTab } from "@/features/chat/stores/chat-thread.store";
|
|
6
|
+
import type * as ReactQuery from "@tanstack/react-query";
|
|
7
|
+
|
|
8
|
+
const mocks = vi.hoisted(() => ({
|
|
9
|
+
syncVisibleWorkspaceSelection: vi.fn(),
|
|
10
|
+
selectChildSessionDetail: vi.fn(),
|
|
11
|
+
selectWorkspaceFile: vi.fn(),
|
|
12
|
+
closeWorkspaceFile: vi.fn(),
|
|
13
|
+
openSessionCronPanel: vi.fn(),
|
|
14
|
+
goBackWorkspacePanel: vi.fn(),
|
|
15
|
+
goForwardWorkspacePanel: vi.fn(),
|
|
16
|
+
closeWorkspacePanel: vi.fn(),
|
|
17
|
+
invalidateQueries: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@tanstack/react-query", async (importOriginal) => ({
|
|
21
|
+
...(await importOriginal<typeof ReactQuery>()),
|
|
22
|
+
useQueryClient: () => ({
|
|
23
|
+
invalidateQueries: mocks.invalidateQueries,
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
28
|
+
usePresenter: () => ({
|
|
29
|
+
chatThreadManager: mocks,
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock("@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view", () => ({
|
|
34
|
+
useNcpChildSessionTabsView: () => [],
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock("@/features/chat/features/workspace/components/chat-session-workspace-panel-content", () => ({
|
|
38
|
+
ChatSessionWorkspacePanelContent: ({
|
|
39
|
+
activeSelection,
|
|
40
|
+
filePreviewRefreshVersion,
|
|
41
|
+
}: {
|
|
42
|
+
activeSelection: { kind: string };
|
|
43
|
+
filePreviewRefreshVersion: number;
|
|
44
|
+
}) => (
|
|
45
|
+
<div
|
|
46
|
+
data-file-refresh-version={filePreviewRefreshVersion}
|
|
47
|
+
data-testid="workspace-panel-content"
|
|
48
|
+
>
|
|
49
|
+
{activeSelection.kind}
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
function createWorkspaceFileTab(): ChatWorkspaceFileTab {
|
|
55
|
+
return {
|
|
56
|
+
key: "file-tab",
|
|
57
|
+
parentSessionKey: "session-1",
|
|
58
|
+
path: "README.md",
|
|
59
|
+
viewMode: "preview",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderPanel(displayMode: "docked" | "overlay" = "docked") {
|
|
64
|
+
const fileTab = createWorkspaceFileTab();
|
|
65
|
+
|
|
66
|
+
return render(
|
|
67
|
+
<ChatSessionWorkspacePanel
|
|
68
|
+
sessionKey="session-1"
|
|
69
|
+
childSessionTabs={[]}
|
|
70
|
+
activeChildSessionKey={null}
|
|
71
|
+
activeSideChatDraft={null}
|
|
72
|
+
workspaceFileTabs={[fileTab]}
|
|
73
|
+
activeWorkspaceFileKey={fileTab.key}
|
|
74
|
+
workspaceNavigationHistory={[{ kind: "file", key: fileTab.key }]}
|
|
75
|
+
workspaceNavigationHistoryIndex={0}
|
|
76
|
+
activePanelKind="file"
|
|
77
|
+
sessionCronJobs={[]}
|
|
78
|
+
sessionProjectRoot={null}
|
|
79
|
+
sessionWorkingDir={null}
|
|
80
|
+
displayMode={displayMode}
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("ChatSessionWorkspacePanel", () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
vi.clearAllMocks();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("refreshes the active workspace file from the top action bar", async () => {
|
|
91
|
+
const user = userEvent.setup();
|
|
92
|
+
renderPanel();
|
|
93
|
+
|
|
94
|
+
await user.click(screen.getByRole("button", { name: "Refresh preview" }));
|
|
95
|
+
|
|
96
|
+
expect(mocks.invalidateQueries).toHaveBeenCalledWith({
|
|
97
|
+
queryKey: ["server-path-read", "README.md", null],
|
|
98
|
+
});
|
|
99
|
+
expect(mocks.invalidateQueries).toHaveBeenCalledWith({
|
|
100
|
+
queryKey: ["server-path-browse", "README.md", "", true],
|
|
101
|
+
});
|
|
102
|
+
expect(
|
|
103
|
+
screen
|
|
104
|
+
.getByTestId("workspace-panel-content")
|
|
105
|
+
.getAttribute("data-file-refresh-version"),
|
|
106
|
+
).toBe("1");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("maximizes and restores the docked workspace panel within its container", async () => {
|
|
110
|
+
const user = userEvent.setup();
|
|
111
|
+
renderPanel();
|
|
112
|
+
|
|
113
|
+
const panel = screen.getByTestId("chat-session-workspace-panel");
|
|
114
|
+
expect(panel.className).not.toContain("absolute");
|
|
115
|
+
|
|
116
|
+
await user.click(
|
|
117
|
+
screen.getByRole("button", { name: "Maximize workspace panel" }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(panel.className).toContain("absolute");
|
|
121
|
+
expect(panel.className).not.toContain("fixed");
|
|
122
|
+
expect(
|
|
123
|
+
screen.queryByTestId("resizable-right-panel-handle"),
|
|
124
|
+
).toBeNull();
|
|
125
|
+
|
|
126
|
+
await user.click(
|
|
127
|
+
screen.getByRole("button", { name: "Restore workspace panel" }),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(panel.className).not.toContain("absolute");
|
|
131
|
+
expect(screen.getByTestId("resizable-right-panel-handle")).toBeTruthy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("does not show a redundant maximize action in viewport overlay mode", () => {
|
|
135
|
+
renderPanel("overlay");
|
|
136
|
+
|
|
137
|
+
expect(
|
|
138
|
+
screen.queryByRole("button", { name: "Maximize workspace panel" }),
|
|
139
|
+
).toBeNull();
|
|
140
|
+
expect(screen.getByTestId("chat-session-workspace-panel").className).toContain(
|
|
141
|
+
"fixed",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
});
|
package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
|
|
3
|
+
import { ChevronRight, FileCode2, Folder } from "lucide-react";
|
|
4
|
+
import type { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
|
|
5
|
+
import type { ServerPathEntryView } from "@/shared/lib/api";
|
|
6
|
+
import { t } from "@/shared/lib/i18n";
|
|
7
|
+
import { cn } from "@/shared/lib/utils";
|
|
8
|
+
|
|
9
|
+
type ChatSessionWorkspaceDirectoryBrowserProps = {
|
|
10
|
+
browseQuery: ReturnType<typeof useServerPathBrowse>;
|
|
11
|
+
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
12
|
+
renderStatus: (params: {
|
|
13
|
+
text: string;
|
|
14
|
+
tone?: "muted" | "error";
|
|
15
|
+
}) => ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function WorkspaceDirectoryEntryIcon({
|
|
19
|
+
kind,
|
|
20
|
+
}: {
|
|
21
|
+
kind: ServerPathEntryView["kind"];
|
|
22
|
+
}) {
|
|
23
|
+
return kind === "directory" ? (
|
|
24
|
+
<Folder className="h-4 w-4 shrink-0 text-emerald-600" />
|
|
25
|
+
) : (
|
|
26
|
+
<FileCode2 className="h-4 w-4 shrink-0 text-sky-600" />
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildDirectoryEntryLabel(entry: ServerPathEntryView): string {
|
|
31
|
+
const actionLabel =
|
|
32
|
+
entry.kind === "directory"
|
|
33
|
+
? t("chatWorkspaceOpenDirectory")
|
|
34
|
+
: t("chatWorkspaceOpenFile");
|
|
35
|
+
return `${actionLabel}: ${entry.name}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ChatSessionWorkspaceDirectoryBrowser({
|
|
39
|
+
browseQuery,
|
|
40
|
+
onFileOpen,
|
|
41
|
+
renderStatus,
|
|
42
|
+
}: ChatSessionWorkspaceDirectoryBrowserProps) {
|
|
43
|
+
const entries = browseQuery.data?.entries ?? [];
|
|
44
|
+
const errorMessage = browseQuery.error
|
|
45
|
+
? browseQuery.error instanceof Error
|
|
46
|
+
? browseQuery.error.message
|
|
47
|
+
: String(browseQuery.error)
|
|
48
|
+
: null;
|
|
49
|
+
const openEntry = (entry: ServerPathEntryView) => {
|
|
50
|
+
onFileOpen({
|
|
51
|
+
path: entry.path,
|
|
52
|
+
label: entry.name,
|
|
53
|
+
viewMode: "preview",
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (browseQuery.isLoading && !browseQuery.data) {
|
|
58
|
+
return renderStatus({ text: t("chatWorkspaceLoadingDirectory") });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (errorMessage && !browseQuery.data) {
|
|
62
|
+
return renderStatus({
|
|
63
|
+
tone: "error",
|
|
64
|
+
text: `${t("chatWorkspaceDirectoryLoadFailed")}: ${errorMessage}`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (entries.length === 0) {
|
|
69
|
+
return renderStatus({ text: t("chatWorkspaceDirectoryEmpty") });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
data-testid="workspace-directory-browser"
|
|
75
|
+
className="h-full overflow-auto bg-white px-2 py-2 custom-scrollbar"
|
|
76
|
+
>
|
|
77
|
+
<div className="space-y-0.5">
|
|
78
|
+
{entries.map((entry) => (
|
|
79
|
+
<button
|
|
80
|
+
key={entry.path}
|
|
81
|
+
type="button"
|
|
82
|
+
aria-label={buildDirectoryEntryLabel(entry)}
|
|
83
|
+
className={cn(
|
|
84
|
+
"flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
|
|
85
|
+
entry.hidden ? "opacity-65" : null,
|
|
86
|
+
)}
|
|
87
|
+
onClick={() => openEntry(entry)}
|
|
88
|
+
>
|
|
89
|
+
<WorkspaceDirectoryEntryIcon kind={entry.kind} />
|
|
90
|
+
<span className="min-w-0 flex-1 truncate">{entry.name}</span>
|
|
91
|
+
{entry.kind === "directory" ? (
|
|
92
|
+
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-gray-300" />
|
|
93
|
+
) : null}
|
|
94
|
+
</button>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|