Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 4 additions & 26 deletions apps/desktop/src/chat/components/chat-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useCallback } from "react";

import { cn } from "@hypr/utils";

Expand All @@ -11,28 +11,19 @@ import { useSessionTab } from "./use-session-tab";
import { useLanguageModel } from "~/ai/hooks";
import { useChatActions } from "~/chat/store/use-chat-actions";
import { useShell } from "~/contexts/shell";
import { id } from "~/shared/utils";
import * as main from "~/store/tinybase/store/main";

export function ChatView() {
const { chat } = useShell();
const { groupId, setGroupId } = chat;
const { groupId, sessionId, setGroupId, startNewChat, selectChat } = chat;

const { currentSessionId } = useSessionTab();

// sessionId drives the ChatSession key and useChat id.
// It is managed explicitly — not derived from groupId — so that we can distinguish:
// handleNewChat: new random ID → fresh useChat instance
// handleSelectChat: set to groupId → forces ChatSession remount to load history
// onGroupCreated: groupId changes but sessionId stays stable → keeps useChat alive for the in-flight stream
const [sessionId, setSessionId] = useState<string>(() => groupId ?? id());

const model = useLanguageModel("chat");
const { user_id } = main.UI.useValues(main.STORE_ID);

const handleGroupCreated = useCallback(
(newGroupId: string) => {
// Don't update sessionId — keep current one so useChat stays alive for the in-flight stream
setGroupId(newGroupId);
},
[setGroupId],
Expand All @@ -43,19 +34,6 @@ export function ChatView() {
onGroupCreated: handleGroupCreated,
});

const handleNewChat = useCallback(() => {
setGroupId(undefined);
setSessionId(id());
}, [setGroupId]);

const handleSelectChat = useCallback(
(selectedGroupId: string) => {
setGroupId(selectedGroupId);
setSessionId(selectedGroupId);
},
[setGroupId],
);

return (
<div
className={cn([
Expand All @@ -65,8 +43,8 @@ export function ChatView() {
>
<ChatHeader
currentChatGroupId={groupId}
onNewChat={handleNewChat}
onSelectChat={handleSelectChat}
onNewChat={startNewChat}
onSelectChat={selectChat}
handleClose={() => chat.sendEvent({ type: "CLOSE" })}
/>
{user_id && (
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/chat/components/context-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export function ContextBar({
[entities],
);

if (chips.length === 0 && !onAddEntity) {
if (chips.length === 0) {
return null;
}

Expand Down
11 changes: 9 additions & 2 deletions apps/desktop/src/chat/components/input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function ChatMessageInput({
isRightPanel={chat.mode === "RightPanelOpen"}
>
<div
data-chat-message-input
className={cn([
"flex flex-col pt-3 pb-2",
chat.mode === "RightPanelOpen" ? "px-2" : "px-2",
Expand Down Expand Up @@ -148,8 +149,14 @@ function Container({
<div
className={cn([
"flex max-h-full flex-col border border-neutral-200 bg-white",
isRightPanel ? "rounded-t-xl rounded-b-none" : "rounded-b-xl",
hasContextBar && "rounded-t-none border-t-0",
isRightPanel
? hasContextBar
? "rounded-t-none rounded-b-none"
: "rounded-t-xl rounded-b-none"
: hasContextBar
? "rounded-t-none rounded-b-xl"
: "rounded-xl",
hasContextBar && "border-t-0",
])}
>
{children}
Expand Down
12 changes: 11 additions & 1 deletion apps/desktop/src/chat/components/message/normal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,24 @@ function Part({ part }: { part: Part }) {
}

function Reasoning({ part }: { part: Extract<Part, { type: "reasoning" }> }) {
const cleaned = part.text
const raw = part.text.trim();

if (!raw) {
return null;
}

const cleaned = raw
.replace(/[\n`*#"]/g, " ")
.replace(/\s+/g, " ")
.trim();

const streaming = part.state !== "done";
const title = streaming ? cleaned.slice(-150) : cleaned;

if (!title) {
return null;
}

return (
<Disclosure
icon={<BrainIcon className="h-3 w-3" />}
Expand Down
25 changes: 25 additions & 0 deletions apps/desktop/src/chat/components/shared.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, test } from "vitest";

import { hasRenderableContent } from "./shared";

describe("hasRenderableContent", () => {
test("returns false for blank reasoning-only messages", () => {
expect(
hasRenderableContent({
id: "message-1",
role: "assistant",
parts: [{ type: "reasoning", text: " ", state: "done" }],
}),
).toBe(false);
});

test("returns true for non-empty reasoning messages", () => {
expect(
hasRenderableContent({
id: "message-2",
role: "assistant",
parts: [{ type: "reasoning", text: "Thinking", state: "done" }],
}),
).toBe(true);
});
});
12 changes: 11 additions & 1 deletion apps/desktop/src/chat/components/shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import type { HyprUIMessage } from "~/chat/types";

export function hasRenderableContent(message: HyprUIMessage): boolean {
return message.parts.some((part) => part.type !== "step-start");
return message.parts.some((part) => {
if (part.type === "step-start") {
return false;
}

if (part.type === "reasoning") {
return part.text.trim().length > 0;
}

return true;
});
}
33 changes: 33 additions & 0 deletions apps/desktop/src/chat/state/chat-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { beforeEach, describe, expect, test } from "vitest";

import { useChatContext } from "./chat-context";

describe("chat context", () => {
beforeEach(() => {
useChatContext.setState({
groupId: undefined,
sessionId: "session-initial",
});
});

test("startNewChat resets the group and rotates the session id", () => {
useChatContext.setState({
groupId: "group-1",
sessionId: "session-1",
});

useChatContext.getState().startNewChat();

const state = useChatContext.getState();
expect(state.groupId).toBeUndefined();
expect(state.sessionId).not.toBe("session-1");
});

test("selectChat syncs the selected group and session id", () => {
useChatContext.getState().selectChat("group-2");

const state = useChatContext.getState();
expect(state.groupId).toBe("group-2");
expect(state.sessionId).toBe("group-2");
});
});
8 changes: 8 additions & 0 deletions apps/desktop/src/chat/state/chat-context.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { create } from "zustand";

import { id } from "~/shared/utils";

interface ChatContextState {
groupId: string | undefined;
sessionId: string;
}

interface ChatContextActions {
setGroupId: (groupId: string | undefined) => void;
startNewChat: () => void;
selectChat: (groupId: string) => void;
}

export const useChatContext = create<ChatContextState & ChatContextActions>(
(set) => ({
groupId: undefined,
sessionId: id(),
setGroupId: (groupId) => set({ groupId }),
startNewChat: () => set({ groupId: undefined, sessionId: id() }),
selectChat: (groupId) => set({ groupId, sessionId: groupId }),
}),
);
6 changes: 6 additions & 0 deletions apps/desktop/src/chat/state/use-chat-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export function useChatMode() {
const transitionChatMode = useTabs((state) => state.transitionChatMode);

const groupId = useChatContext((state) => state.groupId);
const sessionId = useChatContext((state) => state.sessionId);
const setGroupId = useChatContext((state) => state.setGroupId);
const startNewChat = useChatContext((state) => state.startNewChat);
const selectChat = useChatContext((state) => state.selectChat);

useHotkeys(
"mod+j",
Expand All @@ -28,6 +31,9 @@ export function useChatMode() {
mode,
sendEvent: transitionChatMode,
groupId,
sessionId,
setGroupId,
startNewChat,
selectChat,
};
}
26 changes: 25 additions & 1 deletion apps/desktop/src/shared/main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,11 @@ function useTabsShortcuts() {
useHotkeys(
"mod+n",
() => {
if (isPersistentChatInputFocused(chat.mode)) {
chat.startNewChat();
return;
}

if (currentTab?.type === "empty") {
newNoteCurrent();
} else {
Expand All @@ -948,7 +953,7 @@ function useTabsShortcuts() {
enableOnFormTags: true,
enableOnContentEditable: true,
},
[currentTab, newNote, newNoteCurrent],
[chat, currentTab, newNote, newNoteCurrent],
);

useHotkeys(
Expand Down Expand Up @@ -1129,3 +1134,22 @@ function useNewEmptyTab() {

return handler;
}

function isPersistentChatInputFocused(
mode: ReturnType<typeof useShell>["chat"]["mode"],
) {
if (mode !== "FloatingOpen" && mode !== "RightPanelOpen") {
return false;
}

if (typeof document === "undefined") {
return false;
}

const activeElement = document.activeElement;
if (!(activeElement instanceof HTMLElement)) {
return false;
}

return activeElement.closest("[data-chat-message-input]") !== null;
}
Loading