@parallelworks/ui 0.51.0 → 0.52.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 (227) hide show
  1. package/README.md +155 -14
  2. package/dist/ai/agent/toolStrings.d.ts +2 -0
  3. package/dist/ai/agent/toolStrings.js +20 -18
  4. package/dist/ai/ai.css +12 -0
  5. package/dist/ai/components/AllocationSelector.js +5 -1
  6. package/dist/ai/components/AttachmentManager.js +51 -42
  7. package/dist/ai/components/AttachmentUpload.d.ts +4 -2
  8. package/dist/ai/components/AttachmentUpload.js +9 -9
  9. package/dist/ai/components/BlockedGroupBanner.js +8 -14
  10. package/dist/ai/components/BranchNavigator.js +11 -12
  11. package/dist/ai/components/ChatEmptyState.js +12 -19
  12. package/dist/ai/components/ChatInput.d.ts +4 -3
  13. package/dist/ai/components/ChatInput.js +27 -13
  14. package/dist/ai/components/ChatMessage.js +45 -121
  15. package/dist/ai/components/ChatMessageList.d.ts +5 -2
  16. package/dist/ai/components/ChatMessageList.js +89 -51
  17. package/dist/ai/components/ChatSidebar.js +11 -7
  18. package/dist/ai/components/ChatThread.js +1 -13
  19. package/dist/ai/components/DragOverlay.js +3 -3
  20. package/dist/ai/components/MessageAttachments.d.ts +10 -0
  21. package/dist/ai/components/MessageAttachments.js +78 -0
  22. package/dist/ai/components/ModelSelector.js +3 -3
  23. package/dist/ai/components/PastedText.js +2 -2
  24. package/dist/ai/components/ProviderIssueBanner.js +19 -23
  25. package/dist/ai/components/ShareDialog.js +2 -2
  26. package/dist/ai/components/agent/ActivityLine.d.ts +0 -2
  27. package/dist/ai/components/agent/ActivityLine.js +8 -51
  28. package/dist/ai/components/agent/AgentMessageParts.js +114 -53
  29. package/dist/ai/core/ChatProvider.d.ts +16 -5
  30. package/dist/ai/core/ChatProvider.js +41 -8
  31. package/dist/ai/core/config.js +4 -3
  32. package/dist/ai/index.d.ts +1 -1
  33. package/dist/ai/index.js +3 -3
  34. package/dist/ai/strings.d.ts +29 -3
  35. package/dist/ai/strings.js +81 -26
  36. package/dist/ai/types.d.ts +4 -0
  37. package/dist/ai/ui/Notice.d.ts +2 -2
  38. package/dist/ai/ui/Notice.js +29 -62
  39. package/dist/ai/ui/sidebar.d.ts +5 -3
  40. package/dist/ai/ui/sidebar.js +21 -9
  41. package/dist/components/ActionMenu.d.ts +1 -0
  42. package/dist/components/ActionMenu.js +6 -1
  43. package/dist/components/AppLink.d.ts +26 -0
  44. package/dist/components/AppLink.js +51 -0
  45. package/dist/components/Avatar.d.ts +2 -1
  46. package/dist/components/Avatar.js +4 -2
  47. package/dist/components/Button.d.ts +15 -5
  48. package/dist/components/Button.js +47 -40
  49. package/dist/components/Callout.d.ts +31 -7
  50. package/dist/components/Callout.js +50 -19
  51. package/dist/components/Card.d.ts +11 -6
  52. package/dist/components/Card.js +14 -8
  53. package/dist/components/Checkbox.js +2 -1
  54. package/dist/components/ColorInput.js +5 -3
  55. package/dist/components/Combobox.js +49 -19
  56. package/dist/components/ConfirmModal.d.ts +19 -8
  57. package/dist/components/ConfirmModal.js +31 -53
  58. package/dist/components/CopyButton.d.ts +38 -0
  59. package/dist/components/CopyButton.js +68 -0
  60. package/dist/components/CreateModal.js +118 -111
  61. package/dist/components/DatePicker.js +12 -8
  62. package/dist/components/DescriptionList.d.ts +25 -5
  63. package/dist/components/DescriptionList.js +56 -22
  64. package/dist/components/Disclosure.js +1 -1
  65. package/dist/components/Drawer.js +10 -16
  66. package/dist/components/Field.d.ts +15 -5
  67. package/dist/components/Field.js +40 -8
  68. package/dist/components/IconButton.d.ts +14 -5
  69. package/dist/components/IconButton.js +25 -14
  70. package/dist/components/InfoTip.d.ts +4 -1
  71. package/dist/components/InfoTip.js +2 -1
  72. package/dist/components/Input.d.ts +14 -8
  73. package/dist/components/Input.js +46 -31
  74. package/dist/components/Loader.js +1 -1
  75. package/dist/components/MenuEntries.d.ts +4 -0
  76. package/dist/components/MenuEntries.js +11 -12
  77. package/dist/components/Meter.js +5 -4
  78. package/dist/components/Modal.d.ts +28 -4
  79. package/dist/components/Modal.js +37 -14
  80. package/dist/components/Overview.d.ts +20 -12
  81. package/dist/components/Overview.js +14 -9
  82. package/dist/components/PageHeader.d.ts +47 -65
  83. package/dist/components/PageHeader.js +275 -153
  84. package/dist/components/PageHeaderAction.d.ts +67 -0
  85. package/dist/components/PageHeaderAction.js +107 -0
  86. package/dist/components/Pagination.js +6 -16
  87. package/dist/components/Popover.js +3 -2
  88. package/dist/components/Provider.d.ts +77 -19
  89. package/dist/components/Provider.js +139 -130
  90. package/dist/components/Radio.js +2 -1
  91. package/dist/components/RefreshButton.js +7 -8
  92. package/dist/components/RequiredMark.js +1 -1
  93. package/dist/components/Section.js +1 -1
  94. package/dist/components/Select.d.ts +4 -9
  95. package/dist/components/Select.js +24 -11
  96. package/dist/components/SettingModeOption.d.ts +1 -1
  97. package/dist/components/SettingModeOption.js +7 -1
  98. package/dist/components/SettingsCard.d.ts +5 -3
  99. package/dist/components/SettingsCard.js +27 -20
  100. package/dist/components/Slider.js +1 -1
  101. package/dist/components/StatusBadge.js +2 -2
  102. package/dist/components/Switch.js +5 -4
  103. package/dist/components/Table.d.ts +30 -20
  104. package/dist/components/Table.js +50 -73
  105. package/dist/components/Tabs.d.ts +3 -7
  106. package/dist/components/Tabs.js +6 -10
  107. package/dist/components/ThemeEditor.js +18 -8
  108. package/dist/components/Toast.d.ts +11 -3
  109. package/dist/components/Toast.js +45 -20
  110. package/dist/components/buttonClasses.js +1 -1
  111. package/dist/components/comboboxItems.d.ts +4 -0
  112. package/dist/components/comboboxItems.js +6 -1
  113. package/dist/components/dismiss.d.ts +12 -0
  114. package/dist/components/dismiss.js +19 -0
  115. package/dist/components/fieldBox.d.ts +4 -0
  116. package/dist/components/fieldBox.js +7 -3
  117. package/dist/components/floating.d.ts +3 -3
  118. package/dist/components/floating.js +1 -1
  119. package/dist/components/markdownCodeBreaks.js +3 -4
  120. package/dist/components/pageHeaderStore.d.ts +41 -0
  121. package/dist/components/pageHeaderStore.js +67 -0
  122. package/dist/components/pathBreaks.d.ts +8 -0
  123. package/dist/components/pathBreaks.js +28 -0
  124. package/dist/components/segmented.js +1 -1
  125. package/dist/components/shortcutKey.d.ts +2 -1
  126. package/dist/components/shortcutKey.js +19 -2
  127. package/dist/components/splitter.js +1 -1
  128. package/dist/components/useCssIsDark.js +40 -18
  129. package/dist/components/useTheme.js +9 -10
  130. package/dist/dev.d.ts +9 -0
  131. package/dist/dev.js +14 -0
  132. package/dist/editor/Editor.d.ts +3 -2
  133. package/dist/editor/Editor.js +11 -2
  134. package/dist/editor/EditorField.d.ts +2 -1
  135. package/dist/editor/EditorField.js +11 -29
  136. package/dist/editor/FormCodePanel.d.ts +7 -3
  137. package/dist/editor/FormCodePanel.js +14 -7
  138. package/dist/editor/Monaco.d.ts +14 -5
  139. package/dist/editor/Monaco.js +163 -112
  140. package/dist/editor/index.d.ts +5 -5
  141. package/dist/editor/index.js +3 -3
  142. package/dist/editor/themes.d.ts +8 -5
  143. package/dist/editor/themes.js +58 -30
  144. package/dist/editor/yaml.d.ts +6 -2
  145. package/dist/editor/yaml.js +11 -2
  146. package/dist/file-explorer/FileExplorer.js +143 -124
  147. package/dist/file-explorer/FileExplorerContext.js +1 -1
  148. package/dist/file-explorer/FilePreview/StreamedBadge.js +3 -1
  149. package/dist/file-explorer/FilePreview/Viewer.js +2 -2
  150. package/dist/file-explorer/FilePreview/index.js +4 -4
  151. package/dist/file-explorer/FileRow.js +1 -1
  152. package/dist/file-explorer/FileRowsWindow.js +34 -3
  153. package/dist/file-explorer/FileTable.js +3 -1
  154. package/dist/file-explorer/TreeView.js +37 -14
  155. package/dist/file-explorer/helpers.d.ts +2 -1
  156. package/dist/file-explorer/helpers.js +21 -20
  157. package/dist/file-explorer/toasts.js +27 -19
  158. package/dist/file-explorer/treeRows.d.ts +6 -0
  159. package/dist/file-explorer/treeRows.js +19 -1
  160. package/dist/file-explorer/useNearEnd.js +3 -6
  161. package/dist/form/Fields/CheckboxGroupField.js +12 -5
  162. package/dist/form/Fields/DurationField.js +1 -1
  163. package/dist/form/Fields/LabelField.js +1 -1
  164. package/dist/form/Fields/ObjectField.d.ts +1 -1
  165. package/dist/form/Fields/ObjectField.js +3 -2
  166. package/dist/form/Fields/StringField.js +16 -29
  167. package/dist/form/Fields/TextAreaField.js +3 -4
  168. package/dist/form/Form.js +3 -8
  169. package/dist/form/Wizard/WizardNavigation.js +0 -3
  170. package/dist/form/Wizard/utils.js +1 -1
  171. package/dist/form/fieldFrame.d.ts +1 -1
  172. package/dist/form/fieldFrame.js +2 -2
  173. package/dist/form/types/fieldComponentTypes.d.ts +2 -1
  174. package/dist/graph/AnnotationBanner.d.ts +1 -2
  175. package/dist/graph/AnnotationBanner.js +30 -32
  176. package/dist/graph/Connectors.js +4 -1
  177. package/dist/graph/DependencyGraph.js +2 -2
  178. package/dist/graph/GraphEditor.js +11 -9
  179. package/dist/graph/GraphEditorDialogs.js +1 -1
  180. package/dist/graph/GraphNode.js +7 -4
  181. package/dist/graph/InputDialog.js +1 -1
  182. package/dist/graph/InputsEditor.js +14 -11
  183. package/dist/graph/MatrixGroup.js +9 -3
  184. package/dist/graph/Reveal.js +18 -10
  185. package/dist/graph/ShortcutsButton.d.ts +11 -2
  186. package/dist/graph/ShortcutsButton.js +14 -3
  187. package/dist/graph/TreeView.js +35 -18
  188. package/dist/graph/WorkflowSettingsDialog.js +1 -1
  189. package/dist/graph/editorFields.js +1 -1
  190. package/dist/graph/editorRows.js +16 -10
  191. package/dist/graph/editorStrings.d.ts +7 -0
  192. package/dist/graph/editorStrings.js +8 -1
  193. package/dist/graph/inputRefs.js +1 -1
  194. package/dist/graph/settingsViews.js +11 -18
  195. package/dist/graph/stepWith.js +1 -1
  196. package/dist/icons.d.ts +2 -0
  197. package/dist/icons.js +4 -2
  198. package/dist/index.d.ts +12 -12
  199. package/dist/index.js +16 -16
  200. package/dist/list/ActionToolbar.d.ts +42 -6
  201. package/dist/list/ActionToolbar.js +297 -101
  202. package/dist/list/ListPage.d.ts +15 -23
  203. package/dist/list/ListPage.js +42 -43
  204. package/dist/list/ListTable.d.ts +32 -7
  205. package/dist/list/ListTable.js +48 -12
  206. package/dist/list/ListViewControls.js +1 -1
  207. package/dist/list/UserHoverCard.js +7 -15
  208. package/dist/list/index.d.ts +2 -2
  209. package/dist/list/index.js +2 -2
  210. package/dist/list/listSearch.js +12 -10
  211. package/dist/list/listView.d.ts +10 -1
  212. package/dist/list/listView.js +15 -1
  213. package/dist/logviewer/LogViewer.d.ts +4 -2
  214. package/dist/logviewer/LogViewer.js +87 -69
  215. package/dist/logviewer/logviewer.css +3 -2
  216. package/dist/reducedMotion.d.ts +5 -0
  217. package/dist/reducedMotion.js +20 -0
  218. package/dist/safeUrl.d.ts +1 -1
  219. package/dist/safeUrl.js +4 -2
  220. package/dist/styles.css +1 -1
  221. package/package.json +1 -1
  222. package/dist/components/Breadcrumbs.d.ts +0 -37
  223. package/dist/components/Breadcrumbs.js +0 -90
  224. package/dist/components/CopyToClipboard.d.ts +0 -17
  225. package/dist/components/CopyToClipboard.js +0 -66
  226. package/dist/components/Sort.d.ts +0 -9
  227. package/dist/components/Sort.js +0 -42
@@ -4,17 +4,36 @@ import { useStrings } from "../../components/Provider.js";
4
4
  import { formatDuration } from "../../format.js";
5
5
  import { useElapsed } from "../../components/relativeTime.js";
6
6
  import { useChatConfig } from "../core/config.js";
7
- import { useChat } from "../core/ChatProvider.js";
7
+ import { useChat, useChatStream } from "../core/ChatProvider.js";
8
8
  import Markdown from "../ui/Markdown.js";
9
9
  import { Reasoning, ReasoningBody } from "./Reasoning.js";
10
10
  import AgentMessageParts from "./agent/AgentMessageParts.js";
11
11
  import ChatMessage_default from "./ChatMessage.js";
12
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
13
+ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
14
14
  //#region src/ai/components/ChatMessageList.tsx
15
- function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, canRemoveQueued, queueDelivery = "afterReply", editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator = true }) {
16
- const { adapter, isStreaming: providerStreaming, isThinking, streamingReasoning, streamingParts, thinkingStartTime } = useChat();
15
+ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMessageProp, queuedMessages, onRemoveQueued, canRemoveQueued, queueDelivery = "afterReply", editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator = true }) {
16
+ const { adapter, isStreaming: providerStreaming } = useChat();
17
+ const { isThinking, streamingContent, streamingReasoning, streamingParts, thinkingStartTime } = useChatStream();
17
18
  const isStreaming = isStreamingProp ?? providerStreaming;
19
+ const conversationId = messages[0]?.conversationId;
20
+ const providerStreamingMessage = useMemo(() => providerStreaming && streamingContent ? {
21
+ id: "streaming",
22
+ role: "assistant",
23
+ content: streamingContent,
24
+ timestamp: "",
25
+ ...conversationId ? { conversationId } : {}
26
+ } : null, [
27
+ providerStreaming,
28
+ streamingContent,
29
+ conversationId
30
+ ]);
31
+ const streamingMessage = streamingMessageProp === void 0 ? providerStreamingMessage : streamingMessageProp;
32
+ const streamingPartsMessage = useMemo(() => ({
33
+ id: "streaming-parts",
34
+ role: "assistant",
35
+ parts: streamingParts
36
+ }), [streamingParts]);
18
37
  const { strings } = useChatConfig();
19
38
  const { common } = useStrings();
20
39
  const tThinking = strings.thinking;
@@ -29,15 +48,8 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
29
48
  const bottomRef = useRef(null);
30
49
  const containerRef = useRef(null);
31
50
  const reasoningScrollRef = useRef(null);
32
- const knownMessageIdsRef = useRef(/* @__PURE__ */ new Set());
33
- const prevMessageCountRef = useRef(messages.length);
34
- useEffect(() => {
35
- const prev = prevMessageCountRef.current;
36
- if (messages.length > 0 && (prev === 0 || messages.length < prev)) knownMessageIdsRef.current = new Set(messages.map((m) => m.id));
37
- prevMessageCountRef.current = messages.length;
38
- }, [messages]);
39
51
  const messageCount = messages.length;
40
- const streamingContent = streamingMessage?.content;
52
+ const streamingText = streamingMessage?.content;
41
53
  const streamingMessageParts = streamingMessage?.parts;
42
54
  const [finalThinkingDuration, setFinalThinkingDuration] = useState(null);
43
55
  const [reasoningOpen, setReasoningOpen] = useState(false);
@@ -124,7 +136,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
124
136
  if (!containerRef.current || !isNearBottomRef.current || !isStreaming || hasQueuedMessages) return;
125
137
  containerRef.current.scrollTop = containerRef.current.scrollHeight;
126
138
  }, [
127
- streamingContent,
139
+ streamingText,
128
140
  streamingParts,
129
141
  streamingMessageParts,
130
142
  messageCount,
@@ -133,9 +145,8 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
133
145
  ]);
134
146
  useEffect(() => {
135
147
  if (!containerRef.current || !isNearBottomRef.current || isStreaming || !hadMessagesRef.current) return;
136
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
148
+ bottomRef.current?.scrollIntoView({ behavior: prefersReducedMotion() ? "auto" : "smooth" });
137
149
  }, [messageCount, isStreaming]);
138
- const lastIndex = messages.findLastIndex((m) => !m.pseudo);
139
150
  if (messages.length === 0 && !streamingMessage && !hasQueuedMessages) return /* @__PURE__ */ jsxs("div", {
140
151
  className: "chat-thread flex-1 overflow-y-auto flex flex-col bg-(--theme-panel-bg)",
141
152
  children: [
@@ -152,42 +163,33 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
152
163
  });
153
164
  return /* @__PURE__ */ jsxs("div", {
154
165
  ref: containerRef,
155
- ...label ? {
156
- role: "log",
157
- "aria-relevant": "additions",
158
- "aria-label": label
159
- } : {},
166
+ role: "log",
167
+ "aria-relevant": "additions",
168
+ "aria-label": label ?? strings.thread.label,
169
+ "aria-busy": isStreaming || void 0,
160
170
  className: "chat-thread flex-1 overflow-y-auto theme-scrollbar flex flex-col bg-(--theme-panel-bg)",
161
171
  children: [
162
172
  /* @__PURE__ */ jsxs("div", {
163
173
  className: "w-full max-w-[var(--chat-column,50rem)] mx-auto pt-4 pb-6 flex-1 px-4",
164
174
  children: [
165
- messages.map((message, i) => {
166
- if (message.pseudo) return null;
167
- const isNew = !knownMessageIdsRef.current.has(message.id);
168
- if (isNew) knownMessageIdsRef.current.add(message.id);
169
- return /* @__PURE__ */ jsx("div", {
170
- className: isNew ? "animate-chat-enter" : "",
171
- children: /* @__PURE__ */ jsx(ChatMessage_default, {
172
- message,
173
- allMessages,
174
- flush,
175
- latest: i === lastIndex && !streamingMessage,
176
- isEditing: editingMessageId === message.id,
177
- showAuthor,
178
- currentUsername,
179
- attachmentDownloadUrl,
180
- onOpenAttachment,
181
- onNavigateBranch,
182
- onEdit: onEditMessage,
183
- onSaveEdit,
184
- onCancelEdit,
185
- onRegenerate: onRegenerateMessage,
186
- onRetry: onRetryMessage,
187
- onOpenReasoning,
188
- onApprovalAnswer
189
- })
190
- }, message.id);
175
+ /* @__PURE__ */ jsx(MessageRows, {
176
+ messages,
177
+ allMessages,
178
+ streaming: !!streamingMessage,
179
+ flush,
180
+ editingMessageId,
181
+ showAuthor,
182
+ currentUsername,
183
+ attachmentDownloadUrl,
184
+ onOpenAttachment,
185
+ onNavigateBranch,
186
+ onEditMessage,
187
+ onSaveEdit,
188
+ onCancelEdit,
189
+ onRegenerateMessage,
190
+ onRetryMessage,
191
+ onOpenReasoning,
192
+ onApprovalAnswer
191
193
  }),
192
194
  isStreaming && streamingReasoning && /* @__PURE__ */ jsx("div", {
193
195
  className: "animate-chat-enter",
@@ -223,11 +225,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
223
225
  isStreaming && streamingParts.length > 0 && /* @__PURE__ */ jsx("div", {
224
226
  className: cx("animate-chat-enter py-1", !flush && "px-4"),
225
227
  children: /* @__PURE__ */ jsx(AgentMessageParts, {
226
- message: {
227
- id: "streaming-parts",
228
- role: "assistant",
229
- parts: streamingParts
230
- },
228
+ message: streamingPartsMessage,
231
229
  isStreaming
232
230
  })
233
231
  }, "streaming-parts"),
@@ -307,6 +305,9 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
307
305
  ]
308
306
  });
309
307
  }
308
+ function prefersReducedMotion() {
309
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
310
+ }
310
311
  /** "Thinking (12s)" while it thinks, then how long it took. Its own component,
311
312
  * so the clock re-renders only this label, not the list. */
312
313
  function ThinkingLabel({ startedAt, finalDuration }) {
@@ -315,5 +316,42 @@ function ThinkingLabel({ startedAt, finalDuration }) {
315
316
  if (startedAt) return elapsed ? `${t.label} (${formatDuration(elapsed)})` : t.ellipsis;
316
317
  return t.thoughtFor(formatDuration(finalDuration ?? 0));
317
318
  }
319
+ /** The conversation so far. Memoized apart from the list, which re-renders
320
+ * for every streamed token, so a long conversation is not walked per token. */
321
+ var MessageRows = memo(function MessageRows({ messages, allMessages, streaming, flush, editingMessageId, showAuthor, currentUsername, attachmentDownloadUrl, onOpenAttachment, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer }) {
322
+ const shownRef = useRef(/* @__PURE__ */ new Set());
323
+ const shown = shownRef.current;
324
+ const appended = shown.size > 0 && messages.some((m) => shown.has(m.id));
325
+ useLayoutEffect(() => {
326
+ shownRef.current = new Set(messages.map((m) => m.id));
327
+ }, [messages]);
328
+ const lastIndex = messages.findLastIndex((m) => !m.pseudo);
329
+ return messages.map((message, i) => {
330
+ if (message.pseudo) return null;
331
+ const isNew = appended && !shown.has(message.id);
332
+ return /* @__PURE__ */ jsx("div", {
333
+ className: isNew ? "animate-chat-enter" : void 0,
334
+ children: /* @__PURE__ */ jsx(ChatMessage_default, {
335
+ message,
336
+ allMessages,
337
+ flush,
338
+ latest: i === lastIndex && !streaming,
339
+ isEditing: editingMessageId === message.id,
340
+ showAuthor,
341
+ currentUsername,
342
+ attachmentDownloadUrl,
343
+ onOpenAttachment,
344
+ onNavigateBranch,
345
+ onEdit: onEditMessage,
346
+ onSaveEdit,
347
+ onCancelEdit,
348
+ onRegenerate: onRegenerateMessage,
349
+ onRetry: onRetryMessage,
350
+ onOpenReasoning,
351
+ onApprovalAnswer
352
+ })
353
+ }, message.id);
354
+ });
355
+ });
318
356
  //#endregion
319
357
  export { ChatMessageList as default };
@@ -1,5 +1,6 @@
1
1
  import { cx } from "../../cx.js";
2
2
  import { ImageIcon, NewChatIcon, UsersIcon } from "../../icons.js";
3
+ import { isPlainClick } from "../../components/AppLink.js";
3
4
  import { EmptyState } from "../../components/EmptyState.js";
4
5
  import { RelativeTime } from "../../components/relativeTime.js";
5
6
  import { SearchInput } from "../../components/SearchInput.js";
@@ -26,7 +27,9 @@ function ConversationRow({ conv, isCurrent, menuItems, openMenu, onOpen }) {
26
27
  id: conv.id
27
28
  },
28
29
  className: "flex min-w-0 flex-1 items-center gap-1.5 py-1 text-left",
29
- onClick: onOpen,
30
+ onClick: (e) => {
31
+ if (isPlainClick(e)) onOpen();
32
+ },
30
33
  children: [conv.isOwner === false && /* @__PURE__ */ jsx(UsersIcon, {
31
34
  className: "h-3 w-3 shrink-0 theme-muted-text",
32
35
  title: t.sharedWithYou
@@ -82,10 +85,11 @@ function ChatSidebar({ actions = true, top }) {
82
85
  label: tSidebar[key],
83
86
  items
84
87
  })), [filteredConversations, tSidebar]);
85
- const handleNewChat = () => {
86
- closeDrawer();
87
- clearCurrentConversation();
88
- navigation.toNewChat();
88
+ const handleNewChat = (e) => {
89
+ if (isPlainClick(e)) {
90
+ closeDrawer();
91
+ clearCurrentConversation();
92
+ }
89
93
  };
90
94
  const list = /* @__PURE__ */ jsx(ConversationSidebar, {
91
95
  collapsed: sidebarCollapsed,
@@ -123,8 +127,8 @@ function ChatSidebar({ actions = true, top }) {
123
127
  },
124
128
  top: (top || actions) && /* @__PURE__ */ jsxs(Fragment, { children: [top?.({ collapsed: sidebarCollapsed }), actions && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
125
129
  className: "flex flex-col gap-0.5 px-2 pt-2 pb-1",
126
- children: [/* @__PURE__ */ jsxs("button", {
127
- type: "button",
130
+ children: [/* @__PURE__ */ jsxs(LinkComponent, {
131
+ target: { kind: "newChat" },
128
132
  onClick: handleNewChat,
129
133
  className: "flex items-center h-9 px-2 gap-3 rounded-lg font-medium transition-colors hover:chat-tint",
130
134
  title: tSidebar.newChat,
@@ -14,7 +14,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
14
14
  function ChatThread({ conversationId }) {
15
15
  const chatInputRef = useRef(null);
16
16
  const [editingState, setEditingState] = useState(null);
17
- const { adapter, currentUser, currentConversation, loadConversation, loadProviders, sendMessage, retryMessage, isLoadingConversation, isStreaming, streamingContent, navigateToBranch, queuedMessages, removeQueuedMessage } = useChat();
17
+ const { adapter, currentUser, currentConversation, loadConversation, loadProviders, sendMessage, retryMessage, isLoadingConversation, navigateToBranch, queuedMessages, removeQueuedMessage } = useChat();
18
18
  const { slots, strings } = useChatConfig();
19
19
  const requestedConversationIdRef = useRef(null);
20
20
  useEffect(() => {
@@ -69,17 +69,6 @@ function ChatThread({ conversationId }) {
69
69
  }, []);
70
70
  const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped: handleFilesDropped });
71
71
  const attachmentsAvailable = !!adapter.attachments;
72
- const streamingMessage = useMemo(() => isStreaming && streamingContent ? {
73
- id: "streaming",
74
- conversationId,
75
- role: "assistant",
76
- content: streamingContent,
77
- timestamp: ""
78
- } : null, [
79
- isStreaming,
80
- streamingContent,
81
- conversationId
82
- ]);
83
72
  const messages = currentConversation?.messages || [];
84
73
  const showAuthor = useMemo(() => {
85
74
  return new Set(messages.flatMap((m) => m.author ? [m.author.username] : [])).size > 1 || currentConversation?.isOwner === false;
@@ -107,7 +96,6 @@ function ChatThread({ conversationId }) {
107
96
  /* @__PURE__ */ jsx(ChatMessageList, {
108
97
  messages,
109
98
  allMessages: messages,
110
- streamingMessage,
111
99
  queuedMessages,
112
100
  onRemoveQueued: removeQueuedMessage,
113
101
  editingMessageId: editingState?.messageId,
@@ -5,14 +5,14 @@ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  function DragOverlay({ className }) {
6
6
  const t = useChatConfig().strings.attachmentUpload;
7
7
  return /* @__PURE__ */ jsx("div", {
8
- className: cx("absolute inset-0 z-50 flex items-center justify-center bg-blue-500/10 border-2 border-dashed border-blue-500 backdrop-blur-sm pointer-events-none", className),
8
+ className: cx("absolute inset-0 z-50 flex items-center justify-center bg-(--theme-accent)/10 border-2 border-dashed border-(--theme-accent) backdrop-blur-sm pointer-events-none", className),
9
9
  children: /* @__PURE__ */ jsxs("div", {
10
10
  className: "text-center",
11
11
  children: [/* @__PURE__ */ jsx("p", {
12
- className: "text-lg font-medium text-blue-600",
12
+ className: "text-lg font-medium theme-text",
13
13
  children: t.dropFilesHere
14
14
  }), /* @__PURE__ */ jsx("p", {
15
- className: "text-sm text-blue-500/80",
15
+ className: "text-sm theme-muted-text",
16
16
  children: t.releaseToUpload
17
17
  })]
18
18
  })
@@ -0,0 +1,10 @@
1
+ import type { AttachmentRef } from '../types.js';
2
+ /** The files sent with a message. One the reader can download is a link to
3
+ * it, so it can be saved or opened in a new tab like any other; a host that
4
+ * previews attachments takes the plain click. */
5
+ export default function MessageAttachments({ attachments, downloadUrl, onOpen, className, }: {
6
+ attachments: AttachmentRef[];
7
+ downloadUrl?: ((id: string) => string) | undefined;
8
+ onOpen?: ((attachment: AttachmentRef) => void) | undefined;
9
+ className?: string | undefined;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { cx } from "../../cx.js";
2
+ import { DownloadIcon, FileIcon } from "../../icons.js";
3
+ import { safeUrl } from "../../safeUrl.js";
4
+ import { isPlainClick } from "../../components/AppLink.js";
5
+ import { formatBytes } from "../../format.js";
6
+ import { useChatConfig } from "../core/config.js";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/ai/components/MessageAttachments.tsx
9
+ var chipClasses = "inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-lg bg-(--theme-muted-panel-bg)";
10
+ /** The files sent with a message. One the reader can download is a link to
11
+ * it, so it can be saved or opened in a new tab like any other; a host that
12
+ * previews attachments takes the plain click. */
13
+ function MessageAttachments({ attachments, downloadUrl, onOpen, className }) {
14
+ const t = useChatConfig().strings.attachmentManager;
15
+ return /* @__PURE__ */ jsx("div", {
16
+ className: cx("mt-3 flex flex-wrap items-start gap-2", className),
17
+ children: attachments.map((att) => {
18
+ const isImage = att.contentType?.startsWith("image/");
19
+ const name = att.filename || (isImage ? t.unnamedImage : t.unnamedFile);
20
+ const url = att.id && downloadUrl ? downloadUrl(att.id) : void 0;
21
+ const href = safeUrl(url);
22
+ const thumbnail = isImage && url;
23
+ const body = thumbnail ? /* @__PURE__ */ jsx("img", {
24
+ src: url,
25
+ alt: name,
26
+ className: "max-h-[200px] max-w-[200px] object-cover",
27
+ loading: "lazy"
28
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
29
+ /* @__PURE__ */ jsx(FileIcon, {
30
+ "aria-hidden": "true",
31
+ className: "h-3.5 w-3.5 flex-shrink-0"
32
+ }),
33
+ /* @__PURE__ */ jsx("span", {
34
+ className: "max-w-[150px] truncate",
35
+ children: name
36
+ }),
37
+ att.size ? /* @__PURE__ */ jsxs("span", {
38
+ className: "theme-muted-text",
39
+ children: [
40
+ "(",
41
+ formatBytes(att.size),
42
+ ")"
43
+ ]
44
+ }) : null,
45
+ href && /* @__PURE__ */ jsx(DownloadIcon, {
46
+ "aria-hidden": "true",
47
+ className: "h-3 w-3 flex-shrink-0"
48
+ })
49
+ ] });
50
+ const className = thumbnail ? "block overflow-hidden rounded-lg border theme-border" : cx(chipClasses, !href && "theme-muted-text", (href || onOpen) && "transition-colors hover:theme-hover");
51
+ if (href) return /* @__PURE__ */ jsx("a", {
52
+ href,
53
+ download: att.filename || true,
54
+ onClick: onOpen && ((e) => {
55
+ if (isPlainClick(e)) {
56
+ e.preventDefault();
57
+ onOpen(att);
58
+ }
59
+ }),
60
+ className,
61
+ children: body
62
+ }, att.id);
63
+ if (onOpen) return /* @__PURE__ */ jsx("button", {
64
+ type: "button",
65
+ onClick: () => onOpen(att),
66
+ "aria-label": thumbnail ? void 0 : t.open(name),
67
+ className,
68
+ children: body
69
+ }, att.id);
70
+ return /* @__PURE__ */ jsx("span", {
71
+ className,
72
+ children: body
73
+ }, att.id);
74
+ })
75
+ });
76
+ }
77
+ //#endregion
78
+ export { MessageAttachments as default };
@@ -21,7 +21,7 @@ function CspIcon({ cspKind, className }) {
21
21
  const { slots } = useChatConfig();
22
22
  const custom = slots.providerIcon?.(cspKind, className);
23
23
  if (custom) return /* @__PURE__ */ jsx(Fragment, { children: custom });
24
- return /* @__PURE__ */ jsx(CloudIcon, { className: cx("text-purple-500", className) });
24
+ return /* @__PURE__ */ jsx(CloudIcon, { className: cx("theme-muted-text", className) });
25
25
  }
26
26
  function getProviderKeyFromModelId(modelId) {
27
27
  const idx = modelId.indexOf("/");
@@ -60,7 +60,7 @@ function ModelSelector({ targetSession }) {
60
60
  const groups = useMemo(() => {
61
61
  const status = (label, description) => ({
62
62
  label,
63
- icon: /* @__PURE__ */ jsx(CloudIcon, { className: "h-3 w-3 text-amber-500" }),
63
+ icon: /* @__PURE__ */ jsx(CloudIcon, { className: "h-3 w-3 text-(--theme-warning)" }),
64
64
  description,
65
65
  items: []
66
66
  });
@@ -143,7 +143,7 @@ function ModelSelector({ targetSession }) {
143
143
  className: "max-w-[45%] shrink-0 truncate text-xs",
144
144
  children: selected.provider
145
145
  }),
146
- issue && /* @__PURE__ */ jsx(WarningIcon, { className: "h-3.5 w-3.5 shrink-0 text-amber-500" })
146
+ issue && /* @__PURE__ */ jsx(WarningIcon, { className: "h-3.5 w-3.5 shrink-0 text-(--theme-warning)" })
147
147
  ] }) : /* @__PURE__ */ jsx("span", {
148
148
  className: cx("truncate", targetSessionStatus === "connecting" && "animate-pulse"),
149
149
  children: targetSessionStatus === "connecting" ? t.connecting : t.select
@@ -15,7 +15,7 @@ function PasteFileCard({ id, lines, bytes, saving = false, error, onRemove }) {
15
15
  const detail = error ?? (saving ? t.saving : `${t.lines(lines)} · ${formatBytes(bytes)}`);
16
16
  return /* @__PURE__ */ jsxs("div", {
17
17
  "data-testid": "paste-file-card",
18
- className: cx("inline-flex max-w-full items-center gap-2.5 rounded-xl px-3 py-2 align-top whitespace-normal", "bg-(--theme-panel-bg)", error ? "border border-red-500" : "chat-hairline"),
18
+ className: cx("inline-flex max-w-full items-center gap-2.5 rounded-xl px-3 py-2 align-top whitespace-normal", "bg-(--theme-panel-bg)", error ? "border border-(--theme-error)" : "chat-hairline"),
19
19
  children: [
20
20
  /* @__PURE__ */ jsx(FileIcon, { className: "h-5 w-5 flex-shrink-0 theme-muted-text" }),
21
21
  /* @__PURE__ */ jsxs("div", {
@@ -24,7 +24,7 @@ function PasteFileCard({ id, lines, bytes, saving = false, error, onRemove }) {
24
24
  className: "truncate text-sm font-medium theme-text",
25
25
  children: id ? `paste-${id}.txt` : t.title
26
26
  }), /* @__PURE__ */ jsx("div", {
27
- className: cx("truncate text-xs", error ? "text-red-500" : "theme-muted-text"),
27
+ className: cx("truncate text-xs", error ? "text-(--theme-error-text)" : "theme-muted-text"),
28
28
  children: detail
29
29
  })]
30
30
  }),
@@ -1,4 +1,6 @@
1
- import { SettingsIcon, WarningIcon } from "../../icons.js";
1
+ import { buttonClasses } from "../../components/buttonClasses.js";
2
+ import { SettingsIcon } from "../../icons.js";
3
+ import { Callout } from "../../components/Callout.js";
2
4
  import { useChatConfig } from "../core/config.js";
3
5
  import { issueDetail, providerIssueFor } from "../core/providerIssues.js";
4
6
  import { useChat } from "../core/ChatProvider.js";
@@ -18,29 +20,23 @@ function ProviderIssueBanner() {
18
20
  const providerKey = getProviderKeyFromModelId(selectedModel.id);
19
21
  const provider = providers.find((p) => `${p.user}:${p.name}` === providerKey);
20
22
  const settingsHref = provider?.user === currentUser.username ? extraLinks.providerSettings?.(provider.user, provider.name) : void 0;
21
- return /* @__PURE__ */ jsxs("div", {
22
- className: "mx-4 mt-2 flex items-start gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20",
23
- children: [
24
- /* @__PURE__ */ jsx(WarningIcon, { className: "w-4 h-4 text-amber-500 shrink-0 mt-0.5" }),
25
- /* @__PURE__ */ jsxs("div", {
26
- className: "flex-1 min-w-0",
27
- children: [/* @__PURE__ */ jsx("p", {
28
- className: "text-sm font-medium text-amber-700",
29
- children: title
30
- }), /* @__PURE__ */ jsx("p", {
31
- className: "text-xs text-amber-600/80 mt-0.5",
32
- children: issueDetail(t, issue)
33
- })]
23
+ return /* @__PURE__ */ jsx(Callout, {
24
+ tone: "warning",
25
+ size: "sm",
26
+ className: "mx-4 mt-2",
27
+ title,
28
+ action: settingsHref && /* @__PURE__ */ jsxs(LinkComponent, {
29
+ target: {
30
+ kind: "external",
31
+ href: settingsHref
32
+ },
33
+ className: buttonClasses({
34
+ variant: "outline",
35
+ size: "sm"
34
36
  }),
35
- settingsHref && /* @__PURE__ */ jsxs(LinkComponent, {
36
- target: {
37
- kind: "external",
38
- href: settingsHref
39
- },
40
- className: "flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-amber-700 transition-colors hover:bg-amber-500/10",
41
- children: [/* @__PURE__ */ jsx(SettingsIcon, { className: "h-3.5 w-3.5" }), strings.modelPicker.manageProvider]
42
- })
43
- ]
37
+ children: [/* @__PURE__ */ jsx(SettingsIcon, {}), strings.modelPicker.manageProvider]
38
+ }),
39
+ children: issueDetail(t, issue)
44
40
  });
45
41
  }
46
42
  //#endregion
@@ -214,9 +214,9 @@ function ShareDialog({ conversationId, isOpen, onClose }) {
214
214
  children: /* @__PURE__ */ jsx("button", {
215
215
  type: "button",
216
216
  onClick: () => handleRemoveTeam(perm.team),
217
- className: "p-1 rounded hover:bg-red-500/10",
217
+ className: "p-1 rounded hover:bg-(--theme-error-muted)",
218
218
  "aria-label": `${common.remove} ${perm.teamName || perm.team}`,
219
- children: /* @__PURE__ */ jsx(TrashIcon, { className: "w-3 h-3 text-red-500" })
219
+ children: /* @__PURE__ */ jsx(TrashIcon, { className: "w-3 h-3 text-(--theme-error)" })
220
220
  })
221
221
  })]
222
222
  })]
@@ -1,5 +1,3 @@
1
- /** The agent status words: one is picked per turn and held until it ends. */
2
- export declare const activityWords: readonly ['Thinking', 'Cogitating', 'Pondering', 'Percolating', 'Ruminating', 'Noodling', 'Conjuring', 'Simmering', 'Marinating', 'Tinkering', 'Scheming', 'Brewing', 'Wrangling', 'Finagling', 'Spelunking', 'Puttering', 'Composing', 'Synthesizing', 'Deliberating', 'Mulling', 'Contemplating', 'Vibing', 'Whittling', 'Concocting', 'Formulating', 'Hatching', 'Herding', 'Ideating', 'Manifesting', 'Moseying', 'Musing', 'Percussing', 'Reticulating', 'Schlepping', 'Stewing', 'Transmuting', 'Wibbling', 'Crunching', 'Forging', 'Cooking'];
3
1
  /** The terminal's "it's working" line for a running turn: spinner, status
4
2
  * word, how long the turn has run, its token counts so far, and a way to
5
3
  * stop it. */
@@ -7,63 +7,20 @@ import { useChatConfig } from "../../core/config.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { useEffect, useRef, useState } from "react";
9
9
  //#region src/ai/components/agent/ActivityLine.tsx
10
- /** The agent status words: one is picked per turn and held until it ends. */
11
- var activityWords = [
12
- "Thinking",
13
- "Cogitating",
14
- "Pondering",
15
- "Percolating",
16
- "Ruminating",
17
- "Noodling",
18
- "Conjuring",
19
- "Simmering",
20
- "Marinating",
21
- "Tinkering",
22
- "Scheming",
23
- "Brewing",
24
- "Wrangling",
25
- "Finagling",
26
- "Spelunking",
27
- "Puttering",
28
- "Composing",
29
- "Synthesizing",
30
- "Deliberating",
31
- "Mulling",
32
- "Contemplating",
33
- "Vibing",
34
- "Whittling",
35
- "Concocting",
36
- "Formulating",
37
- "Hatching",
38
- "Herding",
39
- "Ideating",
40
- "Manifesting",
41
- "Moseying",
42
- "Musing",
43
- "Percussing",
44
- "Reticulating",
45
- "Schlepping",
46
- "Stewing",
47
- "Transmuting",
48
- "Wibbling",
49
- "Crunching",
50
- "Forging",
51
- "Cooking"
52
- ];
53
- function randomActivityWord() {
54
- return activityWords[Math.floor(Math.random() * activityWords.length)] ?? activityWords[0];
10
+ function randomIndex() {
11
+ return Math.floor(Math.random() * 1e6);
55
12
  }
56
13
  /** A status word held for the whole turn and re-rolled when a new one starts. */
57
- function useTurnWord(startedAt) {
58
- const [word, setWord] = useState(randomActivityWord);
14
+ function useTurnWord(startedAt, words) {
15
+ const [pick, setPick] = useState(randomIndex);
59
16
  const turn = useRef(startedAt);
60
17
  useEffect(() => {
61
18
  if (turn.current !== startedAt) {
62
19
  turn.current = startedAt;
63
- setWord(randomActivityWord());
20
+ setPick(randomIndex());
64
21
  }
65
22
  }, [startedAt]);
66
- return word;
23
+ return words.length > 0 ? words[pick % words.length] ?? "" : "";
67
24
  }
68
25
  /** The terminal's "it's working" line for a running turn: spinner, status
69
26
  * word, how long the turn has run, its token counts so far, and a way to
@@ -71,7 +28,7 @@ function useTurnWord(startedAt) {
71
28
  function ActivityLine({ startedAt, inputTokens, outputTokens, onStop, stopping = false, word, testId }) {
72
29
  const { strings } = useChatConfig();
73
30
  const elapsed = useElapsed(startedAt);
74
- const turnWord = useTurnWord(startedAt);
31
+ const turnWord = useTurnWord(startedAt, strings.activity.words);
75
32
  const meta = [];
76
33
  if (elapsed !== null) meta.push(formatDuration(elapsed));
77
34
  if (inputTokens || outputTokens) meta.push(strings.activity.tokens(formatCount(inputTokens ?? 0), formatCount(outputTokens ?? 0)));
@@ -112,4 +69,4 @@ function ActivityLine({ startedAt, inputTokens, outputTokens, onStop, stopping =
112
69
  });
113
70
  }
114
71
  //#endregion
115
- export { activityWords, ActivityLine as default };
72
+ export { ActivityLine as default };