@nextclaw/ui 0.20.0 → 0.20.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 (108) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/assets/{api-C2iYhdTe.js → api-DvpQWjTS.js} +3 -3
  3. package/dist/assets/{appearance-settings-page-BwQDQr0n.js → appearance-settings-page-U0m0ktu3.js} +1 -1
  4. package/dist/assets/{book-open-C0IlT9zs.js → book-open-DRofSTWr.js} +1 -1
  5. package/dist/assets/channels-list-page-BURQarD0.js +8 -0
  6. package/dist/assets/{config-split-page-VDTf3zZb.js → config-split-page-BrAQYTIl.js} +1 -1
  7. package/dist/assets/{confirm-dialog-BI_Vt2s_.js → confirm-dialog-DDJomLnO.js} +1 -1
  8. package/dist/assets/{createLucideIcon-Bydi9Gc5.js → createLucideIcon-DzkNHr5Z.js} +1 -1
  9. package/dist/assets/{desktop-update-config-DVoD6NxG.js → desktop-update-config-BzZFhMv9.js} +1 -1
  10. package/dist/assets/{dist-CmHtkFbs.js → dist-DXbLo2b1.js} +1 -1
  11. package/dist/assets/{dist-A5btQciA.js → dist-NskPB_yW.js} +1 -1
  12. package/dist/assets/doc-browser-DB22tQKU.js +1 -0
  13. package/dist/assets/{doc-browser-context-Bxw0ora-.js → doc-browser-context-CnKV4gJy.js} +1 -1
  14. package/dist/assets/doc-browser-sJjw9fl5.js +1 -0
  15. package/dist/assets/{extensions-CG4voDCh.js → extensions-DvlFMpAs.js} +1 -1
  16. package/dist/assets/{form-actions-lv47uGIB.js → form-actions-UnPEZBkx.js} +1 -1
  17. package/dist/assets/index-Dc_ZMkmo.css +1 -0
  18. package/dist/assets/index-N1HnesBp.js +109 -0
  19. package/dist/assets/loader-circle-BlPXxgDB.js +1 -0
  20. package/dist/assets/mcp-marketplace-page-BIgFTbMq.js +9 -0
  21. package/dist/assets/mcp-marketplace-page-CZN4ovdK.js +1 -0
  22. package/dist/assets/model-config-page-CnSm7kfa.js +1 -0
  23. package/dist/assets/{navigation-link-D0agTj5t.js → navigation-link-NjUdG6Vp.js} +1 -1
  24. package/dist/assets/plus-DVXol9Q5.js +1 -0
  25. package/dist/assets/privacy-settings-page-DVn2HU7W.js +1 -0
  26. package/dist/assets/providers-config-page-CzHJS3eg.js +1 -0
  27. package/dist/assets/react-DCVbGroo.js +8 -0
  28. package/dist/assets/{refresh-cw-DjgY2Eju.js → refresh-cw-CkeninE0.js} +1 -1
  29. package/dist/assets/remote-B2lsJF1c.js +1 -0
  30. package/dist/assets/{rotate-cw-Dot4PVh0.js → rotate-cw-DP33PuXU.js} +1 -1
  31. package/dist/assets/runtime-config-page-D69buyGp.js +1 -0
  32. package/dist/assets/{save-B-jdTe_c.js → save-Cw2Xz-pB.js} +1 -1
  33. package/dist/assets/{search-CEp2AIiV.js → search-C2CFAVvd.js} +1 -1
  34. package/dist/assets/{search-config-page-Dp5guS5O.js → search-config-page-dqNT0Swf.js} +1 -1
  35. package/dist/assets/{secrets-config-page-C7nRMjiv.js → secrets-config-page-De6J3wVx.js} +2 -2
  36. package/dist/assets/security-config-Dcu9C4OL.js +1 -0
  37. package/dist/assets/{select-BNA3cma8.js → select-DSuMhwVP.js} +1 -1
  38. package/dist/assets/skeleton-QoWydNLp.js +1 -0
  39. package/dist/assets/{tag-chip-DmCNi4-9.js → tag-chip-DFBLgsGf.js} +1 -1
  40. package/dist/assets/x-BXvAzpjb.js +1 -0
  41. package/dist/index.html +21 -21
  42. package/package.json +8 -8
  43. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +4 -3
  44. package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +11 -4
  45. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +17 -17
  46. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +17 -5
  47. package/src/features/chat/components/layout/chat-sidebar.tsx +28 -21
  48. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +41 -1
  49. package/src/features/chat/features/conversation/hooks/use-session-pending-input-actions.ts +22 -4
  50. package/src/features/chat/features/message/components/chat-message-list.container.tsx +33 -4
  51. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +186 -0
  52. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +9 -0
  53. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +71 -0
  54. package/src/features/chat/features/message/utils/__tests__/chat-message-trigger-details.utils.test.ts +147 -0
  55. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  56. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +36 -0
  57. package/src/features/chat/features/message/utils/chat-message-trigger-details.utils.ts +118 -0
  58. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +50 -0
  59. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +90 -1
  60. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +24 -3
  61. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +35 -19
  62. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +26 -2
  63. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +55 -27
  64. package/src/features/chat/features/session/components/chat-sidebar-context-card.tsx +60 -0
  65. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +95 -54
  66. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +9 -8
  67. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +141 -95
  68. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +11 -2
  69. package/src/features/chat/features/session/hooks/use-chat-sidebar-context-counts.ts +40 -0
  70. package/src/features/chat/features/session/utils/ncp-session-context-metadata.utils.ts +5 -1
  71. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +16 -9
  72. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +5 -5
  73. package/src/features/chat/features/workspace/components/__tests__/workspace-markdown-outline.test.tsx +128 -0
  74. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -0
  75. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +20 -2
  76. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -40
  77. package/src/features/chat/features/workspace/components/workspace-markdown-outline.tsx +156 -0
  78. package/src/features/chat/managers/__tests__/chat-completion-notification.manager.test.ts +70 -0
  79. package/src/features/chat/managers/chat-completion-notification.manager.ts +23 -6
  80. package/src/features/settings/pages/__tests__/privacy-settings-page.test.tsx +14 -5
  81. package/src/features/settings/pages/privacy-settings-page.tsx +38 -2
  82. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -8
  83. package/src/shared/components/ui/tooltip.tsx +16 -1
  84. package/src/shared/hooks/use-config.ts +11 -0
  85. package/src/shared/lib/api/types.ts +8 -0
  86. package/src/shared/lib/api/utils/config.utils.ts +5 -0
  87. package/src/shared/lib/i18n/locales/en-US/chat.json +28 -0
  88. package/src/shared/lib/i18n/locales/en-US/core.json +17 -9
  89. package/src/shared/lib/i18n/locales/zh-CN/chat.json +28 -0
  90. package/src/shared/lib/i18n/locales/zh-CN/core.json +17 -9
  91. package/dist/assets/channels-list-page-B0z4bh8e.js +0 -8
  92. package/dist/assets/doc-browser-Cm_tMZ_C.js +0 -1
  93. package/dist/assets/doc-browser-Dto54tCf.js +0 -1
  94. package/dist/assets/index-BTSwNDVi.js +0 -109
  95. package/dist/assets/index-QrdUWHfm.css +0 -1
  96. package/dist/assets/loader-circle-CkaqeEne.js +0 -1
  97. package/dist/assets/mcp-marketplace-page-BSk0qUOX.js +0 -9
  98. package/dist/assets/mcp-marketplace-page-BgZGc-S_.js +0 -1
  99. package/dist/assets/model-config-page-c8RnUGoH.js +0 -1
  100. package/dist/assets/plus-BiMjkv3R.js +0 -1
  101. package/dist/assets/privacy-settings-page-CPkyTvEE.js +0 -1
  102. package/dist/assets/providers-config-page-CGUEb5Dx.js +0 -1
  103. package/dist/assets/react-BMYFkaGi.js +0 -8
  104. package/dist/assets/remote-1LxkpRED.js +0 -1
  105. package/dist/assets/runtime-config-page-C6nzqTHg.js +0 -1
  106. package/dist/assets/security-config-DvHUDxSg.js +0 -1
  107. package/dist/assets/skeleton-CbroEO8Q.js +0 -1
  108. package/dist/assets/x-BTo5-A8h.js +0 -1
@@ -1,17 +1,17 @@
1
- import { render, screen } from '@testing-library/react';
2
- import userEvent from '@testing-library/user-event';
3
- import { beforeEach, expect, it, vi } from 'vitest';
4
- import { ChatSidebarSessionItem } from '@/features/chat/features/session/components/chat-sidebar-session-item';
1
+ import { render, screen, within } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { beforeEach, expect, it, vi } from "vitest";
4
+ import { ChatSidebarSessionItem } from "@/features/chat/features/session/components/chat-sidebar-session-item";
5
5
 
6
6
  const mocks = vi.hoisted(() => ({
7
7
  copyText: vi.fn(),
8
8
  }));
9
9
 
10
- vi.mock('@nextclaw/agent-chat-ui', () => ({
10
+ vi.mock("@nextclaw/agent-chat-ui", () => ({
11
11
  copyText: mocks.copyText,
12
12
  }));
13
13
 
14
- vi.mock('sonner', () => ({
14
+ vi.mock("sonner", () => ({
15
15
  toast: {
16
16
  success: vi.fn(),
17
17
  error: vi.fn(),
@@ -28,9 +28,9 @@ beforeEach(() => {
28
28
  showUnreadDot={false}
29
29
  context={{
30
30
  icon: {
31
- kind: 'runtime-image',
32
- src: '/runtime-icons/codex-openai.svg',
33
- name: 'Codex',
31
+ kind: "runtime-image",
32
+ src: "/runtime-icons/codex-openai.svg",
33
+ name: "Codex",
34
34
  },
35
35
  label: null,
36
36
  }}
@@ -38,6 +38,9 @@ beforeEach(() => {
38
38
  title="Current Task"
39
39
  previewText="Preview"
40
40
  trailingText="Now"
41
+ childSessionCount={2}
42
+ cronJobCount={3}
43
+ projectName="nextbot"
41
44
  isEditing={false}
42
45
  draftLabel="Current Task"
43
46
  isSaving={false}
@@ -51,33 +54,58 @@ beforeEach(() => {
51
54
  );
52
55
  });
53
56
 
54
- it('shows session actions only on hover or when an action owns focus', () => {
55
- screen.getByText('Current Task').closest('button')?.focus();
56
- const actions = screen.getByLabelText('Pin session').parentElement;
57
+ it("shows session actions only on hover or when an action owns focus", () => {
58
+ screen.getByText("Current Task").closest("button")?.focus();
59
+ const actions = screen.getByLabelText("Pin session").parentElement;
60
+ const titleRow =
61
+ screen.getByText("Current Task").parentElement?.parentElement;
57
62
 
58
- expect(actions?.className).toContain('opacity-0');
59
- expect(actions?.className).toContain('group-hover/session:opacity-100');
60
- expect(actions?.className).toContain('focus-within:opacity-100');
63
+ expect(actions?.className).toContain("opacity-0");
64
+ expect(actions?.className).toContain("group-hover/session:opacity-100");
65
+ expect(actions?.className).toContain("focus-within:opacity-100");
61
66
  expect(actions?.className).not.toContain(
62
- 'group-focus-within/session:opacity-100',
67
+ "group-focus-within/session:opacity-100",
63
68
  );
64
- expect(screen.getByRole('button', { name: 'More actions' })).toBeTruthy();
69
+ expect(titleRow?.classList.contains("pr-20")).toBe(false);
70
+ expect(titleRow?.className).toContain("group-hover/session:pr-20");
71
+ expect(titleRow?.className).toContain(
72
+ "group-has-[[data-session-actions]:focus-within]/session:pr-20",
73
+ );
74
+ expect(titleRow?.className).not.toContain("pr-32");
75
+ expect(
76
+ screen.queryByRole("button", { name: "View child sessions" }),
77
+ ).toBeNull();
78
+ expect(screen.getByRole("button", { name: "More actions" })).toBeTruthy();
65
79
  });
66
80
 
67
- it('copies the sidebar session ID from the more-actions menu', async () => {
81
+ it("copies the sidebar session ID from the more-actions menu", async () => {
68
82
  const user = userEvent.setup();
69
83
 
70
- await user.click(screen.getByRole('button', { name: 'More actions' }));
71
- await user.click(screen.getByRole('button', { name: 'Copy session ID' }));
84
+ await user.click(screen.getByRole("button", { name: "More actions" }));
85
+ await user.click(screen.getByRole("button", { name: "Copy session ID" }));
86
+
87
+ expect(mocks.copyText).toHaveBeenCalledWith("session:current");
88
+ });
89
+
90
+ it("sizes the runtime icon to the session title text", () => {
91
+ const runtimeIcon = screen.getByRole("img", { name: "Codex logo" });
72
92
 
73
- expect(mocks.copyText).toHaveBeenCalledWith('session:current');
93
+ expect(runtimeIcon.parentElement?.className).toContain("h-[13px]");
94
+ expect(runtimeIcon.parentElement?.className).toContain("w-[13px]");
95
+ expect(runtimeIcon.parentElement?.parentElement?.className).toContain("h-4");
96
+ expect(runtimeIcon.parentElement?.parentElement?.className).toContain("w-4");
74
97
  });
75
98
 
76
- it('sizes the runtime icon to the session title text', () => {
77
- const runtimeIcon = screen.getByRole('img', { name: 'Codex logo' });
99
+ it("shows session context from the row on hover", async () => {
100
+ const user = userEvent.setup();
101
+
102
+ await user.hover(screen.getByText("Current Task").closest("button")!);
78
103
 
79
- expect(runtimeIcon.parentElement?.className).toContain('h-[13px]');
80
- expect(runtimeIcon.parentElement?.className).toContain('w-[13px]');
81
- expect(runtimeIcon.parentElement?.parentElement?.className).toContain('h-4');
82
- expect(runtimeIcon.parentElement?.parentElement?.className).toContain('w-4');
104
+ const tooltip = await screen.findByRole("tooltip");
105
+ expect(within(tooltip).getByText("Project")).toBeTruthy();
106
+ expect(within(tooltip).getByText("Child sessions")).toBeTruthy();
107
+ expect(within(tooltip).getByText("Scheduled tasks")).toBeTruthy();
108
+ expect(within(tooltip).getByText("nextbot")).toBeTruthy();
109
+ expect(within(tooltip).getByText("2")).toBeTruthy();
110
+ expect(within(tooltip).getByText("3")).toBeTruthy();
83
111
  });
@@ -0,0 +1,60 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import {
3
+ Tooltip,
4
+ TooltipContent,
5
+ TooltipProvider,
6
+ TooltipTrigger,
7
+ } from "@/shared/components/ui/tooltip";
8
+
9
+ export type ChatSidebarContextMetric = {
10
+ icon: ReactNode;
11
+ label: string;
12
+ value: ReactNode;
13
+ };
14
+
15
+ export function ChatSidebarContextCard({
16
+ children,
17
+ metrics,
18
+ title,
19
+ }: {
20
+ children: ReactElement;
21
+ metrics: readonly ChatSidebarContextMetric[];
22
+ title: string;
23
+ }) {
24
+ return (
25
+ <TooltipProvider delayDuration={350} skipDelayDuration={100}>
26
+ <Tooltip>
27
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
28
+ <TooltipContent
29
+ side="right"
30
+ align="start"
31
+ sideOffset={10}
32
+ collisionPadding={12}
33
+ className="pointer-events-none w-64 rounded-xl border-border/80 bg-popover p-3 text-popover-foreground shadow-[0_18px_48px_-24px_rgba(15,23,42,0.45)]"
34
+ >
35
+ <div className="truncate text-sm font-semibold leading-5">
36
+ {title}
37
+ </div>
38
+ <div className="mt-2 space-y-0.5">
39
+ {metrics.map((metric) => (
40
+ <div
41
+ key={metric.label}
42
+ className="flex h-5 min-w-0 items-center gap-2 text-xs leading-5"
43
+ >
44
+ <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
45
+ {metric.icon}
46
+ </span>
47
+ <span className="min-w-0 flex-1 truncate text-muted-foreground">
48
+ {metric.label}
49
+ </span>
50
+ <span className="min-w-0 max-w-40 truncate tabular-nums text-foreground/80">
51
+ {metric.value}
52
+ </span>
53
+ </div>
54
+ ))}
55
+ </div>
56
+ </TooltipContent>
57
+ </Tooltip>
58
+ </TooltipProvider>
59
+ );
60
+ }
@@ -1,15 +1,24 @@
1
- import { useMemo, useState, type ReactNode } from 'react';
2
- import { ChevronDown, ChevronRight, Folder, Pin, Plus } from 'lucide-react';
3
- import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
1
+ import { useMemo, useState, type ReactNode } from "react";
2
+ import {
3
+ AlarmClock,
4
+ ChevronDown,
5
+ ChevronRight,
6
+ Folder,
7
+ MessageSquareText,
8
+ Pin,
9
+ Plus,
10
+ } from "lucide-react";
11
+ import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
4
12
  import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
5
- import { Popover, PopoverTrigger } from '@/shared/components/ui/popover';
6
- import { ChatPopoverContent } from '@/features/chat/components/chat-popover-content';
7
- import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
13
+ import { Popover, PopoverTrigger } from "@/shared/components/ui/popover";
14
+ import { ChatPopoverContent } from "@/features/chat/components/chat-popover-content";
15
+ import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
8
16
  import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
9
- import type { NcpSessionListItemView } from '@/features/chat/features/ncp/hooks/use-ncp-session-list-view';
10
- import type { ChatSidebarProjectGroup } from '@/features/chat/features/session/utils/chat-sidebar-session-groups.utils';
11
- import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
12
- import { t } from '@/shared/lib/i18n';
17
+ import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
18
+ import type { ChatSidebarProjectGroup } from "@/features/chat/features/session/utils/chat-sidebar-session-groups.utils";
19
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
20
+ import { t } from "@/shared/lib/i18n";
21
+ import { ChatSidebarContextCard } from "@/features/chat/features/session/components/chat-sidebar-context-card";
13
22
 
14
23
  export type { ChatSidebarProjectGroup };
15
24
 
@@ -20,80 +29,112 @@ type ChatSidebarProjectGroupsProps = {
20
29
  defaultSessionType: string;
21
30
  sessionTypeOptions: SessionTypeOption[];
22
31
  renderSessionItem: (item: NcpSessionListItemView) => ReactNode;
32
+ projectCronJobCountByRoot: ReadonlyMap<string, number>;
23
33
  };
24
34
 
25
35
  function resolveProjectGroupDefaultSessionType(
26
36
  defaultSessionType: string,
27
- sessionTypeOptions: SessionTypeOption[]
37
+ sessionTypeOptions: SessionTypeOption[],
28
38
  ): string {
29
- if (sessionTypeOptions.some((option) => option.value === defaultSessionType)) {
39
+ if (
40
+ sessionTypeOptions.some((option) => option.value === defaultSessionType)
41
+ ) {
30
42
  return defaultSessionType;
31
43
  }
32
44
  return sessionTypeOptions[0]?.value ?? defaultSessionType;
33
45
  }
34
46
 
35
47
  export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
36
- const { groups, defaultSessionType, sessionTypeOptions, renderSessionItem } = props;
48
+ const {
49
+ groups,
50
+ defaultSessionType,
51
+ sessionTypeOptions,
52
+ renderSessionItem,
53
+ projectCronJobCountByRoot,
54
+ } = props;
37
55
  const presenter = usePresenter();
38
56
  const collapsedProjectRoots = useChatSessionListStore(
39
57
  (state) => state.snapshot.collapsedProjectRoots,
40
58
  );
41
59
  const [openProjectRoot, setOpenProjectRoot] = useState<string | null>(null);
42
60
  const preferredSessionType = useMemo(
43
- () => resolveProjectGroupDefaultSessionType(defaultSessionType, sessionTypeOptions),
44
- [defaultSessionType, sessionTypeOptions]
61
+ () =>
62
+ resolveProjectGroupDefaultSessionType(
63
+ defaultSessionType,
64
+ sessionTypeOptions,
65
+ ),
66
+ [defaultSessionType, sessionTypeOptions],
45
67
  );
46
68
  const supportsSessionTypeChoice = sessionTypeOptions.length > 1;
47
69
 
48
70
  return (
49
71
  <div className="space-y-0.5">
50
72
  {groups.map((group) => {
51
- const actionLabel = `${t('chatSidebarNewTask')} · ${group.projectName}`;
73
+ const actionLabel = `${t("chatSidebarNewTask")} · ${group.projectName}`;
52
74
  const isCollapsed = collapsedProjectRoots.includes(group.projectRoot);
53
75
  const pinLabel = t(
54
- group.isPinned ? 'chatSidebarUnpinProject' : 'chatSidebarPinProject',
76
+ group.isPinned ? "chatSidebarUnpinProject" : "chatSidebarPinProject",
55
77
  );
56
78
 
57
79
  return (
58
80
  <div key={group.projectRoot}>
59
81
  <div className="group/project relative h-8 rounded-lg px-2 text-muted-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900">
60
- <button
61
- type="button"
62
- aria-expanded={!isCollapsed}
63
- aria-label={t(
64
- isCollapsed
65
- ? 'chatSidebarExpandProject'
66
- : 'chatSidebarCollapseProject',
67
- )}
68
- 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-1 focus-visible:ring-border"
69
- onClick={() =>
70
- presenter.chatSessionListManager.toggleProjectCollapsed(
71
- group.projectRoot,
72
- )
73
- }
82
+ <ChatSidebarContextCard
83
+ title={group.projectName}
84
+ metrics={[
85
+ {
86
+ icon: <Folder className="h-3.5 w-3.5" />,
87
+ label: t("chatSidebarContextPath"),
88
+ value: group.projectRoot,
89
+ },
90
+ {
91
+ icon: <MessageSquareText className="h-3.5 w-3.5" />,
92
+ label: t("chatSidebarContextSessions"),
93
+ value: group.items.length,
94
+ },
95
+ {
96
+ icon: <AlarmClock className="h-3.5 w-3.5" />,
97
+ label: t("chatSidebarContextScheduledTasks"),
98
+ value:
99
+ projectCronJobCountByRoot.get(group.projectRoot) ?? 0,
100
+ },
101
+ ]}
74
102
  >
75
- <Folder className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
76
- <span
77
- className="truncate text-[11px] font-medium uppercase tracking-wider"
78
- title={group.projectRoot}
103
+ <button
104
+ type="button"
105
+ aria-expanded={!isCollapsed}
106
+ aria-label={t(
107
+ isCollapsed
108
+ ? "chatSidebarExpandProject"
109
+ : "chatSidebarCollapseProject",
110
+ )}
111
+ 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-1 focus-visible:ring-border"
112
+ onClick={() =>
113
+ presenter.chatSessionListManager.toggleProjectCollapsed(
114
+ group.projectRoot,
115
+ )
116
+ }
79
117
  >
80
- {group.projectName}
81
- </span>
82
- {isCollapsed ? (
83
- <ChevronRight
84
- className="h-3.5 w-3.5 shrink-0"
85
- aria-hidden="true"
86
- />
87
- ) : (
88
- <ChevronDown
89
- className="h-3.5 w-3.5 shrink-0"
90
- aria-hidden="true"
91
- />
92
- )}
93
- <span className="shrink-0 text-[10px] text-muted-foreground/70">
94
- {group.items.length}
95
- </span>
96
- </button>
118
+ <Folder className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
119
+ <span
120
+ className="truncate text-[11px] font-medium uppercase tracking-wider"
121
+ title={group.projectRoot}
122
+ >
123
+ {group.projectName}
124
+ </span>
125
+ {isCollapsed ? (
126
+ <ChevronRight
127
+ className="h-3.5 w-3.5 shrink-0 opacity-0 transition-opacity group-hover/project:opacity-100 group-focus-within/project:opacity-100"
128
+ aria-hidden="true"
129
+ />
130
+ ) : (
131
+ <ChevronDown
132
+ className="h-3.5 w-3.5 shrink-0 opacity-0 transition-opacity group-hover/project:opacity-100 group-focus-within/project:opacity-100"
133
+ aria-hidden="true"
134
+ />
135
+ )}
136
+ </button>
137
+ </ChatSidebarContextCard>
97
138
  <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">
98
139
  {supportsSessionTypeChoice ? (
99
140
  <Popover
@@ -142,8 +183,8 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
142
183
  <Pin
143
184
  className={
144
185
  group.isPinned
145
- ? 'h-3.5 w-3.5 fill-current text-foreground'
146
- : 'h-3.5 w-3.5'
186
+ ? "h-3.5 w-3.5 fill-current text-foreground"
187
+ : "h-3.5 w-3.5"
147
188
  }
148
189
  />
149
190
  }
@@ -8,6 +8,7 @@ import {
8
8
  } from "@/features/chat/features/session/utils/chat-session-display.utils";
9
9
  import { resolveSessionContextView } from "@/features/chat/features/session/utils/session-context.utils";
10
10
  import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
11
+ import { t } from "@/shared/lib/i18n";
11
12
 
12
13
  export function ChatSidebarSessionEntry(props: {
13
14
  item: NcpSessionListItemView;
@@ -18,6 +19,7 @@ export function ChatSidebarSessionEntry(props: {
18
19
  { displayName?: string | null; avatarUrl?: string | null }
19
20
  >;
20
21
  childSessionsByParentKey: Map<string, NcpSessionListItemView[]>;
22
+ cronJobCount: number;
21
23
  editingSessionKey: string | null;
22
24
  draftLabel: string;
23
25
  savingSessionKey: string | null;
@@ -25,10 +27,6 @@ export function ChatSidebarSessionEntry(props: {
25
27
  isPinned: boolean;
26
28
  sessionTitle: (session: SessionEntryView) => string;
27
29
  onSelectSession: (sessionKey: string) => void;
28
- onOpenChildSessions: (
29
- parentSessionKey: string,
30
- activeChildSessionKey: string | null,
31
- ) => void;
32
30
  onStartEditingSessionLabel: (session: SessionEntryView) => void;
33
31
  onDraftLabelChange: (value: string) => void;
34
32
  onSaveSessionLabel: (session: SessionEntryView) => void;
@@ -41,6 +39,7 @@ export function ChatSidebarSessionEntry(props: {
41
39
  optimisticReadAtBySessionKey,
42
40
  agentsById,
43
41
  childSessionsByParentKey,
42
+ cronJobCount,
44
43
  editingSessionKey,
45
44
  draftLabel,
46
45
  savingSessionKey,
@@ -48,7 +47,6 @@ export function ChatSidebarSessionEntry(props: {
48
47
  isPinned,
49
48
  sessionTitle,
50
49
  onSelectSession,
51
- onOpenChildSessions,
52
50
  onStartEditingSessionLabel,
53
51
  onDraftLabelChange,
54
52
  onSaveSessionLabel,
@@ -98,13 +96,16 @@ export function ChatSidebarSessionEntry(props: {
98
96
  : null
99
97
  }
100
98
  childSessionCount={childSessions.length}
99
+ cronJobCount={cronJobCount}
100
+ projectName={
101
+ session.projectName?.trim() ||
102
+ session.projectRoot?.trim().split(/[\\/]/).filter(Boolean).at(-1) ||
103
+ t("chatSidebarContextNoProject")
104
+ }
101
105
  isEditing={editingSessionKey === session.key}
102
106
  draftLabel={draftLabel}
103
107
  isSaving={savingSessionKey === session.key}
104
108
  onSelect={() => onSelectSession(session.key)}
105
- onOpenChildSessions={() =>
106
- onOpenChildSessions(session.key, childSessions[0]?.session.key ?? null)
107
- }
108
109
  onStartEditing={() => onStartEditingSessionLabel(session)}
109
110
  onDraftLabelChange={onDraftLabelChange}
110
111
  onSave={() => onSaveSessionLabel(session)}