@nextclaw/ui 0.15.20 → 0.15.22

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 +42 -0
  2. package/dist/assets/{api-tWUa6l9J.js → api-B1yHQYs-.js} +6 -6
  3. package/dist/assets/app-presenter-provider-uP9waOzv.js +108 -0
  4. package/dist/assets/appearance-settings-page-C9Zq3hP9.js +1 -0
  5. package/dist/assets/{book-open-BQ29ZmDE.js → book-open-oSmyVXea.js} +1 -1
  6. package/dist/assets/{card-CtKueklV.js → card-CGvJZTZp.js} +1 -1
  7. package/dist/assets/{channels-list-page-C8y3rqYt.js → channels-list-page-5BTxpQHw.js} +2 -2
  8. package/dist/assets/chat-page-jTvli8Jp.js +1 -0
  9. package/dist/assets/config-split-page-BgBsUmp1.js +1 -0
  10. package/dist/assets/confirm-dialog-h849QRWO.js +5 -0
  11. package/dist/assets/desktop-update-config-BGGHxClR.js +1 -0
  12. package/dist/assets/{dist-rYHnBo8t.js → dist-CxUhurq9.js} +1 -1
  13. package/dist/assets/doc-browser-0eL8AX42.js +1 -0
  14. package/dist/assets/{doc-browser-D8ng4EUc.js → doc-browser-DEqE9lsh.js} +1 -1
  15. package/dist/assets/{doc-browser-context-CPHNzQ8m.js → doc-browser-context-DkzFIUoK.js} +1 -1
  16. package/dist/assets/{eye-CiJz3ODY.js → eye-gOWQGt8N.js} +1 -1
  17. package/dist/assets/{form-actions-BnZVzLZf.js → form-actions-CgjYZUzM.js} +1 -1
  18. package/dist/assets/index-DSAUWCfl.js +2 -0
  19. package/dist/assets/index-G7cBryJ4.css +1 -0
  20. package/dist/assets/loader-circle-BAuycGf6.js +1 -0
  21. package/dist/assets/{logo-badge-BXI-Kdhd.js → logo-badge-BNcsDsUM.js} +1 -1
  22. package/dist/assets/{mcp-marketplace-page-CxNe-Q9Q.js → mcp-marketplace-page-BX39MtPa.js} +3 -3
  23. package/dist/assets/mcp-marketplace-page-CkDtOEA4.js +1 -0
  24. package/dist/assets/{model-config-page-CR_mgEVp.js → model-config-page-7MIXg2fQ.js} +1 -1
  25. package/dist/assets/{navigation-link-C1cvB7y4.js → navigation-link-B3iFOdxx.js} +1 -1
  26. package/dist/assets/plus-yvhFgfal.js +1 -0
  27. package/dist/assets/{popover-ZoDwVwwd.js → popover-Cpep_rJe.js} +1 -1
  28. package/dist/assets/{provider-models-CXUgDxGR.js → provider-models-CUrptZ-o.js} +1 -1
  29. package/dist/assets/provider-scoped-model-input-CHmOzz7x.js +1 -0
  30. package/dist/assets/providers-config-page-A6J7Y0II.js +1 -0
  31. package/dist/assets/react-BCJGCjrP.js +8 -0
  32. package/dist/assets/{refresh-cw-DKewcdAl.js → refresh-cw-CkKwPztT.js} +1 -1
  33. package/dist/assets/remote-FD-ZfSIn.js +1 -0
  34. package/dist/assets/rotate-cw-DrJMfHhp.js +1 -0
  35. package/dist/assets/runtime-config-page-DwjO-Vs6.js +1 -0
  36. package/dist/assets/{save-BrckuAXY.js → save-BINMWate.js} +1 -1
  37. package/dist/assets/{search-BhXQB2TO.js → search-7M38azRp.js} +1 -1
  38. package/dist/assets/{search-config-page-DFOQ6SDJ.js → search-config-page-T6tMhXbP.js} +1 -1
  39. package/dist/assets/{secrets-config-page-BqqeClqE.js → secrets-config-page-2mVejXf2.js} +2 -2
  40. package/dist/assets/security-config-DD5RrjDe.js +1 -0
  41. package/dist/assets/{select-CVzGGT38.js → select-BvidP5Oa.js} +1 -1
  42. package/dist/assets/{settings-2-BP6L2enG.js → settings-2-DqZstREV.js} +1 -1
  43. package/dist/assets/skeleton-BEQW2Dva.js +1 -0
  44. package/dist/assets/{tabs-B-QWjKCL.js → tabs-BeLxVmkt.js} +1 -1
  45. package/dist/assets/{tag-chip-C88mxjwx.js → tag-chip-BoDaLnWt.js} +1 -1
  46. package/dist/assets/{trash-2-ck3t33I3.js → trash-2-CYUskMMN.js} +1 -1
  47. package/dist/assets/{use-config-BAr5bH2c.js → use-config-CxKRXXCl.js} +1 -1
  48. package/dist/assets/use-confirm-dialog-HASTvOP2.js +1 -0
  49. package/dist/assets/{use-viewport-layout-k1isXfNm.js → use-viewport-layout-CJxkjoRz.js} +1 -1
  50. package/dist/assets/x-DdUwjre5.js +1 -0
  51. package/dist/index.html +29 -29
  52. package/package.json +8 -8
  53. package/src/app/components/__tests__/app-notification-runtime.test.tsx +32 -0
  54. package/src/app/components/app-notification-runtime.tsx +15 -0
  55. package/src/app/components/layout/sidebar-items.tsx +23 -9
  56. package/src/app/components/layout/sidebar-rail.styles.ts +0 -2
  57. package/src/app/components/layout/sidebar.tsx +2 -2
  58. package/src/app/configs/app-navigation.config.ts +21 -0
  59. package/src/app/index.tsx +22 -2
  60. package/src/app/presenters/app.presenter.ts +8 -0
  61. package/src/app/styles/design-system.css +12 -0
  62. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +2 -2
  63. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +19 -1
  64. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +1 -1
  65. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -6
  66. package/src/features/chat/components/conversation/chat-conversation-header.tsx +1 -1
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +1 -1
  68. package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +52 -0
  69. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +11 -17
  70. package/src/features/chat/components/layout/chat-page-shell.tsx +9 -13
  71. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +40 -38
  72. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +13 -64
  73. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +1 -4
  74. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -1
  75. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +15 -4
  76. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +7 -7
  77. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +4 -2
  78. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +1 -0
  79. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +1 -0
  80. package/src/features/chat/features/conversation/utils/session-queued-input.utils.ts +1 -0
  81. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +3 -0
  82. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +0 -1
  83. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +3 -0
  84. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +29 -0
  85. package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +4 -1
  86. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +10 -10
  87. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  88. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
  89. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +1 -1
  90. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +1 -1
  91. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
  92. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +26 -0
  93. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +4 -2
  94. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +48 -12
  95. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +13 -4
  96. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +6 -0
  97. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +10 -7
  98. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +1 -1
  99. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +3 -14
  100. package/src/features/chat/features/welcome/components/chat-welcome-prompt-suggestions.tsx +37 -0
  101. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  102. package/src/features/chat/features/welcome/components/chat-welcome.tsx +33 -31
  103. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -1
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -1
  105. package/src/features/chat/index.ts +1 -0
  106. package/src/features/chat/managers/__tests__/chat-completion-notification.manager.test.ts +177 -0
  107. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +15 -3
  108. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +3 -10
  109. package/src/features/chat/managers/chat-completion-notification.manager.ts +146 -0
  110. package/src/features/chat/managers/chat-session-list.manager.ts +3 -23
  111. package/src/features/chat/managers/chat-thread.manager.ts +2 -5
  112. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +32 -11
  113. package/src/features/chat/pages/ncp-chat-page.tsx +9 -1
  114. package/src/features/chat/types/chat-input-bar.types.ts +1 -0
  115. package/src/features/cron/components/cron-task-discovery.tsx +1 -1
  116. package/src/features/inbox/components/inbox-delivery-content.test.tsx +57 -0
  117. package/src/features/inbox/components/inbox-delivery-content.tsx +118 -0
  118. package/src/features/inbox/components/inbox-reader-dialog.test.tsx +77 -0
  119. package/src/features/inbox/components/inbox-reader-dialog.tsx +224 -0
  120. package/src/features/inbox/components/inbox-runtime.tsx +14 -0
  121. package/src/features/inbox/hooks/use-inbox-deliveries.ts +14 -0
  122. package/src/features/inbox/index.ts +7 -0
  123. package/src/features/inbox/managers/inbox.manager.test.ts +86 -0
  124. package/src/features/inbox/managers/inbox.manager.ts +178 -0
  125. package/src/features/inbox/pages/inbox-page.test.ts +28 -0
  126. package/src/features/inbox/pages/inbox-page.tsx +385 -0
  127. package/src/features/inbox/stores/inbox.store.ts +23 -0
  128. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  129. package/src/features/notifications/components/__tests__/app-notification-toast.test.tsx +51 -0
  130. package/src/features/notifications/components/app-notification-toast.tsx +80 -0
  131. package/src/features/notifications/index.ts +8 -0
  132. package/src/features/notifications/managers/__tests__/app-notification.manager.test.ts +75 -0
  133. package/src/features/notifications/managers/app-notification.manager.ts +41 -0
  134. package/src/features/panel-apps/components/panel-app-list-item.tsx +1 -1
  135. package/src/features/panel-apps/components/panel-app-toolbar.tsx +2 -2
  136. package/src/features/panel-apps/components/panel-apps-list.tsx +4 -4
  137. package/src/features/service-apps/components/service-apps-panel.tsx +1 -1
  138. package/src/features/settings/components/config/provider-auth-section.tsx +90 -59
  139. package/src/features/settings/components/config/provider-form.tsx +10 -17
  140. package/src/features/settings/components/config/provider-status-badge.tsx +4 -2
  141. package/src/features/settings/pages/__tests__/providers-config-page.test.tsx +12 -0
  142. package/src/features/settings/pages/providers-config-page.tsx +2 -1
  143. package/src/platforms/mobile/components/__tests__/mobile-app-shell.test.tsx +13 -7
  144. package/src/platforms/mobile/components/__tests__/mobile-bottom-nav.test.tsx +18 -20
  145. package/src/platforms/mobile/components/mobile-bottom-nav.tsx +14 -7
  146. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  147. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +1 -1
  148. package/src/shared/components/path-picker/server-path-picker-locations.tsx +1 -1
  149. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +3 -3
  150. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +1 -1
  151. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  152. package/src/shared/components/ui/button.tsx +4 -4
  153. package/src/shared/components/ui/dialog.tsx +1 -1
  154. package/src/shared/components/ui/input.tsx +1 -1
  155. package/src/shared/components/ui/scroll-area.tsx +2 -0
  156. package/src/shared/components/ui/select.tsx +1 -1
  157. package/src/shared/components/ui/switch.tsx +1 -1
  158. package/src/shared/components/ui/tabs.tsx +1 -1
  159. package/src/shared/components/ui/textarea.tsx +1 -1
  160. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  161. package/src/shared/lib/api/types.ts +2 -0
  162. package/src/shared/lib/i18n/index.ts +4 -0
  163. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -14
  164. package/src/shared/lib/i18n/locales/en-US/inbox.json +30 -0
  165. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -14
  166. package/src/shared/lib/i18n/locales/zh-CN/inbox.json +30 -0
  167. package/src/shared/lib/provider-models/__tests__/anonymous-provider-configuration.test.ts +40 -0
  168. package/src/shared/lib/provider-models/index.ts +4 -3
  169. package/dist/assets/app-presenter-provider-s-FrOinZ.js +0 -106
  170. package/dist/assets/appearance-settings-page-dMRBD7ni.js +0 -1
  171. package/dist/assets/chat-page-MXBltS3G.js +0 -1
  172. package/dist/assets/config-split-page-uC6d_r9c.js +0 -1
  173. package/dist/assets/confirm-dialog-C1pbqHZX.js +0 -5
  174. package/dist/assets/desktop-update-config-DssP8LCk.js +0 -1
  175. package/dist/assets/doc-browser-AXF1zRsM.js +0 -1
  176. package/dist/assets/index-CocHWEsI.js +0 -2
  177. package/dist/assets/index-DN6KM7WD.css +0 -1
  178. package/dist/assets/loader-circle-AOpK5kkW.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-5ogeBHnl.js +0 -1
  180. package/dist/assets/plus-DI37QuHc.js +0 -1
  181. package/dist/assets/provider-scoped-model-input-DIL6Ex_d.js +0 -1
  182. package/dist/assets/providers-config-page-D2NXVctW.js +0 -1
  183. package/dist/assets/react-39CDNZlM.js +0 -8
  184. package/dist/assets/remote-DYOOO1YG.js +0 -1
  185. package/dist/assets/rotate-cw-PmqA8LQT.js +0 -1
  186. package/dist/assets/runtime-config-page-0lb5NyKc.js +0 -1
  187. package/dist/assets/security-config-13AtPIlm.js +0 -1
  188. package/dist/assets/skeleton-OwCNQJ7M.js +0 -1
  189. package/dist/assets/use-confirm-dialog-C4e0nG_4.js +0 -1
  190. package/dist/assets/x-C1ijNWcE.js +0 -1
  191. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +0 -59
@@ -23,7 +23,7 @@ export function ChatSessionHeaderMenuItem({
23
23
  'flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50',
24
24
  destructive
25
25
  ? 'text-destructive hover:bg-destructive/10'
26
- : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
26
+ : 'text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground'
27
27
  )}
28
28
  onClick={onClick}
29
29
  disabled={disabled}
@@ -57,7 +57,7 @@ export function ChatSessionProjectBadge({
57
57
  <button
58
58
  type="button"
59
59
  title={projectRoot ?? undefined}
60
- className="min-w-0 max-w-[320px] shrink rounded-full border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:border-primary/35 hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-60"
60
+ className="min-w-0 max-w-[320px] shrink rounded-full border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:border-primary/35 hover:bg-[var(--interaction-hover)] hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-60"
61
61
  aria-label={t('chatSessionSetProject')}
62
62
  disabled={isProjectPending}
63
63
  >
@@ -69,7 +69,7 @@ function ChatSessionSwitchItem({
69
69
  "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-1 focus-visible:ring-border",
70
70
  active
71
71
  ? "bg-accent text-accent-foreground"
72
- : "text-foreground hover:bg-accent/65 hover:text-accent-foreground",
72
+ : "text-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground",
73
73
  )}
74
74
  onClick={() => onSelect(session.key)}
75
75
  >
@@ -29,7 +29,7 @@ import { cn } from "@/shared/lib/utils";
29
29
  import { useProjects } from "@/shared/hooks/use-projects";
30
30
 
31
31
  const SWITCHER_TRIGGER_CLASS =
32
- "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-1 focus-visible:ring-border";
32
+ "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-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border";
33
33
 
34
34
  function ChatSessionTitle({ title }: { title: string }) {
35
35
  return (
@@ -73,6 +73,32 @@ describe('chat-session-display', () => {
73
73
  ).toBe('正在调用工具:shell');
74
74
  });
75
75
 
76
+ it('localizes structured tool activity previews', () => {
77
+ const session = createSession({
78
+ activityPreview: {
79
+ state: 'running',
80
+ statusKind: 'tool-completed',
81
+ statusText: 'gateway',
82
+ timestamp: '2026-05-16T01:00:00.000Z'
83
+ }
84
+ });
85
+
86
+ expect(sessionActivityPreviewText(session, 'zh')).toBe('工具调用 · 已完成 · gateway');
87
+ expect(sessionActivityPreviewText(session, 'en')).toBe('Tool Call · Completed · gateway');
88
+ });
89
+
90
+ it('localizes persisted legacy tool activity previews', () => {
91
+ const session = createSession({
92
+ activityPreview: {
93
+ state: 'running',
94
+ statusText: 'Tool call completed: gateway',
95
+ timestamp: '2026-05-16T01:00:00.000Z'
96
+ }
97
+ });
98
+
99
+ expect(sessionActivityPreviewText(session, 'zh')).toBe('工具调用 · 已完成 · gateway');
100
+ });
101
+
76
102
  it('shows the final assistant reply after completion', () => {
77
103
  expect(
78
104
  sessionActivityPreviewText(
@@ -106,7 +106,8 @@ describe('adaptNcpSessionSummary', () => {
106
106
  last_activity_preview: {
107
107
  state: 'completed',
108
108
  replyText: 'Plan is ready',
109
- statusText: 'Tool call completed',
109
+ statusKind: 'tool-completed',
110
+ statusText: 'read_file',
110
111
  timestamp: '2026-05-16T01:00:00.000Z',
111
112
  },
112
113
  },
@@ -116,7 +117,8 @@ describe('adaptNcpSessionSummary', () => {
116
117
  expect(adapted.activityPreview).toEqual({
117
118
  state: 'completed',
118
119
  replyText: 'Plan is ready',
119
- statusText: 'Tool call completed',
120
+ statusKind: 'tool-completed',
121
+ statusText: 'read_file',
120
122
  timestamp: '2026-05-16T01:00:00.000Z',
121
123
  });
122
124
  });
@@ -1,6 +1,17 @@
1
- import type { SessionEntryView } from '@/shared/lib/api';
1
+ import type { SessionActivityPreviewView, SessionEntryView } from '@/shared/lib/api';
2
2
  import { getLanguage, getLocale, t, type I18nLanguage } from '@/shared/lib/i18n';
3
3
 
4
+ const LEGACY_ACTIVITY_STATUS_LABELS: Array<[
5
+ string,
6
+ NonNullable<SessionActivityPreviewView['statusKind']>
7
+ ]> = [
8
+ ['Thinking', 'thinking'],
9
+ ['Calling tool', 'tool-running'],
10
+ ['Tool call completed', 'tool-completed'],
11
+ ['Run failed', 'run-failed'],
12
+ ['Run interrupted', 'run-interrupted']
13
+ ];
14
+
4
15
  export function sessionDisplayName(session: SessionEntryView): string {
5
16
  const label = session.label?.trim();
6
17
  if (label) {
@@ -10,21 +21,46 @@ export function sessionDisplayName(session: SessionEntryView): string {
10
21
  return chunks[chunks.length - 1] || session.key;
11
22
  }
12
23
 
13
- export function sessionActivityPreviewText(session: SessionEntryView): string | null {
14
- const preview = session.activityPreview;
15
- if (!preview) {
16
- return null;
24
+ function formatSessionActivityStatus(
25
+ preview: SessionActivityPreviewView,
26
+ lang: I18nLanguage
27
+ ): string | null {
28
+ const detail = preview.statusText?.trim();
29
+ const legacyStatus = preview.statusKind || !detail
30
+ ? undefined
31
+ : LEGACY_ACTIVITY_STATUS_LABELS.find(([label]) => detail === label || detail.startsWith(`${label}: `));
32
+ const statusKind = preview.statusKind ?? legacyStatus?.[1];
33
+ const localizedDetail = legacyStatus
34
+ ? detail?.slice(legacyStatus[0].length).replace(/^:\s*/, '') || undefined
35
+ : detail;
36
+ switch (statusKind) {
37
+ case 'thinking':
38
+ return t('chatTyping', lang);
39
+ case 'tool-running':
40
+ return [t('chatToolCall', lang), t('chatToolStatusRunning', lang), localizedDetail].filter(Boolean).join(' · ');
41
+ case 'tool-completed':
42
+ return [t('chatToolCall', lang), t('chatToolStatusCompleted', lang), localizedDetail].filter(Boolean).join(' · ');
43
+ case 'run-failed':
44
+ return [t('chatToolStatusFailed', lang), localizedDetail].filter(Boolean).join(' · ');
45
+ case 'run-interrupted':
46
+ return t('chatSessionActivityInterrupted', lang);
47
+ default:
48
+ return localizedDetail ?? null;
17
49
  }
18
- if (preview.state === 'cancelled') {
50
+ }
51
+
52
+ export function sessionActivityPreviewText(
53
+ session: SessionEntryView,
54
+ lang: I18nLanguage = getLanguage()
55
+ ): string | null {
56
+ const preview = session.activityPreview;
57
+ if (!preview || preview.state === 'cancelled') {
19
58
  return null;
20
59
  }
21
- if (preview.state === 'failed' || preview.state === 'running') {
22
- return preview.statusText ?? preview.replyText ?? null;
23
- }
24
- if (preview.state === 'completed') {
25
- return preview.replyText ?? preview.statusText ?? null;
60
+ if (preview.state === 'completed' && preview.replyText) {
61
+ return preview.replyText;
26
62
  }
27
- return preview.statusText ?? preview.replyText ?? null;
63
+ return formatSessionActivityStatus(preview, lang) ?? preview.replyText ?? null;
28
64
  }
29
65
 
30
66
  function startOfLocalDate(date: Date): number {
@@ -21,6 +21,13 @@ const SESSION_ACTIVITY_PREVIEW_STATE_SET = new Set<SessionActivityPreviewView['s
21
21
  'cancelled',
22
22
  'idle'
23
23
  ]);
24
+ const SESSION_ACTIVITY_PREVIEW_STATUS_KIND_SET = new Set<NonNullable<SessionActivityPreviewView['statusKind']>>([
25
+ 'thinking',
26
+ 'tool-running',
27
+ 'tool-completed',
28
+ 'run-failed',
29
+ 'run-interrupted'
30
+ ]);
24
31
 
25
32
  function stringifyUnknown(value: unknown): string {
26
33
  if (typeof value === 'string') {
@@ -66,13 +73,15 @@ function readNcpSessionActivityPreview(summary: NcpSessionSummaryView): SessionA
66
73
  if (!SESSION_ACTIVITY_PREVIEW_STATE_SET.has(state as SessionActivityPreviewView['state']) || !timestamp) {
67
74
  return null;
68
75
  }
69
- const statusText = readOptionalString(previewRecord.statusText);
70
- const replyText = readOptionalString(previewRecord.replyText);
76
+ const rawStatusKind = readOptionalString(previewRecord.statusKind);
71
77
  return {
72
78
  state: state as SessionActivityPreviewView['state'],
73
79
  timestamp,
74
- ...(statusText ? { statusText } : {}),
75
- ...(replyText ? { replyText } : {})
80
+ statusKind: SESSION_ACTIVITY_PREVIEW_STATUS_KIND_SET.has(rawStatusKind as NonNullable<SessionActivityPreviewView['statusKind']>)
81
+ ? rawStatusKind as NonNullable<SessionActivityPreviewView['statusKind']>
82
+ : undefined,
83
+ statusText: readOptionalString(previewRecord.statusText) ?? undefined,
84
+ replyText: readOptionalString(previewRecord.replyText) ?? undefined
76
85
  };
77
86
  }
78
87
 
@@ -79,6 +79,12 @@ vi.mock("@/shared/hooks/use-agents", () => ({
79
79
  }),
80
80
  }));
81
81
 
82
+ vi.mock("@/shared/hooks/use-projects", () => ({
83
+ useProjects: () => ({
84
+ data: { projects: [] },
85
+ }),
86
+ }));
87
+
82
88
  function createFetchedQuery<TData>(data: TData) {
83
89
  return {
84
90
  data,
@@ -50,21 +50,24 @@ describe('ChatWelcome', () => {
50
50
  expect(screen.queryByText('Current Agent:')).toBeNull();
51
51
  });
52
52
 
53
- it('uses an adaptive capability grid for narrow docked layouts', () => {
53
+ it('renders a single-purpose welcome surface with compact prompt suggestions', () => {
54
54
  renderWelcome({ agents: [{ id: 'main', displayName: 'Main' }] });
55
55
 
56
- expect(
57
- screen.getByRole('button', { name: /Smart Conversations/ }).parentElement
58
- ?.className,
59
- ).toContain('auto-fit');
56
+ expect(screen.getByRole('heading', { name: 'What would you like to get done?' })).toBeTruthy();
57
+ expect(screen.queryByText(/Describe the goal/)).toBeNull();
58
+ expect(screen.getByRole('group', { name: 'Task context' })).toBeTruthy();
59
+ const suggestion = screen.getByRole('button', { name: 'Review this project' });
60
+ expect(suggestion.className).toContain('rounded-full');
61
+ expect(suggestion.className).not.toContain('border');
62
+ expect(suggestion.parentElement?.className).toContain('flex-wrap');
60
63
  });
61
64
 
62
- it('fills the composer with a prompt suggestion when clicking a capability card', () => {
65
+ it('fills the composer with a prompt suggestion when clicking a suggestion', () => {
63
66
  const onSelectPrompt = vi.fn();
64
67
 
65
68
  renderWelcome({ onSelectPrompt });
66
69
 
67
- fireEvent.click(screen.getByRole('button', { name: /Smart Conversations/ }));
70
+ fireEvent.click(screen.getByRole('button', { name: 'Review this project' }));
68
71
 
69
72
  expect(onSelectPrompt).toHaveBeenCalledWith(
70
73
  expect.stringContaining('next three concrete things'),
@@ -28,7 +28,7 @@ export function ChatWelcomeAgentPicker({
28
28
  <Select value={selectedAgentId} onValueChange={onSelectAgent}>
29
29
  <SelectTrigger
30
30
  aria-label={t('chatDraftAgentTitle')}
31
- className="h-auto w-auto min-w-0 gap-1 rounded-lg border-0 bg-transparent px-2 py-1.5 text-muted-foreground shadow-none hover:bg-accent hover:text-accent-foreground focus:ring-0"
31
+ className="h-auto w-auto min-w-0 gap-1 rounded-lg border-0 bg-transparent px-2 py-1.5 text-muted-foreground shadow-none hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus:ring-0"
32
32
  >
33
33
  <span className="sr-only">{t('chatDraftAgentTitle')}</span>
34
34
  <div className="flex min-w-0 items-center gap-1.5">
@@ -11,7 +11,6 @@ import { t } from '@/shared/lib/i18n';
11
11
  import { getSessionProjectName } from '@/shared/lib/session-project';
12
12
 
13
13
  type ChatWelcomeProjectPickerProps = {
14
- defaultProjectRoot?: string | null;
15
14
  isSaving: boolean;
16
15
  projectOptions: readonly ChatWelcomeProjectOption[];
17
16
  projectRoot: string | null;
@@ -23,7 +22,6 @@ type ChatWelcomeProjectPickerProps = {
23
22
  const PROJECT_PICKER_MAX_HEIGHT = createPopoverAvailableHeightLimit('20rem');
24
23
 
25
24
  export function ChatWelcomeProjectPicker({
26
- defaultProjectRoot,
27
25
  isSaving,
28
26
  projectOptions,
29
27
  projectRoot,
@@ -34,10 +32,6 @@ export function ChatWelcomeProjectPicker({
34
32
  const [isOpen, setIsOpen] = useState(false);
35
33
  const projectLabel =
36
34
  getSessionProjectName(projectRoot) ?? t('chatWelcomeProjectPickerPlaceholder');
37
- const isDefaultProject =
38
- defaultProjectRoot !== null &&
39
- defaultProjectRoot !== undefined &&
40
- projectRoot === defaultProjectRoot;
41
35
 
42
36
  const selectProjectRoot = async (nextProjectRoot: string) => {
43
37
  setIsOpen(false);
@@ -49,18 +43,13 @@ export function ChatWelcomeProjectPicker({
49
43
  <PopoverTrigger asChild>
50
44
  <button
51
45
  type="button"
52
- className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
46
+ className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
53
47
  title={projectRoot ?? undefined}
54
48
  aria-label={t('chatWelcomeProjectPickerLabel')}
55
49
  disabled={!selectable || isSaving}
56
50
  >
57
51
  <FolderOpen className="h-4 w-4 shrink-0" />
58
52
  <span className="truncate">{projectLabel}</span>
59
- {isDefaultProject ? (
60
- <span className="shrink-0 text-xs font-normal text-muted-foreground/70">
61
- {t('chatWelcomeProjectDefaultBadge')}
62
- </span>
63
- ) : null}
64
53
  <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" />
65
54
  </button>
66
55
  </PopoverTrigger>
@@ -78,7 +67,7 @@ export function ChatWelcomeProjectPicker({
78
67
  <button
79
68
  key={option.projectRoot}
80
69
  type="button"
81
- className="flex w-full min-w-0 items-start gap-2 rounded-xl px-2 py-2 text-left transition-colors hover:bg-accent"
70
+ className="flex w-full min-w-0 items-start gap-2 rounded-xl px-2 py-2 text-left transition-colors hover:bg-[var(--interaction-hover)]"
82
71
  title={option.projectRoot}
83
72
  onClick={() => {
84
73
  void selectProjectRoot(option.projectRoot);
@@ -111,7 +100,7 @@ export function ChatWelcomeProjectPicker({
111
100
  <div className="shrink-0 border-t border-border p-1.5">
112
101
  <button
113
102
  type="button"
114
- className="flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left text-[13px] font-semibold text-foreground transition-colors hover:bg-accent"
103
+ className="flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left text-[13px] font-semibold text-foreground transition-colors hover:bg-[var(--interaction-hover)]"
115
104
  onClick={() => {
116
105
  setIsOpen(false);
117
106
  onOpenProjectDialog();
@@ -0,0 +1,37 @@
1
+ import { t } from '@/shared/lib/i18n';
2
+
3
+ const WELCOME_PROMPT_SUGGESTIONS = [
4
+ {
5
+ labelKey: 'chatWelcomeSuggestion1Label' as const,
6
+ promptKey: 'chatWelcomeSuggestion1Prompt' as const,
7
+ },
8
+ {
9
+ labelKey: 'chatWelcomeSuggestion2Label' as const,
10
+ promptKey: 'chatWelcomeSuggestion2Prompt' as const,
11
+ },
12
+ {
13
+ labelKey: 'chatWelcomeSuggestion3Label' as const,
14
+ promptKey: 'chatWelcomeSuggestion3Prompt' as const,
15
+ },
16
+ ];
17
+
18
+ export function ChatWelcomePromptSuggestions({
19
+ onSelectPrompt,
20
+ }: {
21
+ onSelectPrompt: (prompt: string) => void;
22
+ }) {
23
+ return (
24
+ <div className="mt-5 flex flex-wrap justify-center gap-2">
25
+ {WELCOME_PROMPT_SUGGESTIONS.map((suggestion) => (
26
+ <button
27
+ key={suggestion.labelKey}
28
+ type="button"
29
+ onClick={() => onSelectPrompt(t(suggestion.promptKey))}
30
+ className="rounded-full bg-muted/55 px-3.5 py-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
31
+ >
32
+ {t(suggestion.labelKey)}
33
+ </button>
34
+ ))}
35
+ </div>
36
+ );
37
+ }
@@ -39,7 +39,7 @@ export function ChatWelcomeSessionTypePicker({
39
39
  <PopoverTrigger asChild>
40
40
  <button
41
41
  type="button"
42
- className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
42
+ className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
43
43
  aria-label={t('chatWelcomeSessionTypePickerLabel')}
44
44
  >
45
45
  {selectedOption.icon?.src ? (
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import { useState } from 'react';
3
3
  import { ChatSessionProjectDialog } from '@/features/chat/features/session/components/session-header/chat-session-project-dialog';
4
4
  import { ChatWelcomeAgentPicker } from '@/features/chat/features/welcome/components/chat-welcome-agent-picker';
5
- import { ChatWelcomeCapabilityGrid } from '@/features/chat/features/welcome/components/chat-welcome-capability-grid';
5
+ import { ChatWelcomePromptSuggestions } from '@/features/chat/features/welcome/components/chat-welcome-prompt-suggestions';
6
6
  import { ChatWelcomeProjectPicker } from '@/features/chat/features/welcome/components/chat-welcome-project-picker';
7
7
  import { ChatWelcomeSessionTypePicker } from '@/features/chat/features/welcome/components/chat-welcome-session-type-picker';
8
8
  import type { ChatWelcomeProjectOption } from '@/features/chat/features/welcome/utils/chat-welcome-project-options.utils';
@@ -61,45 +61,47 @@ export function ChatWelcome({
61
61
  const selectableProjectRoot = Boolean(onSelectProjectRoot);
62
62
 
63
63
  return (
64
- <div className="flex min-h-full items-center justify-center px-4 py-8 sm:p-8">
65
- <div className="min-w-0 w-full max-w-[min(760px,100%)]">
64
+ <div className="flex min-h-full items-center justify-center px-4 py-10 sm:p-8">
65
+ <div className="w-full min-w-0 max-w-[min(680px,100%)]">
66
66
  <div className="text-center">
67
- <h2 className="text-2xl font-semibold text-foreground sm:text-3xl">
67
+ <h2 className="text-2xl font-semibold leading-tight text-foreground sm:text-[2rem]">
68
68
  {t('chatWelcomeTitle')}
69
69
  </h2>
70
- <p className="mt-3 text-sm font-medium text-muted-foreground sm:text-base">
71
- {t('chatWelcomeSubtitle')}
72
- </p>
73
70
  </div>
74
71
 
75
- {inputSlot ? <div className="mt-8">{inputSlot}</div> : null}
72
+ {inputSlot ? <div className="mt-6">{inputSlot}</div> : null}
76
73
 
77
- <div className="mt-3 flex flex-wrap items-center gap-2 px-1 text-sm text-muted-foreground">
78
- {onSelectProjectRoot ? (
79
- <ChatWelcomeProjectPicker
80
- defaultProjectRoot={defaultProjectRoot}
81
- isSaving={isProjectSaving}
82
- projectOptions={projectOptions}
83
- projectRoot={resolvedProjectRoot}
84
- selectable={selectableProjectRoot}
85
- onOpenProjectDialog={() => setIsProjectDialogOpen(true)}
86
- onSelectProjectRoot={saveProjectRoot}
74
+ <div className="mt-2 flex justify-center px-1">
75
+ <div
76
+ role="group"
77
+ aria-label={t('chatWelcomeContextLabel')}
78
+ className="flex min-w-0 max-w-full flex-wrap items-center justify-center gap-0.5 rounded-xl bg-muted/40 p-1 text-sm text-muted-foreground"
79
+ >
80
+ {onSelectProjectRoot ? (
81
+ <ChatWelcomeProjectPicker
82
+ isSaving={isProjectSaving}
83
+ projectOptions={projectOptions}
84
+ projectRoot={resolvedProjectRoot}
85
+ selectable={selectableProjectRoot}
86
+ onOpenProjectDialog={() => setIsProjectDialogOpen(true)}
87
+ onSelectProjectRoot={saveProjectRoot}
88
+ />
89
+ ) : null}
90
+ <ChatWelcomeAgentPicker
91
+ agents={agents}
92
+ selectedAgent={selectedAgent}
93
+ selectedAgentId={selectedAgentId}
94
+ onSelectAgent={onSelectAgent}
87
95
  />
88
- ) : null}
89
- <ChatWelcomeAgentPicker
90
- agents={agents}
91
- selectedAgent={selectedAgent}
92
- selectedAgentId={selectedAgentId}
93
- onSelectAgent={onSelectAgent}
94
- />
95
- <ChatWelcomeSessionTypePicker
96
- options={sessionTypeOptions}
97
- selectedSessionType={selectedSessionType}
98
- onSelectSessionType={onSelectSessionType}
99
- />
96
+ <ChatWelcomeSessionTypePicker
97
+ options={sessionTypeOptions}
98
+ selectedSessionType={selectedSessionType}
99
+ onSelectSessionType={onSelectSessionType}
100
+ />
101
+ </div>
100
102
  </div>
101
103
 
102
- <ChatWelcomeCapabilityGrid onSelectPrompt={onSelectPrompt} />
104
+ <ChatWelcomePromptSuggestions onSelectPrompt={onSelectPrompt} />
103
105
  </div>
104
106
 
105
107
  {onSelectProjectRoot ? (
@@ -113,7 +113,7 @@ it("shows all session workspace entries in the overview", async () => {
113
113
  name: /Child sessions/,
114
114
  });
115
115
  const cronJobsButton = screen.getByRole("button", {
116
- name: /Session cron jobs/,
116
+ name: /Scheduled tasks/,
117
117
  });
118
118
 
119
119
  expect((childSessionsButton as HTMLButtonElement).disabled).toBe(false);
@@ -96,7 +96,7 @@ describe("ChatSessionWorkspacePanel", () => {
96
96
  await user.click(screen.getByRole("button", { name: "Refresh preview" }));
97
97
 
98
98
  expect(mocks.invalidateQueries).toHaveBeenCalledWith({
99
- queryKey: ["server-path-read", "README.md", null],
99
+ queryKey: ["server-path-read", "README.md", null, null],
100
100
  });
101
101
  expect(mocks.invalidateQueries).toHaveBeenCalledWith({
102
102
  queryKey: ["server-path-browse", "README.md", "", true],
@@ -4,6 +4,7 @@ export { ChatPresenterProvider } from "./components/providers/chat-presenter.pro
4
4
  export { usePresenter } from "./components/providers/chat-presenter.provider";
5
5
  export { ChatPresenter } from "./presenters/chat.presenter";
6
6
  export { ChatDraftIntentManager } from "./managers/chat-draft-intent.manager";
7
+ export { ChatCompletionNotificationManager } from "./managers/chat-completion-notification.manager";
7
8
  export { useChatSessionListStore } from "./stores/chat-session-list.store";
8
9
  export { useChatThreadStore } from "./stores/chat-thread.store";
9
10
  export { useChatMessageLayoutStore } from "./stores/chat-message-layout.store";