Skip to content

PromptManager: Stages tab silently discards unsaved template and config edits when switching stages #6021

Description

@atomantic

Problem

In client/src/pages/PromptManager.jsx:607-613, selecting another stage in the prompt stages list directly calls setSelectedStage(name):

{groupStages.map(([name, config]) => (
  <button
    key={name}
    onClick={() => setSelectedStage(name)}
...

Unlike the Job Skills tab in the same page (which implements an inline confirm row via isJobSkillDirty and pendingJobSkill, added in #3939), the Stages tab has no dirty tracking or unsaved edits guard. When selectedStage updates in the URL, the useEffect hook at client/src/pages/PromptManager.jsx:171-195 immediately triggers:

useEffect(() => {
  if (!selectedStage) { setStageTemplate(''); setStageConfig({}); setPreview(''); return; }
  let cancelled = false;
  getPrompt(selectedStage, { silent: true })
    .then(res => {
      if (cancelled || !res) return;
      setStageTemplate(res.template || '');
      const cfg = { name: res.name, description: res.description, model: res.model, provider: res.provider || null, variables: res.variables || [] };
      const timeout = parseTimeoutMs(res.timeout);
      if (timeout !== null) cfg.timeout = timeout;
      setStageConfig(cfg);
      setPreview('');
    })
    ...

Any unsaved modifications made to stageTemplate, model/tier selections, or timeout overrides in stageConfig are immediately and silently overwritten with the newly clicked stage's data.

Trigger

  1. Navigate to /prompts (or /prompts?stage=pipeline-prose-draft).
  2. In the Stages editor, make changes to the prompt template in the textarea (or alter the Model tier / Specific provider, or modify the timeout override).
  3. Without clicking "Save", click any other stage in the accordion sidebar list (e.g. pipeline-comic-script or brain-classifier).
  4. setSelectedStage(name) runs synchronously, triggering a fetch for the new stage and obliterating all unsaved template and configuration edits.

Impact

Prompt templates are extensive, delicate prompts often containing hundreds of lines of instructions and variable interpolation markup. A user who inadvertently clicks a neighboring row in the 120+ item stage list, or clicks another stage to reference its wording, permanently loses their in-progress work with no warning, confirmation, or undo mechanism.

Fix

Adopt the same dirty-state and inline confirmation pattern proven in the Job Skills tab (#3939):

  1. In client/src/pages/PromptManager.jsx:
    • Store baseline state for the loaded stage: savedStageTemplate (string) and savedStageConfig (object), populated when getPrompt resolves.
    • Compute isStageDirty = Boolean(selectedStage) && (stageTemplate !== savedStageTemplate || JSON.stringify(stageConfig) !== JSON.stringify(savedStageConfig)).
    • Add state pendingStage (nullable string), cleared when isStageDirty is false.
    • When a stage row is clicked:
      • If name === selectedStage, clear pendingStage.
      • If isStageDirty, set setPendingStage(name) instead of calling setSelectedStage(name).
      • If clean, call setSelectedStage(name).
    • In the stage list items:
      • If pendingStage === name && isStageDirty, render InlineConfirmRow inside the list slot with question={Discard unsaved changes to "${stages[selectedStage]?.name || selectedStage}"?}, confirmText="Discard", cancelText="Keep editing". On confirm, switch to pendingStage and clear pendingStage; on cancel, clear pendingStage.
      • When selectedStage === name && isStageDirty, render an <span className="shrink-0 text-[10px] px-1.5 py-0.5 bg-port-warning/20 text-port-warning rounded uppercase font-semibold">Unsaved</span> badge beside the stage name.
    • In the stage editor header, display {isStageDirty && <span className="text-port-warning">Unsaved changes</span>}.
    • In saveStage, update savedStageTemplate and savedStageConfig on successful PUT so the dirty state resets cleanly.
  2. Rejected alternative: A browser-native window.confirm modal. Rejected per client UI convention (client/src/AGENTS.md: "No window.alert/confirm - use inline confirmations or toast notifications").

Dispatch rationale: model:medium + effort:medium — requires coordinating state between grouped accordion stage rows, dirty computation over template and config objects, inline confirmation components, and companion vitest assertions without regressing URL-driven deep linking.

Files to touch:

  • client/src/pages/PromptManager.jsx
  • client/src/pages/PromptManager.test.jsx (add test suite mirroring the job skill unsaved-edit guard tests for stages)

Acceptance criteria

  • Modifying a prompt stage's template or configuration marks the stage editor as dirty and renders the "Unsaved changes" header badge and "Unsaved" list row badge.
  • Clicking another stage in the list while edits are unsaved displays an InlineConfirmRow asking whether to discard unsaved changes, without switching the open stage or URL.
  • Clicking "Keep editing" (or reverting edits back to saved content) dismisses the confirmation row and keeps all in-progress edits intact.
  • Clicking "Discard" confirms discarding and navigates to the newly selected stage.
  • Saving the stage updates the saved baseline and resets the dirty state.
  • Existing stage list and keyboard navigation tests continue to pass.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:uiUI components and stylingbugSomething isn't workingeffort:mediumEffort: mediummodel:mediumModel size: mediumplanTracked by /do:replanplanner:gemini-3-7-flash-highPlan authored by the gemini-3-7-flash-high modeluxProposed from a UX/design audit

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions