feat: integrate custom features for smart context management
Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled
Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled
- Added a new integration script to manage custom features related to smart context. - Implemented handlers for smart context operations (get, update, clear, stats) in ipc. - Created a SmartContextStore class to manage context snippets and summaries. - Developed hooks for React to interact with smart context (useSmartContext, useUpdateSmartContext, useClearSmartContext, useSmartContextStats). - Included backup and restore functionality in the integration script. - Validated integration by checking for custom modifications and file existence.
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import type {
|
||||
SmartContextMeta,
|
||||
SmartContextSnippet,
|
||||
SmartContextRetrieveResult,
|
||||
} from "@/ipc/ipc_types";
|
||||
|
||||
export function useSmartContextMeta(chatId: number) {
|
||||
return useQuery<SmartContextMeta, Error>({
|
||||
queryKey: ["smart-context", chatId, "meta"],
|
||||
queryFn: async () => {
|
||||
const ipc = IpcClient.getInstance();
|
||||
return ipc.getSmartContextMeta(chatId);
|
||||
},
|
||||
enabled: !!chatId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRetrieveSmartContext(
|
||||
chatId: number,
|
||||
query: string,
|
||||
budgetTokens: number,
|
||||
) {
|
||||
return useQuery<SmartContextRetrieveResult, Error>({
|
||||
queryKey: ["smart-context", chatId, "retrieve", query, budgetTokens],
|
||||
queryFn: async () => {
|
||||
const ipc = IpcClient.getInstance();
|
||||
return ipc.retrieveSmartContext({ chatId, query, budgetTokens });
|
||||
},
|
||||
enabled: !!chatId && !!query && budgetTokens > 0,
|
||||
meta: { showErrorToast: true },
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpsertSmartContextSnippets(chatId: number) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation<number, Error, Array<Pick<SmartContextSnippet, "text" | "source">>>({
|
||||
mutationFn: async (snippets) => {
|
||||
const ipc = IpcClient.getInstance();
|
||||
return ipc.upsertSmartContextSnippets(chatId, snippets);
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["smart-context", chatId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateRollingSummary(chatId: number) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation<SmartContextMeta, Error, { summary: string }>({
|
||||
mutationFn: async ({ summary }) => {
|
||||
const ipc = IpcClient.getInstance();
|
||||
return ipc.updateSmartContextRollingSummary(chatId, summary);
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["smart-context", chatId, "meta"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Custom modules for moreminimore-vibe
|
||||
// This file exports all custom functionality to make imports easier
|
||||
|
||||
// Custom hooks
|
||||
export { useSmartContextMeta, useRetrieveSmartContext, useUpsertSmartContextSnippets, useUpdateRollingSummary } from './hooks/useSmartContext';
|
||||
|
||||
// Custom IPC handlers (these will need to be imported and registered in the main process)
|
||||
export { registerSmartContextHandlers } from './ipc/smart_context_handlers';
|
||||
|
||||
// Custom utilities
|
||||
export * from './utils/smart_context_store';
|
||||
|
||||
// Re-export types that might be needed
|
||||
export type {
|
||||
SmartContextMeta,
|
||||
SmartContextSnippet,
|
||||
SmartContextRetrieveResult,
|
||||
} from '../ipc/ipc_types';
|
||||
@@ -1,65 +0,0 @@
|
||||
import log from "electron-log";
|
||||
import { createLoggedHandler } from "./safe_handle";
|
||||
import {
|
||||
appendSnippets,
|
||||
readMeta,
|
||||
retrieveContext,
|
||||
updateRollingSummary,
|
||||
rebuildIndex,
|
||||
type SmartContextSnippet,
|
||||
type SmartContextMeta,
|
||||
} from "../utils/smart_context_store";
|
||||
|
||||
const logger = log.scope("smart_context_handlers");
|
||||
const handle = createLoggedHandler(logger);
|
||||
|
||||
export interface UpsertSnippetsParams {
|
||||
chatId: number;
|
||||
snippets: Array<{
|
||||
text: string;
|
||||
source:
|
||||
| { type: "message"; messageIndex?: number }
|
||||
| { type: "code"; filePath: string }
|
||||
| { type: "attachment"; name: string; mime?: string }
|
||||
| { type: "other"; label?: string };
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface RetrieveContextParams {
|
||||
chatId: number;
|
||||
query: string;
|
||||
budgetTokens: number;
|
||||
}
|
||||
|
||||
export function registerSmartContextHandlers() {
|
||||
handle("sc:get-meta", async (_event, chatId: number): Promise<SmartContextMeta> => {
|
||||
return readMeta(chatId);
|
||||
});
|
||||
|
||||
handle(
|
||||
"sc:upsert-snippets",
|
||||
async (_event, params: UpsertSnippetsParams): Promise<number> => {
|
||||
const count = await appendSnippets(params.chatId, params.snippets);
|
||||
return count;
|
||||
},
|
||||
);
|
||||
|
||||
handle(
|
||||
"sc:update-rolling-summary",
|
||||
async (_event, params: { chatId: number; summary: string }): Promise<SmartContextMeta> => {
|
||||
return updateRollingSummary(params.chatId, params.summary);
|
||||
},
|
||||
);
|
||||
|
||||
handle(
|
||||
"sc:retrieve-context",
|
||||
async (_event, params: RetrieveContextParams) => {
|
||||
return retrieveContext(params.chatId, params.query, params.budgetTokens);
|
||||
},
|
||||
);
|
||||
|
||||
handle("sc:rebuild-index", async (_event, chatId: number) => {
|
||||
await rebuildIndex(chatId);
|
||||
return { ok: true } as const;
|
||||
});
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import path from "node:path";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getUserDataPath } from "../../paths/paths";
|
||||
import { estimateTokens } from "./token_utils";
|
||||
|
||||
export type SmartContextSource =
|
||||
| { type: "message"; messageIndex?: number }
|
||||
| { type: "code"; filePath: string }
|
||||
| { type: "attachment"; name: string; mime?: string }
|
||||
| { type: "other"; label?: string };
|
||||
|
||||
export interface SmartContextSnippet {
|
||||
id: string;
|
||||
text: string;
|
||||
score?: number;
|
||||
source: SmartContextSource;
|
||||
ts: number; // epoch ms
|
||||
tokens?: number;
|
||||
}
|
||||
|
||||
export interface SmartContextMetaConfig {
|
||||
maxSnippets?: number;
|
||||
}
|
||||
|
||||
export interface SmartContextMeta {
|
||||
entityId: string; // e.g., chatId as string
|
||||
updatedAt: number;
|
||||
rollingSummary?: string;
|
||||
summaryTokens?: number;
|
||||
config?: SmartContextMetaConfig;
|
||||
}
|
||||
|
||||
function getThreadDir(chatId: number): string {
|
||||
const base = path.join(getUserDataPath(), "smart-context", "threads");
|
||||
return path.join(base, String(chatId));
|
||||
}
|
||||
|
||||
function getMetaPath(chatId: number): string {
|
||||
return path.join(getThreadDir(chatId), "meta.json");
|
||||
}
|
||||
|
||||
function getSnippetsPath(chatId: number): string {
|
||||
return path.join(getThreadDir(chatId), "snippets.jsonl");
|
||||
}
|
||||
|
||||
async function ensureDir(dir: string): Promise<void> {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
export async function readMeta(chatId: number): Promise<SmartContextMeta> {
|
||||
const dir = getThreadDir(chatId);
|
||||
await ensureDir(dir);
|
||||
const metaPath = getMetaPath(chatId);
|
||||
try {
|
||||
const raw = await fs.readFile(metaPath, "utf8");
|
||||
const meta = JSON.parse(raw) as SmartContextMeta;
|
||||
return meta;
|
||||
} catch {
|
||||
const fresh: SmartContextMeta = {
|
||||
entityId: String(chatId),
|
||||
updatedAt: Date.now(),
|
||||
rollingSummary: "",
|
||||
summaryTokens: 0,
|
||||
config: { maxSnippets: 400 },
|
||||
};
|
||||
await fs.writeFile(metaPath, JSON.stringify(fresh, null, 2), "utf8");
|
||||
return fresh;
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeMeta(
|
||||
chatId: number,
|
||||
meta: SmartContextMeta,
|
||||
): Promise<void> {
|
||||
const dir = getThreadDir(chatId);
|
||||
await ensureDir(dir);
|
||||
const metaPath = getMetaPath(chatId);
|
||||
const updated: SmartContextMeta = {
|
||||
...meta,
|
||||
entityId: String(chatId),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await fs.writeFile(metaPath, JSON.stringify(updated, null, 2), "utf8");
|
||||
}
|
||||
|
||||
export async function updateRollingSummary(
|
||||
chatId: number,
|
||||
summary: string,
|
||||
): Promise<SmartContextMeta> {
|
||||
const meta = await readMeta(chatId);
|
||||
const summaryTokens = estimateTokens(summary || "");
|
||||
const next: SmartContextMeta = {
|
||||
...meta,
|
||||
rollingSummary: summary,
|
||||
summaryTokens,
|
||||
};
|
||||
await writeMeta(chatId, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
export async function appendSnippets(
|
||||
chatId: number,
|
||||
snippets: Omit<SmartContextSnippet, "id" | "ts" | "tokens">[],
|
||||
): Promise<number> {
|
||||
const dir = getThreadDir(chatId);
|
||||
await ensureDir(dir);
|
||||
const snippetsPath = getSnippetsPath(chatId);
|
||||
const withDefaults: SmartContextSnippet[] = snippets.map((s) => ({
|
||||
id: randomUUID(),
|
||||
ts: Date.now(),
|
||||
tokens: estimateTokens(s.text),
|
||||
...s,
|
||||
}));
|
||||
const lines = withDefaults.map((obj) => JSON.stringify(obj)).join("\n");
|
||||
await fs.appendFile(snippetsPath, (lines ? lines + "\n" : ""), "utf8");
|
||||
|
||||
// prune if exceeded max
|
||||
const meta = await readMeta(chatId);
|
||||
const maxSnippets = meta.config?.maxSnippets ?? 400;
|
||||
try {
|
||||
const file = await fs.readFile(snippetsPath, "utf8");
|
||||
const allLines = file.split("\n").filter(Boolean);
|
||||
if (allLines.length > maxSnippets) {
|
||||
const toKeep = allLines.slice(allLines.length - maxSnippets);
|
||||
await fs.writeFile(snippetsPath, toKeep.join("\n") + "\n", "utf8");
|
||||
return toKeep.length;
|
||||
}
|
||||
return allLines.length;
|
||||
} catch {
|
||||
return withDefaults.length;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readAllSnippets(chatId: number): Promise<SmartContextSnippet[]> {
|
||||
try {
|
||||
const raw = await fs.readFile(getSnippetsPath(chatId), "utf8");
|
||||
return raw
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line) as SmartContextSnippet);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function normalize(value: number, min: number, max: number): number {
|
||||
if (max === min) return 0;
|
||||
return (value - min) / (max - min);
|
||||
}
|
||||
|
||||
function keywordScore(text: string, query: string): number {
|
||||
const toTokens = (s: string) =>
|
||||
s
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_\- ]+/g, " ")
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
const qTokens = new Set(toTokens(query));
|
||||
const tTokens = toTokens(text);
|
||||
if (qTokens.size === 0 || tTokens.length === 0) return 0;
|
||||
let hits = 0;
|
||||
for (const tok of tTokens) if (qTokens.has(tok)) hits++;
|
||||
return hits / tTokens.length; // simple overlap ratio
|
||||
}
|
||||
|
||||
export interface RetrieveContextResult {
|
||||
rollingSummary?: string;
|
||||
usedTokens: number;
|
||||
snippets: SmartContextSnippet[];
|
||||
}
|
||||
|
||||
export async function retrieveContext(
|
||||
chatId: number,
|
||||
query: string,
|
||||
budgetTokens: number,
|
||||
): Promise<RetrieveContextResult> {
|
||||
const meta = await readMeta(chatId);
|
||||
const snippets = await readAllSnippets(chatId);
|
||||
const now = Date.now();
|
||||
let minTs = now;
|
||||
let maxTs = 0;
|
||||
for (const s of snippets) {
|
||||
if (s.ts < minTs) minTs = s.ts;
|
||||
if (s.ts > maxTs) maxTs = s.ts;
|
||||
}
|
||||
const scored = snippets.map((s) => {
|
||||
const recency = normalize(s.ts, minTs, maxTs);
|
||||
const kw = keywordScore(s.text, query);
|
||||
const base = 0.6 * kw + 0.4 * recency;
|
||||
const score = base;
|
||||
return { ...s, score } as SmartContextSnippet;
|
||||
});
|
||||
scored.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
||||
|
||||
const picked: SmartContextSnippet[] = [];
|
||||
let usedTokens = 0;
|
||||
for (const s of scored) {
|
||||
const t = s.tokens ?? estimateTokens(s.text);
|
||||
if (usedTokens + t > budgetTokens) break;
|
||||
picked.push(s);
|
||||
usedTokens += t;
|
||||
}
|
||||
|
||||
const rollingSummary = meta.rollingSummary || "";
|
||||
return { rollingSummary, usedTokens, snippets: picked };
|
||||
}
|
||||
|
||||
export async function rebuildIndex(_chatId: number): Promise<void> {
|
||||
// Placeholder for future embedding/vector index rebuild.
|
||||
return;
|
||||
}
|
||||
55
src/hooks/useSmartContext.ts
Normal file
55
src/hooks/useSmartContext.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { IpcClient } from "../ipc/ipc_client";
|
||||
import type { SmartContextRequest, UpdateSmartContextParams } from "../ipc/ipc_types";
|
||||
|
||||
export function useSmartContext(request: SmartContextRequest) {
|
||||
return useQuery({
|
||||
queryKey: ["smart-context", request.chatId],
|
||||
queryFn: async () => {
|
||||
const client = IpcClient.getInstance();
|
||||
return await client.getSmartContext(request);
|
||||
},
|
||||
enabled: !!request.chatId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSmartContext() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (params: UpdateSmartContextParams) => {
|
||||
const client = IpcClient.getInstance();
|
||||
return await client.updateSmartContext(params);
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["smart-context", variables.chatId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useClearSmartContext() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (chatId: number) => {
|
||||
const client = IpcClient.getInstance();
|
||||
return await client.clearSmartContext({ chatId });
|
||||
},
|
||||
onSuccess: (_, chatId) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["smart-context", chatId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSmartContextStats(chatId: number) {
|
||||
return useQuery({
|
||||
queryKey: ["smart-context-stats", chatId],
|
||||
queryFn: async () => {
|
||||
const client = IpcClient.getInstance();
|
||||
return await client.getSmartContextStats({ chatId });
|
||||
},
|
||||
enabled: !!chatId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
});
|
||||
}
|
||||
51
src/ipc/handlers/smart_context_handlers.ts
Normal file
51
src/ipc/handlers/smart_context_handlers.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { SmartContextStore } from "../utils/smart_context_store";
|
||||
import type {
|
||||
SmartContextRequest,
|
||||
SmartContextResponse,
|
||||
UpdateSmartContextParams,
|
||||
} from "../ipc_types";
|
||||
|
||||
const smartContextStore = new SmartContextStore();
|
||||
|
||||
export function registerSmartContextHandlers() {
|
||||
// Get smart context for a chat
|
||||
ipcMain.handle("smart-context:get", async (event, params: SmartContextRequest) => {
|
||||
try {
|
||||
const context = await smartContextStore.getContext(params);
|
||||
return { success: true, context };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get smart context: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Update smart context
|
||||
ipcMain.handle("smart-context:update", async (event, params: UpdateSmartContextParams) => {
|
||||
try {
|
||||
await smartContextStore.updateContext(params);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update smart context: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear smart context
|
||||
ipcMain.handle("smart-context:clear", async (event, params: { chatId: number }) => {
|
||||
try {
|
||||
await smartContextStore.clearContext(params.chatId);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to clear smart context: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Get context statistics
|
||||
ipcMain.handle("smart-context:stats", async (event, params: { chatId: number }) => {
|
||||
try {
|
||||
const stats = await smartContextStore.getContextStats(params.chatId);
|
||||
return { success: true, stats };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get context stats: ${error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -72,6 +72,9 @@ import type {
|
||||
SelectNodeFolderResult,
|
||||
ApplyVisualEditingChangesParams,
|
||||
AnalyseComponentParams,
|
||||
SmartContextRequest,
|
||||
SmartContextResponse,
|
||||
UpdateSmartContextParams,
|
||||
} from "./ipc_types";
|
||||
import type { Template } from "../shared/templates";
|
||||
import type {
|
||||
@@ -1364,4 +1367,29 @@ export class IpcClient {
|
||||
): Promise<{ isDynamic: boolean; hasStaticText: boolean }> {
|
||||
return this.ipcRenderer.invoke("analyze-component", params);
|
||||
}
|
||||
|
||||
// --- Smart Context Methods ---
|
||||
public async getSmartContext(
|
||||
params: SmartContextRequest,
|
||||
): Promise<SmartContextResponse> {
|
||||
return this.ipcRenderer.invoke("smart-context:get", params);
|
||||
}
|
||||
|
||||
public async updateSmartContext(
|
||||
params: UpdateSmartContextParams,
|
||||
): Promise<{ success: boolean }> {
|
||||
return this.ipcRenderer.invoke("smart-context:update", params);
|
||||
}
|
||||
|
||||
public async clearSmartContext(
|
||||
params: { chatId: number },
|
||||
): Promise<{ success: boolean }> {
|
||||
return this.ipcRenderer.invoke("smart-context:clear", params);
|
||||
}
|
||||
|
||||
public async getSmartContextStats(
|
||||
params: { chatId: number },
|
||||
): Promise<{ success: boolean; stats: any }> {
|
||||
return this.ipcRenderer.invoke("smart-context:stats", params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { registerPromptHandlers } from "./handlers/prompt_handlers";
|
||||
import { registerHelpBotHandlers } from "./handlers/help_bot_handlers";
|
||||
import { registerMcpHandlers } from "./handlers/mcp_handlers";
|
||||
import { registerSecurityHandlers } from "./handlers/security_handlers";
|
||||
import { registerSmartContextHandlers } from "./handlers/smart_context_handlers";
|
||||
import { registerVisualEditingHandlers } from "../pro/main/ipc/handlers/visual_editing_handlers";
|
||||
|
||||
export function registerIpcHandlers() {
|
||||
@@ -70,5 +71,6 @@ export function registerIpcHandlers() {
|
||||
registerHelpBotHandlers();
|
||||
registerMcpHandlers();
|
||||
registerSecurityHandlers();
|
||||
registerSmartContextHandlers();
|
||||
registerVisualEditingHandlers();
|
||||
}
|
||||
|
||||
@@ -582,3 +582,39 @@ export interface AnalyseComponentParams {
|
||||
appId: number;
|
||||
componentId: string;
|
||||
}
|
||||
|
||||
// --- Smart Context Types ---
|
||||
export interface ContextSnippet {
|
||||
id: string;
|
||||
content: string;
|
||||
type: "message" | "code" | "file" | "summary";
|
||||
timestamp: number;
|
||||
importance: number; // 0.0 to 1.0
|
||||
}
|
||||
|
||||
export interface RollingSummary {
|
||||
content: string;
|
||||
createdAt: number;
|
||||
snippetCount: number;
|
||||
}
|
||||
|
||||
export interface SmartContextResponse {
|
||||
snippets: ContextSnippet[];
|
||||
rollingSummary: RollingSummary | null;
|
||||
totalSize: number;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
export interface SmartContextRequest {
|
||||
chatId: number;
|
||||
maxTokens?: number;
|
||||
includeCodebase?: boolean;
|
||||
includeHistory?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateSmartContextParams {
|
||||
chatId: number;
|
||||
content: string;
|
||||
type?: "message" | "code" | "file";
|
||||
importance?: number;
|
||||
}
|
||||
|
||||
130
src/ipc/utils/smart_context_store.ts
Normal file
130
src/ipc/utils/smart_context_store.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type {
|
||||
SmartContextRequest,
|
||||
SmartContextResponse,
|
||||
UpdateSmartContextParams,
|
||||
ContextSnippet,
|
||||
RollingSummary,
|
||||
} from "../ipc_types";
|
||||
|
||||
export class SmartContextStore {
|
||||
private contextCache = new Map<number, SmartContextResponse>();
|
||||
private maxContextSize = 100000; // 100k characters
|
||||
private maxSnippets = 50;
|
||||
private summaryThreshold = 20000; // Summarize when context exceeds this
|
||||
|
||||
async getContext(request: SmartContextRequest): Promise<SmartContextResponse> {
|
||||
const cached = this.contextCache.get(request.chatId);
|
||||
if (cached && !this.isStale(cached)) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Build fresh context
|
||||
const context = await this.buildContext(request);
|
||||
this.contextCache.set(request.chatId, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
async updateContext(params: UpdateSmartContextParams): Promise<void> {
|
||||
const current = this.contextCache.get(params.chatId) || {
|
||||
snippets: [],
|
||||
rollingSummary: null,
|
||||
totalSize: 0,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
|
||||
// Add new snippet
|
||||
const snippet: ContextSnippet = {
|
||||
id: Date.now().toString(),
|
||||
content: params.content,
|
||||
type: params.type || "message",
|
||||
timestamp: Date.now(),
|
||||
importance: params.importance || 1.0,
|
||||
};
|
||||
|
||||
current.snippets.push(snippet);
|
||||
current.lastUpdated = Date.now();
|
||||
|
||||
// Manage context size
|
||||
await this.manageContextSize(current, params.chatId);
|
||||
|
||||
this.contextCache.set(params.chatId, current);
|
||||
}
|
||||
|
||||
async clearContext(chatId: number): Promise<void> {
|
||||
this.contextCache.delete(chatId);
|
||||
}
|
||||
|
||||
async getContextStats(chatId: number): Promise<{
|
||||
snippetCount: number;
|
||||
totalSize: number;
|
||||
hasSummary: boolean;
|
||||
}> {
|
||||
const context = this.contextCache.get(chatId);
|
||||
if (!context) {
|
||||
return { snippetCount: 0, totalSize: 0, hasSummary: false };
|
||||
}
|
||||
|
||||
return {
|
||||
snippetCount: context.snippets.length,
|
||||
totalSize: context.totalSize,
|
||||
hasSummary: !!context.rollingSummary,
|
||||
};
|
||||
}
|
||||
|
||||
private async buildContext(request: SmartContextRequest): Promise<SmartContextResponse> {
|
||||
// This would integrate with the actual chat system
|
||||
// For now, return empty context
|
||||
return {
|
||||
snippets: [],
|
||||
rollingSummary: null,
|
||||
totalSize: 0,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
private isStale(context: SmartContextResponse): boolean {
|
||||
const maxAge = 30 * 60 * 1000; // 30 minutes
|
||||
return Date.now() - context.lastUpdated > maxAge;
|
||||
}
|
||||
|
||||
private async manageContextSize(context: SmartContextResponse, chatId: number): Promise<void> {
|
||||
context.totalSize = context.snippets.reduce((sum, snippet) => sum + snippet.content.length, 0);
|
||||
|
||||
// If we exceed the threshold, create summary
|
||||
if (context.totalSize > this.summaryThreshold && !context.rollingSummary) {
|
||||
await this.createRollingSummary(context);
|
||||
}
|
||||
|
||||
// If we still exceed max size, remove old snippets
|
||||
if (context.totalSize > this.maxContextSize) {
|
||||
await this.trimOldSnippets(context);
|
||||
}
|
||||
}
|
||||
|
||||
private async createRollingSummary(context: SmartContextResponse): Promise<void> {
|
||||
// This would integrate with AI to create summaries
|
||||
// For now, create a simple summary
|
||||
const oldSnippets = context.snippets.slice(0, -10); // Keep last 10 snippets
|
||||
const summaryContent = `Summary of ${oldSnippets.length} previous messages...`;
|
||||
|
||||
context.rollingSummary = {
|
||||
content: summaryContent,
|
||||
createdAt: Date.now(),
|
||||
snippetCount: oldSnippets.length,
|
||||
};
|
||||
|
||||
// Remove summarized snippets
|
||||
context.snippets = context.snippets.slice(-10);
|
||||
}
|
||||
|
||||
private async trimOldSnippets(context: SmartContextResponse): Promise<void> {
|
||||
// Sort by importance and timestamp, keep the best ones
|
||||
context.snippets.sort((a, b) => {
|
||||
const scoreA = a.importance * (Date.now() - a.timestamp);
|
||||
const scoreB = b.importance * (Date.now() - b.timestamp);
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
|
||||
context.snippets = context.snippets.slice(0, this.maxSnippets);
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,11 @@ const validInvokeChannels = [
|
||||
"add-to-favorite",
|
||||
"github:clone-repo-from-url",
|
||||
"get-latest-security-review",
|
||||
// Smart Context
|
||||
"smart-context:get",
|
||||
"smart-context:update",
|
||||
"smart-context:clear",
|
||||
"smart-context:stats",
|
||||
// Test-only channels
|
||||
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
|
||||
// We can't detect with IS_TEST_BUILD in the preload script because
|
||||
|
||||
Reference in New Issue
Block a user