@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.
Files changed (158) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/assets/{api-b1R9VdeH.js → api-B7Qigvtm.js} +2 -2
  3. package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
  4. package/dist/assets/{book-open-B77lQ2Tg.js → book-open-Ba2f7YiG.js} +1 -1
  5. package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
  6. package/dist/assets/{chat-page-CZ7WxPir.js → chat-page-C_5dcuE3.js} +1 -1
  7. package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
  8. package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-D2EG3nqE.js} +1 -1
  9. package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-BGR3HUi1.js} +1 -1
  10. package/dist/assets/{dist-jdGmbb6w.js → dist-7JTjycxE.js} +1 -1
  11. package/dist/assets/{dist-YtoqU-AT.js → dist-wXLhk4hl.js} +1 -1
  12. package/dist/assets/doc-browser-bHyXnxlj.js +1 -0
  13. package/dist/assets/doc-browser-context-CYAGm5d5.js +1 -0
  14. package/dist/assets/doc-browser-gTrQ3jCF.js +1 -0
  15. package/dist/assets/{es2015-bNpL2LKy.js → es2015-j1SRCIX5.js} +1 -1
  16. package/dist/assets/{external-link-BM6fk8q-.js → external-link-DxuPaDoK.js} +1 -1
  17. package/dist/assets/form-actions-BztL7P_T.js +1 -0
  18. package/dist/assets/{i18n-provider-CntWolRA.js → i18n-provider-B4oMxOFx.js} +1 -1
  19. package/dist/assets/index-CvrvblgX.css +1 -0
  20. package/dist/assets/index-Obd4JYU2.js +104 -0
  21. package/dist/assets/loader-circle-CdzgK4eH.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-CLmUstLN.js → mcp-marketplace-page-Bt7CuOoq.js} +2 -2
  23. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +1 -0
  24. package/dist/assets/model-config-page-DeKk9gvE.js +1 -0
  25. package/dist/assets/plus-Bx7wQbhG.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-Cm_a4GHf.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
  27. package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
  28. package/dist/assets/{react-BN8uzPnp.js → react-UirzUuST.js} +1 -1
  29. package/dist/assets/{refresh-cw-BpVcFPmJ.js → refresh-cw-eT8BF-8B.js} +1 -1
  30. package/dist/assets/remote-CDkHZtA8.js +1 -0
  31. package/dist/assets/rotate-cw-1JqxHTuw.js +1 -0
  32. package/dist/assets/runtime-config-page-BelAqiNG.js +1 -0
  33. package/dist/assets/{save-BpFW5fpk.js → save-Cb2g0BOi.js} +1 -1
  34. package/dist/assets/{search-D0uO7enf.js → search-BcHjwdsb.js} +1 -1
  35. package/dist/assets/search-config-page-zPm6--Ig.js +1 -0
  36. package/dist/assets/secrets-config-page-Cxyp-KuQ.js +3 -0
  37. package/dist/assets/security-config-B_gCq9fn.js +1 -0
  38. package/dist/assets/{select-Bh-CrA4p.js → select-CdSEOwVM.js} +1 -1
  39. package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-CTAtGexr.js} +1 -1
  40. package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-2DWauOjY.js} +1 -1
  41. package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-DPvakJ0a.js} +2 -2
  42. package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-P5oM14E1.js} +1 -1
  43. package/dist/assets/{switch-CFLJFNSd.js → switch-BU_UEULb.js} +1 -1
  44. package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-D-WsKoT1.js} +1 -1
  45. package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-CjkOaHW7.js} +1 -1
  46. package/dist/assets/{use-config-CVPgsBmU.js → use-config--hx5oxQn.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CSeBk9SF.js → use-confirm-dialog-CD9a_SK1.js} +1 -1
  48. package/dist/assets/x-DT5x45JU.js +1 -0
  49. package/dist/index.html +38 -30
  50. package/index.html +12 -4
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
  53. package/src/app/components/layout/sidebar-items.tsx +43 -4
  54. package/src/app/components/layout/sidebar.tsx +83 -61
  55. package/src/app/configs/app-navigation.config.ts +49 -0
  56. package/src/app/styles/design-system.css +82 -0
  57. package/src/features/channels/components/config/channel-form.tsx +14 -11
  58. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
  59. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
  60. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  61. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  63. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  64. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  65. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +29 -30
  66. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  67. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  68. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  69. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  70. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  71. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  72. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  73. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  74. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  75. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  76. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  77. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  78. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  79. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  80. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +159 -56
  81. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  82. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  83. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  84. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -1
  85. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +66 -7
  86. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  87. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  88. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  89. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  90. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  91. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  92. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  93. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  94. package/src/features/panel-apps/index.ts +1 -0
  95. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  96. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  97. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  98. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  99. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  100. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  101. package/src/features/settings/components/config/provider-form.tsx +11 -8
  102. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  103. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  104. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  105. package/src/features/settings/pages/model-config-page.tsx +26 -25
  106. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  107. package/src/features/settings/pages/search-config-page.tsx +8 -5
  108. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  109. package/src/index.css +18 -0
  110. package/src/shared/components/config-split-page.tsx +6 -6
  111. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  112. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  113. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  114. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +5 -1
  115. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  116. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  117. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  118. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  119. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  120. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  121. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  122. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  123. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  124. package/src/shared/components/ui/card.tsx +12 -4
  125. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  126. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  127. package/src/shared/hooks/use-server-path-read.ts +11 -1
  128. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -0
  130. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  131. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  132. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -0
  133. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  134. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  135. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  136. package/src/shared/lib/theme/index.test.ts +21 -0
  137. package/src/shared/lib/theme/index.ts +6 -4
  138. package/src/vite-env.d.ts +2 -0
  139. package/dist/assets/channels-list-page-DcsIQc6A.js +0 -8
  140. package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
  141. package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
  142. package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
  143. package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
  144. package/dist/assets/index-BIcEw90u.js +0 -104
  145. package/dist/assets/index-BiNSTKTU.css +0 -1
  146. package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
  147. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
  148. package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
  149. package/dist/assets/plus-vDqW5Kxp.js +0 -1
  150. package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
  151. package/dist/assets/remote-CLQ0IBM8.js +0 -1
  152. package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
  153. package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
  154. package/dist/assets/search-config-page-D0klFUXd.js +0 -1
  155. package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
  156. package/dist/assets/security-config-C5gnlkuj.js +0 -1
  157. package/dist/assets/x-BF2KQ--V.js +0 -1
  158. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -2,13 +2,17 @@ import { useEffect, useId, useMemo, useRef } from 'react';
2
2
  import type { ChatPanelAppCardViewModel } from '@nextclaw/agent-chat-ui';
3
3
  import { AppWindow, Maximize2 } from 'lucide-react';
4
4
  import { getPresenter } from '@/app/presenters/app.presenter';
5
- import { findPanelAppEntryByDisplayId, usePanelApps } from '@/features/panel-apps';
5
+ import {
6
+ PANEL_APP_IFRAME_SANDBOX,
7
+ findPanelAppEntryByDisplayId,
8
+ focusPanelAppIframe,
9
+ usePanelApps,
10
+ } from '@/features/panel-apps';
6
11
  import { createPanelAppRightPanelResourceTarget } from '@/features/right-panel-resources';
7
12
  import {
8
13
  createInlinePanelAppCardUrl,
9
14
  createFallbackPanelAppContentPath,
10
15
  createInlinePanelAppTab,
11
- PANEL_APP_INLINE_IFRAME_SANDBOX,
12
16
  } from '@/features/chat/features/message/utils/chat-inline-panel-app-card.utils';
13
17
  import { useDocBrowser } from '@/shared/components/doc-browser';
14
18
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
@@ -98,7 +102,7 @@ export function ChatInlinePanelAppCard({
98
102
  </div>
99
103
  {showExpandAction ? (
100
104
  <IconActionButton
101
- className="h-7 w-7 rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
105
+ size="md"
102
106
  icon={<Maximize2 className="h-3.5 w-3.5" />}
103
107
  label={t('chatPanelCardExpand')}
104
108
  onClick={openExpanded}
@@ -113,8 +117,9 @@ export function ChatInlinePanelAppCard({
113
117
  key={iframeInstanceId}
114
118
  src={cardUrl}
115
119
  title={title}
116
- sandbox={PANEL_APP_INLINE_IFRAME_SANDBOX}
117
- scrolling="auto"
120
+ sandbox={PANEL_APP_IFRAME_SANDBOX}
121
+ tabIndex={0}
122
+ onPointerOver={() => focusPanelAppIframe(iframeRef.current)}
118
123
  className="h-full w-full border-0 bg-card"
119
124
  />
120
125
  </div>
@@ -4,17 +4,6 @@ import {
4
4
  } from '@/features/right-panel-resources';
5
5
  import type { DocBrowserTab } from '@/shared/components/doc-browser/doc-browser-context';
6
6
 
7
- export const PANEL_APP_INLINE_IFRAME_SANDBOX = [
8
- 'allow-scripts',
9
- 'allow-forms',
10
- 'allow-modals',
11
- 'allow-popups',
12
- 'allow-popups-to-escape-sandbox',
13
- 'allow-downloads',
14
- 'allow-pointer-lock',
15
- 'allow-presentation',
16
- ].join(' ');
17
-
18
7
  const PANEL_APP_INLINE_CARD_SEARCH_PARAMS = {
19
8
  nextclawDisplayMode: 'card',
20
9
  nextclawPlacement: 'inline',
@@ -0,0 +1,116 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { ChatSidebarProjectGroups } from "@/features/chat/features/session/components/chat-sidebar-project-groups";
4
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ createSession: vi.fn(),
8
+ toggleProjectCollapsed: vi.fn(),
9
+ toggleProjectPinned: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
13
+ usePresenter: () => ({
14
+ chatSessionListManager: mocks,
15
+ }),
16
+ }));
17
+
18
+ const projectGroup = {
19
+ projectRoot: "/tmp/analysis-project",
20
+ projectName: "analysis-project",
21
+ items: [
22
+ {
23
+ session: {
24
+ key: "session:analysis",
25
+ createdAt: "2026-07-12T00:00:00.000Z",
26
+ updatedAt: "2026-07-12T00:00:00.000Z",
27
+ sessionType: "native",
28
+ sessionTypeMutable: false,
29
+ messageCount: 1,
30
+ },
31
+ },
32
+ ],
33
+ latestUpdatedAt: 1,
34
+ isPinned: false,
35
+ };
36
+
37
+ function renderProjectGroups(isPinned = false) {
38
+ return render(
39
+ <ChatSidebarProjectGroups
40
+ groups={[{ ...projectGroup, isPinned }]}
41
+ defaultSessionType="native"
42
+ sessionTypeOptions={[
43
+ { value: "native", label: "Native", icon: null, ready: true },
44
+ ]}
45
+ renderSessionItem={() => <div>Project session</div>}
46
+ />,
47
+ );
48
+ }
49
+
50
+ describe("ChatSidebarProjectGroups", () => {
51
+ beforeEach(() => {
52
+ mocks.createSession.mockReset();
53
+ mocks.toggleProjectCollapsed.mockReset();
54
+ mocks.toggleProjectPinned.mockReset();
55
+ useChatSessionListStore.setState({
56
+ snapshot: {
57
+ ...useChatSessionListStore.getState().snapshot,
58
+ collapsedProjectRoots: [],
59
+ selectedSessionKey: "session:current",
60
+ },
61
+ });
62
+ mocks.toggleProjectCollapsed.mockImplementation((projectRoot: string) => {
63
+ useChatSessionListStore.getState().setSnapshot({
64
+ collapsedProjectRoots: [projectRoot],
65
+ });
66
+ });
67
+ });
68
+
69
+ it("keeps the project header as the full-width folder row and collapses its sessions", () => {
70
+ renderProjectGroups();
71
+
72
+ const header = screen.getByLabelText("Collapse project");
73
+ expect(header.parentElement?.className).toContain("h-10");
74
+ expect(header.parentElement?.className).toContain("hover:bg-gray-200/60");
75
+ expect(header.parentElement?.className).not.toContain("focus-within:bg");
76
+ expect(screen.getByText("analysis-project").nextElementSibling?.tagName).toBe("svg");
77
+
78
+ fireEvent.click(header);
79
+
80
+ expect(mocks.toggleProjectCollapsed).toHaveBeenCalledWith(
81
+ "/tmp/analysis-project",
82
+ );
83
+ expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe(
84
+ "session:current",
85
+ );
86
+ expect(screen.queryByText("Project session")).toBeNull();
87
+ });
88
+
89
+ it("keeps project creation and pinning in one trailing action cluster", () => {
90
+ renderProjectGroups();
91
+
92
+ fireEvent.click(screen.getByLabelText("Pin project"));
93
+
94
+ expect(mocks.toggleProjectPinned).toHaveBeenCalledWith(
95
+ "/tmp/analysis-project",
96
+ );
97
+ expect(screen.getByLabelText("New Task · analysis-project")).not.toBeNull();
98
+ });
99
+
100
+ it("uses the same pin control to show and clear the project pin state", () => {
101
+ renderProjectGroups(true);
102
+
103
+ const unpinButton = screen.getByLabelText("Unpin project");
104
+ const pinIcon = unpinButton.querySelector("svg");
105
+
106
+ expect(pinIcon?.getAttribute("class")).toContain("fill-current");
107
+ expect(pinIcon?.getAttribute("class")).toContain("text-foreground");
108
+ expect(document.querySelectorAll("svg.fill-current")).toHaveLength(1);
109
+
110
+ fireEvent.click(unpinButton);
111
+
112
+ expect(mocks.toggleProjectPinned).toHaveBeenCalledWith(
113
+ "/tmp/analysis-project",
114
+ );
115
+ });
116
+ });
@@ -1,11 +1,13 @@
1
1
  import { useMemo, useState, type ReactNode } from 'react';
2
- import { Plus } from 'lucide-react';
2
+ import { ChevronDown, ChevronRight, Folder, Pin, Plus } from 'lucide-react';
3
+ import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
3
4
  import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
4
5
  import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
6
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
6
7
  import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
7
8
  import type { NcpSessionListItemView } from '@/features/chat/features/ncp/hooks/use-ncp-session-list-view';
8
9
  import type { ChatSidebarProjectGroup } from '@/features/chat/features/session/utils/chat-sidebar-session-groups.utils';
10
+ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
9
11
  import { t } from '@/shared/lib/i18n';
10
12
 
11
13
  export type { ChatSidebarProjectGroup };
@@ -17,7 +19,6 @@ type ChatSidebarProjectGroupsProps = {
17
19
  defaultSessionType: string;
18
20
  sessionTypeOptions: SessionTypeOption[];
19
21
  renderSessionItem: (item: NcpSessionListItemView) => ReactNode;
20
- onCreateSession: (sessionType: string, projectRoot: string) => void;
21
22
  };
22
23
 
23
24
  function resolveProjectGroupDefaultSessionType(
@@ -31,7 +32,11 @@ function resolveProjectGroupDefaultSessionType(
31
32
  }
32
33
 
33
34
  export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
34
- const { groups, defaultSessionType, sessionTypeOptions, renderSessionItem, onCreateSession } = props;
35
+ const { groups, defaultSessionType, sessionTypeOptions, renderSessionItem } = props;
36
+ const presenter = usePresenter();
37
+ const collapsedProjectRoots = useChatSessionListStore(
38
+ (state) => state.snapshot.collapsedProjectRoots,
39
+ );
35
40
  const [openProjectRoot, setOpenProjectRoot] = useState<string | null>(null);
36
41
  const preferredSessionType = useMemo(
37
42
  () => resolveProjectGroupDefaultSessionType(defaultSessionType, sessionTypeOptions),
@@ -43,62 +48,119 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
43
48
  <div className="space-y-3">
44
49
  {groups.map((group) => {
45
50
  const actionLabel = `${t('chatSidebarNewTask')} · ${group.projectName}`;
51
+ const isCollapsed = collapsedProjectRoots.includes(group.projectRoot);
52
+ const pinLabel = t(
53
+ group.isPinned ? 'chatSidebarUnpinProject' : 'chatSidebarPinProject',
54
+ );
46
55
 
47
56
  return (
48
57
  <div key={group.projectRoot}>
49
- <div className="flex items-center justify-between gap-2 px-2 py-0.5">
50
- <div className="flex min-w-0 items-center gap-1.5">
51
- <div
52
- className="truncate text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
58
+ <div className="group/project relative h-10 rounded-lg px-2 text-muted-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900">
59
+ <button
60
+ type="button"
61
+ aria-expanded={!isCollapsed}
62
+ aria-label={t(
63
+ isCollapsed
64
+ ? 'chatSidebarExpandProject'
65
+ : 'chatSidebarCollapseProject',
66
+ )}
67
+ className="flex h-full w-full min-w-0 items-center gap-1.5 pr-14 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
68
+ onClick={() =>
69
+ presenter.chatSessionListManager.toggleProjectCollapsed(
70
+ group.projectRoot,
71
+ )
72
+ }
73
+ >
74
+ <Folder className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
75
+ <span
76
+ className="truncate text-[11px] font-medium uppercase tracking-wider"
53
77
  title={group.projectRoot}
54
78
  >
55
79
  {group.projectName}
56
- </div>
80
+ </span>
81
+ {isCollapsed ? (
82
+ <ChevronRight
83
+ className="h-3.5 w-3.5 shrink-0"
84
+ aria-hidden="true"
85
+ />
86
+ ) : (
87
+ <ChevronDown
88
+ className="h-3.5 w-3.5 shrink-0"
89
+ aria-hidden="true"
90
+ />
91
+ )}
57
92
  <span className="shrink-0 text-[10px] text-muted-foreground/70">
58
93
  {group.items.length}
59
94
  </span>
60
- </div>
61
- {supportsSessionTypeChoice ? (
62
- <Popover
63
- open={openProjectRoot === group.projectRoot}
64
- onOpenChange={(nextOpen) => {
65
- setOpenProjectRoot(nextOpen ? group.projectRoot : null);
66
- }}
67
- >
68
- <PopoverTrigger asChild>
69
- <IconActionButton
70
- icon={<Plus className="h-3.5 w-3.5" />}
71
- label={actionLabel}
72
- tooltip={false}
73
- className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900"
74
- />
75
- </PopoverTrigger>
76
- <PopoverContent
77
- align="end"
78
- className="w-56 rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
95
+ </button>
96
+ <div className="pointer-events-none absolute right-2 top-1/2 flex -translate-y-1/2 items-center opacity-0 transition-opacity group-hover/project:pointer-events-auto group-hover/project:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100">
97
+ {supportsSessionTypeChoice ? (
98
+ <Popover
99
+ open={openProjectRoot === group.projectRoot}
100
+ onOpenChange={(nextOpen) => {
101
+ setOpenProjectRoot(nextOpen ? group.projectRoot : null);
102
+ }}
79
103
  >
80
- <ChatSessionTypeMenu
81
- options={sessionTypeOptions}
82
- selectedSessionType={preferredSessionType}
83
- onSelect={(sessionType) => {
84
- onCreateSession(sessionType, group.projectRoot);
85
- setOpenProjectRoot(null);
86
- }}
87
- />
88
- </PopoverContent>
89
- </Popover>
90
- ) : (
104
+ <PopoverTrigger asChild>
105
+ <IconActionButton
106
+ icon={<Plus className="h-3.5 w-3.5" />}
107
+ label={actionLabel}
108
+ />
109
+ </PopoverTrigger>
110
+ <PopoverContent
111
+ align="end"
112
+ className="w-56 rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
113
+ >
114
+ <ChatSessionTypeMenu
115
+ options={sessionTypeOptions}
116
+ selectedSessionType={preferredSessionType}
117
+ onSelect={(sessionType) => {
118
+ presenter.chatSessionListManager.createSession(
119
+ sessionType,
120
+ group.projectRoot,
121
+ );
122
+ setOpenProjectRoot(null);
123
+ }}
124
+ />
125
+ </PopoverContent>
126
+ </Popover>
127
+ ) : (
128
+ <IconActionButton
129
+ icon={<Plus className="h-3.5 w-3.5" />}
130
+ label={actionLabel}
131
+ onClick={() =>
132
+ presenter.chatSessionListManager.createSession(
133
+ preferredSessionType,
134
+ group.projectRoot,
135
+ )
136
+ }
137
+ />
138
+ )}
91
139
  <IconActionButton
92
- icon={<Plus className="h-3.5 w-3.5" />}
93
- label={actionLabel}
94
- className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900"
95
- onClick={() => onCreateSession(preferredSessionType, group.projectRoot)}
140
+ icon={
141
+ <Pin
142
+ className={
143
+ group.isPinned
144
+ ? 'h-3.5 w-3.5 fill-current text-foreground'
145
+ : 'h-3.5 w-3.5'
146
+ }
147
+ />
148
+ }
149
+ label={pinLabel}
150
+ tooltipSide="right"
151
+ onClick={() =>
152
+ presenter.chatSessionListManager.toggleProjectPinned(
153
+ group.projectRoot,
154
+ )
155
+ }
96
156
  />
97
- )}
98
- </div>
99
- <div className="space-y-0.5 pl-2">
100
- {group.items.map(renderSessionItem)}
157
+ </div>
101
158
  </div>
159
+ {isCollapsed ? null : (
160
+ <div className="mt-0.5 space-y-0.5 pl-2">
161
+ {group.items.map(renderSessionItem)}
162
+ </div>
163
+ )}
102
164
  </div>
103
165
  );
104
166
  })}
@@ -22,6 +22,7 @@ export function ChatSidebarSessionEntry(props: {
22
22
  draftLabel: string;
23
23
  savingSessionKey: string | null;
24
24
  sessionTypeOptions: ChatSessionTypeOption[];
25
+ isPinned: boolean;
25
26
  sessionTitle: (session: SessionEntryView) => string;
26
27
  onSelectSession: (sessionKey: string) => void;
27
28
  onOpenChildSessions: (
@@ -32,6 +33,7 @@ export function ChatSidebarSessionEntry(props: {
32
33
  onDraftLabelChange: (value: string) => void;
33
34
  onSaveSessionLabel: (session: SessionEntryView) => void;
34
35
  onCancelEditingSessionLabel: () => void;
36
+ onTogglePinned: () => void;
35
37
  }) {
36
38
  const {
37
39
  item,
@@ -43,6 +45,7 @@ export function ChatSidebarSessionEntry(props: {
43
45
  draftLabel,
44
46
  savingSessionKey,
45
47
  sessionTypeOptions,
48
+ isPinned,
46
49
  sessionTitle,
47
50
  onSelectSession,
48
51
  onOpenChildSessions,
@@ -50,6 +53,7 @@ export function ChatSidebarSessionEntry(props: {
50
53
  onDraftLabelChange,
51
54
  onSaveSessionLabel,
52
55
  onCancelEditingSessionLabel,
56
+ onTogglePinned,
53
57
  } = props;
54
58
  const { session, runStatus } = item;
55
59
  const active = selectedSessionKey === session.key;
@@ -80,6 +84,7 @@ export function ChatSidebarSessionEntry(props: {
80
84
  })}
81
85
  runStatus={runStatus}
82
86
  context={resolveSessionContextView(session, sessionTypeOptions)}
87
+ isPinned={isPinned}
83
88
  title={sessionTitle(session)}
84
89
  previewText={previewText}
85
90
  trailingText={formatSessionListTime(
@@ -104,6 +109,7 @@ export function ChatSidebarSessionEntry(props: {
104
109
  onDraftLabelChange={onDraftLabelChange}
105
110
  onSave={() => onSaveSessionLabel(session)}
106
111
  onCancel={onCancelEditingSessionLabel}
112
+ onTogglePinned={onTogglePinned}
107
113
  />
108
114
  );
109
115
  }
@@ -7,7 +7,7 @@ import { type SessionContextView } from '@/features/chat/features/session/utils/
7
7
  import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
8
8
  import { cn } from '@/shared/lib/utils';
9
9
  import { t } from '@/shared/lib/i18n';
10
- import { Check, GitBranch, Pencil, X } from 'lucide-react';
10
+ import { Check, GitBranch, Pencil, Pin, X } from 'lucide-react';
11
11
 
12
12
  type ChatSidebarSessionItemProps = {
13
13
  sessionKey: string;
@@ -15,6 +15,7 @@ type ChatSidebarSessionItemProps = {
15
15
  showUnreadDot: boolean;
16
16
  runStatus?: SessionRunStatus;
17
17
  context: SessionContextView;
18
+ isPinned: boolean;
18
19
  title: string;
19
20
  previewText: string;
20
21
  trailingText: string;
@@ -31,6 +32,7 @@ type ChatSidebarSessionItemProps = {
31
32
  onDraftLabelChange: (value: string) => void;
32
33
  onSave: () => void | Promise<void>;
33
34
  onCancel: () => void;
35
+ onTogglePinned: () => void;
34
36
  };
35
37
 
36
38
  type ChatSidebarSessionEditingViewProps = Pick<
@@ -72,7 +74,6 @@ function ChatSidebarSessionEditingView({
72
74
  icon={<Check className="h-3.5 w-3.5" />}
73
75
  label={t('save')}
74
76
  tooltip={false}
75
- className="h-7 w-7 rounded-lg text-gray-500 hover:bg-white hover:text-gray-900"
76
77
  onClick={() => void onSave()}
77
78
  disabled={isSaving}
78
79
  />
@@ -80,7 +81,6 @@ function ChatSidebarSessionEditingView({
80
81
  icon={<X className="h-3.5 w-3.5" />}
81
82
  label={t('cancel')}
82
83
  tooltip={false}
83
- className="h-7 w-7 rounded-lg text-gray-500 hover:bg-white hover:text-gray-900"
84
84
  onClick={onCancel}
85
85
  disabled={isSaving}
86
86
  />
@@ -100,6 +100,7 @@ function ChatSidebarSessionDisplayView({
100
100
  showUnreadDot,
101
101
  runStatus,
102
102
  context,
103
+ isPinned,
103
104
  title,
104
105
  previewText,
105
106
  trailingText,
@@ -109,14 +110,15 @@ function ChatSidebarSessionDisplayView({
109
110
  childSessionCount = 0,
110
111
  onSelect,
111
112
  onOpenChildSessions,
112
- onStartEditing
113
+ onStartEditing,
114
+ onTogglePinned
113
115
  }: ChatSidebarSessionDisplayViewProps) {
114
- const trailingControlsClassName = childSessionCount > 0 && onOpenChildSessions ? 'pr-14' : 'pr-6';
116
+ const trailingControlsClassName = childSessionCount > 0 && onOpenChildSessions ? 'pr-20' : 'pr-12';
115
117
 
116
118
  return (
117
119
  <div className="group/session relative">
118
120
  <button type="button" onClick={onSelect} className="w-full text-left">
119
- <div className={cn('flex min-w-0 items-start', trailingControlsClassName)}>
121
+ <div className={cn('flex min-h-6 min-w-0 items-center', trailingControlsClassName)}>
120
122
  <span className="flex min-w-0 items-center gap-1.5">
121
123
  {agentId?.trim() && agentId.trim().toLowerCase() !== 'main' ? (
122
124
  <AgentAvatar
@@ -126,7 +128,7 @@ function ChatSidebarSessionDisplayView({
126
128
  className="h-5 w-5 shrink-0"
127
129
  />
128
130
  ) : null}
129
- <span className="truncate font-medium">{title}</span>
131
+ <span className="truncate leading-6 font-medium">{title}</span>
130
132
  {context.label ? (
131
133
  <span
132
134
  className={cn(
@@ -140,7 +142,7 @@ function ChatSidebarSessionDisplayView({
140
142
  </span>
141
143
  ) : null}
142
144
  {context.icon ? (
143
- <span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center">
145
+ <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">
144
146
  <SessionContextIconNode icon={context.icon} className={active ? 'text-gray-700' : 'text-gray-500'} />
145
147
  </span>
146
148
  ) : null}
@@ -168,7 +170,7 @@ function ChatSidebarSessionDisplayView({
168
170
  onOpenChildSessions();
169
171
  }}
170
172
  className={cn(
171
- 'absolute right-6 top-0 inline-flex h-5 items-center gap-1 rounded-md px-1.5 text-[10px] font-medium text-gray-400 transition-all hover:bg-white hover:text-gray-900',
173
+ 'absolute right-12 top-0 inline-flex h-6 items-center gap-1 rounded-md px-1.5 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-black/10 hover:text-foreground',
172
174
  active
173
175
  ? 'opacity-100'
174
176
  : 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
@@ -181,23 +183,41 @@ function ChatSidebarSessionDisplayView({
181
183
  </button>
182
184
  ) : null}
183
185
  {runStatus ? (
184
- <span className="absolute right-0 top-0 inline-flex h-5 w-5 items-center justify-center transition-opacity group-hover/session:opacity-0 group-focus-within/session:opacity-0">
186
+ <span className="absolute right-0 top-0 inline-flex h-6 w-6 items-center justify-center transition-opacity group-hover/session:opacity-0 group-focus-within/session:opacity-0">
185
187
  <SessionRunBadge status={runStatus} />
186
188
  </span>
187
189
  ) : null}
188
- <IconActionButton
189
- icon={<Pencil className="h-3 w-3" />}
190
- label={t('edit')}
191
- tooltip={false}
192
- onClick={(event) => {
193
- event.stopPropagation();
194
- onStartEditing();
195
- }}
196
- className={cn(
197
- 'absolute right-0 top-0 inline-flex h-5 w-5 items-center justify-center rounded-md text-gray-400 transition-all hover:bg-white hover:text-gray-900',
198
- 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
199
- )}
200
- />
190
+ <div className="absolute right-0 top-0 flex h-6 items-center gap-0.5 opacity-0 transition-opacity group-hover/session:opacity-100 group-focus-within/session:opacity-100">
191
+ <IconActionButton
192
+ size="sm"
193
+ tone="strong"
194
+ icon={
195
+ <Pin
196
+ className={cn(
197
+ 'h-3.5 w-3.5',
198
+ isPinned && 'fill-current text-foreground',
199
+ )}
200
+ />
201
+ }
202
+ label={t(isPinned ? 'chatSidebarUnpinSession' : 'chatSidebarPinSession')}
203
+ tooltipSide="right"
204
+ onClick={(event) => {
205
+ event.stopPropagation();
206
+ onTogglePinned();
207
+ }}
208
+ />
209
+ <IconActionButton
210
+ size="sm"
211
+ tone="strong"
212
+ icon={<Pencil className="h-3.5 w-3.5" />}
213
+ label={t('edit')}
214
+ tooltipSide="right"
215
+ onClick={(event) => {
216
+ event.stopPropagation();
217
+ onStartEditing();
218
+ }}
219
+ />
220
+ </div>
201
221
  </div>
202
222
  );
203
223
  }
@@ -208,6 +228,7 @@ export function ChatSidebarSessionItem({
208
228
  showUnreadDot,
209
229
  runStatus,
210
230
  context,
231
+ isPinned,
211
232
  title,
212
233
  previewText,
213
234
  trailingText,
@@ -223,7 +244,8 @@ export function ChatSidebarSessionItem({
223
244
  onStartEditing,
224
245
  onDraftLabelChange,
225
246
  onSave,
226
- onCancel
247
+ onCancel,
248
+ onTogglePinned
227
249
  }: ChatSidebarSessionItemProps) {
228
250
  return (
229
251
  <div
@@ -249,6 +271,7 @@ export function ChatSidebarSessionItem({
249
271
  showUnreadDot={showUnreadDot}
250
272
  runStatus={runStatus}
251
273
  context={context}
274
+ isPinned={isPinned}
252
275
  title={title}
253
276
  previewText={previewText}
254
277
  trailingText={trailingText}
@@ -259,6 +282,7 @@ export function ChatSidebarSessionItem({
259
282
  childSessionCount={childSessionCount}
260
283
  onOpenChildSessions={onOpenChildSessions}
261
284
  onStartEditing={onStartEditing}
285
+ onTogglePinned={onTogglePinned}
262
286
  />
263
287
  )}
264
288
  </div>
@@ -19,7 +19,6 @@ type ChatSidebarSessionListProps = {
19
19
  defaultSessionType: string;
20
20
  sessionTypeOptions: SessionTypeOption[];
21
21
  renderSessionItem: (item: NcpSessionListItemView) => ReactNode;
22
- onCreateSession: (sessionType: string, projectRoot?: string | null) => void;
23
22
  };
24
23
 
25
24
  function ChatSidebarEmptyState({ label }: { label: string }) {
@@ -36,7 +35,6 @@ export function ChatSidebarSessionList({
36
35
  groups,
37
36
  isLoading,
38
37
  isProjectFirstView,
39
- onCreateSession,
40
38
  projectGroups,
41
39
  renderSessionItem,
42
40
  sessionTypeOptions,
@@ -54,7 +52,6 @@ export function ChatSidebarSessionList({
54
52
  defaultSessionType={defaultSessionType}
55
53
  sessionTypeOptions={sessionTypeOptions}
56
54
  renderSessionItem={renderSessionItem}
57
- onCreateSession={onCreateSession}
58
55
  />
59
56
  );
60
57
  }
@@ -9,7 +9,6 @@ import { ChatSessionProjectDialog } from './chat-session-project-dialog';
9
9
  import { t } from '@/shared/lib/i18n';
10
10
 
11
11
  const SESSION_HEADER_ACTION_GROUP_CLASS = 'flex shrink-0 items-center gap-1.5';
12
- const SESSION_HEADER_ACTION_BUTTON_CLASS = 'h-7 w-7 shrink-0 rounded-lg text-muted-foreground/70 hover:bg-accent hover:text-accent-foreground';
13
12
 
14
13
  type ChatSessionHeaderActionsProps = {
15
14
  sessionKey: string;
@@ -65,7 +64,6 @@ export function ChatSessionHeaderActions({
65
64
  <IconActionButton
66
65
  icon={<GitBranch className="h-4 w-4" />}
67
66
  label={t('chatSessionOpenChildSessions')}
68
- className={SESSION_HEADER_ACTION_BUTTON_CLASS}
69
67
  onClick={onOpenChildSessions}
70
68
  disabled={isBusy}
71
69
  />
@@ -74,7 +72,6 @@ export function ChatSessionHeaderActions({
74
72
  <IconActionButton
75
73
  icon={<AlarmClock className="h-4 w-4" />}
76
74
  label={t('chatSessionOpenCronJobs')}
77
- className={SESSION_HEADER_ACTION_BUTTON_CLASS}
78
75
  onClick={onOpenSessionCronJobs}
79
76
  disabled={isBusy}
80
77
  />
@@ -85,7 +82,6 @@ export function ChatSessionHeaderActions({
85
82
  icon={<MoreVertical className="h-4 w-4" />}
86
83
  label={t('chatSessionMoreActions')}
87
84
  tooltip={false}
88
- className={SESSION_HEADER_ACTION_BUTTON_CLASS}
89
85
  disabled={isBusy}
90
86
  />
91
87
  </PopoverTrigger>