@nextclaw/ui 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/assets/{api-DLtsiBKm.js → api-B7Qigvtm.js} +2 -2
  3. package/dist/assets/{appearance-settings-page-CtkeYXux.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
  4. package/dist/assets/{book-open-BQwWBDKb.js → book-open-Ba2f7YiG.js} +1 -1
  5. package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
  6. package/dist/assets/chat-page-C_5dcuE3.js +1 -0
  7. package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
  8. package/dist/assets/{confirm-dialog-iIJFFp2T.js → confirm-dialog-D2EG3nqE.js} +1 -1
  9. package/dist/assets/desktop-update-config-BGR3HUi1.js +1 -0
  10. package/dist/assets/{dist-D2eZC3OR.js → dist-7JTjycxE.js} +1 -1
  11. package/dist/assets/{dist-CBxWOazz.js → dist-wXLhk4hl.js} +1 -1
  12. package/dist/assets/doc-browser-bHyXnxlj.js +1 -0
  13. package/dist/assets/doc-browser-context-CYAGm5d5.js +1 -0
  14. package/dist/assets/doc-browser-gTrQ3jCF.js +1 -0
  15. package/dist/assets/{es2015-DhQo_oS6.js → es2015-j1SRCIX5.js} +1 -1
  16. package/dist/assets/{external-link-CM5dnUI6.js → external-link-DxuPaDoK.js} +1 -1
  17. package/dist/assets/form-actions-BztL7P_T.js +1 -0
  18. package/dist/assets/{i18n-provider-di1waTGX.js → i18n-provider-B4oMxOFx.js} +1 -1
  19. package/dist/assets/index-CvrvblgX.css +1 -0
  20. package/dist/assets/index-Obd4JYU2.js +104 -0
  21. package/dist/assets/loader-circle-CdzgK4eH.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-C_MVQUbT.js → mcp-marketplace-page-Bt7CuOoq.js} +2 -2
  23. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +1 -0
  24. package/dist/assets/model-config-page-DeKk9gvE.js +1 -0
  25. package/dist/assets/plus-Bx7wQbhG.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-g1WkGiJ8.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
  27. package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
  28. package/dist/assets/{react-DjpzoZXB.js → react-UirzUuST.js} +1 -1
  29. package/dist/assets/{refresh-cw-DaTv3zxE.js → refresh-cw-eT8BF-8B.js} +1 -1
  30. package/dist/assets/remote-CDkHZtA8.js +1 -0
  31. package/dist/assets/rotate-cw-1JqxHTuw.js +1 -0
  32. package/dist/assets/runtime-config-page-BelAqiNG.js +1 -0
  33. package/dist/assets/{save-BZof1uie.js → save-Cb2g0BOi.js} +1 -1
  34. package/dist/assets/{search-MxAarS_g.js → search-BcHjwdsb.js} +1 -1
  35. package/dist/assets/search-config-page-zPm6--Ig.js +1 -0
  36. package/dist/assets/secrets-config-page-Cxyp-KuQ.js +3 -0
  37. package/dist/assets/security-config-B_gCq9fn.js +1 -0
  38. package/dist/assets/{select-D7GcNP0z.js → select-CdSEOwVM.js} +1 -1
  39. package/dist/assets/{setting-row-CuozlnJd.js → setting-row-CTAtGexr.js} +1 -1
  40. package/dist/assets/settings-2-2DWauOjY.js +1 -0
  41. package/dist/assets/{side-dock-BcLau5S_.js → side-dock-DPvakJ0a.js} +2 -2
  42. package/dist/assets/{skeleton-shfmXuYp.js → skeleton-P5oM14E1.js} +1 -1
  43. package/dist/assets/{switch-C1BWMU5c.js → switch-BU_UEULb.js} +1 -1
  44. package/dist/assets/{tag-chip-DCUrQUCP.js → tag-chip-D-WsKoT1.js} +1 -1
  45. package/dist/assets/{tooltip-DfAHHA76.js → tooltip-CjkOaHW7.js} +1 -1
  46. package/dist/assets/{use-config-DAdbT5QJ.js → use-config--hx5oxQn.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-eWcJ6V-s.js → use-confirm-dialog-CD9a_SK1.js} +1 -1
  48. package/dist/assets/x-DT5x45JU.js +1 -0
  49. package/dist/index.html +38 -30
  50. package/index.html +12 -4
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
  53. package/src/app/components/layout/sidebar-items.tsx +43 -4
  54. package/src/app/components/layout/sidebar.tsx +83 -61
  55. package/src/app/configs/app-navigation.config.ts +49 -0
  56. package/src/app/styles/design-system.css +82 -0
  57. package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
  58. package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
  59. package/src/features/agents/components/agent-details-dialog.tsx +398 -0
  60. package/src/features/agents/components/agent-dialogs.tsx +20 -47
  61. package/src/features/agents/components/agents-page.tsx +76 -10
  62. package/src/features/agents/types/agent-form.types.ts +21 -0
  63. package/src/features/agents/utils/agent-form.utils.ts +39 -0
  64. package/src/features/channels/components/config/channel-form.tsx +14 -11
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +172 -1
  66. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +11 -1
  67. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
  68. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  69. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  70. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  71. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  72. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  73. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +29 -30
  74. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  75. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +18 -3
  76. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  77. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  78. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +1 -1
  79. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  80. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  81. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +23 -1
  82. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
  83. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
  84. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  85. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  86. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  87. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  88. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  89. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  90. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
  91. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
  92. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
  93. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
  94. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  95. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
  96. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  97. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +193 -56
  99. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  100. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  102. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -1
  103. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +67 -14
  104. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  107. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
  108. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  109. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +47 -0
  110. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  111. package/src/features/chat/managers/chat-thread.manager.ts +8 -2
  112. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  113. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  114. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  115. package/src/features/panel-apps/index.ts +1 -0
  116. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  117. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  118. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  119. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  120. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  121. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  122. package/src/features/settings/components/config/provider-form.tsx +11 -8
  123. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  124. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  125. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  126. package/src/features/settings/pages/model-config-page.tsx +26 -25
  127. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  128. package/src/features/settings/pages/search-config-page.tsx +8 -5
  129. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
  130. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  131. package/src/features/system-status/components/desktop-update-config.tsx +91 -2
  132. package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
  133. package/src/index.css +18 -0
  134. package/src/shared/components/config-split-page.tsx +6 -6
  135. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  136. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  137. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  138. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +5 -1
  139. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  140. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  141. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  142. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  143. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  144. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  145. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  146. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  147. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  148. package/src/shared/components/ui/card.tsx +12 -4
  149. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  150. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  151. package/src/shared/hooks/use-server-path-read.ts +11 -1
  152. package/src/shared/lib/api/ncp-session.types.ts +1 -1
  153. package/src/shared/lib/api/types.ts +12 -3
  154. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  155. package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
  156. package/src/shared/lib/i18n/locales/en-US/chat.json +19 -0
  157. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  158. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
  159. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  160. package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
  161. package/src/shared/lib/i18n/locales/zh-CN/chat.json +19 -0
  162. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  163. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
  164. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  165. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  166. package/src/shared/lib/theme/index.test.ts +21 -0
  167. package/src/shared/lib/theme/index.ts +6 -4
  168. package/src/vite-env.d.ts +2 -0
  169. package/dist/assets/channels-list-page-Cv8o9Hxg.js +0 -8
  170. package/dist/assets/chat-page-BtA7H1Ea.js +0 -1
  171. package/dist/assets/config-split-page-EidRBpP2.js +0 -1
  172. package/dist/assets/desktop-update-config-C6n_TB1r.js +0 -1
  173. package/dist/assets/doc-browser-B76XOXmW.js +0 -1
  174. package/dist/assets/doc-browser-DBnj9UMB.js +0 -1
  175. package/dist/assets/doc-browser-context-Sv-044Cl.js +0 -1
  176. package/dist/assets/ellipsis-2uzhypw5.js +0 -1
  177. package/dist/assets/index-CBRP-7-a.css +0 -1
  178. package/dist/assets/index-Z1fYpbNd.js +0 -104
  179. package/dist/assets/loader-circle-BjmN5DeJ.js +0 -1
  180. package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +0 -1
  181. package/dist/assets/model-config-page-Cdr9P6TQ.js +0 -1
  182. package/dist/assets/plus-3ztRZD_l.js +0 -1
  183. package/dist/assets/providers-config-page-CWlO7J4A.js +0 -1
  184. package/dist/assets/remote-CKWZfVVB.js +0 -1
  185. package/dist/assets/rotate-cw-CHcaRUDz.js +0 -1
  186. package/dist/assets/runtime-config-page-CfTqh1nv.js +0 -1
  187. package/dist/assets/search-config-page-Dm8daYny.js +0 -1
  188. package/dist/assets/secrets-config-page-D8AbcR05.js +0 -3
  189. package/dist/assets/security-config-Ds0LsJmo.js +0 -1
  190. package/dist/assets/x-DqL1mYLC.js +0 -1
  191. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -7,7 +7,7 @@ import { type SessionContextView } from '@/features/chat/features/session/utils/
7
7
  import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
8
8
  import { cn } from '@/shared/lib/utils';
9
9
  import { t } from '@/shared/lib/i18n';
10
- import { Check, GitBranch, Pencil, X } from 'lucide-react';
10
+ import { Check, GitBranch, Pencil, Pin, X } from 'lucide-react';
11
11
 
12
12
  type ChatSidebarSessionItemProps = {
13
13
  sessionKey: string;
@@ -15,6 +15,7 @@ type ChatSidebarSessionItemProps = {
15
15
  showUnreadDot: boolean;
16
16
  runStatus?: SessionRunStatus;
17
17
  context: SessionContextView;
18
+ isPinned: boolean;
18
19
  title: string;
19
20
  previewText: string;
20
21
  trailingText: string;
@@ -31,6 +32,7 @@ type ChatSidebarSessionItemProps = {
31
32
  onDraftLabelChange: (value: string) => void;
32
33
  onSave: () => void | Promise<void>;
33
34
  onCancel: () => void;
35
+ onTogglePinned: () => void;
34
36
  };
35
37
 
36
38
  type ChatSidebarSessionEditingViewProps = Pick<
@@ -72,7 +74,6 @@ function ChatSidebarSessionEditingView({
72
74
  icon={<Check className="h-3.5 w-3.5" />}
73
75
  label={t('save')}
74
76
  tooltip={false}
75
- className="h-7 w-7 rounded-lg text-gray-500 hover:bg-white hover:text-gray-900"
76
77
  onClick={() => void onSave()}
77
78
  disabled={isSaving}
78
79
  />
@@ -80,7 +81,6 @@ function ChatSidebarSessionEditingView({
80
81
  icon={<X className="h-3.5 w-3.5" />}
81
82
  label={t('cancel')}
82
83
  tooltip={false}
83
- className="h-7 w-7 rounded-lg text-gray-500 hover:bg-white hover:text-gray-900"
84
84
  onClick={onCancel}
85
85
  disabled={isSaving}
86
86
  />
@@ -100,6 +100,7 @@ function ChatSidebarSessionDisplayView({
100
100
  showUnreadDot,
101
101
  runStatus,
102
102
  context,
103
+ isPinned,
103
104
  title,
104
105
  previewText,
105
106
  trailingText,
@@ -109,14 +110,15 @@ function ChatSidebarSessionDisplayView({
109
110
  childSessionCount = 0,
110
111
  onSelect,
111
112
  onOpenChildSessions,
112
- onStartEditing
113
+ onStartEditing,
114
+ onTogglePinned
113
115
  }: ChatSidebarSessionDisplayViewProps) {
114
- const trailingControlsClassName = childSessionCount > 0 && onOpenChildSessions ? 'pr-14' : 'pr-6';
116
+ const trailingControlsClassName = childSessionCount > 0 && onOpenChildSessions ? 'pr-20' : 'pr-12';
115
117
 
116
118
  return (
117
119
  <div className="group/session relative">
118
120
  <button type="button" onClick={onSelect} className="w-full text-left">
119
- <div className={cn('flex min-w-0 items-start', trailingControlsClassName)}>
121
+ <div className={cn('flex min-h-6 min-w-0 items-center', trailingControlsClassName)}>
120
122
  <span className="flex min-w-0 items-center gap-1.5">
121
123
  {agentId?.trim() && agentId.trim().toLowerCase() !== 'main' ? (
122
124
  <AgentAvatar
@@ -126,7 +128,7 @@ function ChatSidebarSessionDisplayView({
126
128
  className="h-5 w-5 shrink-0"
127
129
  />
128
130
  ) : null}
129
- <span className="truncate font-medium">{title}</span>
131
+ <span className="truncate leading-6 font-medium">{title}</span>
130
132
  {context.label ? (
131
133
  <span
132
134
  className={cn(
@@ -140,7 +142,7 @@ function ChatSidebarSessionDisplayView({
140
142
  </span>
141
143
  ) : null}
142
144
  {context.icon ? (
143
- <span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center">
145
+ <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">
144
146
  <SessionContextIconNode icon={context.icon} className={active ? 'text-gray-700' : 'text-gray-500'} />
145
147
  </span>
146
148
  ) : null}
@@ -168,7 +170,7 @@ function ChatSidebarSessionDisplayView({
168
170
  onOpenChildSessions();
169
171
  }}
170
172
  className={cn(
171
- 'absolute right-6 top-0 inline-flex h-5 items-center gap-1 rounded-md px-1.5 text-[10px] font-medium text-gray-400 transition-all hover:bg-white hover:text-gray-900',
173
+ 'absolute right-12 top-0 inline-flex h-6 items-center gap-1 rounded-md px-1.5 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-black/10 hover:text-foreground',
172
174
  active
173
175
  ? 'opacity-100'
174
176
  : 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
@@ -181,23 +183,41 @@ function ChatSidebarSessionDisplayView({
181
183
  </button>
182
184
  ) : null}
183
185
  {runStatus ? (
184
- <span className="absolute right-0 top-0 inline-flex h-5 w-5 items-center justify-center transition-opacity group-hover/session:opacity-0 group-focus-within/session:opacity-0">
186
+ <span className="absolute right-0 top-0 inline-flex h-6 w-6 items-center justify-center transition-opacity group-hover/session:opacity-0 group-focus-within/session:opacity-0">
185
187
  <SessionRunBadge status={runStatus} />
186
188
  </span>
187
189
  ) : null}
188
- <IconActionButton
189
- icon={<Pencil className="h-3 w-3" />}
190
- label={t('edit')}
191
- tooltip={false}
192
- onClick={(event) => {
193
- event.stopPropagation();
194
- onStartEditing();
195
- }}
196
- className={cn(
197
- 'absolute right-0 top-0 inline-flex h-5 w-5 items-center justify-center rounded-md text-gray-400 transition-all hover:bg-white hover:text-gray-900',
198
- 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
199
- )}
200
- />
190
+ <div className="absolute right-0 top-0 flex h-6 items-center gap-0.5 opacity-0 transition-opacity group-hover/session:opacity-100 group-focus-within/session:opacity-100">
191
+ <IconActionButton
192
+ size="sm"
193
+ tone="strong"
194
+ icon={
195
+ <Pin
196
+ className={cn(
197
+ 'h-3.5 w-3.5',
198
+ isPinned && 'fill-current text-foreground',
199
+ )}
200
+ />
201
+ }
202
+ label={t(isPinned ? 'chatSidebarUnpinSession' : 'chatSidebarPinSession')}
203
+ tooltipSide="right"
204
+ onClick={(event) => {
205
+ event.stopPropagation();
206
+ onTogglePinned();
207
+ }}
208
+ />
209
+ <IconActionButton
210
+ size="sm"
211
+ tone="strong"
212
+ icon={<Pencil className="h-3.5 w-3.5" />}
213
+ label={t('edit')}
214
+ tooltipSide="right"
215
+ onClick={(event) => {
216
+ event.stopPropagation();
217
+ onStartEditing();
218
+ }}
219
+ />
220
+ </div>
201
221
  </div>
202
222
  );
203
223
  }
@@ -208,6 +228,7 @@ export function ChatSidebarSessionItem({
208
228
  showUnreadDot,
209
229
  runStatus,
210
230
  context,
231
+ isPinned,
211
232
  title,
212
233
  previewText,
213
234
  trailingText,
@@ -223,7 +244,8 @@ export function ChatSidebarSessionItem({
223
244
  onStartEditing,
224
245
  onDraftLabelChange,
225
246
  onSave,
226
- onCancel
247
+ onCancel,
248
+ onTogglePinned
227
249
  }: ChatSidebarSessionItemProps) {
228
250
  return (
229
251
  <div
@@ -249,6 +271,7 @@ export function ChatSidebarSessionItem({
249
271
  showUnreadDot={showUnreadDot}
250
272
  runStatus={runStatus}
251
273
  context={context}
274
+ isPinned={isPinned}
252
275
  title={title}
253
276
  previewText={previewText}
254
277
  trailingText={trailingText}
@@ -259,6 +282,7 @@ export function ChatSidebarSessionItem({
259
282
  childSessionCount={childSessionCount}
260
283
  onOpenChildSessions={onOpenChildSessions}
261
284
  onStartEditing={onStartEditing}
285
+ onTogglePinned={onTogglePinned}
262
286
  />
263
287
  )}
264
288
  </div>
@@ -19,7 +19,6 @@ type ChatSidebarSessionListProps = {
19
19
  defaultSessionType: string;
20
20
  sessionTypeOptions: SessionTypeOption[];
21
21
  renderSessionItem: (item: NcpSessionListItemView) => ReactNode;
22
- onCreateSession: (sessionType: string, projectRoot?: string | null) => void;
23
22
  };
24
23
 
25
24
  function ChatSidebarEmptyState({ label }: { label: string }) {
@@ -36,7 +35,6 @@ export function ChatSidebarSessionList({
36
35
  groups,
37
36
  isLoading,
38
37
  isProjectFirstView,
39
- onCreateSession,
40
38
  projectGroups,
41
39
  renderSessionItem,
42
40
  sessionTypeOptions,
@@ -54,7 +52,6 @@ export function ChatSidebarSessionList({
54
52
  defaultSessionType={defaultSessionType}
55
53
  sessionTypeOptions={sessionTypeOptions}
56
54
  renderSessionItem={renderSessionItem}
57
- onCreateSession={onCreateSession}
58
55
  />
59
56
  );
60
57
  }
@@ -9,7 +9,6 @@ import { ChatSessionProjectDialog } from './chat-session-project-dialog';
9
9
  import { t } from '@/shared/lib/i18n';
10
10
 
11
11
  const SESSION_HEADER_ACTION_GROUP_CLASS = 'flex shrink-0 items-center gap-1.5';
12
- const SESSION_HEADER_ACTION_BUTTON_CLASS = 'h-7 w-7 shrink-0 rounded-lg text-muted-foreground/70 hover:bg-accent hover:text-accent-foreground';
13
12
 
14
13
  type ChatSessionHeaderActionsProps = {
15
14
  sessionKey: string;
@@ -65,7 +64,6 @@ export function ChatSessionHeaderActions({
65
64
  <IconActionButton
66
65
  icon={<GitBranch className="h-4 w-4" />}
67
66
  label={t('chatSessionOpenChildSessions')}
68
- className={SESSION_HEADER_ACTION_BUTTON_CLASS}
69
67
  onClick={onOpenChildSessions}
70
68
  disabled={isBusy}
71
69
  />
@@ -74,7 +72,6 @@ export function ChatSessionHeaderActions({
74
72
  <IconActionButton
75
73
  icon={<AlarmClock className="h-4 w-4" />}
76
74
  label={t('chatSessionOpenCronJobs')}
77
- className={SESSION_HEADER_ACTION_BUTTON_CLASS}
78
75
  onClick={onOpenSessionCronJobs}
79
76
  disabled={isBusy}
80
77
  />
@@ -85,7 +82,6 @@ export function ChatSessionHeaderActions({
85
82
  icon={<MoreVertical className="h-4 w-4" />}
86
83
  label={t('chatSessionMoreActions')}
87
84
  tooltip={false}
88
- className={SESSION_HEADER_ACTION_BUTTON_CLASS}
89
85
  disabled={isBusy}
90
86
  />
91
87
  </PopoverTrigger>
@@ -0,0 +1,294 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ Check,
4
+ ChevronDown,
5
+ Loader2,
6
+ MessageSquareText,
7
+ Search,
8
+ } from "lucide-react";
9
+ import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
10
+ import {
11
+ useNcpSessionListView,
12
+ type NcpSessionListItemView,
13
+ } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
14
+ import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
15
+ import { SessionRunBadge } from "@/features/chat/features/session/components/session-run-badge";
16
+ import {
17
+ formatSessionListTime,
18
+ sessionActivityPreviewText,
19
+ sessionDisplayName,
20
+ } from "@/features/chat/features/session/utils/chat-session-display.utils";
21
+ import { sortSessionItemsByActivityAtDesc } from "@/features/chat/features/session/utils/chat-sidebar-session-groups.utils";
22
+ import { resolveSessionContextView } from "@/features/chat/features/session/utils/session-context.utils";
23
+ import {
24
+ buildSessionTypeOptions,
25
+ type ChatSessionTypeOption,
26
+ } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
27
+ import {
28
+ shouldShowUnreadSessionIndicator,
29
+ useChatSessionListStore,
30
+ } from "@/features/chat/stores/chat-session-list.store";
31
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
32
+ import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
33
+ import {
34
+ Popover,
35
+ PopoverContent,
36
+ PopoverTrigger,
37
+ } from "@/shared/components/ui/popover";
38
+ import { Input } from "@/shared/components/ui/input";
39
+ import { t } from "@/shared/lib/i18n";
40
+ import { cn } from "@/shared/lib/utils";
41
+
42
+ const SWITCHER_TRIGGER_CLASS =
43
+ "group inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40";
44
+
45
+ function ChatSessionTitle({ title }: { title: string }) {
46
+ return (
47
+ <span className="text-sm font-medium text-foreground truncate">
48
+ {title}
49
+ </span>
50
+ );
51
+ }
52
+
53
+ function ChatSessionTitleSwitcherEmptyState({ label }: { label: string }) {
54
+ return (
55
+ <div className="flex items-center gap-2 px-3 py-4 text-xs text-muted-foreground">
56
+ <MessageSquareText className="h-4 w-4 shrink-0 text-muted-foreground/55" />
57
+ <span>{label}</span>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function ChatSessionTitleSwitcherLoadingState() {
63
+ return (
64
+ <div className="flex items-center gap-2 px-3 py-4 text-xs text-muted-foreground">
65
+ <Loader2 className="h-4 w-4 shrink-0 animate-spin" />
66
+ <span>{t("sessionsLoading")}</span>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ function ChatSessionSwitchItem({
72
+ item,
73
+ selectedSessionKey,
74
+ optimisticReadAtBySessionKey,
75
+ sessionTypeOptions,
76
+ onSelect,
77
+ }: {
78
+ item: NcpSessionListItemView;
79
+ selectedSessionKey: string | null;
80
+ optimisticReadAtBySessionKey: Record<string, string>;
81
+ sessionTypeOptions: ChatSessionTypeOption[];
82
+ onSelect: (sessionKey: string) => void;
83
+ }) {
84
+ const { session, runStatus } = item;
85
+ const active = selectedSessionKey === session.key;
86
+ const optimisticReadAt = optimisticReadAtBySessionKey[session.key];
87
+ const effectiveReadAt =
88
+ optimisticReadAt && session.readAt
89
+ ? optimisticReadAt.localeCompare(session.readAt) > 0
90
+ ? optimisticReadAt
91
+ : session.readAt
92
+ : (optimisticReadAt ?? session.readAt);
93
+ const shouldShowUnread = shouldShowUnreadSessionIndicator({
94
+ active,
95
+ lastMessageAt: session.lastMessageAt,
96
+ readAt: effectiveReadAt,
97
+ runStatus,
98
+ });
99
+ const previewText =
100
+ sessionActivityPreviewText(session) ??
101
+ session.projectName ??
102
+ session.projectRoot ??
103
+ `${session.messageCount}`;
104
+ const trailingText = formatSessionListTime(
105
+ session.lastMessageAt ?? session.createdAt,
106
+ );
107
+ const sessionContext = resolveSessionContextView(session, sessionTypeOptions);
108
+
109
+ return (
110
+ <button
111
+ type="button"
112
+ aria-current={active ? "page" : undefined}
113
+ className={cn(
114
+ "flex w-full min-w-0 items-start gap-2 rounded-lg px-2.5 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
115
+ active
116
+ ? "bg-accent text-accent-foreground"
117
+ : "text-popover-foreground hover:bg-accent/70",
118
+ )}
119
+ onClick={() => onSelect(session.key)}
120
+ >
121
+ <span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center">
122
+ {active ? (
123
+ <Check className="h-3.5 w-3.5" />
124
+ ) : shouldShowUnread ? (
125
+ <span
126
+ aria-label={t("chatSessionUnread")}
127
+ className="h-2 w-2 rounded-full bg-primary"
128
+ />
129
+ ) : null}
130
+ </span>
131
+ <span className="min-w-0 flex-1">
132
+ <span className="flex min-w-0 items-center gap-1.5">
133
+ <span className="min-w-0 truncate text-[13px] font-medium">
134
+ {sessionDisplayName(session)}
135
+ </span>
136
+ {sessionContext.label ? (
137
+ <span className="shrink-0 rounded-full border border-border/70 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-muted-foreground">
138
+ {sessionContext.label}
139
+ </span>
140
+ ) : null}
141
+ {sessionContext.icon ? (
142
+ <span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center text-muted-foreground">
143
+ <SessionContextIconNode icon={sessionContext.icon} />
144
+ </span>
145
+ ) : null}
146
+ </span>
147
+ <span className="mt-0.5 flex min-w-0 items-center gap-2 text-[11px] text-muted-foreground">
148
+ <span className="min-w-0 truncate">{previewText}</span>
149
+ <span className="shrink-0">{trailingText}</span>
150
+ </span>
151
+ </span>
152
+ {runStatus ? (
153
+ <span className="mt-0.5 shrink-0">
154
+ <SessionRunBadge status={runStatus} />
155
+ </span>
156
+ ) : null}
157
+ </button>
158
+ );
159
+ }
160
+
161
+ function ChatSessionTitleSwitcherPopover({
162
+ selectedSessionKey,
163
+ title,
164
+ }: {
165
+ selectedSessionKey: string | null;
166
+ title: string;
167
+ }) {
168
+ const presenter = usePresenter();
169
+ const [isOpen, setIsOpen] = useState(false);
170
+ const [searchQuery, setSearchQuery] = useState("");
171
+ const optimisticReadAtBySessionKey = useChatSessionListStore(
172
+ (state) => state.optimisticReadAtBySessionKey,
173
+ );
174
+ const sessionTypesData = useChatQueryStore(
175
+ (state) => state.snapshot.sessionTypesQuery?.data ?? null,
176
+ );
177
+ const { isLoading, items } = useNcpSessionListView({ query: searchQuery });
178
+ const sortedItems = useMemo(
179
+ () => sortSessionItemsByActivityAtDesc(items),
180
+ [items],
181
+ );
182
+ const sessionTypeOptions = useMemo(
183
+ () => buildSessionTypeOptions(sessionTypesData?.options ?? []),
184
+ [sessionTypesData?.options],
185
+ );
186
+
187
+ const selectSession = (sessionKey: string) => {
188
+ setIsOpen(false);
189
+ setSearchQuery("");
190
+ if (sessionKey === selectedSessionKey) {
191
+ return;
192
+ }
193
+ presenter.chatSessionListManager.selectSession(sessionKey);
194
+ };
195
+ const updateOpen = (open: boolean) => {
196
+ setIsOpen(open);
197
+ if (!open) {
198
+ setSearchQuery("");
199
+ }
200
+ };
201
+
202
+ return (
203
+ <Popover open={isOpen} onOpenChange={updateOpen}>
204
+ <PopoverTrigger asChild>
205
+ <button
206
+ type="button"
207
+ aria-label={`${t("chatSessionSwitch")}: ${title}`}
208
+ aria-expanded={isOpen}
209
+ className={SWITCHER_TRIGGER_CLASS}
210
+ >
211
+ <span className="min-w-0 truncate">{title}</span>
212
+ <ChevronDown
213
+ className={cn(
214
+ "h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform group-hover:text-accent-foreground",
215
+ isOpen ? "rotate-180" : null,
216
+ )}
217
+ />
218
+ </button>
219
+ </PopoverTrigger>
220
+ <PopoverContent
221
+ align="start"
222
+ className="w-[22rem] max-w-[calc(100vw-2rem)] p-0"
223
+ >
224
+ <div className="space-y-2 border-b border-border px-3 py-2">
225
+ <div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/75">
226
+ {t("chatSessionSwitcherTitle")}
227
+ </div>
228
+ <div className="relative">
229
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/65" />
230
+ <Input
231
+ value={searchQuery}
232
+ onChange={(event) => setSearchQuery(event.target.value)}
233
+ placeholder={t("chatSearchSessionPlaceholder")}
234
+ aria-label={t("chatSearchSessionPlaceholder")}
235
+ className="h-8 rounded-lg bg-background pl-8 pr-2 text-xs"
236
+ autoFocus
237
+ />
238
+ </div>
239
+ </div>
240
+ <div className="max-h-80 overflow-y-auto p-1.5">
241
+ {isLoading ? (
242
+ <ChatSessionTitleSwitcherLoadingState />
243
+ ) : sortedItems.length > 0 ? (
244
+ <div className="space-y-1">
245
+ {sortedItems.map((item) => (
246
+ <ChatSessionSwitchItem
247
+ key={item.session.key}
248
+ item={item}
249
+ selectedSessionKey={selectedSessionKey}
250
+ optimisticReadAtBySessionKey={optimisticReadAtBySessionKey}
251
+ sessionTypeOptions={sessionTypeOptions}
252
+ onSelect={selectSession}
253
+ />
254
+ ))}
255
+ </div>
256
+ ) : (
257
+ <ChatSessionTitleSwitcherEmptyState
258
+ label={
259
+ searchQuery.trim()
260
+ ? t("chatSessionSwitcherNoResults")
261
+ : t("sessionsEmpty")
262
+ }
263
+ />
264
+ )}
265
+ </div>
266
+ </PopoverContent>
267
+ </Popover>
268
+ );
269
+ }
270
+
271
+ export function ChatSessionTitleSwitcher({
272
+ layoutMode,
273
+ selectedSessionKey,
274
+ title,
275
+ }: {
276
+ layoutMode: "desktop" | "mobile";
277
+ selectedSessionKey: string | null;
278
+ title: string;
279
+ }) {
280
+ const isSidebarCollapsed = useViewportLayoutStore(
281
+ (state) => state.isSidebarCollapsed,
282
+ );
283
+
284
+ if (layoutMode !== "desktop" || !isSidebarCollapsed) {
285
+ return <ChatSessionTitle title={title} />;
286
+ }
287
+
288
+ return (
289
+ <ChatSessionTitleSwitcherPopover
290
+ selectedSessionKey={selectedSessionKey}
291
+ title={title}
292
+ />
293
+ );
294
+ }
@@ -5,7 +5,7 @@ import {
5
5
  sessionActivityPreviewText,
6
6
  sessionDisplayName,
7
7
  sessionMatchesQuery
8
- } from '../chat-session-display.utils';
8
+ } from '@/features/chat/features/session/utils/chat-session-display.utils';
9
9
 
10
10
  function createSession(overrides: Partial<SessionEntryView> = {}): SessionEntryView {
11
11
  return {
@@ -88,6 +88,20 @@ describe('chat-session-display', () => {
88
88
  ).toBe('最终回复内容');
89
89
  });
90
90
 
91
+ it('does not show cancelled activity as ordinary session preview text', () => {
92
+ expect(
93
+ sessionActivityPreviewText(
94
+ createSession({
95
+ activityPreview: {
96
+ state: 'cancelled',
97
+ statusText: 'Run interrupted: User stopped the current run.',
98
+ timestamp: '2026-05-16T01:00:00.000Z'
99
+ }
100
+ })
101
+ )
102
+ ).toBeNull();
103
+ });
104
+
91
105
  it('formats today activity as time like WeChat session lists', () => {
92
106
  expect(
93
107
  formatSessionListTime(
@@ -105,8 +105,8 @@ describe('adaptNcpSessionSummary', () => {
105
105
  metadata: {
106
106
  last_activity_preview: {
107
107
  state: 'completed',
108
- replyText: '已经整理好方案',
109
- statusText: '工具调用完成',
108
+ replyText: 'Plan is ready',
109
+ statusText: 'Tool call completed',
110
110
  timestamp: '2026-05-16T01:00:00.000Z',
111
111
  },
112
112
  },
@@ -115,8 +115,28 @@ describe('adaptNcpSessionSummary', () => {
115
115
 
116
116
  expect(adapted.activityPreview).toEqual({
117
117
  state: 'completed',
118
- replyText: '已经整理好方案',
119
- statusText: '工具调用完成',
118
+ replyText: 'Plan is ready',
119
+ statusText: 'Tool call completed',
120
+ timestamp: '2026-05-16T01:00:00.000Z',
121
+ });
122
+ });
123
+
124
+ it('maps cancelled session activity preview metadata into the session entry', () => {
125
+ const adapted = adaptNcpSessionSummary(
126
+ createSummary({
127
+ metadata: {
128
+ last_activity_preview: {
129
+ state: 'cancelled',
130
+ statusText: 'Run interrupted: User stopped the current run.',
131
+ timestamp: '2026-05-16T01:00:00.000Z',
132
+ },
133
+ },
134
+ }),
135
+ );
136
+
137
+ expect(adapted.activityPreview).toEqual({
138
+ state: 'cancelled',
139
+ statusText: 'Run interrupted: User stopped the current run.',
120
140
  timestamp: '2026-05-16T01:00:00.000Z',
121
141
  });
122
142
  });
@@ -15,6 +15,9 @@ export function sessionActivityPreviewText(session: SessionEntryView): string |
15
15
  if (!preview) {
16
16
  return null;
17
17
  }
18
+ if (preview.state === 'cancelled') {
19
+ return null;
20
+ }
18
21
  if (preview.state === 'failed' || preview.state === 'running') {
19
22
  return preview.statusText ?? preview.replyText ?? null;
20
23
  }