Skip to content

fix: stabilize session opening state#969

Merged
Astro-Han merged 11 commits into
devfrom
codex/i965-session-opening-skeleton
May 28, 2026
Merged

fix: stabilize session opening state#969
Astro-Han merged 11 commits into
devfrom
codex/i965-session-opening-skeleton

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes session switching so PawWork no longer renders a half-open blank session state. The target session now stays in an opening skeleton until both session info and its message cache are hydrated, and the composer remains editable while send/external actions stay blocked.

Why

Switching sessions could briefly expose raw target state, including a ses_... header, blank timeline, and loading prompt placeholder. In some races it could stay there. The fix keeps route display readiness tied to the real target session cache and replaces the old spinner card with a timeline-shaped skeleton.

Related Issue

Fixes #965

Human Review Status

Pending

Review Focus

Please check the route readiness gate and the composer readiness split. The intended contract is: target timeline only renders after target session info plus target message cache are present; local typing remains available; sending, file attachments, mode changes, and other external actions remain disabled until action readiness returns.

Risk Notes

UI behavior changes are limited to session opening and composer readiness. No platform, packaging, updater, signing, dependency, permission, migration, deletion, docs, release note, credential, or generated-content surfaces were changed.

How To Verify

Full app unit suite: 1622 passed
Command: bun run --cwd packages/app test:ci

Focused route/composer/skeleton/attachment tests: 48 passed
Command: cd packages/app && bun test src/components/prompt-input/attachments.test.ts src/pages/session/use-session-timeline-data.test.ts src/components/prompt-input/readiness.test.ts src/pages/session/session-opening-skeleton.test.ts

App typecheck: passed
Command: bun run --cwd packages/app typecheck

Root lint: passed
Command: bun run lint

Visual snap: passed and generated docs/design/preview/screenshots/session-opening-skeleton.png
Command: bun run snap session-opening-skeleton

Screenshots or Recordings

session-opening-skeleton

Checklist

  • Type label — this PR carries exactly one of bug, enhancement, task, documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.
  • Routing labels — this PR carries at least one of app, ui, platform, harness, ci. The labeler bot assigns these on PR open based on changed paths. Confirm the bot's choice (or override if wrong), then tick this.
  • Priority label — this PR carries exactly one of P0, P1, P2, P3. The priority-triage bot suggests one on PR open. Confirm or override, then tick this.
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason> (default is Pending; "not required" is restricted to bot-authored low-risk PRs).
  • I linked the related issue, or stated in Summary why there is no issue.
  • I described the review focus and any meaningful risks.
  • I replaced the example block in How To Verify with the real verification steps and the key result for each.
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope.
  • (conditional) I manually checked visible UI or copy changes when needed, with screenshots or recordings. Leave unticked only if no visible UI or copy changed.
  • (conditional) I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes. Leave unticked only if no platform/packaging surface was touched.
  • (conditional) I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant. Leave unticked only if none of those surfaces was touched.
  • I reviewed the final diff for unrelated changes and suspicious dependency changes.
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added SessionOpeningSkeleton overlay with animated placeholder content for improved visual feedback during session initialization
    • Enhanced readiness handling for prompt input operations with better state management
  • Refactor

    • Restructured session opening UI to use overlay-based presentation with smooth transitions
    • Improved attachment and clipboard handling with stricter readiness validation
  • Tests

    • Added comprehensive e2e snapshot and unit test coverage for session opening state

Review Change Stack

@Astro-Han Astro-Han added bug Something isn't working P2 Medium priority app Application behavior and product flows ui Design system and user interface labels May 28, 2026
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@Astro-Han, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 24 minutes and 31 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 07df4baf-7662-4d44-ae9a-15958f67bc80

📥 Commits

Reviewing files that changed from the base of the PR and between b033307 and 870514f.

📒 Files selected for processing (3)
  • packages/app/src/components/prompt-input/keydown.ts
  • packages/app/src/components/prompt-input/readiness.test.ts
  • packages/app/src/components/prompt-input/readiness.ts
📝 Walkthrough

Walkthrough

This PR fixes the blank half-loaded session state that occurs during cross-session switching by introducing a SessionOpeningSkeleton overlay that displays while session data hydrates. It refines prompt-input attachment operations to respect readiness callbacks, improves route timeline readiness gating to check both session info and messages, and removes shell-surface action callbacks.

Changes

Session Opening Skeleton Overlay

Layer / File(s) Summary
Shell mode activation readiness helper
packages/app/src/components/prompt-input/readiness.ts, packages/app/src/components/prompt-input/readiness.test.ts, packages/app/src/components/prompt-input/keydown.ts
New shouldActivateShellModeFromBang helper encapsulates bang-key activation logic (normal mode, cursor at start, action-ready) and is called from keydown handler instead of inline checks.
Prompt input attachment readiness gating
packages/app/src/components/prompt-input.tsx, packages/app/src/components/prompt-input/attachments.ts, packages/app/src/components/prompt-input/attachments.test.ts, packages/app/src/components/prompt-input/pick-attachments.ts, packages/app/src/components/prompt-input/pick-attachments.test.ts
Attachment operations now respect actionReady state via isReady and externalReady callbacks; clipboard/drop/picker operations abort when not ready. Tooltip and styling no longer reflect readiness (only operations are gated). Tests verify attachments abort and do not mutate state when not ready.
SessionOpeningSkeleton component and styling
packages/app/src/pages/session/session-opening-skeleton.tsx, packages/app/src/pages/session/session-opening-skeleton.test.ts, packages/ui/src/components/message-part.css
New component renders overlay with animated placeholder lines for user/assistant messages; line widths are deterministic per message via seeded hashing. CSS adds skeleton-line styling with theme-aware color and conditional gap reduction. Unit test validates DOM structure and absence of action/spinner elements.
Session timeline readiness and main view refactoring
packages/app/src/pages/session/use-session-timeline-data.ts, packages/app/src/pages/session/use-session-timeline-data.test.ts, packages/app/src/pages/session/session-main-view.tsx
routeMessagesReady now gates on both sessionInfo presence and message cache via currentSessionCacheReady, keeping opening state visible until both hydrate. SessionMainView renders skeleton overlay (with delayed show and crossfade hide) instead of inline opening UI, removes retry/new-session props, and allows composer to render once activeSessionID is set.
Shell surface cleanup and test expectations
packages/app/src/pages/session.tsx, packages/app/src/shell-frame-contract.test.ts
Removes useShellSurface integration, eliminating retry/new-session callbacks and base64 decoding. Test expectations updated: composer container gains relative class, and composer rendering depends only on activeSessionID.
E2E snapshot test for skeleton
packages/app/e2e/snap/fixtures/session-opening-skeleton-fixture.tsx, packages/app/e2e/snap/session-opening-skeleton.snap.ts
Playwright snapshot test mounts skeleton fixture, waits for theme, validates DOM structure (root visible, no spinner/buttons), and captures desktop/narrow screenshots in grid layout. Fixture helper clears target, applies styling, and renders skeleton with hardcoded label.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Astro-Han/pawwork#107: Introduces pickAttachments attachment ingestion routing; this PR extends it with readiness gating (isReady/externalReady callbacks).
  • Astro-Han/pawwork#424: Adds the opening-state gating/actions in session-main-view-state.ts; this PR replaces the inline opening UI with a SessionOpeningSkeleton overlay.

Poem

🐰 A skeleton appears to guide the way,
While session data finds its golden ray,
No blank half-loaded states to face,
Just graceful shells and breathing space.
Readiness gates the composer's call—
The opening state protects them all! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: stabilize session opening state' is concise, specific, and directly summarizes the main change: preventing a half-loaded blank state when switching sessions by stabilizing the session opening state.
Description check ✅ Passed The PR description is comprehensive and follows the required template. It includes Summary, Why, Related Issue, Human Review Status, Review Focus, Risk Notes, How To Verify with concrete test results, Screenshots, and a completed Checklist.
Linked Issues check ✅ Passed All coding requirements from issue #965 are met: route readiness now gates on both sessionInfo and message cache presence (use-session-timeline-data.ts), the opening skeleton is shown until display-ready (session-main-view.tsx, session-opening-skeleton.tsx), composer remains editable but external actions are blocked via actionReady gating (prompt-input.tsx, attachments.ts, pick-attachments.ts), and regression tests verify the cache-present-but-sessionInfo-missing scenario.
Out of Scope Changes check ✅ Passed All changes are in scope: they address session opening state stabilization (session-main-view.tsx, session-opening-skeleton.tsx, use-session-timeline-data.ts), composer readiness gating (prompt-input.tsx, attachments.ts, pick-attachments.ts, readiness.ts), and cover edge cases with tests and visual snapshots. No unrelated refactors, dependency changes, or out-of-scope modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/i965-session-opening-skeleton

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested priority: P2 (includes user-path files (packages/app/src/components/prompt-input.tsx, packages/app/src/components/prompt-input/attachments.test.ts, packages/app/src/components/prompt-input/attachments.ts, packages/app/src/components/prompt-input/readiness.test.ts, packages/app/src/components/prompt-input/readiness.ts, packages/app/src/pages/session/session-main-view.tsx, packages/app/src/pages/session/session-opening-skeleton.test.ts, packages/app/src/pages/session/session-opening-skeleton.tsx, packages/app/src/pages/session/use-session-timeline-data.test.ts, packages/app/src/pages/session/use-session-timeline-data.ts)).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a timeline-shaped SessionOpeningSkeleton component to replace the previous spinner and action buttons during session initialization, managing its transitions and visibility within SessionMainView. It also refactors PromptInput to allow local text input, navigation, and plain-text pasting while actions are not ready, while still restricting file/image attachments. The review feedback suggests tightening the paste handler in PromptInput to prevent bypassing file attachment blocks with native clipboard images, and optimizing the initial state of openingSkeletonMounted in SessionMainView to avoid an unnecessary re-render on mount.

Comment thread packages/app/src/components/prompt-input.tsx
Comment thread packages/app/src/pages/session/session-main-view.tsx Outdated
@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown

Perf delta summary

Comparator: pass

Profile / Scenario interaction median interaction worst long task max tbt frame gap p95 frame gap max jank count cls status
default / session-streaming-long 32 -> 24 (-8) 40 -> 40 (0) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.7 (0) 16.7 -> 16.8 (+0.1) 0 -> 0 (0) 0 -> 0 (0) pass

Astro-Han added 7 commits May 28, 2026 17:44
Replace the hand-drawn skeleton turns with `[data-component="user-message"]`
and `[data-component="assistant-message"]` DOM so the placeholder bubble
inherits real `user-message-text` geometry (inline-block, max-width 75%,
fit-content, brand bubble fill). User-row placeholder lines use `ch` widths
to let the real inline-block padding/radius drive the bubble shape; assistant
rows use `%` widths against the 100%-wide assistant container.

Adds a small `[data-slot="skeleton-line"]` style in `message-part.css` so the
new placeholder shares palette and dark-mode handling with the surrounding
message styles, and tightens the `assistant-message:has(skeleton-line)` gap
to 6px so single-paragraph skeleton lines don't read as separate parts.

Tests assert the real selectors and the body>text nesting that downstream
CSS relies on.
Pass `timelineMessages` so the skeleton turn count and user/assistant order
match the conversation that is about to render. Without this, every opening
state looked identical regardless of which session was being opened, which
defeats the point of mirroring the target layout.
Two readiness bypasses surfaced by code review on PR #969:

1. `handleGlobalDrop` returned early when `externalReady()` was false
   without calling `preventDefault()` first. Dropping a file during the
   opening state fell through to Electron's default handler, which
   navigates the window to the file URL and discards the in-flight draft.

2. `handlePaste` only checked readiness on the upstream `onPaste` handler
   when the clipboard surfaced a `file` item. Native screenshots arrive
   with no clipboard file item, so the paste fell through to the
   `readClipboardImage()` desktop fallback and silently attached the
   image (or fired an unsupported-image toast) while the composer's
   attachment controls were still disabled.

Cancel the native drop before bailing, and gate the
`readClipboardImage()` fallback with the same `externalReady` check.
Regression tests cover both paths via the existing
`createPromptAttachments` factory.
Initializing the mount signal to false forced an extra render on first
mount whenever showSessionOpeningState() already resolved to true. Seed
it with the current value so the overlay element exists in render 1.
The skeleton overlay does not render retry or new-session actions, so
the onRetryOpenSession / onOpenNewSession props on SessionMainView and
their providers in session.tsx were dead. Removed both ends along with
the now-unused decode64 and useShellSurface imports they pulled in.
The "!" shortcut that flips the prompt into shell mode skipped the
readiness check, so users could change modes while the session was
still opening. The button and command paths already routed through a
guarded setMode. Extract a shouldActivateShellModeFromBang helper that
considers action readiness alongside cursor position and current mode,
then use it from the keydown handler.
Both the native and browser file pickers open async dialogs that can
outlive the readiness window the entry guard checked. Add an isReady
gate inside pickAttachments and re-check actionReady at the browser
file input onChange so opening sessions don't accept stale picks.
@Astro-Han

Copy link
Copy Markdown
Owner Author

Addressed the review:

  • P2 (! shell-mode shortcut bypassing readiness) — fixed in 2bab52e948. Extracted shouldActivateShellModeFromBang in readiness.ts, gated on actionReady alongside cursor/mode, wired into keydown.ts. Four-case unit test in readiness.test.ts.
  • P3 (dead opening recovery callbacks) — fixed in 24bf8d104a. Removed onRetryOpenSession / onOpenNewSession from SessionMainView props and the providers in session.tsx, along with the now-unused decode64 and useShellSurface imports.
  • TBD chore: v1.1 polish — skills, panel fixes, README SEO #1 (file picker readiness re-check) — fixed in 45730f26d9. Added isReady to pickAttachments for a post-dialog re-check; gated the browser-native <input type=file> onChange on actionReady for the same race; new test in pick-attachments.test.ts.
  • TBD feat: split desktop panel and thread locale into skills #2 (composer during opening) — design decision: keep composer visible, gate external actions on actionReady. P2 and TBD chore: v1.1 polish — skills, panel fixes, README SEO #1 above implement that contract. Local typing/edits stay reachable because they don't mutate session state outside the prompt; mode switches, file pickers, drop, and paste-to-attach all go through actionReady / externalReady gates.

Pre-existing test failures on dev (matchKeybind in session-side-panel.test.tsx, isPromptEqual in path-b-integration.test.ts) are not introduced by this PR.

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/app/e2e/snap/session-opening-skeleton.snap.ts (1)

27-27: 💤 Low value

Consider whether the 180-second timeout is necessary.

The test timeout is set to 3 minutes, which is very generous for a snapshot test that navigates, waits for theme boot, mounts a fixture, and captures two screenshots. Even accounting for CI variability and cold starts, this seems excessive—most Playwright tests complete in seconds. If the test consistently finishes much faster, consider reducing this to a more typical value (e.g., 60 seconds) to catch performance regressions earlier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/e2e/snap/session-opening-skeleton.snap.ts` at line 27, The test
currently sets a very long 180_000ms timeout via test.setTimeout; reduce it to a
more typical value (e.g., 60_000) to fail faster on slow tests while still
allowing CI variability, or make it configurable if this specific snapshot
occasionally needs more time; update the test.setTimeout call accordingly
(replace 180_000 with 60_000 or a reference to a TIMEOUT constant) so the change
is clear in the session-opening-skeleton snapshot test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/app/e2e/snap/session-opening-skeleton.snap.ts`:
- Line 27: The test currently sets a very long 180_000ms timeout via
test.setTimeout; reduce it to a more typical value (e.g., 60_000) to fail faster
on slow tests while still allowing CI variability, or make it configurable if
this specific snapshot occasionally needs more time; update the test.setTimeout
call accordingly (replace 180_000 with 60_000 or a reference to a TIMEOUT
constant) so the change is clear in the session-opening-skeleton snapshot test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0cf928ae-5bd3-4011-a617-bf1eef332806

📥 Commits

Reviewing files that changed from the base of the PR and between 7419037 and b033307.

📒 Files selected for processing (18)
  • packages/app/e2e/snap/fixtures/session-opening-skeleton-fixture.tsx
  • packages/app/e2e/snap/session-opening-skeleton.snap.ts
  • packages/app/src/components/prompt-input.tsx
  • packages/app/src/components/prompt-input/attachments.test.ts
  • packages/app/src/components/prompt-input/attachments.ts
  • packages/app/src/components/prompt-input/keydown.ts
  • packages/app/src/components/prompt-input/pick-attachments.test.ts
  • packages/app/src/components/prompt-input/pick-attachments.ts
  • packages/app/src/components/prompt-input/readiness.test.ts
  • packages/app/src/components/prompt-input/readiness.ts
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/session-opening-skeleton.test.ts
  • packages/app/src/pages/session/session-opening-skeleton.tsx
  • packages/app/src/pages/session/use-session-timeline-data.test.ts
  • packages/app/src/pages/session/use-session-timeline-data.ts
  • packages/app/src/shell-frame-contract.test.ts
  • packages/ui/src/components/message-part.css
💤 Files with no reviewable changes (1)
  • packages/app/src/pages/session.tsx

Extract shouldExitShellModeOnBackspace mirroring the bang shortcut helper
so the shell-empty Backspace path honors the same readiness contract as
other mode toggles.
@Astro-Han Astro-Han merged commit 115676b into dev May 28, 2026
27 checks passed
Astro-Han added a commit that referenced this pull request May 28, 2026
Bump PawWork desktop release version to 2026.5.29.

Changes since v2026.5.28:
- feat(settings): move Connections to Integrations + global toast monitor (#975)
- feat(ui): display cache hit rate with one decimal place (#967)
- fix: stabilize session opening state (#969)
- fix(session): repair stale paginated question blockers (#962)
- fix(ui): refit read-file icon into 0-20 viewBox (#964)
- fix: allow running tools to expand
- fix: add run lifecycle diagnostics
- fix: harden Electron repair fallback
- refactor: remove legacy theme choices
- ci: stabilize e2e Playwright install and PR triage paths

Verification: diff scope matches prior release bump (#958) — only the version string in packages/desktop-electron/package.json and the workspace entry in bun.lock. All CI checks green (e2e-artifacts required a rerun due to Playwright browser install flake unrelated to this change).
Astro-Han added a commit that referenced this pull request May 29, 2026
Fixes #985.

Root cause:
- SessionContextUsage used Solid's callback-form non-keyed Show accessors for the active context label/status.
- PR #969 kept the composer mounted during opening; when switching sessions, that exposed a stale Show accessor read after context metrics disappeared.

Changes:
- Derive the context label with a memo instead of reading a Show callback accessor later.
- Render the tooltip through a small content component that accepts stable values.
- Add a browser-condition regression that renders SessionContextUsage and clears context metrics, so reverting to the callback-form Show pattern fails.

Verification:
- bun test --preload ./happydom.ts ./src/components/session/session-context-metrics.test.ts ./src/components/session-context-usage.test.ts
- bun run typecheck
- bun run snap session-context-cache
- GitHub PR checks passed except perf-probe-baseline, which was skipped because it is stuck in the Install Playwright browsers step (`bunx playwright install --with-deps chromium`). The e2e-artifacts workflow already avoids that Playwright browser download path and passed on this head, so this is treated as a CI workflow hang rather than a PR regression.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows bug Something isn't working P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Session switching can skip opening state and get stuck blank

1 participant