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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { commands as miscCommands } from "@hypr/plugin-misc";
import Renderer from "@hypr/tiptap/renderer";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@hypr/ui/components/ui/accordion";
import { PencilRuler } from "lucide-react";
import { useEffect, useState } from "react";
import { MarkdownCard } from "./markdown-card";
Expand All @@ -12,6 +13,52 @@ interface MessageContentProps {
onApplyMarkdown?: (markdownContent: string) => void;
}

function ToolDetailsRenderer({ details }: { details: any }) {
if (!details) {
return (
<div
style={{
color: "rgb(156 163 175)",
fontSize: "0.75rem",
fontStyle: "italic",
paddingLeft: "24px",
}}
>
No details available...
</div>
);
}

return (
<div
style={{
paddingLeft: "24px",
fontSize: "0.75rem",
color: "rgb(75 85 99)",
}}
>
<pre
style={{
backgroundColor: "transparent",
border: "none",
borderRadius: "6px",
padding: "8px 12px",
margin: 0,
fontSize: "0.6875rem",
fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
maxHeight: "200px",
overflow: "auto",
lineHeight: 1.4,
}}
>
{typeof details === 'object' ? JSON.stringify(details, null, 2) : String(details)}
</pre>
</div>
);
}

function MarkdownText({ content }: { content: string }) {
const [htmlContent, setHtmlContent] = useState<string>("");

Expand Down Expand Up @@ -124,31 +171,71 @@ function MarkdownText({ content }: { content: string }) {

export function MessageContent({ message, sessionTitle, hasEnhancedNote, onApplyMarkdown }: MessageContentProps) {
if (message.type === "tool-start") {
return (
<div
style={{
backgroundColor: "rgb(250 250 250)",
border: "1px solid rgb(229 229 229)",
borderRadius: "6px",
padding: "12px 16px",
}}
>
const hasToolDetails = message.toolDetails;

if (hasToolDetails) {
return (
<div
style={{
color: "rgb(115 115 115)",
fontSize: "0.875rem",
display: "flex",
alignItems: "center",
gap: "8px",
backgroundColor: "rgb(250 250 250)",
border: "1px solid rgb(229 229 229)",
borderRadius: "6px",
padding: "12px 16px",
}}
>
<PencilRuler size={16} color="rgb(115 115 115)" />
<span style={{ fontWeight: "400" }}>
Called tool: {message.content}
</span>
<Accordion type="single" collapsible className="border-none">
<AccordionItem value="tool-start-details" className="border-none">
<AccordionTrigger className="hover:no-underline p-0 h-auto [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-gray-400">
<div
style={{
color: "rgb(115 115 115)",
fontSize: "0.875rem",
display: "flex",
alignItems: "center",
gap: "8px",
width: "100%",
}}
>
<PencilRuler size={16} color="rgb(115 115 115)" />
<span style={{ fontWeight: "400", flex: 1, textAlign: "left" }}>
Called tool: {message.content}
</span>
</div>
</AccordionTrigger>
<AccordionContent className="pt-3 pb-0">
<ToolDetailsRenderer details={message.toolDetails} />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
);
);
} else {
return (
<div
style={{
backgroundColor: "rgb(250 250 250)",
border: "1px solid rgb(229 229 229)",
borderRadius: "6px",
padding: "12px 16px",
}}
>
<div
style={{
color: "rgb(115 115 115)",
fontSize: "0.875rem",
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<PencilRuler size={16} color="rgb(115 115 115)" />
<span style={{ fontWeight: "400" }}>
Called tool: {message.content}
</span>
</div>
</div>
);
}
}

if (message.type === "tool-result") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Message {
isUser: boolean;
timestamp: Date;
type: "text-delta" | "tool-start" | "tool-result" | "tool-error" | "generating";
toolDetails?: any;
}

export type ChatSession = {
Expand Down
10 changes: 9 additions & 1 deletion apps/desktop/src/components/right-panel/hooks/useChatLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export function useChatLogic({
role: "User",
content: userMessage.content.trim(),
type: "text-delta",
tool_details: null,
});

const aiMessageId = crypto.randomUUID();
Expand Down Expand Up @@ -340,7 +341,7 @@ export function useChatLogic({
userId,
apiBase,
),
stopWhen: stepCountIs(3),
stopWhen: stepCountIs(5),
tools: {
...(type === "HyprLocal" && { update_progress: tool({ inputSchema: z.any() }) }),
...(shouldUseTools && { ...newMcpTools, search_sessions_multi_keywords: searchTool, ...hyprMcpTools }),
Expand Down Expand Up @@ -428,6 +429,7 @@ export function useChatLogic({
role: "Assistant",
type: "text-delta",
content: aiResponse.trim(),
tool_details: null,
});
} catch (error) {
console.error("Failed to save AI text:", error);
Expand All @@ -445,6 +447,7 @@ export function useChatLogic({
isUser: false,
timestamp: new Date(),
type: "tool-start",
toolDetails: chunk.input,
};
setMessages((prev) => [...prev, toolStartMessage]);

Expand All @@ -456,6 +459,7 @@ export function useChatLogic({
role: "Assistant",
content: toolStartMessage.content,
type: "tool-start",
tool_details: JSON.stringify(chunk.input),
});
}

Expand All @@ -479,6 +483,7 @@ export function useChatLogic({
role: "Assistant",
content: toolResultMessage.content,
type: "tool-result",
tool_details: null,
});
}

Expand All @@ -500,6 +505,7 @@ export function useChatLogic({
role: "Assistant",
content: toolErrorMessage.content,
type: "tool-error",
tool_details: null,
});
}

Expand All @@ -514,6 +520,7 @@ export function useChatLogic({
role: "Assistant",
type: "text-delta",
content: aiResponse.trim(),
tool_details: null,
});
}

Expand Down Expand Up @@ -570,6 +577,7 @@ export function useChatLogic({
role: "Assistant",
content: finalErrorMessage,
type: "text-delta",
tool_details: null,
});
}
};
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/components/right-panel/hooks/useChatQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ export function useChatQueries({
timestamp: new Date(msg.created_at),
type: msg.type || "text-delta",
parts: msg.role === "Assistant" ? parseMarkdownBlocks(msg.content) : undefined,
toolDetails: msg.type === "tool-start" && msg.tool_details
? (() => {
try {
return JSON.parse(msg.tool_details);
} catch (error) {
console.error("Failed to parse tool_details for tool-start:", msg.id, error);
return undefined;
}
})()
: undefined,
}));
},
});
Expand Down
4 changes: 4 additions & 0 deletions crates/db-user/src/chat_messages_migration_2.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE
chat_messages
ADD
COLUMN tool_details TEXT DEFAULT NULL;
Comment on lines +1 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure we persist NULL (not empty string) when there are no tool details.

Downstream code appears to JSON-parse this field; an empty string would throw. Please confirm that inserts bind NULL for missing values rather than "".

If chat_messages_ops.rs currently uses unwrap_or_default(), prefer passing the Option through so the driver writes NULL:

// Before (risk: "" gets stored)
tool_details: message.tool_details.unwrap_or_default(),

// After (store NULL when absent)
tool_details: message.tool_details.as_deref()

Run to verify call sites:


🏁 Script executed:

#!/bin/bash
rg -n "tool_details.*unwrap_or_default\(\)|unwrap_or\(\"\"\)" -C2
rg -n "INSERT.*chat_messages" -C3 crates/db-user/src/chat_messages_ops.rs

Length of output: 3741


Persist NULL for missing tool_details instead of empty string

The current implementation in crates/db-user/src/chat_messages_ops.rs binds an empty string when message.tool_details is None, causing '' to be stored rather than SQL NULL. Downstream JSON parsing will fail on an empty string. Please update the binding to propagate the Option so that the driver writes NULL when there are no tool details.

• Location: crates/db-user/src/chat_messages_ops.rs:29
• Current code binds unwrap_or_default(), which is "" for None.
• Change to as_deref() to pass Option<&str> through.

Suggested diff:

-                    &message.tool_details.unwrap_or_default(),
+                    &message.tool_details.as_deref(),

This ensures:

  • Some(s) → binds s
  • None → binds SQL NULL
🤖 Prompt for AI Agents
In crates/db-user/src/chat_messages_ops.rs around line 29, the code currently
binds message.tool_details using unwrap_or_default() which writes an empty
string instead of SQL NULL; replace the unwrap_or_default() call with as_deref()
so you pass an Option<&str> to the query binder (Some(s) binds s, None binds SQL
NULL). Ensure the prepared statement parameter type accepts Option<&str> (no
further transformation) and remove any .unwrap_or_default() usage for
tool_details so nulls are persisted.

6 changes: 4 additions & 2 deletions crates/db-user/src/chat_messages_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ impl UserDatabase {
created_at,
role,
content,
type
) VALUES (?, ?, ?, ?, ?, ?)
type,
tool_details
) VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING *",
vec![
message.id,
Expand All @@ -25,6 +26,7 @@ impl UserDatabase {
message.role.to_string(),
message.content,
message.r#type.to_string(),
message.tool_details.unwrap_or_default(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inserting empty string for absent tool_details loses null semantics and can break queries expecting NULL; pass SQL NULL when the Option is None for consistency with schema and existing patterns.

(Based on your team's feedback about ensuring Option fields map to SQL NULL instead of empty strings for accurate database semantics.)

Prompt for AI agents
Address the following comment on crates/db-user/src/chat_messages_ops.rs at line 29:

<comment>Inserting empty string for absent tool_details loses null semantics and can break queries expecting NULL; pass SQL NULL when the Option is None for consistency with schema and existing patterns.

(Based on your team&#39;s feedback about ensuring Option fields map to SQL NULL instead of empty strings for accurate database semantics.)</comment>

<file context>
@@ -25,6 +26,7 @@ impl UserDatabase {
                     message.role.to_string(),
                     message.content,
                     message.r#type.to_string(),
+                    message.tool_details.unwrap_or_default(),
                 ],
             )
</file context>

],
)
.await?;
Expand Down
1 change: 1 addition & 0 deletions crates/db-user/src/chat_messages_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ user_common_derives! {
pub role: ChatMessageRole,
pub content: String,
pub r#type: ChatMessageType,
pub tool_details: Option<String>,
}
}
1 change: 1 addition & 0 deletions crates/db-user/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,7 @@ pub async fn seed(db: &UserDatabase, user_id: impl Into<String>) -> Result<(), c
content: "Hello, how are you?".to_string(),
created_at: now,
r#type: ChatMessageType::TextDelta,
tool_details: None,
};

let _ = db.upsert_chat_message(chat_message_1).await?;
Expand Down
3 changes: 2 additions & 1 deletion crates/db-user/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ impl std::ops::Deref for UserDatabase {
}

// Append only. Do not reorder.
const MIGRATIONS: [&str; 23] = [
const MIGRATIONS: [&str; 24] = [
include_str!("./calendars_migration.sql"),
include_str!("./configs_migration.sql"),
include_str!("./events_migration.sql"),
Expand All @@ -153,6 +153,7 @@ const MIGRATIONS: [&str; 23] = [
include_str!("./session_participants_migration_1.sql"),
include_str!("./events_migration_2.sql"),
include_str!("./chat_messages_migration_1.sql"),
include_str!("./chat_messages_migration_2.sql"),
];

pub async fn migrate(db: &UserDatabase) -> Result<(), crate::Error> {
Expand Down
2 changes: 1 addition & 1 deletion plugins/db/js/bindings.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ async deleteTag(tagId: string) : Promise<null> {

export type Calendar = { id: string; tracking_id: string; user_id: string; platform: Platform; name: string; selected: boolean; source: string | null }
export type ChatGroup = { id: string; user_id: string; name: string | null; created_at: string; session_id: string }
export type ChatMessage = { id: string; group_id: string; created_at: string; role: ChatMessageRole; content: string; type: ChatMessageType }
export type ChatMessage = { id: string; group_id: string; created_at: string; role: ChatMessageRole; content: string; type: ChatMessageType; tool_details: string | null }
export type ChatMessageRole = "User" | "Assistant"
export type ChatMessageType = "text-delta" | "tool-start" | "tool-result" | "tool-error"
export type Config = { id: string; user_id: string; general: ConfigGeneral; notification: ConfigNotification; ai: ConfigAI }
Expand Down
Loading