现在会话输入框会记住用户的配置选项和模型选择,避免每次都要重新勾选
This commit is contained in:
@@ -47,6 +47,10 @@ import {
|
|||||||
type MessageTurn,
|
type MessageTurn,
|
||||||
type PromptDraft,
|
type PromptDraft,
|
||||||
} from "@/lib/types"
|
} from "@/lib/types"
|
||||||
|
import {
|
||||||
|
getSavedModeId,
|
||||||
|
saveModePreference,
|
||||||
|
} from "@/lib/selector-prefs-storage"
|
||||||
import {
|
import {
|
||||||
buildConversationDraftStorageKey,
|
buildConversationDraftStorageKey,
|
||||||
buildNewConversationDraftStorageKey,
|
buildNewConversationDraftStorageKey,
|
||||||
@@ -724,7 +728,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
if (dbConvIdRef.current) return
|
if (dbConvIdRef.current) return
|
||||||
|
|
||||||
setDraftAgentType(nextAgentType)
|
setDraftAgentType(nextAgentType)
|
||||||
setModeId(null)
|
setModeId(getSavedModeId(nextAgentType))
|
||||||
setAgentConnectError(null)
|
setAgentConnectError(null)
|
||||||
|
|
||||||
const s = connStatusRef.current
|
const s = connStatusRef.current
|
||||||
@@ -759,6 +763,20 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
[connConnect, connDisconnect, workingDirForConnection]
|
[connConnect, connDisconnect, workingDirForConnection]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleModeChange = useCallback(
|
||||||
|
(newModeId: string) => {
|
||||||
|
setModeId(newModeId)
|
||||||
|
// Persist mode selection to localStorage immediately
|
||||||
|
if (conn.modes) {
|
||||||
|
saveModePreference(selectedAgent, {
|
||||||
|
...conn.modes,
|
||||||
|
current_mode_id: newModeId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[conn.modes, selectedAgent]
|
||||||
|
)
|
||||||
|
|
||||||
const handleAnswerQuestion = useCallback(
|
const handleAnswerQuestion = useCallback(
|
||||||
(answer: string) => {
|
(answer: string) => {
|
||||||
if (connStatus !== "connected") return
|
if (connStatus !== "connected") return
|
||||||
@@ -853,7 +871,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
configOptionsLoading={configOptionsLoading}
|
configOptionsLoading={configOptionsLoading}
|
||||||
selectorsLoading={selectorsLoading}
|
selectorsLoading={selectorsLoading}
|
||||||
selectedModeId={selectedModeId}
|
selectedModeId={selectedModeId}
|
||||||
onModeChange={setModeId}
|
onModeChange={handleModeChange}
|
||||||
onConfigOptionChange={handleSetConfigOption}
|
onConfigOptionChange={handleSetConfigOption}
|
||||||
availableCommands={connectionCommands}
|
availableCommands={connectionCommands}
|
||||||
attachmentTabId={tabId}
|
attachmentTabId={tabId}
|
||||||
@@ -924,7 +942,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
configOptionsLoading={configOptionsLoading}
|
configOptionsLoading={configOptionsLoading}
|
||||||
selectorsLoading={selectorsLoading}
|
selectorsLoading={selectorsLoading}
|
||||||
selectedModeId={selectedModeId}
|
selectedModeId={selectedModeId}
|
||||||
onModeChange={setModeId}
|
onModeChange={handleModeChange}
|
||||||
onConfigOptionChange={handleSetConfigOption}
|
onConfigOptionChange={handleSetConfigOption}
|
||||||
availableCommands={connectionCommands}
|
availableCommands={connectionCommands}
|
||||||
attachmentTabId={tabId}
|
attachmentTabId={tabId}
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ import {
|
|||||||
IDLE_SWEEP_INTERVAL_MS,
|
IDLE_SWEEP_INTERVAL_MS,
|
||||||
} from "@/lib/constants"
|
} from "@/lib/constants"
|
||||||
import { notifyTurnComplete } from "@/lib/notification"
|
import { notifyTurnComplete } from "@/lib/notification"
|
||||||
|
import {
|
||||||
|
applySavedModePreference,
|
||||||
|
applySavedConfigPreferences,
|
||||||
|
saveModePreference,
|
||||||
|
saveConfigPreference,
|
||||||
|
clearStalePrefs,
|
||||||
|
} from "@/lib/selector-prefs-storage"
|
||||||
import { useAlertContext, type AlertAction } from "@/contexts/alert-context"
|
import { useAlertContext, type AlertAction } from "@/contexts/alert-context"
|
||||||
import { useFolderContext } from "@/contexts/folder-context"
|
import { useFolderContext } from "@/contexts/folder-context"
|
||||||
|
|
||||||
@@ -1523,17 +1530,20 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
case "session_modes": {
|
case "session_modes": {
|
||||||
flushStreamingQueue()
|
flushStreamingQueue()
|
||||||
const modeConn = storeRef.current.connections.get(contextKey)
|
const modeConn = storeRef.current.connections.get(contextKey)
|
||||||
|
const resolvedModes = modeConn
|
||||||
|
? applySavedModePreference(modeConn.agentType, e.modes)
|
||||||
|
: e.modes
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SESSION_MODES",
|
type: "SESSION_MODES",
|
||||||
contextKey,
|
contextKey,
|
||||||
modes: e.modes,
|
modes: resolvedModes,
|
||||||
})
|
})
|
||||||
if (modeConn) {
|
if (modeConn) {
|
||||||
const entry = selectorsCache.get(modeConn.agentType) ?? {
|
const entry = selectorsCache.get(modeConn.agentType) ?? {
|
||||||
modes: null,
|
modes: null,
|
||||||
configOptions: null,
|
configOptions: null,
|
||||||
}
|
}
|
||||||
entry.modes = e.modes
|
entry.modes = resolvedModes
|
||||||
selectorsCache.set(modeConn.agentType, entry)
|
selectorsCache.set(modeConn.agentType, entry)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -1541,17 +1551,20 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
case "session_config_options": {
|
case "session_config_options": {
|
||||||
flushStreamingQueue()
|
flushStreamingQueue()
|
||||||
const cfgConn = storeRef.current.connections.get(contextKey)
|
const cfgConn = storeRef.current.connections.get(contextKey)
|
||||||
|
const resolvedConfigOptions = cfgConn
|
||||||
|
? applySavedConfigPreferences(cfgConn.agentType, e.config_options)
|
||||||
|
: e.config_options
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SESSION_CONFIG_OPTIONS",
|
type: "SESSION_CONFIG_OPTIONS",
|
||||||
contextKey,
|
contextKey,
|
||||||
configOptions: e.config_options,
|
configOptions: resolvedConfigOptions,
|
||||||
})
|
})
|
||||||
if (cfgConn) {
|
if (cfgConn) {
|
||||||
const entry = selectorsCache.get(cfgConn.agentType) ?? {
|
const entry = selectorsCache.get(cfgConn.agentType) ?? {
|
||||||
modes: null,
|
modes: null,
|
||||||
configOptions: null,
|
configOptions: null,
|
||||||
}
|
}
|
||||||
entry.configOptions = e.config_options
|
entry.configOptions = resolvedConfigOptions
|
||||||
selectorsCache.set(cfgConn.agentType, entry)
|
selectorsCache.set(cfgConn.agentType, entry)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -1571,6 +1584,13 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
configOptions: rdyConn.configOptions,
|
configOptions: rdyConn.configOptions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// Clean up stale localStorage prefs for agents that genuinely
|
||||||
|
// no longer provide modes or config options.
|
||||||
|
if (rdyConn) {
|
||||||
|
const hasModes = (rdyConn.modes?.available_modes.length ?? 0) > 0
|
||||||
|
const hasConfig = (rdyConn.configOptions?.length ?? 0) > 0
|
||||||
|
clearStalePrefs(rdyConn.agentType, hasModes, hasConfig)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "prompt_capabilities":
|
case "prompt_capabilities":
|
||||||
@@ -1956,6 +1976,15 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
const setMode = useCallback(async (contextKey: string, modeId: string) => {
|
const setMode = useCallback(async (contextKey: string, modeId: string) => {
|
||||||
const conn = storeRef.current.connections.get(contextKey)
|
const conn = storeRef.current.connections.get(contextKey)
|
||||||
if (!conn) return
|
if (!conn) return
|
||||||
|
// Persist user's mode selection to localStorage
|
||||||
|
const modes =
|
||||||
|
conn.modes ?? selectorsCache.get(conn.agentType)?.modes ?? null
|
||||||
|
if (modes) {
|
||||||
|
saveModePreference(conn.agentType, {
|
||||||
|
...modes,
|
||||||
|
current_mode_id: modeId,
|
||||||
|
})
|
||||||
|
}
|
||||||
lastActivityRef.current.set(contextKey, Date.now())
|
lastActivityRef.current.set(contextKey, Date.now())
|
||||||
await acpSetMode(conn.connectionId, modeId)
|
await acpSetMode(conn.connectionId, modeId)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -1970,6 +1999,14 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
configId,
|
configId,
|
||||||
valueId,
|
valueId,
|
||||||
})
|
})
|
||||||
|
// Persist user selection to localStorage
|
||||||
|
const updatedConn = storeRef.current.connections.get(contextKey)
|
||||||
|
const allOptions =
|
||||||
|
updatedConn?.configOptions ??
|
||||||
|
selectorsCache.get(conn.agentType)?.configOptions
|
||||||
|
if (allOptions) {
|
||||||
|
saveConfigPreference(conn.agentType, configId, valueId, allOptions)
|
||||||
|
}
|
||||||
lastActivityRef.current.set(contextKey, Date.now())
|
lastActivityRef.current.set(contextKey, Date.now())
|
||||||
await acpSetConfigOption(conn.connectionId, configId, valueId)
|
await acpSetConfigOption(conn.connectionId, configId, valueId)
|
||||||
},
|
},
|
||||||
|
|||||||
221
src/lib/selector-prefs-storage.ts
Normal file
221
src/lib/selector-prefs-storage.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists user's selector preferences (mode & config option selections)
|
||||||
|
* per agentType to localStorage, so they survive session restarts.
|
||||||
|
*
|
||||||
|
* Structure hash is stored alongside values — when the available options
|
||||||
|
* change (new/removed/renamed items) the saved prefs are discarded.
|
||||||
|
*
|
||||||
|
* Agents may emit empty selectors during early init (config_options=None
|
||||||
|
* becomes []), followed by real selectors later. We therefore only apply
|
||||||
|
* or invalidate prefs when incoming data is non-empty. Stale prefs for
|
||||||
|
* agents that genuinely lose all selectors are cleaned up at
|
||||||
|
* `selectors_ready` via `clearStalePrefs()`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SessionConfigOptionInfo, SessionModeStateInfo } from "@/lib/types"
|
||||||
|
|
||||||
|
const STORAGE_KEY = "codeg:selector-prefs"
|
||||||
|
|
||||||
|
interface SelectorPrefs {
|
||||||
|
modeId?: string
|
||||||
|
modesHash?: string
|
||||||
|
configValues?: Record<string, string>
|
||||||
|
configHash?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllPrefs = Record<string, SelectorPrefs>
|
||||||
|
|
||||||
|
function readAll(): AllPrefs {
|
||||||
|
if (typeof window === "undefined") return {}
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
return raw ? (JSON.parse(raw) as AllPrefs) : {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeAll(all: AllPrefs) {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(all))
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePrefs(
|
||||||
|
agentType: string,
|
||||||
|
fn: (prefs: SelectorPrefs) => SelectorPrefs
|
||||||
|
) {
|
||||||
|
const all = readAll()
|
||||||
|
all[agentType] = fn(all[agentType] ?? {})
|
||||||
|
writeAll(all)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hash helpers ──
|
||||||
|
|
||||||
|
function hashModes(modes: SessionModeStateInfo): string {
|
||||||
|
return modes.available_modes.map((m) => m.id).join("\0")
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashConfigOptions(options: SessionConfigOptionInfo[]): string {
|
||||||
|
return options
|
||||||
|
.map((o) => {
|
||||||
|
if (o.kind.type !== "select") return o.id
|
||||||
|
const vals = o.kind.options.map((v) => v.value).join(",")
|
||||||
|
return `${o.id}:${vals}`
|
||||||
|
})
|
||||||
|
.join("\0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Read ──
|
||||||
|
|
||||||
|
/** Read saved mode id for an agent (no validation, just the raw value). */
|
||||||
|
export function getSavedModeId(agentType: string): string | null {
|
||||||
|
const all = readAll()
|
||||||
|
return all[agentType]?.modeId ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save (user actions only) ──
|
||||||
|
|
||||||
|
export function saveModePreference(
|
||||||
|
agentType: string,
|
||||||
|
modes: SessionModeStateInfo
|
||||||
|
) {
|
||||||
|
updatePrefs(agentType, (prefs) => ({
|
||||||
|
...prefs,
|
||||||
|
modeId: modes.current_mode_id,
|
||||||
|
modesHash: hashModes(modes),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveConfigPreference(
|
||||||
|
agentType: string,
|
||||||
|
configId: string,
|
||||||
|
valueId: string,
|
||||||
|
allOptions: SessionConfigOptionInfo[]
|
||||||
|
) {
|
||||||
|
updatePrefs(agentType, (prefs) => ({
|
||||||
|
...prefs,
|
||||||
|
configValues: { ...prefs.configValues, [configId]: valueId },
|
||||||
|
configHash: hashConfigOptions(allOptions),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Apply (on incoming server events) ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply saved mode preference to incoming server modes.
|
||||||
|
* Skips empty mode lists (agent still initializing).
|
||||||
|
* Clears prefs when structure genuinely changes.
|
||||||
|
*/
|
||||||
|
export function applySavedModePreference(
|
||||||
|
agentType: string,
|
||||||
|
modes: SessionModeStateInfo
|
||||||
|
): SessionModeStateInfo {
|
||||||
|
const all = readAll()
|
||||||
|
const prefs = all[agentType]
|
||||||
|
if (!prefs?.modeId || !prefs.modesHash) return modes
|
||||||
|
if (modes.available_modes.length === 0) return modes
|
||||||
|
|
||||||
|
const incomingHash = hashModes(modes)
|
||||||
|
if (prefs.modesHash !== incomingHash) {
|
||||||
|
delete prefs.modeId
|
||||||
|
delete prefs.modesHash
|
||||||
|
all[agentType] = prefs
|
||||||
|
writeAll(all)
|
||||||
|
return modes
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modes.available_modes.some((m) => m.id === prefs.modeId)) {
|
||||||
|
return modes
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modes.current_mode_id === prefs.modeId) return modes
|
||||||
|
|
||||||
|
return { ...modes, current_mode_id: prefs.modeId! }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply saved config option preferences to incoming server config options.
|
||||||
|
* Skips empty option lists (agent still initializing).
|
||||||
|
* Clears prefs when structure genuinely changes.
|
||||||
|
*/
|
||||||
|
export function applySavedConfigPreferences(
|
||||||
|
agentType: string,
|
||||||
|
options: SessionConfigOptionInfo[]
|
||||||
|
): SessionConfigOptionInfo[] {
|
||||||
|
const all = readAll()
|
||||||
|
const prefs = all[agentType]
|
||||||
|
if (!prefs?.configValues || !prefs.configHash) return options
|
||||||
|
if (options.length === 0) return options
|
||||||
|
|
||||||
|
const incomingHash = hashConfigOptions(options)
|
||||||
|
if (prefs.configHash !== incomingHash) {
|
||||||
|
delete prefs.configValues
|
||||||
|
delete prefs.configHash
|
||||||
|
all[agentType] = prefs
|
||||||
|
writeAll(all)
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
const merged = options.map((opt) => {
|
||||||
|
if (opt.kind.type !== "select") return opt
|
||||||
|
const savedValue = prefs.configValues![opt.id]
|
||||||
|
if (!savedValue || savedValue === opt.kind.current_value) return opt
|
||||||
|
if (!opt.kind.options.some((o) => o.value === savedValue)) return opt
|
||||||
|
changed = true
|
||||||
|
return {
|
||||||
|
...opt,
|
||||||
|
kind: { ...opt.kind, current_value: savedValue },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return changed ? merged : options
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cleanup (called at selectors_ready) ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when `selectors_ready` fires — initialization is complete.
|
||||||
|
* If the agent ended up with no modes / no config options, clear
|
||||||
|
* any stale saved prefs for that category so they don't linger forever.
|
||||||
|
*/
|
||||||
|
export function clearStalePrefs(
|
||||||
|
agentType: string,
|
||||||
|
hasModes: boolean,
|
||||||
|
hasConfigOptions: boolean
|
||||||
|
) {
|
||||||
|
const all = readAll()
|
||||||
|
const prefs = all[agentType]
|
||||||
|
if (!prefs) return
|
||||||
|
|
||||||
|
let dirty = false
|
||||||
|
if (!hasModes && (prefs.modeId || prefs.modesHash)) {
|
||||||
|
delete prefs.modeId
|
||||||
|
delete prefs.modesHash
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
if (!hasConfigOptions && (prefs.configValues || prefs.configHash)) {
|
||||||
|
delete prefs.configValues
|
||||||
|
delete prefs.configHash
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
if (!dirty) return
|
||||||
|
|
||||||
|
const isEmpty =
|
||||||
|
!prefs.modeId &&
|
||||||
|
!prefs.modesHash &&
|
||||||
|
!prefs.configValues &&
|
||||||
|
!prefs.configHash
|
||||||
|
if (isEmpty) {
|
||||||
|
delete all[agentType]
|
||||||
|
} else {
|
||||||
|
all[agentType] = prefs
|
||||||
|
}
|
||||||
|
writeAll(all)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user