CallMeDaniel Claude Opus 4.6 (1M context) commited on
Commit
df36551
·
1 Parent(s): c8b8965

docs: add editable plan card implementation plan

Browse files

12-task TDD plan covering per-field editing, ask-agent buttons, notes
area, dual score display, Save & Continue action, and unified
confirm/revise with plan card auto-fill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

docs/superpowers/plans/2026-04-12-editable-plan-card.md ADDED
@@ -0,0 +1,848 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Editable Plan Card Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make the plan review card editable with per-field edit/ask-agent buttons, a notes area, dual score display, Save & Continue action, and unified confirm/revise behavior.
6
+
7
+ **Architecture:** Extend the existing inline plan card in `web/index.html` with edit toggles and agent-ask buttons per field. Add a `notes` field to `DesignPlan` and `field_agents` config to `PlanningConfig`. Modify `confirmRecommendation()` to auto-fill plan card fields when a plan is visible. No new API endpoints — Save & Continue is frontend-only, Approve already accepts the full plan object.
8
+
9
+ **Tech Stack:** Python/Pydantic (backend models), FastAPI (routes), vanilla JS/HTML/CSS (frontend)
10
+
11
+ ---
12
+
13
+ ### Task 1: Add `notes` field to `DesignPlan` and `field_agents` to config
14
+
15
+ **Files:**
16
+ - Modify: `agents/design_state.py:42-92`
17
+ - Modify: `config/settings.py:98-112`
18
+ - Modify: `config.yaml:74-97`
19
+ - Test: `tests/test_design_state.py`
20
+ - Test: `tests/test_settings.py`
21
+
22
+ - [ ] **Step 1: Write failing test for `notes` on DesignPlan**
23
+
24
+ Add to `tests/test_design_state.py` in the `TestDesignPlan` class:
25
+
26
+ ```python
27
+ def test_plan_notes_default_empty(self):
28
+ plan = DesignPlan(part_name="test")
29
+ assert plan.notes == ""
30
+
31
+ def test_plan_notes_in_render(self):
32
+ plan = DesignPlan(
33
+ part_name="bracket",
34
+ material="aluminum",
35
+ notes="Check if 304 is overkill",
36
+ )
37
+ rendered = plan.render_approved()
38
+ assert "USER NOTES" in rendered
39
+ assert "Check if 304 is overkill" in rendered
40
+
41
+ def test_plan_notes_empty_not_in_render(self):
42
+ plan = DesignPlan(part_name="bracket", material="aluminum", notes="")
43
+ rendered = plan.render_approved()
44
+ assert "USER NOTES" not in rendered
45
+
46
+ def test_plan_from_state_no_notes(self):
47
+ state = DesignState(part_name="bracket", material="steel")
48
+ plan = DesignPlan.from_state(state, confidence_score=5.0)
49
+ assert plan.notes == ""
50
+ ```
51
+
52
+ - [ ] **Step 2: Run tests to verify they fail**
53
+
54
+ Run: `pytest tests/test_design_state.py::TestDesignPlan -v`
55
+ Expected: FAIL — `DesignPlan` has no `notes` field
56
+
57
+ - [ ] **Step 3: Add `notes` field to `DesignPlan`**
58
+
59
+ In `agents/design_state.py`, add the field after `confidence_score` (line 52):
60
+
61
+ ```python
62
+ confidence_score: float = 0.0
63
+ notes: str = ""
64
+ ```
65
+
66
+ Update `render_approved()` — add before the final two lines (`"This plan has been reviewed..."`):
67
+
68
+ ```python
69
+ if self.notes:
70
+ lines.append(f"User Notes: {self.notes}")
71
+ ```
72
+
73
+ - [ ] **Step 4: Run tests to verify they pass**
74
+
75
+ Run: `pytest tests/test_design_state.py::TestDesignPlan -v`
76
+ Expected: PASS
77
+
78
+ - [ ] **Step 5: Write failing test for `field_agents` config**
79
+
80
+ Add to `tests/test_settings.py`:
81
+
82
+ ```python
83
+ def test_planning_field_agents_defaults():
84
+ from config.settings import PlanningConfig
85
+ cfg = PlanningConfig()
86
+ assert cfg.field_agents["material"] == "engineering"
87
+ assert cfg.field_agents["features"] == "design"
88
+ assert cfg.field_agents["constraints"] == "cnc"
89
+ assert cfg.field_agents["axis_recommendation"] == "cnc"
90
+ assert cfg.field_agents["part_name"] == "design"
91
+ ```
92
+
93
+ - [ ] **Step 6: Run test to verify it fails**
94
+
95
+ Run: `pytest tests/test_settings.py::test_planning_field_agents_defaults -v`
96
+ Expected: FAIL — `PlanningConfig` has no `field_agents`
97
+
98
+ - [ ] **Step 7: Add `field_agents` to `PlanningConfig`**
99
+
100
+ In `config/settings.py`, add after `approved_agents` (line 111):
101
+
102
+ ```python
103
+ field_agents: dict[str, str] = Field(default_factory=lambda: {
104
+ "material": "engineering",
105
+ "dimensions": "engineering",
106
+ "features": "design",
107
+ "constraints": "cnc",
108
+ "axis_recommendation": "cnc",
109
+ "machining_notes": "cnc",
110
+ "part_name": "design",
111
+ })
112
+ ```
113
+
114
+ Add to `config.yaml` under `planning:` (after `approved_agents`):
115
+
116
+ ```yaml
117
+ field_agents:
118
+ material: engineering
119
+ dimensions: engineering
120
+ features: design
121
+ constraints: cnc
122
+ axis_recommendation: cnc
123
+ machining_notes: cnc
124
+ part_name: design
125
+ ```
126
+
127
+ - [ ] **Step 8: Run tests to verify they pass**
128
+
129
+ Run: `pytest tests/test_settings.py::test_planning_field_agents_defaults tests/test_design_state.py -v`
130
+ Expected: PASS
131
+
132
+ - [ ] **Step 9: Commit**
133
+
134
+ ```bash
135
+ git add agents/design_state.py config/settings.py config.yaml tests/test_design_state.py tests/test_settings.py
136
+ git commit -m "feat: add notes field to DesignPlan and field_agents config"
137
+ ```
138
+
139
+ ---
140
+
141
+ ### Task 2: Update `plan/approve` endpoint to handle `notes`
142
+
143
+ **Files:**
144
+ - Modify: `server/routes.py:164-178`
145
+ - Test: `tests/test_api_routes.py`
146
+
147
+ - [ ] **Step 1: Write failing test**
148
+
149
+ Add to `tests/test_api_routes.py` in `TestPlanApproveEndpoint`:
150
+
151
+ ```python
152
+ def test_approve_preserves_notes(self):
153
+ resp = client.post("/api/plan/approve", json={
154
+ "plan": {
155
+ "part_name": "bracket",
156
+ "description": "test",
157
+ "material": "aluminum 6061",
158
+ "dimensions": {"width": 60},
159
+ "features": [],
160
+ "constraints": [],
161
+ "axis_recommendation": "3-axis",
162
+ "machining_notes": [],
163
+ "confidence_score": 9.0,
164
+ "notes": "Check material compatibility",
165
+ },
166
+ "design_state": {"phase": "planning"},
167
+ })
168
+ assert resp.status_code == 200
169
+ data = resp.json()
170
+ assert data["design_state"]["plan"]["notes"] == "Check material compatibility"
171
+ ```
172
+
173
+ - [ ] **Step 2: Run test to verify it passes (no backend change needed)**
174
+
175
+ Run: `pytest tests/test_api_routes.py::TestPlanApproveEndpoint::test_approve_preserves_notes -v`
176
+ Expected: PASS — Pydantic already handles the new `notes` field automatically since the endpoint does `DesignPlan(**body.plan)` and `state.plan = plan`
177
+
178
+ - [ ] **Step 3: Commit**
179
+
180
+ ```bash
181
+ git add tests/test_api_routes.py
182
+ git commit -m "test: verify plan/approve preserves notes field"
183
+ ```
184
+
185
+ ---
186
+
187
+ ### Task 3: Add plan card CSS for edit controls, notes, and dual score
188
+
189
+ **Files:**
190
+ - Modify: `web/index.html` (CSS section, lines ~1286-1317)
191
+
192
+ - [ ] **Step 1: Add CSS styles**
193
+
194
+ In `web/index.html`, after the existing `.plan-card-reject` rule (line 1317), add:
195
+
196
+ ```css
197
+ .plan-card-save {
198
+ background: var(--accent); color: var(--bg-void); border-color: var(--accent);
199
+ }
200
+ .plan-field-actions {
201
+ display: inline-flex; gap: 4px; margin-left: 6px; vertical-align: middle;
202
+ }
203
+ .plan-field-btn {
204
+ background: none; border: 1px solid var(--border); border-radius: 3px;
205
+ color: var(--text-secondary); cursor: pointer; font-size: 10px;
206
+ padding: 1px 5px; font-family: var(--font-mono); line-height: 1.4;
207
+ }
208
+ .plan-field-btn:hover { border-color: var(--accent); color: var(--accent); }
209
+ .plan-field-input {
210
+ background: var(--bg-void); border: 1px solid var(--accent); border-radius: 3px;
211
+ color: var(--text-primary); font-family: var(--font-mono); font-size: 11px;
212
+ padding: 2px 6px; width: 60%;
213
+ }
214
+ .plan-field-input:focus { outline: none; border-color: var(--success); }
215
+ .plan-dim-group { display: inline-flex; gap: 4px; }
216
+ .plan-dim-input {
217
+ background: var(--bg-void); border: 1px solid var(--accent); border-radius: 3px;
218
+ color: var(--text-primary); font-family: var(--font-mono); font-size: 11px;
219
+ padding: 2px 4px; width: 60px;
220
+ }
221
+ .plan-dim-input:focus { outline: none; border-color: var(--success); }
222
+ .plan-dim-label {
223
+ font-size: 9px; color: var(--text-secondary); font-family: var(--font-mono);
224
+ }
225
+ .plan-notes {
226
+ width: 100%; min-height: 48px; margin-top: 8px; padding: 6px 8px;
227
+ background: var(--bg-void); border: 1px solid var(--border); border-radius: 5px;
228
+ color: var(--text-primary); font-family: var(--font-mono); font-size: 11px;
229
+ resize: vertical;
230
+ }
231
+ .plan-notes:focus { outline: none; border-color: var(--accent); }
232
+ .plan-score-dual {
233
+ display: flex; gap: 16px; font-family: var(--font-mono); font-size: 11px;
234
+ color: var(--text-secondary); margin-top: 8px;
235
+ }
236
+ .plan-score-current { font-weight: 600; }
237
+ .plan-score-current.score-ok { color: var(--success); }
238
+ .plan-score-current.score-low { color: var(--warning, #ffab40); }
239
+ ```
240
+
241
+ - [ ] **Step 2: Verify the page loads without CSS errors**
242
+
243
+ Open the web app in a browser and confirm no visual regressions. The new styles have no effect yet since no HTML uses them.
244
+
245
+ - [ ] **Step 3: Commit**
246
+
247
+ ```bash
248
+ git add web/index.html
249
+ git commit -m "feat: add CSS for editable plan card controls"
250
+ ```
251
+
252
+ ---
253
+
254
+ ### Task 4: Add i18n strings for editable plan card
255
+
256
+ **Files:**
257
+ - Modify: `web/index.html` (i18n section, lines ~2020-2126)
258
+
259
+ - [ ] **Step 1: Add translation keys**
260
+
261
+ In the English translations (after `planReject` around line 2051), add:
262
+
263
+ ```javascript
264
+ planSave: 'Save & Continue',
265
+ planNotesPlaceholder: 'Add notes about this plan...',
266
+ planScoreOriginal: 'Original',
267
+ planScoreCurrent: 'Current',
268
+ planAskAgent: 'Ask',
269
+ planEdit: '\u270e',
270
+ ```
271
+
272
+ In the Chinese translations (after `planReject` around line 2088), add:
273
+
274
+ ```javascript
275
+ planSave: '\u5132\u5b58\u4e26\u7e7c\u7e8c',
276
+ planNotesPlaceholder: '\u65b0\u589e\u8a08\u756b\u5099\u8a3b...',
277
+ planScoreOriginal: '\u539f\u59cb',
278
+ planScoreCurrent: '\u7576\u524d',
279
+ planAskAgent: '\u8a62\u554f',
280
+ planEdit: '\u270e',
281
+ ```
282
+
283
+ In the Vietnamese translations (after `planReject` around line 2125), add:
284
+
285
+ ```javascript
286
+ planSave: 'L\u01b0u & Ti\u1ebfp T\u1ee5c',
287
+ planNotesPlaceholder: 'Th\u00eam ghi ch\u00fa v\u1ec1 k\u1ebf ho\u1ea1ch...',
288
+ planScoreOriginal: 'G\u1ed1c',
289
+ planScoreCurrent: 'Hi\u1ec7n t\u1ea1i',
290
+ planAskAgent: 'H\u1ecfi',
291
+ planEdit: '\u270e',
292
+ ```
293
+
294
+ - [ ] **Step 2: Verify `t()` returns new keys**
295
+
296
+ Open browser console: `t('planSave')` should return `'Save & Continue'`.
297
+
298
+ - [ ] **Step 3: Commit**
299
+
300
+ ```bash
301
+ git add web/index.html
302
+ git commit -m "feat: add i18n strings for editable plan card"
303
+ ```
304
+
305
+ ---
306
+
307
+ ### Task 5: Rewrite `renderPlanCard()` with edit controls and notes
308
+
309
+ **Files:**
310
+ - Modify: `web/index.html` (lines ~1840-1862)
311
+
312
+ - [ ] **Step 1: Add field-to-agent mapping and question templates**
313
+
314
+ Add above the `renderPlanCard` function:
315
+
316
+ ```javascript
317
+ const PLAN_FIELD_AGENTS = {
318
+ part_name: 'design', material: 'engineering', dimensions: 'engineering',
319
+ features: 'design', constraints: 'cnc', axis_recommendation: 'cnc',
320
+ machining_notes: 'cnc',
321
+ };
322
+ const PLAN_FIELD_QUESTIONS = {
323
+ part_name: 'What should this part be called?',
324
+ material: 'What material do you recommend?',
325
+ dimensions: 'What dimensions are appropriate?',
326
+ features: 'What features should this part have?',
327
+ constraints: 'What manufacturing constraints should we consider?',
328
+ axis_recommendation: 'What axis strategy do you recommend?',
329
+ machining_notes: 'Any machining notes to consider?',
330
+ };
331
+ ```
332
+
333
+ - [ ] **Step 2: Replace `renderPlanCard()` function**
334
+
335
+ Replace the entire `renderPlanCard` function (lines 1840-1862) with:
336
+
337
+ ```javascript
338
+ function renderPlanCard(plan) {
339
+ const fields = [
340
+ ['Part', 'part_name', plan.part_name, 'text'],
341
+ ['Material', 'material', plan.material, 'text'],
342
+ ['Dimensions', 'dimensions', plan.dimensions, 'dimensions'],
343
+ ['Features', 'features', plan.features, 'list'],
344
+ ['Constraints', 'constraints', plan.constraints, 'list'],
345
+ ['Axis', 'axis_recommendation', plan.axis_recommendation || 'Auto', 'text'],
346
+ ];
347
+ if (plan.machining_notes && plan.machining_notes.length) {
348
+ fields.push(['Notes', 'machining_notes', plan.machining_notes, 'list']);
349
+ }
350
+ let html = '<div class="plan-card" id="active-plan-card" data-original-score="' + (plan.confidence_score || 0) + '">';
351
+ html += '<div class="plan-card-title">\u25c6 ' + t('planReady') + '</div>';
352
+ for (const [label, key, value, type] of fields) {
353
+ html += '<div class="wizard-review-field" data-field="' + key + '">';
354
+ html += '<span class="wizard-review-label">' + escapeHtml(label) + '</span>';
355
+ html += '<span class="wizard-review-value" id="plan-val-' + key + '">';
356
+ if (type === 'dimensions') {
357
+ html += escapeHtml(Object.entries(value || {}).map(function(e) { return e[0] + '=' + e[1] + 'mm'; }).join(', ') || '\u2014');
358
+ } else if (type === 'list') {
359
+ html += escapeHtml((value || []).join(', ') || '\u2014');
360
+ } else {
361
+ html += escapeHtml(value || '\u2014');
362
+ }
363
+ html += '</span>';
364
+ html += '<span class="plan-field-actions">';
365
+ html += '<button class="plan-field-btn" onclick="toggleFieldEdit(\'' + key + '\', \'' + type + '\')" title="Edit">' + t('planEdit') + '</button>';
366
+ html += '<button class="plan-field-btn" onclick="askAgentForField(\'' + key + '\')" title="Ask Agent">' + t('planAskAgent') + '</button>';
367
+ html += '</span>';
368
+ html += '</div>';
369
+ }
370
+ // Notes area
371
+ html += '<textarea class="plan-notes" id="plan-notes" placeholder="' + t('planNotesPlaceholder') + '">' + escapeHtml(plan.notes || '') + '</textarea>';
372
+ // Dual score
373
+ var origScore = (plan.confidence_score || 0).toFixed(0);
374
+ html += '<div class="plan-score-dual">';
375
+ html += '<span>' + t('planScoreOriginal') + ': ' + origScore + '/8</span>';
376
+ html += '<span>\u2192</span>';
377
+ html += '<span class="plan-score-current score-ok" id="plan-current-score">' + t('planScoreCurrent') + ': ' + origScore + '/8</span>';
378
+ html += '</div>';
379
+ // Action buttons
380
+ html += '<div class="plan-card-actions">';
381
+ html += '<button class="plan-card-btn plan-card-approve" onclick="approvePlanCard()">' + t('planApprove') + '</button>';
382
+ html += '<button class="plan-card-btn plan-card-save" onclick="savePlanAndContinue()">' + t('planSave') + '</button>';
383
+ html += '<button class="plan-card-btn plan-card-reject" onclick="rejectPlanCard()">' + t('planReject') + '</button>';
384
+ html += '</div></div>';
385
+ return html;
386
+ }
387
+ ```
388
+
389
+ - [ ] **Step 3: Verify the plan card renders correctly**
390
+
391
+ Trigger a plan in the UI (chat until score >= 8 or type a trigger keyword). Confirm:
392
+ - All fields show values with pencil + Ask buttons
393
+ - Notes textarea appears
394
+ - Dual score shows Original and Current (same value initially)
395
+ - Three buttons: Approve, Save & Continue, Reject
396
+
397
+ - [ ] **Step 4: Commit**
398
+
399
+ ```bash
400
+ git add web/index.html
401
+ git commit -m "feat: render editable plan card with field actions, notes, dual score"
402
+ ```
403
+
404
+ ---
405
+
406
+ ### Task 6: Implement `toggleFieldEdit()` and `recalcPlanScore()`
407
+
408
+ **Files:**
409
+ - Modify: `web/index.html` (add new functions after `renderPlanCard`)
410
+
411
+ - [ ] **Step 1: Add `toggleFieldEdit()` function**
412
+
413
+ Add after `renderPlanCard`:
414
+
415
+ ```javascript
416
+ function toggleFieldEdit(fieldKey, fieldType) {
417
+ var valEl = document.getElementById('plan-val-' + fieldKey);
418
+ if (!valEl) return;
419
+ var plan = designState.plan;
420
+ if (!plan) return;
421
+
422
+ // If already editing (has input), save and switch back to display
423
+ var existingInput = valEl.querySelector('input, .plan-dim-group');
424
+ if (existingInput) {
425
+ // Read value from input(s) and update plan
426
+ if (fieldType === 'dimensions') {
427
+ var dimInputs = valEl.querySelectorAll('.plan-dim-input');
428
+ var dims = {};
429
+ dimInputs.forEach(function(inp) { if (inp.value) dims[inp.dataset.dim] = parseFloat(inp.value) || 0; });
430
+ plan.dimensions = dims;
431
+ valEl.textContent = Object.entries(dims).map(function(e) { return e[0] + '=' + e[1] + 'mm'; }).join(', ') || '\u2014';
432
+ } else if (fieldType === 'list') {
433
+ var raw = existingInput.value;
434
+ plan[fieldKey] = raw ? raw.split(',').map(function(s) { return s.trim(); }).filter(Boolean) : [];
435
+ valEl.textContent = plan[fieldKey].join(', ') || '\u2014';
436
+ } else {
437
+ plan[fieldKey] = existingInput.value;
438
+ valEl.textContent = plan[fieldKey] || '\u2014';
439
+ }
440
+ recalcPlanScore();
441
+ return;
442
+ }
443
+
444
+ // Switch to edit mode
445
+ if (fieldType === 'dimensions') {
446
+ var dims = plan.dimensions || {};
447
+ var dimKeys = Object.keys(dims).length ? Object.keys(dims) : ['width', 'height', 'depth'];
448
+ var groupHtml = '<span class="plan-dim-group">';
449
+ dimKeys.forEach(function(dk) {
450
+ groupHtml += '<span><span class="plan-dim-label">' + dk + '</span><input class="plan-dim-input" type="number" data-dim="' + dk + '" value="' + (dims[dk] || '') + '"></span>';
451
+ });
452
+ groupHtml += '</span>';
453
+ valEl.innerHTML = groupHtml;
454
+ } else if (fieldType === 'list') {
455
+ var current = (plan[fieldKey] || []).join(', ');
456
+ valEl.innerHTML = '<input class="plan-field-input" type="text" value="' + escapeHtml(current) + '">';
457
+ } else {
458
+ var current = plan[fieldKey] || '';
459
+ valEl.innerHTML = '<input class="plan-field-input" type="text" value="' + escapeHtml(current) + '">';
460
+ }
461
+ var firstInput = valEl.querySelector('input');
462
+ if (firstInput) firstInput.focus();
463
+ }
464
+ ```
465
+
466
+ - [ ] **Step 2: Add `recalcPlanScore()` function**
467
+
468
+ Add after `toggleFieldEdit`:
469
+
470
+ ```javascript
471
+ function recalcPlanScore() {
472
+ var plan = designState.plan;
473
+ if (!plan) return;
474
+ // Compute score using same logic as wizComputeScore but from plan object
475
+ var s = 0;
476
+ if (plan.material) s += 3;
477
+ if (plan.part_name) s += 1;
478
+ if (plan.description) s += 1;
479
+ if (plan.axis_recommendation) s += 2;
480
+ s += Math.min(Object.keys(plan.dimensions || {}).length, 4);
481
+ s += Math.min((plan.features || []).length, 4);
482
+ s += Math.min((plan.constraints || []).length, 2);
483
+ var el = document.getElementById('plan-current-score');
484
+ if (el) {
485
+ el.textContent = t('planScoreCurrent') + ': ' + s.toFixed(0) + '/8';
486
+ el.className = 'plan-score-current ' + (s >= 8 ? 'score-ok' : 'score-low');
487
+ }
488
+ }
489
+ ```
490
+
491
+ - [ ] **Step 3: Verify field editing works**
492
+
493
+ In the browser, trigger a plan card, then:
494
+ - Click pencil on Material — should show text input with current value
495
+ - Type a new value, click pencil again — should save and show updated text
496
+ - Click pencil on Dimensions — should show number inputs for each dimension key
497
+ - Verify Current score updates when clearing/adding fields
498
+
499
+ - [ ] **Step 4: Commit**
500
+
501
+ ```bash
502
+ git add web/index.html
503
+ git commit -m "feat: implement toggleFieldEdit and recalcPlanScore for plan card"
504
+ ```
505
+
506
+ ---
507
+
508
+ ### Task 7: Implement `askAgentForField()`
509
+
510
+ **Files:**
511
+ - Modify: `web/index.html` (add function after `recalcPlanScore`)
512
+
513
+ - [ ] **Step 1: Add `askAgentForField()` function**
514
+
515
+ ```javascript
516
+ function askAgentForField(fieldKey) {
517
+ var plan = designState.plan;
518
+ if (!plan) return;
519
+ var agentId = PLAN_FIELD_AGENTS[fieldKey] || 'design';
520
+ var question = PLAN_FIELD_QUESTIONS[fieldKey] || 'Can you help with this field?';
521
+ var partCtx = plan.part_name ? ' for "' + plan.part_name + '"' : '';
522
+ var msg = '@' + agentId + ' Regarding the plan' + partCtx + ': ' + question;
523
+ // Switch to chat tab if on guided tab
524
+ var chatTab = document.querySelector('[data-tab="chat"]');
525
+ if (chatTab) chatTab.click();
526
+ sendMessage(msg);
527
+ }
528
+ ```
529
+
530
+ - [ ] **Step 2: Verify Ask Agent sends correct message**
531
+
532
+ In the browser:
533
+ - Trigger a plan card
534
+ - Click "Ask" next to Material field
535
+ - Verify a message like `@engineering Regarding the plan for "bracket": What material do you recommend?` appears in chat
536
+ - Verify the engineering agent responds
537
+
538
+ - [ ] **Step 3: Commit**
539
+
540
+ ```bash
541
+ git add web/index.html
542
+ git commit -m "feat: implement askAgentForField for plan card agent assist"
543
+ ```
544
+
545
+ ---
546
+
547
+ ### Task 8: Implement `savePlanAndContinue()` and update `approvePlanCard()`
548
+
549
+ **Files:**
550
+ - Modify: `web/index.html` (modify `approvePlanCard`, add `savePlanAndContinue`)
551
+
552
+ - [ ] **Step 1: Add `savePlanAndContinue()` function**
553
+
554
+ Add after `askAgentForField`:
555
+
556
+ ```javascript
557
+ function savePlanAndContinue() {
558
+ var plan = designState.plan;
559
+ if (!plan) return;
560
+ // Read notes from textarea
561
+ var notesEl = document.getElementById('plan-notes');
562
+ if (notesEl) plan.notes = notesEl.value;
563
+ // Merge plan fields into designState
564
+ designState.part_name = plan.part_name;
565
+ designState.description = plan.description;
566
+ designState.material = plan.material;
567
+ designState.dimensions = Object.assign({}, plan.dimensions);
568
+ designState.features = (plan.features || []).slice();
569
+ designState.constraints = (plan.constraints || []).slice();
570
+ designState.axis_recommendation = plan.axis_recommendation;
571
+ // Reset to exploring
572
+ designState.phase = 'exploring';
573
+ designState.plan = null;
574
+ saveState();
575
+ var card = document.getElementById('active-plan-card');
576
+ if (card) card.remove();
577
+ }
578
+ ```
579
+
580
+ - [ ] **Step 2: Update `approvePlanCard()` to read edited values**
581
+
582
+ Replace the existing `approvePlanCard` function (lines 1865-1883) with:
583
+
584
+ ```javascript
585
+ async function approvePlanCard() {
586
+ var plan = designState.plan;
587
+ if (!plan) return;
588
+ // Read notes from textarea
589
+ var notesEl = document.getElementById('plan-notes');
590
+ if (notesEl) plan.notes = notesEl.value;
591
+ try {
592
+ var resp = await fetch('/api/plan/approve', {
593
+ method: 'POST',
594
+ headers: { 'Content-Type': 'application/json' },
595
+ body: JSON.stringify({ plan: plan, design_state: designState }),
596
+ });
597
+ var data = await resp.json();
598
+ designState = data.design_state;
599
+ saveState();
600
+ var card = document.getElementById('active-plan-card');
601
+ if (card) card.remove();
602
+ await sendMessage('Generate the approved design');
603
+ } catch (err) {
604
+ console.error('Plan approve failed:', err);
605
+ }
606
+ }
607
+ ```
608
+
609
+ - [ ] **Step 3: Verify Save & Continue and Approve work**
610
+
611
+ In the browser:
612
+ - Trigger a plan card
613
+ - Edit the material field, type notes
614
+ - Click "Save & Continue" — plan card should disappear, designState should retain edited material, phase should be "exploring"
615
+ - Trigger the plan again — verify edited material appears
616
+ - Click "Approve" — verify the edited plan is sent to the API with notes
617
+
618
+ - [ ] **Step 4: Commit**
619
+
620
+ ```bash
621
+ git add web/index.html
622
+ git commit -m "feat: implement savePlanAndContinue and update approvePlanCard to read edits"
623
+ ```
624
+
625
+ ---
626
+
627
+ ### Task 9: Implement `updatePlanCardField()` for external auto-fill
628
+
629
+ **Files:**
630
+ - Modify: `web/index.html` (add function after `savePlanAndContinue`)
631
+
632
+ - [ ] **Step 1: Add `updatePlanCardField()` function**
633
+
634
+ ```javascript
635
+ function updatePlanCardField(fieldKey, value) {
636
+ var plan = designState.plan;
637
+ if (!plan) return;
638
+ var valEl = document.getElementById('plan-val-' + fieldKey);
639
+ // Close any open edit mode first
640
+ var existingInput = valEl ? valEl.querySelector('input, .plan-dim-group') : null;
641
+ if (existingInput) {
642
+ // Cancel edit mode by resetting display
643
+ }
644
+ // Update plan object
645
+ if (fieldKey === 'dimensions' && typeof value === 'object') {
646
+ Object.assign(plan.dimensions, value);
647
+ if (valEl) valEl.textContent = Object.entries(plan.dimensions).map(function(e) { return e[0] + '=' + e[1] + 'mm'; }).join(', ') || '\u2014';
648
+ } else if (fieldKey === 'features' || fieldKey === 'constraints' || fieldKey === 'machining_notes') {
649
+ if (Array.isArray(value)) {
650
+ plan[fieldKey] = value;
651
+ } else {
652
+ if (!plan[fieldKey].includes(value)) plan[fieldKey].push(value);
653
+ }
654
+ if (valEl) valEl.textContent = plan[fieldKey].join(', ') || '\u2014';
655
+ } else {
656
+ plan[fieldKey] = value;
657
+ if (valEl) valEl.textContent = value || '\u2014';
658
+ }
659
+ recalcPlanScore();
660
+ }
661
+ ```
662
+
663
+ - [ ] **Step 2: Commit**
664
+
665
+ ```bash
666
+ git add web/index.html
667
+ git commit -m "feat: add updatePlanCardField for external auto-fill of plan fields"
668
+ ```
669
+
670
+ ---
671
+
672
+ ### Task 10: Implement `detectPlanField()` and `extractFieldValue()`
673
+
674
+ **Files:**
675
+ - Modify: `web/index.html` (add functions after `updatePlanCardField`)
676
+
677
+ - [ ] **Step 1: Add `detectPlanField()` function**
678
+
679
+ This reuses the `category_keywords` from `GapAnalysisConfig` via a frontend mapping:
680
+
681
+ ```javascript
682
+ var CATEGORY_KEYWORDS = {
683
+ dimension: ['width', 'height', 'depth', 'length', 'diameter', 'dimension', 'size'],
684
+ material: ['material', 'alloy', 'grade', 'aluminum', 'steel', 'metal', 'brass', 'titanium', 'nylon'],
685
+ feature: ['hole', 'fillet', 'chamfer', 'pocket', 'slot', 'feature'],
686
+ constraint: ['tolerance', 'wall thickness', 'constraint', 'min wall', 'max size'],
687
+ machining: ['axis', 'machine', 'setup', 'fixture', 'tool access'],
688
+ };
689
+ var CATEGORY_TO_FIELD = {
690
+ material: 'material',
691
+ dimension: 'dimensions',
692
+ feature: 'features',
693
+ constraint: 'constraints',
694
+ machining: 'axis_recommendation',
695
+ };
696
+
697
+ function detectPlanField(agentId, content) {
698
+ var lower = content.toLowerCase();
699
+ // Check each category — if the agent matches AND content contains keywords, return the field
700
+ for (var cat in CATEGORY_KEYWORDS) {
701
+ var expectedAgent = PLAN_FIELD_AGENTS[CATEGORY_TO_FIELD[cat]];
702
+ if (expectedAgent !== agentId) continue;
703
+ var keywords = CATEGORY_KEYWORDS[cat];
704
+ for (var i = 0; i < keywords.length; i++) {
705
+ if (lower.indexOf(keywords[i]) !== -1) {
706
+ return CATEGORY_TO_FIELD[cat];
707
+ }
708
+ }
709
+ }
710
+ return null;
711
+ }
712
+ ```
713
+
714
+ - [ ] **Step 2: Add `extractFieldValue()` function**
715
+
716
+ ```javascript
717
+ function extractFieldValue(fieldKey, content) {
718
+ // Try quoted values first
719
+ var quoted = content.match(/"([^"]+)"/);
720
+ if (quoted) return quoted[1];
721
+
722
+ if (fieldKey === 'material') {
723
+ // Match known materials
724
+ var matList = ['aluminum 6061', 'aluminum 7075', 'stainless steel 304', 'stainless steel 316',
725
+ 'aluminum', 'steel', 'brass', 'copper', 'titanium', 'nylon', 'delrin', 'peek', 'polycarbonate'];
726
+ var lower = content.toLowerCase();
727
+ for (var i = 0; i < matList.length; i++) {
728
+ if (lower.indexOf(matList[i]) !== -1) return matList[i];
729
+ }
730
+ }
731
+
732
+ if (fieldKey === 'axis_recommendation') {
733
+ var axisMatch = content.match(/(3-axis|3\+2[\s-]*axis|5-axis)/i);
734
+ if (axisMatch) return axisMatch[1].toLowerCase();
735
+ }
736
+
737
+ if (fieldKey === 'dimensions') {
738
+ // Extract dimension values as object
739
+ var dims = {};
740
+ var dimPattern = /(\d+\.?\d*)\s*mm\s+(wide|width|tall|height|high|thick|thickness|deep|depth|long|length|diameter)/gi;
741
+ var m;
742
+ while ((m = dimPattern.exec(content)) !== null) {
743
+ var dimMap = {wide:'width',width:'width',tall:'height',height:'height',high:'height',
744
+ thick:'thickness',thickness:'thickness',deep:'depth',depth:'depth',long:'length',length:'length',diameter:'diameter'};
745
+ dims[dimMap[m[2].toLowerCase()] || m[2].toLowerCase()] = parseFloat(m[1]);
746
+ }
747
+ if (Object.keys(dims).length) return dims;
748
+ }
749
+
750
+ // Fallback: text after "recommend"/"suggest"/"use"
751
+ var recMatch = content.match(/(?:recommend|suggest|use)\s+(.+?)(?:\.|,|$)/i);
752
+ if (recMatch) return recMatch[1].trim();
753
+
754
+ return null;
755
+ }
756
+ ```
757
+
758
+ - [ ] **Step 3: Commit**
759
+
760
+ ```bash
761
+ git add web/index.html
762
+ git commit -m "feat: add detectPlanField and extractFieldValue for confirm auto-fill"
763
+ ```
764
+
765
+ ---
766
+
767
+ ### Task 11: Update `confirmRecommendation()` to auto-fill plan card
768
+
769
+ **Files:**
770
+ - Modify: `web/index.html` (lines ~2759-2769)
771
+
772
+ - [ ] **Step 1: Replace `confirmRecommendation()` function**
773
+
774
+ Replace the existing function (lines 2759-2769) with:
775
+
776
+ ```javascript
777
+ function confirmRecommendation(msgId, agentId) {
778
+ var el = document.querySelector('[data-msg-id="' + msgId + '"]');
779
+ if (!el) return;
780
+ var content = el.dataset.content;
781
+ var actions = document.getElementById(msgId + '-actions');
782
+ if (actions) {
783
+ actions.innerHTML = '<div class="msg-confirmed">' + t('confirmed') + '</div>';
784
+ }
785
+ // Send confirmation as user message so it updates design state
786
+ var agentName = (AGENTS[agentId] || {}).name || agentId;
787
+ sendMessage(t('confirmedMsg').replace('{agent}', agentName).replace('{content}', content));
788
+ // Auto-fill plan card field if plan is visible
789
+ if (designState.phase === 'planning' && designState.plan && document.getElementById('active-plan-card')) {
790
+ var field = detectPlanField(agentId, content);
791
+ if (field) {
792
+ var value = extractFieldValue(field, content);
793
+ if (value != null) {
794
+ updatePlanCardField(field, value);
795
+ }
796
+ }
797
+ }
798
+ }
799
+ ```
800
+
801
+ - [ ] **Step 2: Verify unified confirm/plan card behavior**
802
+
803
+ In the browser:
804
+ 1. Chat until agents provide recommendations but don't yet reach plan threshold
805
+ 2. Chat more until plan card appears
806
+ 3. Scroll to an engineering agent message about material — click Confirm
807
+ 4. Verify the plan card's material field updates with the extracted value
808
+ 5. Verify Current score recalculates
809
+ 6. Click Confirm on a message when no plan card is visible — verify it works as before (just sends the confirmation message)
810
+
811
+ - [ ] **Step 3: Commit**
812
+
813
+ ```bash
814
+ git add web/index.html
815
+ git commit -m "feat: confirmRecommendation auto-fills plan card when plan is visible"
816
+ ```
817
+
818
+ ---
819
+
820
+ ### Task 12: Run full test suite and verify end-to-end
821
+
822
+ **Files:** None (verification only)
823
+
824
+ - [ ] **Step 1: Run backend tests**
825
+
826
+ Run: `pytest tests/ -x -q`
827
+ Expected: All tests pass
828
+
829
+ - [ ] **Step 2: End-to-end browser verification**
830
+
831
+ Start the server: `python -m server.web`
832
+
833
+ Test the following flows:
834
+ 1. **Edit flow:** Trigger plan → edit material → edit dimensions → add notes → Approve → verify model generates with edited values
835
+ 2. **Save & Continue flow:** Trigger plan → edit features → Save & Continue → verify state preserved → chat more → re-trigger plan → verify edits are in new plan
836
+ 3. **Reject flow:** Trigger plan → edit fields → Reject → verify edits discarded, phase reset
837
+ 4. **Ask Agent flow:** Trigger plan → click Ask next to material → verify engineering agent responds in chat
838
+ 5. **Confirm auto-fill flow:** While plan is visible, Confirm an agent recommendation → verify plan field updates
839
+ 6. **Dual score:** Edit fields to reduce completeness → verify Current score drops, Original stays
840
+
841
+ - [ ] **Step 3: Commit any fixes found during verification**
842
+
843
+ ```bash
844
+ git add -A
845
+ git commit -m "fix: address issues found during end-to-end verification"
846
+ ```
847
+
848
+ (Skip this step if no fixes are needed.)