pi-design-deck 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  A tool for [Pi coding agent](https://github.com/badlogic/pi-mono/) that presents multi-slide visual decision decks in the browser. Each slide shows 2-4 high-fidelity previews — code diffs, architecture diagrams, UI mockups — and you pick one per slide. The agent gets back a clean selection map and moves on to implementation.
8
8
 
9
+ <img width="1340" alt="Design Deck screenshot" src="https://github.com/user-attachments/assets/20864ac6-9223-4e2e-ba3c-db3eaae0abd8" />
10
+
9
11
  ## Usage
10
12
 
11
13
  Just ask. The agent reaches for the design deck when visual comparison makes sense.
@@ -40,6 +42,8 @@ Restart pi to load the extension and the bundled `design-deck` skill.
40
42
  **Requirements:**
41
43
  - pi-agent v0.35.0 or later (extensions API)
42
44
 
45
+ https://github.com/user-attachments/assets/aff1bac6-8bc2-461a-8828-f588ce655f7f
46
+
43
47
  ## Quick Start
44
48
 
45
49
  The agent builds slides as JSON. Each slide is one decision, each option is one approach:
@@ -104,24 +108,12 @@ The browser opens, the user picks "JWT + Refresh Tokens", and the agent receives
104
108
 
105
109
  ## How It Works
106
110
 
107
- ```
108
- ┌─────────┐ ┌────────────────────────────────────────────┐ ┌─────────┐
109
- │ Agent │ │ Browser Deck │ │ Agent │
110
- │ invokes ├─────►│ ├─────►│ receives │
111
- │design_ │ │ pick → pick → generate more → pick → submit │selections│
112
- │ deck │ │ ↑ ↑ │ └─────────┘
113
- └─────────┘ │ │ SSE push ─┘ heartbeat ────────────┤
114
- └────────────────────────────────────────────┘
115
- ```
116
-
117
- **Lifecycle:**
118
111
  1. Agent calls `design_deck()` with slides JSON — local HTTP server starts, browser opens
119
112
  2. User navigates slides, picks one option per slide
120
113
  3. Optionally clicks "Generate another option" — agent generates and pushes via `add-option`, deck stays open
121
114
  4. User submits — selections returned to agent as `{ slideId: "selected label" }`
122
- 5. Tab auto-closes, thinking level restored to pre-deck value
123
115
 
124
- The server persists across tool re-invocations. When generate-more fires, the tool resolves with instructions for the agent to create a new option and call `design_deck({ action: "add-option", slideId: "...", option: "..." })`. The browser shows a skeleton placeholder with shimmer animation until the new option arrives via SSE.
116
+ The server persists across tool re-invocations. When generate-more fires, the tool resolves with instructions for the agent to create a new option. The browser shows a skeleton placeholder with shimmer animation until the new option arrives via SSE.
125
117
 
126
118
  ## Slides
127
119
 
@@ -188,7 +180,7 @@ The browser shows the new option with an entry animation. The tool blocks again,
188
180
 
189
181
  ### Model Override
190
182
 
191
- The deck shows a model dropdown when 2+ models are available. Users pick which model generates new options. When a model other than the current one is selected, the generate-more result instructs the agent to delegate to a subagent with that model.
183
+ The deck shows a model dropdown when 2+ models are available. Users pick which model generates new options. When a model other than the current one is selected, the generate-more result instructs the agent to use the built-in `deck_generate` tool, which spawns pi headlessly with that model.
192
184
 
193
185
  The default model can be set in the UI (saved to settings) or in `settings.json`:
194
186
 
@@ -296,6 +288,7 @@ Three modes of invocation:
296
288
  pi-design-deck/
297
289
  ├── index.ts # Tool registration, module-level state, lifecycle
298
290
  ├── generate-prompts.ts # Prompt builders for generate-more / regenerate
291
+ ├── model-runner.ts # Headless pi spawner for deck_generate tool
299
292
  ├── deck-schema.ts # TypeScript types and validation (no dependencies)
300
293
  ├── deck-server.ts # HTTP server, SSE, asset serving, snapshots
301
294
  ├── server-utils.ts # Shared HTTP/session utilities
package/deck-server.ts CHANGED
@@ -173,16 +173,16 @@ export interface DeckServerOptions {
173
173
  }
174
174
 
175
175
  export interface DeckServerCallbacks {
176
- onSubmit: (selections: Record<string, string>) => void;
176
+ onSubmit: (selections: Record<string, string>, notes?: Record<string, string>, finalNotes?: string) => void;
177
177
  onCancel: (reason?: "user" | "stale" | "aborted") => void;
178
- onGenerateMore: (slideId: string, prompt?: string, model?: string, thinking?: string) => void;
178
+ onGenerateMore: (slideId: string, prompt?: string, model?: string, thinking?: string, count?: number) => void;
179
179
  onRegenerateSlide: (slideId: string, prompt?: string, model?: string, thinking?: string) => void;
180
180
  }
181
181
 
182
182
  export interface DeckServerHandle {
183
183
  url: string;
184
184
  port: number;
185
- close: () => void;
185
+ close: (reason?: string) => void;
186
186
  pushOption: (slideId: string, option: DeckOption) => void;
187
187
  cancelGenerate: () => void;
188
188
  replaceSlideOptions: (slideId: string, options: DeckOption[]) => void;
@@ -414,12 +414,14 @@ export async function startDeckServer(
414
414
  return;
415
415
  }
416
416
 
417
- const payload = body as { selections?: unknown };
417
+ const payload = body as { selections?: unknown; notes?: unknown; finalNotes?: unknown };
418
418
  const selections = toStringMap(payload.selections);
419
419
  if (!selections) {
420
420
  sendJson(res, 400, { ok: false, error: "Invalid selections payload" });
421
421
  return;
422
422
  }
423
+ const notes = toStringMap(payload.notes) ?? undefined;
424
+ const finalNotes = typeof payload.finalNotes === "string" ? payload.finalNotes.trim() : undefined;
423
425
 
424
426
  touchHeartbeat();
425
427
  if (autoSaveOnSubmit !== false) {
@@ -431,7 +433,7 @@ export async function startDeckServer(
431
433
  unregisterSession(sessionId);
432
434
  pushEvent("deck-close", { reason: "submitted" });
433
435
  sendJson(res, 200, { ok: true });
434
- setImmediate(() => callbacks.onSubmit(selections));
436
+ setImmediate(() => callbacks.onSubmit(selections, notes, finalNotes || undefined));
435
437
  return;
436
438
  }
437
439
 
@@ -492,7 +494,7 @@ export async function startDeckServer(
492
494
  return;
493
495
  }
494
496
 
495
- const payload = body as { slideId?: string; prompt?: string; model?: string; thinking?: string };
497
+ const payload = body as { slideId?: string; prompt?: string; model?: string; thinking?: string; count?: number };
496
498
  if (typeof payload.slideId !== "string" || payload.slideId.trim() === "") {
497
499
  sendJson(res, 400, { ok: false, error: "slideId is required" });
498
500
  return;
@@ -509,12 +511,13 @@ export async function startDeckServer(
509
511
  const prompt = typeof payload.prompt === "string" ? payload.prompt.trim() || undefined : undefined;
510
512
  const model = typeof payload.model === "string" ? (payload.model.trim() || "") : undefined;
511
513
  const thinking = typeof payload.thinking === "string" ? payload.thinking.trim() || undefined : undefined;
514
+ const count = typeof payload.count === "number" && payload.count >= 1 && payload.count <= 5 ? payload.count : 1;
512
515
 
513
516
  setPendingGenerate(payload.slideId as string, false);
514
517
  touchHeartbeat();
515
518
  sendJson(res, 200, { ok: true });
516
519
  setImmediate(() => {
517
- callbacks.onGenerateMore(payload.slideId as string, prompt, model, thinking);
520
+ callbacks.onGenerateMore(payload.slideId as string, prompt, model, thinking, count);
518
521
  });
519
522
  return;
520
523
  }
@@ -603,11 +606,11 @@ export async function startDeckServer(
603
606
  resolve({
604
607
  url,
605
608
  port: addr.port,
606
- close: () => {
609
+ close: (reason?: string) => {
607
610
  if (!completed) {
608
611
  markCompleted();
609
612
  unregisterSession(sessionId);
610
- pushEvent("deck-close", { reason: "closed" });
613
+ pushEvent("deck-close", { reason: reason || "closed" });
611
614
  }
612
615
  try {
613
616
  server.close();
@@ -27,6 +27,30 @@
27
27
  }
28
28
  .btn-gen-more:disabled { opacity: 0.3; cursor: not-allowed; }
29
29
 
30
+ .gen-group { display: flex; align-items: center; gap: 6px; }
31
+
32
+ .gen-count {
33
+ appearance: none;
34
+ -webkit-appearance: none;
35
+ background: rgba(var(--dk-ink),0.12);
36
+ border: 1px solid rgba(var(--dk-ink),0.15);
37
+ border-radius: 4px;
38
+ padding: 4px 8px;
39
+ font-size: 12px;
40
+ font-weight: 600;
41
+ color: var(--dk-accent-text);
42
+ cursor: pointer;
43
+ text-align: center;
44
+ min-width: 32px;
45
+ }
46
+ .gen-count:focus { outline: none; border-color: rgba(138,190,183,0.4); background: rgba(138,190,183,0.1); }
47
+ .gen-count:disabled { opacity: 0.4; cursor: not-allowed; }
48
+
49
+ .gen-count-label {
50
+ font-size: 12px;
51
+ color: var(--dk-text-dim);
52
+ }
53
+
30
54
  .btn-regen {
31
55
  display: inline-flex; align-items: center; gap: 6px;
32
56
  padding: 8px 14px; border-radius: 8px;
@@ -87,6 +111,7 @@
87
111
  background: rgba(16,185,129,0.12); color: var(--dk-status-success);
88
112
  margin-left: auto;
89
113
  }
114
+ .badge-generated + .option-num { margin-left: 6px; }
90
115
 
91
116
  /* Skeleton shimmer placeholder */
92
117
  .option-skeleton {
@@ -119,6 +144,102 @@
119
144
  }
120
145
  @keyframes skel-shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
121
146
 
147
+ /* Regeneration overlay - covers options grid during regenerate-all */
148
+ .regen-overlay {
149
+ position: absolute;
150
+ inset: 0;
151
+ display: grid;
152
+ gap: 20px;
153
+ padding: 0;
154
+ z-index: 10;
155
+ animation: regen-fade-in 0.3s ease-out;
156
+ }
157
+ .regen-overlay.cols-3 { grid-template-columns: repeat(3, 1fr); }
158
+ .regen-overlay.cols-2 { grid-template-columns: repeat(2, 1fr); }
159
+ .regen-overlay.cols-1 { grid-template-columns: 1fr; }
160
+
161
+ @keyframes regen-fade-in {
162
+ from { opacity: 0; }
163
+ to { opacity: 1; }
164
+ }
165
+
166
+ .regen-skeleton {
167
+ border: 2px dashed rgba(138,190,183,0.15);
168
+ border-radius: 12px;
169
+ background: var(--dk-surface);
170
+ min-height: 280px;
171
+ position: relative;
172
+ overflow: hidden;
173
+ }
174
+ .regen-skeleton::before {
175
+ content: '';
176
+ position: absolute;
177
+ top: 0; left: 0; right: 0;
178
+ height: 44px;
179
+ background: linear-gradient(110deg, rgba(var(--dk-ink),0.03) 30%, rgba(var(--dk-ink),0.07) 50%, rgba(var(--dk-ink),0.03) 70%);
180
+ background-size: 200% 100%;
181
+ animation: skel-shimmer 1.5s ease-in-out infinite;
182
+ border-bottom: 1px solid rgba(var(--dk-ink),0.06);
183
+ }
184
+ .regen-skeleton::after {
185
+ content: '';
186
+ position: absolute;
187
+ top: 45px; left: 0; right: 0; bottom: 0;
188
+ background: linear-gradient(110deg, var(--dk-surface) 30%, var(--dk-surface-shimmer) 50%, var(--dk-surface) 70%);
189
+ background-size: 200% 100%;
190
+ animation: skel-shimmer 1.5s ease-in-out infinite;
191
+ animation-delay: 0.1s;
192
+ }
193
+ .regen-skeleton:nth-child(2)::before,
194
+ .regen-skeleton:nth-child(2)::after { animation-delay: 0.2s; }
195
+ .regen-skeleton:nth-child(3)::before,
196
+ .regen-skeleton:nth-child(3)::after { animation-delay: 0.3s; }
197
+ .regen-skeleton:nth-child(4)::before,
198
+ .regen-skeleton:nth-child(4)::after { animation-delay: 0.4s; }
199
+
200
+ /* Regeneration status text */
201
+ .regen-status {
202
+ position: absolute;
203
+ bottom: 16px; left: 50%;
204
+ transform: translateX(-50%);
205
+ font-family: var(--dk-font-mono);
206
+ font-size: 11px;
207
+ color: var(--dk-accent-text);
208
+ letter-spacing: 0.3px;
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 8px;
212
+ }
213
+ .regen-status::before {
214
+ content: '';
215
+ width: 14px; height: 14px;
216
+ border: 2px solid rgba(138,190,183,0.2);
217
+ border-top-color: var(--dk-accent);
218
+ border-radius: 50%;
219
+ animation: spin 0.8s linear infinite;
220
+ }
221
+
222
+ /* Options grid regenerating state */
223
+ .options.regenerating {
224
+ opacity: 0;
225
+ pointer-events: none;
226
+ transition: opacity 0.25s ease-out;
227
+ }
228
+
229
+ /* Regenerated options entry animation */
230
+ .option-regenerated {
231
+ animation: opt-regen-appear 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
232
+ opacity: 0;
233
+ }
234
+ .option-regenerated:nth-child(1) { animation-delay: 0.05s; }
235
+ .option-regenerated:nth-child(2) { animation-delay: 0.1s; }
236
+ .option-regenerated:nth-child(3) { animation-delay: 0.15s; }
237
+ .option-regenerated:nth-child(4) { animation-delay: 0.2s; }
238
+ @keyframes opt-regen-appear {
239
+ 0% { opacity: 0; transform: translateY(12px) scale(0.96); }
240
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
241
+ }
242
+
122
243
  /* ─────────────────────────────────────────────────────────────
123
244
  MODEL BAR
124
245
  ───────────────────────────────────────────────────────────── */
@@ -177,12 +177,6 @@ body {
177
177
  background: rgba(138,190,183,0.08);
178
178
  border-bottom-color: rgba(138,190,183,0.12);
179
179
  }
180
- .option.dimmed { opacity: 0.55; }
181
- .option.dimmed:hover {
182
- opacity: 0.8;
183
- border-color: rgba(var(--dk-ink),0.15);
184
- transform: translateY(-1px);
185
- }
186
180
 
187
181
  .option-header {
188
182
  display: flex; align-items: center; gap: 8px;
@@ -205,8 +199,6 @@ body {
205
199
  .option:hover .option-radio { border-color: rgba(var(--dk-ink),0.35); }
206
200
  .option.selected .option-radio { border-color: var(--dk-accent); background: var(--dk-accent); }
207
201
  .option.selected .option-radio::after { background: #fff; }
208
- .option.dimmed .option-radio { border-color: rgba(var(--dk-ink),0.14); }
209
- .option.dimmed:hover .option-radio { border-color: rgba(var(--dk-ink),0.2); }
210
202
 
211
203
  .option-label { font-size: 13px; font-weight: 600; color: var(--dk-text-label); }
212
204
 
@@ -276,6 +268,21 @@ body {
276
268
  color: var(--dk-text-secondary); font-family: var(--dk-font-mono);
277
269
  font-style: italic;
278
270
  }
271
+ .summary-notes {
272
+ margin-top: 10px; padding: 8px 10px;
273
+ font-size: 11px; line-height: 1.5;
274
+ color: var(--dk-accent-text);
275
+ background: rgba(138,190,183,0.08);
276
+ border-radius: 4px;
277
+ border-left: 2px solid rgba(138,190,183,0.3);
278
+ }
279
+ .summary-notes-label {
280
+ font-weight: 600;
281
+ font-size: 9px;
282
+ text-transform: uppercase;
283
+ letter-spacing: 0.3px;
284
+ opacity: 0.8;
285
+ }
279
286
  .summary-preview {
280
287
  margin-top: 12px; border-radius: 6px; overflow: hidden;
281
288
  border: 1px solid rgba(var(--dk-ink),0.12); max-height: 120px;
@@ -296,6 +303,47 @@ body {
296
303
  }
297
304
  .summary-mermaid svg { max-width: 100%; max-height: 100%; }
298
305
 
306
+ /* Final notes input on summary slide */
307
+ .final-notes {
308
+ width: 100%;
309
+ max-width: 560px;
310
+ margin-top: 24px;
311
+ display: flex;
312
+ flex-direction: column;
313
+ gap: 8px;
314
+ }
315
+ .final-notes-label {
316
+ font-size: 11px;
317
+ font-weight: 500;
318
+ color: var(--dk-text-dim);
319
+ text-transform: uppercase;
320
+ letter-spacing: 0.5px;
321
+ }
322
+ .final-notes-input {
323
+ width: 100%;
324
+ padding: 12px 14px;
325
+ border-radius: 8px;
326
+ border: 1px solid rgba(var(--dk-ink),0.12);
327
+ background: rgba(var(--dk-ink),0.04);
328
+ font-size: 13px;
329
+ font-family: inherit;
330
+ color: var(--dk-text);
331
+ resize: vertical;
332
+ min-height: 60px;
333
+ max-height: 120px;
334
+ transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
335
+ }
336
+ .final-notes-input::placeholder {
337
+ color: var(--dk-text-placeholder);
338
+ font-style: italic;
339
+ }
340
+ .final-notes-input:focus {
341
+ outline: none;
342
+ border-color: rgba(138,190,183,0.4);
343
+ background: rgba(var(--dk-ink),0.06);
344
+ box-shadow: 0 0 0 3px rgba(138,190,183,0.1);
345
+ }
346
+
299
347
  .btn-generate {
300
348
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
301
349
  padding: 14px 48px; border-radius: 10px; font-size: 14px; font-weight: 600;
@@ -59,17 +59,71 @@ code[class*="language-"], pre[class*="language-"] {
59
59
  OPTION ASIDE
60
60
  ───────────────────────────────────────────────────────────── */
61
61
 
62
- .option-aside {
63
- margin: 0 14px 14px;
62
+ /* Option footer - contains aside + notes */
63
+ .option-footer {
64
64
  padding: 12px 14px;
65
- border-left: 2px solid rgba(138,190,183,0.25);
66
- border-radius: 0 6px 6px 0;
67
- background: rgba(var(--dk-ink),0.04);
68
- font-size: 12px; line-height: 1.7;
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: 8px;
68
+ }
69
+
70
+ .option-aside {
71
+ padding: 8px 10px;
72
+ border-left: 2px solid rgba(138,190,183,0.2);
73
+ background: rgba(var(--dk-ink),0.02);
74
+ font-size: 11px; line-height: 1.55;
69
75
  color: var(--dk-text-aside);
70
76
  font-family: var(--dk-font-mono);
71
77
  font-style: italic;
72
78
  letter-spacing: -0.01em;
79
+ border-radius: 0 4px 4px 0;
80
+ }
81
+
82
+ /* Option notes input */
83
+ .option-notes {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 8px;
87
+ }
88
+ .option-notes-label {
89
+ font-size: 9px;
90
+ font-weight: 500;
91
+ color: var(--dk-text-hint);
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.3px;
94
+ white-space: nowrap;
95
+ opacity: 0.6;
96
+ }
97
+ .option-notes-input {
98
+ flex: 1;
99
+ padding: 5px 8px;
100
+ border-radius: 4px;
101
+ border: 1px dashed rgba(var(--dk-ink),0.12);
102
+ background: transparent;
103
+ font-size: 11px;
104
+ font-family: var(--dk-font-mono);
105
+ color: var(--dk-text);
106
+ resize: none;
107
+ min-height: 24px;
108
+ transition: border-color 0.2s, background 0.2s;
109
+ }
110
+ .option-notes-input::placeholder {
111
+ color: var(--dk-text-placeholder);
112
+ opacity: 0.4;
113
+ font-size: 10px;
114
+ }
115
+ .option-notes-input:focus {
116
+ outline: none;
117
+ border-style: solid;
118
+ border-color: rgba(138,190,183,0.3);
119
+ background: rgba(var(--dk-ink),0.02);
120
+ }
121
+ .option.selected .option-notes-input {
122
+ border-color: rgba(138,190,183,0.2);
123
+ }
124
+ .option-notes-input:disabled {
125
+ opacity: 0.4;
126
+ cursor: not-allowed;
73
127
  }
74
128
 
75
129
  /* ─────────────────────────────────────────────────────────────
@@ -23,6 +23,8 @@ let isClosed = false;
23
23
  let isSubmitting = false;
24
24
 
25
25
  const selections = {};
26
+ const optionNotes = {};
27
+ let finalNotes = "";
26
28
  const pendingGenerate = new Map();
27
29
  let selectedModel = "";
28
30
  let selectedThinking = "off";
@@ -184,13 +186,23 @@ function initTheme() {
184
186
  const SELECTIONS_KEY = `pi-deck-${typeof deckData.sessionId === "string" ? deckData.sessionId : "unknown"}`;
185
187
 
186
188
  function saveSelectionsToStorage() {
187
- try { localStorage.setItem(SELECTIONS_KEY, JSON.stringify(selections)); } catch {}
189
+ try {
190
+ const data = { selections, optionNotes };
191
+ if (finalNotes) data.finalNotes = finalNotes;
192
+ localStorage.setItem(SELECTIONS_KEY, JSON.stringify(data));
193
+ } catch {}
188
194
  }
189
195
 
190
196
  function loadSelectionsFromStorage() {
191
197
  try {
192
198
  const saved = localStorage.getItem(SELECTIONS_KEY);
193
- return saved ? JSON.parse(saved) : null;
199
+ if (!saved) return null;
200
+ const parsed = JSON.parse(saved);
201
+ // Handle both old format (just selections) and new format ({ selections, optionNotes })
202
+ if (parsed && typeof parsed === "object" && !parsed.selections) {
203
+ return { selections: parsed, optionNotes: {} };
204
+ }
205
+ return parsed;
194
206
  } catch { return null; }
195
207
  }
196
208
 
@@ -1,8 +1,8 @@
1
1
  // ─── SELECTION ───────────────────────────────────────────────
2
2
 
3
- function applySavedSelections(saved) {
4
- if (!saved || typeof saved !== "object") return;
5
- for (const [slideId, label] of Object.entries(saved)) {
3
+ function applySavedSelections(savedSelections, savedNotes) {
4
+ if (!savedSelections || typeof savedSelections !== "object") return;
5
+ for (const [slideId, label] of Object.entries(savedSelections)) {
6
6
  if (typeof label !== "string") continue;
7
7
  const slideEl = document.querySelector(`.slide[data-id="${CSS.escape(slideId)}"]`);
8
8
  if (!slideEl) continue;
@@ -13,30 +13,44 @@ function applySavedSelections(saved) {
13
13
  }
14
14
  }
15
15
  }
16
+ // Restore notes
17
+ if (savedNotes && typeof savedNotes === "object") {
18
+ for (const [slideId, noteData] of Object.entries(savedNotes)) {
19
+ if (noteData && typeof noteData === "object" && noteData.label && noteData.notes) {
20
+ optionNotes[slideId] = noteData;
21
+ // Find and populate the textarea
22
+ const input = document.querySelector(`.option-notes-input[data-slide-id="${CSS.escape(slideId)}"][data-option-label="${CSS.escape(noteData.label)}"]`);
23
+ if (input) input.value = noteData.notes;
24
+ }
25
+ }
26
+ }
16
27
  }
17
28
 
18
29
  function restoreSelections() {
19
30
  const serverSaved = deckData.savedSelections;
20
31
  if (serverSaved && typeof serverSaved === "object" && Object.keys(serverSaved).length > 0) {
21
- applySavedSelections(serverSaved);
32
+ applySavedSelections(serverSaved, null);
22
33
  return;
23
34
  }
24
35
  const stored = loadSelectionsFromStorage();
25
- if (stored) applySavedSelections(stored);
36
+ if (stored) {
37
+ applySavedSelections(stored.selections || stored, stored.optionNotes);
38
+ // Restore final notes if present
39
+ if (stored.finalNotes) {
40
+ finalNotes = stored.finalNotes;
41
+ const input = document.getElementById("final-notes-input");
42
+ if (input) input.value = stored.finalNotes;
43
+ }
44
+ }
26
45
  }
27
46
 
28
47
  function applySelectionClasses(slideElement, selectedElement) {
29
48
  slideElement.querySelectorAll(".option").forEach((optionEl) => {
30
- optionEl.classList.remove("selected", "dimmed");
49
+ optionEl.classList.remove("selected");
31
50
  optionEl.setAttribute("aria-checked", "false");
32
51
  });
33
52
  selectedElement.classList.add("selected");
34
53
  selectedElement.setAttribute("aria-checked", "true");
35
- slideElement.querySelectorAll(".option").forEach((optionEl) => {
36
- if (optionEl !== selectedElement) {
37
- optionEl.classList.add("dimmed");
38
- }
39
- });
40
54
  }
41
55
 
42
56
  function selectOption(optionElement) {
@@ -342,7 +356,8 @@ function initModelBar(modelsData) {
342
356
  }
343
357
 
344
358
  function syncDefaultCheck() {
345
- defaultCheck.checked = modelsData.defaultModel != null && selectedModel === modelsData.defaultModel;
359
+ const effectiveModel = selectedModel || modelsData.current;
360
+ defaultCheck.checked = modelsData.defaultModel != null && effectiveModel === modelsData.defaultModel;
346
361
  }
347
362
 
348
363
  function activateProvider(provider) {
@@ -374,12 +389,18 @@ function initModelBar(modelsData) {
374
389
  });
375
390
 
376
391
  defaultCheck.addEventListener("change", async () => {
377
- if (defaultCheck.checked && !selectedModel) { defaultCheck.checked = false; return; }
378
- const model = defaultCheck.checked ? selectedModel : null;
392
+ // Use selectedModel, or fall back to current model when on "Current" pill
393
+ const modelToSave = selectedModel || modelsData.current;
394
+ if (defaultCheck.checked && !modelToSave) { defaultCheck.checked = false; return; }
395
+ const model = defaultCheck.checked ? modelToSave : null;
379
396
  try {
380
397
  await postJson("/save-model-default", { token: sessionToken, model });
381
398
  modelsData.defaultModel = model;
382
- } catch {}
399
+ } catch (err) {
400
+ console.error("Failed to save default model:", err);
401
+ // Revert checkbox to match actual state
402
+ syncDefaultCheck();
403
+ }
383
404
  });
384
405
 
385
406
  if (modelsData.defaultModel) {