@nextclaw/ui 0.14.1-beta.0 → 0.14.1-beta.1

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 (126) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/assets/{api-C6EDHBDR.js → api-dQ6uQSZG.js} +3 -3
  3. package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-DfpjB8XB.js} +2 -2
  4. package/dist/assets/chat-page-D3QaEqjs.js +105 -0
  5. package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-CeFfbaqc.js} +1 -1
  6. package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-C95Y4QPC.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-CcK6jiEb.js} +1 -1
  8. package/dist/assets/{dist-CmMro3nV.js → dist-DTGzeil6.js} +1 -1
  9. package/dist/assets/{dist-BIYV2-Ia.js → dist-fGogBmPA.js} +1 -1
  10. package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
  11. package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DVHhEb96.js} +1 -1
  12. package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-BfMXwnrz.js} +1 -1
  13. package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-D2F4KMiU.js} +1 -1
  14. package/dist/assets/{external-link-D2cBOCqn.js → external-link-CcgtBK26.js} +1 -1
  15. package/dist/assets/index-ByJCbQqr.css +1 -0
  16. package/dist/assets/index-DjXqiloY.js +103 -0
  17. package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
  19. package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
  20. package/dist/assets/plus-1Rxl5FlV.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
  22. package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
  23. package/dist/assets/{react-DYQXXGbH.js → react-CTqdhFMk.js} +1 -1
  24. package/dist/assets/remote-fQ7N_r8u.js +1 -0
  25. package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
  26. package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
  27. package/dist/assets/{save-BNVTgBVn.js → save-C9fKWfVB.js} +1 -1
  28. package/dist/assets/{search-CKCzwLId.js → search-D5FQ5q0X.js} +1 -1
  29. package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-AMEJJNIS.js} +1 -1
  30. package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
  31. package/dist/assets/{select-CIRwTNP4.js → select-Cu5n0E-3.js} +2 -2
  32. package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-C5aOtjxu.js} +1 -1
  33. package/dist/assets/use-config-D8TESeo6.js +1 -0
  34. package/dist/assets/x-1qETdQ70.js +1 -0
  35. package/dist/index.html +14 -14
  36. package/package.json +9 -9
  37. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
  38. package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
  39. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
  40. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
  41. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
  42. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
  43. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
  44. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
  45. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
  46. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
  47. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  48. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  49. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  50. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
  51. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  52. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  53. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  54. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  55. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  56. package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
  57. package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
  58. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  59. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  60. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  61. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  62. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  63. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  64. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  65. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  66. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  67. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  68. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  69. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  70. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  71. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  72. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
  73. package/src/features/chat/managers/chat-input.manager.ts +24 -2
  74. package/src/features/chat/stores/chat-input.store.ts +2 -0
  75. package/src/features/chat/types/chat-input-bar.types.ts +5 -0
  76. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  77. package/src/features/settings/components/config/provider-form.tsx +284 -387
  78. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  79. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  80. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  81. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  82. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  83. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  84. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  85. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  86. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  87. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  88. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  89. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  90. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  91. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  92. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  93. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  94. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  95. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  96. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  97. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  98. package/src/shared/components/ui/README.md +7 -0
  99. package/src/shared/components/ui/popover.tsx +19 -3
  100. package/src/shared/components/ui/select.tsx +14 -4
  101. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  102. package/src/shared/lib/api/index.ts +2 -0
  103. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  104. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  105. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
  106. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  107. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
  108. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  109. package/dist/assets/chat-page-BXngLJY6.js +0 -105
  110. package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
  111. package/dist/assets/index-DTb_XVIH.js +0 -103
  112. package/dist/assets/index-ayiPYhf1.css +0 -1
  113. package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
  114. package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
  115. package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
  116. package/dist/assets/plus-CYE1onU2.js +0 -1
  117. package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
  118. package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
  119. package/dist/assets/remote-BlJDbXOI.js +0 -1
  120. package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
  121. package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
  122. package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
  123. package/dist/assets/use-config-DgKM9VIU.js +0 -1
  124. package/dist/assets/x-Cm3JjYut.js +0 -1
  125. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  126. package/src/features/chat/components/chat-welcome.tsx +0 -102
@@ -35,10 +35,16 @@ export function buildModelStateHint(params: {
35
35
  "noModelOptionsLabel" | "configureProviderLabel"
36
36
  >;
37
37
  }): ChatInlineHint | null {
38
- if (!params.isModelOptionsLoading && !params.isModelOptionsEmpty) {
38
+ const {
39
+ isModelOptionsEmpty,
40
+ isModelOptionsLoading,
41
+ onGoToProviders,
42
+ texts,
43
+ } = params;
44
+ if (!isModelOptionsLoading && !isModelOptionsEmpty) {
39
45
  return null;
40
46
  }
41
- if (params.isModelOptionsLoading) {
47
+ if (isModelOptionsLoading) {
42
48
  return {
43
49
  tone: "neutral",
44
50
  loading: true,
@@ -46,25 +52,40 @@ export function buildModelStateHint(params: {
46
52
  }
47
53
  return {
48
54
  tone: "warning",
49
- text: params.texts.noModelOptionsLabel,
50
- actionLabel: params.texts.configureProviderLabel,
51
- onAction: params.onGoToProviders,
55
+ text: texts.noModelOptionsLabel,
56
+ actionLabel: texts.configureProviderLabel,
57
+ onAction: onGoToProviders,
52
58
  };
53
59
  }
54
60
 
55
61
  export function buildModelToolbarSelect({
56
- modelOptions, recentModelValues, selectedModel, isModelOptionsLoading, hasModelOptions, onValueChange, texts,
62
+ modelOptions,
63
+ favoriteModelValues,
64
+ recentModelValues,
65
+ selectedModel,
66
+ isModelOptionsLoading,
67
+ hasModelOptions,
68
+ onFavoriteToggle,
69
+ onValueChange,
70
+ texts,
57
71
  }: {
58
72
  modelOptions: ChatModelRecord[];
73
+ favoriteModelValues?: string[];
59
74
  recentModelValues?: string[];
60
75
  selectedModel: string;
61
76
  isModelOptionsLoading: boolean;
62
77
  hasModelOptions: boolean;
78
+ onFavoriteToggle?: (value: string, favorite: boolean) => void;
63
79
  onValueChange: (value: string) => void;
64
80
  texts: Pick<
65
81
  ChatInputBarAdapterTexts,
66
82
  | "modelSelectPlaceholder"
67
83
  | "modelNoOptionsLabel"
84
+ | "modelSearchPlaceholder"
85
+ | "modelSearchEmptyLabel"
86
+ | "favoriteModelsLabel"
87
+ | "favoriteModelLabel"
88
+ | "unfavoriteModelLabel"
68
89
  | "recentModelsLabel"
69
90
  | "allModelsLabel"
70
91
  >;
@@ -76,19 +97,33 @@ export function buildModelToolbarSelect({
76
97
  const resolvedValue = hasModelOptions
77
98
  ? resolvedModelOption?.value
78
99
  : undefined;
79
- const recentValueSet = new Set(recentModelValues ?? []);
80
100
  const modelOptionMap = new Map(
81
101
  modelOptions.map((option) => [option.value, option] as const),
82
102
  );
83
- const recentOptions = (recentModelValues ?? [])
103
+ const favoriteOptions = (favoriteModelValues ?? [])
84
104
  .map((value) => modelOptionMap.get(value))
85
105
  .filter((option): option is ChatModelRecord => Boolean(option));
106
+ const favoriteValueSet = new Set(favoriteOptions.map((option) => option.value));
107
+ const recentOptions = (recentModelValues ?? [])
108
+ .map((value) => modelOptionMap.get(value))
109
+ .filter(
110
+ (option): option is ChatModelRecord =>
111
+ option !== undefined && !favoriteValueSet.has(option.value),
112
+ );
113
+ const recentValueSet = new Set(recentOptions.map((option) => option.value));
86
114
  const remainingOptions = modelOptions.filter(
87
- (option) => !recentValueSet.has(option.value),
115
+ (option) => !favoriteValueSet.has(option.value) && !recentValueSet.has(option.value),
88
116
  );
89
- const optionGroups =
90
- recentOptions.length > 0
91
- ? [
117
+ const optionGroups = favoriteOptions.length > 0 || recentOptions.length > 0
118
+ ? [
119
+ {
120
+ key: "favorite-models",
121
+ label: texts.favoriteModelsLabel,
122
+ options: favoriteOptions.map((option) => ({
123
+ value: option.value,
124
+ label: formatModelOptionLabel(option),
125
+ })),
126
+ },
92
127
  {
93
128
  key: "recent-models",
94
129
  label: texts.recentModelsLabel,
@@ -106,7 +141,7 @@ export function buildModelToolbarSelect({
106
141
  })),
107
142
  },
108
143
  ].filter((group) => group.options.length > 0)
109
- : undefined;
144
+ : undefined;
110
145
 
111
146
  return {
112
147
  key: "model",
@@ -124,6 +159,19 @@ export function buildModelToolbarSelect({
124
159
  disabled: !hasModelOptions,
125
160
  loading: isModelOptionsLoading,
126
161
  emptyLabel: texts.modelNoOptionsLabel,
162
+ search: {
163
+ placeholder: texts.modelSearchPlaceholder,
164
+ emptyLabel: texts.modelSearchEmptyLabel,
165
+ },
166
+ optionAction: onFavoriteToggle
167
+ ? {
168
+ kind: "favorite",
169
+ activeValues: favoriteOptions.map((option) => option.value),
170
+ activeLabel: texts.unfavoriteModelLabel,
171
+ inactiveLabel: texts.favoriteModelLabel,
172
+ onToggle: onFavoriteToggle,
173
+ }
174
+ : undefined,
127
175
  onValueChange,
128
176
  };
129
177
  }
@@ -135,31 +183,38 @@ export function buildThinkingToolbarSelect(params: {
135
183
  onValueChange: (value: ChatThinkingLevel) => void;
136
184
  texts: Pick<ChatInputBarAdapterTexts, "thinkingLabels">;
137
185
  }): ChatToolbarSelect | null {
138
- if (params.supportedLevels.length === 0) {
186
+ const {
187
+ defaultThinkingLevel,
188
+ onValueChange,
189
+ selectedThinkingLevel,
190
+ supportedLevels,
191
+ texts,
192
+ } = params;
193
+ if (supportedLevels.length === 0) {
139
194
  return null;
140
195
  }
141
196
 
142
- const options = normalizeThinkingLevels(params.supportedLevels);
197
+ const options = normalizeThinkingLevels(supportedLevels);
143
198
  const fallback = options.includes("off") ? "off" : options[0];
144
199
  const resolvedValue =
145
- (params.selectedThinkingLevel &&
146
- options.includes(params.selectedThinkingLevel) &&
147
- params.selectedThinkingLevel) ||
148
- (params.defaultThinkingLevel &&
149
- options.includes(params.defaultThinkingLevel) &&
150
- params.defaultThinkingLevel) ||
200
+ (selectedThinkingLevel &&
201
+ options.includes(selectedThinkingLevel) &&
202
+ selectedThinkingLevel) ||
203
+ (defaultThinkingLevel &&
204
+ options.includes(defaultThinkingLevel) &&
205
+ defaultThinkingLevel) ||
151
206
  fallback;
152
207
 
153
208
  return {
154
209
  key: "thinking",
155
210
  value: resolvedValue,
156
- placeholder: params.texts.thinkingLabels[resolvedValue],
157
- selectedLabel: params.texts.thinkingLabels[resolvedValue],
211
+ placeholder: texts.thinkingLabels[resolvedValue],
212
+ selectedLabel: texts.thinkingLabels[resolvedValue],
158
213
  icon: "brain",
159
214
  options: options.map((level) => ({
160
215
  value: level,
161
- label: params.texts.thinkingLabels[level],
216
+ label: texts.thinkingLabels[level],
162
217
  })),
163
- onValueChange: (value) => params.onValueChange(value as ChatThinkingLevel),
218
+ onValueChange: (value) => onValueChange(value as ChatThinkingLevel),
164
219
  };
165
220
  }
@@ -0,0 +1,310 @@
1
+ import { ToolInvocationStatus } from "@nextclaw/agent-chat";
2
+ import { adapt, type ChatMessageSource } from "./chat-message-test.utils";
3
+
4
+ it("builds edit-file previews from structured args before the tool finishes", () => {
5
+ const adapted = adapt([
6
+ {
7
+ id: "assistant-edit-preview",
8
+ role: "assistant",
9
+ parts: [
10
+ {
11
+ type: "tool-invocation",
12
+ toolInvocation: {
13
+ status: ToolInvocationStatus.CALL,
14
+ toolCallId: "edit-call-1",
15
+ toolName: "edit_file",
16
+ args: JSON.stringify({
17
+ path: "src/app.ts",
18
+ oldText: "const color = 'red';",
19
+ newText: "const color = 'blue';",
20
+ }),
21
+ parsedArgs: {
22
+ path: "src/app.ts",
23
+ oldText: "const color = 'red';",
24
+ newText: "const color = 'blue';",
25
+ },
26
+ },
27
+ },
28
+ ],
29
+ },
30
+ ] as unknown as ChatMessageSource[]);
31
+
32
+ const editLines =
33
+ adapted[0]?.parts[0]?.type === "tool-card"
34
+ ? (adapted[0].parts[0].card.fileOperation?.blocks[0]?.lines ?? [])
35
+ : [];
36
+
37
+ expect(adapted[0]?.parts[0]).toMatchObject({
38
+ type: "tool-card",
39
+ card: {
40
+ toolName: "edit_file",
41
+ summary: "src/app.ts",
42
+ statusTone: "running",
43
+ fileOperation: {
44
+ blocks: [
45
+ {
46
+ path: "src/app.ts",
47
+ lines: [
48
+ {
49
+ kind: "remove",
50
+ text: "const color = 'red';",
51
+ },
52
+ {
53
+ kind: "add",
54
+ text: "const color = 'blue';",
55
+ },
56
+ ],
57
+ },
58
+ ],
59
+ },
60
+ },
61
+ });
62
+ expect(editLines[0]).not.toHaveProperty("oldLineNumber");
63
+ expect(editLines[1]).not.toHaveProperty("newLineNumber");
64
+ });
65
+
66
+ it("uses structured edit-file result line numbers after the tool finishes", () => {
67
+ const adapted = adapt([
68
+ {
69
+ id: "assistant-edit-result",
70
+ role: "assistant",
71
+ parts: [
72
+ {
73
+ type: "tool-invocation",
74
+ toolInvocation: {
75
+ status: ToolInvocationStatus.RESULT,
76
+ toolCallId: "edit-result-1",
77
+ toolName: "edit_file",
78
+ args: JSON.stringify({
79
+ path: "src/app.ts",
80
+ oldText: "const color = 'red';",
81
+ newText: "const color = 'blue';",
82
+ }),
83
+ parsedArgs: {
84
+ path: "src/app.ts",
85
+ oldText: "const color = 'red';",
86
+ newText: "const color = 'blue';",
87
+ },
88
+ result: {
89
+ path: "src/app.ts",
90
+ oldStartLine: 27,
91
+ newStartLine: 27,
92
+ message: "Edited src/app.ts",
93
+ },
94
+ },
95
+ },
96
+ ],
97
+ },
98
+ ] as unknown as ChatMessageSource[]);
99
+
100
+ expect(adapted[0]?.parts[0]).toMatchObject({
101
+ type: "tool-card",
102
+ card: {
103
+ toolName: "edit_file",
104
+ summary: "src/app.ts",
105
+ statusTone: "success",
106
+ fileOperation: {
107
+ blocks: [
108
+ {
109
+ path: "src/app.ts",
110
+ lines: [
111
+ {
112
+ kind: "remove",
113
+ text: "const color = 'red';",
114
+ oldLineNumber: 27,
115
+ },
116
+ {
117
+ kind: "add",
118
+ text: "const color = 'blue';",
119
+ newLineNumber: 27,
120
+ },
121
+ ],
122
+ },
123
+ ],
124
+ },
125
+ },
126
+ });
127
+ });
128
+
129
+ it("builds write-file previews from partial native args before the JSON is complete", () => {
130
+ const adapted = adapt([
131
+ {
132
+ id: "assistant-write-preview",
133
+ role: "assistant",
134
+ parts: [
135
+ {
136
+ type: "tool-invocation",
137
+ toolInvocation: {
138
+ status: ToolInvocationStatus.PARTIAL_CALL,
139
+ toolCallId: "write-call-1",
140
+ toolName: "write_file",
141
+ args: '{"path":"games/snake.html","content":"<!DOCTYPE html>\\n<canvas id=\\"game\\"></canvas>\\n<script>const score = 1;',
142
+ },
143
+ },
144
+ ],
145
+ },
146
+ ] as unknown as ChatMessageSource[]);
147
+
148
+ expect(adapted[0]?.parts[0]).toMatchObject({
149
+ type: "tool-card",
150
+ card: {
151
+ toolName: "write_file",
152
+ summary: "games/snake.html",
153
+ statusTone: "running",
154
+ statusLabel: "Running",
155
+ fileOperation: {
156
+ blocks: [
157
+ {
158
+ display: "preview",
159
+ path: "games/snake.html",
160
+ lines: expect.arrayContaining([
161
+ expect.objectContaining({
162
+ kind: "add",
163
+ text: "<!DOCTYPE html>",
164
+ }),
165
+ expect.objectContaining({
166
+ kind: "add",
167
+ text: '<canvas id="game"></canvas>',
168
+ }),
169
+ ]),
170
+ },
171
+ ],
172
+ },
173
+ },
174
+ });
175
+ });
176
+
177
+ it("keeps completed write-file cards in preview mode instead of falling back to raw byte summaries", () => {
178
+ const adapted = adapt([
179
+ {
180
+ id: "assistant-write-result",
181
+ role: "assistant",
182
+ parts: [
183
+ {
184
+ type: "tool-invocation",
185
+ toolInvocation: {
186
+ status: ToolInvocationStatus.RESULT,
187
+ toolCallId: "write-result-1",
188
+ toolName: "write_file",
189
+ args: JSON.stringify({
190
+ path: "games/snake.html",
191
+ content: '<!DOCTYPE html>\n<canvas id="game"></canvas>',
192
+ }),
193
+ result: "Wrote 3906 bytes to games/snake.html",
194
+ },
195
+ },
196
+ ],
197
+ },
198
+ ] as unknown as ChatMessageSource[]);
199
+
200
+ expect(adapted[0]?.parts[0]).toMatchObject({
201
+ type: "tool-card",
202
+ card: {
203
+ toolName: "write_file",
204
+ summary: "games/snake.html",
205
+ statusTone: "success",
206
+ fileOperation: {
207
+ blocks: [
208
+ {
209
+ display: "preview",
210
+ path: "games/snake.html",
211
+ lines: [
212
+ {
213
+ kind: "add",
214
+ text: "<!DOCTYPE html>",
215
+ newLineNumber: 1,
216
+ },
217
+ {
218
+ kind: "add",
219
+ text: '<canvas id="game"></canvas>',
220
+ newLineNumber: 2,
221
+ },
222
+ ],
223
+ },
224
+ ],
225
+ },
226
+ },
227
+ });
228
+ expect(adapted[0]?.parts[0]).not.toMatchObject({
229
+ type: "tool-card",
230
+ card: {
231
+ output: "Wrote 3906 bytes to games/snake.html",
232
+ },
233
+ });
234
+ });
235
+
236
+ it("renders codex file_change results as structured diff previews", () => {
237
+ const adapted = adapt([
238
+ {
239
+ id: "assistant-file-change",
240
+ role: "assistant",
241
+ parts: [
242
+ {
243
+ type: "tool-invocation",
244
+ toolInvocation: {
245
+ status: ToolInvocationStatus.RESULT,
246
+ toolCallId: "file-change-1",
247
+ toolName: "file_change",
248
+ args: JSON.stringify({
249
+ changes: [
250
+ {
251
+ path: "src/main.ts",
252
+ diff: [
253
+ "--- a/src/main.ts",
254
+ "+++ b/src/main.ts",
255
+ "@@ -109,1 +109,1 @@",
256
+ "-console.log('old');",
257
+ "+console.log('new');",
258
+ ].join("\n"),
259
+ },
260
+ ],
261
+ }),
262
+ result: {
263
+ status: "completed",
264
+ changes: [
265
+ {
266
+ path: "src/main.ts",
267
+ diff: [
268
+ "--- a/src/main.ts",
269
+ "+++ b/src/main.ts",
270
+ "@@ -109,1 +109,1 @@",
271
+ "-console.log('old');",
272
+ "+console.log('new');",
273
+ ].join("\n"),
274
+ },
275
+ ],
276
+ },
277
+ },
278
+ },
279
+ ],
280
+ },
281
+ ] as unknown as ChatMessageSource[]);
282
+
283
+ expect(adapted[0]?.parts[0]).toMatchObject({
284
+ type: "tool-card",
285
+ card: {
286
+ toolName: "file_change",
287
+ summary: "src/main.ts",
288
+ statusTone: "success",
289
+ fileOperation: {
290
+ blocks: [
291
+ {
292
+ path: "src/main.ts",
293
+ lines: [
294
+ {
295
+ kind: "remove",
296
+ text: "console.log('old');",
297
+ oldLineNumber: 109,
298
+ },
299
+ {
300
+ kind: "add",
301
+ text: "console.log('new');",
302
+ newLineNumber: 109,
303
+ },
304
+ ],
305
+ },
306
+ ],
307
+ },
308
+ },
309
+ });
310
+ });
@@ -0,0 +1,43 @@
1
+ import type { UiMessage } from "@nextclaw/agent-chat";
2
+ import {
3
+ adaptChatMessages,
4
+ type ChatMessageSource,
5
+ } from "@/features/chat/features/message/utils/chat-message.utils";
6
+
7
+ export type { ChatMessageSource } from "@/features/chat/features/message/utils/chat-message.utils";
8
+
9
+ export function toSource(uiMessages: UiMessage[]): ChatMessageSource[] {
10
+ return uiMessages as unknown as ChatMessageSource[];
11
+ }
12
+
13
+ const defaultTexts = {
14
+ roleLabels: {
15
+ user: "You",
16
+ assistant: "Assistant",
17
+ tool: "Tool",
18
+ system: "System",
19
+ fallback: "Message",
20
+ },
21
+ reasoningLabel: "Reasoning",
22
+ toolCallLabel: "Tool Call",
23
+ toolResultLabel: "Tool Result",
24
+ toolInputLabel: "Input",
25
+ toolNoOutputLabel: "No output",
26
+ toolOutputLabel: "Output",
27
+ toolStatusPreparingLabel: "Preparing",
28
+ toolStatusRunningLabel: "Running",
29
+ toolStatusCompletedLabel: "Completed",
30
+ toolStatusFailedLabel: "Failed",
31
+ toolStatusCancelledLabel: "Cancelled",
32
+ imageAttachmentLabel: "Image attachment",
33
+ fileAttachmentLabel: "File attachment",
34
+ unknownPartLabel: "Unknown Part",
35
+ };
36
+
37
+ export function adapt(uiMessages: ChatMessageSource[]) {
38
+ return adaptChatMessages({
39
+ uiMessages,
40
+ formatTimestamp: (value) => `formatted:${value}`,
41
+ texts: defaultTexts,
42
+ });
43
+ }