@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
|
@@ -5,6 +5,7 @@ import { BookOpen, ChevronDown } from 'lucide-react';
|
|
|
5
5
|
import { toast } from 'sonner';
|
|
6
6
|
import { LogoBadge } from '@/shared/components/common/logo-badge';
|
|
7
7
|
import { Button } from '@/shared/components/ui/button';
|
|
8
|
+
import { FormActions } from '@/shared/components/ui/actions/form-actions';
|
|
8
9
|
import { StatusDot } from '@/shared/components/status/status-dot';
|
|
9
10
|
import { nextclawClient } from '@/shared/lib/api';
|
|
10
11
|
import { useConfig, useConfigMeta, useConfigSchema, useExecuteConfigAction, useUpdateChannel } from '@/shared/hooks/use-config';
|
|
@@ -363,17 +364,19 @@ function ChannelFormEditor({
|
|
|
363
364
|
/>
|
|
364
365
|
</ConfigSplitPaneBody>
|
|
365
366
|
|
|
366
|
-
<ConfigSplitPaneFooter
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
{
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
367
|
+
<ConfigSplitPaneFooter>
|
|
368
|
+
<FormActions align="between">
|
|
369
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
370
|
+
{actions.filter((action) => action.trigger === 'manual').map((action) => (
|
|
371
|
+
<Button key={action.id} type="button" size="sm" onClick={() => void handleManualAction(action)} disabled={disabled} variant="secondary">
|
|
372
|
+
{runningActionId === action.id ? t('connecting') : action.title}
|
|
373
|
+
</Button>
|
|
374
|
+
))}
|
|
375
|
+
</div>
|
|
376
|
+
<Button type="submit" size="sm" disabled={disabled}>
|
|
377
|
+
{updateChannel.isPending ? t('saving') : t('save')}
|
|
378
|
+
</Button>
|
|
379
|
+
</FormActions>
|
|
377
380
|
</ConfigSplitPaneFooter>
|
|
378
381
|
</form>
|
|
379
382
|
</ConfigSplitDetailPane>
|
|
@@ -7,12 +7,17 @@ import { ChatConversationHeaderSection } from "@/features/chat/components/conver
|
|
|
7
7
|
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
8
8
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
9
9
|
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
10
|
-
import
|
|
10
|
+
import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
|
|
11
|
+
import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
|
|
12
|
+
import type { NcpSessionSummaryView, SessionEntryView } from "@/shared/lib/api";
|
|
11
13
|
|
|
12
14
|
const mocks = vi.hoisted(() => ({
|
|
13
15
|
deleteSession: vi.fn(),
|
|
14
16
|
openChildSessionPanel: vi.fn(),
|
|
15
17
|
openSessionCronPanel: vi.fn(),
|
|
18
|
+
selectSession: vi.fn(),
|
|
19
|
+
sessionItems: [] as NcpSessionListItemView[],
|
|
20
|
+
isSessionListLoading: false,
|
|
16
21
|
}));
|
|
17
22
|
|
|
18
23
|
vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
@@ -22,9 +27,35 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
22
27
|
openChildSessionPanel: mocks.openChildSessionPanel,
|
|
23
28
|
openSessionCronPanel: mocks.openSessionCronPanel,
|
|
24
29
|
},
|
|
30
|
+
chatSessionListManager: {
|
|
31
|
+
selectSession: mocks.selectSession,
|
|
32
|
+
},
|
|
25
33
|
}),
|
|
26
34
|
}));
|
|
27
35
|
|
|
36
|
+
vi.mock("@/features/chat/features/ncp/hooks/use-ncp-session-list-view", () => ({
|
|
37
|
+
useNcpSessionListView: (params?: { query?: string | null }) => {
|
|
38
|
+
const query = params?.query?.trim().toLowerCase() ?? "";
|
|
39
|
+
const items = query
|
|
40
|
+
? mocks.sessionItems.filter(({ session }) =>
|
|
41
|
+
[
|
|
42
|
+
session.key,
|
|
43
|
+
session.label ?? "",
|
|
44
|
+
session.projectRoot ?? "",
|
|
45
|
+
session.projectName ?? "",
|
|
46
|
+
]
|
|
47
|
+
.join(" ")
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.includes(query),
|
|
50
|
+
)
|
|
51
|
+
: mocks.sessionItems;
|
|
52
|
+
return {
|
|
53
|
+
isLoading: mocks.isSessionListLoading,
|
|
54
|
+
items,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
|
|
28
59
|
vi.mock("@/features/cron", () => ({
|
|
29
60
|
useCronJobs: () => ({ data: { jobs: [] } }),
|
|
30
61
|
}));
|
|
@@ -54,6 +85,24 @@ function createSummary(
|
|
|
54
85
|
};
|
|
55
86
|
}
|
|
56
87
|
|
|
88
|
+
function createSessionListItem(
|
|
89
|
+
overrides: Partial<SessionEntryView> & Pick<SessionEntryView, "key">,
|
|
90
|
+
): NcpSessionListItemView {
|
|
91
|
+
return {
|
|
92
|
+
session: {
|
|
93
|
+
createdAt: "2026-06-18T00:00:00.000Z",
|
|
94
|
+
updatedAt: "2026-06-18T00:00:00.000Z",
|
|
95
|
+
lastMessageAt: "2026-06-18T00:00:00.000Z",
|
|
96
|
+
readAt: "2026-06-18T00:00:00.000Z",
|
|
97
|
+
sessionType: "native",
|
|
98
|
+
sessionTypeMutable: false,
|
|
99
|
+
messageCount: 1,
|
|
100
|
+
...overrides,
|
|
101
|
+
},
|
|
102
|
+
runStatus: overrides.status === "running" ? "running" : undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
57
106
|
function renderHeaderSection() {
|
|
58
107
|
const queryClient = new QueryClient({
|
|
59
108
|
defaultOptions: {
|
|
@@ -73,6 +122,9 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
73
122
|
mocks.deleteSession.mockReset();
|
|
74
123
|
mocks.openChildSessionPanel.mockReset();
|
|
75
124
|
mocks.openSessionCronPanel.mockReset();
|
|
125
|
+
mocks.selectSession.mockReset();
|
|
126
|
+
mocks.isSessionListLoading = false;
|
|
127
|
+
viewportLayoutManager.resetForTests();
|
|
76
128
|
useChatSessionListStore.persist.setOptions({
|
|
77
129
|
storage: new MemoryPersistStorage(),
|
|
78
130
|
});
|
|
@@ -129,6 +181,18 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
129
181
|
} as never,
|
|
130
182
|
},
|
|
131
183
|
});
|
|
184
|
+
mocks.sessionItems = [
|
|
185
|
+
createSessionListItem({
|
|
186
|
+
key: "parent-session-1",
|
|
187
|
+
label: "Parent Task",
|
|
188
|
+
}),
|
|
189
|
+
createSessionListItem({
|
|
190
|
+
key: "session:ncp-2",
|
|
191
|
+
label: "Background Task",
|
|
192
|
+
lastMessageAt: "2026-06-18T01:00:00.000Z",
|
|
193
|
+
readAt: "2026-06-18T01:00:00.000Z",
|
|
194
|
+
}),
|
|
195
|
+
];
|
|
132
196
|
});
|
|
133
197
|
|
|
134
198
|
it("shows the child-session entry from session query state before a tool card opens it", async () => {
|
|
@@ -142,4 +206,111 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
142
206
|
activeChildSessionKey: "child-session-1",
|
|
143
207
|
});
|
|
144
208
|
});
|
|
209
|
+
|
|
210
|
+
it("uses the collapsed desktop title as a session switcher", async () => {
|
|
211
|
+
const user = userEvent.setup();
|
|
212
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
213
|
+
|
|
214
|
+
renderHeaderSection();
|
|
215
|
+
|
|
216
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
217
|
+
await user.click(screen.getByRole("button", { name: /Background Task/ }));
|
|
218
|
+
|
|
219
|
+
expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("keeps the desktop title plain while the sidebar is expanded", () => {
|
|
223
|
+
renderHeaderSection();
|
|
224
|
+
|
|
225
|
+
expect(screen.queryByRole("button", { name: /Switch session/ })).toBeNull();
|
|
226
|
+
expect(screen.getByText("Parent Task")).toBeTruthy();
|
|
227
|
+
expect(screen.queryByText("Native")).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("keeps the runtime badge for a non-native session", () => {
|
|
231
|
+
useChatQueryStore.setState({
|
|
232
|
+
snapshot: {
|
|
233
|
+
...useChatQueryStore.getState().snapshot,
|
|
234
|
+
sessionsQuery: {
|
|
235
|
+
data: {
|
|
236
|
+
sessions: [
|
|
237
|
+
createSummary({
|
|
238
|
+
sessionId: "parent-session-1",
|
|
239
|
+
metadata: {
|
|
240
|
+
label: "Parent Task",
|
|
241
|
+
session_type: "codex",
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
],
|
|
245
|
+
total: 1,
|
|
246
|
+
},
|
|
247
|
+
} as never,
|
|
248
|
+
sessionTypesQuery: {
|
|
249
|
+
data: {
|
|
250
|
+
defaultType: "native",
|
|
251
|
+
options: [{ value: "codex", label: "Codex", ready: true }],
|
|
252
|
+
},
|
|
253
|
+
} as never,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
mocks.sessionItems = [
|
|
257
|
+
createSessionListItem({
|
|
258
|
+
key: "parent-session-1",
|
|
259
|
+
label: "Parent Task",
|
|
260
|
+
sessionType: "codex",
|
|
261
|
+
}),
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
renderHeaderSection();
|
|
265
|
+
|
|
266
|
+
expect(screen.getByText("Codex")).toBeTruthy();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("keeps session switching available from a collapsed new-session header", async () => {
|
|
270
|
+
const user = userEvent.setup();
|
|
271
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
272
|
+
useChatSessionListStore.setState({
|
|
273
|
+
snapshot: {
|
|
274
|
+
...useChatSessionListStore.getState().snapshot,
|
|
275
|
+
selectedSessionKey: null,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
useChatThreadStore.setState({
|
|
279
|
+
snapshot: {
|
|
280
|
+
...useChatThreadStore.getState().snapshot,
|
|
281
|
+
sessionKey: null,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
renderHeaderSection();
|
|
286
|
+
|
|
287
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
288
|
+
await user.click(screen.getByRole("button", { name: /Background Task/ }));
|
|
289
|
+
|
|
290
|
+
expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("filters the collapsed title switcher with a local search query", async () => {
|
|
294
|
+
const user = userEvent.setup();
|
|
295
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
296
|
+
|
|
297
|
+
renderHeaderSection();
|
|
298
|
+
|
|
299
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
300
|
+
const searchInput = screen.getByRole("textbox", {
|
|
301
|
+
name: "Search session key / label",
|
|
302
|
+
});
|
|
303
|
+
await user.type(searchInput, "Background");
|
|
304
|
+
await user.click(screen.getByRole("button", { name: /Background Task/ }));
|
|
305
|
+
|
|
306
|
+
expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
|
|
307
|
+
|
|
308
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
309
|
+
await user.type(
|
|
310
|
+
screen.getByRole("textbox", { name: "Search session key / label" }),
|
|
311
|
+
"missing",
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(screen.getByText("No matching sessions")).toBeTruthy();
|
|
315
|
+
});
|
|
145
316
|
});
|
|
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
import { ChatConversationHeader } from "@/features/chat/components/conversation/chat-conversation-header";
|
|
3
3
|
import { ChatSessionHeaderActions } from "@/features/chat/features/session/components/session-header/chat-session-header-actions";
|
|
4
4
|
import { ChatSessionProjectBadge } from "@/features/chat/features/session/components/session-header/chat-session-project-badge";
|
|
5
|
+
import { ChatSessionTitleSwitcher } from "@/features/chat/features/session/components/session-header/chat-session-title-switcher";
|
|
5
6
|
import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
|
|
6
7
|
import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
|
|
7
8
|
import { useChatConversationWorkspaceState } from "@/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state";
|
|
@@ -52,6 +53,8 @@ export function ChatConversationHeaderSection({
|
|
|
52
53
|
sessionTypeOptions.find((option) => option.value === sessionType) ?? null;
|
|
53
54
|
const sessionTypeLabel =
|
|
54
55
|
sessionTypeOption?.label ?? resolveSessionTypeLabel(sessionType);
|
|
56
|
+
const shouldShowSessionTypeBadge =
|
|
57
|
+
sessionType !== DEFAULT_SESSION_TYPE && Boolean(sessionTypeLabel);
|
|
55
58
|
const sessionProjectRoot = selectedSession?.projectRoot ?? null;
|
|
56
59
|
const sessionProjectName =
|
|
57
60
|
selectedSession?.projectName ?? getSessionProjectName(sessionProjectRoot);
|
|
@@ -89,6 +92,13 @@ export function ChatConversationHeaderSection({
|
|
|
89
92
|
<ChatConversationHeader
|
|
90
93
|
layoutMode={layoutMode}
|
|
91
94
|
title={sessionHeaderTitle}
|
|
95
|
+
titleContent={
|
|
96
|
+
<ChatSessionTitleSwitcher
|
|
97
|
+
layoutMode={layoutMode}
|
|
98
|
+
selectedSessionKey={sessionKey}
|
|
99
|
+
title={sessionHeaderTitle}
|
|
100
|
+
/>
|
|
101
|
+
}
|
|
92
102
|
shouldShow={shouldShowSessionHeader}
|
|
93
103
|
onBackToList={onBackToList}
|
|
94
104
|
leading={
|
|
@@ -102,7 +112,7 @@ export function ChatConversationHeaderSection({
|
|
|
102
112
|
) : null
|
|
103
113
|
}
|
|
104
114
|
sessionTypeBadge={
|
|
105
|
-
|
|
115
|
+
shouldShowSessionTypeBadge ? (
|
|
106
116
|
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
107
117
|
{sessionTypeOption?.icon?.src ? (
|
|
108
118
|
<span className="inline-flex h-[1.125rem] w-[1.125rem] items-center justify-center">
|
|
@@ -41,6 +41,7 @@ export function ChatConversationHeader({
|
|
|
41
41
|
sessionTypeBadge,
|
|
42
42
|
shouldShow,
|
|
43
43
|
title,
|
|
44
|
+
titleContent,
|
|
44
45
|
}: {
|
|
45
46
|
layoutMode: "desktop" | "mobile";
|
|
46
47
|
actions?: ReactNode;
|
|
@@ -50,6 +51,7 @@ export function ChatConversationHeader({
|
|
|
50
51
|
sessionTypeBadge?: ReactNode;
|
|
51
52
|
shouldShow: boolean;
|
|
52
53
|
title: string;
|
|
54
|
+
titleContent?: ReactNode;
|
|
53
55
|
}) {
|
|
54
56
|
const isMobileLayout = layoutMode === "mobile";
|
|
55
57
|
|
|
@@ -78,9 +80,11 @@ export function ChatConversationHeader({
|
|
|
78
80
|
/>
|
|
79
81
|
) : null}
|
|
80
82
|
{leading}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
{titleContent ?? (
|
|
84
|
+
<span className="text-sm font-medium text-foreground truncate">
|
|
85
|
+
{title}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
84
88
|
{sessionTypeBadge}
|
|
85
89
|
{projectBadge}
|
|
86
90
|
</div>
|
|
@@ -34,7 +34,7 @@ export function ChatConversationPanel({
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
|
-
<section className="flex-1 min-h-0 flex overflow-hidden bg-background">
|
|
37
|
+
<section className="relative flex-1 min-h-0 flex overflow-hidden bg-background">
|
|
38
38
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
39
39
|
<ChatConversationParentBanner />
|
|
40
40
|
<ChatConversationHeaderSection
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
ChatSidebarDesktopToolbar,
|
|
5
|
+
ChatSidebarMobileToolbar,
|
|
6
|
+
} from "@/features/chat/components/layout/chat-sidebar-toolbar";
|
|
7
|
+
import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
8
|
+
|
|
9
|
+
const sessionTypeOptions: ChatSessionTypeOption[] = [
|
|
10
|
+
{ value: "native", label: "Native", icon: null, ready: true },
|
|
11
|
+
{ value: "codex", label: "Codex", icon: null, ready: true },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const toolbarProps = {
|
|
15
|
+
query: "",
|
|
16
|
+
defaultSessionType: "native",
|
|
17
|
+
sessionTypeOptions,
|
|
18
|
+
nonDefaultSessionTypeOptions: [sessionTypeOptions[1]],
|
|
19
|
+
selectedNewSessionType: "native",
|
|
20
|
+
selectedNewSessionTypeOption: sessionTypeOptions[0],
|
|
21
|
+
isCreateMenuOpen: false,
|
|
22
|
+
onCreateMenuOpenChange: vi.fn(),
|
|
23
|
+
onCreateSession: vi.fn(),
|
|
24
|
+
onSelectNewSessionType: vi.fn(),
|
|
25
|
+
onQueryChange: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function searchIconClassName() {
|
|
29
|
+
return (
|
|
30
|
+
screen
|
|
31
|
+
.getByPlaceholderText("Search conversations...")
|
|
32
|
+
.previousElementSibling?.getAttribute("class") ?? ""
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("ChatSidebarToolbar", () => {
|
|
37
|
+
it("keeps desktop search icon transparent to pointer input", () => {
|
|
38
|
+
render(<ChatSidebarDesktopToolbar {...toolbarProps} />);
|
|
39
|
+
|
|
40
|
+
expect(searchIconClassName()).toContain("pointer-events-none");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps mobile search icon transparent to pointer input", () => {
|
|
44
|
+
render(<ChatSidebarMobileToolbar {...toolbarProps} />);
|
|
45
|
+
|
|
46
|
+
expect(searchIconClassName()).toContain("pointer-events-none");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { MemoryRouter } from "react-router-dom";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ChatSidebarUtilityMenu } from "@/features/chat/components/layout/chat-sidebar-utility-menu";
|
|
5
|
+
|
|
6
|
+
function renderUtilityMenu() {
|
|
7
|
+
return render(
|
|
8
|
+
<MemoryRouter>
|
|
9
|
+
<ChatSidebarUtilityMenu
|
|
10
|
+
isOpen
|
|
11
|
+
onOpenChange={vi.fn()}
|
|
12
|
+
currentTheme="warm"
|
|
13
|
+
currentThemeLabel="Warm"
|
|
14
|
+
themeOptions={[{ value: "warm", label: "Warm" }]}
|
|
15
|
+
onSelectTheme={vi.fn()}
|
|
16
|
+
currentLanguage="en"
|
|
17
|
+
currentLanguageLabel="English"
|
|
18
|
+
languageOptions={[{ value: "en", label: "English" }]}
|
|
19
|
+
onSelectLanguage={vi.fn()}
|
|
20
|
+
onOpenDocs={vi.fn()}
|
|
21
|
+
onOpenApps={vi.fn()}
|
|
22
|
+
/>
|
|
23
|
+
</MemoryRouter>,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function appendComposerFocusTarget(): HTMLElement {
|
|
28
|
+
const composerShell = document.createElement("div");
|
|
29
|
+
composerShell.className = "nextclaw-chat-input-bar-shell";
|
|
30
|
+
const composer = document.createElement("div");
|
|
31
|
+
composer.setAttribute("contenteditable", "true");
|
|
32
|
+
composer.setAttribute("role", "textbox");
|
|
33
|
+
composer.tabIndex = 0;
|
|
34
|
+
composerShell.appendChild(composer);
|
|
35
|
+
document.body.appendChild(composerShell);
|
|
36
|
+
return composer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("ChatSidebarUtilityMenu", () => {
|
|
40
|
+
it("stays open when the streaming composer restores focus", () => {
|
|
41
|
+
const composer = appendComposerFocusTarget();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
renderUtilityMenu();
|
|
45
|
+
|
|
46
|
+
expect(screen.getByRole("button", { name: "Help Docs" })).toBeTruthy();
|
|
47
|
+
|
|
48
|
+
fireEvent.focusIn(composer);
|
|
49
|
+
|
|
50
|
+
expect(screen.getByRole("button", { name: "Help Docs" })).toBeTruthy();
|
|
51
|
+
} finally {
|
|
52
|
+
composer.closest(".nextclaw-chat-input-bar-shell")?.remove();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -134,7 +134,6 @@ export function ChatSidebarSessionArea({
|
|
|
134
134
|
isCollapsed,
|
|
135
135
|
isLoading,
|
|
136
136
|
isProjectFirstView,
|
|
137
|
-
onCreateSession,
|
|
138
137
|
onSelectMode,
|
|
139
138
|
projectGroups,
|
|
140
139
|
renderSessionItem,
|
|
@@ -145,7 +144,6 @@ export function ChatSidebarSessionArea({
|
|
|
145
144
|
isCollapsed: boolean;
|
|
146
145
|
isLoading: boolean;
|
|
147
146
|
isProjectFirstView: boolean;
|
|
148
|
-
onCreateSession: (sessionType: string, projectRoot?: string | null) => void;
|
|
149
147
|
onSelectMode: (mode: "time-first" | "project-first") => void;
|
|
150
148
|
projectGroups: ReturnType<typeof groupSessionsByProject>;
|
|
151
149
|
renderSessionItem: (item: NcpSessionListItemView) => JSX.Element;
|
|
@@ -183,7 +181,6 @@ export function ChatSidebarSessionArea({
|
|
|
183
181
|
defaultSessionType={defaultSessionType}
|
|
184
182
|
sessionTypeOptions={sessionTypeOptions}
|
|
185
183
|
renderSessionItem={renderSessionItem}
|
|
186
|
-
onCreateSession={onCreateSession}
|
|
187
184
|
/>
|
|
188
185
|
</div>
|
|
189
186
|
</>
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
SIDEBAR_RAIL_CONTROL_CLASS,
|
|
17
17
|
SIDEBAR_RAIL_ICON_CLASS,
|
|
18
18
|
SIDEBAR_RAIL_ITEM_GAP_CLASS,
|
|
19
|
-
SIDEBAR_RAIL_PRIMARY_SURFACE_CLASS,
|
|
20
19
|
SIDEBAR_RAIL_SURFACE_CLASS,
|
|
21
20
|
} from "@/app/components/layout/sidebar-rail.styles";
|
|
22
21
|
|
|
@@ -150,7 +149,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
|
|
|
150
149
|
label={t("chatSidebarNewTask")}
|
|
151
150
|
className={cn(
|
|
152
151
|
SIDEBAR_RAIL_CONTROL_CLASS,
|
|
153
|
-
|
|
152
|
+
SIDEBAR_RAIL_SURFACE_CLASS,
|
|
154
153
|
)}
|
|
155
154
|
onClick={() => {
|
|
156
155
|
onCreateMenuOpenChange(false);
|
|
@@ -259,7 +258,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
|
|
|
259
258
|
|
|
260
259
|
<div className="px-4 pb-3">
|
|
261
260
|
<div className="relative">
|
|
262
|
-
<Search className="absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
261
|
+
<Search className="pointer-events-none absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
263
262
|
<Input
|
|
264
263
|
value={query}
|
|
265
264
|
onChange={(event) => onQueryChange(event.target.value)}
|
|
@@ -294,7 +293,7 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
|
|
|
294
293
|
<div className="px-4 pb-2 pt-1">
|
|
295
294
|
<div className="flex items-center gap-2">
|
|
296
295
|
<div className="relative min-w-0 flex-1">
|
|
297
|
-
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
|
|
296
|
+
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
|
|
298
297
|
<Input
|
|
299
298
|
value={query}
|
|
300
299
|
onChange={(event) => onQueryChange(event.target.value)}
|
|
@@ -34,10 +34,7 @@ import {
|
|
|
34
34
|
type LucideIcon,
|
|
35
35
|
} from "lucide-react";
|
|
36
36
|
|
|
37
|
-
type ChatSidebarUtilityOption<Value extends string> = {
|
|
38
|
-
value: Value;
|
|
39
|
-
label: string;
|
|
40
|
-
};
|
|
37
|
+
type ChatSidebarUtilityOption<Value extends string> = { value: Value; label: string };
|
|
41
38
|
|
|
42
39
|
type ChatSidebarUtilityMenuProps = {
|
|
43
40
|
isOpen: boolean;
|
|
@@ -55,6 +52,12 @@ type ChatSidebarUtilityMenuProps = {
|
|
|
55
52
|
collapsed?: boolean;
|
|
56
53
|
};
|
|
57
54
|
|
|
55
|
+
function isChatComposerFocusTarget(target: EventTarget | null): boolean {
|
|
56
|
+
return target instanceof HTMLElement && Boolean(
|
|
57
|
+
target.closest('.nextclaw-chat-input-bar-shell [role="textbox"][contenteditable="true"]'),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
58
61
|
export function ChatSidebarUtilityMenu({
|
|
59
62
|
isOpen,
|
|
60
63
|
onOpenChange,
|
|
@@ -70,16 +73,6 @@ export function ChatSidebarUtilityMenu({
|
|
|
70
73
|
onOpenApps,
|
|
71
74
|
collapsed = false,
|
|
72
75
|
}: ChatSidebarUtilityMenuProps) {
|
|
73
|
-
const handleOpenDocs = () => {
|
|
74
|
-
onOpenDocs();
|
|
75
|
-
onOpenChange(false);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const handleOpenApps = () => {
|
|
79
|
-
onOpenApps();
|
|
80
|
-
onOpenChange(false);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
76
|
return (
|
|
84
77
|
<Popover open={isOpen} onOpenChange={onOpenChange}>
|
|
85
78
|
{collapsed ? (
|
|
@@ -129,6 +122,11 @@ export function ChatSidebarUtilityMenu({
|
|
|
129
122
|
side={collapsed ? "right" : "top"}
|
|
130
123
|
align={collapsed ? "end" : "start"}
|
|
131
124
|
className="w-64 p-2"
|
|
125
|
+
onFocusOutside={(event) => {
|
|
126
|
+
if (isChatComposerFocusTarget(event.detail.originalEvent.target)) {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
}
|
|
129
|
+
}}
|
|
132
130
|
>
|
|
133
131
|
<div className="space-y-1">
|
|
134
132
|
<NavLink
|
|
@@ -139,22 +137,23 @@ export function ChatSidebarUtilityMenu({
|
|
|
139
137
|
<Settings className="h-4 w-4 text-muted-foreground/70" />
|
|
140
138
|
<span className="flex-1 text-left">{t("settings")}</span>
|
|
141
139
|
</NavLink>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
140
|
+
{[
|
|
141
|
+
{ key: "docs", icon: BookOpen, label: t("docBrowserHelp"), action: onOpenDocs },
|
|
142
|
+
{ key: "apps", icon: Boxes, label: t("appsTitle"), action: onOpenApps },
|
|
143
|
+
].map(({ key, icon: Icon, label, action }) => (
|
|
144
|
+
<button
|
|
145
|
+
key={key}
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => {
|
|
148
|
+
action();
|
|
149
|
+
onOpenChange(false);
|
|
150
|
+
}}
|
|
151
|
+
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
|
|
152
|
+
>
|
|
153
|
+
<Icon className="h-4 w-4 text-muted-foreground/70" />
|
|
154
|
+
<span className="flex-1 text-left">{label}</span>
|
|
155
|
+
</button>
|
|
156
|
+
))}
|
|
158
157
|
</div>
|
|
159
158
|
|
|
160
159
|
<div className="my-2 h-px bg-border" />
|
|
@@ -105,7 +105,7 @@ export function ChatSidebar({
|
|
|
105
105
|
const { language, setLanguage } = useI18n();
|
|
106
106
|
const { theme, setTheme } = useTheme();
|
|
107
107
|
const currentThemeLabel = t(
|
|
108
|
-
THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "
|
|
108
|
+
THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "themeWork",
|
|
109
109
|
);
|
|
110
110
|
const currentLanguageLabel =
|
|
111
111
|
LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
|
|
@@ -132,6 +132,14 @@ export function ChatSidebar({
|
|
|
132
132
|
),
|
|
133
133
|
[agentsQuery.data?.agents],
|
|
134
134
|
);
|
|
135
|
+
const pinnedSessionKeys = useMemo(
|
|
136
|
+
() => new Set(listSnapshot.pinnedSessionKeys),
|
|
137
|
+
[listSnapshot.pinnedSessionKeys],
|
|
138
|
+
);
|
|
139
|
+
const pinnedProjectRoots = useMemo(
|
|
140
|
+
() => new Set(listSnapshot.pinnedProjectRoots),
|
|
141
|
+
[listSnapshot.pinnedProjectRoots],
|
|
142
|
+
);
|
|
135
143
|
const sortedItems = useMemo(
|
|
136
144
|
() => sortSessionItemsByActivityAtDesc(items),
|
|
137
145
|
[items],
|
|
@@ -140,10 +148,13 @@ export function ChatSidebar({
|
|
|
140
148
|
() => groupChildSessionsByParentKey(items),
|
|
141
149
|
[items],
|
|
142
150
|
);
|
|
143
|
-
const groups = useMemo(
|
|
151
|
+
const groups = useMemo(
|
|
152
|
+
() => groupSessionsByDate(sortedItems, pinnedSessionKeys),
|
|
153
|
+
[pinnedSessionKeys, sortedItems],
|
|
154
|
+
);
|
|
144
155
|
const projectGroups = useMemo(
|
|
145
|
-
() => groupSessionsByProject(sortedItems),
|
|
146
|
-
[sortedItems],
|
|
156
|
+
() => groupSessionsByProject(sortedItems, pinnedSessionKeys, pinnedProjectRoots),
|
|
157
|
+
[pinnedProjectRoots, pinnedSessionKeys, sortedItems],
|
|
147
158
|
);
|
|
148
159
|
const sessionTypeOptions = useMemo(
|
|
149
160
|
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
@@ -200,6 +211,7 @@ export function ChatSidebar({
|
|
|
200
211
|
draftLabel={draftLabel}
|
|
201
212
|
savingSessionKey={savingSessionKey}
|
|
202
213
|
sessionTypeOptions={sessionTypeOptions}
|
|
214
|
+
isPinned={pinnedSessionKeys.has(item.session.key)}
|
|
203
215
|
sessionTitle={getSessionTitle}
|
|
204
216
|
onSelectSession={presenter.chatSessionListManager.selectSession}
|
|
205
217
|
onOpenChildSessions={(parentSessionKey, activeChildSessionKey) =>
|
|
@@ -212,6 +224,9 @@ export function ChatSidebar({
|
|
|
212
224
|
onDraftLabelChange={setDraftLabel}
|
|
213
225
|
onSaveSessionLabel={saveSessionLabel}
|
|
214
226
|
onCancelEditingSessionLabel={cancelEditingSessionLabel}
|
|
227
|
+
onTogglePinned={() =>
|
|
228
|
+
presenter.chatSessionListManager.toggleSessionPinned(item.session.key)
|
|
229
|
+
}
|
|
215
230
|
/>
|
|
216
231
|
);
|
|
217
232
|
const createSessionAndOpenIfNeeded = (
|
|
@@ -233,9 +248,9 @@ export function ChatSidebar({
|
|
|
233
248
|
: shouldCollapse
|
|
234
249
|
? cn(
|
|
235
250
|
SIDEBAR_RAIL_WIDTH_CLASS,
|
|
236
|
-
"shrink-0
|
|
251
|
+
"shrink-0",
|
|
237
252
|
)
|
|
238
|
-
: "w-[280px] shrink-0
|
|
253
|
+
: "w-[280px] shrink-0",
|
|
239
254
|
)}
|
|
240
255
|
data-sidebar-collapsed={shouldCollapse ? "true" : "false"}
|
|
241
256
|
>
|
|
@@ -295,7 +310,6 @@ export function ChatSidebar({
|
|
|
295
310
|
isCollapsed={shouldCollapse}
|
|
296
311
|
isLoading={isLoading}
|
|
297
312
|
isProjectFirstView={isProjectFirstView}
|
|
298
|
-
onCreateSession={createSessionAndOpenIfNeeded}
|
|
299
313
|
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
300
314
|
projectGroups={projectGroups}
|
|
301
315
|
renderSessionItem={renderSessionItem}
|