Repository navigation
STU-385: Move generation options into the composer settings dialog - #97
Conversation
The composer's gear opens a generation-settings dialog that presents the existing model selector and the supported generation option (image input). #code-generator-model remains the single element the pipeline reads, so selections reach the actual request unchanged; provider keys stay a shortcut into the canonical account connection editor; ordinary close returns focus to the composer and never reopens the walkthrough. Regenerates the tracked dist/index.html and prunes the orphaned bundle.
…s it Removing the on-page #code-generator-model flattened the hero so it no longer overflowed at 390x844, breaking the hard 'page is reachable' scroll invariant (stu389). Keep the canonical selector in hero-settings and make the composer settings dialog's model control a synchronized mirror that forwards to it, so dialog selections still reach the actual generation request while availability labels and the free-tier upgrade action stay in step. stu377 back to baseline.
Free-tier and paid-tier dialog at 360x800, 1440x900.
|
This PR does not match any of the 1 configured review trigger rule. |
There was a problem hiding this comment.
🔍 Settings migration remains incomplete
The original model selector and API-key link remain visible below the composer. Users now see duplicate controls outside the new dialog.
(Refers to this code)
Was this helpful? React with 👍 or 👎 to provide feedback.
| } | ||
| </style> | ||
| <script type="module" crossorigin src="/assets/index-oXwmwfKe.js"></script> | ||
| <script type="module" crossorigin src="/assets/index-BdVVPYTC.js"></script> |
There was a problem hiding this comment.
🔴 Production bundle is missing
A fresh checkout references index-BdVVPYTC.js, which is absent from HEAD. The app cannot load, and the deployment guard rejects the release.
Learn more
The production HTML names a content-hashed JavaScript bundle. The PR deletes the old tracked bundle but does not commit the new one. ensure_dist_self_consistent rejects exactly this state because publishing it would upload broken HTML and prune the working remote bundle.
Example: On a fresh checkout, /assets/index-BdVVPYTC.js returns 404. python3 scripts/deploy_ftp.py --dry-run exits because that referenced file does not exist.
Recommended fix: Run the production build, then force-add and commit the generated dist/assets/index-BdVVPYTC.js. Confirm every local dist/ file is tracked and rerun the deploy guard.
Was this helpful? React with 👍 or 👎 to provide feedback.
| syncComposerSettingsModel(); | ||
| renderComposerSettingsOptions(); |
There was a problem hiding this comment.
🟡 Early Pro selection bypasses gating
Before subscription reconciliation, openComposerSettings exposes ungated Pro choices. A free user's selection skips pricing, then generation silently falls back to Gemini.
Learn more
The page becomes interactive while the asynchronous auth and subscription startup still runs. The canonical Pro-gating listener is installed only when updateModelSelectorGating runs after reconciliation. Opening this dialog earlier copies the raw canonical options, and its change handler can select a Pro model before that listener exists. The pipeline later waits for reconciliation, then getEffectiveModel replaces that selection with the free model without opening pricing.
Example: A returning free user clicks the gear during a slow subscription request and selects Claude Opus 5. The dialog retains Claude and no pricing modal appears, but the generator request uses Gemini.
Recommended fix: Keep the model control disabled or in a loading state until sessionReadiness settles. Then run updateModelSelectorGating(), synchronize the mirror, and enable selection.
Was this helpful? React with 👍 or 👎 to provide feedback.
| </div> | ||
|
|
||
| <div class="rounded-lg border border-gray-200 overflow-hidden"> | ||
| <button type="button" class="w-full flex items-center justify-between gap-3 px-3 py-2.5 text-left hover:bg-gray-50 transition-colors" onclick="openApiKeysModal()" aria-haspopup="dialog" aria-controls="api-keys-modal"> |
There was a problem hiding this comment.
🟡 API-key shortcut loses focus
Opening API keys makes the hidden shortcut button its return target. Closing API keys leaves keyboard focus without a visible control.
Learn more
Opening a new modal automatically closes the active modal without restoring focus. openModal then records the currently focused shortcut as the API-key modal's return target. That shortcut belongs to the now-hidden composer settings dialog, so closeModal cannot return keyboard focus to a visible control.
Example: A keyboard user opens Generation settings, activates “API keys & connection,” and closes API Keys. Focus does not return to the gear or another visible composer control.
Recommended fix: Close the composer settings dialog before opening API keys so focus first returns to the gear, or extend the modal API to accept an explicit return target and pass the gear button. Rebuild dist/ after changing the source HTML.
Was this helpful? React with 👍 or 👎 to provide feedback.
…broken The branch regenerated dist/index.html and deleted dist/assets/index-oXwmwfKe.js without committing a replacement bundle, so dist/index.html referenced /assets/index-BdVVPYTYC.js while dist/assets held only a font and a CSS file — a built page whose entire application script 404s. dist/ is regenerated by CI (`npm run build`) and by the documented deploy path, and tracked build output is what caused a cross-PR merge conflict earlier today. Reverted to the integration branch's copy; no source change.
| mirror.value = canonical.value; | ||
| const canonicalOptions = Array.from(canonical.options); | ||
| Array.from(mirror.options).forEach((mo, i) => { | ||
| const co = canonicalOptions[i]; | ||
| if (co) mo.textContent = co.textContent; | ||
| }); |
There was a problem hiding this comment.
🟡 Plan resolution leaves model mirror stale
When plan resolution finishes with settings open, composer-settings-model retains its earlier selection and labels. A free-tier reset changes the canonical model to Gemini, while the dialog still displays Pro.
Learn more
The dialog mirror synchronizes only when the dialog opens or its own model control changes. Startup intentionally leaves composer controls interactive while authentication and subscription requests run. Later, updateModelSelectorGating can relabel options and reset a Pro selection to FREE_MODEL, but it never synchronizes the open mirror.
Example: A user opens settings before startup plan resolution, selects Claude, and then resolves as free-tier. The canonical selector resets to Gemini, but the open dialog still displays Claude without the current free-tier labels. The next generation uses Gemini despite the visible selection.
Recommended fix: Synchronize the composer mirror whenever updateModelSelectorGating changes the canonical selector or its labels. Also refresh the dialog capability row after a value reset.
Was this helpful? React with 👍 or 👎 to provide feedback.
| <select id="composer-settings-model"> | ||
| <option value="google/gemini-3.7-flash" selected>Gemini 3.7 Flash</option> | ||
| <option value="anthropic/claude-opus-5">Claude Opus 5</option> | ||
| <option value="openai/gpt-5.6-sol">GPT-5.6 Sol</option> | ||
| <option value="z-ai/glm-5.2">GLM 5.2</option> | ||
| <option value="moonshotai/kimi-k3">Kimi K3</option> | ||
| <option value="openrouter/auto-beta">OpenRouter: Auto Router</option> | ||
| <option value="openrouter/free">OpenRouter: Free Models</option> | ||
| <option value="openrouter/deepseek/deepseek-v4-pro">DeepSeek v4 Pro</option> | ||
| </select> |
d3708da
into
milestone/stu-implement-updated-ui-ux
STU-385 — Move generation options into the composer settings dialog
Linear: https://linear.app/stuart-gardoll/issue/STU-385/move-generation-options-into-the-composer-settings-dialog
What changed
The composer's gear now opens a generation settings dialog that presents the existing model selection and the supported generation option (image input), bound to the live
MODEL_LABELS/FREE_MODEL/PRO_MODELSand tier state (free / paid / unresolved, including checking/failed resolution). Provider-key management is a shortcut into the canonical account connection editor (openApiKeysModal, STU-384) — not a second independent editor. Closing the dialog returns focus to the composer and never reopens the walkthrough.#code-generator-modelremains the single element the generation pipeline reads (no key-storage or transport change), so selections flow into the actual generation request. It stays on the page and the dialog's model control is a synchronized mirror that forwards to it — this was required to preserve the hero's vertical-scroll invariant at 390×844 (stu389hard-fails if the on-page selector is removed: basescrollHeight 908vs844after removal).Acceptance criteria
stu385-composer-settings.spec.js"model availability by subscription tier": free labels Pro models "(PRO)" and shows "Upgrade for all models" (selecting a Pro model reverts to Gemini and opens pricing); paid unlocks Pro models unlabelled with no upgrade notice; unresolved never claims a free-only limitation.ENDPOINTS.pipelineand assertsbody.model === "anthropic/claude-opus-5"for the generator stage.#code-generator-modelis unchanged as the request source; no key-storage code touched.#walkthrough-modalnever opens via Escape or Done. Consistent with and non-regressing STU-391 (walkthroughSettingsActiveuntouched by normal close; ghost of the app.js:949–960 bug exercised).CI results (all on this branch, hermetic
CI=true)npm testnpm run buildnpm run test:accessibilitycomposer-settings-modal)npm run test:deploy-guardnpm run test:proxiesCI=truerun 1CI=truerun 2CI=true(control)The base control run (15 min, heavier load) produced more flakiness than this branch, confirming the stu389 timing flakes are pre-existing in this repo (its own
playwright.config.jsnotes CI is "SERIAL on a slower runner"); they were not introduced here and passed on retry in both runs.Falsification (each behavior broken → test failed → restored → passed)
Every test was written against a concrete break I actually induced; observed fail + pass for each:
onclicktoopenApiKeysModal()→ all 8 STU-385 tests failed (#composer-settings-modalnever gets.open). Restored → 8 pass.closeModal(..., { restoreFocus: false })→ "closing the dialog returns focus to the composer" failed (activeElement≠ gear). Restored → pass.closeComposerSettings→ "an ordinary close never reopens the walkthrough" failed. Restored → pass.getEffectiveModelstubbed toFREE_MODEL→ request test failed (generatorModels≠ selected). Restored → pass.canonical.value = mirror.value→ request test failed (request stayed Gemini). Restored → pass.Grep confirmed no
FALSIFICATIONor other break marker remains in the tree.Screenshots (changed surfaces)
screenshots/stu-385/stu385-composer-settings-free-1440.png(1440×900, free tier)screenshots/stu-385/stu385-composer-settings-free-360.png(360×800, free tier)screenshots/stu-385/stu385-composer-settings-paid-1440.png(1440×900, paid tier)Visual slices validate 360×800 and 1440×900 plus the dialog's own responsive column (the modal is centered
modal-overlay); the milestone's full nine-viewport matrix is owned by final acceptance.Files changed
index.html— gear bound toopenComposerSettings();composer-settings-modal(mirror model select + image-input generation option + provider-key shortcut → account editor); hero-settings kept as the canonical selector surface.app.js—openComposerSettings/closeComposerSettings/syncComposerSettingsModel/renderComposerSettingsOptions+windowexports. Closing never touches walkthrough state.e2e/stu376-composer.spec.js— settings-control test now asserts the generation-settings dialog + provider-key shortcut.e2e/stu385-composer-settings.spec.js— new (8 tests).dist/index.html— tracked deployable artifact regenerated; orphaned bundle pruned.screenshots/stu-385/*.png— review captures.Branch:
sgardoll/stu-385-move-generation-options-into-the-composer-settings. Top commitb392fd8; push containment verified:CONTAINED.