feat: auto-delete old conversations, and stop a tool call that repeats (#15)

This commit was merged in pull request #15.
This commit is contained in:
2026-10-05 09:05:15 +00:00
parent 576c5946f7
commit 05fd9ff13b
25 changed files with 2372 additions and 59 deletions
+124 -3
View File
@@ -1,6 +1,13 @@
import { Action, ActionPanel, Icon, List, openExtensionPreferences } from "@vicinae/api";
import { useCallback, useEffect, useMemo, useState } from "react";
import { describeResumeTimeout, getLamPreferences, needsSetup } from "./lib/preferences";
import { Action, ActionPanel, Alert, Icon, List, confirmAlert, openExtensionPreferences } from "@vicinae/api";
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
import { clearAllChats, clearOrphanedSnips, runAutoDelete } from "./lib/cleanup";
import {
describeAutoDelete,
describeMaxToolRounds,
describeResumeTimeout,
getLamPreferences,
needsSetup,
} from "./lib/preferences";
import { listModels } from "./lib/ollama";
import { forgetGeoPlace } from "./lib/geo";
import { forgetVisionSupport, visionSupport, type VisionSupport } from "./lib/vision";
@@ -81,10 +88,58 @@ export default function SetupCommand() {
if (!missingKey) void testConnection();
}, [missingKey, testConnection]);
// Opening any Lam command is a moment to collect expired conversations, and
// this is one of them. Nothing is open here, so nothing is exempt.
useEffect(() => {
void runAutoDelete({ maxAgeMs: preferences.autoDeleteMs });
}, [preferences.autoDeleteMs]);
const openPreferences = useCallback(() => {
void openExtensionPreferences();
}, []);
/**
* Confirms, then runs a destructive tidy-up.
*
* Both actions on this screen delete things that cannot be recovered: there is
* no trash to undo them from, and a conversation's screenshots go with it. That
* is worth one extra keystroke, and the alert says what will go rather than
* asking "are you sure" about nothing in particular.
*/
const confirmThen = useCallback(
async (options: { title: string; message: string }, run: () => Promise<void>) => {
const confirmed = await confirmAlert({
title: options.title,
message: options.message,
primaryAction: { title: "Delete", style: Alert.ActionStyle.Destructive },
});
if (confirmed) await run();
},
[],
);
/**
* The action panel for a maintenance row.
*
* The row's own action comes first, because it is why the user is looking at
* the row, and the two read-only actions sit in their own section below so they
* can never be the default that Return fires.
*/
const maintenanceActions = (primary: ReactNode) => (
<ActionPanel>
{primary}
<ActionPanel.Section>
<Action title="Open Extension Preferences" icon={Icon.BulletPoints} onAction={openPreferences} />
<Action
title="Test Connection"
icon={Icon.ArrowClockwise}
onAction={() => void testConnection()}
/>
</ActionPanel.Section>
</ActionPanel>
);
const settingsActions = (
<ActionPanel>
<Action title="Open Extension Preferences" icon={Icon.BulletPoints} onAction={openPreferences} />
@@ -169,6 +224,18 @@ export default function SetupCommand() {
subtitle={describeResumeTimeout(preferences.resumeTimeoutMs)}
actions={settingsActions}
/>
<List.Item
icon={Icon.Trash}
title="Delete Old Chats After"
subtitle={describeAutoDelete(preferences.autoDeleteMs)}
actions={settingsActions}
/>
<List.Item
icon={Icon.BulletPoints}
title="Tool Call Limit"
subtitle={describeMaxToolRounds(preferences.maxToolRounds)}
actions={settingsActions}
/>
<List.Item
icon={vision === "yes" ? Icon.Image : Icon.QuestionMarkCircle}
title="Image Input"
@@ -195,6 +262,60 @@ export default function SetupCommand() {
/>
</List.Section>
{/*
* Maintenance sits below the settings it acts on. Both rows are destructive
* rather than settings, so each builds its own action panel instead of
* sharing the one above: these are the only two things on this screen that
* delete anything, and neither should be one keystroke away from a row the
* user was only reading.
*/}
<List.Section title="Maintenance">
<List.Item
icon={Icon.Image}
title="Clear Out Unattached Photos"
subtitle="Delete captures no conversation refers to any more. Screenshots still attached to a chat are kept, and so is one snipped in the last day."
actions={maintenanceActions(
<Action
title="Clear Unattached Photos"
icon={Icon.Image}
style="destructive"
onAction={() =>
void confirmThen(
{
title: "Delete unattached photos?",
message:
"Screenshots that belong to no conversation will be deleted. Photos still attached to a chat are kept.",
},
clearOrphanedSnips,
)
}
/>,
)}
/>
<List.Item
icon={Icon.Trash}
title="Clear Out All Chats"
subtitle="Delete every conversation, and every screenshot it captured. This cannot be undone."
actions={maintenanceActions(
<Action
title="Clear All Chats"
icon={Icon.Trash}
style="destructive"
onAction={() =>
void confirmThen(
{
title: "Delete every conversation?",
message:
"Every conversation and every screenshot it captured will be deleted. This cannot be undone.",
},
clearAllChats,
)
}
/>,
)}
/>
</List.Section>
{error ? (
<List.Section title="Last Error">
<List.Item