@nextclaw/ui 0.15.2 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/assets/{api-B7Qigvtm.js → api-iNBpppzj.js} +2 -2
  3. package/dist/assets/{appearance-settings-page-CuE0Iiqj.js → appearance-settings-page-DMffQFR3.js} +1 -1
  4. package/dist/assets/{book-open-Ba2f7YiG.js → book-open-C3mJGwiy.js} +1 -1
  5. package/dist/assets/{channels-list-page-CTsM6TcP.js → channels-list-page-CCtp0BRS.js} +1 -1
  6. package/dist/assets/chat-page-H5a0dWvO.js +1 -0
  7. package/dist/assets/{config-split-page-Fbb4PtTf.js → config-split-page-CrOJgB-Y.js} +1 -1
  8. package/dist/assets/{confirm-dialog-D2EG3nqE.js → confirm-dialog-Chv50USy.js} +2 -2
  9. package/dist/assets/{desktop-update-config-BGR3HUi1.js → desktop-update-config-KzTuXRf_.js} +1 -1
  10. package/dist/assets/{dist-7JTjycxE.js → dist-BunzmC4B.js} +1 -1
  11. package/dist/assets/{dist-wXLhk4hl.js → dist-ChmGwvtK.js} +1 -1
  12. package/dist/assets/doc-browser-DNRL4Zvd.js +1 -0
  13. package/dist/assets/doc-browser-Dg0pmkOg.js +1 -0
  14. package/dist/assets/{doc-browser-context-CYAGm5d5.js → doc-browser-context-BHY-0FSD.js} +1 -1
  15. package/dist/assets/{es2015-j1SRCIX5.js → es2015-DQZ5JWwK.js} +1 -1
  16. package/dist/assets/{external-link-DxuPaDoK.js → external-link-CU0kzzIP.js} +1 -1
  17. package/dist/assets/{form-actions-BztL7P_T.js → form-actions-BvCp_0lS.js} +1 -1
  18. package/dist/assets/{i18n-provider-B4oMxOFx.js → i18n-provider-Cu8XyZb_.js} +1 -1
  19. package/dist/assets/index-CzfBdvK1.css +1 -0
  20. package/dist/assets/index-DGqHne1Q.js +104 -0
  21. package/dist/assets/loader-circle-CDSFuzmk.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +1 -0
  24. package/dist/assets/{model-config-page-DeKk9gvE.js → model-config-page-bNNo-XyG.js} +1 -1
  25. package/dist/assets/plus-D4DsdLN-.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-DsH2LF1q.js → provider-scoped-model-input-IlgQCsoT.js} +1 -1
  27. package/dist/assets/{providers-config-page-BFnGp68F.js → providers-config-page-wLo6CAJw.js} +1 -1
  28. package/dist/assets/{react-UirzUuST.js → react-BPIsQIlO.js} +1 -1
  29. package/dist/assets/{refresh-cw-eT8BF-8B.js → refresh-cw-CILd1vAP.js} +1 -1
  30. package/dist/assets/remote-uKEYp7AR.js +1 -0
  31. package/dist/assets/rotate-cw-wPFPg1kH.js +1 -0
  32. package/dist/assets/{runtime-config-page-BelAqiNG.js → runtime-config-page-DDn1sP4G.js} +1 -1
  33. package/dist/assets/{save-Cb2g0BOi.js → save-ByJqcPTP.js} +1 -1
  34. package/dist/assets/{search-BcHjwdsb.js → search-DVIV6pfD.js} +1 -1
  35. package/dist/assets/{search-config-page-zPm6--Ig.js → search-config-page-B-Cf4iiB.js} +1 -1
  36. package/dist/assets/{secrets-config-page-Cxyp-KuQ.js → secrets-config-page-DPKsY8Iw.js} +1 -1
  37. package/dist/assets/security-config-e9SSBlKr.js +1 -0
  38. package/dist/assets/{select-CdSEOwVM.js → select-BdgczSNw.js} +1 -1
  39. package/dist/assets/{setting-row-CTAtGexr.js → setting-row-BkTVUTKx.js} +1 -1
  40. package/dist/assets/{settings-2-2DWauOjY.js → settings-2-CH5shGg_.js} +1 -1
  41. package/dist/assets/{side-dock-DPvakJ0a.js → side-dock-BsBvcJ9i.js} +2 -2
  42. package/dist/assets/{skeleton-P5oM14E1.js → skeleton-DeX1FKw7.js} +1 -1
  43. package/dist/assets/switch-Cv8Ekb3x.js +1 -0
  44. package/dist/assets/{tag-chip-D-WsKoT1.js → tag-chip-RB9uKcoC.js} +1 -1
  45. package/dist/assets/{tooltip-CjkOaHW7.js → tooltip-oQlEN-l6.js} +1 -1
  46. package/dist/assets/{use-config--hx5oxQn.js → use-config-l3W7y8Qz.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CD9a_SK1.js → use-confirm-dialog-BAxaVOJI.js} +1 -1
  48. package/dist/assets/x-BBMffEN0.js +1 -0
  49. package/dist/index.html +26 -26
  50. package/package.json +9 -9
  51. package/src/app/components/layout/sidebar-items.tsx +3 -3
  52. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  53. package/src/app/components/layout/sidebar.tsx +1 -1
  54. package/src/app/styles/design-system.css +17 -4
  55. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +1 -1
  56. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  57. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  58. package/src/features/chat/features/message/components/chat-message-list.container.tsx +47 -1
  59. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  60. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  61. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  62. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  63. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +1 -1
  64. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +2 -2
  65. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  66. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  67. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +67 -0
  68. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +1 -1
  69. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  70. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +1 -1
  71. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +174 -22
  72. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +4 -0
  73. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  74. package/src/features/chat/stores/chat-thread.store.ts +3 -0
  75. package/src/features/cron/components/cron-config.tsx +63 -80
  76. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  77. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  78. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  79. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  80. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  81. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  82. package/src/features/side-dock/components/side-dock.tsx +3 -3
  83. package/src/index.css +239 -13
  84. package/src/shared/components/actions/action-link.tsx +1 -1
  85. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  86. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  87. package/src/shared/components/ui/button.tsx +1 -1
  88. package/src/shared/components/ui/dialog.tsx +1 -1
  89. package/src/shared/components/ui/input.tsx +1 -1
  90. package/src/shared/components/ui/select.tsx +1 -1
  91. package/src/shared/components/ui/switch.tsx +1 -1
  92. package/src/shared/components/ui/tabs.tsx +1 -1
  93. package/src/shared/components/ui/textarea.tsx +1 -1
  94. package/src/shared/lib/i18n/locales/en-US/chat.json +83 -0
  95. package/src/shared/lib/i18n/locales/zh-CN/chat.json +83 -0
  96. package/dist/assets/chat-page-C_5dcuE3.js +0 -1
  97. package/dist/assets/doc-browser-bHyXnxlj.js +0 -1
  98. package/dist/assets/doc-browser-gTrQ3jCF.js +0 -1
  99. package/dist/assets/index-CvrvblgX.css +0 -1
  100. package/dist/assets/index-Obd4JYU2.js +0 -104
  101. package/dist/assets/loader-circle-CdzgK4eH.js +0 -1
  102. package/dist/assets/mcp-marketplace-page-Bt7CuOoq.js +0 -9
  103. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +0 -1
  104. package/dist/assets/plus-Bx7wQbhG.js +0 -1
  105. package/dist/assets/remote-CDkHZtA8.js +0 -1
  106. package/dist/assets/rotate-cw-1JqxHTuw.js +0 -1
  107. package/dist/assets/security-config-B_gCq9fn.js +0 -1
  108. package/dist/assets/switch-BU_UEULb.js +0 -1
  109. package/dist/assets/x-DT5x45JU.js +0 -1
@@ -108,7 +108,7 @@ export function ChatSidebarUtilityMenu({
108
108
  <button
109
109
  type="button"
110
110
  aria-label={t("settingsMenu")}
111
- className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"
111
+ className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
112
112
  >
113
113
  <Settings className="h-4 w-4 shrink-0 text-muted-foreground/70" />
114
114
  <span className="min-w-0 flex-1 text-left">{t("settings")}</span>
@@ -44,7 +44,7 @@ function QueuedInputIconButton({
44
44
  aria-label={label}
45
45
  disabled={disabled}
46
46
  onClick={onClick}
47
- className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:text-muted-foreground/50"
47
+ className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed disabled:text-muted-foreground/50"
48
48
  >
49
49
  {children}
50
50
  </button>
@@ -9,6 +9,8 @@ const captures = vi.hoisted(() => ({
9
9
  messages: unknown[];
10
10
  onToolAction?: (action: unknown) => void;
11
11
  onFileOpen?: (action: unknown) => void;
12
+ onAttachmentOpen?: (file: unknown) => void;
13
+ onInlineTokenClick?: (token: unknown) => void;
12
14
  renderInlineDisplay?: (display: unknown) => unknown;
13
15
  renderPanelAppCard?: (panelApp: unknown) => unknown;
14
16
  texts?: Record<string, unknown>;
@@ -16,6 +18,7 @@ const captures = vi.hoisted(() => ({
16
18
  language: "en",
17
19
  openFilePreview: vi.fn(),
18
20
  handleToolAction: vi.fn(),
21
+ showContent: vi.fn(),
19
22
  }));
20
23
 
21
24
  vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
@@ -26,6 +29,7 @@ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
26
29
  messages: unknown[];
27
30
  onToolAction?: (action: unknown) => void;
28
31
  onFileOpen?: (action: unknown) => void;
32
+ onAttachmentOpen?: (file: unknown) => void;
29
33
  renderInlineDisplay?: (display: unknown) => unknown;
30
34
  renderPanelAppCard?: (panelApp: unknown) => unknown;
31
35
  texts?: Record<string, unknown>;
@@ -42,6 +46,9 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
42
46
  openFilePreview: captures.openFilePreview,
43
47
  handleToolAction: captures.handleToolAction,
44
48
  },
49
+ chatUiManager: {
50
+ showContent: captures.showContent,
51
+ },
45
52
  }),
46
53
  }));
47
54
 
@@ -59,6 +66,7 @@ beforeEach(() => {
59
66
  captures.language = "en";
60
67
  captures.openFilePreview.mockReset();
61
68
  captures.handleToolAction.mockReset();
69
+ captures.showContent.mockReset();
62
70
  });
63
71
 
64
72
  it("reuses adapted message references when the source message object is unchanged", () => {
@@ -300,6 +308,41 @@ it("wires markdown file link actions to the workspace file preview manager", ()
300
308
  expect(captures.openFilePreview).toHaveBeenCalledWith(action);
301
309
  });
302
310
 
311
+ it("opens message attachments in the workspace file preview", () => {
312
+ const message = {
313
+ id: "assistant-attachment",
314
+ sessionId: "session-1",
315
+ role: "assistant",
316
+ status: "final",
317
+ timestamp: "2026-03-31T10:05:00.000Z",
318
+ parts: [
319
+ {
320
+ type: "file",
321
+ name: "spec.pdf",
322
+ mimeType: "application/pdf",
323
+ contentBase64: "cGRm",
324
+ },
325
+ ],
326
+ } satisfies NcpMessage;
327
+
328
+ render(<ChatMessageListContainer messages={[message]} isSending={false} />);
329
+ captures.renders[captures.renders.length - 1]?.onAttachmentOpen?.({
330
+ label: "spec.pdf",
331
+ mimeType: "application/pdf",
332
+ dataUrl: "data:application/pdf;base64,cGRm",
333
+ isImage: false,
334
+ });
335
+
336
+ expect(captures.openFilePreview).toHaveBeenCalledWith({
337
+ path: "spec.pdf",
338
+ label: "spec.pdf",
339
+ viewMode: "preview",
340
+ contentUrl: "data:application/pdf;base64,cGRm",
341
+ mimeType: "application/pdf",
342
+ });
343
+ expect(captures.showContent).not.toHaveBeenCalled();
344
+ });
345
+
303
346
  it("renders context inheritance as a divider without repeating inherited messages", () => {
304
347
  const inheritedMessage = {
305
348
  id: "child-session:inherited:1",
@@ -387,7 +430,7 @@ it("keeps Hermes tool invocation parts as tool cards instead of flattening them
387
430
  });
388
431
  });
389
432
 
390
- it("passes localized attachment card texts to the shared chat UI", () => {
433
+ it("passes localized message presentation texts to the shared chat UI", () => {
391
434
  captures.language = "zh";
392
435
 
393
436
  render(<ChatMessageListContainer messages={[]} isSending={false} />);
@@ -400,6 +443,24 @@ it("passes localized attachment card texts to the shared chat UI", () => {
400
443
  pdf: "chatAttachmentCategoryPdf",
401
444
  generic: "chatAttachmentCategoryGeneric",
402
445
  },
446
+ reasoningCharacterCountTemplates: {
447
+ inProgress: "chatReasoningInProgressCharacterCount",
448
+ completed: "chatReasoningCompletedCharacterCount",
449
+ },
450
+ toolStatusLabels: {
451
+ terminal: {
452
+ running: "chatToolTerminalRunning",
453
+ success: "chatToolTerminalSuccess",
454
+ error: "chatToolTerminalError",
455
+ cancelled: "chatToolTerminalCancelled",
456
+ },
457
+ fileEdit: {
458
+ running: "chatToolFileEditRunning",
459
+ success: "chatToolFileEditSuccess",
460
+ error: "chatToolFileEditError",
461
+ cancelled: "chatToolFileEditCancelled",
462
+ },
463
+ },
403
464
  });
404
465
  });
405
466
 
@@ -1,7 +1,8 @@
1
- import { useMemo } from "react";
1
+ import { useCallback, useMemo } from "react";
2
2
  import type { NcpMessage } from "@nextclaw/ncp";
3
3
  import {
4
4
  type ChatInlineDisplayViewModel,
5
+ type ChatInlineTokenViewModel,
5
6
  type ChatMessageViewModel,
6
7
  ChatMessageList,
7
8
  } from "@nextclaw/agent-chat-ui";
@@ -23,6 +24,7 @@ import {
23
24
  } from "@/features/chat/features/session/utils/ncp-session-context-metadata.utils";
24
25
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
25
26
  import { ChatInlinePanelAppCard } from "@/features/chat/features/message/components/chat-inline-panel-app-card";
27
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
26
28
  import { useI18n } from "@/app/components/i18n-provider";
27
29
  import { formatDateTime, t } from "@/shared/lib/i18n";
28
30
 
@@ -342,6 +344,48 @@ export function ChatMessageListContainer({
342
344
  () => buildTimelineItems({ rawMessages, messages }),
343
345
  [messages, rawMessages],
344
346
  );
347
+ const sessionSkillsQuery = useChatQueryStore((state) => state.snapshot.sessionSkillsQuery);
348
+ const handleInlineTokenClick = useCallback(
349
+ (token: ChatInlineTokenViewModel) => {
350
+ if (token.kind !== "skill") {
351
+ return;
352
+ }
353
+ const skillKey = token.key.trim();
354
+ if (!skillKey) {
355
+ return;
356
+ }
357
+ const records = sessionSkillsQuery?.data?.records ?? [];
358
+ const matched = records.find((record) => record.ref === skillKey || record.name === skillKey);
359
+ const skillPath = matched?.path?.trim();
360
+ if (!skillPath) {
361
+ return;
362
+ }
363
+ presenter.chatThreadManager.openFilePreview({
364
+ path: skillPath,
365
+ label: token.label || matched?.name || skillKey,
366
+ viewMode: "preview",
367
+ previewViewer: "rendered",
368
+ });
369
+ },
370
+ [presenter.chatThreadManager, sessionSkillsQuery],
371
+ );
372
+ const handleAttachmentOpen = useCallback(
373
+ (file: { label: string; mimeType: string; dataUrl?: string; sizeBytes?: number; isImage: boolean }) => {
374
+ const contentUrl = file.dataUrl?.trim();
375
+ if (!contentUrl) {
376
+ return;
377
+ }
378
+ const label = file.label.trim() || "attachment";
379
+ presenter.chatThreadManager.openFilePreview({
380
+ path: label,
381
+ label,
382
+ viewMode: "preview",
383
+ contentUrl,
384
+ mimeType: file.mimeType,
385
+ });
386
+ },
387
+ [presenter.chatThreadManager],
388
+ );
345
389
  return (
346
390
  <div className={className}>
347
391
  {timelineItems.map((item, index) =>
@@ -358,6 +402,8 @@ export function ChatMessageListContainer({
358
402
  texts={messageTexts}
359
403
  onToolAction={presenter.chatThreadManager.handleToolAction}
360
404
  onFileOpen={presenter.chatThreadManager.openFilePreview}
405
+ onAttachmentOpen={handleAttachmentOpen}
406
+ onInlineTokenClick={handleInlineTokenClick}
361
407
  renderInlineDisplay={renderChatInlineDisplay}
362
408
  renderToolAgent={(agentId) => (
363
409
  <AgentIdentityAvatar
@@ -18,7 +18,7 @@ const baseAssistantMessage = {
18
18
  toolCallId: "tool-1",
19
19
  toolName: "exec_command",
20
20
  state: "result",
21
- args: "{\"cmd\":\"git status\"}",
21
+ args: '{"cmd":"git status"}',
22
22
  result: "clean",
23
23
  },
24
24
  {
@@ -56,4 +56,14 @@ describe("buildChatMessageProcessSummary", () => {
56
56
  label: "Processed 3m 51s",
57
57
  });
58
58
  });
59
+
60
+ it("stays free of tool-activity semantics", () => {
61
+ const summary = buildChatMessageProcessSummary({
62
+ message: baseAssistantMessage,
63
+ processedLabel: "Processed",
64
+ });
65
+ expect(summary?.label).toBe("Processed");
66
+ expect(summary?.label.toLowerCase()).not.toContain("bash");
67
+ expect(summary?.label.toLowerCase()).not.toContain("read");
68
+ });
59
69
  });
@@ -0,0 +1,23 @@
1
+ import { expect, it, vi } from "vitest";
2
+ import { buildChatMessageTexts } from "@/features/chat/features/message/utils/chat-message-texts.utils";
3
+
4
+ vi.mock("@/shared/lib/i18n", () => ({
5
+ t: (key: string) => key,
6
+ }));
7
+
8
+ it("provides localized status keys for built-in tool categories", () => {
9
+ expect(buildChatMessageTexts("zh").toolStatusLabels?.builtIn).toMatchObject({
10
+ directory: {
11
+ running: "chatToolDirectoryRunning",
12
+ success: "chatToolDirectorySuccess",
13
+ error: "chatToolDirectoryError",
14
+ cancelled: "chatToolDirectoryCancelled",
15
+ },
16
+ memory: {
17
+ running: "chatToolMemoryRunning",
18
+ success: "chatToolMemorySuccess",
19
+ error: "chatToolMemoryError",
20
+ cancelled: "chatToolMemoryCancelled",
21
+ },
22
+ });
23
+ });
@@ -68,6 +68,11 @@ function formatLifecycleDuration(message: NcpMessage): string | null {
68
68
  return `${seconds}s`;
69
69
  }
70
70
 
71
+ /**
72
+ * Outer process collapse only.
73
+ * Tool activity grouping is a separate layer over consecutive tool-cards
74
+ * and must not be mixed into this summary.
75
+ */
71
76
  export function buildChatMessageProcessSummary({
72
77
  message,
73
78
  processedLabel,
@@ -32,6 +32,71 @@ export function buildChatMessageAdapterTexts(
32
32
  };
33
33
  }
34
34
 
35
+ function buildBuiltInToolStatusLabels() {
36
+ return {
37
+ directory: {
38
+ running: t("chatToolDirectoryRunning"),
39
+ success: t("chatToolDirectorySuccess"),
40
+ error: t("chatToolDirectoryError"),
41
+ cancelled: t("chatToolDirectoryCancelled"),
42
+ },
43
+ web: {
44
+ running: t("chatToolWebRunning"),
45
+ success: t("chatToolWebSuccess"),
46
+ error: t("chatToolWebError"),
47
+ cancelled: t("chatToolWebCancelled"),
48
+ },
49
+ message: {
50
+ running: t("chatToolMessageRunning"),
51
+ success: t("chatToolMessageSuccess"),
52
+ error: t("chatToolMessageError"),
53
+ cancelled: t("chatToolMessageCancelled"),
54
+ },
55
+ session: {
56
+ running: t("chatToolSessionRunning"),
57
+ success: t("chatToolSessionSuccess"),
58
+ error: t("chatToolSessionError"),
59
+ cancelled: t("chatToolSessionCancelled"),
60
+ },
61
+ agent: {
62
+ running: t("chatToolAgentRunning"),
63
+ success: t("chatToolAgentSuccess"),
64
+ error: t("chatToolAgentError"),
65
+ cancelled: t("chatToolAgentCancelled"),
66
+ },
67
+ memory: {
68
+ running: t("chatToolMemoryRunning"),
69
+ success: t("chatToolMemorySuccess"),
70
+ error: t("chatToolMemoryError"),
71
+ cancelled: t("chatToolMemoryCancelled"),
72
+ },
73
+ schedule: {
74
+ running: t("chatToolScheduleRunning"),
75
+ success: t("chatToolScheduleSuccess"),
76
+ error: t("chatToolScheduleError"),
77
+ cancelled: t("chatToolScheduleCancelled"),
78
+ },
79
+ system: {
80
+ running: t("chatToolSystemRunning"),
81
+ success: t("chatToolSystemSuccess"),
82
+ error: t("chatToolSystemError"),
83
+ cancelled: t("chatToolSystemCancelled"),
84
+ },
85
+ image: {
86
+ running: t("chatToolImageRunning"),
87
+ success: t("chatToolImageSuccess"),
88
+ error: t("chatToolImageError"),
89
+ cancelled: t("chatToolImageCancelled"),
90
+ },
91
+ display: {
92
+ running: t("chatToolDisplayRunning"),
93
+ success: t("chatToolDisplaySuccess"),
94
+ error: t("chatToolDisplayError"),
95
+ cancelled: t("chatToolDisplayCancelled"),
96
+ },
97
+ };
98
+ }
99
+
35
100
  export function buildChatMessageTexts(
36
101
  language: string,
37
102
  ): ChatMessageTexts {
@@ -42,8 +107,41 @@ export function buildChatMessageTexts(
42
107
  copyMessageLabel: t("chatMessageCopy"),
43
108
  copiedMessageLabel: t("chatMessageCopied"),
44
109
  typingLabel: t("chatTyping"),
110
+ reasoningCharacterCountTemplates: {
111
+ inProgress: t("chatReasoningInProgressCharacterCount"),
112
+ completed: t("chatReasoningCompletedCharacterCount"),
113
+ },
114
+ toolStatusLabels: {
115
+ terminal: {
116
+ running: t("chatToolTerminalRunning"),
117
+ success: t("chatToolTerminalSuccess"),
118
+ error: t("chatToolTerminalError"),
119
+ cancelled: t("chatToolTerminalCancelled"),
120
+ },
121
+ fileRead: {
122
+ running: t("chatToolFileReadRunning"),
123
+ success: t("chatToolFileReadSuccess"),
124
+ error: t("chatToolFileReadError"),
125
+ cancelled: t("chatToolFileReadCancelled"),
126
+ },
127
+ fileEdit: {
128
+ running: t("chatToolFileEditRunning"),
129
+ success: t("chatToolFileEditSuccess"),
130
+ error: t("chatToolFileEditError"),
131
+ cancelled: t("chatToolFileEditCancelled"),
132
+ },
133
+ search: {
134
+ running: t("chatToolSearchRunning"),
135
+ success: t("chatToolSearchSuccess"),
136
+ error: t("chatToolSearchError"),
137
+ cancelled: t("chatToolSearchCancelled"),
138
+ },
139
+ builtIn: buildBuiltInToolStatusLabels(),
140
+ },
45
141
  attachmentOpenLabel: t("chatAttachmentOpen"),
46
142
  attachmentAttachedLabel: t("chatAttachmentAttached"),
143
+ attachmentExpandLabel: t("chatAttachmentExpand"),
144
+ attachmentCloseLabel: t("chatAttachmentClosePreview"),
47
145
  attachmentCategoryLabels: {
48
146
  archive: t("chatAttachmentCategoryArchive"),
49
147
  audio: t("chatAttachmentCategoryAudio"),
@@ -56,5 +154,45 @@ export function buildChatMessageTexts(
56
154
  sheet: t("chatAttachmentCategorySheet"),
57
155
  video: t("chatAttachmentCategoryVideo"),
58
156
  },
157
+ toolActivitySegmentTemplates: {
158
+ read: {
159
+ one: t("chatToolActivityReadOne"),
160
+ other: t("chatToolActivityReadOther"),
161
+ },
162
+ edit: {
163
+ one: t("chatToolActivityEditOne"),
164
+ other: t("chatToolActivityEditOther"),
165
+ },
166
+ directory: {
167
+ one: t("chatToolActivityDirectoryOne"),
168
+ other: t("chatToolActivityDirectoryOther"),
169
+ },
170
+ search: {
171
+ one: t("chatToolActivitySearchOne"),
172
+ other: t("chatToolActivitySearchOther"),
173
+ },
174
+ bash: {
175
+ one: t("chatToolActivityBashOne"),
176
+ other: t("chatToolActivityBashOther"),
177
+ },
178
+ web: {
179
+ one: t("chatToolActivityWebOne"),
180
+ other: t("chatToolActivityWebOther"),
181
+ },
182
+ agent: {
183
+ one: t("chatToolActivityAgentOne"),
184
+ other: t("chatToolActivityAgentOther"),
185
+ },
186
+ panel: {
187
+ one: t("chatToolActivityPanelOne"),
188
+ other: t("chatToolActivityPanelOther"),
189
+ },
190
+ other: {
191
+ one: t("chatToolActivityOtherOne"),
192
+ other: t("chatToolActivityOtherOther"),
193
+ },
194
+ },
195
+ toolActivityFailedLabel: t("chatProcessSummaryFailed"),
196
+ toolActivityCancelledLabel: t("chatProcessSummaryCancelled"),
59
197
  };
60
198
  }
@@ -64,7 +64,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
64
64
  ? 'chatSidebarExpandProject'
65
65
  : 'chatSidebarCollapseProject',
66
66
  )}
67
- className="flex h-full w-full min-w-0 items-center gap-1.5 pr-14 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
67
+ className="flex h-full w-full min-w-0 items-center gap-1.5 pr-14 text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
68
68
  onClick={() =>
69
69
  presenter.chatSessionListManager.toggleProjectCollapsed(
70
70
  group.projectRoot,
@@ -40,7 +40,7 @@ import { t } from "@/shared/lib/i18n";
40
40
  import { cn } from "@/shared/lib/utils";
41
41
 
42
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";
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-1 focus-visible:ring-border";
44
44
 
45
45
  function ChatSessionTitle({ title }: { title: string }) {
46
46
  return (
@@ -111,7 +111,7 @@ function ChatSessionSwitchItem({
111
111
  type="button"
112
112
  aria-current={active ? "page" : undefined}
113
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",
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-1 focus-visible:ring-border",
115
115
  active
116
116
  ? "bg-accent text-accent-foreground"
117
117
  : "text-popover-foreground hover:bg-accent/70",
@@ -49,7 +49,7 @@ export function ChatWelcomeProjectPicker({
49
49
  <PopoverTrigger asChild>
50
50
  <button
51
51
  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-2 focus-visible:ring-primary/25"
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"
53
53
  title={projectRoot ?? undefined}
54
54
  aria-label={t('chatWelcomeProjectPickerLabel')}
55
55
  disabled={!selectable || isSaving}
@@ -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-2 focus-visible:ring-primary/25"
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"
43
43
  aria-label={t('chatWelcomeSessionTypePickerLabel')}
44
44
  >
45
45
  {selectedOption.icon?.src ? (
@@ -167,6 +167,11 @@ function mockWorkspaceBreadcrumbBrowseTree() {
167
167
  beforeEach(() => {
168
168
  serverPathReadMock.mockReset();
169
169
  serverPathBrowseMock.mockReset();
170
+ serverPathReadMock.mockReturnValue({
171
+ isLoading: false,
172
+ error: null,
173
+ data: null,
174
+ });
170
175
  serverPathBrowseMock.mockReturnValue({
171
176
  isLoading: false,
172
177
  error: new Error("server path must point to a directory"),
@@ -175,6 +180,68 @@ beforeEach(() => {
175
180
  });
176
181
 
177
182
  describe("ChatSessionWorkspaceFilePreview rendering", () => {
183
+ it("renders attachment content URLs as workspace-native media previews", () => {
184
+ renderWorkspaceFilePreview({
185
+ file: {
186
+ key: "attachment-image",
187
+ path: "photo.png",
188
+ label: "photo.png",
189
+ viewMode: "preview",
190
+ contentUrl: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Fphoto",
191
+ mimeType: "image/png",
192
+ },
193
+ });
194
+
195
+ expect(serverPathReadMock).toHaveBeenCalledWith(
196
+ expect.objectContaining({ enabled: false }),
197
+ );
198
+ expect(serverPathBrowseMock).toHaveBeenCalledWith(
199
+ expect.objectContaining({ enabled: false }),
200
+ );
201
+ const image = screen.getByTestId("workspace-content-image");
202
+ expect(image.getAttribute("src")).toBe(
203
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Fphoto",
204
+ );
205
+ expect(image.getAttribute("alt")).toBe("photo.png");
206
+ });
207
+
208
+ it("offers download and system open actions for unsupported office attachments", () => {
209
+ renderWorkspaceFilePreview({
210
+ file: {
211
+ key: "attachment-docx",
212
+ path: "overview.zh-CN.docx",
213
+ label: "overview.zh-CN.docx",
214
+ viewMode: "preview",
215
+ contentUrl: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
216
+ mimeType:
217
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
218
+ },
219
+ });
220
+
221
+ expect(screen.getByTestId("workspace-content-unsupported")).toBeTruthy();
222
+ expect(
223
+ screen.getByTestId("workspace-content-unsupported").textContent,
224
+ ).toContain("overview.zh-CN.docx");
225
+ expect(screen.getByText(t("chatWorkspacePreviewUnsupported"))).toBeTruthy();
226
+ expect(screen.getByText(t("chatWorkspacePreviewUnsupportedHint"))).toBeTruthy();
227
+
228
+ const download = screen.getByRole("link", {
229
+ name: t("chatWorkspacePreviewDownload"),
230
+ });
231
+ expect(download.getAttribute("href")).toBe(
232
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
233
+ );
234
+ expect(download.getAttribute("download")).toBe("overview.zh-CN.docx");
235
+
236
+ const openExternally = screen.getByRole("link", {
237
+ name: t("chatWorkspacePreviewOpenExternally"),
238
+ });
239
+ expect(openExternally.getAttribute("href")).toBe(
240
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
241
+ );
242
+ expect(openExternally.getAttribute("target")).toBe("_blank");
243
+ });
244
+
178
245
  it("renders preview files inside a full-height workspace code surface", () => {
179
246
  mockTextRead();
180
247
  renderWorkspaceFilePreview();
@@ -81,7 +81,7 @@ export function ChatSessionWorkspaceDirectoryBrowser({
81
81
  type="button"
82
82
  aria-label={buildDirectoryEntryLabel(entry)}
83
83
  className={cn(
84
- "flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
84
+ "flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
85
85
  entry.hidden ? "opacity-65" : null,
86
86
  )}
87
87
  onClick={() => openEntry(entry)}
@@ -67,7 +67,7 @@ export function WorkspaceBreadcrumbBrowser({
67
67
  <Fragment key={breadcrumb.path}>
68
68
  <button
69
69
  type="button"
70
- className="h-5 rounded-sm px-1 text-left hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
70
+ className="h-5 rounded-sm px-1 text-left hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
71
71
  onClick={() => onBrowsePathChange(breadcrumb.path)}
72
72
  >
73
73
  {breadcrumb.label}
@@ -97,7 +97,7 @@ export function WorkspaceBreadcrumbBrowser({
97
97
  key={entry.path}
98
98
  type="button"
99
99
  className={cn(
100
- "flex h-6 w-full items-center gap-1.5 rounded-sm px-1.5 text-left text-[11px] text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
100
+ "flex h-6 w-full items-center gap-1.5 rounded-sm px-1.5 text-left text-[11px] text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
101
101
  entry.hidden ? "opacity-65" : null,
102
102
  )}
103
103
  onClick={() => openEntry(entry)}
@@ -48,7 +48,7 @@ function WorkspaceBreadcrumbSegmentButton({
48
48
  type="button"
49
49
  aria-current={segment.isCurrent ? "page" : undefined}
50
50
  className={cn(
51
- "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none transition-colors hover:bg-gray-200/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
51
+ "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none transition-colors hover:bg-gray-200/80 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
52
52
  segment.kind === "workspace"
53
53
  ? "bg-primary/8 text-primary hover:bg-primary/12"
54
54
  : segment.isCurrent