@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,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
+ }
@@ -91,7 +91,11 @@ export type ContextCompactionTimelineView = {
91
91
 
92
92
  export function readContextCompactionTimeline(message: Pick<NcpMessageView, 'metadata'>): ContextCompactionTimelineView | null {
93
93
  const { metadata } = message;
94
- if (!metadata || metadata[NEXTCLAW_TIMELINE_KIND_METADATA_KEY] !== CONTEXT_COMPACTION_TIMELINE_KIND) {
94
+ if (
95
+ !metadata ||
96
+ readOptionalString(metadata.inherited_from_session_id) ||
97
+ metadata[NEXTCLAW_TIMELINE_KIND_METADATA_KEY] !== CONTEXT_COMPACTION_TIMELINE_KIND
98
+ ) {
95
99
  return null;
96
100
  }
97
101
  const rawCheckpoint =
@@ -1,4 +1,5 @@
1
1
  import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
3
  import type { ReactNode } from "react";
3
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
5
  import type {
@@ -123,16 +124,21 @@ function renderWorkspaceFilePreview({
123
124
  sessionWorkingDir = "/tmp",
124
125
  showBreadcrumbs,
125
126
  }: RenderWorkspaceFilePreviewOptions = {}) {
127
+ const queryClient = new QueryClient({
128
+ defaultOptions: { queries: { retry: false } },
129
+ });
126
130
  const view = render(
127
- <ChatSessionWorkspaceFilePreview
128
- file={buildWorkspaceFile(file ?? {})}
129
- sessionProjectRoot={sessionProjectRoot}
130
- sessionWorkingDir={sessionWorkingDir}
131
- refreshVersion={refreshVersion}
132
- showBreadcrumbs={showBreadcrumbs}
133
- onHtmlContentHeightChange={onHtmlContentHeightChange}
134
- onFileOpen={onFileOpen}
135
- />,
131
+ <QueryClientProvider client={queryClient}>
132
+ <ChatSessionWorkspaceFilePreview
133
+ file={buildWorkspaceFile(file ?? {})}
134
+ sessionProjectRoot={sessionProjectRoot}
135
+ sessionWorkingDir={sessionWorkingDir}
136
+ refreshVersion={refreshVersion}
137
+ showBreadcrumbs={showBreadcrumbs}
138
+ onHtmlContentHeightChange={onHtmlContentHeightChange}
139
+ onFileOpen={onFileOpen}
140
+ />
141
+ </QueryClientProvider>,
136
142
  );
137
143
  return { ...view, onFileOpen };
138
144
  }
@@ -580,6 +586,7 @@ describe("ChatSessionWorkspaceFilePreview text rendering", () => {
580
586
 
581
587
  expect(screen.queryByTestId("markdown-preview")).toBeNull();
582
588
  expect(screen.getByTestId("file-code-surface")).toBeTruthy();
589
+ expect(screen.queryByRole("button", { name: t("chatWorkspaceMarkdownOutline") })).toBeNull();
583
590
  });
584
591
 
585
592
  it("keeps HTML files in the source preview when preview viewer is automatic", () => {
@@ -222,7 +222,7 @@ function createChildTab(): ResolvedChildSessionTab {
222
222
  };
223
223
  }
224
224
 
225
- it('shows compact child session metadata in a full-height column layout', () => {
225
+ it('shows child session content without a redundant metadata header', () => {
226
226
  render(
227
227
  <ChatSessionWorkspacePanelContent
228
228
  activeSelection={{
@@ -239,10 +239,10 @@ it('shows compact child session metadata in a full-height column layout', () =>
239
239
  );
240
240
 
241
241
  expect(screen.queryByText('Child title')).toBeNull();
242
- expect(screen.getByText('原生')).toBeTruthy();
243
- expect(screen.getByText('minimax/MiniMax-M3')).toBeTruthy();
244
- expect(screen.getByText('nextbot')).toBeTruthy();
245
- expect(screen.getByTitle('/Users/peiwang/Projects/nextbot')).toBeTruthy();
242
+ expect(screen.queryByText('原生')).toBeNull();
243
+ expect(screen.queryByText('minimax/MiniMax-M3')).toBeNull();
244
+ expect(screen.queryByText('nextbot')).toBeNull();
245
+ expect(screen.queryByTitle('/Users/peiwang/Projects/nextbot')).toBeNull();
246
246
  const conversationArea = screen.getByTestId('session-conversation-area');
247
247
  const selectedContentHost = conversationArea.parentElement?.parentElement;
248
248
  expect(Array.from(selectedContentHost?.classList ?? [])).toEqual(
@@ -0,0 +1,128 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { createRef } from "react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { WorkspaceMarkdownOutline } from "@/features/chat/features/workspace/components/workspace-markdown-outline";
5
+ import { t } from "@/shared/lib/i18n";
6
+
7
+ describe("WorkspaceMarkdownOutline", () => {
8
+ it("does not render an outline action when the document has no headings", async () => {
9
+ const scrollContainerRef = createRef<HTMLDivElement>();
10
+ render(
11
+ <>
12
+ <WorkspaceMarkdownOutline
13
+ contentKey="plain fixture"
14
+ documentKey="notes.md"
15
+ scrollContainerRef={scrollContainerRef}
16
+ />
17
+ <div ref={scrollContainerRef}>Plain paragraph</div>
18
+ </>,
19
+ );
20
+
21
+ await waitFor(() => {
22
+ expect(
23
+ screen.queryByRole("button", {
24
+ name: t("chatWorkspaceMarkdownOutline"),
25
+ }),
26
+ ).toBeNull();
27
+ });
28
+ });
29
+
30
+ it("preserves outline scroll state and instantly jumps to the selected rendered heading", async () => {
31
+ const scrollContainerRef = createRef<HTMLDivElement>();
32
+ const scrollTo = vi.fn();
33
+ const { rerender } = render(
34
+ <>
35
+ <WorkspaceMarkdownOutline
36
+ contentKey="outline fixture"
37
+ documentKey="README.md"
38
+ scrollContainerRef={scrollContainerRef}
39
+ />
40
+ <div ref={scrollContainerRef}>
41
+ <h1>Overview</h1>
42
+ <h3>
43
+ Install <em>quickly</em>
44
+ </h3>
45
+ <h2>Overview</h2>
46
+ </div>
47
+ </>,
48
+ );
49
+
50
+ const outlineTrigger = await screen.findByRole("button", {
51
+ name: t("chatWorkspaceMarkdownOutline"),
52
+ });
53
+ const scrollContainer = scrollContainerRef.current!;
54
+ const headings = screen.getAllByRole("heading");
55
+ Object.defineProperty(scrollContainer, "scrollTop", {
56
+ configurable: true,
57
+ value: 40,
58
+ writable: true,
59
+ });
60
+ Object.defineProperty(scrollContainer, "scrollTo", {
61
+ configurable: true,
62
+ value: scrollTo,
63
+ });
64
+ vi.spyOn(scrollContainer, "getBoundingClientRect").mockReturnValue({
65
+ top: 100,
66
+ } as DOMRect);
67
+ vi.spyOn(headings[1]!, "getBoundingClientRect").mockReturnValue({
68
+ top: 420,
69
+ } as DOMRect);
70
+
71
+ fireEvent.click(outlineTrigger);
72
+
73
+ expect(
74
+ screen.getByTestId("workspace-markdown-outline-popover").className,
75
+ ).toContain("w-[min(20rem,calc(100vw-1.5rem))]");
76
+ const outlineNavigation = screen.getByRole("navigation");
77
+ expect(outlineNavigation.className).toContain("max-h-80");
78
+ expect(screen.getAllByRole("button", { name: "Overview" })).toHaveLength(2);
79
+ const nestedHeading = screen.getByRole("button", {
80
+ name: "Install quickly",
81
+ });
82
+ expect(nestedHeading.style.paddingInlineStart).toBe("1.25rem");
83
+
84
+ outlineNavigation.scrollTop = 176;
85
+ fireEvent.click(outlineTrigger);
86
+ await waitFor(() => {
87
+ expect(
88
+ screen.queryByTestId("workspace-markdown-outline-popover"),
89
+ ).toBeNull();
90
+ });
91
+ fireEvent.click(outlineTrigger);
92
+ expect((await screen.findByRole("navigation")).scrollTop).toBe(176);
93
+ fireEvent.click(outlineTrigger);
94
+ rerender(
95
+ <>
96
+ <WorkspaceMarkdownOutline
97
+ contentKey="outline fixture"
98
+ documentKey="docs/other.md"
99
+ scrollContainerRef={scrollContainerRef}
100
+ />
101
+ <div ref={scrollContainerRef}>
102
+ <h1>Overview</h1>
103
+ <h3>
104
+ Install <em>quickly</em>
105
+ </h3>
106
+ <h2>Overview</h2>
107
+ </div>
108
+ </>,
109
+ );
110
+ fireEvent.click(outlineTrigger);
111
+ expect((await screen.findByRole("navigation")).scrollTop).toBe(0);
112
+
113
+ fireEvent.click(screen.getByRole("button", { name: "Install quickly" }));
114
+
115
+ expect(scrollTo).toHaveBeenCalledWith({ top: 344 });
116
+ await waitFor(() => {
117
+ expect(
118
+ screen.queryByTestId("workspace-markdown-outline-popover"),
119
+ ).toBeNull();
120
+ });
121
+ expect(document.activeElement).toBe(outlineTrigger);
122
+ expect(
123
+ document.querySelector(
124
+ '[role="tooltip"][data-state="delayed-open"], [role="tooltip"][data-state="instant-open"]',
125
+ ),
126
+ ).toBeNull();
127
+ });
128
+ });