@parallelworks/ui 0.22.0 → 0.24.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 (41) hide show
  1. package/dist/ai/adapter/mock/mockAdapter.js +2 -1
  2. package/dist/ai/adapter/openai/openaiAdapter.js +6 -3
  3. package/dist/ai/adapter/openai/wire.d.ts +2 -1
  4. package/dist/ai/adapter/types.d.ts +2 -1
  5. package/dist/ai/components/ChatEmptyState.js +2 -0
  6. package/dist/ai/components/ChatInput.d.ts +4 -2
  7. package/dist/ai/components/ChatInput.js +36 -10
  8. package/dist/ai/components/ChatMessage.js +9 -2
  9. package/dist/ai/components/ChatMessageList.d.ts +3 -1
  10. package/dist/ai/components/ChatMessageList.js +2 -2
  11. package/dist/ai/components/ChatThread.js +2 -0
  12. package/dist/ai/components/ModelSelector.js +94 -62
  13. package/dist/ai/components/PastedText.d.ts +15 -0
  14. package/dist/ai/components/PastedText.js +59 -0
  15. package/dist/ai/components/ProviderIssueBanner.d.ts +1 -0
  16. package/dist/ai/components/ProviderIssueBanner.js +30 -0
  17. package/dist/ai/components/usePasteCards.d.ts +30 -0
  18. package/dist/ai/components/usePasteCards.js +129 -0
  19. package/dist/ai/core/ChatProvider.d.ts +2 -1
  20. package/dist/ai/core/ChatProvider.js +7 -3
  21. package/dist/ai/core/chatReducer.d.ts +3 -1
  22. package/dist/ai/core/chatReducer.js +2 -0
  23. package/dist/ai/core/pastes.d.ts +14 -0
  24. package/dist/ai/core/pastes.js +43 -0
  25. package/dist/ai/core/providerIssues.d.ts +2 -0
  26. package/dist/ai/core/providerIssues.js +7 -0
  27. package/dist/ai/index.d.ts +4 -0
  28. package/dist/ai/index.js +4 -1
  29. package/dist/ai/strings.d.ts +14 -0
  30. package/dist/ai/strings.js +14 -0
  31. package/dist/ai/types.d.ts +18 -0
  32. package/dist/file-explorer/FileExplorer.d.ts +7 -1
  33. package/dist/file-explorer/FileExplorer.js +3 -1
  34. package/dist/list/ListPage.js +19 -6
  35. package/dist/list/ListTable.d.ts +4 -1
  36. package/dist/list/ListTable.js +7 -4
  37. package/dist/list/UserHoverCard.js +28 -9
  38. package/dist/list/listView.d.ts +10 -0
  39. package/dist/list/listView.js +8 -1
  40. package/dist/styles.css +1 -1
  41. package/package.json +1 -1
@@ -45,7 +45,8 @@ function createMockChatAdapter(options) {
45
45
  provider: "Mock Provider",
46
46
  tool_calling_mode: "none"
47
47
  }],
48
- unreachableSessions: []
48
+ unreachableSessions: [],
49
+ providerIssues: []
49
50
  };
50
51
  } },
51
52
  streamCompletion: store.recordingStream(async (req, handlers, signal) => {
@@ -31,16 +31,19 @@ function createOpenAIChatAdapter(options) {
31
31
  const providerLabel = providerLabelFor(baseUrl);
32
32
  if (options.models?.length) return {
33
33
  models: options.models.map((id) => staticModel(id, providerLabel)),
34
- unreachableSessions: []
34
+ unreachableSessions: [],
35
+ providerIssues: []
35
36
  };
36
37
  const res = await doFetch(`${baseUrl}/models`, { headers });
37
38
  if (!res.ok) throw new ChatAdapterError(await errorMessageFromResponse(res), { status: res.status });
39
+ const body = await res.json();
38
40
  return {
39
- models: ((await res.json()).data ?? []).map((m) => ({
41
+ models: (body.data ?? []).map((m) => ({
40
42
  ...staticModel(m.id, providerLabel),
41
43
  ...m
42
44
  })),
43
- unreachableSessions: []
45
+ unreachableSessions: [],
46
+ providerIssues: body.provider_issues ?? []
44
47
  };
45
48
  } },
46
49
  streamCompletion: store.recordingStream(createOpenAICompatibleStream({
@@ -1,4 +1,4 @@
1
- import type { ChatModel, TokenUsage, ToolCall, ToolCallDelta, UnreachableSession } from '../../types.js';
1
+ import type { ChatModel, ProviderIssue, TokenUsage, ToolCall, ToolCallDelta, UnreachableSession } from '../../types.js';
2
2
  export interface CompletionMessage {
3
3
  role: string;
4
4
  content?: unknown;
@@ -37,4 +37,5 @@ export interface ModelsWireResponse {
37
37
  object: string;
38
38
  data: ChatModel[] | null;
39
39
  unreachable_sessions?: UnreachableSession[] | null;
40
+ provider_issues?: ProviderIssue[] | null;
40
41
  }
@@ -1,4 +1,4 @@
1
- import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, ToolCall, UnreachableSession } from '../types.js';
1
+ import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ProviderIssue, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, ToolCall, UnreachableSession } from '../types.js';
2
2
  import type { CompletionMessage } from './openai/wire.js';
3
3
  export declare class ChatAdapterError extends Error {
4
4
  status?: number | undefined;
@@ -10,6 +10,7 @@ export declare class ChatAdapterError extends Error {
10
10
  export interface ModelsList {
11
11
  models: ChatModel[];
12
12
  unreachableSessions: UnreachableSession[];
13
+ providerIssues: ProviderIssue[];
13
14
  }
14
15
  export interface StreamRequest {
15
16
  model: string;
@@ -11,6 +11,7 @@ import useDragDrop from "../core/useDragDrop.js";
11
11
  import { ComposerControls, ConnectToolsLink } from "./ComposerChrome.js";
12
12
  import DragOverlay from "./DragOverlay.js";
13
13
  import ChatInput from "./ChatInput.js";
14
+ import ProviderIssueBanner from "./ProviderIssueBanner.js";
14
15
  import cx from "classnames";
15
16
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
16
17
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -75,6 +76,7 @@ function ChatEmptyState({ targetSession }) {
75
76
  className: "flex-1 flex flex-col min-w-0",
76
77
  children: [
77
78
  /* @__PURE__ */ jsx(BlockedGroupBanner, {}),
79
+ /* @__PURE__ */ jsx(ProviderIssueBanner, {}),
78
80
  sessionUnreachable && !sessionBannerDismissed && /* @__PURE__ */ jsxs("div", {
79
81
  className: "mx-4 mt-2 flex items-start gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20",
80
82
  children: [
@@ -1,8 +1,9 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { AttachmentMeta } from '../types.js';
2
+ import type { AttachmentMeta, MessagePaste } from '../types.js';
3
3
  import { type SlashMenuConfig } from './SlashMenu.js';
4
+ import { type ComposerPastes } from './usePasteCards.js';
4
5
  interface ChatInputProps {
5
- onSend: (content: string, attachmentIds?: string[], attachmentMeta?: AttachmentMeta) => void;
6
+ onSend: (content: string, attachmentIds?: string[], attachmentMeta?: AttachmentMeta, pastes?: MessagePaste[]) => void;
6
7
  disabled?: boolean;
7
8
  placeholder?: string | undefined;
8
9
  conversationId?: string;
@@ -33,6 +34,7 @@ interface ChatInputProps {
33
34
  onStop: () => void;
34
35
  stopping?: boolean;
35
36
  };
37
+ pastes?: ComposerPastes | undefined;
36
38
  }
37
39
  export interface ChatInputHandle {
38
40
  addFiles: (files: FileList | File[]) => void;
@@ -7,7 +7,9 @@ import AttachmentUpload from "./AttachmentUpload.js";
7
7
  import useDragDrop from "../core/useDragDrop.js";
8
8
  import { ComposerContext, ComposerSettings } from "./ComposerChrome.js";
9
9
  import DragOverlay from "./DragOverlay.js";
10
+ import { PasteFileCard } from "./PastedText.js";
10
11
  import { SlashMenu, useSlashMenu } from "./SlashMenu.js";
12
+ import { usePasteCards } from "./usePasteCards.js";
11
13
  import cx from "classnames";
12
14
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
13
15
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -84,8 +86,12 @@ function streamControls(chat, turn) {
84
86
  queuedCount: chat.queuedMessages.length
85
87
  };
86
88
  }
87
- var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEditLastMessage, flush = false, allowEmpty = false, attachments: attachmentsWanted = true, requireModel = true, inputLabel, sendLabel, sendTestId, slash, context, settingsLeft, settingsRight, turn }, ref) {
89
+ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEditLastMessage, flush = false, allowEmpty = false, attachments: attachmentsWanted = true, requireModel = true, inputLabel, sendLabel, sendTestId, slash, context, settingsLeft, settingsRight, turn, pastes }, ref) {
88
90
  const [input, setInput] = useState(() => readDraft(conversationId));
91
+ const { cards, handleTextPaste, removeCard, takeCards, blocking: pastesBlocking } = usePasteCards(pastes, input, (value) => {
92
+ setInput(value);
93
+ storeDraft(conversationId, value);
94
+ });
89
95
  const [attachments, setAttachments] = useState([]);
90
96
  const [showAttachments, setShowAttachments] = useState(false);
91
97
  const textareaRef = useRef(null);
@@ -118,8 +124,9 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
118
124
  if (previousConversationRef.current !== conversationId) {
119
125
  previousConversationRef.current = conversationId;
120
126
  setInput(readDraft(conversationId));
127
+ takeCards();
121
128
  }
122
- }, [conversationId]);
129
+ }, [conversationId, takeCards]);
123
130
  const setInputAndPersist = (value) => {
124
131
  setInput(value);
125
132
  storeDraft(conversationId, value);
@@ -128,16 +135,16 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
128
135
  const pendingCount = attachments.length - uploadedCount;
129
136
  const canFlushQueue = !isStreaming && queuedCount > 0 && !disabled;
130
137
  const { canSend, emphasised } = sendability({
131
- hasText: input.trim().length > 0,
138
+ hasText: input.trim().length > 0 || cards.length > 0,
132
139
  canFlushQueue,
133
140
  allowEmpty,
134
141
  disabled,
135
142
  modelReady: isSelectedModelAvailable || !requireModel,
136
- pendingCount
143
+ pendingCount: pendingCount + (pastesBlocking ? 1 : 0)
137
144
  });
138
145
  const handleSubmit = () => {
139
146
  if (!canSend) return;
140
- if (!input.trim() && canFlushQueue) {
147
+ if (!input.trim() && cards.length === 0 && canFlushQueue) {
141
148
  flushQueuedMessages();
142
149
  return;
143
150
  }
@@ -152,7 +159,12 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
152
159
  size: a.size
153
160
  };
154
161
  }
155
- onSend(input.trim(), attachmentIds.length > 0 ? attachmentIds : void 0, attachmentIds.length > 0 ? meta : void 0);
162
+ const sentPastes = takeCards();
163
+ const content = [input.trim(), ...sentPastes.map((p) => p.placeholder)].filter(Boolean).join("\n\n");
164
+ const ids = attachmentIds.length > 0 ? attachmentIds : void 0;
165
+ const attachmentMeta = attachmentIds.length > 0 ? meta : void 0;
166
+ if (sentPastes.length > 0) onSend(content, ids, attachmentMeta, sentPastes);
167
+ else onSend(content, ids, attachmentMeta);
156
168
  setInput("");
157
169
  clearDraft(conversationId);
158
170
  setAttachments([]);
@@ -177,11 +189,13 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
177
189
  }, 0);
178
190
  }, []);
179
191
  const handlePaste = (e) => {
180
- if (!attachmentsAvailable) return;
181
192
  const files = Array.from(e.clipboardData?.files ?? []);
182
- if (files.length === 0) return;
183
- e.preventDefault();
184
- handleFilesDropped(files);
193
+ if (attachmentsAvailable && files.length > 0) {
194
+ e.preventDefault();
195
+ handleFilesDropped(files);
196
+ return;
197
+ }
198
+ if (pastes && handleTextPaste(e.clipboardData.getData("text/plain"))) e.preventDefault();
185
199
  };
186
200
  const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped: handleFilesDropped });
187
201
  return /* @__PURE__ */ jsxs("div", {
@@ -201,6 +215,18 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
201
215
  })
202
216
  }),
203
217
  context && /* @__PURE__ */ jsx(ComposerContext, { children: context }),
218
+ cards.length > 0 && /* @__PURE__ */ jsx("div", {
219
+ className: "mb-2 flex flex-wrap gap-2",
220
+ "data-testid": "composer-pastes",
221
+ children: cards.map((card) => /* @__PURE__ */ jsx(PasteFileCard, {
222
+ id: card.paste?.id,
223
+ lines: card.lines,
224
+ bytes: card.bytes,
225
+ saving: !card.paste && !card.error,
226
+ error: card.error,
227
+ onRemove: () => removeCard(card.key)
228
+ }, card.key))
229
+ }),
204
230
  /* @__PURE__ */ jsxs("div", {
205
231
  className: "relative",
206
232
  children: [slash && slashMenu.open && /* @__PURE__ */ jsx(SlashMenu, {
@@ -15,6 +15,7 @@ import Markdown from "../ui/Markdown.js";
15
15
  import { chatProseClasses } from "../ui/prose.js";
16
16
  import AgentMessageParts from "./agent/AgentMessageParts.js";
17
17
  import BranchNavigator from "./BranchNavigator.js";
18
+ import { UserMessageText } from "./PastedText.js";
18
19
  import { Avatar } from "../ui/Avatar.js";
19
20
  import cx from "classnames";
20
21
  import { memo, useEffect, useRef, useState } from "react";
@@ -190,11 +191,17 @@ function ChatMessage({ message, allMessages, isStreaming = false, isEditing = fa
190
191
  className: "flex-shrink-0 mb-0.5"
191
192
  }), /* @__PURE__ */ jsx("div", {
192
193
  className: "px-4 py-2.5 chat-ink rounded-2xl rounded-bl-md max-w-[70%] inline-block whitespace-pre-wrap text-base leading-6 bg-[color-mix(in_oklab,var(--theme-panel)_6%,transparent)]",
193
- children: message.content
194
+ children: /* @__PURE__ */ jsx(UserMessageText, {
195
+ content: message.content,
196
+ pastes: message.pastes
197
+ })
194
198
  })]
195
199
  }) : isUser ? /* @__PURE__ */ jsx("div", {
196
200
  className: "px-4 py-2.5 chat-ink rounded-2xl rounded-br-md max-w-[70%] inline-block whitespace-pre-wrap text-base leading-6 bg-[color-mix(in_oklab,var(--theme-panel)_6%,transparent)]",
197
- children: message.content
201
+ children: /* @__PURE__ */ jsx(UserMessageText, {
202
+ content: message.content,
203
+ pastes: message.pastes
204
+ })
198
205
  }) : /* @__PURE__ */ jsx("div", {
199
206
  ref: contentRef,
200
207
  className: cx(chatProseClasses, collapsed && "chat-message-clamp"),
@@ -6,6 +6,8 @@ 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. */
10
+ canRemoveQueued?: (id: string) => boolean;
9
11
  editingMessageId?: string | null | undefined;
10
12
  showAuthor?: boolean;
11
13
  currentUsername?: string;
@@ -31,5 +33,5 @@ interface ChatMessageListProps {
31
33
  * for the first frame is not announced twice. */
32
34
  workingIndicator?: boolean;
33
35
  }
34
- export default function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator, }: ChatMessageListProps): import("react").JSX.Element;
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;
35
37
  export {};
@@ -11,7 +11,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
11
11
  import { jsx, jsxs } from "react/jsx-runtime";
12
12
  import { Duration } from "luxon";
13
13
  //#region src/ai/components/ChatMessageList.tsx
14
- function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator = true }) {
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
15
  const { adapter, isStreaming: providerStreaming, isThinking, streamingReasoning, streamingParts, thinkingStartTime } = useChat();
16
16
  const isStreaming = isStreamingProp ?? providerStreaming;
17
17
  const { strings } = useChatConfig();
@@ -266,7 +266,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage, queuedMessag
266
266
  flush,
267
267
  attachmentDownloadUrl,
268
268
  onOpenAttachment
269
- }), onRemoveQueued && /* @__PURE__ */ jsx("button", {
269
+ }), onRemoveQueued && (canRemoveQueued?.(msg.id) ?? true) && /* @__PURE__ */ jsx("button", {
270
270
  type: "button",
271
271
  "aria-label": strings.queue.remove,
272
272
  title: strings.queue.remove,
@@ -8,6 +8,7 @@ import useDragDrop from "../core/useDragDrop.js";
8
8
  import { ComposerControls, ConnectToolsLink } from "./ComposerChrome.js";
9
9
  import DragOverlay from "./DragOverlay.js";
10
10
  import ChatInput from "./ChatInput.js";
11
+ import ProviderIssueBanner from "./ProviderIssueBanner.js";
11
12
  import { ShareButton } from "./ShareDialog.js";
12
13
  import ChatMessageList from "./ChatMessageList.js";
13
14
  import cx from "classnames";
@@ -118,6 +119,7 @@ function ChatThread({ conversationId }) {
118
119
  children: [
119
120
  isDragging && attachmentsAvailable && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-lg" }),
120
121
  /* @__PURE__ */ jsx(BlockedGroupBanner, {}),
122
+ /* @__PURE__ */ jsx(ProviderIssueBanner, {}),
121
123
  /* @__PURE__ */ jsx(ChatMessageList, {
122
124
  messages,
123
125
  allMessages: messages,
@@ -2,9 +2,11 @@ import { FaCheck } from "../../icons/fa/FaCheck.js";
2
2
  import { FaChevronDown } from "../../icons/fa/FaChevronDown.js";
3
3
  import { FaCloud } from "../../icons/fa/FaCloud.js";
4
4
  import { FaCog } from "../../icons/fa/FaCog.js";
5
+ import { FaExclamationTriangle } from "../../icons/fa/FaExclamationTriangle.js";
5
6
  import { FaRobot } from "../../icons/fa/FaRobot.js";
6
7
  import { FaUndoAlt } from "../../icons/fa/FaUndoAlt.js";
7
8
  import { useChatConfig } from "../core/config.js";
9
+ import { providerIssueFor } from "../core/providerIssues.js";
8
10
  import { useChat } from "../core/ChatProvider.js";
9
11
  import cx from "classnames";
10
12
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -46,8 +48,8 @@ function groupModelsByProvider(models) {
46
48
  }
47
49
  function ModelSelector({ targetSession, variant = "default" }) {
48
50
  const bare = variant === "bare";
49
- const { providers, models, unreachableSessions, selectedProvider, setSelectedProvider, isLoadingModels, hasLoadedModels, modelsError, refreshModels, currentUser } = useChat();
50
- const { extraLinks, LinkComponent } = useChatConfig();
51
+ const { providers, models, unreachableSessions, providerIssues, selectedProvider, setSelectedProvider, isLoadingModels, hasLoadedModels, modelsError, refreshModels, currentUser } = useChat();
52
+ const { extraLinks, LinkComponent, strings } = useChatConfig();
51
53
  const [isOpen, setIsOpen] = useState(false);
52
54
  const [searchQuery, setSearchQuery] = useState("");
53
55
  const [dropdownPosition, setDropdownPosition] = useState({
@@ -115,6 +117,10 @@ function ModelSelector({ targetSession, variant = "default" }) {
115
117
  const selectedModelInfo = models.find((m) => m.id === selectedProvider);
116
118
  const selectedModelName = selectedModelInfo ? getModelLabel(selectedModelInfo) : null;
117
119
  const selectedProviderName = selectedModelInfo?.provider || null;
120
+ const issueLabel = (issue) => issue.status === "unauthorized" ? strings.providerIssue.keyRejected : strings.providerIssue.unreachable;
121
+ const issueDetail = (issue) => issue.message || (issue.status === "unauthorized" ? strings.providerIssue.keyRejectedHint : strings.providerIssue.unreachableHint);
122
+ const issueSummary = (issue) => `${issueLabel(issue)} \u2014 ${issueDetail(issue)}`;
123
+ const selectedModelIssue = providerIssueFor(providerIssues, selectedModelInfo);
118
124
  const handleSelect = (modelId) => {
119
125
  setSelectedProvider(modelId);
120
126
  setIsOpen(false);
@@ -183,15 +189,22 @@ function ModelSelector({ targetSession, variant = "default" }) {
183
189
  className: "flex-1 min-w-0 text-left",
184
190
  children: selectedModelName ? /* @__PURE__ */ jsxs("div", {
185
191
  className: "flex items-baseline gap-1.5 min-w-0",
186
- children: [/* @__PURE__ */ jsx("span", {
187
- title: selectedModelName,
188
- className: cx("truncate", bare ? "text-[11px] theme-muted-text" : "text-sm font-medium text-(--theme-panel)"),
189
- children: selectedModelName
190
- }), selectedProviderName && /* @__PURE__ */ jsx("span", {
191
- title: selectedProviderName,
192
- className: cx("theme-muted-text truncate flex-shrink-0 max-w-[45%]", bare ? "text-[10px]" : "text-xs"),
193
- children: selectedProviderName
194
- })]
192
+ children: [
193
+ /* @__PURE__ */ jsx("span", {
194
+ title: selectedModelName,
195
+ className: cx("truncate", bare ? "text-[11px] theme-muted-text" : "text-sm font-medium text-(--theme-panel)"),
196
+ children: selectedModelName
197
+ }),
198
+ selectedProviderName && /* @__PURE__ */ jsx("span", {
199
+ title: selectedProviderName,
200
+ className: cx("theme-muted-text truncate flex-shrink-0 max-w-[45%]", bare ? "text-[10px]" : "text-xs"),
201
+ children: selectedProviderName
202
+ }),
203
+ selectedModelIssue && /* @__PURE__ */ jsx(FaExclamationTriangle, {
204
+ title: issueSummary(selectedModelIssue),
205
+ className: "h-3.5 w-3.5 text-amber-500 flex-shrink-0 self-center"
206
+ })
207
+ ]
195
208
  }) : /* @__PURE__ */ jsx("span", {
196
209
  className: cx(bare ? "text-[11px]" : "text-sm", targetSessionStatus === "connecting" ? "text-amber-600" : "theme-muted-text"),
197
210
  children: triggerText
@@ -287,58 +300,77 @@ function ModelSelector({ targetSession, variant = "default" }) {
287
300
  const platformSuffix = providerInfo && platform && platform.toLowerCase() !== providerName.toLowerCase() ? platform : null;
288
301
  const isOwner = providerInfo?.user === currentUser.username;
289
302
  const settingsHref = providerInfo && extraLinks.providerSettings ? extraLinks.providerSettings(providerInfo.user, providerInfo.name) : null;
290
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
291
- className: "px-3 py-1.5 theme-muted-panel sticky top-0",
292
- children: /* @__PURE__ */ jsxs("div", {
293
- className: "flex items-center gap-2",
294
- children: [
295
- /* @__PURE__ */ jsx(CspIcon, {
296
- cspKind: providerInfo?.cspKind ?? "other",
297
- className: "h-3 w-3"
298
- }),
299
- /* @__PURE__ */ jsx("span", {
300
- className: "text-xs font-semibold theme-muted-text uppercase tracking-wide",
301
- children: providerName
302
- }),
303
- /* @__PURE__ */ jsx("span", {
304
- className: "min-w-0 flex-1 truncate text-xs theme-muted-text",
305
- children: platformSuffix ? `· ${platformSuffix}` : ""
306
- }),
307
- /* @__PURE__ */ jsxs("span", {
308
- className: "text-xs theme-muted-text",
309
- children: [
310
- "(",
311
- providerModels.length,
312
- ")"
313
- ]
314
- }),
315
- isOwner && settingsHref && /* @__PURE__ */ jsx(LinkComponent, {
316
- target: {
317
- kind: "external",
318
- href: settingsHref
319
- },
320
- onClick: (e) => e.stopPropagation(),
321
- className: "p-0.5 rounded hover:theme-hover theme-muted-text hover:text-blue-600 transition-colors",
322
- title: "Manage provider",
323
- children: /* @__PURE__ */ jsx(FaCog, { className: "h-3 w-3" })
324
- })
325
- ]
303
+ const issue = providerIssueFor(providerIssues, providerModels[0]);
304
+ return /* @__PURE__ */ jsxs("div", { children: [
305
+ /* @__PURE__ */ jsx("div", {
306
+ className: "px-3 py-1.5 theme-muted-panel sticky top-0",
307
+ children: /* @__PURE__ */ jsxs("div", {
308
+ className: "flex items-center gap-2",
309
+ children: [
310
+ /* @__PURE__ */ jsx(CspIcon, {
311
+ cspKind: providerInfo?.cspKind ?? "other",
312
+ className: "h-3 w-3"
313
+ }),
314
+ /* @__PURE__ */ jsx("span", {
315
+ className: "text-xs font-semibold theme-muted-text uppercase tracking-wide",
316
+ children: providerName
317
+ }),
318
+ /* @__PURE__ */ jsx("span", {
319
+ className: "min-w-0 flex-1 truncate text-xs theme-muted-text",
320
+ children: platformSuffix ? `· ${platformSuffix}` : ""
321
+ }),
322
+ /* @__PURE__ */ jsxs("span", {
323
+ className: "text-xs theme-muted-text",
324
+ children: [
325
+ "(",
326
+ providerModels.length,
327
+ ")"
328
+ ]
329
+ }),
330
+ issue && /* @__PURE__ */ jsx("span", {
331
+ className: "text-[10px] text-amber-600 bg-amber-500/10 px-1.5 py-0.5 rounded-full flex-shrink-0",
332
+ children: issueLabel(issue)
333
+ }),
334
+ isOwner && settingsHref && /* @__PURE__ */ jsx(LinkComponent, {
335
+ target: {
336
+ kind: "external",
337
+ href: settingsHref
338
+ },
339
+ onClick: (e) => e.stopPropagation(),
340
+ className: "p-0.5 rounded hover:theme-hover theme-muted-text hover:text-blue-600 transition-colors",
341
+ title: "Manage provider",
342
+ children: /* @__PURE__ */ jsx(FaCog, { className: "h-3 w-3" })
343
+ })
344
+ ]
345
+ })
346
+ }),
347
+ issue && /* @__PURE__ */ jsxs("div", {
348
+ className: "flex items-start gap-2 px-4 py-1.5 bg-amber-500/5",
349
+ children: [/* @__PURE__ */ jsx(FaExclamationTriangle, { className: "h-3 w-3 text-amber-500 flex-shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", {
350
+ className: "text-xs text-amber-600 line-clamp-2",
351
+ children: issueDetail(issue)
352
+ })]
353
+ }),
354
+ providerModels.map((model) => {
355
+ const modelName = getModelLabel(model);
356
+ const isSelected = model.id === selectedProvider;
357
+ return /* @__PURE__ */ jsxs("button", {
358
+ type: "button",
359
+ "aria-disabled": !!issue,
360
+ title: issue ? issueSummary(issue) : void 0,
361
+ onClick: () => {
362
+ if (!issue) handleSelect(model.id);
363
+ },
364
+ className: cx("w-full flex items-center gap-3 px-4 py-2 text-left", issue ? "opacity-40 cursor-not-allowed" : "cursor-pointer hover:bg-blue-500/5 transition-colors", isSelected && "bg-blue-500/10"),
365
+ children: [/* @__PURE__ */ jsx("span", {
366
+ title: modelName,
367
+ className: cx("flex-1 min-w-0 text-sm truncate", isSelected ? "text-blue-600 font-medium" : ""),
368
+ style: !isSelected ? { color: "var(--theme-panel, #1f2937)" } : void 0,
369
+ children: modelName
370
+ }), isSelected && /* @__PURE__ */ jsx(FaCheck, { className: "h-3.5 w-3.5 text-blue-600 flex-shrink-0" })]
371
+ }, model.id);
326
372
  })
327
- }), providerModels.map((model) => {
328
- const modelName = getModelLabel(model);
329
- const isSelected = model.id === selectedProvider;
330
- return /* @__PURE__ */ jsxs("button", {
331
- type: "button",
332
- onClick: () => handleSelect(model.id),
333
- className: cx("w-full flex items-center gap-3 px-4 py-2 text-left cursor-pointer", "hover:bg-blue-500/5 transition-colors", isSelected && "bg-blue-500/10"),
334
- children: [/* @__PURE__ */ jsx("span", {
335
- title: modelName,
336
- className: cx("flex-1 min-w-0 text-sm truncate", isSelected ? "text-blue-600 font-medium" : ""),
337
- style: !isSelected ? { color: "var(--theme-panel, #1f2937)" } : void 0,
338
- children: modelName
339
- }), isSelected && /* @__PURE__ */ jsx(FaCheck, { className: "h-3.5 w-3.5 text-blue-600 flex-shrink-0" })]
340
- }, model.id);
341
- })] }, providerKey);
373
+ ] }, providerKey);
342
374
  })
343
375
  ]
344
376
  })]
@@ -0,0 +1,15 @@
1
+ import type { MessagePaste } from '../types.js';
2
+ /** A paste sent, or about to be sent, as its saved file. */
3
+ export declare function PasteFileCard({ id, lines, bytes, saving, error, onRemove, }: {
4
+ id?: number | undefined;
5
+ lines: number;
6
+ bytes: number;
7
+ saving?: boolean;
8
+ error?: string | undefined;
9
+ onRemove?: (() => void) | undefined;
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. */
12
+ export declare function UserMessageText({ content, pastes, }: {
13
+ content: string;
14
+ pastes?: MessagePaste[] | null | undefined;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,59 @@
1
+ import { FiFile } from "../../icons/fi/FiFile.js";
2
+ import { FiX } from "../../icons/fi/FiX.js";
3
+ import { ImSpinner5 } from "../../icons/im/ImSpinner5.js";
4
+ import { useChatConfig } from "../core/config.js";
5
+ import { formatFileSize } from "../utils.js";
6
+ import { splitPastes } from "../core/pastes.js";
7
+ import cx from "classnames";
8
+ import { Fragment } from "react";
9
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
+ //#region src/ai/components/PastedText.tsx
11
+ /** A paste sent, or about to be sent, as its saved file. */
12
+ function PasteFileCard({ id, lines, bytes, saving = false, error, onRemove }) {
13
+ const t = useChatConfig().strings.paste;
14
+ const detail = error ?? (saving ? t.saving : `${t.lines(lines)} · ${formatFileSize(bytes)}`);
15
+ return /* @__PURE__ */ jsxs("div", {
16
+ "data-testid": "paste-file-card",
17
+ className: cx("inline-flex max-w-full items-center gap-2.5 rounded-xl border px-3 py-2 align-top whitespace-normal", "bg-(--theme-muted-panel-bg)", error ? "border-red-500" : "theme-border"),
18
+ children: [
19
+ /* @__PURE__ */ jsx(FiFile, { className: "h-5 w-5 flex-shrink-0 theme-muted-text" }),
20
+ /* @__PURE__ */ jsxs("div", {
21
+ className: "min-w-0 text-left leading-tight",
22
+ children: [/* @__PURE__ */ jsx("div", {
23
+ className: "truncate text-sm font-medium theme-text",
24
+ children: id ? `paste-${id}.txt` : t.title
25
+ }), /* @__PURE__ */ jsx("div", {
26
+ className: cx("truncate text-xs", error ? "text-red-500" : "theme-muted-text"),
27
+ children: detail
28
+ })]
29
+ }),
30
+ saving && /* @__PURE__ */ jsx(ImSpinner5, { className: "h-3.5 w-3.5 flex-shrink-0 animate-spin theme-muted-text" }),
31
+ onRemove && /* @__PURE__ */ jsx("button", {
32
+ type: "button",
33
+ onClick: onRemove,
34
+ "aria-label": t.remove,
35
+ title: t.remove,
36
+ className: "flex-shrink-0 rounded p-0.5 theme-muted-text hover:theme-text",
37
+ children: /* @__PURE__ */ jsx(FiX, { className: "h-3.5 w-3.5" })
38
+ })
39
+ ]
40
+ });
41
+ }
42
+ /** A user message as typed: an inline paste as its text, one sent as its file as a card. */
43
+ function UserMessageText({ content, pastes }) {
44
+ if (!pastes?.length) return /* @__PURE__ */ jsx(Fragment$1, { children: content });
45
+ return /* @__PURE__ */ jsx(Fragment$1, { children: splitPastes(content, pastes).map((segment, i) => {
46
+ if (typeof segment === "string") return /* @__PURE__ */ jsx(Fragment, { children: segment }, i);
47
+ if (segment.inline && segment.text !== void 0) return /* @__PURE__ */ jsx(Fragment, { children: segment.text }, i);
48
+ return /* @__PURE__ */ jsx("span", {
49
+ className: "my-1 block",
50
+ children: /* @__PURE__ */ jsx(PasteFileCard, {
51
+ id: segment.id,
52
+ lines: segment.lines,
53
+ bytes: segment.bytes
54
+ })
55
+ }, i);
56
+ }) });
57
+ }
58
+ //#endregion
59
+ export { PasteFileCard, UserMessageText };
@@ -0,0 +1 @@
1
+ export default function ProviderIssueBanner(): import("react").JSX.Element | null;
@@ -0,0 +1,30 @@
1
+ import { FaExclamationTriangle } from "../../icons/fa/FaExclamationTriangle.js";
2
+ import { useChatConfig } from "../core/config.js";
3
+ import { providerIssueFor } from "../core/providerIssues.js";
4
+ import { useChat } from "../core/ChatProvider.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/ai/components/ProviderIssueBanner.tsx
7
+ function ProviderIssueBanner() {
8
+ const { strings } = useChatConfig();
9
+ const { models, providerIssues, selectedProvider } = useChat();
10
+ const selectedModel = models.find((m) => m.id === selectedProvider);
11
+ const issue = providerIssueFor(providerIssues, selectedModel);
12
+ if (!issue) return null;
13
+ const title = issue.status === "unauthorized" ? strings.providerIssue.bannerKeyRejected(issue.provider) : strings.providerIssue.bannerUnreachable(issue.provider);
14
+ const detail = issue.message || (issue.status === "unauthorized" ? strings.providerIssue.keyRejectedHint : strings.providerIssue.unreachableHint);
15
+ return /* @__PURE__ */ jsxs("div", {
16
+ className: "mx-4 mt-2 flex items-start gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20",
17
+ children: [/* @__PURE__ */ jsx(FaExclamationTriangle, { className: "w-4 h-4 text-amber-500 shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
18
+ className: "flex-1 min-w-0",
19
+ children: [/* @__PURE__ */ jsx("p", {
20
+ className: "text-sm font-medium text-amber-700",
21
+ children: title
22
+ }), /* @__PURE__ */ jsx("p", {
23
+ className: "text-xs text-amber-600/80 mt-0.5",
24
+ children: detail
25
+ })]
26
+ })]
27
+ });
28
+ }
29
+ //#endregion
30
+ export { ProviderIssueBanner as default };
@@ -0,0 +1,30 @@
1
+ import type { MessagePaste } from '../types.js';
2
+ export type PasteUploadResult = {
3
+ paste: MessagePaste;
4
+ } | {
5
+ error: string;
6
+ } | {
7
+ unsupported: true;
8
+ };
9
+ /** Pastes over inlineMaxBytes are saved with the target and sent by placeholder; undefined keeps them text. */
10
+ export interface ComposerPastes {
11
+ inlineMaxBytes: number | undefined;
12
+ upload: (text: string) => Promise<PasteUploadResult>;
13
+ }
14
+ /** A paste waiting in the composer as a file card; paste is set once saved. */
15
+ export interface PasteCard {
16
+ key: string;
17
+ text: string;
18
+ lines: number;
19
+ bytes: number;
20
+ paste?: MessagePaste;
21
+ error?: string;
22
+ }
23
+ /** Keeps the composer's cards in step with the model, so text or card shows how each paste is sent. */
24
+ export declare function usePasteCards(pastes: ComposerPastes | undefined, input: string, setInput: (value: string) => void): {
25
+ cards: PasteCard[];
26
+ handleTextPaste: (raw: string) => boolean;
27
+ removeCard: (key: string) => void;
28
+ takeCards: () => MessagePaste[];
29
+ blocking: boolean;
30
+ };