diff --git a/.gitignore b/.gitignore index 878dbe41..39636667 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ release/ .vite/ .electron-vite/ out/ +.dir-locals.el \ No newline at end of file diff --git a/dev-docs/solidjs-llms.txt b/dev-docs/solidjs-llms.txt new file mode 100644 index 00000000..0158a846 --- /dev/null +++ b/dev-docs/solidjs-llms.txt @@ -0,0 +1,82 @@ +# SolidJS Documentation + +> Solid is a modern JavaScript framework for building user interfaces with fine-grained reactivity. It compiles JSX to real DOM elements and updates only what changes, delivering exceptional performance without a virtual DOM. Solid provides reactive primitives like signals, effects, and stores for predictable state management. + +SolidJS is a declarative JavaScript framework that prioritizes performance and developer experience. Unlike frameworks that re-run components on every update, Solid components run once during initialization and set up a reactive system that precisely updates the DOM when dependencies change. + +Key principles: +- Fine-grained reactivity: Updates only the specific DOM nodes that depend on changed data +- Compile-time optimization: JSX transforms into efficient DOM operations +- Unidirectional data flow: Props are read-only, promoting predictable state management +- Component lifecycle: Components run once, with reactive primitives handling updates + +**Use your web fetch tool on any of the following links to understand the relevant concept**. + +## Quick Start + +- [Overview](https://docs.solidjs.com/): Framework introduction and key advantages +- [Quick Start](https://docs.solidjs.com/quick-start): Installation and project setup with create-solid +- [Interactive Tutorial](https://www.solidjs.com/tutorial/introduction_basics): Learn Solid basics through guided examples +- [Playground](https://playground.solidjs.com/): Experiment with Solid directly in your browser + +## Core Concepts + +- [Intro to Reactivity](https://docs.solidjs.com/concepts/intro-to-reactivity): Signals, subscribers, and reactive principles +- [Understanding JSX](https://docs.solidjs.com/concepts/understanding-jsx): How Solid uses JSX and key differences from HTML +- [Components Basics](https://docs.solidjs.com/concepts/components/basics): Component trees, lifecycles, and composition patterns +- [Signals](https://docs.solidjs.com/concepts/signals): Core reactive primitive for state management with getters/setters +- [Effects](https://docs.solidjs.com/concepts/effects): Side effects, dependency tracking, and lifecycle functions +- [Stores](https://docs.solidjs.com/concepts/stores): Complex state management with proxy-based reactivity +- [Context](https://docs.solidjs.com/concepts/context): Cross-component state sharing without prop drilling + +## Component APIs + +- [Props](https://docs.solidjs.com/concepts/components/props): Passing data and handlers to child components +- [Event Handlers](https://docs.solidjs.com/concepts/components/event-handlers): Managing user interactions +- [Class and Style](https://docs.solidjs.com/concepts/components/class-style): Dynamic styling approaches +- [Refs](https://docs.solidjs.com/concepts/refs): Accessing DOM elements directly + +## Control Flow + +- [Conditional Rendering](https://docs.solidjs.com/concepts/control-flow/conditional-rendering): Show, Switch, and Match components +- [List Rendering](https://docs.solidjs.com/concepts/control-flow/list-rendering): For, Index, and keyed iteration +- [Dynamic](https://docs.solidjs.com/concepts/control-flow/dynamic): Dynamic component switching +- [Portal](https://docs.solidjs.com/concepts/control-flow/portal): Rendering outside component hierarchy +- [Error Boundary](https://docs.solidjs.com/concepts/control-flow/error-boundary): Graceful error handling + +## Derived Values + +- [Derived Signals](https://docs.solidjs.com/concepts/derived-values/derived-signals): Computed values from signals +- [Memos](https://docs.solidjs.com/concepts/derived-values/memos): Cached computed values for performance + +## State Management + +- [Basic State Management](https://docs.solidjs.com/guides/state-management): One-way data flow and lifting state +- [Complex State Management](https://docs.solidjs.com/guides/complex-state-management): Stores for scalable applications +- [Fetching Data](https://docs.solidjs.com/guides/fetching-data): Async data with createResource + +## Routing + +- [Routing & Navigation](https://docs.solidjs.com/guides/routing-and-navigation): @solidjs/router setup and usage +- [Dynamic Routes](https://docs.solidjs.com/guides/routing-and-navigation#dynamic-routes): Route parameters and validation +- [Nested Routes](https://docs.solidjs.com/guides/routing-and-navigation#nested-routes): Hierarchical route structures +- [Preload Functions](https://docs.solidjs.com/guides/routing-and-navigation#preload-functions): Parallel data fetching + +## Advanced Topics + +- [Fine-Grained Reactivity](https://docs.solidjs.com/advanced-concepts/fine-grained-reactivity): Deep dive into reactive system +- [TypeScript](https://docs.solidjs.com/configuration/typescript): Type safety and configuration + +## Ecosystem + +- [Solid Router](https://docs.solidjs.com/solid-router/): File-system routing and data APIs +- [SolidStart](https://docs.solidjs.com/solid-start/): Full-stack meta-framework +- [Solid Meta](https://docs.solidjs.com/solid-meta/): Document head management +- [Templates](https://github.com/solidjs/templates): Starter templates for different setups + +## Optional + +- [Ecosystem Libraries](https://www.solidjs.com/ecosystem): Community packages and tools +- [API Reference](https://docs.solidjs.com/reference/): Complete API documentation +- [Testing](https://docs.solidjs.com/guides/testing): Testing strategies and utilities +- [Deployment](https://docs.solidjs.com/guides/deploying-your-app): Build and deployment options diff --git a/packages/server/src/config/schema.ts b/packages/server/src/config/schema.ts index a2c88f02..266d69b4 100644 --- a/packages/server/src/config/schema.ts +++ b/packages/server/src/config/schema.ts @@ -18,6 +18,7 @@ const PreferencesSchema = z.object({ toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), showUsageMetrics: z.boolean().default(true), + autoCleanupBlankSessions: z.boolean().default(true), }) const RecentFolderSchema = z.object({ diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index e76a69b7..dd8f5e2c 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -47,6 +47,7 @@ const App: Component = () => { preferences, recordWorkspaceLaunch, toggleShowThinkingBlocks, + toggleAutoCleanupBlankSessions, toggleUsageMetrics, setDiffViewMode, setToolOutputExpansion, @@ -206,6 +207,7 @@ const App: Component = () => { const { commands: paletteCommands, executeCommand } = useCommands({ preferences, + toggleAutoCleanupBlankSessions, toggleShowThinkingBlocks, toggleUsageMetrics, setDiffViewMode, diff --git a/packages/ui/src/lib/hooks/use-commands.ts b/packages/ui/src/lib/hooks/use-commands.ts index 0bc52575..6b5ff746 100644 --- a/packages/ui/src/lib/hooks/use-commands.ts +++ b/packages/ui/src/lib/hooks/use-commands.ts @@ -16,11 +16,13 @@ import { showAlertDialog } from "../../stores/alerts" import type { Instance } from "../../types/instance" import type { MessageRecord } from "../../stores/message-v2/types" import { messageStoreBus } from "../../stores/message-v2/bus" +import { cleanupBlankSessions } from "../../stores/session-state" export interface UseCommandsOptions { preferences: Accessor toggleShowThinkingBlocks: () => void toggleUsageMetrics: () => void + toggleAutoCleanupBlankSessions: () => void setDiffViewMode: (mode: "split" | "unified") => void setToolOutputExpansion: (mode: ExpansionPreference) => void setDiagnosticsExpansion: (mode: ExpansionPreference) => void @@ -142,6 +144,19 @@ export function useCommands(options: UseCommandsOptions) { }, }) + commandRegistry.register({ + id: "cleanup-blank-sessions", + label: "Scrub Sessions", + description: "Remove empty sessions, subagent sessions that have completed their primary task, and extraneous forked sessions.", + category: "Session", + keywords: ["cleanup", "blank", "empty", "sessions", "remove", "delete", "scrub"], + action: async () => { + const instance = activeInstance() + if (!instance) return + cleanupBlankSessions(instance.id, undefined, true) + }, + }) + commandRegistry.register({ id: "switch-to-info", label: "Instance Info", @@ -467,6 +482,18 @@ export function useCommands(options: UseCommandsOptions) { keywords: ["token", "usage", "cost", "stats"], action: options.toggleUsageMetrics, }) + + commandRegistry.register({ + id: "auto-cleanup-blank-sessions", + label: () => { + const enabled = options.preferences().autoCleanupBlankSessions + return `Auto-Cleanup Blank Sessions ยท ${enabled ? "Enabled" : "Disabled"}` + }, + description: "Automatically clean up blank sessions when creating new ones", + category: "System", + keywords: ["auto", "cleanup", "blank", "sessions", "toggle"], + action: options.toggleAutoCleanupBlankSessions, + }) commandRegistry.register({ id: "help", diff --git a/packages/ui/src/stores/preferences.tsx b/packages/ui/src/stores/preferences.tsx index 59611498..5849dc3f 100644 --- a/packages/ui/src/stores/preferences.tsx +++ b/packages/ui/src/stores/preferences.tsx @@ -37,6 +37,7 @@ export interface Preferences { toolOutputExpansion: ExpansionPreference diagnosticsExpansion: ExpansionPreference showUsageMetrics: boolean + autoCleanupBlankSessions?: boolean } export interface OpenCodeBinary { @@ -64,6 +65,7 @@ const defaultPreferences: Preferences = { toolOutputExpansion: "expanded", diagnosticsExpansion: "expanded", showUsageMetrics: true, + autoCleanupBlankSessions: true, } function deepEqual(a: unknown, b: unknown): boolean { @@ -98,6 +100,7 @@ function normalizePreferences(pref?: Partial & { agentModelSelectio toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultPreferences.toolOutputExpansion, diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultPreferences.diagnosticsExpansion, showUsageMetrics: sanitized.showUsageMetrics ?? defaultPreferences.showUsageMetrics, + autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultPreferences.autoCleanupBlankSessions, } } @@ -285,6 +288,11 @@ function toggleUsageMetrics(): void { updatePreferences({ showUsageMetrics: !preferences().showUsageMetrics }) } +function toggleAutoCleanupBlankSessions(): void { + console.log("toggle auto cleanup") + updatePreferences({ autoCleanupBlankSessions: !preferences().autoCleanupBlankSessions }) +} + function addRecentFolder(path: string): void { updateConfig((draft) => { draft.recentFolders = buildRecentFolderList(path, draft.recentFolders) @@ -386,6 +394,7 @@ interface ConfigContextValue { updateConfig: typeof updateConfig toggleShowThinkingBlocks: typeof toggleShowThinkingBlocks toggleUsageMetrics: typeof toggleUsageMetrics + toggleAutoCleanupBlankSessions: typeof toggleAutoCleanupBlankSessions setDiffViewMode: typeof setDiffViewMode setToolOutputExpansion: typeof setToolOutputExpansion setDiagnosticsExpansion: typeof setDiagnosticsExpansion @@ -418,6 +427,7 @@ const configContextValue: ConfigContextValue = { updateConfig, toggleShowThinkingBlocks, toggleUsageMetrics, + toggleAutoCleanupBlankSessions, setDiffViewMode, setToolOutputExpansion, setDiagnosticsExpansion, @@ -473,6 +483,7 @@ export { updateConfig, updatePreferences, toggleShowThinkingBlocks, + toggleAutoCleanupBlankSessions, toggleUsageMetrics, recentFolders, addRecentFolder, diff --git a/packages/ui/src/stores/session-api.ts b/packages/ui/src/stores/session-api.ts index 4d00cee8..ab213316 100644 --- a/packages/ui/src/stores/session-api.ts +++ b/packages/ui/src/stores/session-api.ts @@ -2,15 +2,17 @@ import type { Session } from "../types/session" import type { Message } from "../types/message" import { instances, refreshPermissionsForSession } from "./instances" -import { setAgentModelPreference } from "./preferences" +import { preferences, setAgentModelPreference } from "./preferences" import { setSessionCompactionState } from "./session-compaction" import { activeSessionId, agents, clearSessionDraftPrompt, + getChildSessions, + isBlankSession, messagesLoaded, - providers, pruneDraftPrompts, + providers, setActiveSessionId, setAgents, setMessagesLoaded, @@ -20,6 +22,7 @@ import { sessions, loading, setLoading, + cleanupBlankSessions, } from "./session-state" import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models" import { normalizeMessagePart } from "./message-v2/normalizers" @@ -228,6 +231,10 @@ async function createSession(instanceId: string, agent?: string): Promise { + const created = session.time?.created || 0 + const updated = session.time?.updated || 0 + const hasChildren = getChildSessions(instanceId, session.id).length > 0 + const isFreshSession = created === updated && !hasChildren + + // Common short-circuit: fresh sessions without children + if (!fetchIfNeeded) { + return isFreshSession + } + + // For a more thorough deep clean, we need to look at actual messages + + const instance = instances().get(instanceId) + if (!instance?.client) { + return isFreshSession + } + let messages: any[] = [] + try { + const response = await instance.client.session.messages({ path: { id: session.id } }) + messages = response.data || [] + } catch (error) { + console.error(`Failed to fetch messages for session ${session.id}:`, error) + return isFreshSession + } + + // Specific logic by session type + if (session.parentId === null) { + // Parent: blank if no messages and no children (fresh !== blank sometimes!) + const hasChildren = getChildSessions(instanceId, session.id).length > 0 + return messages.length === 0 && !hasChildren + } else if (session.title?.includes("subagent)")) { + // Subagent: "blank" (really: finished doing its job) if actually blank... + // ... OR no streaming, no pending perms, no tool parts + if (messages.length === 0) return true + + const hasStreaming = messages.some((msg) => { + const info = msg.info.status || msg.status + return info === "streaming" || info === "sending" + }) + + const lastMessage = messages[messages.length - 1] + const lastParts = lastMessage?.parts || [] + const hasToolPart = lastParts.some((part: any) => + part.type === "tool" || part.data?.type === "tool" + ) + + return !hasStreaming && !session.pendingPermission && !hasToolPart + } else { + // Fork: blank if somehow has no messages or at revert point + if (messages.length === 0) return true + + const lastMessage = messages[messages.length - 1] + const lastInfo = lastMessage?.info || lastMessage + return lastInfo?.id === session.revert?.messageID + } +} + + +async function cleanupBlankSessions(instanceId: string, excludeSessionId?: string, fetchIfNeeded = false): Promise { + const instanceSessions = sessions().get(instanceId) + if (!instanceSessions) return + + if (fetchIfNeeded) { + const confirmed = await showConfirmDialog( + "This cleanup may be slow, and may delete sessions you didn't intend to delete. Are you sure?", + { + title: "Deep Clean Sessions", + detail: "Deep Clean Sessions will delete all sessions that have no messages, remove any finished sub-agent sessions, and clear out any unused forks of a session.", + confirmLabel: "Continue", + cancelLabel: "Cancel" + } + ) + if (!confirmed) return + } + + const cleanupPromises = Array.from(instanceSessions) + .filter(([sessionId]) => sessionId !== excludeSessionId) + .map(async ([sessionId, session]) => { + const isBlank = await isBlankSession(session, instanceId, fetchIfNeeded) + if (!isBlank) return false + + await deleteSession(instanceId, sessionId).catch((error: Error) => { + console.error(`Failed to delete blank session ${sessionId}:`, error) + }) + return true + }) + + if (cleanupPromises.length > 0) { + console.log(`Cleaning up ${cleanupPromises.length} blank sessions`) + const deletionResults = await Promise.all(cleanupPromises) + const deletedCount = deletionResults.filter(Boolean).length + + if (deletedCount > 0) { + showToastNotification({ + message: `Cleaned up ${deletedCount} blank session${deletedCount === 1 ? "" : "s"}`, + variant: "info" + }) + } + } +} + export { sessions, setSessions, @@ -259,4 +366,6 @@ export { isSessionBusy, isSessionMessagesLoading, getSessionInfo, + isBlankSession, + cleanupBlankSessions, }