@nextclaw/ui 0.14.4 → 0.15.0

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 (194) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/assets/api-DLtsiBKm.js +13 -0
  3. package/dist/assets/appearance-settings-page-CtkeYXux.js +1 -0
  4. package/dist/assets/book-open-BQwWBDKb.js +1 -0
  5. package/dist/assets/channels-list-page-Cv8o9Hxg.js +8 -0
  6. package/dist/assets/chat-page-BtA7H1Ea.js +1 -0
  7. package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-EidRBpP2.js} +1 -1
  8. package/dist/assets/confirm-dialog-iIJFFp2T.js +5 -0
  9. package/dist/assets/desktop-update-config-C6n_TB1r.js +1 -0
  10. package/dist/assets/{dist-CikkhOlq.js → dist-CBxWOazz.js} +1 -1
  11. package/dist/assets/{dist-PVA_xL1D.js → dist-D2eZC3OR.js} +1 -1
  12. package/dist/assets/doc-browser-B76XOXmW.js +1 -0
  13. package/dist/assets/doc-browser-DBnj9UMB.js +1 -0
  14. package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-Sv-044Cl.js} +1 -1
  15. package/dist/assets/{ellipsis-y6vDtq4M.js → ellipsis-2uzhypw5.js} +1 -1
  16. package/dist/assets/es2015-DhQo_oS6.js +41 -0
  17. package/dist/assets/{external-link-DVJIQf_G.js → external-link-CM5dnUI6.js} +1 -1
  18. package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
  19. package/dist/assets/i18n-provider-di1waTGX.js +1 -0
  20. package/dist/assets/index-CBRP-7-a.css +1 -0
  21. package/dist/assets/index-Z1fYpbNd.js +104 -0
  22. package/dist/assets/loader-circle-BjmN5DeJ.js +1 -0
  23. package/dist/assets/mcp-marketplace-page-C_MVQUbT.js +9 -0
  24. package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +1 -0
  25. package/dist/assets/model-config-page-Cdr9P6TQ.js +1 -0
  26. package/dist/assets/plus-3ztRZD_l.js +1 -0
  27. package/dist/assets/provider-scoped-model-input-g1WkGiJ8.js +1 -0
  28. package/dist/assets/providers-config-page-CWlO7J4A.js +1 -0
  29. package/dist/assets/{react-VNqT5Zp7.js → react-DjpzoZXB.js} +1 -1
  30. package/dist/assets/refresh-cw-DaTv3zxE.js +1 -0
  31. package/dist/assets/remote-CKWZfVVB.js +1 -0
  32. package/dist/assets/rotate-cw-CHcaRUDz.js +1 -0
  33. package/dist/assets/runtime-config-page-CfTqh1nv.js +1 -0
  34. package/dist/assets/{save-BOJ477H-.js → save-BZof1uie.js} +1 -1
  35. package/dist/assets/{search-BEti5beA.js → search-MxAarS_g.js} +1 -1
  36. package/dist/assets/search-config-page-Dm8daYny.js +1 -0
  37. package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-D8AbcR05.js} +2 -2
  38. package/dist/assets/security-config-Ds0LsJmo.js +1 -0
  39. package/dist/assets/select-D7GcNP0z.js +1 -0
  40. package/dist/assets/setting-row-CuozlnJd.js +1 -0
  41. package/dist/assets/side-dock-BcLau5S_.js +3 -0
  42. package/dist/assets/skeleton-shfmXuYp.js +1 -0
  43. package/dist/assets/switch-C1BWMU5c.js +1 -0
  44. package/dist/assets/tag-chip-DCUrQUCP.js +1 -0
  45. package/dist/assets/tooltip-DfAHHA76.js +1 -0
  46. package/dist/assets/use-config-DAdbT5QJ.js +1 -0
  47. package/dist/assets/use-confirm-dialog-eWcJ6V-s.js +1 -0
  48. package/dist/assets/x-DqL1mYLC.js +1 -0
  49. package/dist/index.html +53 -17
  50. package/index.html +25 -2
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
  53. package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
  54. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
  55. package/src/app/components/layout/app-layout.tsx +10 -3
  56. package/src/app/components/layout/sidebar-items.tsx +8 -8
  57. package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
  58. package/src/app/components/layout/sidebar.tsx +21 -28
  59. package/src/app/configs/app-navigation.config.ts +11 -5
  60. package/src/app/index.tsx +8 -0
  61. package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
  62. package/src/app/managers/viewport-layout.manager.ts +39 -0
  63. package/src/app/presenters/app.presenter.ts +25 -2
  64. package/src/app/styles/design-system.css +91 -4
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
  66. package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
  68. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
  69. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
  70. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +51 -2
  71. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
  72. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
  73. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
  74. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
  75. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
  76. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
  77. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
  78. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
  79. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
  80. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
  81. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
  82. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
  83. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +166 -0
  84. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
  85. package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
  86. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
  87. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
  88. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
  89. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
  90. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
  91. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
  92. package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
  93. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +39 -0
  94. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  95. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
  96. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
  97. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +270 -115
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +70 -5
  102. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
  103. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +79 -1
  104. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
  105. package/src/features/chat/managers/chat-thread.manager.ts +9 -3
  106. package/src/features/chat/managers/chat-ui.manager.ts +5 -1
  107. package/src/features/chat/presenters/chat.presenter.ts +2 -1
  108. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  109. package/src/features/chat/stores/chat-thread.store.ts +6 -64
  110. package/src/features/chat/types/chat-message.types.ts +4 -0
  111. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
  112. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
  113. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  114. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
  116. package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
  117. package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
  118. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
  119. package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
  120. package/src/features/marketplace/index.ts +3 -0
  121. package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
  122. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
  123. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
  124. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
  125. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
  126. package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
  127. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
  128. package/src/features/side-dock/components/side-dock.tsx +100 -13
  129. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
  130. package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
  131. package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
  132. package/src/features/side-dock/stores/side-dock.store.ts +9 -0
  133. package/src/features/side-dock/types/side-dock.types.ts +10 -3
  134. package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
  135. package/src/index.css +501 -27
  136. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  137. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
  138. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
  139. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
  140. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
  141. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
  142. package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
  143. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
  144. package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
  145. package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
  146. package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
  147. package/src/shared/components/ui/dialog.tsx +2 -2
  148. package/src/shared/components/ui/popover.tsx +1 -1
  149. package/src/shared/components/ui/select.tsx +1 -1
  150. package/src/shared/components/ui/switch.tsx +4 -3
  151. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
  152. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
  153. package/src/shared/components/ui/tooltip.tsx +1 -1
  154. package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
  155. package/src/shared/lib/i18n/locales/en-US/chat.json +15 -6
  156. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  157. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
  158. package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
  159. package/src/shared/lib/i18n/locales/zh-CN/chat.json +15 -6
  160. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  161. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
  162. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
  163. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
  164. package/src/shared/lib/session-project/index.ts +1 -1
  165. package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
  166. package/src/shared/lib/theme/index.ts +51 -13
  167. package/dist/assets/api-BP_8IaxB.js +0 -13
  168. package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
  169. package/dist/assets/chat-page-BRB0N5WW.js +0 -105
  170. package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
  171. package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
  172. package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
  173. package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
  174. package/dist/assets/index-BiE-K8-q.css +0 -1
  175. package/dist/assets/index-D9BAE_L_.js +0 -105
  176. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
  177. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
  178. package/dist/assets/model-config-page-DotaTUHb.js +0 -1
  179. package/dist/assets/plus-BbZqquJe.js +0 -1
  180. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
  181. package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
  182. package/dist/assets/remote-B8TiUfwW.js +0 -1
  183. package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
  184. package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
  185. package/dist/assets/search-config-page-DGj5am03.js +0 -1
  186. package/dist/assets/select-hoj8dK4K.js +0 -41
  187. package/dist/assets/tag-chip-T8-h-die.js +0 -1
  188. package/dist/assets/use-config-CjRV2hwL.js +0 -1
  189. package/dist/assets/x-D9PXs_xP.js +0 -1
  190. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
  191. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
  192. package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
  193. /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
  194. /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
@@ -8,17 +8,20 @@ import {
8
8
  createInlinePanelAppCardUrl,
9
9
  createFallbackPanelAppContentPath,
10
10
  createInlinePanelAppTab,
11
- PANEL_APP_INLINE_CARD_MAX_HEIGHT_PX,
12
11
  PANEL_APP_INLINE_IFRAME_SANDBOX,
13
12
  } from '@/features/chat/features/message/utils/chat-inline-panel-app-card.utils';
14
13
  import { useDocBrowser } from '@/shared/components/doc-browser';
15
14
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
16
15
  import { t } from '@/shared/lib/i18n';
17
16
 
17
+ type ChatInlinePanelAppDescriptor = Pick<ChatPanelAppCardViewModel, 'appId' | 'title'>;
18
+
18
19
  export function ChatInlinePanelAppCard({
19
20
  panelApp,
21
+ showExpandAction = true,
20
22
  }: {
21
- panelApp: ChatPanelAppCardViewModel;
23
+ panelApp: ChatInlinePanelAppDescriptor;
24
+ showExpandAction?: boolean;
22
25
  }) {
23
26
  const docBrowser = useDocBrowser();
24
27
  const iframeRef = useRef<HTMLIFrameElement>(null);
@@ -77,25 +80,30 @@ export function ChatInlinePanelAppCard({
77
80
  if (!url) {
78
81
  return (
79
82
  <div
80
- className="h-[320px] min-h-[220px] w-full animate-pulse rounded-lg border border-border bg-muted/45"
81
- style={{ maxHeight: PANEL_APP_INLINE_CARD_MAX_HEIGHT_PX }}
83
+ className="h-[320px] min-h-[220px] w-full max-w-[48rem] animate-pulse rounded-lg border border-border bg-muted/45"
84
+ data-chat-message-wide-content="true"
82
85
  />
83
86
  );
84
87
  }
85
88
 
86
89
  return (
87
- <div className="w-full max-w-[42rem] overflow-hidden rounded-lg border border-border bg-card shadow-sm">
90
+ <div
91
+ className="w-full max-w-[48rem] overflow-hidden rounded-lg border border-border bg-card shadow-sm"
92
+ data-chat-message-wide-content="true"
93
+ >
88
94
  <div className="flex h-9 items-center justify-between gap-2 border-b border-border bg-muted/45 px-2.5">
89
95
  <div className="flex min-w-0 items-center gap-2 text-xs font-medium text-foreground">
90
96
  <AppWindow className="h-3.5 w-3.5 shrink-0 text-primary" />
91
97
  <span className="truncate">{title}</span>
92
98
  </div>
93
- <IconActionButton
94
- className="h-7 w-7 rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
95
- icon={<Maximize2 className="h-3.5 w-3.5" />}
96
- label={t('chatPanelCardExpand')}
97
- onClick={openExpanded}
98
- />
99
+ {showExpandAction ? (
100
+ <IconActionButton
101
+ className="h-7 w-7 rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
102
+ icon={<Maximize2 className="h-3.5 w-3.5" />}
103
+ label={t('chatPanelCardExpand')}
104
+ onClick={openExpanded}
105
+ />
106
+ ) : null}
99
107
  </div>
100
108
  <div
101
109
  className="relative h-[420px] min-h-[220px] max-h-[min(60vh,420px)] overflow-hidden bg-card"
@@ -1,15 +1,20 @@
1
1
  import { useMemo } from "react";
2
2
  import type { NcpMessage } from "@nextclaw/ncp";
3
3
  import {
4
+ type ChatInlineDisplayViewModel,
4
5
  type ChatMessageViewModel,
5
6
  ChatMessageList,
6
7
  } from "@nextclaw/agent-chat-ui";
7
8
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
8
9
  import {
9
10
  adaptChatMessage,
10
- type ChatMessageAdapterTexts,
11
11
  type ChatMessageSource,
12
12
  } from "@/features/chat/features/message/utils/chat-message.utils";
13
+ import { buildChatMessageProcessSummary } from "@/features/chat/features/message/utils/chat-message-process-summary.utils";
14
+ import {
15
+ buildChatMessageAdapterTexts,
16
+ buildChatMessageTexts,
17
+ } from "@/features/chat/features/message/utils/chat-message-texts.utils";
13
18
  import { readInlineTokensFromMetadata } from "@/features/chat/features/input/utils/chat-inline-token.utils";
14
19
  import { adaptNcpMessageToUiMessage } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
15
20
  import {
@@ -31,9 +36,27 @@ const INHERITED_FROM_SESSION_METADATA_KEY = "inherited_from_session_id";
31
36
 
32
37
  const messageViewModelCache = new WeakMap<
33
38
  NcpMessage,
34
- { language: string; viewModel: ChatMessageViewModel }
39
+ {
40
+ language: string;
41
+ processSummaryLabel: string | null;
42
+ viewModel: ChatMessageViewModel;
43
+ }
35
44
  >();
36
45
 
46
+ function renderChatInlineDisplay(display: ChatInlineDisplayViewModel) {
47
+ if (display.target.type !== "panel_app") {
48
+ return undefined;
49
+ }
50
+ return (
51
+ <ChatInlinePanelAppCard
52
+ panelApp={{
53
+ appId: display.target.payload.appId,
54
+ title: display.title,
55
+ }}
56
+ />
57
+ );
58
+ }
59
+
37
60
  type ContextInheritanceTimelineView = {
38
61
  sourceSessionId: string;
39
62
  inheritedMessageCount: number;
@@ -60,61 +83,6 @@ type ChatTimelineItem =
60
83
  inheritance: ContextInheritanceTimelineView;
61
84
  };
62
85
 
63
- function buildChatMessageAdapterTexts(
64
- language: string,
65
- ): ChatMessageAdapterTexts {
66
- void language;
67
- return {
68
- roleLabels: {
69
- user: t("chatRoleUser"),
70
- assistant: t("chatRoleAssistant"),
71
- tool: t("chatRoleTool"),
72
- system: t("chatRoleSystem"),
73
- fallback: t("chatRoleMessage"),
74
- },
75
- reasoningLabel: t("chatReasoning"),
76
- toolCallLabel: t("chatToolCall"),
77
- toolResultLabel: t("chatToolResult"),
78
- toolInputLabel: t("chatToolInput"),
79
- toolNoOutputLabel: t("chatToolNoOutput"),
80
- toolOutputLabel: t("chatToolOutput"),
81
- toolStatusPreparingLabel: t("chatToolStatusPreparing"),
82
- toolStatusRunningLabel: t("chatToolStatusRunning"),
83
- toolStatusCompletedLabel: t("chatToolStatusCompleted"),
84
- toolStatusFailedLabel: t("chatToolStatusFailed"),
85
- toolStatusCancelledLabel: t("chatToolStatusCancelled"),
86
- showContentActionLabel: t("chatShowContentAction"),
87
- imageAttachmentLabel: t("chatImageAttachment"),
88
- fileAttachmentLabel: t("chatFileAttachment"),
89
- unknownPartLabel: t("chatUnknownPart"),
90
- };
91
- }
92
-
93
- function buildChatMessageTexts(language: string) {
94
- void language;
95
- return {
96
- copyCodeLabel: t("chatCodeCopy"),
97
- copiedCodeLabel: t("chatCodeCopied"),
98
- copyMessageLabel: t("chatMessageCopy"),
99
- copiedMessageLabel: t("chatMessageCopied"),
100
- typingLabel: t("chatTyping"),
101
- attachmentOpenLabel: t("chatAttachmentOpen"),
102
- attachmentAttachedLabel: t("chatAttachmentAttached"),
103
- attachmentCategoryLabels: {
104
- archive: t("chatAttachmentCategoryArchive"),
105
- audio: t("chatAttachmentCategoryAudio"),
106
- code: t("chatAttachmentCategoryCode"),
107
- data: t("chatAttachmentCategoryData"),
108
- document: t("chatAttachmentCategoryDocument"),
109
- generic: t("chatAttachmentCategoryGeneric"),
110
- image: t("chatAttachmentCategoryImage"),
111
- pdf: t("chatAttachmentCategoryPdf"),
112
- sheet: t("chatAttachmentCategorySheet"),
113
- video: t("chatAttachmentCategoryVideo"),
114
- },
115
- };
116
- }
117
-
118
86
  function ChatContextCompactionDivider({
119
87
  checkpoint,
120
88
  }: {
@@ -316,18 +284,22 @@ export function ChatMessageListContainer({
316
284
  }: ChatMessageListContainerProps) {
317
285
  const presenter = usePresenter();
318
286
  const { language } = useI18n();
319
- const texts = useMemo<ChatMessageAdapterTexts>(
287
+ const texts = useMemo(
320
288
  () => buildChatMessageAdapterTexts(language),
321
289
  [language],
322
290
  );
323
291
 
324
292
  const messages = useMemo(() => {
325
- return rawMessages.flatMap((message) => {
326
- if (!isVisibleChatMessage(message)) {
327
- return [];
328
- }
293
+ const visibleRawMessages = rawMessages.filter(isVisibleChatMessage);
294
+ const processedLabel = t("chatProcessSummaryProcessed");
295
+ return visibleRawMessages.flatMap((message) => {
296
+ const processSummary = buildChatMessageProcessSummary({
297
+ message,
298
+ processedLabel,
299
+ });
300
+ const processSummaryLabel = processSummary?.label ?? null;
329
301
  const cached = messageViewModelCache.get(message);
330
- if (cached && cached.language === language) {
302
+ if (cached && cached.language === language && cached.processSummaryLabel === processSummaryLabel) {
331
303
  return [cached.viewModel];
332
304
  }
333
305
 
@@ -339,6 +311,7 @@ export function ChatMessageListContainer({
339
311
  timestamp: uiMessage.meta?.timestamp,
340
312
  status: uiMessage.meta?.status,
341
313
  inlineTokens: readInlineTokensFromMetadata(message.metadata),
314
+ processSummary,
342
315
  },
343
316
  parts: uiMessage.parts as unknown as ChatMessageSource["parts"],
344
317
  };
@@ -347,7 +320,7 @@ export function ChatMessageListContainer({
347
320
  texts,
348
321
  });
349
322
 
350
- messageViewModelCache.set(message, { language, viewModel });
323
+ messageViewModelCache.set(message, { language, processSummaryLabel, viewModel });
351
324
  return [viewModel];
352
325
  });
353
326
  }, [language, rawMessages, texts]);
@@ -385,6 +358,7 @@ export function ChatMessageListContainer({
385
358
  texts={messageTexts}
386
359
  onToolAction={presenter.chatThreadManager.handleToolAction}
387
360
  onFileOpen={presenter.chatThreadManager.openFilePreview}
361
+ renderInlineDisplay={renderChatInlineDisplay}
388
362
  renderToolAgent={(agentId) => (
389
363
  <AgentIdentityAvatar
390
364
  agentId={agentId}
@@ -0,0 +1,59 @@
1
+ import type { NcpMessage } from "@nextclaw/ncp";
2
+ import { describe, expect, it } from "vitest";
3
+ import { buildChatMessageProcessSummary } from "@/features/chat/features/message/utils/chat-message-process-summary.utils";
4
+
5
+ const baseAssistantMessage = {
6
+ id: "assistant-1",
7
+ sessionId: "session-1",
8
+ role: "assistant",
9
+ status: "final",
10
+ timestamp: "2026-03-31T10:03:51.000Z",
11
+ parts: [
12
+ {
13
+ type: "reasoning",
14
+ text: "Inspecting current state.",
15
+ },
16
+ {
17
+ type: "tool-invocation",
18
+ toolCallId: "tool-1",
19
+ toolName: "exec_command",
20
+ state: "result",
21
+ args: "{\"cmd\":\"git status\"}",
22
+ result: "clean",
23
+ },
24
+ {
25
+ type: "text",
26
+ text: "Done.",
27
+ },
28
+ ],
29
+ } satisfies NcpMessage;
30
+
31
+ describe("buildChatMessageProcessSummary", () => {
32
+ it("does not invent duration when lifecycle timing is absent", () => {
33
+ expect(
34
+ buildChatMessageProcessSummary({
35
+ message: baseAssistantMessage,
36
+ processedLabel: "Processed",
37
+ }),
38
+ ).toEqual({
39
+ label: "Processed",
40
+ });
41
+ });
42
+
43
+ it("derives duration from lifecycle startedAt and endedAt", () => {
44
+ expect(
45
+ buildChatMessageProcessSummary({
46
+ message: {
47
+ ...baseAssistantMessage,
48
+ lifecycle: {
49
+ startedAt: "2026-03-31T10:00:00.000Z",
50
+ endedAt: "2026-03-31T10:03:51.000Z",
51
+ },
52
+ },
53
+ processedLabel: "Processed",
54
+ }),
55
+ ).toEqual({
56
+ label: "Processed 3m 51s",
57
+ });
58
+ });
59
+ });
@@ -29,6 +29,59 @@ const TEXTS: ChatMessageAdapterTexts = {
29
29
  };
30
30
 
31
31
  describe("buildShowContentToolCard", () => {
32
+ it("builds a show-content action from a normalized show_file result", () => {
33
+ const card = buildShowContentToolCard({
34
+ invocation: {
35
+ status: "result",
36
+ toolCallId: "call-show-file",
37
+ toolName: "show_file",
38
+ result: {
39
+ ok: true,
40
+ action: "showContent",
41
+ request: {
42
+ target: {
43
+ type: "file",
44
+ payload: {
45
+ path: "docs/example.md",
46
+ line: 2,
47
+ viewer: "rendered",
48
+ },
49
+ },
50
+ title: "Example",
51
+ purpose: "read",
52
+ },
53
+ },
54
+ },
55
+ actionLabel: "Show content",
56
+ statusLabel: "Completed",
57
+ });
58
+
59
+ expect(card).toMatchObject({
60
+ kind: "result",
61
+ name: "show_file",
62
+ detail: "Example",
63
+ statusTone: "success",
64
+ statusLabel: "Completed",
65
+ action: {
66
+ kind: "show-content",
67
+ label: "Show content",
68
+ request: {
69
+ target: {
70
+ type: "file",
71
+ payload: {
72
+ path: "docs/example.md",
73
+ line: 2,
74
+ viewer: "rendered",
75
+ },
76
+ },
77
+ title: "Example",
78
+ purpose: "read",
79
+ placement: "side_panel",
80
+ },
81
+ },
82
+ });
83
+ });
84
+
32
85
  it("builds a show-content action from a normalized show_content result", () => {
33
86
  const card = buildShowContentToolCard({
34
87
  invocation: {
@@ -44,6 +97,7 @@ describe("buildShowContentToolCard", () => {
44
97
  payload: {
45
98
  path: "docs/example.md",
46
99
  line: 2,
100
+ viewer: "rendered",
47
101
  },
48
102
  },
49
103
  title: "Example",
@@ -70,6 +124,7 @@ describe("buildShowContentToolCard", () => {
70
124
  payload: {
71
125
  path: "docs/example.md",
72
126
  line: 2,
127
+ viewer: "rendered",
73
128
  },
74
129
  },
75
130
  title: "Example",
@@ -85,7 +140,7 @@ describe("buildShowContentToolCard", () => {
85
140
  invocation: {
86
141
  status: "result",
87
142
  toolCallId: "call-show-content",
88
- toolName: "show_content",
143
+ toolName: "show_panel_app",
89
144
  result: {
90
145
  ok: true,
91
146
  action: "showContent",
@@ -15,8 +15,6 @@ export const PANEL_APP_INLINE_IFRAME_SANDBOX = [
15
15
  'allow-presentation',
16
16
  ].join(' ');
17
17
 
18
- export const PANEL_APP_INLINE_CARD_MAX_HEIGHT_PX = 420;
19
-
20
18
  const PANEL_APP_INLINE_CARD_SEARCH_PARAMS = {
21
19
  nextclawDisplayMode: 'card',
22
20
  nextclawPlacement: 'inline',
@@ -0,0 +1,80 @@
1
+ import type { NcpMessage } from "@nextclaw/ncp";
2
+ import type { ChatMessageProcessSummarySource } from "@/features/chat/types/chat-message.types";
3
+
4
+ type BuildChatMessageProcessSummaryParams = {
5
+ message: NcpMessage;
6
+ processedLabel: string;
7
+ };
8
+
9
+ function isAssistantProcessPart(part: NcpMessage["parts"][number]): boolean {
10
+ return part.type === "reasoning" || part.type === "tool-invocation";
11
+ }
12
+
13
+ function isAssistantFinalContentPart(
14
+ part: NcpMessage["parts"][number],
15
+ ): boolean {
16
+ if (part.type === "text" || part.type === "rich-text") {
17
+ return part.text.trim().length > 0;
18
+ }
19
+ return part.type === "file" || part.type === "source" || part.type === "card";
20
+ }
21
+
22
+ function hasCollapsibleAssistantProcess(message: NcpMessage): boolean {
23
+ if (
24
+ message.role !== "assistant" ||
25
+ message.status === "pending" ||
26
+ message.status === "streaming"
27
+ ) {
28
+ return false;
29
+ }
30
+ let lastProcessPartIndex = -1;
31
+ for (let index = message.parts.length - 1; index >= 0; index -= 1) {
32
+ const part = message.parts[index];
33
+ if (part && isAssistantProcessPart(part)) {
34
+ lastProcessPartIndex = index;
35
+ break;
36
+ }
37
+ }
38
+ return (
39
+ lastProcessPartIndex >= 0 &&
40
+ lastProcessPartIndex < message.parts.length - 1 &&
41
+ message.parts
42
+ .slice(lastProcessPartIndex + 1)
43
+ .some(isAssistantFinalContentPart)
44
+ );
45
+ }
46
+
47
+ function formatLifecycleDuration(message: NcpMessage): string | null {
48
+ const startedAt = message.lifecycle?.startedAt;
49
+ const endedAt = message.lifecycle?.endedAt;
50
+ if (!startedAt || !endedAt) {
51
+ return null;
52
+ }
53
+ const started = Date.parse(startedAt);
54
+ const ended = Date.parse(endedAt);
55
+ if (!Number.isFinite(started) || !Number.isFinite(ended) || ended < started) {
56
+ return null;
57
+ }
58
+ const totalSeconds = Math.round((ended - started) / 1000);
59
+ const hours = Math.floor(totalSeconds / 3600);
60
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
61
+ const seconds = totalSeconds % 60;
62
+ if (hours > 0) {
63
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
64
+ }
65
+ if (minutes > 0) {
66
+ return `${minutes}m ${seconds}s`;
67
+ }
68
+ return `${seconds}s`;
69
+ }
70
+
71
+ export function buildChatMessageProcessSummary({
72
+ message,
73
+ processedLabel,
74
+ }: BuildChatMessageProcessSummaryParams): ChatMessageProcessSummarySource | undefined {
75
+ if (!hasCollapsibleAssistantProcess(message)) {
76
+ return undefined;
77
+ }
78
+ const duration = formatLifecycleDuration(message);
79
+ return { label: duration ? `${processedLabel} ${duration}` : processedLabel };
80
+ }
@@ -8,7 +8,7 @@ import type {
8
8
  ToolCardViewSource,
9
9
  } from "./chat-message-tool-card.utils";
10
10
 
11
- const SHOW_CONTENT_TOOL_NAME = "show_content";
11
+ const SHOW_CONTENT_TOOL_NAMES = ["show_content", "show_file", "show_url", "show_panel_app"];
12
12
 
13
13
  type ShowContentTargetType = ChatUiShowContentRequest["target"]["type"];
14
14
 
@@ -48,6 +48,14 @@ function readPlacement(value: unknown): ChatUiShowContentRequest["placement"] {
48
48
  : undefined;
49
49
  }
50
50
 
51
+ function readFileViewer(value: unknown): NonNullable<
52
+ Extract<ChatUiShowContentRequest["target"], { type: "file" }>["payload"]["viewer"]
53
+ > | undefined {
54
+ return value === "auto" || value === "source" || value === "rendered"
55
+ ? value
56
+ : undefined;
57
+ }
58
+
51
59
  function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null {
52
60
  if (!isRecord(value) || !isRecord(value.target)) {
53
61
  return null;
@@ -77,6 +85,7 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
77
85
  path,
78
86
  line: readOptionalPositiveInteger(payload.line),
79
87
  column: readOptionalPositiveInteger(payload.column),
88
+ viewer: readFileViewer(payload.viewer),
80
89
  },
81
90
  },
82
91
  title,
@@ -150,7 +159,7 @@ export function buildShowContentToolCard(params: {
150
159
  invocation,
151
160
  statusLabel,
152
161
  } = params;
153
- if (invocation.toolName !== SHOW_CONTENT_TOOL_NAME) {
162
+ if (!SHOW_CONTENT_TOOL_NAMES.includes(invocation.toolName)) {
154
163
  return null;
155
164
  }
156
165
  const request = readShowContentResult(invocation.result);
@@ -175,7 +184,7 @@ export function buildShowContentToolCard(params: {
175
184
  : undefined;
176
185
  return {
177
186
  kind: "result",
178
- name: SHOW_CONTENT_TOOL_NAME,
187
+ name: invocation.toolName,
179
188
  detail: summarizeShowContentRequest(request),
180
189
  text: undefined,
181
190
  outputData: invocation.result,
@@ -0,0 +1,60 @@
1
+ import type { ChatMessageTexts } from "@nextclaw/agent-chat-ui";
2
+ import type { ChatMessageAdapterTexts } from "@/features/chat/features/message/utils/chat-message.utils";
3
+ import { t } from "@/shared/lib/i18n";
4
+
5
+ export function buildChatMessageAdapterTexts(
6
+ language: string,
7
+ ): ChatMessageAdapterTexts {
8
+ void language;
9
+ return {
10
+ roleLabels: {
11
+ user: t("chatRoleUser"),
12
+ assistant: t("chatRoleAssistant"),
13
+ tool: t("chatRoleTool"),
14
+ system: t("chatRoleSystem"),
15
+ fallback: t("chatRoleMessage"),
16
+ },
17
+ reasoningLabel: t("chatReasoning"),
18
+ toolCallLabel: t("chatToolCall"),
19
+ toolResultLabel: t("chatToolResult"),
20
+ toolInputLabel: t("chatToolInput"),
21
+ toolNoOutputLabel: t("chatToolNoOutput"),
22
+ toolOutputLabel: t("chatToolOutput"),
23
+ toolStatusPreparingLabel: t("chatToolStatusPreparing"),
24
+ toolStatusRunningLabel: t("chatToolStatusRunning"),
25
+ toolStatusCompletedLabel: t("chatToolStatusCompleted"),
26
+ toolStatusFailedLabel: t("chatToolStatusFailed"),
27
+ toolStatusCancelledLabel: t("chatToolStatusCancelled"),
28
+ showContentActionLabel: t("chatShowContentAction"),
29
+ imageAttachmentLabel: t("chatImageAttachment"),
30
+ fileAttachmentLabel: t("chatFileAttachment"),
31
+ unknownPartLabel: t("chatUnknownPart"),
32
+ };
33
+ }
34
+
35
+ export function buildChatMessageTexts(
36
+ language: string,
37
+ ): ChatMessageTexts {
38
+ void language;
39
+ return {
40
+ copyCodeLabel: t("chatCodeCopy"),
41
+ copiedCodeLabel: t("chatCodeCopied"),
42
+ copyMessageLabel: t("chatMessageCopy"),
43
+ copiedMessageLabel: t("chatMessageCopied"),
44
+ typingLabel: t("chatTyping"),
45
+ attachmentOpenLabel: t("chatAttachmentOpen"),
46
+ attachmentAttachedLabel: t("chatAttachmentAttached"),
47
+ attachmentCategoryLabels: {
48
+ archive: t("chatAttachmentCategoryArchive"),
49
+ audio: t("chatAttachmentCategoryAudio"),
50
+ code: t("chatAttachmentCategoryCode"),
51
+ data: t("chatAttachmentCategoryData"),
52
+ document: t("chatAttachmentCategoryDocument"),
53
+ generic: t("chatAttachmentCategoryGeneric"),
54
+ image: t("chatAttachmentCategoryImage"),
55
+ pdf: t("chatAttachmentCategoryPdf"),
56
+ sheet: t("chatAttachmentCategorySheet"),
57
+ video: t("chatAttachmentCategoryVideo"),
58
+ },
59
+ };
60
+ }
@@ -7,6 +7,7 @@ import type {
7
7
  import type {
8
8
  ChatMessageAdapterTexts,
9
9
  ChatMessagePartSource,
10
+ ChatMessageProcessSummarySource,
10
11
  } from "@/features/chat/types/chat-message.types";
11
12
 
12
13
  export type {
@@ -21,6 +22,7 @@ export type ChatMessageSource = {
21
22
  timestamp?: string;
22
23
  status?: string;
23
24
  inlineTokens?: ChatInlineTokenSource[];
25
+ processSummary?: ChatMessageProcessSummarySource;
24
26
  };
25
27
  parts: ChatMessagePartSource[];
26
28
  };
@@ -79,6 +81,7 @@ export function adaptChatMessage(
79
81
  roleLabel: resolveRoleLabel(message.role, params.texts.roleLabels),
80
82
  timestampLabel: params.formatTimestamp(resolveMessageTimestamp(message)),
81
83
  status: message.meta?.status,
84
+ processSummary: message.meta?.processSummary,
82
85
  parts: message.parts
83
86
  .map((part) =>
84
87
  adaptChatMessagePart({
@@ -220,6 +220,45 @@ describe("useNcpAgentRuntime", () => {
220
220
  expect(client.abort).toHaveBeenCalledWith({ sessionId: "session-running" });
221
221
  });
222
222
 
223
+ it("clears the local running state when the live stream publishes message.abort", async () => {
224
+ const client = new DeferredSendClient();
225
+ const manager = new DefaultNcpAgentConversationStateManager();
226
+ const { result } = renderHook(() =>
227
+ useNcpAgentRuntime({ sessionId: "session-running", client, manager: manager as never }),
228
+ );
229
+
230
+ await act(async () => {
231
+ await client.emit({
232
+ type: NcpEventType.RunStarted,
233
+ payload: {
234
+ sessionId: "session-running",
235
+ messageId: "assistant-1",
236
+ runId: "run-1",
237
+ },
238
+ });
239
+ });
240
+
241
+ await waitFor(() => {
242
+ expect(result.current.isRunning).toBe(true);
243
+ expect(result.current.activeRunId).toBe("run-1");
244
+ });
245
+
246
+ await act(async () => {
247
+ await client.emit({
248
+ type: NcpEventType.MessageAbort,
249
+ payload: {
250
+ sessionId: "session-running",
251
+ messageId: "assistant-1",
252
+ },
253
+ });
254
+ });
255
+
256
+ await waitFor(() => {
257
+ expect(result.current.isRunning).toBe(false);
258
+ expect(result.current.activeRunId).toBeNull();
259
+ });
260
+ });
261
+
223
262
  it("uses the hydrated live stream as the only event source while sending to an existing session", async () => {
224
263
  const client = new ExistingSessionLiveClient();
225
264
  const manager = new DefaultNcpAgentConversationStateManager();
@@ -70,7 +70,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
70
70
  icon={<Plus className="h-3.5 w-3.5" />}
71
71
  label={actionLabel}
72
72
  tooltip={false}
73
- className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-accent hover:text-accent-foreground"
73
+ className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900"
74
74
  />
75
75
  </PopoverTrigger>
76
76
  <PopoverContent
@@ -91,7 +91,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
91
91
  <IconActionButton
92
92
  icon={<Plus className="h-3.5 w-3.5" />}
93
93
  label={actionLabel}
94
- className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-accent hover:text-accent-foreground"
94
+ className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900"
95
95
  onClick={() => onCreateSession(preferredSessionType, group.projectRoot)}
96
96
  />
97
97
  )}
@@ -230,7 +230,7 @@ export function ChatSidebarSessionItem({
230
230
  className={cn(
231
231
  'w-full rounded-xl px-3 py-2.5 text-left transition-all text-[13px]',
232
232
  active
233
- ? 'bg-gray-200 text-gray-900 font-semibold shadow-sm'
233
+ ? 'bg-gray-200/80 text-gray-900 font-semibold shadow-sm'
234
234
  : 'text-gray-700 hover:bg-gray-200/60 hover:text-gray-900'
235
235
  )}
236
236
  >