diff --git a/README.md b/README.md index 71316a9..8d764cc 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ Alle Werte sind in den Raycast-Einstellungen pro Extension einstellbar. **Verhalten:** - `Standard-Sitzungsmodus` — `Aktive Sitzung wiederverwenden` (Default), `Neue Sitzung pro Anfrage`, `Tagessitzung` - `Ausgabe der Schnellbefehle` — `In die Zwischenablage kopieren` (Default) oder `Am Cursor einfügen` -- `Standard-Modell für AI-Befehle` — Modell-ID, die in den AI-Views vorausgewählt ist (Default `anthropic-claude-sonnet-4-6`, benötigt Raycast Pro). Die View-Dropdowns listen aktuelle Modelle aus `src/ai.ts`; neue Modelle dort eintragen, wenn Raycast sie ausrollt. - `Eigener Name` — Default-Signatur für AI-generierte Email-Antworten, im Antwort-Befehl pro Aufruf überschreibbar + +Das KI-Modell ist keine Preference: jeder AI-Befehl zeigt ein `KI-Modell`-Dropdown mit dem aktuellen Modell-Katalog aus `src/ai.ts`. Die Auswahl wird in `LocalStorage` (`velum.ai.last-model`) persistiert und ist beim nächsten Aufruf in jedem AI-Befehl vorausgewählt. - `Maximale Anzahl gespeicherter Sitzungen` — älteste werden geprunt (Default 20) - `Raycast nach Kopieren/Einfügen schließen` — Auto-Close und Pop-To-Root nach AI-Workflow-Abschluss (Default an) diff --git a/package.json b/package.json index 08597a3..5c96f6d 100644 --- a/package.json +++ b/package.json @@ -178,15 +178,6 @@ "required": true, "placeholder": "Raphael" }, - { - "name": "summaryModel", - "title": "Standard-Modell für AI-Befehle", - "description": "Raycast-KI-Modell-ID, vorausgewählt in den AI-Views. Die volle Modellliste ist dort als Dropdown verfügbar. Benötigt Raycast Pro.", - "type": "textfield", - "required": true, - "default": "anthropic-claude-sonnet-4-6", - "placeholder": "anthropic-claude-sonnet-4-6" - }, { "name": "maxSessions", "title": "Maximale Anzahl gespeicherter Sitzungen", diff --git a/src/ai.ts b/src/ai.ts index f821e6a..64f63bd 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -1,3 +1,5 @@ +import { LocalStorage } from "@raycast/api"; + export type Creativity = "none" | "low" | "medium" | "high"; export const CREATIVITY_OPTIONS: Creativity[] = [ @@ -77,6 +79,22 @@ export const MODEL_OPTIONS: ModelOption[] = [ { value: "together-moonshotai/Kimi-K2.5", title: "Kimi K2.5" }, ]; +const LAST_MODEL_STORAGE_KEY = "velum.ai.last-model"; + +export const DEFAULT_MODEL: string = MODEL_OPTIONS[0].value; + +export async function getLastUsedModel(): Promise { + const stored = await LocalStorage.getItem(LAST_MODEL_STORAGE_KEY); + if (stored && MODEL_OPTIONS.some((option) => option.value === stored)) { + return stored; + } + return DEFAULT_MODEL; +} + +export async function setLastUsedModel(model: string): Promise { + await LocalStorage.setItem(LAST_MODEL_STORAGE_KEY, model); +} + export const STRICT_PLACEHOLDER_RULE = [ "STRENGE REGEL: Gib jeden Platzhalter zeichengetreu (inklusive spitzer Klammern, Großbuchstaben und Unterstrich + Nummer) zurück.", "Du darfst Platzhalter NIEMALS auflösen, raten, übersetzen oder mit erfundenen Namen ersetzen. Schreibe sie exakt so, wie sie in der Eingabe stehen.", diff --git a/src/briefing-from-notes.tsx b/src/briefing-from-notes.tsx index 80dd3d4..75ca232 100644 --- a/src/briefing-from-notes.tsx +++ b/src/briefing-from-notes.tsx @@ -12,11 +12,13 @@ import { useEffect, useMemo, useState } from "react"; import { type Creativity, CREATIVITY_OPTIONS, + DEFAULT_MODEL, + getLastUsedModel, MODEL_OPTIONS, + setLastUsedModel, STRICT_PLACEHOLDER_RULE, } from "./ai"; import { PseudonymizationConfirm } from "./ai-views"; -import { getPreferences } from "./preferences"; import { getSelectedTextSafely } from "./selection"; import { createSession, @@ -75,23 +77,25 @@ type FormValues = { }; export default function Command() { - const preferences = getPreferences(); const [text, setText] = useState(""); const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(); const [entityTypes, setEntityTypes] = useState([]); const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [model, setModel] = useState(DEFAULT_MODEL); const [isLoading, setIsLoading] = useState(true); const { push } = useNavigation(); useEffect(() => { async function load() { - const [loadedSessions, activeId] = await Promise.all([ + const [loadedSessions, activeId, lastModel] = await Promise.all([ listSessions(), getActiveSessionId(), + getLastUsedModel(), ]); setSessions(loadedSessions); setActiveSessionId(activeId); + setModel(lastModel); const selection = await getSelectedTextSafely(); if (selection && selection.trim()) { @@ -150,6 +154,8 @@ export default function Command() { return; } + void setLastUsedModel(values.model); + const toast = await showToast({ style: Toast.Style.Animated, title: "Pseudonymisiere…", @@ -272,7 +278,8 @@ export default function Command() { {MODEL_OPTIONS.map((option) => ( ([]); const [activeSessionId, setActiveSessionId] = useState(); const [entityTypes, setEntityTypes] = useState([]); const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [model, setModel] = useState(DEFAULT_MODEL); const [isLoading, setIsLoading] = useState(true); const { push } = useNavigation(); useEffect(() => { async function load() { - const [loadedSessions, activeId] = await Promise.all([ + const [loadedSessions, activeId, lastModel] = await Promise.all([ listSessions(), getActiveSessionId(), + getLastUsedModel(), ]); setSessions(loadedSessions); setActiveSessionId(activeId); + setModel(lastModel); const selection = await getSelectedTextSafely(); if (selection && selection.trim()) { @@ -155,6 +159,8 @@ export default function Command() { return; } + void setLastUsedModel(values.model); + const toast = await showToast({ style: Toast.Style.Animated, title: "Pseudonymisiere…", @@ -277,7 +283,8 @@ export default function Command() { {MODEL_OPTIONS.map((option) => ( ([]); const [activeSessionId, setActiveSessionId] = useState(); const [entityTypes, setEntityTypes] = useState([]); const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [model, setModel] = useState(DEFAULT_MODEL); const [isLoading, setIsLoading] = useState(true); const { push } = useNavigation(); useEffect(() => { async function load() { - const [loadedSessions, activeId] = await Promise.all([ + const [loadedSessions, activeId, lastModel] = await Promise.all([ listSessions(), getActiveSessionId(), + getLastUsedModel(), ]); setSessions(loadedSessions); setActiveSessionId(activeId); + setModel(lastModel); const selection = await getSelectedTextSafely(); if (selection && selection.trim()) { @@ -183,6 +187,8 @@ export default function Command() { return; } + void setLastUsedModel(values.model); + const toast = await showToast({ style: Toast.Style.Animated, title: "Pseudonymisiere…", @@ -337,7 +343,8 @@ export default function Command() { {MODEL_OPTIONS.map((option) => ( (DEFAULT_MODEL); const [creativity, setCreativity] = useState("medium"); const [disclosure, setDisclosure] = useState(false); const [isLoading, setIsLoading] = useState(true); @@ -73,17 +79,20 @@ export default function Command() { useEffect(() => { async function load() { - const [loadedSessions, activeId, defaults] = await Promise.all([ - listSessions(), - getActiveSessionId(), - loadReplyDefaults(), - ]); + const [loadedSessions, activeId, defaults, lastModel] = await Promise.all( + [ + listSessions(), + getActiveSessionId(), + loadReplyDefaults(), + getLastUsedModel(), + ], + ); setSessions(loadedSessions); setActiveSessionId(activeId); + setModel(lastModel); if (defaults.greeting) setGreeting(defaults.greeting); if (defaults.signOff) setSignOff(defaults.signOff); - if (defaults.model) setModel(defaults.model); if (defaults.creativity) setCreativity(defaults.creativity as Creativity); if (typeof defaults.disclosure === "boolean") setDisclosure(defaults.disclosure); @@ -104,13 +113,12 @@ export default function Command() { } } load(); - }, [preferences.summaryModel, preferences.userFullName]); + }, [preferences.userFullName]); function persist( overrides: Partial<{ greeting: string; signOff: string; - model: string; creativity: Creativity; disclosure: boolean; }>, @@ -118,7 +126,6 @@ export default function Command() { saveReplyDefaults({ greeting: overrides.greeting ?? greeting, signOff: overrides.signOff ?? signOff, - model: overrides.model ?? model, creativity: overrides.creativity ?? creativity, disclosure: overrides.disclosure ?? disclosure, }).catch(() => { @@ -173,10 +180,10 @@ export default function Command() { return; } + void setLastUsedModel(model); await saveReplyDefaults({ greeting, signOff, - model, creativity, disclosure, }); @@ -358,10 +365,7 @@ export default function Command() { id="model" title="KI-Modell" value={model} - onChange={(v) => { - setModel(v); - persist({ model: v }); - }} + onChange={setModel} > {MODEL_OPTIONS.map((option) => ( {}); @@ -510,7 +512,6 @@ function ConfirmReply(props: StageProps) { saveReplyDefaults({ greeting, signOff: v, - model: props.model, creativity: props.creativity, disclosure, }).catch(() => {}); @@ -546,7 +547,6 @@ function ConfirmReply(props: StageProps) { saveReplyDefaults({ greeting, signOff, - model: props.model, creativity: props.creativity, disclosure: v, }).catch(() => {}); diff --git a/src/reply.ts b/src/reply.ts index 00f2c19..73aa423 100644 --- a/src/reply.ts +++ b/src/reply.ts @@ -6,7 +6,6 @@ const REPLY_DEFAULTS_KEY = "velum.reply.defaults.v1"; export type ReplyDefaults = { greeting: string; signOff: string; - model: string; creativity: string; disclosure: boolean; }; diff --git a/src/summarize-email.tsx b/src/summarize-email.tsx index 027e7e4..ee65ca6 100644 --- a/src/summarize-email.tsx +++ b/src/summarize-email.tsx @@ -14,7 +14,6 @@ import { import { copyRichText, markdownToHtml, maybeCloseRaycast } from "./ai-views"; import { getSelectedTextSafely } from "./selection"; import { useEffect, useMemo, useRef, useState } from "react"; -import { getPreferences } from "./preferences"; import { createSession, getActiveSessionId, @@ -23,7 +22,13 @@ import { setActiveSession, updateSessionMapping, } from "./sessions"; -import { CREATIVITY_OPTIONS, MODEL_OPTIONS } from "./ai"; +import { + CREATIVITY_OPTIONS, + DEFAULT_MODEL, + getLastUsedModel, + MODEL_OPTIONS, + setLastUsedModel, +} from "./ai"; import { buildSummaryPrompt, type Creativity } from "./summarize"; import type { EntityType, VelumSession } from "./types"; import { @@ -49,23 +54,25 @@ type FormValues = { }; export default function Command() { - const preferences = getPreferences(); const [text, setText] = useState(""); const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(); const [entityTypes, setEntityTypes] = useState([]); const [selectedEntityTypes, setSelectedEntityTypes] = useState([]); + const [model, setModel] = useState(DEFAULT_MODEL); const [isLoading, setIsLoading] = useState(true); const { push } = useNavigation(); useEffect(() => { async function load() { - const [loadedSessions, activeId] = await Promise.all([ + const [loadedSessions, activeId, lastModel] = await Promise.all([ listSessions(), getActiveSessionId(), + getLastUsedModel(), ]); setSessions(loadedSessions); setActiveSessionId(activeId); + setModel(lastModel); const selection = await getSelectedTextSafely(); if (selection && selection.trim()) { @@ -124,6 +131,8 @@ export default function Command() { return; } + void setLastUsedModel(values.model); + const toast = await showToast({ style: Toast.Style.Animated, title: "Pseudonymisiere…", @@ -238,7 +247,8 @@ export default function Command() { {MODEL_OPTIONS.map((option) => (