Skip to content
Open
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
21 changes: 18 additions & 3 deletions apps/desktop/src/components/chat/message/tool/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ function RenderContent({ part }: { part: Part }) {

function RenderSession({ sessionId }: { sessionId: string }) {
const session = main.UI.useRow("sessions", sessionId, main.STORE_ID);
const enhancedNoteIds = main.UI.useSliceRowIds(
main.INDEXES.enhancedNotesBySession,
sessionId,
main.STORE_ID,
);
const firstEnhancedNoteId = enhancedNoteIds?.[0];
const enhancedNoteContent = main.UI.useCell(
"enhanced_notes",
firstEnhancedNoteId ?? "",
"content",
main.STORE_ID,
);
const openNew = useTabs((state) => state.openNew);

const handleClick = useCallback(() => {
Expand All @@ -112,14 +124,17 @@ function RenderSession({ sessionId }: { sessionId: string }) {
);
}

const displayContent =
typeof enhancedNoteContent === "string" && enhancedNoteContent
? enhancedNoteContent
: session.raw_md;

return (
<div className="text-xs flex flex-col gap-1" onClick={handleClick}>
<span className="font-medium truncate">
{session.title || "Untitled"}
</span>
<span className="text-muted-foreground truncate">
{session.enhanced_md ?? session.raw_md}
</span>
<span className="text-muted-foreground truncate">{displayContent}</span>
</div>
);
}
3 changes: 0 additions & 3 deletions apps/desktop/src/components/devtool/seed/data/curated.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@
{
"title": "Q4 Strategy Meeting",
"raw_md": "## Q4 Product Strategy\n\nAttendees: John Doe, Sarah Chen, Michael Rodriguez\n\nDiscussed Q4 roadmap, feature priorities, resource allocation, and timeline for product releases...",
"enhanced_md": "# Key Decisions\n- Slack integration prioritized as phase 1 (3 weeks)\n- Performance optimization parallel track (2 weeks profiling + 4-8 weeks optimization)\n- Salesforce integration phase 2 (6 weeks)\n- Resource allocation: 2 senior engineers + 1 junior for integrations\n- Board approval needed by end of week\n\n<p></p>\n\n# Market Insights\n- 2000+ customer survey respondents\n- 78% want better integrations\n- 65% concerned about performance\n- Enterprise pricing model needs adjustment\n\n<p></p>\n\n# Action Items\n- Michael: Technical spec by Nov 5 (responsible)\n- Sarah: Sales constraints analysis (responsible)\n- John: Board coordination for resource approval",
"folder": "Q4 Planning",
"event": "Q4 Product Strategy Planning",
"participants": [
Expand Down Expand Up @@ -1478,7 +1477,6 @@
{
"title": "TechStart Partnership Call",
"raw_md": "## Partnership Discussion\n\nAttendees: John Doe, Emma Watson, David Kim\n\nExplored strategic partnership opportunities between Acme Corp and TechStart Labs for API integration and co-marketing...",
"enhanced_md": "# Strategic Partnership Framework\n\n### Partnership Proposal\n- API integration: TechStart ML capabilities + Acme data processing\n- Revenue model: 30% for TechStart on referrals (70% Acme)\n- Joint go-to-market to mid-market enterprises\n\n<p></p>\n\n### Technical Integration Plan\n- Authentication: OAuth 2.0 (both systems ready)\n- Data sync: Real-time webhooks\n- Rate limits: 1000 req/min initially\n- Security: Mutual TLS for server-to-server\n- Regions: US-primary, EU secondary\n- POC timeline: 3-4 weeks\n\n<p></p>\n\n### Next Steps\n- Legal review: 2 weeks\n- Technical POC: 3-4 weeks (parallel)\n- Pilot program: 6 weeks total",
"folder": "Projects",
"event": null,
"participants": [
Expand Down Expand Up @@ -2675,7 +2673,6 @@
{
"title": "Quick Check-in",
"raw_md": "Quick conversation between colleagues.",
"enhanced_md": "Quick conversation between colleagues.",
"folder": null,
"event": null,
"participants": [
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/components/devtool/seed/data/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ export const loadCuratedData = (data: CuratedData): Tables<Schemas[0]> => {
user_id: DEFAULT_USER_ID,
title: session.title,
raw_md: JSON.stringify(md2json(session.raw_md)),
enhanced_md: JSON.stringify(md2json(session.enhanced_md)),
created_at: new Date().toISOString(),
event_id: eventId,
folder_id: folderId,
Expand Down
4 changes: 0 additions & 4 deletions apps/desktop/src/components/devtool/seed/data/schema.gen.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,6 @@
"raw_md": {
"type": "string"
},
"enhanced_md": {
"type": "string"
},
"folder": {
"anyOf": [
{
Expand Down Expand Up @@ -300,7 +297,6 @@
"required": [
"title",
"raw_md",
"enhanced_md",
"folder",
"event",
"participants",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/components/devtool/seed/data/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ const CuratedEventSchema = z.object({
const CuratedSessionSchema = z.object({
title: z.string(),
raw_md: z.string(),
enhanced_md: z.string(),
folder: z.string().nullable(),
event: z.string().nullable(),
participants: z.array(z.string()),
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/components/devtool/seed/shared/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,13 @@ export const createSession = (
faker.number.int({ min: 2, max: 5 }),
"\n\n",
);
const enhanced_md = generateEnhancedMarkdown();

return {
id: id(),
data: {
user_id: DEFAULT_USER_ID,
title,
raw_md: JSON.stringify(md2json(raw_md)),
enhanced_md: JSON.stringify(md2json(enhanced_md)),
created_at: faker.date.recent({ days: 30 }).toISOString(),
event_id: eventId,
folder_id: folderId,
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/contexts/search/engine/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { flattenTranscript, mergeContent } from "./utils";

export function createSessionSearchableContent(
row: Record<string, unknown>,
enhancedContent?: string,
): string {
return mergeContent([
row.raw_md,
row.enhanced_md,
enhancedContent,
flattenTranscript(row.transcript),
]);
}
Expand Down
12 changes: 9 additions & 3 deletions apps/desktop/src/contexts/search/engine/indexing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
createSessionSearchableContent,
} from "./content";
import type { Index } from "./types";
import { collectCells, toNumber, toTrimmedString } from "./utils";
import {
collectCells,
getEnhancedContentForSession,
toNumber,
toTrimmedString,
} from "./utils";

export function indexSessions(db: Index, store: PersistedStore): void {
const fields = [
Expand All @@ -16,19 +21,20 @@ export function indexSessions(db: Index, store: PersistedStore): void {
"event_id",
"title",
"raw_md",
"enhanced_md",
"transcript",
];

store.forEachRow("sessions", (rowId: string, _forEachCell) => {
const row = collectCells(store, "sessions", rowId, fields);
const title = toTrimmedString(row.title) || "Untitled";

const enhancedContent = getEnhancedContentForSession(store, rowId);

void insert(db, {
id: rowId,
type: "session",
title,
content: createSessionSearchableContent(row),
content: createSessionSearchableContent(row, enhancedContent),
created_at: toNumber(row.created_at),
});
});
Expand Down
18 changes: 13 additions & 5 deletions apps/desktop/src/contexts/search/engine/listeners.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { remove, type TypedDocument, update } from "@orama/orama";
import { RowListener } from "tinybase/with-schemas";

import { Schemas } from "../../../store/tinybase/main";
import { type Store as PersistedStore } from "../../../store/tinybase/main";
import {
type Store as PersistedStore,
Schemas,
} from "../../../store/tinybase/main";
import {
createHumanSearchableContent,
createSessionSearchableContent,
} from "./content";
import type { Index } from "./types";
import { collectCells, toNumber, toTrimmedString } from "./utils";
import {
collectCells,
getEnhancedContentForSession,
toNumber,
toTrimmedString,
} from "./utils";

export function createSessionListener(
index: Index,
Expand All @@ -25,17 +32,18 @@ export function createSessionListener(
"created_at",
"title",
"raw_md",
"enhanced_md",
"transcript",
];
const row = collectCells(store, "sessions", rowId, fields);
const title = toTrimmedString(row.title) || "Untitled";

const enhancedContent = getEnhancedContentForSession(store, rowId);

const data: TypedDocument<Index> = {
id: rowId,
type: "session",
title,
content: createSessionSearchableContent(row),
content: createSessionSearchableContent(row, enhancedContent),
created_at: toNumber(row.created_at),
};

Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/src/contexts/search/engine/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { type Store as PersistedStore } from "../../../store/tinybase/main";

const SPACE_REGEX = /\s+/g;

export function safeParseJSON(value: unknown): unknown {
Expand Down Expand Up @@ -110,3 +112,20 @@ export function collectCells(
return acc;
}, {});
}

export function getEnhancedContentForSession(
store: PersistedStore,
sessionId: string,
): string {
const contents: string[] = [];
store.forEachRow("enhanced_notes", (rowId: string, _forEachCell) => {
const noteSessionId = store.getCell("enhanced_notes", rowId, "session_id");
if (noteSessionId === sessionId) {
const content = store.getCell("enhanced_notes", rowId, "content");
if (typeof content === "string" && content) {
contents.push(content);
}
}
});
return contents.join(" ");
}
Comment on lines +116 to +131
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add type checking for session_id comparison.

The function correctly validates that content is a non-empty string before adding it to the array. However, the session_id comparison on line 122 doesn't include a type check. If noteSessionId is undefined or a different type, the comparison may not work as expected.

Consider adding a type check:

 export function getEnhancedContentForSession(
   store: PersistedStore,
   sessionId: string,
 ): string {
   const contents: string[] = [];
   store.forEachRow("enhanced_notes", (rowId: string, _forEachCell) => {
     const noteSessionId = store.getCell("enhanced_notes", rowId, "session_id");
-    if (noteSessionId === sessionId) {
+    if (typeof noteSessionId === "string" && noteSessionId === sessionId) {
       const content = store.getCell("enhanced_notes", rowId, "content");
       if (typeof content === "string" && content) {
         contents.push(content);
       }
     }
   });
   return contents.join(" ");
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getEnhancedContentForSession(
store: PersistedStore,
sessionId: string,
): string {
const contents: string[] = [];
store.forEachRow("enhanced_notes", (rowId: string, _forEachCell) => {
const noteSessionId = store.getCell("enhanced_notes", rowId, "session_id");
if (noteSessionId === sessionId) {
const content = store.getCell("enhanced_notes", rowId, "content");
if (typeof content === "string" && content) {
contents.push(content);
}
}
});
return contents.join(" ");
}
export function getEnhancedContentForSession(
store: PersistedStore,
sessionId: string,
): string {
const contents: string[] = [];
store.forEachRow("enhanced_notes", (rowId: string, _forEachCell) => {
const noteSessionId = store.getCell("enhanced_notes", rowId, "session_id");
if (typeof noteSessionId === "string" && noteSessionId === sessionId) {
const content = store.getCell("enhanced_notes", rowId, "content");
if (typeof content === "string" && content) {
contents.push(content);
}
}
});
return contents.join(" ");
}
🤖 Prompt for AI Agents
In apps/desktop/src/contexts/search/engine/utils.ts around lines 116 to 131, the
code compares noteSessionId to sessionId without confirming noteSessionId is a
string; add a type check before the comparison to avoid unexpected matches or
runtime issues (e.g., if noteSessionId is undefined or a number). Update the
loop so you retrieve noteSessionId, verify typeof noteSessionId === "string" (or
otherwise coerce safely) and only then compare with sessionId using strict
equality; keep the existing content type check and push logic unchanged.

10 changes: 6 additions & 4 deletions apps/desktop/src/store/tinybase/localPersister2.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { createCustomPersister } from "tinybase/persisters/with-schemas";
import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";

import { Session } from "./schema-external";
import { EnhancedNote } from "./schema-external";

// https://tinybase.org/api/persisters/functions/creation/createcustompersister
export function createLocalPersister2<Schemas extends OptionalSchemas>(
store: MergeableStore<Schemas>,
handlePersist: (session: Session & { id: string }) => Promise<void>,
handlePersist: (enhancedNote: EnhancedNote & { id: string }) => Promise<void>,
) {
return createCustomPersister(
store,
Expand All @@ -16,11 +16,13 @@ export function createLocalPersister2<Schemas extends OptionalSchemas>(
async (getContent, _changes) => {
const [tables, _values] = getContent();

Object.entries(tables?.sessions ?? {}).forEach(([id, row]) => {
const promises: Promise<void>[] = [];
Object.entries(tables?.enhanced_notes ?? {}).forEach(([id, row]) => {
// @ts-ignore
row.id = id;
handlePersist(row as Session & { id: string });
promises.push(handlePersist(row as EnhancedNote & { id: string }));
});
await Promise.all(promises);
},
(listener) => setInterval(listener, 1000),
(interval) => clearInterval(interval),
Expand Down
38 changes: 33 additions & 5 deletions apps/desktop/src/store/tinybase/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {

import { TABLE_HUMANS, TABLE_SESSIONS } from "@hypr/db";
import { getCurrentWebviewWindowLabel } from "@hypr/plugin-windows";
import { isValidTiptapContent, json2md } from "@hypr/tiptap/shared";
import { format } from "@hypr/utils";

import { DEFAULT_USER_ID } from "../../utils";
Expand Down Expand Up @@ -145,7 +146,6 @@ export const StoreComponent = ({ persist = true }: { persist?: boolean }) => {
created_at: now,
title: "Welcome to Hyprnote",
raw_md: "",
enhanced_md: "",
});
}
});
Expand Down Expand Up @@ -176,15 +176,43 @@ export const StoreComponent = ({ persist = true }: { persist?: boolean }) => {

const persister = createLocalPersister2<Schemas>(
store as Store,
async (session) => {
if (session.enhanced_md) {
async (enhancedNote) => {
if (!enhancedNote.content || !enhancedNote.session_id) {
return;
}

try {
const parsed = JSON.parse(enhancedNote.content);
if (!isValidTiptapContent(parsed)) {
return;
}

const markdown = json2md(parsed);
const sessionDir = `hyprnote/sessions/${enhancedNote.session_id}`;

const sessionDirExists = await exists(sessionDir, {
baseDir: BaseDirectory.Data,
});
if (!sessionDirExists) {
await mkdir(sessionDir, {
baseDir: BaseDirectory.Data,
recursive: true,
});
}

await writeTextFile(
`hyprnote/sessions/${session.id}.md`,
session.enhanced_md,
`${sessionDir}/${enhancedNote.id}.md`,
markdown,
{
baseDir: BaseDirectory.Data,
},
);
} catch (error) {
console.error(
"Failed to save enhanced note markdown:",
enhancedNote.id,
error,
);
}
},
);
Expand Down
13 changes: 7 additions & 6 deletions apps/desktop/src/store/tinybase/schema-external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ export const folderSchema = baseFolderSchema.omit({ id: true }).extend({
),
});

export const sessionSchema = baseSessionSchema.omit({ id: true }).extend({
created_at: z.string(),
event_id: z.preprocess((val) => val ?? undefined, z.string().optional()),
folder_id: z.preprocess((val) => val ?? undefined, z.string().optional()),
});
export const sessionSchema = baseSessionSchema
.omit({ id: true, enhanced_md: true })
.extend({
created_at: z.string(),
event_id: z.preprocess((val) => val ?? undefined, z.string().optional()),
folder_id: z.preprocess((val) => val ?? undefined, z.string().optional()),
});

export const transcriptSchema = baseTranscriptSchema.omit({ id: true }).extend({
created_at: z.string(),
Expand Down Expand Up @@ -192,7 +194,6 @@ export const externalTableSchemaForTinybase = {
event_id: { type: "string" },
title: { type: "string" },
raw_md: { type: "string" },
enhanced_md: { type: "string" },
} satisfies InferTinyBaseSchema<typeof sessionSchema>,
transcripts: {
user_id: { type: "string" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ async function transformArgs(
}

function readEnhancedMarkdown(store: MainStore, sessionId: string): string {
const value = store.getCell("sessions", sessionId, "enhanced_md");
return typeof value === "string" ? value : "";
const contents: string[] = [];
store.forEachRow("enhanced_notes", (rowId, _forEachCell) => {
const noteSessionId = store.getCell("enhanced_notes", rowId, "session_id");
if (noteSessionId === sessionId) {
const content = store.getCell("enhanced_notes", rowId, "content");
if (typeof content === "string" && content) {
contents.push(content);
}
}
});
return contents.join("\n\n");
}
Loading
Loading