mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
98113182b5
* feat(guide): reviewer-supplied extra instructions for Guided Review (#1265) Adds a quiet, collapsed-by-default Custom instructions affordance to the guide launch page. The text is APPENDED to the built-in organizer methodology as a clearly delimited section (composeGuideMethodology) and never replaces it; absent or blank instructions produce byte-identical prompts to before. Persisted in a dedicated cookie (plannotator-guide-instructions) so a standing team preference survives sessions without bloating the plannotator.agents blob past the browser's per-cookie limit. Server side, the launch body gains an optional guide-only instructions field (both the Bun and Pi node:http agent-jobs handlers accept and thread it); prompt composition lives in the shared guide-review.ts that vendor.sh already vendors to Pi, so both runtimes compose identically. Text is capped at GUIDE_EXTRA_INSTRUCTIONS_MAX_CHARS (2000) server-side and mirrored by the textarea maxLength. Repair launches deliberately ignore instructions: a repair is a mechanical JSON fix, not a rewrite. Tests pin the regression contract (empty input keeps prior prompt bytes), appended-not-replacing composition, the length cap, repair isolation, and the cookie round-trip via the storage backend seam. * refactor(guide): store standing instructions server-side, not in a cookie Review findings on the cookie approach (silent write failure past the encoded 4KB per-cookie limit for multi-byte text) pointed at the real design problem: the instructions are consumed by the SERVER at launch time, so they belong in the data dir like review-skills.json, where no size ceiling or encoding inflation exists and the preference follows the machine instead of one browser profile. New GET/PUT /api/agents/guide-instructions in both runtimes backed by shared guide-instructions-store (vendored to Pi). Guide launches apply the stored text when the body carries none; the launch page still sends its live textarea value (explicit wins), so a just-typed preference can never race the debounced save. The sidebar surface sends nothing and inherits the stored text server-side. All cookie machinery removed. Also folds in the review fixes: marker-tag-shaped strings in instructions are defanged so first-match nonce recovery cannot be hijacked by pasted examples.
175 lines
7.6 KiB
TypeScript
175 lines
7.6 KiB
TypeScript
import type { AgentCapabilities } from '@plannotator/ui/types';
|
|
import type { AgentLaunchParams } from '@plannotator/ui/hooks/useAgentJobs';
|
|
import { useAgentSettings } from '@plannotator/ui/hooks/useAgentSettings';
|
|
import type { ReviewEngine } from '@plannotator/ui/hooks/useAgentSettings';
|
|
import { REVIEW_ENGINE_LABEL } from '@plannotator/ui/components/AgentsTab';
|
|
|
|
export const GUIDE_ENGINES = Object.keys(REVIEW_ENGINE_LABEL) as ReviewEngine[];
|
|
|
|
// Marker-engine fallbacks until the server delivers the live catalogs on the
|
|
// capability entries (mirrors AgentsTab's fallbacks).
|
|
const CURSOR_FALLBACK = [{ value: 'auto', label: 'Auto' }];
|
|
const OPENCODE_FALLBACK = [{ value: '', label: 'Default' }];
|
|
const PI_FALLBACK = [{ value: '', label: 'Default' }];
|
|
const COPILOT_FALLBACK = [{ value: '', label: 'Default' }];
|
|
|
|
export type GuideModelOption = { value: string; label: string };
|
|
|
|
export interface GuideLaunchState {
|
|
/** The persisted agent settings bundle (pickers read/write through this). */
|
|
settings: ReturnType<typeof useAgentSettings>;
|
|
guideAvailable: boolean;
|
|
availableEngines: ReviewEngine[];
|
|
/** Effective engine: the persisted choice, snapped to an available one. */
|
|
engine: ReviewEngine;
|
|
cursorOptions: GuideModelOption[];
|
|
opencodeOptions: GuideModelOption[];
|
|
piOptions: GuideModelOption[];
|
|
copilotOptions: GuideModelOption[];
|
|
effectiveCursorModel: string;
|
|
effectiveOpencodeModel: string;
|
|
effectivePiModel: string;
|
|
effectiveCopilotModel: string;
|
|
/** Whether a guide launch can be attempted at all on this machine. */
|
|
canLaunch: boolean;
|
|
/** Build the launch params for the current effective engine + models.
|
|
* `instructions` is the caller's LIVE textarea value (GuideEmptyState),
|
|
* sent so a just-typed preference can never race the server-side save;
|
|
* callers without an editor (GuideView Regenerate) pass nothing and the
|
|
* server applies its stored standing instructions. */
|
|
buildParams: (instructions?: string) => AgentLaunchParams;
|
|
}
|
|
|
|
/**
|
|
* Shared guide-launch defaults: resolves the persisted engine/model settings
|
|
* against the live capability catalogs and builds the exact AgentLaunchParams
|
|
* shape the server expects (one shape per engine, mirroring AgentsTab's
|
|
* buildGuideLaunch). Used by GuideEmptyState's Generate button and by
|
|
* GuideView's "Regenerate" hint on an outdated saved guide, so both launch
|
|
* surfaces stay in lockstep.
|
|
*/
|
|
export function useGuideLaunch(capabilities: AgentCapabilities | null): GuideLaunchState {
|
|
const settings = useAgentSettings();
|
|
const {
|
|
guideEngine,
|
|
guideClaudeModel,
|
|
guideClaudeEffort,
|
|
guideCodexModel,
|
|
guideCodexReasoning,
|
|
guideCursorModel,
|
|
guideOpencodeModel,
|
|
guidePiModel,
|
|
guidePiThinking,
|
|
guideCopilotModel,
|
|
} = settings;
|
|
|
|
const providerAvailable = (id: string) =>
|
|
capabilities?.providers.some((p) => p.id === id && p.available) ?? false;
|
|
const guideAvailable = providerAvailable('guide');
|
|
const availableEngines = GUIDE_ENGINES.filter(providerAvailable);
|
|
// A persisted engine can be unavailable on this machine — fall back to the
|
|
// first available one rather than a dead selection.
|
|
const engine: ReviewEngine = providerAvailable(guideEngine) ? guideEngine : (availableEngines[0] ?? guideEngine);
|
|
|
|
// Marker model catalogs are discovered server-side and delivered on the
|
|
// capability entry; fall back to the engine-default option until then.
|
|
// Mirrors AgentsTab's per-engine catalog semantics exactly: opencode/pi
|
|
// PREPEND their engine-managed "Default" ('' value) to the discovered list —
|
|
// the discovered catalogs are real models only, and dropping Default left a
|
|
// saved-default user with a blank pill and no way back after picking a
|
|
// concrete model. Cursor REPLACES: its discovered list natively includes
|
|
// 'auto', so prepending would duplicate it.
|
|
const markerModels = (id: 'cursor' | 'opencode' | 'pi' | 'copilot', fallback: GuideModelOption[]): GuideModelOption[] => {
|
|
const models = capabilities?.providers.find((p) => p.id === id)?.models;
|
|
if (!models || models.length === 0) return fallback;
|
|
const discovered = models.map((m) => ({ value: m.id, label: m.label }));
|
|
return id === 'cursor' ? discovered : [...fallback, ...discovered];
|
|
};
|
|
|
|
const cursorOptions = markerModels('cursor', CURSOR_FALLBACK);
|
|
const opencodeOptions = markerModels('opencode', OPENCODE_FALLBACK);
|
|
const piOptions = markerModels('pi', PI_FALLBACK);
|
|
const copilotOptions = markerModels('copilot', COPILOT_FALLBACK);
|
|
|
|
// A stale saved model id is reconciled at read time: if it's not in the
|
|
// current catalog, fall back to the catalog's first entry rather than
|
|
// POSTing a dead model id to the server. ('' / Default is present in the
|
|
// opencode/pi catalogs after the prepend above, so a saved '' stays valid.)
|
|
const effectiveModel = (saved: string, options: GuideModelOption[]): string =>
|
|
options.some((o) => o.value === saved) ? saved : (options[0]?.value ?? saved);
|
|
|
|
const effectiveCursorModel = effectiveModel(guideCursorModel, cursorOptions);
|
|
const effectiveOpencodeModel = effectiveModel(guideOpencodeModel, opencodeOptions);
|
|
const effectivePiModel = effectiveModel(guidePiModel, piOptions);
|
|
const effectiveCopilotModel = effectiveModel(guideCopilotModel, copilotOptions);
|
|
|
|
// Config shapes mirror AgentsTab's buildGuideLaunch exactly — one shape
|
|
// per engine, so the server sees identical launches from every surface.
|
|
// Extra instructions (#1265) are server-stored; only the launch page's
|
|
// live editor value rides the body (explicit wins server-side), so a
|
|
// surface without an editor omits the field and the server applies the
|
|
// stored standing instructions itself.
|
|
const buildParams = (instructions?: string): AgentLaunchParams => {
|
|
const live = instructions?.trim();
|
|
const params = buildEngineParams();
|
|
return live ? { ...params, instructions: live } : params;
|
|
};
|
|
|
|
const buildEngineParams = (): AgentLaunchParams =>
|
|
engine === 'cursor'
|
|
? {
|
|
provider: 'guide',
|
|
label: 'Guided Review',
|
|
engine: 'cursor',
|
|
...(effectiveCursorModel && effectiveCursorModel.toLowerCase() !== 'auto' ? { model: effectiveCursorModel } : {}),
|
|
}
|
|
: engine === 'opencode'
|
|
? {
|
|
provider: 'guide',
|
|
label: 'Guided Review',
|
|
engine: 'opencode',
|
|
...(effectiveOpencodeModel ? { model: effectiveOpencodeModel } : {}),
|
|
}
|
|
: engine === 'pi'
|
|
? {
|
|
provider: 'guide',
|
|
label: 'Guided Review',
|
|
engine: 'pi',
|
|
...(effectivePiModel ? { model: effectivePiModel } : {}),
|
|
thinking: guidePiThinking,
|
|
}
|
|
: engine === 'copilot'
|
|
? {
|
|
provider: 'guide',
|
|
label: 'Guided Review',
|
|
engine: 'copilot',
|
|
...(effectiveCopilotModel ? { model: effectiveCopilotModel } : {}),
|
|
}
|
|
: {
|
|
provider: 'guide',
|
|
label: 'Guided Review',
|
|
engine,
|
|
model: engine === 'claude' ? guideClaudeModel : guideCodexModel,
|
|
...(engine === 'claude'
|
|
? { effort: guideClaudeEffort }
|
|
: { reasoningEffort: guideCodexReasoning }),
|
|
};
|
|
|
|
return {
|
|
settings,
|
|
guideAvailable,
|
|
availableEngines,
|
|
engine,
|
|
cursorOptions,
|
|
opencodeOptions,
|
|
piOptions,
|
|
copilotOptions,
|
|
effectiveCursorModel,
|
|
effectiveOpencodeModel,
|
|
effectivePiModel,
|
|
effectiveCopilotModel,
|
|
canLaunch: guideAvailable && availableEngines.length > 0,
|
|
buildParams,
|
|
};
|
|
}
|