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

- 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:
Kunthawat Greethong
2025-12-18 15:56:48 +07:00
parent 99b0cdf8ac
commit 5660de49de
423 changed files with 70726 additions and 982 deletions

View File

@@ -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"] });
},
});
}

View File

@@ -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';

View File

@@ -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;
});
}

View File

@@ -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;
}

View 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
});
}

View 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}`);
}
});
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View 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);
}
}

View File

@@ -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