Merge branch 'main' into pip
This commit is contained in:
commit
9066946615
4 changed files with 81 additions and 39 deletions
|
|
@ -225,6 +225,7 @@ const LoraCompareContent = memo(function LoraCompareContent({
|
|||
modelType="base"
|
||||
pairId={pairId}
|
||||
initialThreadId={baseThreadId}
|
||||
syncActiveThreadId={false}
|
||||
>
|
||||
<RegisterCompareHandle name="base" />
|
||||
<Thread hideComposer={true} hideWelcome={true} />
|
||||
|
|
@ -242,6 +243,7 @@ const LoraCompareContent = memo(function LoraCompareContent({
|
|||
modelType="lora"
|
||||
pairId={pairId}
|
||||
initialThreadId={loraThreadId}
|
||||
syncActiveThreadId={false}
|
||||
>
|
||||
<RegisterCompareHandle name="lora" />
|
||||
<Thread hideComposer={true} hideWelcome={true} />
|
||||
|
|
@ -343,6 +345,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
|||
modelType="model1"
|
||||
pairId={pairId}
|
||||
initialThreadId={model1ThreadId}
|
||||
syncActiveThreadId={false}
|
||||
>
|
||||
<RegisterCompareHandle name="model1" />
|
||||
<Thread hideComposer={true} hideWelcome={true} />
|
||||
|
|
@ -376,6 +379,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
|||
modelType="model2"
|
||||
pairId={pairId}
|
||||
initialThreadId={model2ThreadId}
|
||||
syncActiveThreadId={false}
|
||||
>
|
||||
<RegisterCompareHandle name="model2" />
|
||||
<Thread hideComposer={true} hideWelcome={true} />
|
||||
|
|
@ -479,11 +483,19 @@ function TopBarActions({
|
|||
);
|
||||
}
|
||||
|
||||
function getInitialSingleChatView(): ChatView {
|
||||
const id = useChatRuntimeStore.getState().activeThreadId;
|
||||
if (typeof id === "string" && id.length > 0 && !id.startsWith("__LOCALID_")) {
|
||||
return { mode: "single", threadId: id };
|
||||
}
|
||||
return { mode: "single" };
|
||||
}
|
||||
|
||||
export function ChatPage(): ReactElement {
|
||||
const [view, setView] = useState<ChatView>({
|
||||
mode: "single",
|
||||
newThreadNonce: crypto.randomUUID(),
|
||||
});
|
||||
// Do not set newThreadNonce here: each /chat mount would run ThreadNewChatSwitch
|
||||
// and create spurious threads when navigating (e.g. Recipes / Export). New Chat
|
||||
// explicitly sets a nonce in handleNewThread.
|
||||
const [view, setView] = useState<ChatView>(getInitialSingleChatView);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
|
||||
const [modelSelectorLocked, setModelSelectorLocked] = useState(false);
|
||||
|
|
@ -587,9 +599,20 @@ export function ChatPage(): ReactElement {
|
|||
void ejectModel();
|
||||
}, [ejectModel]);
|
||||
const handleNewThread = useCallback(() => {
|
||||
// Skip if we are already on a fresh unsaved draft with no messages sent.
|
||||
// Once the user sends a message, append() sets activeThreadId in the store,
|
||||
// so we check the store to know whether the current draft has been sent.
|
||||
if (
|
||||
view.mode === "single" &&
|
||||
!view.threadId &&
|
||||
!useChatRuntimeStore.getState().activeThreadId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
useChatRuntimeStore.getState().setActiveThreadId(null);
|
||||
setView({ mode: "single", newThreadNonce: crypto.randomUUID() });
|
||||
}, []);
|
||||
}, [view]);
|
||||
const handleNewCompare = useCallback(() => {
|
||||
setView({ mode: "compare", pairId: crypto.randomUUID() });
|
||||
// Clear activeThreadId so compare panes do not inherit the single-chat
|
||||
|
|
@ -922,7 +945,7 @@ export function ChatPage(): ReactElement {
|
|||
|
||||
{view.mode === "single" ? (
|
||||
<SingleContent
|
||||
key={view.threadId ?? view.newThreadNonce ?? "new"}
|
||||
key={view.threadId ?? "single"}
|
||||
threadId={view.threadId}
|
||||
newThreadNonce={view.newThreadNonce}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -596,6 +596,15 @@ function ThreadHistoryProvider({
|
|||
|
||||
async append({ parentId, message }: ExportedMessageRepositoryItem) {
|
||||
const { remoteId } = await aui.threadListItem().initialize();
|
||||
// Keep single-chat runtime state in sync once a new chat is first
|
||||
// persisted. Compare panes intentionally do not write global activeThreadId.
|
||||
const thread = await db.threads.get(remoteId);
|
||||
if (thread?.modelType === "base" && !thread.pairId) {
|
||||
const store = useChatRuntimeStore.getState();
|
||||
if (store.activeThreadId !== remoteId) {
|
||||
store.setActiveThreadId(remoteId);
|
||||
}
|
||||
}
|
||||
const content = cloneContent(message.content);
|
||||
const attachments =
|
||||
message.role === "user" ? cloneAttachments(message.attachments) : [];
|
||||
|
|
@ -658,7 +667,11 @@ function useRuntimeHook(): ReturnType<typeof useLocalRuntime> {
|
|||
|
||||
function ThreadAutoSwitch({
|
||||
threadId,
|
||||
}: { threadId: string }): ReactElement | null {
|
||||
syncActiveThreadId = true,
|
||||
}: {
|
||||
threadId: string;
|
||||
syncActiveThreadId?: boolean;
|
||||
}): ReactElement | null {
|
||||
const aui = useAui();
|
||||
const isLoading = useAuiState(({ threads }) => threads.isLoading);
|
||||
const mainThreadId = useAuiState(({ threads }) => threads.mainThreadId);
|
||||
|
|
@ -669,6 +682,13 @@ function ThreadAutoSwitch({
|
|||
}
|
||||
}, [aui, isLoading, mainThreadId, threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!syncActiveThreadId || isLoading || mainThreadId !== threadId) {
|
||||
return;
|
||||
}
|
||||
useChatRuntimeStore.getState().setActiveThreadId(threadId);
|
||||
}, [isLoading, mainThreadId, syncActiveThreadId, threadId]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -682,30 +702,10 @@ function ThreadNewChatSwitch({
|
|||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
// Clear immediately so the adapter never picks up a stale thread ID
|
||||
// from a previous chat while we initialize the new one.
|
||||
// Switch to a fresh local thread without persisting it yet.
|
||||
// Persistence still happens on first message append.
|
||||
void aui.threads().switchToNewThread();
|
||||
useChatRuntimeStore.getState().setActiveThreadId(null);
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
aui.threads().switchToNewThread();
|
||||
const { remoteId } = await aui.threadListItem().initialize();
|
||||
if (!cancelled) {
|
||||
useChatRuntimeStore.getState().setActiveThreadId(remoteId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
useChatRuntimeStore.getState().setActiveThreadId(null);
|
||||
}
|
||||
console.error("Failed to initialize new chat thread", error);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [aui, isLoading, nonce]);
|
||||
|
||||
return null;
|
||||
|
|
@ -733,12 +733,14 @@ export function ChatRuntimeProvider({
|
|||
pairId,
|
||||
initialThreadId,
|
||||
newThreadNonce,
|
||||
syncActiveThreadId = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
modelType?: ModelType;
|
||||
pairId?: string;
|
||||
initialThreadId?: string;
|
||||
newThreadNonce?: string;
|
||||
syncActiveThreadId?: boolean;
|
||||
}): ReactElement {
|
||||
const runtime = useRemoteThreadListRuntime({
|
||||
runtimeHook: useRuntimeHook,
|
||||
|
|
@ -754,8 +756,15 @@ export function ChatRuntimeProvider({
|
|||
|
||||
return (
|
||||
<AssistantRuntimeProvider runtime={runtime} aui={aui}>
|
||||
<ActiveThreadSync enabled={modelType === "base" && !pairId && !newThreadNonce} />
|
||||
{initialThreadId && <ThreadAutoSwitch threadId={initialThreadId} />}
|
||||
<ActiveThreadSync
|
||||
enabled={modelType === "base" && !pairId && !newThreadNonce && !initialThreadId}
|
||||
/>
|
||||
{initialThreadId && (
|
||||
<ThreadAutoSwitch
|
||||
threadId={initialThreadId}
|
||||
syncActiveThreadId={syncActiveThreadId}
|
||||
/>
|
||||
)}
|
||||
{!initialThreadId && newThreadNonce && (
|
||||
<ThreadNewChatSwitch nonce={newThreadNonce} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { db, useLiveQuery } from "./db";
|
||||
import { useChatRuntimeStore } from "./stores/chat-runtime-store";
|
||||
import type { ChatView, ThreadRecord } from "./types";
|
||||
|
||||
interface SidebarItem {
|
||||
|
|
@ -76,12 +77,17 @@ export function ThreadSidebar({
|
|||
onNewCompare: () => void;
|
||||
showCompare: boolean;
|
||||
}) {
|
||||
const allThreads = useLiveQuery(
|
||||
() => db.threads.orderBy("createdAt").reverse().toArray(),
|
||||
[],
|
||||
);
|
||||
const allThreads = useLiveQuery(async () => {
|
||||
const threadIdsWithMessage = new Set(
|
||||
(await db.messages.orderBy("threadId").uniqueKeys()) as string[],
|
||||
);
|
||||
const rows = await db.threads.orderBy("createdAt").reverse().toArray();
|
||||
return rows.filter((t) => !t.archived && threadIdsWithMessage.has(t.id));
|
||||
}, []);
|
||||
const items = groupThreads(allThreads ?? []);
|
||||
const activeId = view.mode === "single" ? view.threadId : view.pairId;
|
||||
const storeThreadId = useChatRuntimeStore((s) => s.activeThreadId);
|
||||
const activeId =
|
||||
view.mode === "single" ? (view.threadId ?? storeThreadId) : view.pairId;
|
||||
|
||||
function viewForItem(item: SidebarItem): ChatView {
|
||||
return item.type === "single"
|
||||
|
|
@ -101,7 +107,11 @@ export function ThreadSidebar({
|
|||
}
|
||||
}
|
||||
if (activeId === item.id) {
|
||||
onSelect({ mode: "single" });
|
||||
// Directly set a new view with a nonce rather than going through
|
||||
// onNewThread(), which may return early if the guard sees no
|
||||
// threadId and no activeThreadId (after we just cleared it).
|
||||
useChatRuntimeStore.getState().setActiveThreadId(null);
|
||||
onSelect({ mode: "single", newThreadNonce: crypto.randomUUID() });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__version__ = "2026.4.3"
|
||||
__version__ = "2026.4.4"
|
||||
|
||||
__all__ = [
|
||||
"SUPPORTS_BFLOAT16",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue