现在会话输入框会记住用户的配置选项和模型选择,避免每次都要重新勾选
This commit is contained in:
@@ -47,6 +47,10 @@ import {
|
||||
type MessageTurn,
|
||||
type PromptDraft,
|
||||
} from "@/lib/types"
|
||||
import {
|
||||
getSavedModeId,
|
||||
saveModePreference,
|
||||
} from "@/lib/selector-prefs-storage"
|
||||
import {
|
||||
buildConversationDraftStorageKey,
|
||||
buildNewConversationDraftStorageKey,
|
||||
@@ -724,7 +728,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
||||
if (dbConvIdRef.current) return
|
||||
|
||||
setDraftAgentType(nextAgentType)
|
||||
setModeId(null)
|
||||
setModeId(getSavedModeId(nextAgentType))
|
||||
setAgentConnectError(null)
|
||||
|
||||
const s = connStatusRef.current
|
||||
@@ -759,6 +763,20 @@ const ConversationTabView = memo(function ConversationTabView({
|
||||
[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(
|
||||
(answer: string) => {
|
||||
if (connStatus !== "connected") return
|
||||
@@ -853,7 +871,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
||||
configOptionsLoading={configOptionsLoading}
|
||||
selectorsLoading={selectorsLoading}
|
||||
selectedModeId={selectedModeId}
|
||||
onModeChange={setModeId}
|
||||
onModeChange={handleModeChange}
|
||||
onConfigOptionChange={handleSetConfigOption}
|
||||
availableCommands={connectionCommands}
|
||||
attachmentTabId={tabId}
|
||||
@@ -924,7 +942,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
||||
configOptionsLoading={configOptionsLoading}
|
||||
selectorsLoading={selectorsLoading}
|
||||
selectedModeId={selectedModeId}
|
||||
onModeChange={setModeId}
|
||||
onModeChange={handleModeChange}
|
||||
onConfigOptionChange={handleSetConfigOption}
|
||||
availableCommands={connectionCommands}
|
||||
attachmentTabId={tabId}
|
||||
|
||||
@@ -43,6 +43,13 @@ import {
|
||||
IDLE_SWEEP_INTERVAL_MS,
|
||||
} from "@/lib/constants"
|
||||
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 { useFolderContext } from "@/contexts/folder-context"
|
||||
|
||||
@@ -1523,17 +1530,20 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
case "session_modes": {
|
||||
flushStreamingQueue()
|
||||
const modeConn = storeRef.current.connections.get(contextKey)
|
||||
const resolvedModes = modeConn
|
||||
? applySavedModePreference(modeConn.agentType, e.modes)
|
||||
: e.modes
|
||||
dispatch({
|
||||
type: "SESSION_MODES",
|
||||
contextKey,
|
||||
modes: e.modes,
|
||||
modes: resolvedModes,
|
||||
})
|
||||
if (modeConn) {
|
||||
const entry = selectorsCache.get(modeConn.agentType) ?? {
|
||||
modes: null,
|
||||
configOptions: null,
|
||||
}
|
||||
entry.modes = e.modes
|
||||
entry.modes = resolvedModes
|
||||
selectorsCache.set(modeConn.agentType, entry)
|
||||
}
|
||||
break
|
||||
@@ -1541,17 +1551,20 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
case "session_config_options": {
|
||||
flushStreamingQueue()
|
||||
const cfgConn = storeRef.current.connections.get(contextKey)
|
||||
const resolvedConfigOptions = cfgConn
|
||||
? applySavedConfigPreferences(cfgConn.agentType, e.config_options)
|
||||
: e.config_options
|
||||
dispatch({
|
||||
type: "SESSION_CONFIG_OPTIONS",
|
||||
contextKey,
|
||||
configOptions: e.config_options,
|
||||
configOptions: resolvedConfigOptions,
|
||||
})
|
||||
if (cfgConn) {
|
||||
const entry = selectorsCache.get(cfgConn.agentType) ?? {
|
||||
modes: null,
|
||||
configOptions: null,
|
||||
}
|
||||
entry.configOptions = e.config_options
|
||||
entry.configOptions = resolvedConfigOptions
|
||||
selectorsCache.set(cfgConn.agentType, entry)
|
||||
}
|
||||
break
|
||||
@@ -1571,6 +1584,13 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
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
|
||||
}
|
||||
case "prompt_capabilities":
|
||||
@@ -1956,6 +1976,15 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
const setMode = useCallback(async (contextKey: string, modeId: string) => {
|
||||
const conn = storeRef.current.connections.get(contextKey)
|
||||
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())
|
||||
await acpSetMode(conn.connectionId, modeId)
|
||||
}, [])
|
||||
@@ -1970,6 +1999,14 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
configId,
|
||||
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())
|
||||
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