Skip to content

BUG: Scrolling in development projects forces back to top upon scrolldown #1012

Description

@bc037

Important

Problem: Scrolling in the amicode dashboard snaps back to the top on every scroll-down attempt, making content below the fold unreachable. Reported on WSL2 (linux-x64).

Approach: Three targeted fixes addressing shared patterns across all scrollable surfaces — structural equality on sessionListDirectories consumers, visibility-based loading gates, and a defensive CSS overflow fix.

Scope: session-header.tsx, session-side-panel.tsx, and home.tsx in the opencode fork overlay (fork-side edits, synced via opencode:build + sync:apply). No schema, backend, or extension-host changes.

Assumptions: The v2 layout is active (always ON for release builds). The reporter's "Development projects view" is one of the four candidate surfaces identified below — see Open question.

Original report

See first comment below for the full original bug filing by @bc037.

Context: harmoniqs.amicode v0.3.4 (linux-x64), WSL2, engine v1.18.29-amicode.26, "Development projects view in the extension UI."

Adversarial review

Reviewed manually (no amico binary). Round 1 was blocking: the original plan targeted HomeDesign in home.tsx, which is not rendered in the v2 route tree — the "/" route goes to NewSessionLanding (app.tsx:781), which redirects to a session. Round 2 re-scoped to the actual rendered surfaces and passed (approved-mechanical). Spec on disk.

Affected surface — ambiguous, four candidates

In the v2 layout, there is no standalone "home page" at "/". The reporter's "Development projects view" is one of these surfaces:

# Surface File Scroll container Rendered in v2?
S1 Sessions flyout (titlebar) session-header.tsx:980 overflow: hidden auto, max-height 70vh yes
S2 Side-panel "home" tab (widgets) session-side-panel.tsx:284 overflow-y-auto on flex-1 div yes
S3 WorkbenchPanel sidebar workbench-panel.tsx:55 overflow-y-auto on flex-1 div yes
S4 HomeDesign (full dashboard) home.tsx:1078 overflow-y-auto with md:overflow-hidden parent no

Root-cause analysis

Cause A — sessionListDirectories creates fresh arrays that cascade refetches

sessionListDirectories() (helpers.ts:69) always returns a new array. Every consumer that uses the result as a memo or query key triggers unnecessary downstream updates:

  • S1 (session-header.tsx:730): activeSessions memo calls sessionListDirectories on every reactive tick → new session list → <For> re-renders all rows.
  • S4 (home.tsx:293): projectDirectories memo passes the fresh array as a queryKey to sessionLoad → refetch → isLoading flashes true → session list unmounts → scroll resets.

Evidence: sameProjectList (home-projects.ts:126) already exists as a structural comparator for the project reconcile path.

Cause B — <Show> gates unmount content during async loading transitions

Multiple surfaces wrap scrollable content in <Show when={!loading}> gates that unmount the list on any refetch:

  • S1 (session-header.tsx:1070–1091): nested <Show> gates on filteredActiveSessions().length > 0.
  • S4 (home.tsx:1354): <Show when={!homeCardsLoading()}> unmounts the entire widget grid during resource refetch.
  • S2 (session-side-panel.tsx:285–306): <Show when={widgetInfos().length > 0 && dashboard()}>.

Cause C — home.tsx responsive overflow gap (S4 only)

Parent has md:overflow-hidden — below Tailwind 768px breakpoint, no clip. S4-only.

Acceptance criteria

  • Scrolling down in the sessions flyout (S1) holds position across polling cycles
  • Side-panel "home" tab (S2) does not flash skeleton on widget resource refetch
  • Scroll position is preserved at panel widths < 768px (S4 defensive)
  • pnpm --filter amicode test passes
  • Open: screen recording from reporter confirming the fix addresses their surface

Key decisions

Fix 1 — Structural equality on sessionListDirectories consumers

home.tsx:293 (S4):

  const projectDirectories = createMemo(() =>
    sessionListDirectories(projects(), focusedSync().data.project ?? []),
+   { equals: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) }
  )

session-header.tsx:730 (S1): extract directories into a structurally-compared memo so the session computation only re-runs when directories actually change:

const flyoutDirectories = createMemo(
  () => {
    if (!open()) return []
    const conn = server.current
    if (!conn) return []
    const ctx = globalCtx.ensureServerCtx(conn)
    if (!ctx) return []
    return sessionListDirectories(ctx.projects.list(), ctx.sync.data?.project ?? [])
  },
  { equals: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) }
)

Then activeSessions reads flyoutDirectories() instead of calling sessionListDirectories inline.

Fix 2 — Prevent cards unmount on refetch (S2 and S4)

session-side-panel.tsx:285 and home.tsx:1354: replace binary <Show when={!loading}> with a visibility approach. Show skeleton only on initial load (!widgetsRaw.latest); use CSS visibility: hidden during refetch.

SolidJS peer dep is ^1.9.0.latest confirmed available.

Fix 3 — home.tsx overflow-hidden gap (S4 defensive)

- class="... m-2 min-h-0 md:overflow-hidden ..."
+ class="... m-2 min-h-0 overflow-hidden ..."

Defensive — S4 is not routed in v2, but the component exists and the CSS bug is real.

Constraints & invariants

  • All edits are in the opencode fork, synced to the overlay via opencode:build + sync:apply.
  • Widget iframes are never remounted by the fix — context/theme updates stay on the postMessage bridge.
  • No fix targets a component that is not rendered in the reporter's layout (S4 fixes are explicitly marked defensive).

Open question

The exact surface has not been confirmed. The spec fixes the shared patterns across S1–S4 but cannot guarantee the reporter's surface is among them. @bc037: a screen recording would resolve this. If the surface is S3 (WorkbenchPanel) or the chat timeline virtualizer, a separate investigation is needed.

Testing

Test Verifies
pnpm --filter amicode test No regressions
Manual: open sessions flyout (S1), scroll down, wait 10s Fix 1 — position held
Manual: open sessions flyout during live solve, scroll down Fix 1 — position held across polling
Manual: open side-panel "home" tab (S2), trigger refetch Fix 2 — no skeleton flash
Manual: narrow panel < 768px, scroll in home (S4) Fix 3 — scroll works at all widths

Prior art

  • session.tsx:1674 — same class of bug (auto-scroll ResizeObserver snapping on any reflow), fixed by gating on session_working().
  • home-projects-view.tsx:349 — project list uses string-keyed <For> to avoid remounting.
  • sameProjectList() (home-projects.ts:126) — structural comparison already exists for project reconciliation.

intake: ready
spec: spec-20260911-153000-home-scroll-snap-back
review: approved-mechanical (manual, round 2 of 2; round 1 was blocking — wrong surface)
suggested_path: B

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions