@parallelworks/ui 0.28.0 → 0.30.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 +9 -0
- package/dist/ai/components/ChatLayout.d.ts +11 -2
- package/dist/ai/components/ChatLayout.js +53 -5
- package/dist/ai/components/ChatSidebar.js +121 -187
- package/dist/ai/components/ModelSelector.js +5 -1
- package/dist/ai/core/ChatProvider.d.ts +18 -1
- package/dist/ai/core/ChatProvider.js +38 -28
- package/dist/ai/core/chatReducer.d.ts +0 -3
- package/dist/ai/core/chatReducer.js +0 -5
- package/dist/ai/core/sidebarState.d.ts +19 -0
- package/dist/ai/core/sidebarState.js +57 -0
- package/dist/ai/index.d.ts +4 -2
- package/dist/ai/index.js +4 -3
- package/dist/ai/strings.d.ts +8 -0
- package/dist/ai/strings.js +4 -0
- package/dist/ai/ui/ConversationSidebar.d.ts +61 -0
- package/dist/ai/ui/ConversationSidebar.js +165 -0
- package/dist/ai/ui/sidebar.d.ts +13 -2
- package/dist/ai/ui/sidebar.js +37 -2
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -2,25 +2,13 @@ import { applyPartDelta, finalizeParts } from "./parts.js";
|
|
|
2
2
|
import { chatReducer, initialState } from "./chatReducer.js";
|
|
3
3
|
import { ChatConfigProvider, resolveChatConfig } from "./config.js";
|
|
4
4
|
import { providerIssueFor } from "./providerIssues.js";
|
|
5
|
+
import { ReportSidebarPresentationContext, readStoredSidebar, storeSidebar, useControllable } from "./sidebarState.js";
|
|
5
6
|
import { useSSEStream } from "./useSSEStream.js";
|
|
6
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from "react";
|
|
7
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
7
8
|
import { jsx } from "react/jsx-runtime";
|
|
8
9
|
import { DateTime } from "luxon";
|
|
9
10
|
//#region src/ai/core/ChatProvider.tsx
|
|
10
11
|
var SELECTED_MODEL_STORAGE_KEY = "aiChatSelectedModel";
|
|
11
|
-
var SIDEBAR_COLLAPSED_STORAGE_KEY = "aiChatSidebarCollapsed";
|
|
12
|
-
function readStoredSidebarCollapsed() {
|
|
13
|
-
try {
|
|
14
|
-
return globalThis.localStorage?.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY) === "1";
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function storeSidebarCollapsed(collapsed) {
|
|
20
|
-
try {
|
|
21
|
-
globalThis.localStorage?.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, collapsed ? "1" : "0");
|
|
22
|
-
} catch {}
|
|
23
|
-
}
|
|
24
12
|
function readStoredModel() {
|
|
25
13
|
try {
|
|
26
14
|
return globalThis.localStorage?.getItem(SELECTED_MODEL_STORAGE_KEY) ?? null;
|
|
@@ -73,11 +61,8 @@ function useChat() {
|
|
|
73
61
|
if (!context) throw new Error("useChat must be used within a ChatProvider");
|
|
74
62
|
return context;
|
|
75
63
|
}
|
|
76
|
-
function ChatProvider({ adapter, currentUser, navigation, notify, activeConversationId = null, config, children }) {
|
|
77
|
-
const [state, dispatch] = useReducer(chatReducer, initialState
|
|
78
|
-
...seed,
|
|
79
|
-
sidebarCollapsed: readStoredSidebarCollapsed()
|
|
80
|
-
}));
|
|
64
|
+
function ChatProvider({ adapter, currentUser, navigation, notify, activeConversationId = null, config, sidebar: sidebarProp, onSidebarChange, drawerOpen: drawerOpenProp, onDrawerOpenChange, children }) {
|
|
65
|
+
const [state, dispatch] = useReducer(chatReducer, initialState);
|
|
81
66
|
const hasLoadedConversationsRef = useRef(false);
|
|
82
67
|
const { onStreamingChunk, startStreaming, stopAnimationLoop, resetAnimationRefs, targetStreamContentRef, streamActiveRef, streamingConversationIdRef, backgroundStreamsRef, abortControllerRef } = useSSEStream(dispatch);
|
|
83
68
|
const currentConversationIdRef = useRef(null);
|
|
@@ -700,12 +685,24 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
700
685
|
state.currentConversation?.messages,
|
|
701
686
|
state.thinkingStartTime
|
|
702
687
|
]);
|
|
703
|
-
const
|
|
704
|
-
|
|
688
|
+
const [sidebar, setSidebar, sidebarRef] = useControllable(sidebarProp, readStoredSidebar, onSidebarChange, storeSidebar);
|
|
689
|
+
const [drawerOpen, setDrawerOpen, drawerOpenRef] = useControllable(drawerOpenProp, () => false, onDrawerOpenChange);
|
|
690
|
+
const [sidebarPresentation, setSidebarPresentation] = useState("inline");
|
|
691
|
+
const presentationRef = useRef(sidebarPresentation);
|
|
692
|
+
presentationRef.current = sidebarPresentation;
|
|
693
|
+
const reportSidebarPresentation = useCallback((presentation) => {
|
|
694
|
+
setSidebarPresentation(presentation);
|
|
695
|
+
if (presentation === "inline") setDrawerOpen(false);
|
|
696
|
+
}, [setDrawerOpen]);
|
|
705
697
|
const toggleSidebar = useCallback(() => {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}, [
|
|
698
|
+
if (presentationRef.current === "drawer") setDrawerOpen(!drawerOpenRef.current.value);
|
|
699
|
+
else setSidebar(sidebarRef.current.value === "expanded" ? "collapsed" : "expanded");
|
|
700
|
+
}, [
|
|
701
|
+
setDrawerOpen,
|
|
702
|
+
drawerOpenRef,
|
|
703
|
+
setSidebar,
|
|
704
|
+
sidebarRef
|
|
705
|
+
]);
|
|
709
706
|
const navigateToBranch = useCallback((messageId) => {
|
|
710
707
|
if (!state.currentConversation) return;
|
|
711
708
|
dispatch({
|
|
@@ -770,7 +767,10 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
770
767
|
streamingReasoning: state.streamingReasoning,
|
|
771
768
|
streamingParts: state.streamingParts,
|
|
772
769
|
thinkingStartTime: state.thinkingStartTime,
|
|
773
|
-
|
|
770
|
+
sidebar,
|
|
771
|
+
sidebarCollapsed: sidebar !== "expanded",
|
|
772
|
+
sidebarPresentation,
|
|
773
|
+
drawerOpen,
|
|
774
774
|
queuedMessages: state.queuedMessages,
|
|
775
775
|
loadConversations,
|
|
776
776
|
loadConversation,
|
|
@@ -786,6 +786,8 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
786
786
|
stopStreaming,
|
|
787
787
|
removeQueuedMessage,
|
|
788
788
|
flushQueuedMessages,
|
|
789
|
+
setSidebar,
|
|
790
|
+
setDrawerOpen,
|
|
789
791
|
toggleSidebar,
|
|
790
792
|
updateConversationTitle,
|
|
791
793
|
navigateToBranch,
|
|
@@ -797,6 +799,9 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
797
799
|
notify,
|
|
798
800
|
activeConversationId,
|
|
799
801
|
state,
|
|
802
|
+
sidebar,
|
|
803
|
+
sidebarPresentation,
|
|
804
|
+
drawerOpen,
|
|
800
805
|
isSelectedModelAvailable,
|
|
801
806
|
loadConversations,
|
|
802
807
|
loadConversation,
|
|
@@ -812,6 +817,8 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
812
817
|
stopStreaming,
|
|
813
818
|
removeQueuedMessage,
|
|
814
819
|
flushQueuedMessages,
|
|
820
|
+
setSidebar,
|
|
821
|
+
setDrawerOpen,
|
|
815
822
|
toggleSidebar,
|
|
816
823
|
updateConversationTitle,
|
|
817
824
|
navigateToBranch,
|
|
@@ -819,9 +826,12 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
819
826
|
]);
|
|
820
827
|
return /* @__PURE__ */ jsx(ChatConfigProvider, {
|
|
821
828
|
value: resolvedConfig,
|
|
822
|
-
children: /* @__PURE__ */ jsx(
|
|
823
|
-
value,
|
|
824
|
-
children
|
|
829
|
+
children: /* @__PURE__ */ jsx(ReportSidebarPresentationContext.Provider, {
|
|
830
|
+
value: reportSidebarPresentation,
|
|
831
|
+
children: /* @__PURE__ */ jsx(ChatContext.Provider, {
|
|
832
|
+
value,
|
|
833
|
+
children
|
|
834
|
+
})
|
|
825
835
|
})
|
|
826
836
|
});
|
|
827
837
|
}
|
|
@@ -20,7 +20,6 @@ export interface ChatState {
|
|
|
20
20
|
streamingReasoning: string;
|
|
21
21
|
streamingParts: MessagePart[];
|
|
22
22
|
thinkingStartTime: number | null;
|
|
23
|
-
sidebarCollapsed: boolean;
|
|
24
23
|
queuedMessages: ChatMessage[];
|
|
25
24
|
}
|
|
26
25
|
export type ChatAction = {
|
|
@@ -80,8 +79,6 @@ export type ChatAction = {
|
|
|
80
79
|
messageId: string;
|
|
81
80
|
updates: Partial<ChatMessage>;
|
|
82
81
|
conversationId: string;
|
|
83
|
-
} | {
|
|
84
|
-
type: 'TOGGLE_SIDEBAR';
|
|
85
82
|
} | {
|
|
86
83
|
type: 'ADD_CONVERSATION';
|
|
87
84
|
conversation: ConversationSummary;
|
|
@@ -20,7 +20,6 @@ var initialState = {
|
|
|
20
20
|
streamingReasoning: "",
|
|
21
21
|
streamingParts: [],
|
|
22
22
|
thinkingStartTime: null,
|
|
23
|
-
sidebarCollapsed: false,
|
|
24
23
|
queuedMessages: []
|
|
25
24
|
};
|
|
26
25
|
function chatReducer(state, action) {
|
|
@@ -165,10 +164,6 @@ function chatReducer(state, action) {
|
|
|
165
164
|
} : msg)
|
|
166
165
|
}
|
|
167
166
|
};
|
|
168
|
-
case "TOGGLE_SIDEBAR": return {
|
|
169
|
-
...state,
|
|
170
|
-
sidebarCollapsed: !state.sidebarCollapsed
|
|
171
|
-
};
|
|
172
167
|
case "ADD_CONVERSATION":
|
|
173
168
|
if (state.conversations.some((c) => c.id === action.conversation.id)) return state;
|
|
174
169
|
return {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The conversation list beside the thread: full width, a rail of icons, or
|
|
2
|
+
* not shown at all (the host offers its own way back). */
|
|
3
|
+
export type SidebarState = 'expanded' | 'collapsed' | 'hidden';
|
|
4
|
+
/** How the layout shows the list right now: as a column beside the thread,
|
|
5
|
+
* or as a drawer over it. */
|
|
6
|
+
export type SidebarPresentation = 'inline' | 'drawer';
|
|
7
|
+
export declare function readStoredSidebar(): SidebarState;
|
|
8
|
+
export declare function storeSidebar(state: SidebarState): void;
|
|
9
|
+
/** A value the host may own through a prop. Without the prop the provider
|
|
10
|
+
* keeps it; either way the change callback hears every change. The ref
|
|
11
|
+
* reads the latest value from callbacks that should not re-create. */
|
|
12
|
+
export declare function useControllable<T>(controlled: T | undefined, initial: () => T, onChange: ((value: T) => void) | undefined, remember?: (value: T) => void): readonly [T, (next: T) => void, import("react").RefObject<{
|
|
13
|
+
value: T;
|
|
14
|
+
controlled: boolean;
|
|
15
|
+
onChange: ((value: T) => void) | undefined;
|
|
16
|
+
remember: ((value: T) => void) | undefined;
|
|
17
|
+
}>];
|
|
18
|
+
export declare const ReportSidebarPresentationContext: import("react").Context<(presentation: SidebarPresentation) => void>;
|
|
19
|
+
export declare function useReportSidebarPresentation(): (presentation: SidebarPresentation) => void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useRef, useState } from "react";
|
|
2
|
+
//#region src/ai/core/sidebarState.ts
|
|
3
|
+
var SIDEBAR_STORAGE_KEY = "aiChatSidebar";
|
|
4
|
+
var LEGACY_COLLAPSED_STORAGE_KEY = "aiChatSidebarCollapsed";
|
|
5
|
+
function readStoredSidebar() {
|
|
6
|
+
try {
|
|
7
|
+
const stored = globalThis.localStorage?.getItem(SIDEBAR_STORAGE_KEY);
|
|
8
|
+
if (stored === "expanded" || stored === "collapsed") return stored;
|
|
9
|
+
return globalThis.localStorage?.getItem(LEGACY_COLLAPSED_STORAGE_KEY) === "1" ? "collapsed" : "expanded";
|
|
10
|
+
} catch {
|
|
11
|
+
return "expanded";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function storeSidebar(state) {
|
|
15
|
+
if (state === "hidden") return;
|
|
16
|
+
try {
|
|
17
|
+
globalThis.localStorage?.setItem(SIDEBAR_STORAGE_KEY, state);
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
/** A value the host may own through a prop. Without the prop the provider
|
|
21
|
+
* keeps it; either way the change callback hears every change. The ref
|
|
22
|
+
* reads the latest value from callbacks that should not re-create. */
|
|
23
|
+
function useControllable(controlled, initial, onChange, remember) {
|
|
24
|
+
const [own, setOwn] = useState(initial);
|
|
25
|
+
const value = controlled === void 0 ? own : controlled;
|
|
26
|
+
const latest = useRef({
|
|
27
|
+
value,
|
|
28
|
+
controlled: controlled !== void 0,
|
|
29
|
+
onChange,
|
|
30
|
+
remember
|
|
31
|
+
});
|
|
32
|
+
latest.current = {
|
|
33
|
+
value,
|
|
34
|
+
controlled: controlled !== void 0,
|
|
35
|
+
onChange,
|
|
36
|
+
remember
|
|
37
|
+
};
|
|
38
|
+
return [
|
|
39
|
+
value,
|
|
40
|
+
useCallback((next) => {
|
|
41
|
+
const l = latest.current;
|
|
42
|
+
if (Object.is(next, l.value)) return;
|
|
43
|
+
if (!l.controlled) {
|
|
44
|
+
setOwn(next);
|
|
45
|
+
l.remember?.(next);
|
|
46
|
+
}
|
|
47
|
+
l.onChange?.(next);
|
|
48
|
+
}, []),
|
|
49
|
+
latest
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
var ReportSidebarPresentationContext = createContext(() => {});
|
|
53
|
+
function useReportSidebarPresentation() {
|
|
54
|
+
return useContext(ReportSidebarPresentationContext);
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { ReportSidebarPresentationContext, readStoredSidebar, storeSidebar, useControllable, useReportSidebarPresentation };
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { default as BlockedGroupBanner } from './components/BlockedGroupBanner.j
|
|
|
11
11
|
export { default as BranchNavigator } from './components/BranchNavigator.js';
|
|
12
12
|
export { default as ChatEmptyState } from './components/ChatEmptyState.js';
|
|
13
13
|
export { type ChatInputHandle, default as ChatInput, } from './components/ChatInput.js';
|
|
14
|
-
export { default as ChatLayout } from './components/ChatLayout.js';
|
|
14
|
+
export { DRAWER_BELOW_PX, default as ChatLayout, type SidebarMode, } from './components/ChatLayout.js';
|
|
15
15
|
export { default as ChatMessageView } from './components/ChatMessage.js';
|
|
16
16
|
export { default as ChatMessageList } from './components/ChatMessageList.js';
|
|
17
17
|
export { default as ChatSidebar } from './components/ChatSidebar.js';
|
|
@@ -34,11 +34,13 @@ export { KeyboardShortcutsProvider } from './core/KeyboardShortcutsProvider.js';
|
|
|
34
34
|
export { applyPartDelta, finalizeParts } from './core/parts.js';
|
|
35
35
|
export { pasteInlineMaxBytes } from './core/pastes.js';
|
|
36
36
|
export { providerIssueFor } from './core/providerIssues.js';
|
|
37
|
+
export type { SidebarPresentation, SidebarState } from './core/sidebarState.js';
|
|
37
38
|
export { default as useDragDrop } from './core/useDragDrop.js';
|
|
38
39
|
export { useKeyboardShortcuts } from './core/useKeyboardShortcuts.js';
|
|
39
40
|
export { type ChatStrings, defaultChatStrings } from './strings.js';
|
|
40
41
|
export * from './types.js';
|
|
42
|
+
export { ConversationSidebar, type ConversationSidebarGroup, useResizableSidebarWidth, } from './ui/ConversationSidebar.js';
|
|
41
43
|
export { type DropdownOption, default as Dropdown } from './ui/Dropdown.js';
|
|
42
44
|
export { NoticeBar, NoticeCard, type NoticeTone, OutputCard } from './ui/Notice.js';
|
|
43
|
-
export { RenameDialog, type RowDialogStrings, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs, } from './ui/sidebar.js';
|
|
45
|
+
export { RenameDialog, type RowDialogStrings, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs, } from './ui/sidebar.js';
|
|
44
46
|
export { formatFileSize, isMac, isOrgModel } from './utils.js';
|
package/dist/ai/index.js
CHANGED
|
@@ -35,13 +35,14 @@ import ChatEmptyState from "./components/ChatEmptyState.js";
|
|
|
35
35
|
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
|
-
import { RenameDialog, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs } from "./ui/sidebar.js";
|
|
38
|
+
import { RenameDialog, SidebarDrawer, 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
|
-
import ChatLayout from "./components/ChatLayout.js";
|
|
42
|
+
import ChatLayout, { DRAWER_BELOW_PX } from "./components/ChatLayout.js";
|
|
42
43
|
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
47
|
import { NoticeBar, NoticeCard, OutputCard } 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, 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, useRowDialogs, wordEmphasisRanges };
|
|
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, DRAWER_BELOW_PX, 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, SidebarDrawer, 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 };
|
package/dist/ai/strings.d.ts
CHANGED
|
@@ -33,6 +33,10 @@ export interface ChatStrings {
|
|
|
33
33
|
bannerKeyRejected: (provider: string) => string;
|
|
34
34
|
bannerUnreachable: (provider: string) => string;
|
|
35
35
|
};
|
|
36
|
+
modelPicker: {
|
|
37
|
+
/** Beside a provider another user registered and shared with the reader. */
|
|
38
|
+
sharedBy: (owner: string) => string;
|
|
39
|
+
};
|
|
36
40
|
queue: {
|
|
37
41
|
remove: string;
|
|
38
42
|
/** Under a message that goes out once the running reply finishes. */
|
|
@@ -96,6 +100,10 @@ export interface ChatStrings {
|
|
|
96
100
|
sharedWithYou: string;
|
|
97
101
|
openSidebar: string;
|
|
98
102
|
closeSidebar: string;
|
|
103
|
+
/** Names the drawer the list opens in on a narrow layout. */
|
|
104
|
+
drawerLabel: string;
|
|
105
|
+
resizeLabel: string;
|
|
106
|
+
resizeHint: string;
|
|
99
107
|
newChat: string;
|
|
100
108
|
attachments: string;
|
|
101
109
|
connectTools: string;
|
package/dist/ai/strings.js
CHANGED
|
@@ -47,6 +47,7 @@ var defaultChatStrings = {
|
|
|
47
47
|
bannerKeyRejected: (provider) => `API key rejected for \u201c${provider}\u201d`,
|
|
48
48
|
bannerUnreachable: (provider) => `\u201c${provider}\u201d is unreachable`
|
|
49
49
|
},
|
|
50
|
+
modelPicker: { sharedBy: (owner) => `Shared by ${owner}` },
|
|
50
51
|
activity: {
|
|
51
52
|
tokens: (input, output) => `↑${input} ↓${output} tokens`,
|
|
52
53
|
stop: "Stop"
|
|
@@ -100,6 +101,9 @@ var defaultChatStrings = {
|
|
|
100
101
|
sharedWithYou: "Shared with you",
|
|
101
102
|
openSidebar: "Open sidebar",
|
|
102
103
|
closeSidebar: "Close sidebar",
|
|
104
|
+
drawerLabel: "Conversations",
|
|
105
|
+
resizeLabel: "Resize conversation list",
|
|
106
|
+
resizeHint: "Drag to resize; double-click to reset",
|
|
103
107
|
newChat: "New chat",
|
|
104
108
|
attachments: "Attachments",
|
|
105
109
|
connectTools: "Connect Tools",
|
|
@@ -0,0 +1,61 @@
|
|
|
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: collapsedProp, onToggle, toggleLabels, resize, fill, 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
|
+
/** Takes the width of its container, as inside a drawer, with no rail and
|
|
37
|
+
* no drag handle. */
|
|
38
|
+
fill?: boolean;
|
|
39
|
+
groups: ConversationSidebarGroup<T>[];
|
|
40
|
+
getKey: (item: T) => string;
|
|
41
|
+
/** One `SidebarRow`. */
|
|
42
|
+
renderRow: (item: T) => ReactNode;
|
|
43
|
+
loading?: boolean;
|
|
44
|
+
/** Shown instead of the groups when there are none, such as an empty
|
|
45
|
+
* state, a search with no matches, or an error. */
|
|
46
|
+
placeholder?: ReactNode;
|
|
47
|
+
/** Above the list, collapsed or not: new-item buttons and search. */
|
|
48
|
+
top?: ReactNode;
|
|
49
|
+
/** What the collapsed rail holds, as `SidebarRailItem`s. */
|
|
50
|
+
rail?: ReactNode;
|
|
51
|
+
/** Beside the toggle at the bottom, while expanded. */
|
|
52
|
+
footer?: ReactNode;
|
|
53
|
+
/** ⌘⌥↑ and ⌘⌥↓ (Ctrl+Alt elsewhere) select the previous or next item. */
|
|
54
|
+
cycle?: {
|
|
55
|
+
current: T | undefined;
|
|
56
|
+
onSelect: (item: T) => void;
|
|
57
|
+
};
|
|
58
|
+
groupTestId?: string;
|
|
59
|
+
/** Dialogs and menus that belong to the list. */
|
|
60
|
+
children?: ReactNode;
|
|
61
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,165 @@
|
|
|
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: collapsedProp, onToggle, toggleLabels, resize, fill = false, 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
|
+
const collapsed = collapsedProp && !fill;
|
|
97
|
+
const resizable = resize && !fill;
|
|
98
|
+
return /* @__PURE__ */ jsxs(SidebarPanel, {
|
|
99
|
+
collapsed,
|
|
100
|
+
label,
|
|
101
|
+
...resizable ? {
|
|
102
|
+
width: sized.width,
|
|
103
|
+
resizing: sized.resizing
|
|
104
|
+
} : {},
|
|
105
|
+
...fill ? { style: { width: "100%" } } : {},
|
|
106
|
+
children: [
|
|
107
|
+
resizable && !collapsed && /* @__PURE__ */ jsx("div", {
|
|
108
|
+
role: "separator",
|
|
109
|
+
tabIndex: 0,
|
|
110
|
+
"aria-orientation": "vertical",
|
|
111
|
+
"aria-label": resize.label,
|
|
112
|
+
"aria-valuemin": 200,
|
|
113
|
+
"aria-valuemax": 480,
|
|
114
|
+
"aria-valuenow": sized.width,
|
|
115
|
+
title: resize.hint,
|
|
116
|
+
onPointerDown: sized.startResize,
|
|
117
|
+
onDoubleClick: sized.reset,
|
|
118
|
+
onKeyDown: (e) => {
|
|
119
|
+
if (e.key === "ArrowLeft") sized.nudge(-16);
|
|
120
|
+
else if (e.key === "ArrowRight") sized.nudge(16);
|
|
121
|
+
},
|
|
122
|
+
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)")
|
|
123
|
+
}),
|
|
124
|
+
top,
|
|
125
|
+
collapsed && rail ? /* @__PURE__ */ jsx("ul", {
|
|
126
|
+
className: "flex min-h-0 flex-1 flex-col items-center gap-1 overflow-y-auto py-1",
|
|
127
|
+
children: rail
|
|
128
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
129
|
+
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"),
|
|
130
|
+
children: loading ? /* @__PURE__ */ jsx("div", {
|
|
131
|
+
className: "space-y-3 px-2 pt-2",
|
|
132
|
+
children: positionKeys(5, "skeleton").map((key, i) => /* @__PURE__ */ jsx("div", {
|
|
133
|
+
className: "animate-pulse",
|
|
134
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
135
|
+
className: "h-4 rounded theme-muted-panel",
|
|
136
|
+
style: { width: `${70 - i * 10}%` }
|
|
137
|
+
})
|
|
138
|
+
}, key))
|
|
139
|
+
}) : groups.length === 0 ? placeholder : groups.map((group) => /* @__PURE__ */ jsxs("div", {
|
|
140
|
+
className: "mb-2",
|
|
141
|
+
children: [/* @__PURE__ */ jsx(SidebarGroupHeading, {
|
|
142
|
+
tone: group.tone ?? "default",
|
|
143
|
+
testId: groupTestId,
|
|
144
|
+
children: group.label
|
|
145
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
146
|
+
className: "space-y-0.5",
|
|
147
|
+
children: group.items.map((item) => /* @__PURE__ */ jsx(Fragment, { children: renderRow(item) }, getKey(item)))
|
|
148
|
+
})]
|
|
149
|
+
}, group.key))
|
|
150
|
+
}),
|
|
151
|
+
/* @__PURE__ */ jsxs("div", {
|
|
152
|
+
className: cx("flex shrink-0 items-center gap-1 p-1.5", collapsed ? "justify-center" : footer ? "justify-between" : "justify-end"),
|
|
153
|
+
children: [!collapsed && footer, /* @__PURE__ */ jsx(SidebarToggle, {
|
|
154
|
+
collapsed,
|
|
155
|
+
onToggle,
|
|
156
|
+
openLabel: toggleLabels.open,
|
|
157
|
+
closeLabel: toggleLabels.close
|
|
158
|
+
})]
|
|
159
|
+
}),
|
|
160
|
+
children
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
export { ConversationSidebar, useResizableSidebarWidth };
|
package/dist/ai/ui/sidebar.d.ts
CHANGED
|
@@ -8,11 +8,22 @@ export declare function SidebarPanel({ collapsed, width, railWidth, resizing, la
|
|
|
8
8
|
width?: number;
|
|
9
9
|
railWidth?: number;
|
|
10
10
|
resizing?: boolean;
|
|
11
|
-
label?: string;
|
|
11
|
+
label?: string | undefined;
|
|
12
12
|
className?: string;
|
|
13
13
|
style?: CSSProperties;
|
|
14
14
|
children: ReactNode;
|
|
15
15
|
}): import("react").JSX.Element;
|
|
16
|
+
/** The list over the content of a layout too narrow to keep it beside the
|
|
17
|
+
* content. Place it in a positioned container: the drawer covers that
|
|
18
|
+
* container, not the window, so an embedded chat keeps to its own panel.
|
|
19
|
+
* Tapping outside or Escape closes it, and focus returns where it was. */
|
|
20
|
+
export declare function SidebarDrawer({ open, onClose, label, closeLabel, children, }: {
|
|
21
|
+
open: boolean;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
label: string;
|
|
24
|
+
closeLabel: string;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
16
27
|
export declare function SidebarToggle({ collapsed, onToggle, openLabel, closeLabel, className, }: {
|
|
17
28
|
collapsed: boolean;
|
|
18
29
|
onToggle: () => void;
|
|
@@ -24,7 +35,7 @@ export declare function SidebarGroupHeading({ children, tone, testId, }: {
|
|
|
24
35
|
children: ReactNode;
|
|
25
36
|
/** 'attention' is for the group the reader is being asked to act on. */
|
|
26
37
|
tone?: 'default' | 'attention';
|
|
27
|
-
testId?: string;
|
|
38
|
+
testId?: string | undefined;
|
|
28
39
|
}): import("react").JSX.Element;
|
|
29
40
|
/** One entry in the list: the row highlights while it is the open one or
|
|
30
41
|
* while its menu is up, and the menu opens from its button or a right-click. */
|
package/dist/ai/ui/sidebar.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ConfirmModal } from "../../components/ConfirmModal.js";
|
|
|
3
3
|
import { focusOnMount } from "../../components/focus.js";
|
|
4
4
|
import { MoreButton, rowContextMenuProps, useRowMenuActive } from "../../list/RowContextMenu.js";
|
|
5
5
|
import cx from "classnames";
|
|
6
|
-
import { useState } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/ai/ui/sidebar.tsx
|
|
9
9
|
var SIDEBAR_RAIL_WIDTH_PX = 48;
|
|
@@ -22,6 +22,41 @@ function SidebarPanel({ collapsed, width = 256, railWidth = SIDEBAR_RAIL_WIDTH_P
|
|
|
22
22
|
children
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
+
/** The list over the content of a layout too narrow to keep it beside the
|
|
26
|
+
* content. Place it in a positioned container: the drawer covers that
|
|
27
|
+
* container, not the window, so an embedded chat keeps to its own panel.
|
|
28
|
+
* Tapping outside or Escape closes it, and focus returns where it was. */
|
|
29
|
+
function SidebarDrawer({ open, onClose, label, closeLabel, children }) {
|
|
30
|
+
const panelRef = useRef(null);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!open) return;
|
|
33
|
+
const returnTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
34
|
+
panelRef.current?.focus();
|
|
35
|
+
return () => returnTo?.focus();
|
|
36
|
+
}, [open]);
|
|
37
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
38
|
+
type: "button",
|
|
39
|
+
tabIndex: -1,
|
|
40
|
+
"aria-label": closeLabel,
|
|
41
|
+
onClick: onClose,
|
|
42
|
+
className: cx("absolute inset-0 z-20 bg-black/40 transition-opacity duration-300", open ? "opacity-100" : "pointer-events-none opacity-0")
|
|
43
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
44
|
+
ref: panelRef,
|
|
45
|
+
role: "dialog",
|
|
46
|
+
"aria-modal": "true",
|
|
47
|
+
"aria-label": label,
|
|
48
|
+
tabIndex: -1,
|
|
49
|
+
inert: !open,
|
|
50
|
+
onKeyDown: (e) => {
|
|
51
|
+
if (e.key === "Escape") {
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
onClose();
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
className: cx("absolute inset-y-0 left-0 z-30 flex w-[min(20rem,85%)] shadow-xl transition-transform duration-300 ease-in-out focus:outline-none", open ? "translate-x-0" : "-translate-x-full"),
|
|
57
|
+
children
|
|
58
|
+
})] });
|
|
59
|
+
}
|
|
25
60
|
function SidebarToggle({ collapsed, onToggle, openLabel, closeLabel, className }) {
|
|
26
61
|
const label = collapsed ? openLabel : closeLabel;
|
|
27
62
|
return /* @__PURE__ */ jsx("button", {
|
|
@@ -147,4 +182,4 @@ function useRowDialogs({ strings, nameOf, onDelete, onRename }) {
|
|
|
147
182
|
};
|
|
148
183
|
}
|
|
149
184
|
//#endregion
|
|
150
|
-
export { RenameDialog, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs };
|
|
185
|
+
export { RenameDialog, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs };
|