@nextclaw/ui 0.20.1 → 0.20.3

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 (138) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/assets/{api-BBFMpRGl.js → api-iMkU_o5P.js} +3 -3
  3. package/dist/assets/{appearance-settings-page-C1Erayzl.js → appearance-settings-page-CU5k4b-M.js} +1 -1
  4. package/dist/assets/{book-open-BhTIpBKg.js → book-open-nD8gdMhA.js} +1 -1
  5. package/dist/assets/channels-list-page-Drlt62kY.js +8 -0
  6. package/dist/assets/{config-split-page-By7p-4X-.js → config-split-page-BtJZp1E5.js} +1 -1
  7. package/dist/assets/{confirm-dialog-xpl1Yl7r.js → confirm-dialog-k2-Jq_x3.js} +1 -1
  8. package/dist/assets/{createLucideIcon-B_8khHn8.js → createLucideIcon-DxlwKLr-.js} +1 -1
  9. package/dist/assets/{desktop-update-config-CGFfa7-Z.js → desktop-update-config-BiZmy7FC.js} +1 -1
  10. package/dist/assets/{dist-CXN62DSs.js → dist-Cy_e4kNb.js} +1 -1
  11. package/dist/assets/{dist-DdE-kCbi.js → dist-m08DUswB.js} +1 -1
  12. package/dist/assets/doc-browser-CPsPHh12.js +1 -0
  13. package/dist/assets/{doc-browser-context-B8NCuMv5.js → doc-browser-context-jiy-O4F8.js} +1 -1
  14. package/dist/assets/doc-browser-npOvWtgf.js +1 -0
  15. package/dist/assets/{extensions-CMctxyTf.js → extensions-CIb17__o.js} +1 -1
  16. package/dist/assets/{form-actions-BFgTdxqI.js → form-actions-DGf5zUg_.js} +1 -1
  17. package/dist/assets/index-C86hxgv5.css +1 -0
  18. package/dist/assets/index-CYBXR7zN.js +109 -0
  19. package/dist/assets/loader-circle-BlLuRFVW.js +1 -0
  20. package/dist/assets/mcp-marketplace-page-B12JNh7Q.js +1 -0
  21. package/dist/assets/mcp-marketplace-page-kLWW6kzt.js +9 -0
  22. package/dist/assets/model-config-page-DSeAjVT_.js +1 -0
  23. package/dist/assets/{navigation-link-BwWrm4k1.js → navigation-link-DAfwFCj4.js} +1 -1
  24. package/dist/assets/plus-D4LtWtTk.js +1 -0
  25. package/dist/assets/privacy-settings-page-tN0Mt15x.js +1 -0
  26. package/dist/assets/providers-config-page-CgPrXR1u.js +1 -0
  27. package/dist/assets/react-8iBFECZE.js +8 -0
  28. package/dist/assets/{refresh-cw-B7NmPJ_Z.js → refresh-cw-D_Eibznw.js} +1 -1
  29. package/dist/assets/remote-Derr_AQp.js +1 -0
  30. package/dist/assets/{rotate-cw-B0ajMaHu.js → rotate-cw-QQjmoWuq.js} +1 -1
  31. package/dist/assets/runtime-config-page-Crq96Jjq.js +1 -0
  32. package/dist/assets/{save-DWnYYpgQ.js → save-W2xthJJ2.js} +1 -1
  33. package/dist/assets/{search-D6pX7FC-.js → search-Vih1R2BD.js} +1 -1
  34. package/dist/assets/{search-config-page-Cx3o_675.js → search-config-page-CtBcL2uc.js} +1 -1
  35. package/dist/assets/{secrets-config-page-tyFl7Rvt.js → secrets-config-page-Q6XfIj0h.js} +2 -2
  36. package/dist/assets/security-config-CE9rkQvq.js +1 -0
  37. package/dist/assets/{select-qEtFTWB5.js → select-lNdozU4o.js} +1 -1
  38. package/dist/assets/skeleton-B__aTcly.js +1 -0
  39. package/dist/assets/{tag-chip-vcjAYdV_.js → tag-chip-gdaU9bE7.js} +1 -1
  40. package/dist/assets/x-CWdWBu_U.js +1 -0
  41. package/dist/index.html +21 -21
  42. package/package.json +7 -7
  43. package/src/app/components/layout/sidebar.tsx +7 -0
  44. package/src/features/apps/components/app-package-card.tsx +29 -7
  45. package/src/features/apps/components/app-packages-panel.tsx +4 -0
  46. package/src/features/chat/components/conversation/chat-conversation-content.tsx +18 -2
  47. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +6 -0
  48. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +4 -3
  49. package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +27 -6
  50. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +17 -17
  51. package/src/features/chat/components/layout/chat-page-shell.tsx +19 -2
  52. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +33 -5
  53. package/src/features/chat/components/layout/chat-sidebar.tsx +31 -21
  54. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +41 -1
  55. package/src/features/chat/features/conversation/hooks/use-session-pending-input-actions.ts +22 -4
  56. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline-compaction.utils.test.ts +2 -2
  57. package/src/features/chat/features/message/utils/__tests__/chat-message-trigger-details.utils.test.ts +35 -0
  58. package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +2 -2
  59. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +50 -0
  60. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +35 -6
  61. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +43 -20
  62. package/src/features/chat/features/ncp/hooks/use-ncp-session-queries.ts +15 -1
  63. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +26 -2
  64. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +55 -27
  65. package/src/features/chat/features/session/components/chat-sidebar-context-card.tsx +60 -0
  66. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +95 -54
  67. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +9 -8
  68. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +141 -95
  69. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +11 -2
  70. package/src/features/chat/features/session/hooks/use-chat-sidebar-context-counts.ts +40 -0
  71. package/src/features/chat/features/workspace/components/__tests__/chat-session-token-usage.test.tsx +4 -1
  72. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.scroll-restoration.test.tsx +182 -0
  73. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +16 -9
  74. package/src/features/chat/features/workspace/components/__tests__/session-cron-job-content.test.tsx +78 -0
  75. package/src/features/chat/features/workspace/components/__tests__/workspace-markdown-outline.test.tsx +128 -0
  76. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -0
  77. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +65 -2
  78. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +14 -1
  79. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +9 -0
  80. package/src/features/chat/features/workspace/components/overview/chat-session-token-usage.tsx +17 -0
  81. package/src/features/chat/features/workspace/components/session-cron-job-content.tsx +269 -75
  82. package/src/features/chat/features/workspace/components/workspace-markdown-outline.tsx +156 -0
  83. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +5 -0
  84. package/src/features/cron/components/__tests__/cron-config.test.tsx +16 -5
  85. package/src/features/cron/components/cron-config.tsx +20 -46
  86. package/src/features/cron/components/cron-job-row.tsx +1 -1
  87. package/src/features/cron/hooks/__tests__/use-cron-job-actions.test.tsx +95 -0
  88. package/src/features/cron/hooks/use-cron-job-actions.ts +109 -0
  89. package/src/features/cron/index.ts +2 -0
  90. package/src/features/inbox/pages/inbox-page.tsx +18 -2
  91. package/src/features/marketplace/components/marketplace-page.tsx +7 -0
  92. package/src/features/panel-apps/pages/panel-app-main-page.tsx +8 -0
  93. package/src/features/settings/pages/__tests__/privacy-settings-page.test.tsx +14 -5
  94. package/src/features/settings/pages/privacy-settings-page.tsx +38 -2
  95. package/src/platforms/desktop/components/desktop-app-shell.tsx +10 -1
  96. package/src/shared/components/doc-browser/doc-browser.tsx +0 -1
  97. package/src/shared/components/doc-browser/hooks/use-doc-browser-scroll-restoration.ts +9 -113
  98. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -8
  99. package/src/shared/components/ui/tooltip.tsx +16 -1
  100. package/src/shared/hooks/use-config.ts +11 -0
  101. package/src/shared/hooks/use-panel-app-scroll-restoration.test.tsx +66 -0
  102. package/src/shared/hooks/use-panel-app-scroll-restoration.ts +107 -0
  103. package/src/shared/hooks/use-scroll-restoration.test.tsx +62 -0
  104. package/src/shared/hooks/use-scroll-restoration.ts +61 -0
  105. package/src/shared/lib/api/ncp-session-query-cache.utils.ts +1 -0
  106. package/src/shared/lib/api/ncp-session.types.ts +3 -0
  107. package/src/shared/lib/api/types.ts +9 -0
  108. package/src/shared/lib/api/utils/config.utils.ts +5 -0
  109. package/src/shared/lib/api/utils/ncp-session.utils.ts +3 -0
  110. package/src/shared/lib/i18n/locales/en-US/chat.json +15 -5
  111. package/src/shared/lib/i18n/locales/en-US/core.json +17 -9
  112. package/src/shared/lib/i18n/locales/en-US/cron.json +4 -2
  113. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +1 -0
  114. package/src/shared/lib/i18n/locales/zh-CN/chat.json +15 -5
  115. package/src/shared/lib/i18n/locales/zh-CN/core.json +17 -9
  116. package/src/shared/lib/i18n/locales/zh-CN/cron.json +4 -2
  117. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +1 -0
  118. package/src/shared/lib/navigation-history/index.ts +1 -0
  119. package/src/shared/lib/navigation-history/scroll-restoration.manager.test.ts +37 -0
  120. package/src/shared/lib/navigation-history/scroll-restoration.manager.ts +60 -0
  121. package/dist/assets/channels-list-page-OTCkjMOW.js +0 -8
  122. package/dist/assets/doc-browser-5xrHcQ2z.js +0 -1
  123. package/dist/assets/doc-browser-c2yO477-.js +0 -1
  124. package/dist/assets/index-DUCA8m2w.js +0 -109
  125. package/dist/assets/index-QrdUWHfm.css +0 -1
  126. package/dist/assets/loader-circle-CbdTrSp3.js +0 -1
  127. package/dist/assets/mcp-marketplace-page-DGve0muV.js +0 -9
  128. package/dist/assets/mcp-marketplace-page-_37VzG2q.js +0 -1
  129. package/dist/assets/model-config-page-DyC5H0bu.js +0 -1
  130. package/dist/assets/plus-DbeE4r-Q.js +0 -1
  131. package/dist/assets/privacy-settings-page--MOnhAZ6.js +0 -1
  132. package/dist/assets/providers-config-page-ChL3FUvK.js +0 -1
  133. package/dist/assets/react-DVMmnPWz.js +0 -8
  134. package/dist/assets/remote-DFoxj4ls.js +0 -1
  135. package/dist/assets/runtime-config-page-BQdT3nmd.js +0 -1
  136. package/dist/assets/security-config-CUTUm6JT.js +0 -1
  137. package/dist/assets/skeleton-7MYPzn8w.js +0 -1
  138. package/dist/assets/x-DThaS5ZL.js +0 -1
@@ -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)}
@@ -1,14 +1,23 @@
1
- import { AgentAvatar } from '@/shared/components/common/agent-avatar';
2
- import { SessionContextIconNode } from '@/features/chat/features/session/components/session-context-icon';
3
- import { SessionRunBadge } from '@/features/chat/features/session/components/session-run-badge';
4
- import { ChatSessionMoreActionsMenu } from '@/features/chat/features/session/components/session-header/chat-session-more-actions-menu';
5
- import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
6
- import { Input } from '@/shared/components/ui/input';
7
- import { type SessionContextView } from '@/features/chat/features/session/utils/session-context.utils';
8
- import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
9
- import { cn } from '@/shared/lib/utils';
10
- import { t } from '@/shared/lib/i18n';
11
- import { Check, GitBranch, Pencil, Pin, X } from 'lucide-react';
1
+ import { AgentAvatar } from "@/shared/components/common/agent-avatar";
2
+ import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
3
+ import { SessionRunBadge } from "@/features/chat/features/session/components/session-run-badge";
4
+ import { ChatSessionMoreActionsMenu } from "@/features/chat/features/session/components/session-header/chat-session-more-actions-menu";
5
+ import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
6
+ import { Input } from "@/shared/components/ui/input";
7
+ import { type SessionContextView } from "@/features/chat/features/session/utils/session-context.utils";
8
+ import type { SessionRunStatus } from "@/features/chat/types/session-run-status.types";
9
+ import { cn } from "@/shared/lib/utils";
10
+ import { t } from "@/shared/lib/i18n";
11
+ import {
12
+ AlarmClock,
13
+ Check,
14
+ Folder,
15
+ GitBranch,
16
+ Pencil,
17
+ Pin,
18
+ X,
19
+ } from "lucide-react";
20
+ import { ChatSidebarContextCard } from "@/features/chat/features/session/components/chat-sidebar-context-card";
12
21
 
13
22
  type ChatSidebarSessionItemProps = {
14
23
  sessionKey: string;
@@ -24,11 +33,12 @@ type ChatSidebarSessionItemProps = {
24
33
  agentLabel?: string | null;
25
34
  agentAvatarUrl?: string | null;
26
35
  childSessionCount?: number;
36
+ cronJobCount?: number;
37
+ projectName?: string | null;
27
38
  isEditing: boolean;
28
39
  draftLabel: string;
29
40
  isSaving: boolean;
30
41
  onSelect: () => void;
31
- onOpenChildSessions?: () => void;
32
42
  onStartEditing: () => void;
33
43
  onDraftLabelChange: (value: string) => void;
34
44
  onSave: () => void | Promise<void>;
@@ -38,7 +48,12 @@ type ChatSidebarSessionItemProps = {
38
48
 
39
49
  type ChatSidebarSessionEditingViewProps = Pick<
40
50
  ChatSidebarSessionItemProps,
41
- 'sessionKey' | 'draftLabel' | 'isSaving' | 'onDraftLabelChange' | 'onSave' | 'onCancel'
51
+ | "sessionKey"
52
+ | "draftLabel"
53
+ | "isSaving"
54
+ | "onDraftLabelChange"
55
+ | "onSave"
56
+ | "onCancel"
42
57
  >;
43
58
 
44
59
  function ChatSidebarSessionEditingView({
@@ -47,7 +62,7 @@ function ChatSidebarSessionEditingView({
47
62
  isSaving,
48
63
  onDraftLabelChange,
49
64
  onSave,
50
- onCancel
65
+ onCancel,
51
66
  }: ChatSidebarSessionEditingViewProps) {
52
67
  return (
53
68
  <div className="space-y-2">
@@ -55,32 +70,34 @@ function ChatSidebarSessionEditingView({
55
70
  value={draftLabel}
56
71
  onChange={(event) => onDraftLabelChange(event.target.value)}
57
72
  onKeyDown={(event) => {
58
- if (event.key === 'Enter') {
73
+ if (event.key === "Enter") {
59
74
  event.preventDefault();
60
75
  void onSave();
61
- } else if (event.key === 'Escape') {
76
+ } else if (event.key === "Escape") {
62
77
  event.preventDefault();
63
78
  onCancel();
64
79
  }
65
80
  }}
66
- placeholder={t('sessionsLabelPlaceholder')}
81
+ placeholder={t("sessionsLabelPlaceholder")}
67
82
  className="h-8 rounded-lg border-border bg-background text-xs"
68
83
  autoFocus
69
84
  disabled={isSaving}
70
85
  />
71
86
  <div className="flex items-center justify-between gap-2">
72
- <div className="min-w-0 truncate text-[11px] text-muted-foreground/65">{sessionKey}</div>
87
+ <div className="min-w-0 truncate text-[11px] text-muted-foreground/65">
88
+ {sessionKey}
89
+ </div>
73
90
  <div className="flex items-center gap-1">
74
91
  <IconActionButton
75
92
  icon={<Check className="h-3.5 w-3.5" />}
76
- label={t('save')}
93
+ label={t("save")}
77
94
  tooltip={false}
78
95
  onClick={() => void onSave()}
79
96
  disabled={isSaving}
80
97
  />
81
98
  <IconActionButton
82
99
  icon={<X className="h-3.5 w-3.5" />}
83
- label={t('cancel')}
100
+ label={t("cancel")}
84
101
  tooltip={false}
85
102
  onClick={onCancel}
86
103
  disabled={isSaving}
@@ -93,8 +110,15 @@ function ChatSidebarSessionEditingView({
93
110
 
94
111
  type ChatSidebarSessionDisplayViewProps = Omit<
95
112
  ChatSidebarSessionItemProps,
96
- 'sessionKey' | 'isEditing' | 'draftLabel' | 'isSaving' | 'onDraftLabelChange' | 'onSave' | 'onCancel'
97
- > & Pick<ChatSidebarSessionItemProps, 'sessionKey'>;
113
+ | "sessionKey"
114
+ | "isEditing"
115
+ | "draftLabel"
116
+ | "isSaving"
117
+ | "onDraftLabelChange"
118
+ | "onSave"
119
+ | "onCancel"
120
+ > &
121
+ Pick<ChatSidebarSessionItemProps, "sessionKey">;
98
122
 
99
123
  function ChatSidebarSessionDisplayView({
100
124
  sessionKey,
@@ -110,93 +134,113 @@ function ChatSidebarSessionDisplayView({
110
134
  agentLabel,
111
135
  agentAvatarUrl,
112
136
  childSessionCount = 0,
137
+ cronJobCount = 0,
138
+ projectName,
113
139
  onSelect,
114
- onOpenChildSessions,
115
140
  onStartEditing,
116
- onTogglePinned
141
+ onTogglePinned,
117
142
  }: ChatSidebarSessionDisplayViewProps) {
118
- const trailingControlsClassName = childSessionCount > 0 && onOpenChildSessions ? 'pr-32' : 'pr-20';
119
-
120
143
  return (
121
144
  <div className="group/session relative">
122
- <button type="button" onClick={onSelect} className="w-full text-left">
123
- <div className={cn('flex min-h-6 min-w-0 items-center', trailingControlsClassName)}>
124
- <span className="flex min-w-0 items-center gap-1.5">
125
- {agentId?.trim() && agentId.trim().toLowerCase() !== 'main' ? (
126
- <AgentAvatar
127
- agentId={agentId}
128
- displayName={agentLabel}
129
- avatarUrl={agentAvatarUrl}
130
- className="h-5 w-5 shrink-0"
131
- />
132
- ) : null}
133
- <span className="truncate leading-6 font-medium">{title}</span>
134
- {context.label ? (
145
+ <ChatSidebarContextCard
146
+ title={title}
147
+ metrics={[
148
+ {
149
+ icon: <Folder className="h-3.5 w-3.5" />,
150
+ label: t("chatSidebarContextProject"),
151
+ value: projectName,
152
+ },
153
+ {
154
+ icon: <GitBranch className="h-3.5 w-3.5" />,
155
+ label: t("chatSidebarContextChildSessions"),
156
+ value: childSessionCount,
157
+ },
158
+ {
159
+ icon: <AlarmClock className="h-3.5 w-3.5" />,
160
+ label: t("chatSidebarContextScheduledTasks"),
161
+ value: cronJobCount,
162
+ },
163
+ ]}
164
+ >
165
+ <button type="button" onClick={onSelect} className="w-full text-left">
166
+ <div
167
+ className={cn(
168
+ "flex min-h-6 min-w-0 items-center",
169
+ runStatus && "pr-6",
170
+ "group-hover/session:pr-20 group-has-[[data-session-actions]:focus-within]/session:pr-20",
171
+ )}
172
+ >
173
+ <span className="flex min-w-0 items-center gap-1.5">
174
+ {agentId?.trim() && agentId.trim().toLowerCase() !== "main" ? (
175
+ <AgentAvatar
176
+ agentId={agentId}
177
+ displayName={agentLabel}
178
+ avatarUrl={agentAvatarUrl}
179
+ className="h-5 w-5 shrink-0"
180
+ />
181
+ ) : null}
182
+ <span className="truncate leading-6 font-medium">{title}</span>
183
+ {context.label ? (
184
+ <span
185
+ className={cn(
186
+ "shrink-0 rounded-full border px-1.5 py-0.5 text-[10px] font-semibold leading-none",
187
+ active
188
+ ? "border-border/70 bg-background/80 text-foreground/75"
189
+ : "border-transparent bg-muted/70 text-muted-foreground",
190
+ )}
191
+ >
192
+ {context.label}
193
+ </span>
194
+ ) : null}
195
+ {context.icon ? (
196
+ <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center">
197
+ <SessionContextIconNode
198
+ icon={context.icon}
199
+ className={cn(
200
+ "h-[13px] w-[13px]",
201
+ active ? "text-foreground/75" : "text-muted-foreground",
202
+ )}
203
+ />
204
+ </span>
205
+ ) : null}
206
+ </span>
207
+ </div>
208
+ <div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground/65">
209
+ <span className="min-w-0 truncate">{previewText}</span>
210
+ {showUnreadDot ? (
135
211
  <span
136
- className={cn(
137
- 'shrink-0 rounded-full border px-1.5 py-0.5 text-[10px] font-semibold leading-none',
138
- active
139
- ? 'border-border/70 bg-background/80 text-foreground/75'
140
- : 'border-transparent bg-muted/70 text-muted-foreground'
141
- )}
142
- >
143
- {context.label}
144
- </span>
145
- ) : null}
146
- {context.icon ? (
147
- <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center">
148
- <SessionContextIconNode icon={context.icon} className={cn('h-[13px] w-[13px]', active ? 'text-foreground/75' : 'text-muted-foreground')} />
149
- </span>
150
- ) : null}
151
- </span>
152
- </div>
153
- <div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground/65">
154
- <span className="min-w-0 truncate">
155
- {previewText}
156
- </span>
157
- {showUnreadDot ? (
158
- <span
159
- aria-label={t('chatSessionUnread')}
160
- className="ml-auto h-2 w-2 shrink-0 rounded-full bg-primary"
161
- />
162
- ) : (
163
- <span className="ml-auto shrink-0">{trailingText}</span>
164
- )}
165
- </div>
166
- </button>
167
- {childSessionCount > 0 && onOpenChildSessions ? (
168
- <button
169
- type="button"
170
- onClick={(event) => {
171
- event.stopPropagation();
172
- onOpenChildSessions();
173
- }}
174
- className="pointer-events-none 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 opacity-0 transition-colors hover:bg-background/80 hover:text-foreground group-hover/session:pointer-events-auto group-hover/session:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100"
175
- aria-label={t('chatSessionOpenChildSessions')}
176
- title={t('chatSessionOpenChildSessions')}
177
- >
178
- <GitBranch className="h-3.5 w-3.5" />
179
- <span>{childSessionCount}</span>
212
+ aria-label={t("chatSessionUnread")}
213
+ className="ml-auto h-2 w-2 shrink-0 rounded-full bg-primary"
214
+ />
215
+ ) : (
216
+ <span className="ml-auto shrink-0">{trailingText}</span>
217
+ )}
218
+ </div>
180
219
  </button>
181
- ) : null}
220
+ </ChatSidebarContextCard>
182
221
  {runStatus ? (
183
222
  <span className="absolute right-0 top-0 inline-flex h-6 w-6 items-center justify-center transition-opacity group-hover/session:opacity-0">
184
223
  <SessionRunBadge status={runStatus} />
185
224
  </span>
186
225
  ) : null}
187
- <div className="pointer-events-none absolute right-0 top-0 flex h-6 items-center gap-0.5 opacity-0 transition-opacity group-hover/session:pointer-events-auto group-hover/session:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100">
226
+ <div
227
+ data-session-actions
228
+ className="pointer-events-none absolute right-0 top-0 flex h-6 items-center gap-0.5 opacity-0 transition-opacity group-hover/session:pointer-events-auto group-hover/session:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100"
229
+ >
188
230
  <IconActionButton
189
231
  size="sm"
190
232
  tone="strong"
191
233
  icon={
192
234
  <Pin
193
235
  className={cn(
194
- 'h-3.5 w-3.5',
195
- isPinned && 'fill-current text-foreground',
236
+ "h-3.5 w-3.5",
237
+ isPinned && "fill-current text-foreground",
196
238
  )}
197
239
  />
198
240
  }
199
- label={t(isPinned ? 'chatSidebarUnpinSession' : 'chatSidebarPinSession')}
241
+ label={t(
242
+ isPinned ? "chatSidebarUnpinSession" : "chatSidebarPinSession",
243
+ )}
200
244
  tooltipSide="right"
201
245
  onClick={(event) => {
202
246
  event.stopPropagation();
@@ -207,7 +251,7 @@ function ChatSidebarSessionDisplayView({
207
251
  size="sm"
208
252
  tone="strong"
209
253
  icon={<Pencil className="h-3.5 w-3.5" />}
210
- label={t('edit')}
254
+ label={t("edit")}
211
255
  tooltipSide="right"
212
256
  onClick={(event) => {
213
257
  event.stopPropagation();
@@ -238,24 +282,25 @@ export function ChatSidebarSessionItem({
238
282
  agentLabel,
239
283
  agentAvatarUrl,
240
284
  childSessionCount,
285
+ cronJobCount,
286
+ projectName,
241
287
  isEditing,
242
288
  draftLabel,
243
289
  isSaving,
244
290
  onSelect,
245
- onOpenChildSessions,
246
291
  onStartEditing,
247
292
  onDraftLabelChange,
248
293
  onSave,
249
294
  onCancel,
250
- onTogglePinned
295
+ onTogglePinned,
251
296
  }: ChatSidebarSessionItemProps) {
252
297
  return (
253
298
  <div
254
299
  className={cn(
255
- 'w-full rounded-lg px-2.5 py-2 text-left text-[13px] transition-colors',
300
+ "w-full rounded-lg px-2.5 py-2 text-left text-[13px] transition-colors",
256
301
  active
257
- ? 'bg-background/90 font-medium text-foreground'
258
- : 'text-foreground/80 hover:bg-background/65 hover:text-foreground'
302
+ ? "bg-background/90 font-medium text-foreground"
303
+ : "text-foreground/80 hover:bg-background/65 hover:text-foreground",
259
304
  )}
260
305
  >
261
306
  {isEditing ? (
@@ -283,7 +328,8 @@ export function ChatSidebarSessionItem({
283
328
  agentAvatarUrl={agentAvatarUrl}
284
329
  onSelect={onSelect}
285
330
  childSessionCount={childSessionCount}
286
- onOpenChildSessions={onOpenChildSessions}
331
+ cronJobCount={cronJobCount}
332
+ projectName={projectName}
287
333
  onStartEditing={onStartEditing}
288
334
  onTogglePinned={onTogglePinned}
289
335
  />
@@ -16,6 +16,7 @@ type ChatSidebarSessionListProps = {
16
16
  isProjectFirstView: boolean;
17
17
  groups: ChatSidebarDateGroup[];
18
18
  projectGroups: ChatSidebarProjectGroup[];
19
+ projectCronJobCountByRoot: ReadonlyMap<string, number>;
19
20
  defaultSessionType: string;
20
21
  sessionTypeOptions: SessionTypeOption[];
21
22
  renderSessionItem: (item: NcpSessionListItemView) => ReactNode;
@@ -36,11 +37,16 @@ export function ChatSidebarSessionList({
36
37
  isLoading,
37
38
  isProjectFirstView,
38
39
  projectGroups,
40
+ projectCronJobCountByRoot,
39
41
  renderSessionItem,
40
42
  sessionTypeOptions,
41
43
  }: ChatSidebarSessionListProps) {
42
44
  if (isLoading) {
43
- return <div className="p-3 text-xs text-muted-foreground">{t("sessionsLoading")}</div>;
45
+ return (
46
+ <div className="p-3 text-xs text-muted-foreground">
47
+ {t("sessionsLoading")}
48
+ </div>
49
+ );
44
50
  }
45
51
 
46
52
  if (isProjectFirstView) {
@@ -49,6 +55,7 @@ export function ChatSidebarSessionList({
49
55
  ) : (
50
56
  <ChatSidebarProjectGroups
51
57
  groups={projectGroups}
58
+ projectCronJobCountByRoot={projectCronJobCountByRoot}
52
59
  defaultSessionType={defaultSessionType}
53
60
  sessionTypeOptions={sessionTypeOptions}
54
61
  renderSessionItem={renderSessionItem}
@@ -67,7 +74,9 @@ export function ChatSidebarSessionList({
67
74
  <div className="px-2 pb-1 pt-0.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/65">
68
75
  {group.label}
69
76
  </div>
70
- <div className="space-y-0.5">{group.items.map(renderSessionItem)}</div>
77
+ <div className="space-y-0.5">
78
+ {group.items.map(renderSessionItem)}
79
+ </div>
71
80
  </div>
72
81
  ))}
73
82
  </div>
@@ -0,0 +1,40 @@
1
+ import { useMemo } from "react";
2
+ import { useCronJobs } from "@/features/cron";
3
+ import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
4
+
5
+ export function useChatSidebarContextCounts(
6
+ allItems: readonly NcpSessionListItemView[],
7
+ ) {
8
+ const cronQuery = useCronJobs({ all: true });
9
+
10
+ return useMemo(() => {
11
+ const projectRootBySessionKey = new Map(
12
+ allItems.flatMap(({ session }) => {
13
+ const projectRoot = session.projectRoot?.trim();
14
+ return projectRoot ? [[session.key, projectRoot] as const] : [];
15
+ }),
16
+ );
17
+ const cronJobCountBySessionKey = new Map<string, number>();
18
+ const cronJobCountByProjectRoot = new Map<string, number>();
19
+
20
+ for (const job of cronQuery.data?.jobs ?? []) {
21
+ const sessionKey = job.payload.sessionId?.trim();
22
+ if (!sessionKey) {
23
+ continue;
24
+ }
25
+ cronJobCountBySessionKey.set(
26
+ sessionKey,
27
+ (cronJobCountBySessionKey.get(sessionKey) ?? 0) + 1,
28
+ );
29
+ const projectRoot = projectRootBySessionKey.get(sessionKey);
30
+ if (projectRoot) {
31
+ cronJobCountByProjectRoot.set(
32
+ projectRoot,
33
+ (cronJobCountByProjectRoot.get(projectRoot) ?? 0) + 1,
34
+ );
35
+ }
36
+ }
37
+
38
+ return { cronJobCountByProjectRoot, cronJobCountBySessionKey };
39
+ }, [allItems, cronQuery.data?.jobs]);
40
+ }
@@ -66,12 +66,15 @@ beforeEach(() => {
66
66
  mocks.query.isError = false;
67
67
  });
68
68
 
69
- it('shows totals, cache hit rate, and usage grouped by model', () => {
69
+ it('shows totals, model calls, cache read ratio, and usage grouped by model', () => {
70
70
  render(<ChatSessionTokenUsage sessionKey="parent-1" />);
71
71
 
72
72
  expect(screen.getByText('Token usage')).toBeTruthy();
73
73
  expect(screen.getByText('1,490')).toBeTruthy();
74
74
  expect(screen.getAllByText('40%').length).toBeGreaterThan(0);
75
+ expect(screen.getByText('Model calls')).toBeTruthy();
76
+ expect(screen.getByText('Reported calls')).toBeTruthy();
77
+ expect(screen.getByText('2 agent rounds · 3 model calls · Cache read ratio: 40%')).toBeTruthy();
75
78
  expect(screen.getByText('openai/gpt-5')).toBeTruthy();
76
79
  expect(screen.getByText('anthropic/claude-sonnet-4')).toBeTruthy();
77
80
  expect(screen.getAllByText('Partial usage')).toHaveLength(2);