@parallelworks/ui 0.67.0 → 0.69.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.
package/README.md CHANGED
@@ -691,8 +691,32 @@ glyph per meaning across the apps.
691
691
  parallel one, while `NetworkShareIcon` is a folder shared over the network.
692
692
  `LibraryIcon` is a collection of write-ups to browse, `ArticleIcon` one
693
693
  write-up to read, such as a case study, and `NotebookIcon` one notebook.
694
- `PendingIcon` is a requirement not met yet and `NotRunningIcon` a run not
695
- started; they share the empty circle, so keep them out of the same list.
694
+ `PendingIcon` (a dashed circle) is a requirement not met yet and
695
+ `NotRunningIcon` (an empty circle) a run not started.
696
+ `ProvisionIcon` brings infrastructure up and `DeprovisionIcon` takes it down
697
+ but keeps its definition, while `DestroyIcon` breaks it apart for good.
698
+ `SharedIcon` is infrastructure someone else shared with you.
699
+ `WorkerIcon` is a machine that takes jobs, `WorkloadIcon` copies of one
700
+ thing running in a cluster, and `ServiceIcon` a long-running unit others
701
+ connect to. `WorkflowIcon` is a workflow as a thing, `RunWorkflowIcon`
702
+ running one and `BuildWorkflowIcon` editing one; `RunsIcon` lists runs and
703
+ `JobIcon` is one submitted job.
704
+ `RestartIcon` stops and starts something running, `RetryIcon` tries a failed
705
+ thing again, `RefreshIcon` reloads data, `ResetIcon` returns settings to
706
+ their defaults, `RollbackIcon` goes back to an earlier version and
707
+ `RestoreIcon` brings an archived item back. `LoadingIcon` is the interface
708
+ busy, while `RunningIcon` is a run in progress.
709
+ `KeyIcon` is an SSH or API key, `TokenIcon` a short-lived token and
710
+ `PasswordIcon` a password; `RevokeIcon` withdraws a credential or grant,
711
+ while `ReleaseIcon` gives back a reserved address or name.
712
+ `LogsIcon` is a live stream of log lines, `DefinitionIcon` the file that
713
+ defines a resource, `ConfigIcon` key-value configuration, `VariableIcon` a
714
+ named value and `ReportIcon` a report to open.
715
+ `WorkspaceMountIcon` is storage mounted into a workspace and
716
+ `CloudNetworkIcon` the private network hosts attach to.
717
+ `NamespaceIcon`, `CommandShellIcon` and `QuickActionIcon` were
718
+ `KubernetesNamespaceIcon`, `PowershellIcon` and `ZapIcon`; the old names
719
+ still work but are deprecated.
696
720
  - **Adding an icon:** draw it as one path on the 16px grid and add
697
721
  `export const NameIcon = /* @__PURE__ */ drawn('M…')` to `src/icons.tsx`,
698
722
  named for what it means rather than what it shows. Reuse the existing parts
@@ -834,6 +858,18 @@ The chat's links (each conversation, a new chat, attachments, and the
834
858
  these links, so `navigation` handles only the moves that aren't clicks, such as
835
859
  keyboard shortcuts.
836
860
 
861
+ `ChatThread` is `SessionView` bound to the chat provider. A host with its own
862
+ transcript, such as an agent session fed by an event stream, renders
863
+ `SessionView` with its messages, streaming and queued messages, and callbacks:
864
+ `onSend`, and `turn` (`running`, `onStop`) for a turn the provider doesn't run.
865
+ What it leaves out it doesn't offer: `onEdit`, `onRegenerate`, `onRetry` and
866
+ `onNavigateBranch` turn on editing, regenerating, retrying and branches.
867
+ Escape stops the running turn either way. `queueDelivery` says when a queued
868
+ message reaches the model, per message when it takes a function. `composer`
869
+ passes `ChatInput`'s props; `ComposerControls` takes `model` and `allocation`
870
+ as a `BoundSetting` for a session that owns its own, and `tools={false}` keeps
871
+ the chat's tools out of a composer they don't apply to.
872
+
837
873
  `ChatLayout sidebarMode="auto"` shows the conversation list as a drawer over
838
874
  the thread when the chat is narrower than `drawerBelowPx` (`DRAWER_BELOW_PX`, 896, by default),
839
875
  measured on the chat's own box; `"drawer"` always does, and `"inline"` (the
@@ -1,3 +1,9 @@
1
+ import type { BoundSetting } from '../types.js';
1
2
  /** Which allocation an organization model bills, with its spend so far. Shows
2
3
  * only for an organization model, and picks the first allocation for it. */
3
- export default function AllocationSelector(): import("react").JSX.Element | null;
4
+ export default function AllocationSelector({ model, allocation, }?: {
5
+ /** The model a session owns, which decides whether it bills an allocation. */
6
+ model?: string | null | undefined;
7
+ /** The allocation a session owns, in place of the chat's own selection. */
8
+ allocation?: BoundSetting | undefined;
9
+ }): import("react").JSX.Element | null;
@@ -15,22 +15,28 @@ var dollars = new Intl.NumberFormat(void 0, {
15
15
  var money = (amount) => dollars.format(amount);
16
16
  /** Which allocation an organization model bills, with its spend so far. Shows
17
17
  * only for an organization model, and picks the first allocation for it. */
18
- function AllocationSelector() {
19
- const { adapter, selectedProvider, selectedAllocation, setSelectedAllocation } = useChat();
18
+ function AllocationSelector({ model, allocation } = {}) {
19
+ const chat = useChat();
20
20
  const t = useChatConfig().strings.allocationPicker;
21
+ const { adapter } = chat;
22
+ const bound = allocation !== void 0;
23
+ const selectedProvider = bound ? model ?? null : chat.selectedProvider;
24
+ const selectedAllocation = bound ? allocation.value ?? null : chat.selectedAllocation;
21
25
  const isOrgProvider = isOrgModel(selectedProvider);
22
26
  const allocationsAdapter = adapter.allocations;
23
27
  const { data: allocations } = useSWR(isOrgProvider && allocationsAdapter ? ["ai-chat-allocations"] : null, () => allocationsAdapter?.list());
24
28
  const first = allocations?.[0];
25
29
  useEffect(() => {
30
+ if (bound) return;
26
31
  if (!isOrgProvider) {
27
- if (selectedAllocation) setSelectedAllocation(null);
28
- } else if (first && !selectedAllocation) setSelectedAllocation(first.name);
32
+ if (selectedAllocation) chat.setSelectedAllocation(null);
33
+ } else if (first && !selectedAllocation) chat.setSelectedAllocation(first.name);
29
34
  }, [
35
+ bound,
30
36
  isOrgProvider,
31
37
  first,
32
38
  selectedAllocation,
33
- setSelectedAllocation
39
+ chat.setSelectedAllocation
34
40
  ]);
35
41
  if (!isOrgProvider || !allocations?.length) return null;
36
42
  const selected = allocations.find((a) => a.name === selectedAllocation);
@@ -43,7 +49,8 @@ function AllocationSelector() {
43
49
  label: a.name
44
50
  })),
45
51
  value: selectedAllocation,
46
- onValueChange: setSelectedAllocation,
52
+ disabled: !!allocation?.disabled,
53
+ onValueChange: (name) => bound ? name && allocation.onChange(name) : chat.setSelectedAllocation(name),
47
54
  trigger: /* @__PURE__ */ jsxs("button", {
48
55
  type: "button",
49
56
  className: chipClassName,
@@ -42,6 +42,9 @@ interface ChatInputProps {
42
42
  onStop: () => void;
43
43
  stopping?: boolean;
44
44
  };
45
+ /** False where the chat's tools do not apply, such as starting an agent
46
+ * session, which brings its own. */
47
+ tools?: boolean;
45
48
  pastes?: ComposerPastes | undefined;
46
49
  ref?: Ref<ChatInputHandle> | undefined;
47
50
  }
@@ -49,5 +52,5 @@ export interface ChatInputHandle {
49
52
  addFiles: (files: FileList | File[]) => void;
50
53
  focus: () => void;
51
54
  }
52
- export default function ChatInput({ onSend, disabled, placeholder, conversationId, onEditLastMessage, allowEmpty, attachments: attachmentsWanted, requireModel, inputLabel, sendLabel, sendTestId, slash, notices, activity, context, reserveContext, settingsLeft, settingsRight, hint, turn, pastes, ref, }: ChatInputProps): import("react").JSX.Element;
55
+ export default function ChatInput({ onSend, disabled, placeholder, conversationId, onEditLastMessage, allowEmpty, attachments: attachmentsWanted, requireModel, inputLabel, sendLabel, sendTestId, slash, notices, activity, context, reserveContext, settingsLeft, settingsRight, hint, turn, tools: toolsAllowed, pastes, ref, }: ChatInputProps): import("react").JSX.Element;
53
56
  export {};
@@ -58,7 +58,7 @@ function fitToContent(el) {
58
58
  el.style.height = `${el.scrollHeight}px`;
59
59
  }
60
60
  var draftsSwept = false;
61
- function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEditLastMessage, allowEmpty = false, attachments: attachmentsWanted = true, requireModel = true, inputLabel, sendLabel, sendTestId, slash, notices, activity, context, reserveContext = false, settingsLeft, settingsRight = /* @__PURE__ */ jsx(ComposerControls, {}), hint, turn, pastes, ref }) {
61
+ function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEditLastMessage, allowEmpty = false, attachments: attachmentsWanted = true, requireModel = true, inputLabel, sendLabel, sendTestId, slash, notices, activity, context, reserveContext = false, settingsLeft, settingsRight = /* @__PURE__ */ jsx(ComposerControls, {}), hint, turn, tools: toolsAllowed = true, pastes, ref }) {
62
62
  const [input, setInput] = useState(() => readDraft(conversationId));
63
63
  useEffect(() => {
64
64
  if (!draftsSwept) {
@@ -186,7 +186,7 @@ function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEd
186
186
  if (pastes && handleTextPaste(e.clipboardData.getData("text/plain"))) e.preventDefault();
187
187
  };
188
188
  const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped: handleFilesDropped });
189
- const toolsAvailable = !turn && tools.length > 0;
189
+ const toolsAvailable = toolsAllowed && !turn && tools.length > 0;
190
190
  const toolbar = attachmentsAvailable || toolsAvailable || !!settingsRight;
191
191
  const action = isStreaming && !input.trim() ? /* @__PURE__ */ jsx("button", {
192
192
  type: "button",
@@ -275,7 +275,7 @@ function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEd
275
275
  attachmentsOpen: showAttachments && attachmentsAvailable,
276
276
  onToggleAttachments: attachmentsAvailable ? () => setShowAttachments(!showAttachments) : void 0,
277
277
  disabled,
278
- withTools: !turn
278
+ withTools: toolsAllowed && !turn
279
279
  }), /* @__PURE__ */ jsxs("div", {
280
280
  className: "ml-auto flex min-w-0 items-center gap-1",
281
281
  children: [/* @__PURE__ */ jsx("div", {
@@ -85,19 +85,57 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
85
85
  const maxHeight = 200;
86
86
  e.target.style.height = `${Math.min(e.target.scrollHeight, maxHeight)}px`;
87
87
  };
88
- const hasParts = !!message.parts?.length;
89
- if (hasParts && !message.content) return /* @__PURE__ */ jsx("div", {
90
- className: cx("group py-3 w-full", !flush && "px-4"),
91
- children: /* @__PURE__ */ jsx(AgentMessageParts, {
92
- message,
93
- isStreaming,
94
- onApprovalAnswer
95
- })
88
+ const timestamp = parseTimestamp(message.timestamp);
89
+ const totalTokens = message.tokensUsed?.total_tokens;
90
+ const meta = (timestamp || !isUser && (message.model || totalTokens)) && /* @__PURE__ */ jsxs("span", {
91
+ className: cx("chat-meta flex items-center gap-2 ml-2 text-xs tabular-nums theme-muted-text transition-opacity duration-150 select-none", "opacity-0 group-focus-within:opacity-100 group-hover:opacity-100"),
92
+ children: [
93
+ /* @__PURE__ */ jsx(RelativeTime, { value: timestamp }),
94
+ !isUser && message.model && /* @__PURE__ */ jsx("span", { children: message.model }),
95
+ !isUser && totalTokens ? /* @__PURE__ */ jsx("span", {
96
+ title: message.tokensUsed?.prompt_tokens !== void 0 && message.tokensUsed?.completion_tokens !== void 0 ? t.tokensBreakdown(message.tokensUsed.prompt_tokens, message.tokensUsed.completion_tokens) : void 0,
97
+ children: t.tokens(totalTokens)
98
+ }) : null
99
+ ]
96
100
  });
101
+ const hasParts = !!message.parts?.length;
102
+ if (hasParts && !message.content) {
103
+ const said = message.parts?.flatMap((p) => p.kind === "text" && p.text.trim() ? [p.text.trim()] : []).join("\n\n");
104
+ return /* @__PURE__ */ jsxs("div", {
105
+ className: cx("group py-3 w-full", !flush && "px-4"),
106
+ children: [/* @__PURE__ */ jsx(AgentMessageParts, {
107
+ message,
108
+ isStreaming,
109
+ onApprovalAnswer
110
+ }), !isUser && !isStreaming && (said || onRegenerate || meta) && /* @__PURE__ */ jsxs("div", {
111
+ className: "chat-actions -ml-1.5 mt-2 flex items-center gap-0.5",
112
+ children: [
113
+ said && /* @__PURE__ */ jsx(CopyButton, {
114
+ text: said,
115
+ label: t.copyLabel,
116
+ quiet: true,
117
+ size: "md",
118
+ className: actionClasses
119
+ }),
120
+ onRegenerate && /* @__PURE__ */ jsx(IconButton, {
121
+ label: t.regenerate,
122
+ onClick: () => onRegenerate(message.id),
123
+ size: "md",
124
+ className: actionClasses,
125
+ icon: /* @__PURE__ */ jsx(RetryIcon, {})
126
+ }),
127
+ onNavigateBranch && /* @__PURE__ */ jsx(BranchNavigator, {
128
+ messages: allMessages,
129
+ currentMessageId: message.id,
130
+ onNavigate: onNavigateBranch
131
+ }),
132
+ meta
133
+ ]
134
+ })]
135
+ });
136
+ }
97
137
  if (isTool) return null;
98
138
  if (!isUser && !message.content && message.toolCalls?.length) return null;
99
- const timestamp = parseTimestamp(message.timestamp);
100
- const totalTokens = message.tokensUsed?.total_tokens;
101
139
  const collapsed = !isUser && collapsible && !expanded && !isStreaming && !latest;
102
140
  return /* @__PURE__ */ jsx("div", {
103
141
  role: "none",
@@ -246,17 +284,7 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
246
284
  currentMessageId: message.id,
247
285
  onNavigate: onNavigateBranch
248
286
  }),
249
- (timestamp || !isUser && (message.model || totalTokens)) && /* @__PURE__ */ jsxs("span", {
250
- className: cx("chat-meta flex items-center gap-2 ml-2 text-xs tabular-nums theme-muted-text transition-opacity duration-150 select-none", "opacity-0 group-focus-within:opacity-100 group-hover:opacity-100"),
251
- children: [
252
- /* @__PURE__ */ jsx(RelativeTime, { value: timestamp }),
253
- !isUser && message.model && /* @__PURE__ */ jsx("span", { children: message.model }),
254
- !isUser && totalTokens ? /* @__PURE__ */ jsx("span", {
255
- title: message.tokensUsed?.prompt_tokens !== void 0 && message.tokensUsed?.completion_tokens !== void 0 ? t.tokensBreakdown(message.tokensUsed.prompt_tokens, message.tokensUsed.completion_tokens) : void 0,
256
- children: t.tokens(totalTokens)
257
- }) : null
258
- ]
259
- })
287
+ meta
260
288
  ]
261
289
  })
262
290
  ]
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { ApprovalAnswerValue, ChatMessage as Message } from '../types.js';
3
+ export type QueueDelivery = 'afterReply' | 'nextStep';
3
4
  interface ChatMessageListProps {
4
5
  messages: Message[];
5
6
  allMessages: Message[];
@@ -13,8 +14,9 @@ interface ChatMessageListProps {
13
14
  canRemoveQueued?: (id: string) => boolean;
14
15
  /** When a queued message reaches the model: once the running reply
15
16
  * finishes (the chat provider's queue), or between the agent's steps for a
16
- * host that steers the turn in progress. */
17
- queueDelivery?: 'afterReply' | 'nextStep';
17
+ * host that steers the turn in progress. A host with both kinds says which
18
+ * each message is. */
19
+ queueDelivery?: QueueDelivery | ((messageId: string) => QueueDelivery);
18
20
  editingMessageId?: string | null | undefined;
19
21
  showAuthor?: boolean;
20
22
  currentUsername?: string;
@@ -253,7 +253,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
253
253
  }, "streaming"),
254
254
  queuedMessages?.map((msg) => {
255
255
  const removable = canRemoveQueued?.(msg.id) ?? true;
256
- const status = !removable ? strings.queue.handedOver : queueDelivery === "nextStep" ? strings.queue.waitingNextStep : strings.queue.waiting;
256
+ const status = !removable ? strings.queue.handedOver : (typeof queueDelivery === "function" ? queueDelivery(msg.id) : queueDelivery) === "nextStep" ? strings.queue.waitingNextStep : strings.queue.waiting;
257
257
  return /* @__PURE__ */ jsxs("div", {
258
258
  className: cx("animate-chat-enter", removable && "chat-queued"),
259
259
  children: [/* @__PURE__ */ jsx(ChatMessage_default, {
@@ -1,19 +1,14 @@
1
1
  import { useChatConfig } from "../core/config.js";
2
2
  import { useChat } from "../core/ChatProvider.js";
3
3
  import BlockedGroupBanner from "./BlockedGroupBanner.js";
4
- import useDragDrop from "../core/useDragDrop.js";
5
4
  import { ComposerControls, ComposerUsage, ConnectToolsLink } from "./ComposerChrome.js";
6
- import DragOverlay from "./DragOverlay.js";
7
- import ChatInput from "./ChatInput.js";
8
5
  import ProviderIssueBanner from "./ProviderIssueBanner.js";
9
6
  import { ShareButton } from "./ShareDialog.js";
10
- import ChatMessageList from "./ChatMessageList.js";
7
+ import SessionView from "./SessionView.js";
11
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
+ import { useCallback, useEffect, useMemo, useRef } from "react";
13
10
  //#region src/ai/components/ChatThread.tsx
14
11
  function ChatThread({ conversationId }) {
15
- const chatInputRef = useRef(null);
16
- const [editingState, setEditingState] = useState(null);
17
12
  const { adapter, currentUser, currentConversation, loadConversation, loadProviders, sendMessage, retryMessage, isLoadingConversation, navigateToBranch, queuedMessages, removeQueuedMessage } = useChat();
18
13
  const { slots, strings } = useChatConfig();
19
14
  const requestedConversationIdRef = useRef(null);
@@ -28,98 +23,46 @@ function ChatThread({ conversationId }) {
28
23
  loadConversation,
29
24
  loadProviders
30
25
  ]);
31
- const handleSendMessage = useCallback((content, attachmentIds, attachmentMeta) => {
26
+ const messages = currentConversation?.messages ?? [];
27
+ const canWrite = currentConversation?.canCollaborate !== false;
28
+ const onSend = useCallback((content, attachmentIds, attachmentMeta) => {
32
29
  sendMessage(content, attachmentIds, void 0, attachmentMeta);
33
30
  }, [sendMessage]);
34
- const handleEditMessage = useCallback((messageId) => {
35
- const message = (currentConversation?.messages || []).find((m) => m.id === messageId);
36
- if (message && message.role === "user") setEditingState({
37
- messageId: message.id,
38
- parentId: message.parentId || null
39
- });
40
- }, [currentConversation?.messages]);
41
- const handleSaveEdit = useCallback((_messageId, content) => {
42
- if (!editingState) return;
43
- sendMessage(content, void 0, editingState.parentId || void 0);
44
- setEditingState(null);
45
- }, [sendMessage, editingState]);
46
- const handleCancelEdit = useCallback(() => {
47
- setEditingState(null);
48
- }, []);
49
- const handleEditLastMessage = useCallback(() => {
50
- const lastUserMessage = (currentConversation?.messages || []).findLast((m) => m.role === "user");
51
- if (lastUserMessage) handleEditMessage(lastUserMessage.id);
52
- }, [currentConversation?.messages, handleEditMessage]);
53
- const handleNavigateBranch = useCallback((messageId) => {
54
- navigateToBranch?.(messageId);
55
- }, [navigateToBranch]);
56
- const handleRegenerateMessage = useCallback((messageId) => {
57
- const conversationMessages = currentConversation?.messages || [];
58
- const assistantMessage = conversationMessages.find((m) => m.id === messageId);
59
- if (assistantMessage?.role !== "assistant") return;
60
- const userMessage = conversationMessages.find((m) => m.id === assistantMessage.parentId);
61
- if (userMessage?.role !== "user") return;
62
- sendMessage(userMessage.content || "", userMessage.attachments?.map((a) => a.id), userMessage.parentId || void 0);
63
- }, [currentConversation?.messages, sendMessage]);
64
- const handleRetryMessage = useCallback((messageId) => {
65
- retryMessage(messageId);
66
- }, [retryMessage]);
67
- const handleFilesDropped = useCallback((files) => {
68
- chatInputRef.current?.addFiles(files);
69
- }, []);
70
- const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped: handleFilesDropped });
71
- const attachmentsAvailable = !!adapter.attachments;
72
- const messages = currentConversation?.messages || [];
31
+ const onEdit = useCallback((message, content) => {
32
+ sendMessage(content, void 0, message.parentId || void 0);
33
+ }, [sendMessage]);
34
+ const onRegenerate = useCallback((messageId) => {
35
+ const reply = messages.find((m) => m.id === messageId);
36
+ const asked = messages.find((m) => m.id === reply?.parentId);
37
+ if (reply?.role !== "assistant" || asked?.role !== "user") return;
38
+ sendMessage(asked.content || "", asked.attachments?.map((a) => a.id), asked.parentId || void 0);
39
+ }, [messages, sendMessage]);
73
40
  const showAuthor = useMemo(() => {
74
41
  return new Set(messages.flatMap((m) => m.author ? [m.author.username] : [])).size > 1 || currentConversation?.isOwner === false;
75
42
  }, [messages, currentConversation?.isOwner]);
76
- if (isLoadingConversation && !currentConversation) return /* @__PURE__ */ jsxs("div", {
77
- className: "flex flex-col h-full bg-(--theme-panel-bg)",
78
- children: [/* @__PURE__ */ jsx("div", { className: "flex-1" }), /* @__PURE__ */ jsx(ChatInput, {
79
- ref: chatInputRef,
80
- onSend: handleSendMessage,
81
- disabled: true,
82
- conversationId,
83
- settingsLeft: /* @__PURE__ */ jsx(ConnectToolsLink, {}),
43
+ return /* @__PURE__ */ jsx(SessionView, {
44
+ sessionKey: conversationId,
45
+ messages,
46
+ queuedMessages,
47
+ onRemoveQueued: removeQueuedMessage,
48
+ onSend,
49
+ ...canWrite ? {
50
+ onEdit,
51
+ onRegenerate,
52
+ onRetry: (id) => void retryMessage(id)
53
+ } : {},
54
+ onNavigateBranch: navigateToBranch,
55
+ showAuthor,
56
+ currentUsername: currentUser.username,
57
+ loading: isLoadingConversation && !currentConversation,
58
+ dropFiles: !!adapter.attachments,
59
+ banners: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(BlockedGroupBanner, {}), /* @__PURE__ */ jsx(ProviderIssueBanner, {})] }),
60
+ composer: {
61
+ disabled: isLoadingConversation || !canWrite,
62
+ ...canWrite ? {} : { placeholder: strings.thread.viewOnly },
63
+ settingsLeft: /* @__PURE__ */ jsxs(Fragment, { children: [currentConversation && currentConversation.isOwner !== false && /* @__PURE__ */ jsx(ShareButton, { conversationId }), /* @__PURE__ */ jsx(ConnectToolsLink, {})] }),
84
64
  settingsRight: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ComposerUsage, { children: slots.composerUsage }), /* @__PURE__ */ jsx(ComposerControls, {})] })
85
- })]
86
- });
87
- return /* @__PURE__ */ jsx("div", {
88
- className: "flex h-full relative bg-(--theme-panel-bg)",
89
- children: /* @__PURE__ */ jsxs("div", {
90
- className: "flex-1 flex flex-col min-w-0",
91
- ...attachmentsAvailable ? dragHandlers : {},
92
- children: [
93
- isDragging && attachmentsAvailable && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-lg" }),
94
- /* @__PURE__ */ jsx(BlockedGroupBanner, {}),
95
- /* @__PURE__ */ jsx(ProviderIssueBanner, {}),
96
- /* @__PURE__ */ jsx(ChatMessageList, {
97
- messages,
98
- allMessages: messages,
99
- queuedMessages,
100
- onRemoveQueued: removeQueuedMessage,
101
- editingMessageId: editingState?.messageId,
102
- showAuthor,
103
- currentUsername: currentUser.username,
104
- onNavigateBranch: handleNavigateBranch,
105
- onEditMessage: currentConversation?.canCollaborate !== false ? handleEditMessage : void 0,
106
- onRegenerateMessage: currentConversation?.canCollaborate !== false ? handleRegenerateMessage : void 0,
107
- onRetryMessage: currentConversation?.canCollaborate !== false ? handleRetryMessage : void 0,
108
- onSaveEdit: handleSaveEdit,
109
- onCancelEdit: handleCancelEdit,
110
- inputElement: /* @__PURE__ */ jsx(ChatInput, {
111
- ref: chatInputRef,
112
- onSend: handleSendMessage,
113
- disabled: isLoadingConversation || currentConversation?.canCollaborate === false,
114
- placeholder: currentConversation?.canCollaborate === false ? strings.thread.viewOnly : void 0,
115
- conversationId,
116
- onEditLastMessage: handleEditLastMessage,
117
- settingsLeft: /* @__PURE__ */ jsxs(Fragment, { children: [currentConversation?.isOwner !== false && /* @__PURE__ */ jsx(ShareButton, { conversationId }), /* @__PURE__ */ jsx(ConnectToolsLink, {})] }),
118
- settingsRight: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ComposerUsage, { children: slots.composerUsage }), /* @__PURE__ */ jsx(ComposerControls, {})] })
119
- })
120
- })
121
- ]
122
- })
65
+ }
123
66
  });
124
67
  }
125
68
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { BoundSetting } from '../types.js';
2
3
  /** Where the turn runs, sitting above the composer the way the terminal shows
3
4
  * it: the machine, the workspace, and whatever else names the place. */
4
5
  export declare function ComposerContext({ children }: {
@@ -19,8 +20,12 @@ export declare function ComposerUsage({ children }: {
19
20
  }): import("react").JSX.Element | null;
20
21
  /** The two selectors every surface carries, reading as text beside whatever
21
22
  * the surface puts next to them. */
22
- export declare function ComposerControls({ targetSession }: {
23
+ export declare function ComposerControls({ targetSession, model, allocation, }: {
23
24
  targetSession?: string | null | undefined;
25
+ /** A session's own model and allocation, for a session that keeps them
26
+ * apart from the chat's selection. */
27
+ model?: BoundSetting | undefined;
28
+ allocation?: BoundSetting | undefined;
24
29
  }): import("react").JSX.Element;
25
30
  /** Where the reader manages the providers behind the model picker, at the
26
31
  * foot of a conversation list. */
@@ -40,8 +40,14 @@ function ComposerUsage({ children }) {
40
40
  }
41
41
  /** The two selectors every surface carries, reading as text beside whatever
42
42
  * the surface puts next to them. */
43
- function ComposerControls({ targetSession }) {
44
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ModelSelector, { targetSession }), /* @__PURE__ */ jsx(AllocationSelector, {})] });
43
+ function ComposerControls({ targetSession, model, allocation }) {
44
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ModelSelector, {
45
+ targetSession,
46
+ model
47
+ }), /* @__PURE__ */ jsx(AllocationSelector, { ...allocation ? {
48
+ allocation,
49
+ model: model ? model.value : void 0
50
+ } : {} })] });
45
51
  }
46
52
  /** Where the reader manages the providers behind the model picker, at the
47
53
  * foot of a conversation list. */
@@ -1,8 +1,11 @@
1
+ import type { BoundSetting } from '../types.js';
1
2
  export declare function getProviderKeyFromModelId(modelId: string): string;
2
3
  /** Picks the model a conversation runs on, grouped by provider. Sessions that
3
4
  * are still connecting or not responding show as headings with their status,
4
5
  * so the reader sees why their models are missing. */
5
- export default function ModelSelector({ targetSession, }: {
6
+ export default function ModelSelector({ targetSession, model, }: {
6
7
  /** A session, as "owner:name", whose models the reader is waiting for. */
7
8
  targetSession?: string | null | undefined;
9
+ /** The model a session owns, in place of the chat's own selection. */
10
+ model?: BoundSetting | undefined;
8
11
  }): import("react").JSX.Element;
@@ -50,11 +50,12 @@ function groupModelsByProvider(models) {
50
50
  /** Picks the model a conversation runs on, grouped by provider. Sessions that
51
51
  * are still connecting or not responding show as headings with their status,
52
52
  * so the reader sees why their models are missing. */
53
- function ModelSelector({ targetSession }) {
54
- const { providers, models, unreachableSessions, providerIssues, selectedProvider, setSelectedProvider, isLoadingModels, hasLoadedModels, modelsError, refreshModels, currentUser } = useChat();
53
+ function ModelSelector({ targetSession, model }) {
54
+ const { providers, models, unreachableSessions, providerIssues, selectedProvider: chatSelection, setSelectedProvider, isLoadingModels, hasLoadedModels, modelsError, refreshModels, currentUser } = useChat();
55
55
  const { strings } = useChatConfig();
56
56
  const { common } = useStrings();
57
57
  const t = strings.modelPicker;
58
+ const selectedProvider = model ? model.value ?? null : chatSelection;
58
59
  const targetSessionFound = !!targetSession && models.some((m) => m.id.startsWith(`session:${targetSession}/`));
59
60
  const targetSessionStatus = !targetSession ? null : isLoadingModels ? "connecting" : hasLoadedModels && !targetSessionFound ? "offline" : null;
60
61
  const groups = useMemo(() => {
@@ -128,8 +129,8 @@ function ModelSelector({ targetSession }) {
128
129
  placeholder: t.search,
129
130
  items: groups,
130
131
  value: selectedProvider,
131
- onValueChange: (id) => id && setSelectedProvider(id),
132
- disabled: groups.length === 0,
132
+ onValueChange: (id) => id && (model ? model.onChange(id) : setSelectedProvider(id)),
133
+ disabled: groups.length === 0 || !!model?.disabled,
133
134
  triggerTooltip: issue ? `${issueLabel(strings.providerIssue, issue)} \u2014 ${issueDetail(strings.providerIssue, issue)}` : void 0,
134
135
  trigger: /* @__PURE__ */ jsxs("button", {
135
136
  type: "button",
@@ -0,0 +1,56 @@
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ import type { ApprovalAnswerValue, AttachmentMeta, ChatMessage as Message } from '../types.js';
3
+ import ChatInput from './ChatInput.js';
4
+ import ChatMessageList from './ChatMessageList.js';
5
+ type ComposerProps = Omit<ComponentProps<typeof ChatInput>, 'ref' | 'onSend' | 'turn' | 'conversationId' | 'onEditLastMessage'>;
6
+ type ListProps = ComponentProps<typeof ChatMessageList>;
7
+ export interface SessionViewProps {
8
+ /** Keys the composer's draft, so each conversation or session keeps its own. */
9
+ sessionKey: string;
10
+ messages: Message[];
11
+ /** Every message branch navigation and turn attribution look through;
12
+ * defaults to `messages`. */
13
+ allMessages?: Message[];
14
+ streamingMessage?: Message | null;
15
+ queuedMessages?: Message[];
16
+ onRemoveQueued?: (id: string) => void;
17
+ canRemoveQueued?: (id: string) => boolean;
18
+ queueDelivery?: ListProps['queueDelivery'];
19
+ /** A turn the chat provider does not run, such as an agent session's: it
20
+ * drives the composer's Stop and Escape in place of the provider's stream. */
21
+ turn?: {
22
+ running: boolean;
23
+ onStop: () => void;
24
+ stopping?: boolean;
25
+ };
26
+ onSend: (content: string, attachmentIds?: string[], attachmentMeta?: AttachmentMeta) => void;
27
+ /** Resends an edited user message as a new branch. Without it, messages
28
+ * cannot be edited. */
29
+ onEdit?: (message: Message, content: string) => void;
30
+ onRegenerate?: (messageId: string) => void;
31
+ onRetry?: (messageId: string) => void;
32
+ onNavigateBranch?: (messageId: string) => void;
33
+ onApprovalAnswer?: (id: string, answer: ApprovalAnswerValue) => void;
34
+ showAuthor?: boolean;
35
+ currentUsername?: string;
36
+ /** Shown above the transcript, such as a blocked allocation or an
37
+ * unreachable machine. */
38
+ banners?: ReactNode;
39
+ /** In place of the transcript, such as a failed load with a way to retry. */
40
+ error?: ReactNode;
41
+ /** Still loading the first messages: the transcript stays empty and the
42
+ * composer disabled. */
43
+ loading?: boolean;
44
+ /** Files dropped anywhere on the view go to the composer's attachments. */
45
+ dropFiles?: boolean;
46
+ footer?: ReactNode;
47
+ label?: string;
48
+ flush?: boolean;
49
+ workingIndicator?: boolean;
50
+ composer?: ComposerProps;
51
+ }
52
+ /** One conversation or agent session: its transcript, with the composer at
53
+ * the foot of the scrolling column. The host binds its own data and actions;
54
+ * what it leaves out (editing, regenerating, branches) the view leaves out. */
55
+ export default function SessionView({ sessionKey, messages, allMessages, streamingMessage, queuedMessages, onRemoveQueued, canRemoveQueued, queueDelivery, turn, onSend, onEdit, onRegenerate, onRetry, onNavigateBranch, onApprovalAnswer, showAuthor, currentUsername, banners, error, loading, dropFiles, footer, label, flush, workingIndicator, composer, }: SessionViewProps): import("react").JSX.Element;
56
+ export {};