@nextclaw/ui 0.15.1 → 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 +31 -0
- package/dist/assets/{api-b1R9VdeH.js → api-B7Qigvtm.js} +2 -2
- package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
- package/dist/assets/{book-open-B77lQ2Tg.js → book-open-Ba2f7YiG.js} +1 -1
- package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
- package/dist/assets/{chat-page-CZ7WxPir.js → chat-page-C_5dcuE3.js} +1 -1
- package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
- package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-D2EG3nqE.js} +1 -1
- package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-BGR3HUi1.js} +1 -1
- package/dist/assets/{dist-jdGmbb6w.js → dist-7JTjycxE.js} +1 -1
- package/dist/assets/{dist-YtoqU-AT.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-bNpL2LKy.js → es2015-j1SRCIX5.js} +1 -1
- package/dist/assets/{external-link-BM6fk8q-.js → external-link-DxuPaDoK.js} +1 -1
- package/dist/assets/form-actions-BztL7P_T.js +1 -0
- package/dist/assets/{i18n-provider-CntWolRA.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-CLmUstLN.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-Cm_a4GHf.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
- package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
- package/dist/assets/{react-BN8uzPnp.js → react-UirzUuST.js} +1 -1
- package/dist/assets/{refresh-cw-BpVcFPmJ.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-BpFW5fpk.js → save-Cb2g0BOi.js} +1 -1
- package/dist/assets/{search-D0uO7enf.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-Bh-CrA4p.js → select-CdSEOwVM.js} +1 -1
- package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-CTAtGexr.js} +1 -1
- package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-2DWauOjY.js} +1 -1
- package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-DPvakJ0a.js} +2 -2
- package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-P5oM14E1.js} +1 -1
- package/dist/assets/{switch-CFLJFNSd.js → switch-BU_UEULb.js} +1 -1
- package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-D-WsKoT1.js} +1 -1
- package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-CjkOaHW7.js} +1 -1
- package/dist/assets/{use-config-CVPgsBmU.js → use-config--hx5oxQn.js} +1 -1
- package/dist/assets/{use-confirm-dialog-CSeBk9SF.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/channels/components/config/channel-form.tsx +14 -11
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
- 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/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/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/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/utils/chat-sidebar-session-groups.utils.ts +25 -3
- 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 +159 -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 +66 -7
- 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/managers/__tests__/chat-session-list.manager.test.ts +33 -2
- package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
- 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/config/runtime-config-editor.tsx +5 -4
- 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/utils/server-path.utils.ts +1 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -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-DcsIQc6A.js +0 -8
- package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
- package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
- package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
- package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
- package/dist/assets/index-BIcEw90u.js +0 -104
- package/dist/assets/index-BiNSTKTU.css +0 -1
- package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
- package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
- package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
- package/dist/assets/plus-vDqW5Kxp.js +0 -1
- package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
- package/dist/assets/remote-CLQ0IBM8.js +0 -1
- package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
- package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
- package/dist/assets/search-config-page-D0klFUXd.js +0 -1
- package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
- package/dist/assets/security-config-C5gnlkuj.js +0 -1
- package/dist/assets/x-BF2KQ--V.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
|
|
|
@@ -229,6 +239,26 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
|
|
|
229
239
|
expect(screen.queryByTestId("file-code-surface")).toBeNull();
|
|
230
240
|
});
|
|
231
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
|
+
|
|
232
262
|
it("keeps HTML files in the source preview when source is requested", () => {
|
|
233
263
|
mockTextRead({
|
|
234
264
|
resolvedPath: "/tmp/example.html",
|
|
@@ -291,6 +321,79 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
|
|
|
291
321
|
expect(screen.getByText("tmp")).toBeTruthy();
|
|
292
322
|
expect(screen.getByText("example.ts")).toBeTruthy();
|
|
293
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
|
+
});
|
|
294
397
|
});
|
|
295
398
|
|
|
296
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
|
+
}
|
package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment, useState } from "react";
|
|
2
2
|
import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
|
|
3
|
-
import { ChevronRight, FileCode2, FolderTree } from "lucide-react";
|
|
3
|
+
import { ChevronRight, FileCode2, Folder, FolderTree } from "lucide-react";
|
|
4
4
|
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
|
|
5
5
|
import { WorkspaceBreadcrumbBrowser } from "./chat-session-workspace-file-breadcrumb-browser";
|
|
6
6
|
import type {
|
|
@@ -59,6 +59,8 @@ function WorkspaceBreadcrumbSegmentButton({
|
|
|
59
59
|
>
|
|
60
60
|
{segment.kind === "workspace" ? (
|
|
61
61
|
<FolderTree className="h-3 w-3 shrink-0" />
|
|
62
|
+
) : segment.kind === "directory" && segment.isCurrent ? (
|
|
63
|
+
<Folder className="h-3 w-3 shrink-0" />
|
|
62
64
|
) : segment.isCurrent ? (
|
|
63
65
|
<FileCode2 className="h-3 w-3 shrink-0" />
|
|
64
66
|
) : null}
|