@parallelworks/ui 0.26.0 → 0.27.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.
@@ -6,13 +6,13 @@ import { FaUndoAlt } from "../../icons/fa/FaUndoAlt.js";
6
6
  import { FiCopy } from "../../icons/fi/FiCopy.js";
7
7
  import { FiDownload } from "../../icons/fi/FiDownload.js";
8
8
  import { FiFile } from "../../icons/fi/FiFile.js";
9
- import { LuChevronRight } from "../../icons/lu/LuChevronRight.js";
10
9
  import { MdRefresh } from "../../icons/md/MdRefresh.js";
11
10
  import { durationToAbsHumanDuration } from "../../duration.js";
12
11
  import { useChatConfig } from "../core/config.js";
13
12
  import { formatFileSize } from "../utils.js";
14
13
  import Markdown from "../ui/Markdown.js";
15
14
  import { chatProseClasses } from "../ui/prose.js";
15
+ import { ReasoningBody, ReasoningToggle } from "./Reasoning.js";
16
16
  import AgentMessageParts from "./agent/AgentMessageParts.js";
17
17
  import BranchNavigator from "./BranchNavigator.js";
18
18
  import { UserMessageText } from "./PastedText.js";
@@ -49,6 +49,7 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
49
49
  const isOtherUser = !!otherAuthor;
50
50
  const isRightAligned = isUser && !isOtherUser;
51
51
  const hasError = !!message.error;
52
+ const hasPastes = !!message.pastes?.length;
52
53
  const hasReasoning = !!message.reasoning;
53
54
  const toolCallCount = message.parts?.filter((p) => p.kind === "tool_call").length ?? 0;
54
55
  const showReasoningButton = !isUser && !isStreaming && hasReasoning;
@@ -61,6 +62,7 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
61
62
  const contentRef = useRef(null);
62
63
  const [collapsible, setCollapsible] = useState(false);
63
64
  const [expanded, setExpanded] = useState(false);
65
+ const [reasoningOpen, setReasoningOpen] = useState(false);
64
66
  useEffect(() => {
65
67
  if (isUser || isStreaming || !contentRef.current) return;
66
68
  const el = contentRef.current;
@@ -100,7 +102,7 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
100
102
  };
101
103
  const hasParts = !!message.parts?.length;
102
104
  if (hasParts && !message.content) return /* @__PURE__ */ jsx("div", {
103
- className: cx("group py-5 w-full", !flush && "px-4"),
105
+ className: cx("group py-3 w-full", !flush && "px-4"),
104
106
  children: /* @__PURE__ */ jsx(AgentMessageParts, {
105
107
  message,
106
108
  isStreaming,
@@ -121,7 +123,7 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
121
123
  };
122
124
  return /* @__PURE__ */ jsx("div", {
123
125
  role: "none",
124
- className: cx("group py-5 w-full", !flush && "px-4"),
126
+ className: cx("group w-full", isUser ? "pt-8 pb-5" : "py-3", !flush && "px-4"),
125
127
  onMouseEnter: () => setShowActions(true),
126
128
  onMouseLeave: () => setShowActions(false),
127
129
  onFocus: () => setShowActions(true),
@@ -133,20 +135,20 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
133
135
  value: editContent,
134
136
  onChange: handleTextareaChange,
135
137
  onKeyDown: handleKeyDown,
136
- className: "w-full min-h-[100px] max-h-[200px] overflow-y-auto p-4 rounded-xl theme-input resize-none focus:outline-none focus:ring-2 focus:ring-blue-500",
138
+ className: "chat-composer block w-full min-h-[100px] max-h-[200px] overflow-y-auto px-4 py-3.5 rounded-[1.25rem] bg-(--theme-panel-bg) theme-text text-[1rem] leading-relaxed resize-none focus:outline-none",
137
139
  placeholder: t.editPlaceholder
138
140
  }), /* @__PURE__ */ jsxs("div", {
139
- className: "flex items-center justify-end gap-2 mt-3",
141
+ className: "flex items-center justify-end gap-2 mt-2.5",
140
142
  children: [/* @__PURE__ */ jsx("button", {
141
143
  type: "button",
142
144
  onClick: onCancelEdit,
143
- className: "px-4 py-1.5 text-sm font-medium rounded-full transition-colors bg-(--theme-muted-panel-bg) text-(--theme-panel)",
145
+ className: "px-4 py-1.5 text-sm font-medium rounded-full transition-colors chat-tint hover:chat-tint-strong text-(--theme-panel)",
144
146
  children: t.cancel
145
147
  }), /* @__PURE__ */ jsx("button", {
146
148
  type: "button",
147
149
  onClick: handleSave,
148
150
  disabled: !editContent.trim(),
149
- className: "px-4 py-1.5 text-sm font-medium rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed bg-(--theme-panel) text-(--theme-panel-bg)",
151
+ className: "px-4 py-1.5 text-sm font-medium rounded-full transition-colors hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed bg-(--theme-panel) text-(--theme-panel-bg)",
150
152
  children: t.send
151
153
  })]
152
154
  })]
@@ -155,14 +157,13 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
155
157
  children: /* @__PURE__ */ jsxs("div", {
156
158
  className: cx("flex-1 min-w-0 space-y-1", isRightAligned && "relative flex flex-col items-end"),
157
159
  children: [
158
- showReasoningButton && /* @__PURE__ */ jsx("div", {
160
+ showReasoningButton && /* @__PURE__ */ jsxs("div", {
159
161
  className: "mb-2",
160
- children: /* @__PURE__ */ jsxs("button", {
161
- type: "button",
162
- onClick: () => onOpenReasoning?.(message.reasoning || "", message.reasoningDuration ?? null),
163
- className: "flex items-center gap-x-1 text-sm theme-muted-text hover:theme-text transition-colors",
164
- children: [/* @__PURE__ */ jsx("span", { children: message.reasoningDuration ? `Thought for ${formatDuration(message.reasoningDuration)}` : "Thought process" }), /* @__PURE__ */ jsx(LuChevronRight, { className: "w-3.5 h-3.5" })]
165
- })
162
+ children: [/* @__PURE__ */ jsx(ReasoningToggle, {
163
+ open: reasoningOpen,
164
+ onToggle: () => onOpenReasoning ? onOpenReasoning(message.reasoning || "", message.reasoningDuration ?? null) : setReasoningOpen((o) => !o),
165
+ children: message.reasoningDuration ? `Thought for ${formatDuration(message.reasoningDuration)}` : "Thought process"
166
+ }), reasoningOpen && /* @__PURE__ */ jsx(ReasoningBody, { children: /* @__PURE__ */ jsx(Markdown, { children: message.reasoning || "" }) })]
166
167
  }),
167
168
  showTurnDurationLabel && /* @__PURE__ */ jsx("div", {
168
169
  className: "mb-2",
@@ -190,14 +191,14 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
190
191
  name: otherAuthor.name || otherAuthor.username,
191
192
  className: "flex-shrink-0 mb-0.5"
192
193
  }), /* @__PURE__ */ jsx("div", {
193
- className: "px-4 py-2.5 chat-ink rounded-2xl rounded-bl-md max-w-[70%] inline-block whitespace-pre-wrap wrap-break-word text-[1rem] leading-6 bg-[color-mix(in_oklab,var(--theme-panel)_6%,transparent)]",
194
+ className: cx("px-4 py-2.5 chat-ink rounded-[1.25rem] max-w-[80%] inline-block whitespace-pre-wrap wrap-break-word text-[1rem] leading-6 chat-tint", hasPastes && "chat-bubble-roomy"),
194
195
  children: /* @__PURE__ */ jsx(UserMessageText, {
195
196
  content: message.content,
196
197
  pastes: message.pastes
197
198
  })
198
199
  })]
199
200
  }) : isUser ? /* @__PURE__ */ jsx("div", {
200
- className: "px-4 py-2.5 chat-ink rounded-2xl rounded-br-md max-w-[70%] inline-block whitespace-pre-wrap wrap-break-word text-[1rem] leading-6 bg-[color-mix(in_oklab,var(--theme-panel)_6%,transparent)]",
201
+ className: cx("px-4 py-2.5 chat-ink rounded-[1.25rem] max-w-[80%] inline-block whitespace-pre-wrap wrap-break-word text-[1rem] leading-6 chat-bubble-user", hasPastes && "chat-bubble-roomy"),
201
202
  children: /* @__PURE__ */ jsx(UserMessageText, {
202
203
  content: message.content,
203
204
  pastes: message.pastes
@@ -307,21 +308,21 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
307
308
  /* @__PURE__ */ jsx("button", {
308
309
  type: "button",
309
310
  onClick: copyToClipboard,
310
- className: "p-1.5 rounded-lg hover:theme-hover theme-muted-text transition-colors",
311
+ className: "p-1.5 rounded-md hover:chat-tint theme-muted-text hover:theme-text transition-colors",
311
312
  title: t.copy,
312
313
  children: copied ? /* @__PURE__ */ jsx(FaCheck, { className: "w-4 h-4 text-green-500" }) : /* @__PURE__ */ jsx(FiCopy, { className: "w-4 h-4" })
313
314
  }),
314
315
  isUser && !isOtherUser && onEdit && /* @__PURE__ */ jsx("button", {
315
316
  type: "button",
316
317
  onClick: () => onEdit(message.id),
317
- className: "p-1.5 rounded-lg hover:theme-hover theme-muted-text transition-colors",
318
+ className: "p-1.5 rounded-md hover:chat-tint theme-muted-text hover:theme-text transition-colors",
318
319
  title: t.edit,
319
320
  children: /* @__PURE__ */ jsx(FaEdit, { className: "w-4 h-4" })
320
321
  }),
321
322
  !isUser && onRegenerate && /* @__PURE__ */ jsx("button", {
322
323
  type: "button",
323
324
  onClick: () => onRegenerate(message.id),
324
- className: "p-1.5 rounded-lg hover:theme-hover theme-muted-text transition-colors",
325
+ className: "p-1.5 rounded-md hover:chat-tint theme-muted-text hover:theme-text transition-colors",
325
326
  title: t.regenerate,
326
327
  children: /* @__PURE__ */ jsx(MdRefresh, { className: "w-4 h-4" })
327
328
  }),
@@ -331,7 +332,7 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
331
332
  onNavigate: onNavigateBranch
332
333
  }),
333
334
  (timestamp || !isUser && (message.model || totalTokens)) && /* @__PURE__ */ jsxs("span", {
334
- className: cx("chat-meta flex items-center gap-2 ml-2 text-[11px] theme-muted-text transition-opacity duration-150 select-none", showActions ? "opacity-100" : "opacity-0"),
335
+ className: cx("chat-meta flex items-center gap-2 ml-2 text-xs tabular-nums theme-muted-text transition-opacity duration-150 select-none", showActions ? "opacity-100" : "opacity-0"),
335
336
  children: [
336
337
  timestamp && /* @__PURE__ */ jsx("span", {
337
338
  title: timestamp.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
@@ -6,8 +6,13 @@ interface ChatMessageListProps {
6
6
  streamingMessage?: Message | null;
7
7
  queuedMessages?: Message[];
8
8
  onRemoveQueued?: (id: string) => void;
9
- /** Which queued messages can still be taken back; all of them when unset. */
9
+ /** Which queued messages can still be taken back; all of them when unset.
10
+ * One that cannot has been handed to the running turn. */
10
11
  canRemoveQueued?: (id: string) => boolean;
12
+ /** When a queued message reaches the model: once the running reply
13
+ * finishes (the chat provider's queue), or between the agent's steps for a
14
+ * host that steers the turn in progress. */
15
+ queueDelivery?: 'afterReply' | 'nextStep';
11
16
  editingMessageId?: string | null | undefined;
12
17
  showAuthor?: boolean;
13
18
  currentUsername?: string;
@@ -33,5 +38,5 @@ interface ChatMessageListProps {
33
38
  * for the first frame is not announced twice. */
34
39
  workingIndicator?: boolean;
35
40
  }
36
- export default function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, canRemoveQueued, editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator, }: ChatMessageListProps): import("react").JSX.Element;
41
+ export default function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, canRemoveQueued, queueDelivery, editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator, }: ChatMessageListProps): import("react").JSX.Element;
37
42
  export {};
@@ -1,17 +1,18 @@
1
- import { FiX } from "../../icons/fi/FiX.js";
2
- import { LuChevronRight } from "../../icons/lu/LuChevronRight.js";
1
+ import { FaCheck } from "../../icons/fa/FaCheck.js";
2
+ import { FiClock } from "../../icons/fi/FiClock.js";
3
3
  import { durationToAbsHumanDuration } from "../../duration.js";
4
4
  import { useChatConfig } from "../core/config.js";
5
5
  import { useChat } from "../core/ChatProvider.js";
6
6
  import Markdown from "../ui/Markdown.js";
7
+ import { ReasoningBody, ReasoningToggle } from "./Reasoning.js";
7
8
  import AgentMessageParts from "./agent/AgentMessageParts.js";
8
9
  import ChatMessage_default from "./ChatMessage.js";
9
10
  import cx from "classnames";
10
11
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
11
- import { jsx, jsxs } from "react/jsx-runtime";
12
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
12
13
  import { Duration } from "luxon";
13
14
  //#region src/ai/components/ChatMessageList.tsx
14
- function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, canRemoveQueued, editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator = true }) {
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 }) {
15
16
  const { adapter, isStreaming: providerStreaming, isThinking, streamingReasoning, streamingParts, thinkingStartTime } = useChat();
16
17
  const isStreaming = isStreamingProp ?? providerStreaming;
17
18
  const { strings } = useChatConfig();
@@ -39,6 +40,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
39
40
  const streamingMessageParts = streamingMessage?.parts;
40
41
  const [elapsedTime, setElapsedTime] = useState(0);
41
42
  const [finalThinkingDuration, setFinalThinkingDuration] = useState(null);
43
+ const [reasoningOpen, setReasoningOpen] = useState(false);
42
44
  const wasThinkingRef = useRef(false);
43
45
  useEffect(() => {
44
46
  if (isThinking && !wasThinkingRef.current) {
@@ -53,6 +55,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
53
55
  if (!isStreaming) {
54
56
  wasThinkingRef.current = false;
55
57
  setFinalThinkingDuration(null);
58
+ setReasoningOpen(false);
56
59
  }
57
60
  }, [isStreaming]);
58
61
  useEffect(() => {
@@ -143,7 +146,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
143
146
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
144
147
  }, [messageCount, isStreaming]);
145
148
  if (messages.length === 0 && !streamingMessage && !hasQueuedMessages) return /* @__PURE__ */ jsxs("div", {
146
- className: "flex-1 overflow-y-auto flex flex-col bg-(--theme-panel-bg)",
149
+ className: "chat-thread flex-1 overflow-y-auto flex flex-col bg-(--theme-panel-bg)",
147
150
  children: [
148
151
  /* @__PURE__ */ jsx("div", { className: "flex-1" }),
149
152
  footer && /* @__PURE__ */ jsx("div", {
@@ -163,10 +166,10 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
163
166
  "aria-relevant": "additions",
164
167
  "aria-label": label
165
168
  } : {},
166
- className: "flex-1 overflow-y-auto theme-scrollbar flex flex-col bg-(--theme-panel-bg)",
169
+ className: "chat-thread flex-1 overflow-y-auto theme-scrollbar flex flex-col bg-(--theme-panel-bg)",
167
170
  children: [
168
171
  /* @__PURE__ */ jsxs("div", {
169
- className: "w-full max-w-[50rem] mx-auto pt-10 pb-6 flex-1 px-4",
172
+ className: "w-full max-w-[50rem] mx-auto pt-4 pb-6 flex-1 px-4",
170
173
  children: [
171
174
  messages.map((message) => {
172
175
  if (message.pseudo) return null;
@@ -196,37 +199,31 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
196
199
  }),
197
200
  isStreaming && streamingReasoning && /* @__PURE__ */ jsx("div", {
198
201
  className: "animate-chat-enter",
199
- children: /* @__PURE__ */ jsx("div", {
200
- className: "py-3 px-4",
201
- children: /* @__PURE__ */ jsxs("div", {
202
- className: "flex flex-col gap-2",
203
- children: [/* @__PURE__ */ jsx("button", {
204
- type: "button",
205
- onClick: () => onOpenReasoning?.(streamingReasoning, finalThinkingDuration),
206
- className: "flex items-center gap-x-2 text-left",
207
- children: isThinking ? /* @__PURE__ */ jsx("div", {
208
- className: "animate-shimmer text-shimmer flex items-center gap-x-2",
209
- children: /* @__PURE__ */ jsx("span", { children: elapsedTime > 0 ? `${tThinking.label} (${elapsedTime}s)` : tThinking.ellipsis })
210
- }) : /* @__PURE__ */ jsxs("div", {
211
- className: "flex items-center gap-x-1 text-sm theme-muted-text hover:theme-text transition-colors",
212
- children: [/* @__PURE__ */ jsx("span", { children: tThinking.thoughtFor(finalThinkingDuration ? durationToAbsHumanDuration(Duration.fromMillis(finalThinkingDuration)) : `${elapsedTime}s`) }), /* @__PURE__ */ jsx(LuChevronRight, { className: "w-3.5 h-3.5" })]
202
+ children: /* @__PURE__ */ jsxs("div", {
203
+ className: cx("pt-3", !flush && "px-4"),
204
+ children: [/* @__PURE__ */ jsx(ReasoningToggle, {
205
+ open: reasoningOpen,
206
+ shimmer: isThinking,
207
+ onToggle: () => onOpenReasoning ? onOpenReasoning(streamingReasoning, finalThinkingDuration) : setReasoningOpen((o) => !o),
208
+ children: isThinking ? elapsedTime > 0 ? `${tThinking.label} (${elapsedTime}s)` : tThinking.ellipsis : tThinking.thoughtFor(finalThinkingDuration ? durationToAbsHumanDuration(Duration.fromMillis(finalThinkingDuration)) : `${elapsedTime}s`)
209
+ }), reasoningOpen ? /* @__PURE__ */ jsx(ReasoningBody, { children: /* @__PURE__ */ jsx(Markdown, {
210
+ isStreaming: isThinking,
211
+ children: streamingReasoning
212
+ }) }) : isThinking && /* @__PURE__ */ jsx(ReasoningBody, {
213
+ className: "chat-reasoning-tail",
214
+ children: /* @__PURE__ */ jsx("div", {
215
+ ref: reasoningScrollRef,
216
+ className: "max-h-20 overflow-y-hidden",
217
+ children: /* @__PURE__ */ jsx(Markdown, {
218
+ isStreaming: true,
219
+ children: streamingReasoning
213
220
  })
214
- }), isThinking && /* @__PURE__ */ jsxs("div", {
215
- className: "relative ml-6 pl-3 border-l-2 theme-border",
216
- children: [/* @__PURE__ */ jsx("div", { className: "absolute inset-x-0 top-0 h-3 bg-gradient-to-b from-(--theme-app-bg) to-transparent pointer-events-none z-[1]" }), /* @__PURE__ */ jsx("div", {
217
- ref: reasoningScrollRef,
218
- className: "text-sm theme-muted-text max-h-20 overflow-y-hidden pt-1",
219
- children: /* @__PURE__ */ jsx(Markdown, {
220
- isStreaming: true,
221
- children: streamingReasoning
222
- })
223
- })]
224
- })]
225
- })
221
+ })
222
+ })]
226
223
  })
227
224
  }, "thinking"),
228
225
  isStreaming && streamingParts.length > 0 && /* @__PURE__ */ jsx("div", {
229
- className: "animate-chat-enter py-1 px-4",
226
+ className: cx("animate-chat-enter py-1", !flush && "px-4"),
230
227
  children: /* @__PURE__ */ jsx(AgentMessageParts, {
231
228
  message: {
232
229
  id: "streaming-parts",
@@ -239,9 +236,9 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
239
236
  showWorkingIndicator && /* @__PURE__ */ jsx("div", {
240
237
  className: "animate-chat-enter",
241
238
  children: /* @__PURE__ */ jsx("div", {
242
- className: "py-3 px-4",
239
+ className: cx("py-3", !flush && "px-4"),
243
240
  children: /* @__PURE__ */ jsx("div", {
244
- className: "animate-shimmer text-shimmer",
241
+ className: "animate-shimmer text-shimmer text-sm",
245
242
  children: tThinking.ellipsis
246
243
  })
247
244
  })
@@ -258,23 +255,37 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
258
255
  onApprovalAnswer
259
256
  })
260
257
  }, "streaming"),
261
- queuedMessages?.map((msg) => /* @__PURE__ */ jsxs("div", {
262
- className: "group/queued relative animate-chat-enter opacity-60",
263
- children: [/* @__PURE__ */ jsx(ChatMessage_default, {
264
- message: msg,
265
- allMessages,
266
- flush,
267
- attachmentDownloadUrl,
268
- onOpenAttachment
269
- }), onRemoveQueued && (canRemoveQueued?.(msg.id) ?? true) && /* @__PURE__ */ jsx("button", {
270
- type: "button",
271
- "aria-label": strings.queue.remove,
272
- title: strings.queue.remove,
273
- onClick: () => onRemoveQueued(msg.id),
274
- className: "absolute top-1 right-1 rounded p-1 opacity-0 transition-opacity group-hover/queued:opacity-100 focus-visible:opacity-100 hover:bg-(--theme-hover) text-(--theme-muted-text-color)",
275
- children: /* @__PURE__ */ jsx(FiX, { className: "h-3.5 w-3.5" })
276
- })]
277
- }, msg.id)),
258
+ queuedMessages?.map((msg) => {
259
+ const removable = canRemoveQueued?.(msg.id) ?? true;
260
+ const status = !removable ? strings.queue.handedOver : queueDelivery === "nextStep" ? strings.queue.waitingNextStep : strings.queue.waiting;
261
+ return /* @__PURE__ */ jsxs("div", {
262
+ className: cx("animate-chat-enter", removable && "chat-queued"),
263
+ children: [/* @__PURE__ */ jsx(ChatMessage_default, {
264
+ message: msg,
265
+ allMessages,
266
+ flush,
267
+ attachmentDownloadUrl,
268
+ onOpenAttachment
269
+ }), /* @__PURE__ */ jsxs("div", {
270
+ "data-testid": "queued-status",
271
+ className: cx("-mt-4 mb-2 flex items-center justify-end gap-2 text-xs theme-muted-text", !flush && "px-4"),
272
+ children: [
273
+ removable ? /* @__PURE__ */ jsx(FiClock, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FaCheck, { className: "h-3 w-3" }),
274
+ /* @__PURE__ */ jsx("span", { children: status }),
275
+ onRemoveQueued && removable && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
276
+ "aria-hidden": "true",
277
+ children: "·"
278
+ }), /* @__PURE__ */ jsx("button", {
279
+ type: "button",
280
+ "aria-label": strings.queue.remove,
281
+ onClick: () => onRemoveQueued(msg.id),
282
+ className: "rounded font-medium underline-offset-2 hover:theme-text hover:underline",
283
+ children: strings.queue.removeShort
284
+ })] })
285
+ ]
286
+ })]
287
+ }, msg.id);
288
+ }),
278
289
  footer,
279
290
  /* @__PURE__ */ jsx("div", {
280
291
  ref: bottomRef,
@@ -287,7 +298,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
287
298
  children: /* @__PURE__ */ jsx("button", {
288
299
  type: "button",
289
300
  onClick: jumpToLatest,
290
- className: "-translate-y-full rounded-full border theme-border bg-(--theme-panel-bg) px-3 py-1 text-xs shadow-sm hover:theme-hover",
301
+ className: "chat-composer -translate-y-full rounded-full bg-(--theme-panel-bg) px-3 py-1.5 text-xs font-medium theme-text transition-colors hover:bg-(--theme-muted-panel-bg)",
291
302
  children: strings.chrome.jumpToLatest
292
303
  })
293
304
  }),
@@ -238,11 +238,11 @@ function ChatSidebar({ actions = true }) {
238
238
  className: cx("absolute inset-y-0 right-0 z-10 w-1 cursor-col-resize touch-none", resizing ? "bg-(--theme-element)" : "hover:bg-(--theme-border)")
239
239
  }),
240
240
  actions && /* @__PURE__ */ jsxs("div", {
241
- className: "flex flex-col gap-1 p-1.5",
241
+ className: "flex flex-col gap-0.5 px-2 pt-2 pb-1",
242
242
  children: [/* @__PURE__ */ jsxs("button", {
243
243
  type: "button",
244
244
  onClick: handleNewChat,
245
- className: cx("flex items-center h-9 px-2 gap-3 rounded-lg hover:theme-muted-panel", "transition-colors", !sidebarCollapsed && "border theme-border"),
245
+ className: "flex items-center h-9 px-2 gap-3 rounded-lg font-medium transition-colors hover:chat-tint",
246
246
  title: tSidebar.newChat,
247
247
  children: [/* @__PURE__ */ jsx(TbEdit, { className: cx("flex-shrink-0", "w-4 h-4") }), /* @__PURE__ */ jsx("span", {
248
248
  className: cx("text-sm transition-opacity duration-200", sidebarCollapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100"),
@@ -250,7 +250,7 @@ function ChatSidebar({ actions = true }) {
250
250
  })]
251
251
  }), attachmentsAvailable && /* @__PURE__ */ jsxs(LinkComponent, {
252
252
  target: { kind: "attachments" },
253
- className: "flex items-center h-9 px-2 gap-3 rounded-lg hover:theme-muted-panel",
253
+ className: "flex items-center h-9 px-2 gap-3 rounded-lg transition-colors hover:chat-tint",
254
254
  title: tSidebar.attachments,
255
255
  children: [/* @__PURE__ */ jsx(FaRegFileImage, { className: "flex-shrink-0 w-4 h-4" }), /* @__PURE__ */ jsx("span", {
256
256
  className: cx("text-sm transition-opacity duration-200", sidebarCollapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100"),
@@ -259,7 +259,7 @@ function ChatSidebar({ actions = true }) {
259
259
  })]
260
260
  }),
261
261
  actions && !sidebarCollapsed && conversations.length > 0 && /* @__PURE__ */ jsx("div", {
262
- className: "px-2 pb-1",
262
+ className: "px-2 pb-2",
263
263
  children: /* @__PURE__ */ jsxs("div", {
264
264
  className: "relative",
265
265
  children: [/* @__PURE__ */ jsx(FaSearch, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 theme-muted-text pointer-events-none" }), /* @__PURE__ */ jsx("input", {
@@ -275,7 +275,7 @@ function ChatSidebar({ actions = true }) {
275
275
  },
276
276
  placeholder: strings.chrome.searchPlaceholder,
277
277
  "aria-label": strings.chrome.searchLabel,
278
- className: "w-full h-8 pl-8 pr-2 text-sm rounded-lg border theme-border bg-(--theme-input-bg) placeholder:theme-muted-text focus:outline-none focus:border-(--theme-element)"
278
+ className: "w-full h-9 pl-8 pr-2 text-sm rounded-lg border border-transparent chat-tint theme-text placeholder:theme-muted-text transition-colors focus:outline-none focus:border-(--theme-border) focus:bg-(--theme-panel-bg)"
279
279
  })]
280
280
  })
281
281
  }),
@@ -320,7 +320,7 @@ function ChatSidebar({ actions = true }) {
320
320
  kind: "external",
321
321
  href: extraLinks.manageProviders
322
322
  },
323
- className: "flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-xs theme-muted-text hover:theme-hover",
323
+ className: "flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-xs theme-muted-text transition-colors hover:chat-tint hover:theme-text",
324
324
  title: tSidebar.manageProviders,
325
325
  children: [/* @__PURE__ */ jsx(IoSettingsOutline, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ jsx("span", {
326
326
  className: "truncate",
@@ -1,8 +1,5 @@
1
- import { IoMdClose } from "../../icons/io/IoMdClose.js";
2
- import { TbBrain } from "../../icons/tb/TbBrain.js";
3
1
  import { useChatConfig } from "../core/config.js";
4
2
  import { useChat } from "../core/ChatProvider.js";
5
- import Markdown from "../ui/Markdown.js";
6
3
  import BlockedGroupBanner from "./BlockedGroupBanner.js";
7
4
  import useDragDrop from "../core/useDragDrop.js";
8
5
  import { ComposerControls, ConnectToolsLink } from "./ComposerChrome.js";
@@ -11,25 +8,12 @@ import ChatInput from "./ChatInput.js";
11
8
  import ProviderIssueBanner from "./ProviderIssueBanner.js";
12
9
  import { ShareButton } from "./ShareDialog.js";
13
10
  import ChatMessageList from "./ChatMessageList.js";
14
- import cx from "classnames";
15
11
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
16
12
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
17
13
  //#region src/ai/components/ChatThread.tsx
18
14
  function ChatThread({ conversationId }) {
19
15
  const chatInputRef = useRef(null);
20
16
  const [editingState, setEditingState] = useState(null);
21
- const [reasoningDrawerOpen, setReasoningDrawerOpen] = useState(false);
22
- const [selectedReasoning, setSelectedReasoning] = useState(null);
23
- const handleOpenReasoning = useCallback((reasoning, duration) => {
24
- if (reasoningDrawerOpen && selectedReasoning?.content === reasoning) setReasoningDrawerOpen(false);
25
- else {
26
- setSelectedReasoning({
27
- content: reasoning,
28
- duration
29
- });
30
- setReasoningDrawerOpen(true);
31
- }
32
- }, [reasoningDrawerOpen, selectedReasoning?.content]);
33
17
  const { adapter, currentUser, currentConversation, loadConversation, loadProviders, sendMessage, retryMessage, isLoadingConversation, isStreaming, streamingContent, navigateToBranch, queuedMessages, removeQueuedMessage } = useChat();
34
18
  const { slots, strings } = useChatConfig();
35
19
  const requestedConversationIdRef = useRef(null);
@@ -111,9 +95,9 @@ function ChatThread({ conversationId }) {
111
95
  settingsRight: /* @__PURE__ */ jsxs(Fragment$1, { children: [slots.composerUsage, /* @__PURE__ */ jsx(ComposerControls, {})] })
112
96
  })]
113
97
  });
114
- return /* @__PURE__ */ jsxs("div", {
98
+ return /* @__PURE__ */ jsx("div", {
115
99
  className: "flex h-full relative bg-(--theme-panel-bg)",
116
- children: [/* @__PURE__ */ jsxs("div", {
100
+ children: /* @__PURE__ */ jsxs("div", {
117
101
  className: "flex-1 flex flex-col min-w-0",
118
102
  ...attachmentsAvailable ? dragHandlers : {},
119
103
  children: [
@@ -135,7 +119,6 @@ function ChatThread({ conversationId }) {
135
119
  onRetryMessage: currentConversation?.canCollaborate !== false ? handleRetryMessage : void 0,
136
120
  onSaveEdit: handleSaveEdit,
137
121
  onCancelEdit: handleCancelEdit,
138
- onOpenReasoning: handleOpenReasoning,
139
122
  inputElement: /* @__PURE__ */ jsx(ChatInput, {
140
123
  ref: chatInputRef,
141
124
  onSend: handleSendMessage,
@@ -148,50 +131,7 @@ function ChatThread({ conversationId }) {
148
131
  })
149
132
  })
150
133
  ]
151
- }), /* @__PURE__ */ jsx("div", {
152
- className: cx("flex-shrink-0 border-l theme-border flex flex-col h-full transition-[width] duration-300 ease-in-out overflow-hidden bg-(--theme-panel-bg)", reasoningDrawerOpen ? "w-[400px]" : "w-0 border-l-0"),
153
- children: /* @__PURE__ */ jsxs("div", {
154
- className: "w-[400px] h-full flex flex-col",
155
- children: [/* @__PURE__ */ jsxs("div", {
156
- className: "flex items-center justify-between p-3 border-b theme-border",
157
- children: [/* @__PURE__ */ jsxs("div", {
158
- className: "flex items-center gap-2",
159
- children: [
160
- /* @__PURE__ */ jsx(TbBrain, { className: "w-5 h-5 theme-muted-text" }),
161
- /* @__PURE__ */ jsx("span", {
162
- className: "font-medium",
163
- children: strings.thinking.activity
164
- }),
165
- selectedReasoning?.duration && /* @__PURE__ */ jsxs("span", {
166
- className: "text-sm theme-muted-text",
167
- children: [
168
- "· ",
169
- Math.round(selectedReasoning.duration / 1e3),
170
- "s"
171
- ]
172
- })
173
- ]
174
- }), /* @__PURE__ */ jsx("button", {
175
- type: "button",
176
- onClick: () => setReasoningDrawerOpen(false),
177
- className: "p-1 rounded hover:theme-hover transition-colors",
178
- children: /* @__PURE__ */ jsx(IoMdClose, { className: "w-5 h-5 theme-muted-text" })
179
- })]
180
- }), /* @__PURE__ */ jsx("div", {
181
- className: "flex-1 overflow-y-auto p-4",
182
- children: selectedReasoning?.content && /* @__PURE__ */ jsxs("div", {
183
- className: "space-y-4",
184
- children: [/* @__PURE__ */ jsxs("div", {
185
- className: "flex items-center gap-2 text-sm font-medium theme-text",
186
- children: [/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-blue-500" }), /* @__PURE__ */ jsx("span", { children: strings.thinking.label })]
187
- }), /* @__PURE__ */ jsx("div", {
188
- className: "text-sm theme-muted-text leading-relaxed pl-4 border-l-2 theme-border",
189
- children: /* @__PURE__ */ jsx(Markdown, { children: selectedReasoning.content })
190
- })]
191
- })
192
- })]
193
- })
194
- })]
134
+ })
195
135
  });
196
136
  }
197
137
  //#endregion
@@ -129,8 +129,8 @@ function ModelSelector({ targetSession, variant = "default" }) {
129
129
  const targetSessionDisplayName = targetSession?.split(":").pop();
130
130
  const targetSessionFound = targetSession && models.length > 0 ? models.some((m) => m.id.startsWith(`session:${targetSession}/`)) : false;
131
131
  const targetSessionStatus = targetSession ? isLoadingModels ? "connecting" : hasLoadedModels && !targetSessionFound ? "offline" : null : null;
132
- const boxClass = bare ? "flex items-center gap-1 rounded-md px-1.5 py-0.5 max-w-[280px]" : "flex items-center gap-2 px-3 h-8 rounded-lg min-w-[180px] max-w-[280px]";
133
- const textClass = bare ? "text-[11px]" : "text-sm";
132
+ const boxClass = bare ? "flex items-center gap-1.5 rounded-full h-8 px-2.5 max-w-[280px]" : "flex items-center gap-2 px-3 h-8 rounded-lg min-w-[180px] max-w-[280px]";
133
+ const textClass = bare ? "text-[13px]" : "text-sm";
134
134
  if (isLoadingModels && models.length === 0 && !targetSession) return /* @__PURE__ */ jsx("div", {
135
135
  ref: containerRef,
136
136
  className: "relative",
@@ -156,7 +156,7 @@ function ModelSelector({ targetSession, variant = "default" }) {
156
156
  /* @__PURE__ */ jsxs("button", {
157
157
  type: "button",
158
158
  onClick: () => refreshModels(),
159
- className: cx("flex items-center gap-1 text-blue-600 hover:underline ml-1", bare ? "text-[11px]" : "text-xs"),
159
+ className: "flex items-center gap-1 text-xs text-blue-600 hover:underline ml-1",
160
160
  children: [/* @__PURE__ */ jsx(FaUndoAlt, { className: "h-2.5 w-2.5" }), "Retry"]
161
161
  })
162
162
  ]
@@ -182,7 +182,7 @@ function ModelSelector({ targetSession, variant = "default" }) {
182
182
  type: "button",
183
183
  onClick: () => setIsOpen(!isOpen),
184
184
  disabled: models.length === 0 && !targetSessionStatus && unreachableSessions.length === 0,
185
- className: cx(boxClass, "transition-all cursor-pointer bg-transparent", "focus:outline-none", "disabled:opacity-50 disabled:cursor-not-allowed", bare ? "hover:theme-muted-panel" : "border theme-border hover:theme-hover"),
185
+ className: cx(boxClass, "transition-all cursor-pointer bg-transparent", "focus:outline-none", "disabled:opacity-50 disabled:cursor-not-allowed", bare ? "hover:chat-tint" : "border theme-border hover:theme-hover"),
186
186
  children: [
187
187
  !bare && /* @__PURE__ */ jsx(FaRobot, { className: cx("h-4 w-4 flex-shrink-0", targetSessionStatus === "connecting" ? "text-amber-500 animate-pulse" : "text-purple-500") }),
188
188
  /* @__PURE__ */ jsx("div", {
@@ -192,12 +192,12 @@ function ModelSelector({ targetSession, variant = "default" }) {
192
192
  children: [
193
193
  /* @__PURE__ */ jsx("span", {
194
194
  title: selectedModelName,
195
- className: cx("truncate", bare ? "text-[11px] theme-muted-text" : "text-sm font-medium text-(--theme-panel)"),
195
+ className: cx("truncate", bare ? "text-[13px] font-medium theme-text" : "text-sm font-medium text-(--theme-panel)"),
196
196
  children: selectedModelName
197
197
  }),
198
198
  selectedProviderName && /* @__PURE__ */ jsx("span", {
199
199
  title: selectedProviderName,
200
- className: cx("theme-muted-text truncate flex-shrink-0 max-w-[45%]", bare ? "text-[10px]" : "text-xs"),
200
+ className: "theme-muted-text truncate flex-shrink-0 max-w-[45%] text-xs",
201
201
  children: selectedProviderName
202
202
  }),
203
203
  selectedModelIssue && /* @__PURE__ */ jsx(FaExclamationTriangle, {
@@ -206,7 +206,7 @@ function ModelSelector({ targetSession, variant = "default" }) {
206
206
  })
207
207
  ]
208
208
  }) : /* @__PURE__ */ jsx("span", {
209
- className: cx(bare ? "text-[11px]" : "text-sm", targetSessionStatus === "connecting" ? "text-amber-600" : "theme-muted-text"),
209
+ className: cx(textClass, targetSessionStatus === "connecting" ? "text-amber-600" : "theme-muted-text"),
210
210
  children: triggerText
211
211
  })
212
212
  }),
@@ -8,7 +8,9 @@ export declare function PasteFileCard({ id, lines, bytes, saving, error, onRemov
8
8
  error?: string | undefined;
9
9
  onRemove?: (() => void) | undefined;
10
10
  }): import("react").JSX.Element;
11
- /** A user message as typed: an inline paste as its text, one sent as its file as a card. */
11
+ /** A user message as typed: an inline paste as a code block in the language
12
+ * it reads as, one sent as its file as a card. The blank lines the composer puts around a
13
+ * paste would stack on the block's own spacing, so they are dropped. */
12
14
  export declare function UserMessageText({ content, pastes, }: {
13
15
  content: string;
14
16
  pastes?: MessagePaste[] | null | undefined;