Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
77 changes: 75 additions & 2 deletions apps/desktop/src/chat/context-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { SessionContext } from "@hypr/plugin-template";
import type { HyprUIMessage } from "./types";
import { isRecord } from "./utils";

export const CURRENT_SESSION_CONTEXT_KEY = "session:current";

export type ContextEntitySource = "tool";

export type ContextEntity =
Expand Down Expand Up @@ -58,15 +60,16 @@ function parseSearchSessionsOutput(output: unknown): ContextEntity[] {
return [];
}

const parsedSessionContext = parseSessionContext(item.sessionContext);
const title = typeof item.title === "string" ? item.title : null;
const content = typeof item.content === "string" ? item.content : null;
const content = typeof item.excerpt === "string" ? item.excerpt : null;

return [
{
kind: "session",
key: `session:search:${item.id}`,
source: "tool",
sessionContext: {
sessionContext: parsedSessionContext ?? {
title,
date: null,
rawContent: content,
Expand All @@ -81,6 +84,76 @@ function parseSearchSessionsOutput(output: unknown): ContextEntity[] {
});
}

function parseSessionContext(value: unknown): SessionContext | null {
if (!isRecord(value)) {
return null;
}

const title = typeof value.title === "string" ? value.title : null;
const date = typeof value.date === "string" ? value.date : null;
const rawContent =
typeof value.rawContent === "string" ? value.rawContent : null;
const enhancedContent =
typeof value.enhancedContent === "string" ? value.enhancedContent : null;

const participants = Array.isArray(value.participants)
? value.participants.flatMap((participant) => {
if (!isRecord(participant) || typeof participant.name !== "string") {
return [];
}
return [
{
name: participant.name,
jobTitle:
typeof participant.jobTitle === "string"
? participant.jobTitle
: null,
},
];
})
: [];

const event =
isRecord(value.event) && typeof value.event.name === "string"
? { name: value.event.name }
: null;

const transcript = isRecord(value.transcript)
? {
segments: Array.isArray(value.transcript.segments)
? value.transcript.segments.flatMap((segment) => {
if (
!isRecord(segment) ||
typeof segment.speaker !== "string" ||
typeof segment.text !== "string"
) {
return [];
}
return [{ speaker: segment.speaker, text: segment.text }];
})
: [],
startedAt:
typeof value.transcript.startedAt === "number"
? value.transcript.startedAt
: null,
endedAt:
typeof value.transcript.endedAt === "number"
? value.transcript.endedAt
: null,
}
: null;

return {
title,
date,
rawContent,
enhancedContent,
transcript,
participants,
event,
};
}

const toolEntityExtractors: Record<
string,
(output: unknown) => ContextEntity[]
Expand Down
28 changes: 0 additions & 28 deletions apps/desktop/src/chat/context/device-info.ts

This file was deleted.

94 changes: 61 additions & 33 deletions apps/desktop/src/chat/context/prompt-context.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,78 @@
import type { SessionContext } from "@hypr/plugin-template";

import type { ContextEntity } from "../context-item";

type SessionEntity = Extract<ContextEntity, { kind: "session" }>;
import {
type ContextEntity,
CURRENT_SESSION_CONTEXT_KEY,
} from "../context-item";

export type ChatSystemContext = {
context: SessionContext | null;
relatedSessions: SessionContext[];
};

// Tool-derived entities (e.g. search_sessions results) are already visible to
// the model through message history. Excluding them from the system prompt
// avoids duplicate/competing context and keeps the prompt focused.
export function filterForPrompt(entities: ContextEntity[]): ContextEntity[] {
return entities.filter((e) => e.source !== "tool");
}

function toSessionContext(entity: SessionEntity): SessionContext {
const { sessionContext } = entity;
return {
title: sessionContext.title,
date: sessionContext.date,
rawContent: sessionContext.rawContent,
enhancedContent: sessionContext.enhancedContent,
transcript: sessionContext.transcript,
participants: sessionContext.participants,
event: sessionContext.event,
};
export function getPersistableContextEntities(
entities: ContextEntity[],
): ContextEntity[] {
return entities.filter((entity) => entity.source !== "tool");
}

export function buildChatSystemContext(
entities: ContextEntity[],
): ChatSystemContext {
const sessions = entities.filter(
(e): e is SessionEntity => e.kind === "session",
const sessionEntities = entities.filter(
(entity): entity is Extract<ContextEntity, { kind: "session" }> =>
entity.kind === "session",
);

const primaryIdx = sessions.findIndex((s) => s.key === "session:info");
const primary = primaryIdx !== -1 ? sessions[primaryIdx] : null;

const relatedSessions = sessions
.filter((_, i) => i !== primaryIdx)
.map(toSessionContext);
const primary =
sessionEntities.find(
(entity) => entity.key === CURRENT_SESSION_CONTEXT_KEY,
) ?? sessionEntities[0];

return {
context: primary ? toSessionContext(primary) : null,
relatedSessions,
context: primary?.sessionContext ?? null,
};
}

export function stableContextFingerprint(entities: ContextEntity[]): string {
const serialize = (value: unknown): string => {
if (Array.isArray(value)) {
return `[${value.map((item) => serialize(item)).join(",")}]`;
}
if (value && typeof value === "object") {
const entries = Object.entries(value as Record<string, unknown>).sort(
([a], [b]) => a.localeCompare(b),
);
return `{${entries
.map(([key, val]) => `${JSON.stringify(key)}:${serialize(val)}`)
.join(",")}}`;
}
return JSON.stringify(value);
};

return serialize(
entities.map((entity) => ({
kind: entity.kind,
key: entity.key,
source: entity.source ?? null,
removable: "removable" in entity ? (entity.removable ?? false) : false,
payload:
entity.kind === "session"
? entity.sessionContext
: entity.kind === "account"
? {
userId: entity.userId ?? null,
email: entity.email ?? null,
fullName: entity.fullName ?? null,
avatarUrl: entity.avatarUrl ?? null,
stripeCustomerId: entity.stripeCustomerId ?? null,
}
: {
platform: entity.platform ?? null,
arch: entity.arch ?? null,
osVersion: entity.osVersion ?? null,
appVersion: entity.appVersion ?? null,
buildHash: entity.buildHash ?? null,
locale: entity.locale ?? null,
},
})),
);
}
2 changes: 1 addition & 1 deletion apps/desktop/src/chat/context/support-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function collectSupportContextBlock(): Promise<{
}

const result = await templateCommands.renderSupport({
supportSystem: { account: accountInfo, device: deviceInfo },
supportContext: { account: accountInfo, device: deviceInfo },
});

return {
Expand Down
Loading
Loading