@parallelworks/ui 0.27.0 → 0.29.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.
@@ -28,6 +28,11 @@ export interface StreamHandlers {
28
28
  onPart?: (delta: PartDelta) => void;
29
29
  }
30
30
  export type PartDelta = {
31
+ type: 'text' | 'reasoning';
32
+ text: string;
33
+ } | {
34
+ type: 'retract';
35
+ } | {
31
36
  type: 'tool_start';
32
37
  id: string;
33
38
  name: string;
@@ -7,19 +7,20 @@ import { FaUsers } from "../../icons/fa/FaUsers.js";
7
7
  import { IoSettingsOutline } from "../../icons/io5/IoSettingsOutline.js";
8
8
  import { PiShareBold } from "../../icons/pi/PiShareBold.js";
9
9
  import { TbEdit } from "../../icons/tb/TbEdit.js";
10
- import { positionKeys } from "../../components/keys.js";
11
10
  import { useRowMenu } from "../../list/RowContextMenu.js";
12
11
  import { useChatConfig } from "../core/config.js";
13
12
  import { useChat } from "../core/ChatProvider.js";
14
13
  import { FOCUS_SIDEBAR_SEARCH_EVENT } from "../core/events.js";
15
14
  import { conversationToMarkdown, downloadText, exportFilename } from "../core/exportConversation.js";
16
- import { SidebarGroupHeading, SidebarPanel, SidebarRow, SidebarToggle, useRowDialogs } from "../ui/sidebar.js";
15
+ import { SidebarRow, useRowDialogs } from "../ui/sidebar.js";
16
+ import { ConversationSidebar } from "../ui/ConversationSidebar.js";
17
17
  import ShareDialog from "./ShareDialog.js";
18
18
  import cx from "classnames";
19
19
  import { useEffect, useMemo, useRef, useState } from "react";
20
20
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
21
21
  import { DateTime } from "luxon";
22
22
  //#region src/ai/components/ChatSidebar.tsx
23
+ var SIDEBAR_WIDTH_STORAGE_KEY = "aiChatSidebarWidth";
23
24
  function groupByDate(conversations) {
24
25
  const now = DateTime.now();
25
26
  const groups = {
@@ -90,64 +91,6 @@ function ConversationRow({ conv, isCurrent, sharingAvailable, openMenu, onRename
90
91
  })
91
92
  });
92
93
  }
93
- var SIDEBAR_WIDTH_STORAGE_KEY = "aiChatSidebarWidth";
94
- var SIDEBAR_MIN_WIDTH_PX = 200;
95
- var SIDEBAR_MAX_WIDTH_PX = 480;
96
- function clampSidebarWidth(width) {
97
- return Math.min(SIDEBAR_MAX_WIDTH_PX, Math.max(SIDEBAR_MIN_WIDTH_PX, width));
98
- }
99
- function readStoredSidebarWidth() {
100
- try {
101
- const stored = Number(window.localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY));
102
- return Number.isFinite(stored) && stored > 0 ? clampSidebarWidth(stored) : 256;
103
- } catch {
104
- return 256;
105
- }
106
- }
107
- function storeSidebarWidth(width) {
108
- try {
109
- window.localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(width));
110
- } catch {}
111
- }
112
- function useResizableSidebarWidth() {
113
- const [width, setWidth] = useState(readStoredSidebarWidth);
114
- const [resizing, setResizing] = useState(false);
115
- const widthRef = useRef(width);
116
- widthRef.current = width;
117
- const startResize = (e) => {
118
- e.preventDefault();
119
- const startX = e.clientX;
120
- const startWidth = widthRef.current;
121
- setResizing(true);
122
- const onMove = (ev) => {
123
- setWidth(clampSidebarWidth(startWidth + ev.clientX - startX));
124
- };
125
- const onUp = () => {
126
- window.removeEventListener("pointermove", onMove);
127
- window.removeEventListener("pointerup", onUp);
128
- setResizing(false);
129
- storeSidebarWidth(widthRef.current);
130
- };
131
- window.addEventListener("pointermove", onMove);
132
- window.addEventListener("pointerup", onUp);
133
- };
134
- const reset = () => {
135
- setWidth(256);
136
- storeSidebarWidth(256);
137
- };
138
- const nudge = (delta) => {
139
- const next = clampSidebarWidth(widthRef.current + delta);
140
- setWidth(next);
141
- storeSidebarWidth(next);
142
- };
143
- return {
144
- width,
145
- resizing,
146
- startResize,
147
- reset,
148
- nudge
149
- };
150
- }
151
94
  function ChatSidebar({ actions = true }) {
152
95
  const { extraLinks, LinkComponent, strings } = useChatConfig();
153
96
  const tSidebar = strings.sidebar;
@@ -171,7 +114,6 @@ function ChatSidebar({ actions = true }) {
171
114
  onDelete: (conv) => deleteConversation(conv.id),
172
115
  onRename: (conv, title) => updateConversationTitle(conv.id, title)
173
116
  });
174
- const { width: sidebarWidth, resizing, startResize, reset: resetSidebarWidth, nudge: nudgeSidebarWidth } = useResizableSidebarWidth();
175
117
  const [search, setSearch] = useState("");
176
118
  const searchInputRef = useRef(null);
177
119
  const sidebarCollapsedRef = useRef(sidebarCollapsed);
@@ -197,7 +139,11 @@ function ChatSidebar({ actions = true }) {
197
139
  search,
198
140
  tSidebar.untitled
199
141
  ]);
200
- const groupedConversations = useMemo(() => groupByDate(filteredConversations), [filteredConversations]);
142
+ const groups = useMemo(() => groupByDate(filteredConversations).map(([key, items]) => ({
143
+ key,
144
+ label: tSidebar[key],
145
+ items
146
+ })), [filteredConversations, tSidebar]);
201
147
  const handleExport = async (id) => {
202
148
  try {
203
149
  const conversation = await adapter.conversations.get(id);
@@ -215,125 +161,95 @@ function ChatSidebar({ actions = true }) {
215
161
  navigation.toNewChat();
216
162
  };
217
163
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
218
- /* @__PURE__ */ jsxs(SidebarPanel, {
164
+ /* @__PURE__ */ jsx(ConversationSidebar, {
219
165
  collapsed: sidebarCollapsed,
220
- width: sidebarWidth,
221
- resizing,
222
- children: [
223
- !sidebarCollapsed && /* @__PURE__ */ jsx("div", {
224
- role: "separator",
225
- tabIndex: 0,
226
- "aria-orientation": "vertical",
227
- "aria-label": "Resize conversation list",
228
- "aria-valuemin": SIDEBAR_MIN_WIDTH_PX,
229
- "aria-valuemax": SIDEBAR_MAX_WIDTH_PX,
230
- "aria-valuenow": sidebarWidth,
231
- title: "Drag to resize; double-click to reset",
232
- onPointerDown: startResize,
233
- onDoubleClick: resetSidebarWidth,
234
- onKeyDown: (e) => {
235
- if (e.key === "ArrowLeft") nudgeSidebarWidth(-16);
236
- else if (e.key === "ArrowRight") nudgeSidebarWidth(16);
237
- },
238
- className: cx("absolute inset-y-0 right-0 z-10 w-1 cursor-col-resize touch-none", resizing ? "bg-(--theme-element)" : "hover:bg-(--theme-border)")
239
- }),
240
- actions && /* @__PURE__ */ jsxs("div", {
241
- className: "flex flex-col gap-0.5 px-2 pt-2 pb-1",
242
- children: [/* @__PURE__ */ jsxs("button", {
243
- type: "button",
244
- onClick: handleNewChat,
245
- className: "flex items-center h-9 px-2 gap-3 rounded-lg font-medium transition-colors hover:chat-tint",
246
- title: tSidebar.newChat,
247
- children: [/* @__PURE__ */ jsx(TbEdit, { className: cx("flex-shrink-0", "w-4 h-4") }), /* @__PURE__ */ jsx("span", {
248
- className: cx("text-sm transition-opacity duration-200", sidebarCollapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100"),
249
- children: tSidebar.newChat
250
- })]
251
- }), attachmentsAvailable && /* @__PURE__ */ jsxs(LinkComponent, {
252
- target: { kind: "attachments" },
253
- className: "flex items-center h-9 px-2 gap-3 rounded-lg transition-colors hover:chat-tint",
254
- title: tSidebar.attachments,
255
- children: [/* @__PURE__ */ jsx(FaRegFileImage, { className: "flex-shrink-0 w-4 h-4" }), /* @__PURE__ */ jsx("span", {
256
- className: cx("text-sm transition-opacity duration-200", sidebarCollapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100"),
257
- children: tSidebar.attachments
258
- })]
166
+ onToggle: toggleSidebar,
167
+ toggleLabels: {
168
+ open: tSidebar.openSidebar,
169
+ close: tSidebar.closeSidebar
170
+ },
171
+ resize: {
172
+ storageKey: SIDEBAR_WIDTH_STORAGE_KEY,
173
+ label: tSidebar.resizeLabel,
174
+ hint: tSidebar.resizeHint
175
+ },
176
+ groups,
177
+ getKey: (conv) => conv.id,
178
+ renderRow: (conv) => /* @__PURE__ */ jsx(ConversationRow, {
179
+ conv,
180
+ isCurrent: activeConversationId === conv.id,
181
+ sharingAvailable,
182
+ openMenu,
183
+ onRename: requestRename,
184
+ onShare: handleShareClick,
185
+ onDelete: requestDelete,
186
+ onExport: handleExport
187
+ }),
188
+ loading: isLoading,
189
+ placeholder: conversations.length === 0 ? /* @__PURE__ */ jsx("p", {
190
+ className: "theme-muted-text text-sm px-2 whitespace-nowrap",
191
+ children: tSidebar.noConversations
192
+ }) : /* @__PURE__ */ jsx("p", {
193
+ className: "theme-muted-text text-sm px-2",
194
+ children: strings.chrome.noMatches
195
+ }),
196
+ cycle: {
197
+ current: filteredConversations.find((conv) => conv.id === activeConversationId),
198
+ onSelect: (conv) => navigation.toConversation(conv.id)
199
+ },
200
+ top: actions && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
201
+ className: "flex flex-col gap-0.5 px-2 pt-2 pb-1",
202
+ children: [/* @__PURE__ */ jsxs("button", {
203
+ type: "button",
204
+ onClick: handleNewChat,
205
+ className: "flex items-center h-9 px-2 gap-3 rounded-lg font-medium transition-colors hover:chat-tint",
206
+ title: tSidebar.newChat,
207
+ children: [/* @__PURE__ */ jsx(TbEdit, { className: cx("flex-shrink-0", "w-4 h-4") }), /* @__PURE__ */ jsx("span", {
208
+ className: cx("text-sm transition-opacity duration-200", sidebarCollapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100"),
209
+ children: tSidebar.newChat
210
+ })]
211
+ }), attachmentsAvailable && /* @__PURE__ */ jsxs(LinkComponent, {
212
+ target: { kind: "attachments" },
213
+ className: "flex items-center h-9 px-2 gap-3 rounded-lg transition-colors hover:chat-tint",
214
+ title: tSidebar.attachments,
215
+ children: [/* @__PURE__ */ jsx(FaRegFileImage, { className: "flex-shrink-0 w-4 h-4" }), /* @__PURE__ */ jsx("span", {
216
+ className: cx("text-sm transition-opacity duration-200", sidebarCollapsed ? "opacity-0 w-0 overflow-hidden" : "opacity-100"),
217
+ children: tSidebar.attachments
259
218
  })]
260
- }),
261
- actions && !sidebarCollapsed && conversations.length > 0 && /* @__PURE__ */ jsx("div", {
262
- className: "px-2 pb-2",
263
- children: /* @__PURE__ */ jsxs("div", {
264
- className: "relative",
265
- children: [/* @__PURE__ */ jsx(FaSearch, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 theme-muted-text pointer-events-none" }), /* @__PURE__ */ jsx("input", {
266
- ref: searchInputRef,
267
- type: "text",
268
- value: search,
269
- onChange: (e) => setSearch(e.target.value),
270
- onKeyDown: (e) => {
271
- if (e.key === "Escape" && search) {
272
- e.stopPropagation();
273
- setSearch("");
274
- }
275
- },
276
- placeholder: strings.chrome.searchPlaceholder,
277
- "aria-label": strings.chrome.searchLabel,
278
- className: "w-full h-9 pl-8 pr-2 text-sm rounded-lg border border-transparent chat-tint theme-text placeholder:theme-muted-text transition-colors focus:outline-none focus:border-(--theme-border) focus:bg-(--theme-panel-bg)"
279
- })]
280
- })
281
- }),
282
- /* @__PURE__ */ jsx("div", {
283
- className: cx("min-h-0 flex-1 overflow-y-auto px-2 pb-2 transition-opacity duration-200", sidebarCollapsed ? "opacity-0 pointer-events-none" : "opacity-100"),
284
- children: isLoading ? /* @__PURE__ */ jsx("div", {
285
- className: "space-y-3 px-2 pt-2",
286
- children: positionKeys(5, "skeleton").map((key, i) => /* @__PURE__ */ jsx("div", {
287
- className: "animate-pulse",
288
- children: /* @__PURE__ */ jsx("div", {
289
- className: "h-4 rounded theme-muted-panel",
290
- style: { width: `${70 - i * 10}%` }
291
- })
292
- }, key))
293
- }) : conversations.length === 0 ? /* @__PURE__ */ jsx("p", {
294
- className: "theme-muted-text text-sm px-2 whitespace-nowrap",
295
- children: tSidebar.noConversations
296
- }) : filteredConversations.length === 0 ? /* @__PURE__ */ jsx("p", {
297
- className: "theme-muted-text text-sm px-2",
298
- children: strings.chrome.noMatches
299
- }) : groupedConversations.map(([label, convs]) => /* @__PURE__ */ jsxs("div", {
300
- className: "mb-2",
301
- children: [/* @__PURE__ */ jsx(SidebarGroupHeading, { children: tSidebar[label] }), /* @__PURE__ */ jsx("ul", {
302
- className: "space-y-0.5",
303
- children: convs.map((conv) => /* @__PURE__ */ jsx(ConversationRow, {
304
- conv,
305
- isCurrent: activeConversationId === conv.id,
306
- sharingAvailable,
307
- openMenu,
308
- onRename: requestRename,
309
- onShare: handleShareClick,
310
- onDelete: requestDelete,
311
- onExport: handleExport
312
- }, conv.id))
313
- })]
314
- }, label))
315
- }),
316
- /* @__PURE__ */ jsxs("div", {
317
- className: cx("flex shrink-0 items-center gap-1 p-1.5", sidebarCollapsed ? "justify-center" : extraLinks.manageProviders ? "justify-between" : "justify-end"),
318
- children: [!sidebarCollapsed && extraLinks.manageProviders && /* @__PURE__ */ jsxs(LinkComponent, {
319
- target: {
320
- kind: "external",
321
- href: extraLinks.manageProviders
219
+ })]
220
+ }), !sidebarCollapsed && conversations.length > 0 && /* @__PURE__ */ jsx("div", {
221
+ className: "px-2 pb-2",
222
+ children: /* @__PURE__ */ jsxs("div", {
223
+ className: "relative",
224
+ children: [/* @__PURE__ */ jsx(FaSearch, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 theme-muted-text pointer-events-none" }), /* @__PURE__ */ jsx("input", {
225
+ ref: searchInputRef,
226
+ type: "text",
227
+ value: search,
228
+ onChange: (e) => setSearch(e.target.value),
229
+ onKeyDown: (e) => {
230
+ if (e.key === "Escape" && search) {
231
+ e.stopPropagation();
232
+ setSearch("");
233
+ }
322
234
  },
323
- className: "flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-xs theme-muted-text transition-colors hover:chat-tint hover:theme-text",
324
- title: tSidebar.manageProviders,
325
- children: [/* @__PURE__ */ jsx(IoSettingsOutline, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ jsx("span", {
326
- className: "truncate",
327
- children: tSidebar.manageProviders
328
- })]
329
- }), /* @__PURE__ */ jsx(SidebarToggle, {
330
- collapsed: sidebarCollapsed,
331
- onToggle: toggleSidebar,
332
- openLabel: tSidebar.openSidebar,
333
- closeLabel: tSidebar.closeSidebar
235
+ placeholder: strings.chrome.searchPlaceholder,
236
+ "aria-label": strings.chrome.searchLabel,
237
+ className: "w-full h-9 pl-8 pr-2 text-sm rounded-lg border border-transparent chat-tint theme-text placeholder:theme-muted-text transition-colors focus:outline-none focus:border-(--theme-border) focus:bg-(--theme-panel-bg)"
334
238
  })]
335
239
  })
336
- ]
240
+ })] }),
241
+ footer: extraLinks.manageProviders && /* @__PURE__ */ jsxs(LinkComponent, {
242
+ target: {
243
+ kind: "external",
244
+ href: extraLinks.manageProviders
245
+ },
246
+ className: "flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-xs theme-muted-text transition-colors hover:chat-tint hover:theme-text",
247
+ title: tSidebar.manageProviders,
248
+ children: [/* @__PURE__ */ jsx(IoSettingsOutline, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ jsx("span", {
249
+ className: "truncate",
250
+ children: tSidebar.manageProviders
251
+ })]
252
+ })
337
253
  }),
338
254
  contextMenu,
339
255
  dialogs,
@@ -5,6 +5,23 @@
5
5
  * rather than invented — an end without a start has nothing to render. */
6
6
  function applyPartDelta(parts, delta) {
7
7
  switch (delta.type) {
8
+ case "text":
9
+ case "reasoning": {
10
+ const last = parts.at(-1);
11
+ if (last?.kind === delta.type) return [...parts.slice(0, -1), {
12
+ ...last,
13
+ text: last.text + delta.text
14
+ }];
15
+ return [...parts, {
16
+ kind: delta.type,
17
+ text: delta.text
18
+ }];
19
+ }
20
+ case "retract": {
21
+ let end = parts.length;
22
+ while (end > 0 && isStreamedText(parts[end - 1])) end -= 1;
23
+ return end === parts.length ? parts : parts.slice(0, end);
24
+ }
8
25
  case "tool_start": return [...parts, {
9
26
  kind: "tool_call",
10
27
  id: delta.id,
@@ -70,6 +87,9 @@ function applyPartDelta(parts, delta) {
70
87
  default: return parts;
71
88
  }
72
89
  }
90
+ function isStreamedText(part) {
91
+ return part?.kind === "text" || part?.kind === "reasoning";
92
+ }
73
93
  /** A turn can end (completed or stopped) with tool calls still marked
74
94
  * running — the transport died before their tool_end arrived. A stored
75
95
  * message must not spin forever, so close them out as interrupted. */
@@ -38,7 +38,8 @@ export { default as useDragDrop } from './core/useDragDrop.js';
38
38
  export { useKeyboardShortcuts } from './core/useKeyboardShortcuts.js';
39
39
  export { type ChatStrings, defaultChatStrings } from './strings.js';
40
40
  export * from './types.js';
41
+ export { ConversationSidebar, type ConversationSidebarGroup, useResizableSidebarWidth, } from './ui/ConversationSidebar.js';
41
42
  export { type DropdownOption, default as Dropdown } from './ui/Dropdown.js';
42
- export { NoticeBar, NoticeCard, type NoticeTone } from './ui/Notice.js';
43
+ export { NoticeBar, NoticeCard, type NoticeTone, OutputCard } from './ui/Notice.js';
43
44
  export { RenameDialog, type RowDialogStrings, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs, } from './ui/sidebar.js';
44
45
  export { formatFileSize, isMac, isOrgModel } from './utils.js';
package/dist/ai/index.js CHANGED
@@ -36,6 +36,7 @@ import { FOCUS_SIDEBAR_SEARCH_EVENT } from "./core/events.js";
36
36
  import { useKeyboardShortcuts } from "./core/useKeyboardShortcuts.js";
37
37
  import { KeyboardShortcutsProvider } from "./core/KeyboardShortcutsProvider.js";
38
38
  import { RenameDialog, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs } from "./ui/sidebar.js";
39
+ import { ConversationSidebar, useResizableSidebarWidth } from "./ui/ConversationSidebar.js";
39
40
  import ShareDialog, { ShareButton } from "./components/ShareDialog.js";
40
41
  import ChatSidebar from "./components/ChatSidebar.js";
41
42
  import ChatLayout from "./components/ChatLayout.js";
@@ -43,5 +44,5 @@ import ChatMessage_default from "./components/ChatMessage.js";
43
44
  import ChatMessageList from "./components/ChatMessageList.js";
44
45
  import ChatThread from "./components/ChatThread.js";
45
46
  import ComposerFrame from "./components/ComposerFrame.js";
46
- import { NoticeBar, NoticeCard } from "./ui/Notice.js";
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 };
47
+ import { NoticeBar, NoticeCard, OutputCard } from "./ui/Notice.js";
48
+ 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, ConversationSidebar, 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, OutputCard, 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, useResizableSidebarWidth, useRowDialogs, wordEmphasisRanges };
@@ -96,6 +96,8 @@ export interface ChatStrings {
96
96
  sharedWithYou: string;
97
97
  openSidebar: string;
98
98
  closeSidebar: string;
99
+ resizeLabel: string;
100
+ resizeHint: string;
99
101
  newChat: string;
100
102
  attachments: string;
101
103
  connectTools: string;
@@ -100,6 +100,8 @@ var defaultChatStrings = {
100
100
  sharedWithYou: "Shared with you",
101
101
  openSidebar: "Open sidebar",
102
102
  closeSidebar: "Close sidebar",
103
+ resizeLabel: "Resize conversation list",
104
+ resizeHint: "Drag to resize; double-click to reset",
103
105
  newChat: "New chat",
104
106
  attachments: "Attachments",
105
107
  connectTools: "Connect Tools",
@@ -0,0 +1,58 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare const SIDEBAR_MIN_WIDTH_PX = 200;
3
+ export declare const SIDEBAR_MAX_WIDTH_PX = 480;
4
+ /** A sidebar width the reader drags, nudges or resets, kept per list under
5
+ * its own storage key. */
6
+ export declare function useResizableSidebarWidth(storageKey: string): {
7
+ width: number;
8
+ resizing: boolean;
9
+ startResize: (e: React.PointerEvent<HTMLElement>) => void;
10
+ reset: () => void;
11
+ nudge: (delta: number) => void;
12
+ };
13
+ export interface ConversationSidebarGroup<T> {
14
+ key: string;
15
+ label: ReactNode;
16
+ tone?: 'default' | 'attention';
17
+ items: T[];
18
+ }
19
+ /** A list of conversations beside the thread: headed groups of rows that
20
+ * collapse to a rail, with an optional drag handle for the width. The host
21
+ * decides what is listed, how it is grouped and how each row reads. */
22
+ export declare function ConversationSidebar<T>({ label, collapsed, onToggle, toggleLabels, resize, groups, getKey, renderRow, loading, placeholder, top, rail, footer, cycle, groupTestId, children, }: {
23
+ label?: string;
24
+ collapsed: boolean;
25
+ onToggle: () => void;
26
+ toggleLabels: {
27
+ open: string;
28
+ close: string;
29
+ };
30
+ /** Omit for a fixed width. */
31
+ resize?: {
32
+ storageKey: string;
33
+ label: string;
34
+ hint: string;
35
+ };
36
+ groups: ConversationSidebarGroup<T>[];
37
+ getKey: (item: T) => string;
38
+ /** One `SidebarRow`. */
39
+ renderRow: (item: T) => ReactNode;
40
+ loading?: boolean;
41
+ /** Shown instead of the groups when there are none, such as an empty
42
+ * state, a search with no matches, or an error. */
43
+ placeholder?: ReactNode;
44
+ /** Above the list, collapsed or not: new-item buttons and search. */
45
+ top?: ReactNode;
46
+ /** What the collapsed rail holds, as `SidebarRailItem`s. */
47
+ rail?: ReactNode;
48
+ /** Beside the toggle at the bottom, while expanded. */
49
+ footer?: ReactNode;
50
+ /** ⌘⌥↑ and ⌘⌥↓ (Ctrl+Alt elsewhere) select the previous or next item. */
51
+ cycle?: {
52
+ current: T | undefined;
53
+ onSelect: (item: T) => void;
54
+ };
55
+ groupTestId?: string;
56
+ /** Dialogs and menus that belong to the list. */
57
+ children?: ReactNode;
58
+ }): import("react").JSX.Element;
@@ -0,0 +1,162 @@
1
+ import { positionKeys } from "../../components/keys.js";
2
+ import { SidebarGroupHeading, SidebarPanel, SidebarToggle } from "./sidebar.js";
3
+ import cx from "classnames";
4
+ import { Fragment, useEffect, useRef, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ function clampSidebarWidth(width) {
7
+ return Math.min(480, Math.max(200, width));
8
+ }
9
+ function readStoredSidebarWidth(storageKey) {
10
+ try {
11
+ const stored = Number(window.localStorage.getItem(storageKey));
12
+ return Number.isFinite(stored) && stored > 0 ? clampSidebarWidth(stored) : 256;
13
+ } catch {
14
+ return 256;
15
+ }
16
+ }
17
+ function storeSidebarWidth(storageKey, width) {
18
+ try {
19
+ window.localStorage.setItem(storageKey, String(width));
20
+ } catch {}
21
+ }
22
+ /** A sidebar width the reader drags, nudges or resets, kept per list under
23
+ * its own storage key. */
24
+ function useResizableSidebarWidth(storageKey) {
25
+ const [width, setWidth] = useState(() => readStoredSidebarWidth(storageKey));
26
+ const [resizing, setResizing] = useState(false);
27
+ const widthRef = useRef(width);
28
+ widthRef.current = width;
29
+ const startResize = (e) => {
30
+ e.preventDefault();
31
+ const startX = e.clientX;
32
+ const startWidth = widthRef.current;
33
+ setResizing(true);
34
+ const onMove = (ev) => {
35
+ setWidth(clampSidebarWidth(startWidth + ev.clientX - startX));
36
+ };
37
+ const onUp = () => {
38
+ window.removeEventListener("pointermove", onMove);
39
+ window.removeEventListener("pointerup", onUp);
40
+ setResizing(false);
41
+ storeSidebarWidth(storageKey, widthRef.current);
42
+ };
43
+ window.addEventListener("pointermove", onMove);
44
+ window.addEventListener("pointerup", onUp);
45
+ };
46
+ const reset = () => {
47
+ setWidth(256);
48
+ storeSidebarWidth(storageKey, 256);
49
+ };
50
+ const nudge = (delta) => {
51
+ const next = clampSidebarWidth(widthRef.current + delta);
52
+ setWidth(next);
53
+ storeSidebarWidth(storageKey, next);
54
+ };
55
+ return {
56
+ width,
57
+ resizing,
58
+ startResize,
59
+ reset,
60
+ nudge
61
+ };
62
+ }
63
+ /** Steps through the listed items in order and wraps, so moving between
64
+ * conversations never needs the pointer. */
65
+ function useItemCycling(ordered, cycle) {
66
+ const latest = useRef({
67
+ ordered,
68
+ cycle
69
+ });
70
+ latest.current = {
71
+ ordered,
72
+ cycle
73
+ };
74
+ const enabled = !!cycle;
75
+ useEffect(() => {
76
+ if (!enabled) return;
77
+ const onKeyDown = (e) => {
78
+ const step = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0;
79
+ const { ordered: items, cycle: c } = latest.current;
80
+ if (!step || !e.altKey || !(e.metaKey || e.ctrlKey) || !c || items.length === 0) return;
81
+ e.preventDefault();
82
+ const at = c.current === void 0 ? -1 : items.indexOf(c.current);
83
+ const next = at < 0 ? items[step > 0 ? 0 : items.length - 1] : items[(at + step + items.length) % items.length];
84
+ if (next !== void 0) c.onSelect(next);
85
+ };
86
+ document.addEventListener("keydown", onKeyDown);
87
+ return () => document.removeEventListener("keydown", onKeyDown);
88
+ }, [enabled]);
89
+ }
90
+ /** A list of conversations beside the thread: headed groups of rows that
91
+ * collapse to a rail, with an optional drag handle for the width. The host
92
+ * decides what is listed, how it is grouped and how each row reads. */
93
+ function ConversationSidebar({ label, collapsed, onToggle, toggleLabels, resize, groups, getKey, renderRow, loading = false, placeholder, top, rail, footer, cycle, groupTestId, children }) {
94
+ const sized = useResizableSidebarWidth(resize?.storageKey ?? "");
95
+ useItemCycling(groups.flatMap((g) => g.items), cycle);
96
+ return /* @__PURE__ */ jsxs(SidebarPanel, {
97
+ collapsed,
98
+ label,
99
+ ...resize ? {
100
+ width: sized.width,
101
+ resizing: sized.resizing
102
+ } : {},
103
+ children: [
104
+ resize && !collapsed && /* @__PURE__ */ jsx("div", {
105
+ role: "separator",
106
+ tabIndex: 0,
107
+ "aria-orientation": "vertical",
108
+ "aria-label": resize.label,
109
+ "aria-valuemin": 200,
110
+ "aria-valuemax": 480,
111
+ "aria-valuenow": sized.width,
112
+ title: resize.hint,
113
+ onPointerDown: sized.startResize,
114
+ onDoubleClick: sized.reset,
115
+ onKeyDown: (e) => {
116
+ if (e.key === "ArrowLeft") sized.nudge(-16);
117
+ else if (e.key === "ArrowRight") sized.nudge(16);
118
+ },
119
+ className: cx("absolute inset-y-0 right-0 z-10 w-1 cursor-col-resize touch-none", sized.resizing ? "bg-(--theme-element)" : "hover:bg-(--theme-border)")
120
+ }),
121
+ top,
122
+ collapsed && rail ? /* @__PURE__ */ jsx("ul", {
123
+ className: "flex min-h-0 flex-1 flex-col items-center gap-1 overflow-y-auto py-1",
124
+ children: rail
125
+ }) : /* @__PURE__ */ jsx("div", {
126
+ className: cx("min-h-0 flex-1 overflow-y-auto px-2 pb-2 transition-opacity duration-200", collapsed ? "pointer-events-none opacity-0" : "opacity-100"),
127
+ children: loading ? /* @__PURE__ */ jsx("div", {
128
+ className: "space-y-3 px-2 pt-2",
129
+ children: positionKeys(5, "skeleton").map((key, i) => /* @__PURE__ */ jsx("div", {
130
+ className: "animate-pulse",
131
+ children: /* @__PURE__ */ jsx("div", {
132
+ className: "h-4 rounded theme-muted-panel",
133
+ style: { width: `${70 - i * 10}%` }
134
+ })
135
+ }, key))
136
+ }) : groups.length === 0 ? placeholder : groups.map((group) => /* @__PURE__ */ jsxs("div", {
137
+ className: "mb-2",
138
+ children: [/* @__PURE__ */ jsx(SidebarGroupHeading, {
139
+ tone: group.tone ?? "default",
140
+ testId: groupTestId,
141
+ children: group.label
142
+ }), /* @__PURE__ */ jsx("ul", {
143
+ className: "space-y-0.5",
144
+ children: group.items.map((item) => /* @__PURE__ */ jsx(Fragment, { children: renderRow(item) }, getKey(item)))
145
+ })]
146
+ }, group.key))
147
+ }),
148
+ /* @__PURE__ */ jsxs("div", {
149
+ className: cx("flex shrink-0 items-center gap-1 p-1.5", collapsed ? "justify-center" : footer ? "justify-between" : "justify-end"),
150
+ children: [!collapsed && footer, /* @__PURE__ */ jsx(SidebarToggle, {
151
+ collapsed,
152
+ onToggle,
153
+ openLabel: toggleLabels.open,
154
+ closeLabel: toggleLabels.close
155
+ })]
156
+ }),
157
+ children
158
+ ]
159
+ });
160
+ }
161
+ //#endregion
162
+ export { ConversationSidebar, useResizableSidebarWidth };
@@ -10,6 +10,14 @@ export declare function NoticeBar({ tone, title, children, action, onAction, tes
10
10
  onAction?: () => void;
11
11
  testId?: string;
12
12
  }): import("react").JSX.Element;
13
+ /** What a command printed, kept verbatim under the command that printed it. */
14
+ export declare function OutputCard({ title, text, onDismiss, dismissLabel, testId, }: {
15
+ title: string;
16
+ text: string;
17
+ onDismiss?: () => void;
18
+ dismissLabel?: string;
19
+ testId?: string;
20
+ }): import("react").JSX.Element;
13
21
  /** An inline notice next to the composer. One with no dismiss states a fact
14
22
  * that is still true; one with a dismiss is a message the reader can put away. */
15
23
  export declare function NoticeCard({ tone, children, onDismiss, dismissLabel, testId, }: {