mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
affeaa07fc
Custom reviews: pick an Agent Skill and its SKILL.md body becomes the review prompt (read live from global skill folders, never copied; default review byte-identical). Agent findings can be a line, a whole file, or a general review-level comment, and are never silently dropped. Plus: agent-icon engine picker, icon-only file-header opener, semantic diff restored as its own dock panel, and a dead-code sweep.
255 lines
8.4 KiB
TypeScript
255 lines
8.4 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { getItem, setItem } from '../utils/storage';
|
|
|
|
const COOKIE_KEY = 'plannotator.agents';
|
|
|
|
export const DEFAULT_CLAUDE_MODEL = 'claude-opus-4-7';
|
|
export const DEFAULT_CLAUDE_EFFORT = 'high';
|
|
export const DEFAULT_CODEX_MODEL = 'gpt-5.3-codex';
|
|
export const DEFAULT_CODEX_REASONING = 'high';
|
|
export const DEFAULT_CODEX_FAST = false;
|
|
export const DEFAULT_TOUR_CLAUDE_MODEL = 'sonnet';
|
|
export const DEFAULT_TOUR_CLAUDE_EFFORT = 'medium';
|
|
export const DEFAULT_TOUR_CODEX_MODEL = 'gpt-5.3-codex';
|
|
export const DEFAULT_TOUR_CODEX_REASONING = 'medium';
|
|
export const DEFAULT_TOUR_CODEX_FAST = false;
|
|
|
|
interface ClaudeSection {
|
|
model: string;
|
|
perModel: Record<string, { effort: string }>;
|
|
}
|
|
|
|
interface CodexSection {
|
|
model: string;
|
|
perModel: Record<string, { reasoning: string; fast: boolean }>;
|
|
}
|
|
|
|
export type AgentMode = 'review' | 'tour';
|
|
export type AgentEngine = 'claude' | 'codex';
|
|
|
|
interface AgentSettingsState {
|
|
selectedMode?: AgentMode;
|
|
reviewEngine: AgentEngine;
|
|
reviewProfileId: string;
|
|
tourEngine: AgentEngine;
|
|
claude: ClaudeSection;
|
|
codex: CodexSection;
|
|
tourClaude: ClaudeSection;
|
|
tourCodex: CodexSection;
|
|
}
|
|
|
|
const initialState: AgentSettingsState = {
|
|
selectedMode: 'review',
|
|
reviewEngine: 'claude',
|
|
reviewProfileId: 'builtin:default',
|
|
tourEngine: 'claude',
|
|
claude: { model: DEFAULT_CLAUDE_MODEL, perModel: {} },
|
|
codex: { model: DEFAULT_CODEX_MODEL, perModel: {} },
|
|
tourClaude: { model: DEFAULT_TOUR_CLAUDE_MODEL, perModel: {} },
|
|
tourCodex: { model: DEFAULT_TOUR_CODEX_MODEL, perModel: {} },
|
|
};
|
|
|
|
// One-shot migration: drop any cached "none" codex reasoning entries. The
|
|
// dropdown no longer offers "None" (codex-rs rejects it as a config value);
|
|
// fall back to the default instead of shipping an invalid flag.
|
|
export function sanitizeCodexPerModel(
|
|
perModel: Record<string, { reasoning: string; fast: boolean }> | undefined,
|
|
): Record<string, { reasoning: string; fast: boolean }> {
|
|
if (!perModel) return {};
|
|
const out: Record<string, { reasoning: string; fast: boolean }> = {};
|
|
for (const [model, entry] of Object.entries(perModel)) {
|
|
if (!entry || typeof entry !== 'object') continue;
|
|
if (entry.reasoning === 'none') {
|
|
if (entry.fast) out[model] = { reasoning: DEFAULT_CODEX_REASONING, fast: true };
|
|
continue;
|
|
}
|
|
out[model] = entry;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function parseEngine(value: unknown): AgentEngine {
|
|
return value === 'codex' ? 'codex' : 'claude';
|
|
}
|
|
|
|
function parseMode(value: unknown): AgentMode | undefined {
|
|
if (value === 'review' || value === 'tour') return value;
|
|
return undefined;
|
|
}
|
|
|
|
function readCookie(): AgentSettingsState {
|
|
const raw = getItem(COOKIE_KEY);
|
|
if (!raw) return initialState;
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
return {
|
|
selectedMode: parseMode(parsed.selectedMode) ?? initialState.selectedMode,
|
|
reviewEngine: parseEngine(parsed.reviewEngine),
|
|
reviewProfileId: typeof parsed.reviewProfileId === 'string' ? parsed.reviewProfileId : 'builtin:default',
|
|
tourEngine: parseEngine(parsed.tourEngine),
|
|
claude: {
|
|
model: typeof parsed.claude?.model === 'string' ? parsed.claude.model : DEFAULT_CLAUDE_MODEL,
|
|
perModel: parsed.claude?.perModel ?? {},
|
|
},
|
|
codex: {
|
|
model: typeof parsed.codex?.model === 'string' ? parsed.codex.model : DEFAULT_CODEX_MODEL,
|
|
perModel: sanitizeCodexPerModel(parsed.codex?.perModel),
|
|
},
|
|
tourClaude: {
|
|
model: typeof parsed.tourClaude?.model === 'string' ? parsed.tourClaude.model : DEFAULT_TOUR_CLAUDE_MODEL,
|
|
perModel: parsed.tourClaude?.perModel ?? {},
|
|
},
|
|
tourCodex: {
|
|
model: typeof parsed.tourCodex?.model === 'string' ? parsed.tourCodex.model : DEFAULT_TOUR_CODEX_MODEL,
|
|
perModel: sanitizeCodexPerModel(parsed.tourCodex?.perModel),
|
|
},
|
|
};
|
|
} catch {
|
|
return initialState;
|
|
}
|
|
}
|
|
|
|
export function useAgentSettings() {
|
|
const [state, setState] = useState<AgentSettingsState>(readCookie);
|
|
|
|
useEffect(() => {
|
|
setItem(COOKIE_KEY, JSON.stringify(state));
|
|
}, [state]);
|
|
|
|
const setSelectedMode = useCallback((mode: AgentMode) => {
|
|
setState((s) => ({ ...s, selectedMode: mode }));
|
|
}, []);
|
|
|
|
const setReviewEngine = useCallback((engine: AgentEngine) => {
|
|
setState((s) => ({ ...s, reviewEngine: engine }));
|
|
}, []);
|
|
|
|
const setReviewProfileId = useCallback((id: string) => {
|
|
setState((s) => ({ ...s, reviewProfileId: id }));
|
|
}, []);
|
|
|
|
const setTourEngine = useCallback((engine: AgentEngine) => {
|
|
setState((s) => ({ ...s, tourEngine: engine }));
|
|
}, []);
|
|
|
|
const setClaudeModel = useCallback((model: string) => {
|
|
setState((s) => ({ ...s, claude: { ...s.claude, model } }));
|
|
}, []);
|
|
|
|
const patchClaude = useCallback(
|
|
(section: 'claude' | 'tourClaude', patch: Partial<{ effort: string }>) => {
|
|
setState((s) => {
|
|
const cur = s[section];
|
|
const prev = cur.perModel[cur.model] ?? { effort: '' };
|
|
return {
|
|
...s,
|
|
[section]: {
|
|
...cur,
|
|
perModel: { ...cur.perModel, [cur.model]: { ...prev, ...patch } },
|
|
},
|
|
};
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const setClaudeEffort = useCallback(
|
|
(effort: string) => patchClaude('claude', { effort }),
|
|
[patchClaude],
|
|
);
|
|
|
|
const setCodexModel = useCallback((model: string) => {
|
|
setState((s) => ({ ...s, codex: { ...s.codex, model } }));
|
|
}, []);
|
|
|
|
const patchCodex = useCallback(
|
|
(
|
|
section: 'codex' | 'tourCodex',
|
|
patch: Partial<{ reasoning: string; fast: boolean }>,
|
|
defaults: { reasoning: string; fast: boolean },
|
|
) => {
|
|
setState((s) => {
|
|
const cur = s[section];
|
|
const prev = cur.perModel[cur.model] ?? defaults;
|
|
return {
|
|
...s,
|
|
[section]: {
|
|
...cur,
|
|
perModel: { ...cur.perModel, [cur.model]: { ...prev, ...patch } },
|
|
},
|
|
};
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const setCodexReasoning = useCallback(
|
|
(reasoning: string) => patchCodex('codex', { reasoning }, { reasoning: DEFAULT_CODEX_REASONING, fast: DEFAULT_CODEX_FAST }),
|
|
[patchCodex],
|
|
);
|
|
const setCodexFast = useCallback(
|
|
(fast: boolean) => patchCodex('codex', { fast }, { reasoning: DEFAULT_CODEX_REASONING, fast: DEFAULT_CODEX_FAST }),
|
|
[patchCodex],
|
|
);
|
|
|
|
const setTourClaudeModel = useCallback((model: string) => {
|
|
setState((s) => ({ ...s, tourClaude: { ...s.tourClaude, model } }));
|
|
}, []);
|
|
|
|
const setTourClaudeEffort = useCallback(
|
|
(effort: string) => patchClaude('tourClaude', { effort }),
|
|
[patchClaude],
|
|
);
|
|
|
|
const setTourCodexModel = useCallback((model: string) => {
|
|
setState((s) => ({ ...s, tourCodex: { ...s.tourCodex, model } }));
|
|
}, []);
|
|
|
|
const setTourCodexReasoning = useCallback(
|
|
(reasoning: string) => patchCodex('tourCodex', { reasoning }, { reasoning: DEFAULT_TOUR_CODEX_REASONING, fast: DEFAULT_TOUR_CODEX_FAST }),
|
|
[patchCodex],
|
|
);
|
|
const setTourCodexFast = useCallback(
|
|
(fast: boolean) => patchCodex('tourCodex', { fast }, { reasoning: DEFAULT_TOUR_CODEX_REASONING, fast: DEFAULT_TOUR_CODEX_FAST }),
|
|
[patchCodex],
|
|
);
|
|
|
|
const claudeEffort = state.claude.perModel[state.claude.model]?.effort ?? DEFAULT_CLAUDE_EFFORT;
|
|
const codexReasoning = state.codex.perModel[state.codex.model]?.reasoning ?? DEFAULT_CODEX_REASONING;
|
|
const codexFast = state.codex.perModel[state.codex.model]?.fast ?? DEFAULT_CODEX_FAST;
|
|
const tourClaudeEffort = state.tourClaude.perModel[state.tourClaude.model]?.effort ?? DEFAULT_TOUR_CLAUDE_EFFORT;
|
|
const tourCodexReasoning = state.tourCodex.perModel[state.tourCodex.model]?.reasoning ?? DEFAULT_TOUR_CODEX_REASONING;
|
|
const tourCodexFast = state.tourCodex.perModel[state.tourCodex.model]?.fast ?? DEFAULT_TOUR_CODEX_FAST;
|
|
|
|
return {
|
|
selectedMode: state.selectedMode,
|
|
reviewEngine: state.reviewEngine,
|
|
reviewProfileId: state.reviewProfileId,
|
|
tourEngine: state.tourEngine,
|
|
claudeModel: state.claude.model,
|
|
claudeEffort,
|
|
codexModel: state.codex.model,
|
|
codexReasoning,
|
|
codexFast,
|
|
tourClaudeModel: state.tourClaude.model,
|
|
tourClaudeEffort,
|
|
tourCodexModel: state.tourCodex.model,
|
|
tourCodexReasoning,
|
|
tourCodexFast,
|
|
setSelectedMode,
|
|
setReviewEngine,
|
|
setReviewProfileId,
|
|
setTourEngine,
|
|
setClaudeModel,
|
|
setClaudeEffort,
|
|
setCodexModel,
|
|
setCodexReasoning,
|
|
setCodexFast,
|
|
setTourClaudeModel,
|
|
setTourClaudeEffort,
|
|
setTourCodexModel,
|
|
setTourCodexReasoning,
|
|
setTourCodexFast,
|
|
};
|
|
}
|