Skip to content

Milestone/stu implement updated UI ux - #100

Merged
sgardoll merged 266 commits into
mainfrom
milestone/stu-implement-updated-ui-ux
Sep 29, 2026
Merged

sgardoll merged 266 commits into
mainfrom
milestone/stu-implement-updated-ui-ux

Conversation

@sgardoll

@sgardoll sgardoll commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Devin Review

…can reach a class transitively

(cherry picked from commit 9d338ad)
…can reach a class transitively

(cherry picked from commit 9d338ad)
…can reach a class transitively

(cherry picked from commit 9d338ad)
…can reach a class transitively

(cherry picked from commit 9d338ad)
…can reach a class transitively

(cherry picked from commit 9d338ad)
…pe in a Set

The public deploy route accepted an unbounded sdkPackages array and deduped
it with repeated result.contains scans, so an unauthenticated caller could
submit a very large list for quadratic parsing work and an equally large
scratch pubspec. Cap the list at maxDependenciesPerRequest (200) - the same
per-request budget the dependency maps use, since every accepted name is
emitted as a dependencies: entry in the generated pubspec - and reject with
the same 'Too many entries in verification...' error style. Deduplicate via
a Set instead, keeping insertion order and the List<String> contract.

Also rebuild dist/ so the committed output carries this branch's token
changes (new CSS bundle beside the unchanged JS).
The default fixture router continued every unrecognized request, so an
endpoint missing from the fixture map (runpipeline-image, the staging
FlutterFlow sync, an env-overridden deploy URL) could silently reach a
paid generation or a real project. The catch-all now aborts unmatched
requests so un-fixture'd calls fail tests loudly.

Only three request classes are allowed through, all explicit:
- fixture-map URLs are fulfilled as before;
- static assets index.html references are fulfilled with inert stubs
  (Tailwind, Google Fonts CSS, YouTube embeds), removing the page
  load's dependency on live CDNs and killing the YouTube player/ad/
  tracking request cascade;
- same-origin requests the local Vite dev server serves (the document,
  modules, Vite internals, public assets) continue, except /api paths,
  which vite.config.js proxies to real AI providers;
- the SRI-pinned static scripts (JSZip, highlight.js, FingerprintJS)
  continue because a local stub can never match the pinned digest and
  app.js needs FingerprintJS to resolve identity; SRI verifies them
  byte-for-byte.

Verified by rerunning the smoke suite (6 passed, 10x faster with the
YouTube cascade gone) and by probing that fetches to runpipeline-image,
the staging sync and /api proxy paths all abort.

STU-372
buildArtifactBundle synthesized deployOrder values like "file-1" while
the artifacts themselves get real IDs such as
"custom-action-greet-user". Normalization preserves unknown order IDs,
and the deployment planner matches order entries against artifact.id,
filtering out every synthetic value — so a deploy journey driven by
these fixtures produced an empty deployment plan instead of exercising
artifact deployment.

Map the artifacts first and derive deployOrder from the mapped
artifacts' IDs, the same relationship src/artifactBundle.js guarantees.
Verified against the real contract: oneArtifact now plans
lib/custom_code/actions/GreetUser.dart and manyArtifacts plans all
three files in class -> action -> widget order with no planner errors.

STU-372
The pull-request workflow ran only unit, build, deploy-guard, proxy and
leak-scan checks, so regressions in fixture routes, selectors and smoke
assertions could merge while CI stayed green.

Add the Chromium browser install and npm run test:ui after the
production build, so the deterministic browser suite gates every pull
request. Step style and the pinned-action/least-permission hardening
from the earlier CI commits are unchanged.

STU-372

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 1 new potential issue.

⚠️ 1 issue in files not directly in the diff

⚠️ Submitted attachments can disappear before generation

When runThinkingPipeline waits on preflight, removing an uploading image aborts it despite its slot already being snapshotted. Generation then silently omits that submitted image.

Devin Review

devin-ai-integration Bot and others added 7 commits September 28, 2026 18:06
…ui-ux' into pr105

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

# Conflicts:
#	e2e/stu385-composer-settings.spec.js
#	e2e/stu391-tutorial-onboarding.spec.js
#	src/composerAdapter.js
- cancelTyping() lands each running demo's end state instead of abandoning
  a submittable prefix (view switches, tab hides).
- A pointerdown takeover releases demo ownership after the restore, so the
  first Backspace trims the complete prompt; focus/keydown restores stay
  demo-owned so typing mid-demo still writes a fresh prompt.
- e2e: click+Backspace trims one char; switchView during typing and during
  the hold both settle the shipped prompt and Generate submits it whole.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
* fix: hero demo takeover can never submit a partial prompt

A mid-demo click/keypress canceled the typing timer but left the frozen
prefix in the field, so a Generate click or Enter could submit a
half-typed example. Takeover now lands the demo's own end state — the
complete shipped prompt with the caret retired — via finishHeroDemo.

Two takeover shapes skip the restore instead: an interaction arriving
with text already selected (select-all, assistive insert) is an edit in
flight — overwriting the field would destroy the selection the incoming
replacement needs — and a takeover during the hold period has nothing
typed to complete. Both freeze the demo on the prefix and mark it
demo-owned: send stays disabled and Enter stays inert until a real
input event hands the text to the user.

Also fixes the reduced-motion listener lifecycle: a pagehide dispose
made bfcache restores lose the listener, so a demo replayed after
back-forward navigation ignored a later flip. The listener now lives
for the document's lifetime (dispose reserved for genuine teardown),
and a repeat initComposer on the same DOM replaces the dead
registration via a WeakMap instead of stacking a second listener.

Adds an e2e covering a mid-demo click followed by an immediate submit,
and demo-completion waits in smoke/stu376 where prompts were being
polled mid-type.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

* fix: edits while the demo owns the field never join the example

Takeover restore landed the full shippedPrompt in the field, but a
keystroke, Backspace, paste, drop or IME commit that followed then
joined the example — the browser inserts into whatever the field
holds, and the input handler treated the combination as user text.

Text the demo supplied while interrupted is now tracked as
demo-owned (heroDemoOwned) through the frozen and restored states.
A beforeinput listener — the single funnel every real edit passes
through — clears it before the native mutation lands, so typing
mid-demo produces a prompt of the user's own and Backspace removes
demo text rather than deleting inside the example. A demo that
finishes untouched hands the shipped text back as the field's own
content, so ordinary editing resumes.

New e2e coverage: mid-demo typing yields only the typed text, and
Backspace during the demo leaves an empty field.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

* fix: system cancellations and pointer takeovers settle the hero demo

- cancelTyping() lands each running demo's end state instead of abandoning
  a submittable prefix (view switches, tab hides).
- A pointerdown takeover releases demo ownership after the restore, so the
  first Backspace trims the complete prompt; focus/keydown restores stay
  demo-owned so typing mid-demo still writes a fresh prompt.
- e2e: click+Backspace trims one char; switchView during typing and during
  the hold both settle the shipped prompt and Generate submits it whole.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
…init

The demo-window catches raced the demo's real-time clock: an 8s deadline
against a ~5.2s hold left <3s for page init on a loaded worker, so the
'a live reduced-motion flip mid-demo' spec timed out before the typing
window opened. Raise the shared catch budget to 20s — it covers init
plus the hold while still ending before the demo could finish, and
leaves every assertion unchanged.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
Fix flaky stu376 demo-window catch: budget must cover page init + the 5.2s hold

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 2 new potential issues.

Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Confirm generated bundle deployment path

The PR commits generated assets alongside application source. Confirm whether deployment serves dist and whether the committed bundle matches the final build.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/heroMarkField.js Resolved
devin-ai-integration Bot and others added 3 commits September 28, 2026 19:00
…e-origin proxy

The runner's CORS allowlist admits only the production origin, so under
npm run dev the browser's preflight was rejected and provisioning failed
instantly as UnconfirmedDeployError. Vite now proxies /api/ffai-runner to
the runner and the dev build calls that same-origin path; production still
uses the runner URL directly. The e2e endpoint fixture is a glob so the
existing routes match whichever URL form the app uses.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
When a second modal opens while the commit-progress overlay still holds
focus, setBackgroundInert marks the overlay aria-hidden around the focused
element and Chrome blocks it. Drop focus to the body so the opening modal's
own focus step can take over, and rebuild dist.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
The fill jumped between stage boundaries (doneCount/3) so nothing moved
while a stage ran. Each stage now owns a third of the track and the fill
eases asymptotically toward 95% of its slice on the existing 250ms tick,
landing on the real boundary only when the stage's response arrives.
Failed stages freeze where they eased; skipped slices (refine re-entry)
start consumed.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 1 new potential issue.

🐛 1 issue in files not directly in the diff

🐛 Server errors trigger duplicate writes

When syncCustomCodeChanges returns HTTP 500, pushCodeWithRetry sends the same write again. The first response can follow a successful write, so retries can apply one deploy multiple times.

Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor

This is the deliberate trade-off this code documents and locks in tests, so flagging the semantics rather than treating it as a bug: an HTTP 500 from the synchronous push is ambiguous — the write may or may not have been applied — and sawUncertainAttempt is set on any 5xx precisely because "it can never prove a refusal." The retry is bounded (exactly-500 only; every other 5xx goes straight to UnconfirmedDeployError), and if all attempts exhaust at 500 the outcome can't be reported as a clean failure — it lands as unconfirmed, per deployOutcome.test.js's "every endpoint answered 500" case.

The alternative — treat a 500 like every other 5xx and stop retrying — removes the duplicate-write risk at the cost of failing a deploy that may have been refused before any write. Happy to make that change if you'd rather have zero retry ambiguity, but it's an intentional design choice on this branch rather than an oversight, so I've left it as designed.

devin-ai-integration Bot and others added 5 commits September 28, 2026 19:12
…their boundary

- Failed runs pin the fill's rendered width with the transition disabled,
  so the bar stops where it visibly was instead of drifting 700ms more.
- A completed active stage sits on its own boundary (activeStage/3), so
  stage-2 re-entry runs finish on 100% instead of retreating to 66.7%.
- e2e: held stages 2/3 assert upper ceilings; a new spec pins the frozen
  width across the transition window; the refine spec asserts a 100% finish.
- Timer comment now covers both elements it drives.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
…rack %

- Reading getComputedStyle after transition:none resolves the pending
  target, not the mid-flight position — capture first, then pin.
- The pin is now renderedPx/trackPx as a percentage, so a later viewport
  resize keeps the same proportion instead of a stale pixel count.
- e2e: frozen pin asserts a %-valued inline width and a fixed computed
  width across a 900ms window.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
* fix: pipeline progress bar eases inside each stage's slice

The fill jumped between stage boundaries (doneCount/3) so nothing moved
while a stage ran. Each stage now owns a third of the track and the fill
eases asymptotically toward 95% of its slice on the existing 250ms tick,
landing on the real boundary only when the stage's response arrives.
Failed stages freeze where they eased; skipped slices (refine re-entry)
start consumed.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

* fix: review follow-ups — freeze fill on failure, done stages land on their boundary

- Failed runs pin the fill's rendered width with the transition disabled,
  so the bar stops where it visibly was instead of drifting 700ms more.
- A completed active stage sits on its own boundary (activeStage/3), so
  stage-2 re-entry runs finish on 100% instead of retreating to 66.7%.
- e2e: held stages 2/3 assert upper ceilings; a new spec pins the frozen
  width across the transition window; the refine spec asserts a 100% finish.
- Timer comment now covers both elements it drives.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

* fix: read rendered width before disabling the transition; freeze as track %

- Reading getComputedStyle after transition:none resolves the pending
  target, not the mid-flight position — capture first, then pin.
- The pin is now renderedPx/trackPx as a percentage, so a later viewport
  resize keeps the same proportion instead of a stale pixel count.
- e2e: frozen pin asserts a %-valued inline width and a fixed computed
  width across a 900ms window.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

* test: assert the frozen fill keeps its track share across a resize

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Stuart Gardoll <sgardoll@gmail.com>
The error-input overlay is absolutely positioned over the results stage,
which clips overflow; its centered card overflowed top and bottom whenever
the stage was shorter than the card, leaving the textarea and actions
unreachable. Scroll the panel and center the card with auto margins so it
centers when there is room and scrolls from the top when there is not.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 3 new potential issues.

⚠️ 1 issue in files not directly in the diff

⚠️ Stored project bypasses conflicting selection

When a different project is bound, getConfirmedStoredProjectId still returns the stored project after listing it. The deploy can target the stored project despite the active selection pointing elsewhere.

Devin Review

Comment thread src/pubspecSync.js Resolved
Comment thread .github/workflows/ci.yml
Comment on lines 40 to 47
- name: Install Playwright Chromium
if: steps.browser-tests.outputs.ui == 'yes' || steps.browser-tests.outputs.accessibility == 'yes'
run: npx playwright install chromium --with-deps

- name: Run browser tests
if: steps.browser-tests.outputs.ui == 'yes'
run: npm run test:ui

- name: Run accessibility modal tests
if: steps.browser-tests.outputs.accessibility == 'yes'
run: npm run test:accessibility

@devin-ai-integration devin-ai-integration Bot Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Check browser-suite CI budget

CI now runs the full Playwright suite and accessibility checks. Confirm the required job has sufficient time for the deployment scenarios and their fixtures.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

At <=920px the phase title measured ~304px against the 298px content
column, so the longest title ('Understanding your prompt') wrapped to a
second line and pushed every later section down 30px — the 390px exact
fidelity spec caught the 527px panel that should be 497px. Tightening the
mobile tracking keeps the same size and line-height on one line.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 0 new potential issues.

Devin Review

- provisionStream: only an explicit "result" event (or a bare JSON
  payload with a boolean success) counts as the runner's answer — a
  heartbeat or progress event no longer fabricates a final result.
- composerAdapter: typing a chip or tapping elsewhere mid-typing now
  completes the chip's end state (finishChipTyping) instead of
  cancelling, so partial text can never be submitted.
- app.js: readAsDataUrl aborts on signal so an aborted upload frees the
  send gate; removePromptImage refuses while a generation is running so
  run-claimed uploads can't be deleted mid-run.
- app.js: verified-write credits in the dirty ledger are spent once a
  fresh export postdates them (matching, reformatted, or missing) —
  uncertain records still survive.
- app.js: getConfirmedStoredProjectId follows a confirmed
  key+generation binding that names a different project instead of
  returning the stored project it replaced.
- heroMarkField/logoMotion: pagehide disposes only when the page is
  not persisted; a bfcache-persisted page pauses and resumes on
  pageshow.
- pubspecSync: rewriteVersionMember skips blank and comment-only lines
  inside a block mapping, so a version member written after them is
  still found and rewritten (regression tests added).

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Resolved the remaining live Devin Review findings in cd4c657:

  • provisionStream phantom results: only an explicit "result" event (or a non-streaming bare JSON payload carrying a boolean success) is now treated as the runner's answer — heartbeats and other progress noise leave the outcome unconfirmed instead of fabricating success.
  • composerAdapter chip truncation: takeover mid-typing (keydown or pointerdown) now calls finishChipTyping(), landing the chip's complete end state before canceling — partial text can no longer be submitted.
  • readAsDataUrl hang: the FileReader aborts with the signal, so an aborted image upload resolves null → the send gate is freed instead of hanging.
  • removePromptImage mid-run: refuses while a generation is running (toast) so run-claimed uploads can't be deleted under the pipeline.
  • Dirty ledger stale credits: once a fresh export postdates the writes, verified credits are spent whether the recorded bytes were observed verbatim, reformatted, or missing — uncertain records still survive.
  • getConfirmedStoredProjectId: a confirmed key+generation binding that names a different project now wins over the stored project it replaced.
  • heroMarkField / logoMotion bfcache: pagehide disposes only when the page is not persisted; persisted pages pause and resume on pageshow.
  • pubspecSync version search: rewriteVersionMember skips blank/comment-only lines inside a block mapping — the exact intl: case from the finding now rewrites (regression test added).

Two findings were left as-is by design (retry on HTTP-500 → sawUncertainAttempt UNCONFIRMED rather than duplicate-write avoidance) — the behavior is deliberate and locked by deployOutcome.test.js.

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 3 new potential issues.

🐛 2 issues in files not directly in the diff

🐛 Abandoned generation remains on screen

When navigation interrupts a running generation, switchView cancels its requests but leaves the progress panel visible. Returning home restores that panel, although no run remains to finish or clear it.


⚠️ Failed replacement resets selected file

When refinement or error regeneration fails, restoreAndShowReplacementFailure restores the selected artifact but showResultsView switches to Summary. The user's previous file view is lost despite the retained result.

Devin Review

Comment thread src/deployOutcome.js
* only the UI's waiting/rendering: on expiry the UI stops claiming progress,
* but it never aborts or retries the in-flight remote write.
*/
export const DEPLOY_UI_TIMEOUT_MS = 120_000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Deploy UI bound versus runner lifetime

The browser reports an unconfirmed deploy after 120 seconds, while the runner can operate for 900 seconds. Check production provisioning durations to determine how often normal deployments enter the reconciliation flow.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bound only governs UI waiting/rendering — it never aborts or retries the in-flight write, and the runner keeps working until its own server-side deadline. The derivation comment above the constant already covers the gap: observed provision latency is on the order of one to a few minutes, so 120s is comfortably longer than a healthy provision yet well below the 900s Cloud Run timeout, meaning the UI can never out-wait a real server decision. A deploy that legitimately takes >120s would land in the UNCONFIRMED → reconciliation path by design; whether that's frequent enough to warrant raising the bound is a product-observability question for real production metrics rather than something the code can decide here.

…ailure

- switchView: a run abandoned by navigation now dismisses its progress
  panel and returns the stage to settled content — the previous result
  when one exists, otherwise the ready state — so returning home can no
  longer resurrect a panel for a run that no longer exists.
- restoreAndShowReplacementFailure: resultsViewMode joins the result
  snapshot, so a failed refinement or regeneration restores the Summary
  or file surface the user was inspecting instead of resetting to
  Summary.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 1 new potential issue.

🐛 1 issue in files not directly in the diff

🐛 Model switch drops submitted images

During upload preflight, switching to a non-vision model makes updatePromptImageAvailability abort the submitted image uploads. runThinkingPipeline retains those slots but later filters out slots without URLs, so generation silently loses the images.

Devin Review

updatePromptImageAvailability now leaves attachments alone while a run
is in flight — the run froze submittedImages before its first await, so
dropping promptImages then would abort uploads the slice still expects
to commit and silently strip the images it was started with.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 0 new potential issues.

Devin Review

@sgardoll
sgardoll merged commit 8d3cbfa into main Sep 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants