@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
@@ -0,0 +1,129 @@
1
+ import { countLines, normalizeNewlines, utf8Bytes } from "../core/pastes.js";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ //#region src/ai/components/usePasteCards.ts
4
+ function appendText(box, text) {
5
+ return box.trim() ? `${box}\n\n${text}` : text;
6
+ }
7
+ /** Keeps the composer's cards in step with the model, so text or card shows how each paste is sent. */
8
+ function usePasteCards(pastes, input, setInput) {
9
+ const [cards, setCards] = useState([]);
10
+ const inBox = useRef([]);
11
+ const saved = useRef(/* @__PURE__ */ new Map());
12
+ const uploading = useRef(/* @__PURE__ */ new Set());
13
+ const seq = useRef(0);
14
+ const latest = useRef({
15
+ cards,
16
+ input,
17
+ pastes,
18
+ setInput
19
+ });
20
+ latest.current = {
21
+ cards,
22
+ input,
23
+ pastes,
24
+ setInput
25
+ };
26
+ const addCard = useCallback((text) => {
27
+ const target = latest.current.pastes;
28
+ if (!target) return;
29
+ seq.current += 1;
30
+ const card = {
31
+ key: `paste-${seq.current}`,
32
+ text,
33
+ lines: countLines(text),
34
+ bytes: utf8Bytes(text)
35
+ };
36
+ const known = saved.current.get(text);
37
+ if (known) {
38
+ setCards((current) => [...current, {
39
+ ...card,
40
+ paste: known
41
+ }]);
42
+ return;
43
+ }
44
+ setCards((current) => [...current, card]);
45
+ uploading.current.add(card.key);
46
+ const settle = (result) => {
47
+ const waiting = uploading.current.delete(card.key);
48
+ if ("unsupported" in result) {
49
+ setCards((current) => current.filter((c) => c.key !== card.key));
50
+ if (waiting) latest.current.setInput(appendText(latest.current.input, text));
51
+ return;
52
+ }
53
+ if ("paste" in result) saved.current.set(text, result.paste);
54
+ setCards((current) => current.map((c) => {
55
+ if (c.key !== card.key) return c;
56
+ return "error" in result ? {
57
+ ...c,
58
+ error: result.error
59
+ } : {
60
+ ...c,
61
+ paste: result.paste
62
+ };
63
+ }));
64
+ };
65
+ target.upload(text).then(settle, (err) => settle({ error: err instanceof Error ? err.message : String(err) }));
66
+ }, []);
67
+ /** Reports whether the paste became a card, so the caller stops the browser pasting it too. */
68
+ const handleTextPaste = (raw) => {
69
+ if (!pastes || !raw) return false;
70
+ const text = normalizeNewlines(raw);
71
+ const bytes = utf8Bytes(text);
72
+ if (pastes.inlineMaxBytes === void 0 || bytes <= pastes.inlineMaxBytes && utf8Bytes(input) + bytes <= 1048576) {
73
+ if (bytes >= 1e3) inBox.current.push(text);
74
+ return false;
75
+ }
76
+ addCard(text);
77
+ return true;
78
+ };
79
+ const inlineMax = pastes?.inlineMaxBytes;
80
+ useEffect(() => {
81
+ if (inlineMax === void 0) return;
82
+ const { cards: current, input: box, setInput: set } = latest.current;
83
+ const boxed = inBox.current.filter((text) => box.includes(text));
84
+ const outgrown = boxed.filter((text) => utf8Bytes(text) > inlineMax);
85
+ let boxBytes = utf8Bytes(box) - outgrown.reduce((sum, text) => sum + utf8Bytes(text), 0);
86
+ for (const text of boxed.toReversed()) {
87
+ if (boxBytes <= 1048576) break;
88
+ if (!outgrown.includes(text)) {
89
+ outgrown.push(text);
90
+ boxBytes -= utf8Bytes(text);
91
+ }
92
+ }
93
+ const fitting = current.filter((c) => {
94
+ if (c.bytes > inlineMax || boxBytes + c.bytes > 1048576) return false;
95
+ boxBytes += c.bytes;
96
+ return true;
97
+ });
98
+ if (fitting.length === 0 && outgrown.length === 0) return;
99
+ let next = box;
100
+ for (const text of outgrown) next = next.replace(text, () => "");
101
+ for (const card of fitting) {
102
+ next = appendText(next, card.text);
103
+ uploading.current.delete(card.key);
104
+ }
105
+ inBox.current = [...inBox.current.filter((text) => !outgrown.includes(text)), ...fitting.map((c) => c.text)];
106
+ setCards((cs) => cs.filter((c) => !fitting.some((f) => f.key === c.key)));
107
+ set(next);
108
+ for (const text of outgrown) addCard(text);
109
+ }, [inlineMax, addCard]);
110
+ return {
111
+ cards,
112
+ handleTextPaste,
113
+ removeCard: useCallback((key) => {
114
+ uploading.current.delete(key);
115
+ setCards((current) => current.filter((c) => c.key !== key));
116
+ }, []),
117
+ takeCards: useCallback(() => {
118
+ const sent = latest.current.cards.flatMap((c) => c.paste ? [c.paste] : []);
119
+ setCards([]);
120
+ uploading.current.clear();
121
+ inBox.current = [];
122
+ saved.current.clear();
123
+ return sent;
124
+ }, []),
125
+ blocking: cards.some((c) => !c.paste)
126
+ };
127
+ }
128
+ //#endregion
129
+ export { usePasteCards };
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { ChatAdapter } from '../adapter/types.js';
3
- import type { AttachmentMeta, ChatModel, ChatNavigation, ChatNotify, ChatUser, Conversation, ConversationSummary, ChatMessage as Message, MessagePart, ProviderInfo, UnreachableSession } from '../types.js';
3
+ import type { AttachmentMeta, ChatModel, ChatNavigation, ChatNotify, ChatUser, Conversation, ConversationSummary, ChatMessage as Message, MessagePart, ProviderInfo, ProviderIssue, UnreachableSession } from '../types.js';
4
4
  import { type ChatUIConfig } from './config.js';
5
5
  interface ChatContextValue {
6
6
  adapter: ChatAdapter;
@@ -13,6 +13,7 @@ interface ChatContextValue {
13
13
  providers: ProviderInfo[];
14
14
  models: ChatModel[];
15
15
  unreachableSessions: UnreachableSession[];
16
+ providerIssues: ProviderIssue[];
16
17
  selectedProvider: string | null;
17
18
  isSelectedModelAvailable: boolean;
18
19
  selectedAllocation: string | null;
@@ -1,6 +1,7 @@
1
1
  import { applyPartDelta, finalizeParts } from "./parts.js";
2
2
  import { chatReducer, initialState } from "./chatReducer.js";
3
3
  import { ChatConfigProvider, resolveChatConfig } from "./config.js";
4
+ import { providerIssueFor } from "./providerIssues.js";
4
5
  import { useSSEStream } from "./useSSEStream.js";
5
6
  import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from "react";
6
7
  import { jsx } from "react/jsx-runtime";
@@ -195,17 +196,19 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
195
196
  isLoadingModels: true
196
197
  });
197
198
  try {
198
- const { models, unreachableSessions } = await adapter.models.list();
199
+ const { models, unreachableSessions, providerIssues } = await adapter.models.list();
199
200
  dispatch({
200
201
  type: "SET_MODELS",
201
202
  models,
202
- unreachableSessions
203
+ unreachableSessions,
204
+ providerIssues
203
205
  });
204
206
  hasLoadedModelsRef.current = true;
205
207
  const current = selectedProviderRef.current;
206
208
  const stored = readStoredModel();
207
209
  const offers = (id) => !!id && models.some((m) => m.id === id);
208
- const resolved = offers(current) ? current : offers(stored) ? stored : models[0]?.id ?? null;
210
+ const firstUsable = models.find((model) => !providerIssueFor(providerIssues, model)) ?? models[0];
211
+ const resolved = offers(current) ? current : offers(stored) ? stored : firstUsable?.id ?? null;
209
212
  if (resolved !== current) dispatch({
210
213
  type: "SET_SELECTED_PROVIDER",
211
214
  providerId: resolved
@@ -750,6 +753,7 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
750
753
  providers: state.providers,
751
754
  models: state.models,
752
755
  unreachableSessions: state.unreachableSessions,
756
+ providerIssues: state.providerIssues,
753
757
  selectedProvider: state.selectedProvider,
754
758
  isSelectedModelAvailable,
755
759
  selectedAllocation: state.selectedAllocation,
@@ -1,11 +1,12 @@
1
1
  import type { PartDelta } from '../adapter/types.js';
2
- import type { ChatMessage, ChatModel, Conversation, ConversationSummary, MessagePart, ProviderInfo, UnreachableSession } from '../types.js';
2
+ import type { ChatMessage, ChatModel, Conversation, ConversationSummary, MessagePart, ProviderInfo, ProviderIssue, UnreachableSession } from '../types.js';
3
3
  export interface ChatState {
4
4
  conversations: ConversationSummary[];
5
5
  currentConversation: Conversation | null;
6
6
  providers: ProviderInfo[];
7
7
  models: ChatModel[];
8
8
  unreachableSessions: UnreachableSession[];
9
+ providerIssues: ProviderIssue[];
9
10
  selectedProvider: string | null;
10
11
  selectedAllocation: string | null;
11
12
  isLoading: boolean;
@@ -35,6 +36,7 @@ export type ChatAction = {
35
36
  type: 'SET_MODELS';
36
37
  models: ChatModel[];
37
38
  unreachableSessions?: UnreachableSession[];
39
+ providerIssues?: ProviderIssue[];
38
40
  } | {
39
41
  type: 'SET_SELECTED_PROVIDER';
40
42
  providerId: string | null;
@@ -6,6 +6,7 @@ var initialState = {
6
6
  providers: [],
7
7
  models: [],
8
8
  unreachableSessions: [],
9
+ providerIssues: [],
9
10
  selectedProvider: null,
10
11
  selectedAllocation: null,
11
12
  isLoading: false,
@@ -80,6 +81,7 @@ function chatReducer(state, action) {
80
81
  ...state,
81
82
  models: action.models,
82
83
  unreachableSessions: action.unreachableSessions || [],
84
+ providerIssues: action.providerIssues || [],
83
85
  hasLoadedModels: true,
84
86
  modelsError: null
85
87
  };
@@ -0,0 +1,14 @@
1
+ import type { MessagePaste } from '../types.js';
2
+ /** Pastes this small never turn into a card, whatever the model. */
3
+ export declare const PASTE_CARD_MIN_BYTES = 1000;
4
+ /** The most text sent inline, for one paste and for a whole message. */
5
+ export declare const PASTE_INLINE_CAP: number;
6
+ export declare function utf8Bytes(text: string): number;
7
+ /** Terminals and some browsers put CR line endings on the clipboard. */
8
+ export declare function normalizeNewlines(text: string): string;
9
+ /** Counts lines as a paste's label shows them: a final newline starts none. */
10
+ export declare function countLines(text: string): number;
11
+ /** The inline limit for a context window, for a host that has not reported its own. */
12
+ export declare function pasteInlineMaxBytes(contextWindow: number | undefined): number;
13
+ /** Splits a message into its text and the pastes its placeholders stand for. */
14
+ export declare function splitPastes(content: string, pastes: MessagePaste[]): (string | MessagePaste)[];
@@ -0,0 +1,43 @@
1
+ //#region src/ai/core/pastes.ts
2
+ /** Pastes this small never turn into a card, whatever the model. */
3
+ var PASTE_CARD_MIN_BYTES = 1e3;
4
+ /** The most text sent inline, for one paste and for a whole message. */
5
+ var PASTE_INLINE_CAP = 1 << 20;
6
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
7
+ var encoder = new TextEncoder();
8
+ function utf8Bytes(text) {
9
+ return encoder.encode(text).length;
10
+ }
11
+ /** Terminals and some browsers put CR line endings on the clipboard. */
12
+ function normalizeNewlines(text) {
13
+ return text.replace(/\r\n?/g, "\n");
14
+ }
15
+ /** Counts lines as a paste's label shows them: a final newline starts none. */
16
+ function countLines(text) {
17
+ return (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n").length;
18
+ }
19
+ /** The inline limit for a context window, for a host that has not reported its own. */
20
+ function pasteInlineMaxBytes(contextWindow) {
21
+ return Math.min(Math.floor((contextWindow || DEFAULT_CONTEXT_WINDOW) * 4 / 10), PASTE_INLINE_CAP);
22
+ }
23
+ function escapeRegExp(text) {
24
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ }
26
+ /** Splits a message into its text and the pastes its placeholders stand for. */
27
+ function splitPastes(content, pastes) {
28
+ const byPlaceholder = new Map(pastes.map((p) => [p.placeholder, p]));
29
+ if (byPlaceholder.size === 0) return [content];
30
+ const pattern = new RegExp([...byPlaceholder.keys()].map(escapeRegExp).join("|"), "g");
31
+ const out = [];
32
+ let last = 0;
33
+ for (const match of content.matchAll(pattern)) {
34
+ if (match.index > last) out.push(content.slice(last, match.index));
35
+ const paste = byPlaceholder.get(match[0]);
36
+ if (paste) out.push(paste);
37
+ last = match.index + match[0].length;
38
+ }
39
+ if (last < content.length) out.push(content.slice(last));
40
+ return out;
41
+ }
42
+ //#endregion
43
+ export { PASTE_CARD_MIN_BYTES, PASTE_INLINE_CAP, countLines, normalizeNewlines, pasteInlineMaxBytes, splitPastes, utf8Bytes };
@@ -0,0 +1,2 @@
1
+ import type { ChatModel, ProviderIssue } from '../types.js';
2
+ export declare function providerIssueFor(issues: ProviderIssue[], model: Pick<ChatModel, 'provider_name' | 'provider_owner'> | undefined): ProviderIssue | undefined;
@@ -0,0 +1,7 @@
1
+ //#region src/ai/core/providerIssues.ts
2
+ function providerIssueFor(issues, model) {
3
+ if (!model?.provider_name) return;
4
+ return issues.find((issue) => issue.provider_name === model.provider_name && (issue.provider_owner ?? "") === (model.provider_owner ?? ""));
5
+ }
6
+ //#endregion
7
+ export { providerIssueFor };
@@ -20,8 +20,10 @@ export { ComposerContext, ComposerControls, ComposerSettings, ConnectToolsLink,
20
20
  export { default as ComposerFrame } from './components/ComposerFrame.js';
21
21
  export { default as DragOverlay } from './components/DragOverlay.js';
22
22
  export { default as ModelSelector, getProviderKeyFromModelId, } from './components/ModelSelector.js';
23
+ export { default as ProviderIssueBanner } from './components/ProviderIssueBanner.js';
23
24
  export { default as ShareDialog, ShareButton, } from './components/ShareDialog.js';
24
25
  export { type SlashCommandOption, type SlashMenuConfig, slashMatches, } from './components/SlashMenu.js';
26
+ export type { ComposerPastes, PasteUploadResult, } from './components/usePasteCards.js';
25
27
  export { ChatProvider, type ChatProviderProps, useChat, } from './core/ChatProvider.js';
26
28
  export { type ChatAction, type ChatState, chatReducer, initialState, } from './core/chatReducer.js';
27
29
  export type { PartRendererProps } from './core/config.js';
@@ -30,6 +32,8 @@ export { FOCUS_SIDEBAR_SEARCH_EVENT } from './core/events.js';
30
32
  export { getGreeting } from './core/greeting.js';
31
33
  export { KeyboardShortcutsProvider } from './core/KeyboardShortcutsProvider.js';
32
34
  export { applyPartDelta, finalizeParts } from './core/parts.js';
35
+ export { pasteInlineMaxBytes } from './core/pastes.js';
36
+ export { providerIssueFor } from './core/providerIssues.js';
33
37
  export { default as useDragDrop } from './core/useDragDrop.js';
34
38
  export { useKeyboardShortcuts } from './core/useKeyboardShortcuts.js';
35
39
  export { type ChatStrings, defaultChatStrings } from './strings.js';
package/dist/ai/index.js CHANGED
@@ -10,6 +10,7 @@ import { applyPartDelta, finalizeParts } from "./core/parts.js";
10
10
  import { chatReducer, initialState } from "./core/chatReducer.js";
11
11
  import { defaultChatStrings } from "./strings.js";
12
12
  import { resolveChatConfig, useChatConfig } from "./core/config.js";
13
+ import { providerIssueFor } from "./core/providerIssues.js";
13
14
  import { ChatProvider, useChat } from "./core/ChatProvider.js";
14
15
  import Dropdown from "./ui/Dropdown.js";
15
16
  import { formatFileSize, isMac, isOrgModel } from "./utils.js";
@@ -26,8 +27,10 @@ import { getGreeting } from "./core/greeting.js";
26
27
  import useDragDrop from "./core/useDragDrop.js";
27
28
  import { ComposerContext, ComposerControls, ComposerSettings, ConnectToolsLink } from "./components/ComposerChrome.js";
28
29
  import DragOverlay from "./components/DragOverlay.js";
30
+ import { pasteInlineMaxBytes } from "./core/pastes.js";
29
31
  import { slashMatches } from "./components/SlashMenu.js";
30
32
  import ChatInput from "./components/ChatInput.js";
33
+ import ProviderIssueBanner from "./components/ProviderIssueBanner.js";
31
34
  import ChatEmptyState from "./components/ChatEmptyState.js";
32
35
  import { FOCUS_SIDEBAR_SEARCH_EVENT } from "./core/events.js";
33
36
  import { useKeyboardShortcuts } from "./core/useKeyboardShortcuts.js";
@@ -41,4 +44,4 @@ import ChatMessageList from "./components/ChatMessageList.js";
41
44
  import ChatThread from "./components/ChatThread.js";
42
45
  import ComposerFrame from "./components/ComposerFrame.js";
43
46
  import { NoticeBar, NoticeCard } from "./ui/Notice.js";
44
- export { AGENT_COLOR_NAMES, ActivityLine, AgentMessageParts, AllocationSelector, ApprovalBlock, AttachmentManager, AttachmentUpload, BlockedGroupBanner, BranchNavigator, ChatAdapterError, ChatEmptyState, ChatInput, ChatLayout, ChatMessageList, ChatMessage_default as ChatMessageView, ChatProvider, ChatSidebar, ChatThread, ComposerContext, ComposerControls, ComposerFrame, ComposerSettings, ConnectToolsLink, DEFAULT_PERMISSION_MODE, DragOverlay, Dropdown, FOCUS_SIDEBAR_SEARCH_EVENT, INTERRUPTED_NOTICE, KeyboardShortcutsProvider, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, ModelSelector, NAMED_PERMISSION_MODES, NoticeBar, NoticeCard, PermissionPicker, RenameDialog, ShareButton, ShareDialog, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, agentColorHex, agentTypeLabel, applyPartDelta, askQuestionFromArgs, bashExitCode, chatReducer, clampPermissionMode, classifyPseudoUserMessage, clip, compactCount, computeEditDiff, computeWriteRows, defaultChatStrings, durationToAbsHumanDuration, editSummary, finalizeParts, firstNonEmptyLine, formatElapsed, formatFileSize, fullToolArgs, getGreeting, getProviderKeyFromModelId, hiddenTranscriptTool, initialState, interruptionNotice, isEditDiffTool, isGroupableTool, isMac, isNamedPermissionMode, isOrgModel, languageForPath, lineDelta, parseEditArgs, parseSubagentNotice, permissionModesUpTo, planFromArgs, plural, resolveChatConfig, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, slashMatches, subagentDeliveryLine, summarizeToolArgs, taskCallFromArgs, thinkingHeading, tokenizeDiffLines, toolHeaderFor, toolSummary, useChat, useChatConfig, useDragDrop, useKeyboardShortcuts, useRowDialogs, wordEmphasisRanges };
47
+ export { AGENT_COLOR_NAMES, ActivityLine, AgentMessageParts, AllocationSelector, ApprovalBlock, AttachmentManager, AttachmentUpload, BlockedGroupBanner, BranchNavigator, ChatAdapterError, ChatEmptyState, ChatInput, ChatLayout, ChatMessageList, ChatMessage_default as ChatMessageView, ChatProvider, ChatSidebar, ChatThread, ComposerContext, ComposerControls, ComposerFrame, ComposerSettings, ConnectToolsLink, DEFAULT_PERMISSION_MODE, DragOverlay, Dropdown, FOCUS_SIDEBAR_SEARCH_EVENT, INTERRUPTED_NOTICE, KeyboardShortcutsProvider, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, ModelSelector, NAMED_PERMISSION_MODES, NoticeBar, NoticeCard, PermissionPicker, ProviderIssueBanner, RenameDialog, ShareButton, ShareDialog, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, agentColorHex, agentTypeLabel, applyPartDelta, askQuestionFromArgs, bashExitCode, chatReducer, clampPermissionMode, classifyPseudoUserMessage, clip, compactCount, computeEditDiff, computeWriteRows, defaultChatStrings, durationToAbsHumanDuration, editSummary, finalizeParts, firstNonEmptyLine, formatElapsed, formatFileSize, fullToolArgs, getGreeting, getProviderKeyFromModelId, hiddenTranscriptTool, initialState, interruptionNotice, isEditDiffTool, isGroupableTool, isMac, isNamedPermissionMode, isOrgModel, languageForPath, lineDelta, parseEditArgs, parseSubagentNotice, pasteInlineMaxBytes, permissionModesUpTo, planFromArgs, plural, providerIssueFor, resolveChatConfig, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, slashMatches, subagentDeliveryLine, summarizeToolArgs, taskCallFromArgs, thinkingHeading, tokenizeDiffLines, toolHeaderFor, toolSummary, useChat, useChatConfig, useDragDrop, useKeyboardShortcuts, useRowDialogs, wordEmphasisRanges };
@@ -25,6 +25,14 @@ export interface ChatStrings {
25
25
  title: string;
26
26
  message: string;
27
27
  };
28
+ providerIssue: {
29
+ keyRejected: string;
30
+ unreachable: string;
31
+ keyRejectedHint: string;
32
+ unreachableHint: string;
33
+ bannerKeyRejected: (provider: string) => string;
34
+ bannerUnreachable: (provider: string) => string;
35
+ };
28
36
  queue: {
29
37
  remove: string;
30
38
  };
@@ -51,6 +59,12 @@ export interface ChatStrings {
51
59
  sendMessage: string;
52
60
  filesAttached: (count: number) => string;
53
61
  };
62
+ paste: {
63
+ title: string;
64
+ lines: (count: number) => string;
65
+ saving: string;
66
+ remove: string;
67
+ };
54
68
  thinking: {
55
69
  label: string;
56
70
  ellipsis: string;
@@ -33,6 +33,14 @@ var defaultChatStrings = {
33
33
  message: "It has reached its allocation limit. Messages to this provider are paused until your administrator increases the allocation."
34
34
  },
35
35
  queue: { remove: "Remove queued message" },
36
+ providerIssue: {
37
+ keyRejected: "API key rejected",
38
+ unreachable: "Endpoint unreachable",
39
+ keyRejectedHint: "The provider rejected this connection’s API key. Update the key in the provider settings to resume.",
40
+ unreachableHint: "The provider’s endpoint did not respond. Check the endpoint URL and network access.",
41
+ bannerKeyRejected: (provider) => `API key rejected for \u201c${provider}\u201d`,
42
+ bannerUnreachable: (provider) => `\u201c${provider}\u201d is unreachable`
43
+ },
36
44
  activity: {
37
45
  tokens: (input, output) => `↑${input} ↓${output} tokens`,
38
46
  stop: "Stop"
@@ -60,6 +68,12 @@ var defaultChatStrings = {
60
68
  sendMessage: "Send message (Enter)",
61
69
  filesAttached: (count) => `${count} file${count > 1 ? "s" : ""} attached`
62
70
  },
71
+ paste: {
72
+ title: "Pasted text",
73
+ lines: (count) => `${count.toLocaleString()} line${count === 1 ? "" : "s"}`,
74
+ saving: "Saving…",
75
+ remove: "Remove paste"
76
+ },
63
77
  thinking: {
64
78
  label: "Thinking",
65
79
  ellipsis: "Thinking...",
@@ -120,6 +120,16 @@ export interface TodoSnapshotPart {
120
120
  export type MessagePart = TextPart | ReasoningPart | ToolCallPart | SubagentPart | ApprovalPart | NoticePart | TodoSnapshotPart;
121
121
  export type MessagePartKind = MessagePart['kind'];
122
122
  export type PseudoUserKind = 'command-input' | 'shell-input' | 'compaction';
123
+ /** A saved paste a user message names by its placeholder. The model got an
124
+ * inline paste as text; a larger one as its saved file. */
125
+ export interface MessagePaste {
126
+ placeholder: string;
127
+ id: number;
128
+ lines: number;
129
+ bytes: number;
130
+ inline?: boolean | undefined;
131
+ text?: string | undefined;
132
+ }
123
133
  export interface ChatMessage {
124
134
  id: string;
125
135
  role: string;
@@ -141,6 +151,7 @@ export interface ChatMessage {
141
151
  error?: string | null | undefined;
142
152
  parts?: MessagePart[] | null | undefined;
143
153
  pseudo?: PseudoUserKind | null | undefined;
154
+ pastes?: MessagePaste[] | null | undefined;
144
155
  }
145
156
  export interface Conversation {
146
157
  id: string;
@@ -187,6 +198,13 @@ export interface UnreachableSession {
187
198
  name: string;
188
199
  owner: string;
189
200
  }
201
+ export interface ProviderIssue {
202
+ provider: string;
203
+ provider_name: string;
204
+ provider_owner?: string;
205
+ status: 'unauthorized' | 'unreachable';
206
+ message?: string;
207
+ }
190
208
  export type CspKind = 'openai' | 'anthropic' | 'google' | 'azure' | 'aws' | 'other';
191
209
  export interface ProviderInfo {
192
210
  id: string;
@@ -1,3 +1,5 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type RowAction } from '../list/index.js';
1
3
  import type { IFileExplorerClient, IFileExplorerProvider } from './lib/fileExplorer.js';
2
4
  import type { ExplorerObject, TreeNode, TStorage } from './lib/types.js';
3
5
  interface IFileExplorerProps {
@@ -42,6 +44,10 @@ interface IFileExplorerProps {
42
44
  label: string;
43
45
  build: (objects: ExplorerObject[]) => string;
44
46
  } | undefined;
47
+ /** Host actions for a row's menu, placed after the built-in ones and before Copy. */
48
+ extraRowActions?: ((node: TreeNode, storage: TStorage | undefined) => RowAction[]) | undefined;
49
+ /** Host content for the selected folder's header, before its badges. */
50
+ headerAccessory?: ((node: TreeNode, storage: TStorage | undefined) => ReactNode) | undefined;
45
51
  }
46
- export default function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy, groups, groupsLoading, selectedPath, onPathChange, getProviderAndClient, treeOnly, initialExpandedPaths, onExpandedPathsChange, objectUris, downloadCommand, }: IFileExplorerProps): import("react").JSX.Element;
52
+ export default function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy, groups, groupsLoading, selectedPath, onPathChange, getProviderAndClient, treeOnly, initialExpandedPaths, onExpandedPathsChange, objectUris, downloadCommand, extraRowActions, headerAccessory, }: IFileExplorerProps): import("react").JSX.Element;
47
53
  export {};
@@ -120,7 +120,7 @@ function TreeFrame({ treeOnly, sidebarWidth, onWidthChange, children }) {
120
120
  children
121
121
  });
122
122
  }
123
- function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy = false, groups = [], groupsLoading = false, selectedPath, onPathChange, getProviderAndClient, treeOnly = false, initialExpandedPaths, onExpandedPathsChange, objectUris, downloadCommand }) {
123
+ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy = false, groups = [], groupsLoading = false, selectedPath, onPathChange, getProviderAndClient, treeOnly = false, initialExpandedPaths, onExpandedPathsChange, objectUris, downloadCommand, extraRowActions, headerAccessory }) {
124
124
  const copySubmenu = useCopySubmenu();
125
125
  const t = useStrings().fileExplorer;
126
126
  const notify = useNotify();
@@ -636,6 +636,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
636
636
  });
637
637
  }
638
638
  actions.push(...getCreateFolderActions(node, storage));
639
+ actions.push(...extraRowActions?.(node, storage) ?? []);
639
640
  if (nodeCanWrite && !node.root) actions.push({
640
641
  key: "delete",
641
642
  label: t.preview.delete,
@@ -1021,6 +1022,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
1021
1022
  children: [renderBreadcrumb(), /* @__PURE__ */ jsxs("div", {
1022
1023
  className: "flex shrink-0 items-center gap-2",
1023
1024
  children: [
1025
+ headerAccessory?.(selectedNode, findStorage(selectedNode.storageId)),
1024
1026
  folderHasMore && /* @__PURE__ */ jsx(StatusBadge, {
1025
1027
  variant: "muted",
1026
1028
  className: "whitespace-nowrap tabular-nums",
@@ -26,17 +26,20 @@ var SECTION_SHELL_PADDED = {
26
26
  var RowActionsContext = createContext({
27
27
  show: true,
28
28
  always: false,
29
- columnClasses: []
29
+ columnClasses: [],
30
+ columnStacks: []
30
31
  });
31
32
  function useRowActions(view) {
32
33
  return useMemo(() => ({
33
34
  show: view?.showActions ?? true,
34
35
  always: view?.alwaysShowActions ?? false,
35
- columnClasses: view?.columnClasses ?? []
36
+ columnClasses: view?.columnClasses ?? [],
37
+ columnStacks: view?.columnStacks ?? []
36
38
  }), [
37
39
  view?.showActions,
38
40
  view?.alwaysShowActions,
39
- view?.columnClasses
41
+ view?.columnClasses,
42
+ view?.columnStacks
40
43
  ]);
41
44
  }
42
45
  function ListRowActionsProvider({ view, children }) {
@@ -71,11 +74,21 @@ function ListPage({ section = "main", shellPadded, embedded = false, breadcrumbs
71
74
  }
72
75
  function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selected, selectionActive, onRowSelect, leading, className, children }) {
73
76
  const { active: menuActive, openMenu: openRowMenu } = useRowMenuActive(openMenu);
74
- const { show: showActions, always: alwaysShowActions, columnClasses } = useContext(RowActionsContext);
75
- const styledCells = Children.toArray(children).map((child, i) => {
77
+ const { show: showActions, always: alwaysShowActions, columnClasses, columnStacks } = useContext(RowActionsContext);
78
+ const cells = Children.toArray(children);
79
+ const stacked = cells.flatMap((child, i) => i > 0 && columnStacks[i] && isValidElement(child) ? [child.props.children] : []);
80
+ const styledCells = cells.map((child, i) => {
76
81
  const cls = columnClasses[i];
77
- if (!cls || !isValidElement(child)) return child;
82
+ if (!isValidElement(child) || !cls && !(i === 0 && stacked.length > 0)) return child;
78
83
  const el = child;
84
+ if (i === 0 && stacked.length > 0) return cloneElement(el, {
85
+ className: cx(el.props.className, cls),
86
+ children: /* @__PURE__ */ jsxs(Fragment$1, { children: [el.props.children, stacked.map((content, j) => /* @__PURE__ */ jsx("div", {
87
+ className: "mt-1 @[36rem]:hidden",
88
+ "data-stacked": true,
89
+ children: content
90
+ }, j))] })
91
+ });
79
92
  return cloneElement(el, { className: cx(el.props.className, cls) });
80
93
  });
81
94
  const handleRowClick = (e) => {
@@ -18,13 +18,16 @@ export declare const listTableFixedProps: {
18
18
  readonly tbodyClassName: 'divide-y border-b border-(--theme-border) divide-(--theme-border)';
19
19
  readonly className: string;
20
20
  };
21
- export declare function NameCell({ icon, name, subtitle, href, badge, title, }: {
21
+ export declare function NameCell({ icon, name, subtitle, href, badge, title, children, }: {
22
22
  icon: ReactNode;
23
23
  name: string;
24
24
  subtitle?: string | undefined;
25
25
  href: string | null;
26
26
  badge?: ReactNode;
27
27
  title?: string | undefined;
28
+ /** More under the name, outside its link: where a stacked column shows on a
29
+ * narrow container (see `ColumnDef.stack`). */
30
+ children?: ReactNode;
28
31
  }): import("react").JSX.Element;
29
32
  export declare function GroupRow({ label, count, colSpan, }: {
30
33
  label: string;
@@ -22,7 +22,7 @@ var listTableFixedProps = {
22
22
  ...listTableProps,
23
23
  className: listTableProps.className.replace("table-auto", "table-fixed")
24
24
  };
25
- function NameCell({ icon, name, subtitle, href, badge, title }) {
25
+ function NameCell({ icon, name, subtitle, href, badge, title, children }) {
26
26
  const Link = useLink();
27
27
  const inner = /* @__PURE__ */ jsxs(Fragment, { children: [icon, /* @__PURE__ */ jsxs("div", {
28
28
  className: "min-w-0",
@@ -38,16 +38,19 @@ function NameCell({ icon, name, subtitle, href, badge, title }) {
38
38
  children: subtitle
39
39
  })]
40
40
  })] });
41
- return /* @__PURE__ */ jsx(Table.Item, {
41
+ return /* @__PURE__ */ jsxs(Table.Item, {
42
42
  className: "py-2.5",
43
- children: href ? /* @__PURE__ */ jsx(Link, {
43
+ children: [href ? /* @__PURE__ */ jsx(Link, {
44
44
  to: href,
45
45
  className: "group/namecell flex items-center gap-2.5 min-w-0 cursor-pointer",
46
46
  children: inner
47
47
  }) : /* @__PURE__ */ jsx("div", {
48
48
  className: "flex items-center gap-2.5 min-w-0",
49
49
  children: inner
50
- })
50
+ }), children && /* @__PURE__ */ jsx("div", {
51
+ className: "pl-[1.875rem]",
52
+ children
53
+ })]
51
54
  });
52
55
  }
53
56
  function GroupRow({ label, count, colSpan }) {