Skip to content

fix(app): stabilize session composer dock scrolling#354

Merged
Astro-Han merged 32 commits intodevfrom
codex/fix-i351-scroll-dock
May 1, 2026
Merged

fix(app): stabilize session composer dock scrolling#354
Astro-Han merged 32 commits intodevfrom
codex/fix-i351-scroll-dock

Conversation

@Astro-Han
Copy link
Copy Markdown
Owner

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

Summary

  • Fix session timeline bottom scrolling by centralizing composer dock height measurement and sticky-bottom behavior.
  • Tighten the latest-turn bottom padding so the last message tail sits closer to the Composer.
  • Split packages/app/src/pages/session.tsx into focused session controllers with unit coverage.

Why

session.tsx mixed route composition, scroll dock layout, history windowing, desktop context sync, followup drafts, revert flow, and review state. The scroll bug came from duplicated dock measurement and bottom-padding paths, which could leave the latest message underneath the Composer after the dock height changed.

Related Issue

Fixes #351

Human Review Status

Pending maintainer review.

Review Focus

  • use-session-scroll-dock.ts: sticky-bottom behavior, composer dock height sync, user-scroll preservation.
  • session.tsx: integration wiring after controller extraction.
  • use-session-followups.ts and use-session-review-state.ts: extracted state ownership and unchanged behavior.
  • session-composer-dock.spec.ts: regression coverage for the covered latest-turn case.

Risk Notes

  • Medium surface area because this splits a large route file into controllers.
  • No intended product behavior change outside the composer dock scroll fix and smaller latest-turn bottom gap.
  • One controller remains above the soft 200-line target because the extracted history window logic is still a single coherent unit.

How To Verify

  • bun test --preload ./happydom.ts src/pages/session/use-session-scroll-dock.test.ts src/pages/session/session-auto-scroll.test.ts src/pages/session/use-session-history-window.test.ts src/pages/session/use-session-desktop-context.test.ts src/pages/session/use-session-followups.test.ts src/pages/session/use-session-revert.test.ts src/pages/session/use-session-review-state.test.ts src/pages/session/review-change-mode.test.ts src/pages/session/session-messages.test.ts src/components/prompt-input/submit.test.ts
    • Result: 42 passed, 0 failed.
  • bun typecheck
    • Result: passed.
  • bun test:e2e:local -- --grep "composer dock keeps latest turn visible"
    • Result: 1 passed.
  • git diff --check
    • Result: passed with no whitespace errors.

Screenshots or Recordings

  • Not attached. The targeted E2E generates a local screenshot artifact for the latest-turn visibility assertion and the artifact was removed from the worktree after the run.

Checklist

Summary by CodeRabbit

  • Refactor

    • Session UI reorganized into a consolidated main view driven by reusable composables for timeline, review, scroll, and composer behavior.
  • New Features

    • Composer dock resizing preserves viewport position; improved timeline/history backfill, active-message navigation, followup queueing/auto-send, revert/restore workflows, inline file comments, VCS-backed review panel, and keyboard-focus/tab syncing.
  • Tests

    • Added E2E and unit tests covering scrolling, desktop sync, followups, history windowing, revert, review state, and related behaviors.
  • Style

    • Adjusted timeline bottom spacing.

@Astro-Han Astro-Han added P2 Medium priority app Application behavior and product flows ui Design system and user interface labels May 1, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fdf19d7a-6261-48c6-93d1-b13116ea45a9

📥 Commits

Reviewing files that changed from the base of the PR and between 32fa04e and 7114877.

📒 Files selected for processing (1)
  • packages/app/src/pages/session/message-timeline.tsx
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: e2e-artifacts
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-desktop
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (1)
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/message-timeline.tsx
🧠 Learnings (6)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/patch/index.ts:337-346
Timestamp: 2026-04-28T04:38:05.946Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/patch/index.ts`), the BOM-transition surfacing gap — where `Bom.split` strips BOM before building `unified_diff`/`new_content` but `Bom.join` later re-attaches BOM on disk write, meaning BOM changes are not reflected in the diff payload — is intentionally deferred. PR `#270` is an upstream-sync graft; fixing the issue here would mix refactor + bugfix intents and drift the diff from the upstream baseline needed for clean future grafts. A dedicated follow-up PR (or upstream report) will address this. Do NOT re-flag the missing BOM-change surfacing in `ApplyPatchFileUpdate`/`ApplyPatchFileChange` as a blocking issue in PR `#270` or in future sync PRs that carry the same upstream baseline.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:58.329Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/session/compaction.ts:169-191
Timestamp: 2026-04-28T05:36:22.128Z
Learning: In `packages/opencode/src/session/compaction.ts` (Astro-Han/pawwork), the internal helper `splitTurn` intentionally returns a raw `Effect.gen(...)` rather than being wrapped with `Effect.fnUntraced`. Converting `splitTurn` (and similar plain-Effect.gen internal helpers in this file, e.g. `turns`, `completedCompactions`, `buildPrompt`) to `Effect.fnUntraced` is a repo-wide convention sweep deferred to a dedicated follow-up PR. The same sweep covers `tool/tool.ts` and other helpers flagged in the same sync. Do NOT re-flag the absence of `Effect.fnUntraced` on `splitTurn` or other internal helpers in `compaction.ts` during upstream-sync PRs; flag it only in the dedicated convention-sweep PR.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/session/session-view-controller.test.ts` and related Solid + Bun test files), the Bun + Solid test environment does NOT reliably exercise `createMemo((current) => ...)` signal recomputation the way a browser runtime does. Adding signal-driven transition tests (e.g., using `createSignal` + `createRoot` to flip reactive inputs) is misleading in this environment because the reactive invalidation/recomputation path is not faithfully replicated. The correct strategy is: cover pure transition logic with plain unit tests (e.g., `nextSessionViewState`), and cover the browser reactive path with E2E tests (e.g., session-switch E2E spec). Do NOT re-flag the absence of signal-driven `createSessionViewController` tests in this environment as a gap.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/app/src/components/dialog-delete-session.tsx:22-31
Timestamp: 2026-04-30T10:26:58.545Z
Learning: In `packages/app/src/components/dialog-delete-session.tsx` (Astro-Han/pawwork, PR `#342`), `handleDelete` uses a `deleting` signal with `try/finally` but intentionally omits an explicit `catch` block. The caller (`deleteSession` in `packages/app/src/pages/layout.tsx` around line 1083) already swallows errors with a toast, so `onConfirm` does not reject in practice. The `try/finally` is sufficient to keep the Delete button usable if a future caller throws. Do NOT re-flag the missing explicit catch in this component.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/test/session/subagent-lifecycle-integration.test.ts:41-47
Timestamp: 2026-04-28T11:24:35.312Z
Learning: In `packages/opencode/test/session/subagent-lifecycle-integration.test.ts` (Astro-Han/pawwork, PR `#287`), the test suite intentionally uses a manual `run` helper (`Effect.runPromise(program.pipe(Effect.provide(Layer.mergeAll(SubagentRun.defaultLayer, Session.defaultLayer)), Effect.orDie))`) and raw `bun:test` test cases rather than the `testEffect(...)` harness. All 27 lifecycle tests pass with this pattern. Migration to `testEffect` + `it.live(...)` is deferred to a dedicated test-harness sweep PR to avoid fixture drift before merge. Do NOT re-flag the manual `Effect.runPromise` runner in this file as needing harness migration until that sweep PR lands.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:12.228Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/prompt.ts` and `packages/opencode/src/session/processor.ts`, PR `#264`), the loop-gate race condition between `buildLoopContext()` and `recordSyntheticBlock`/`recordSyntheticStop` is intentionally handled via idempotence guards (re-check sigKey presence / `hasStopped` inside the record helpers) rather than a full per-parent `Effect.Mutex`. Threading a `Map<MessageID, Mutex>` through the processor was considered too large a surface change for this edge case; the residual TOCTOU window only produces extra synthetic parts with no behavioral drift on the "turn ends" contract. A code comment documents the trade-off and points to a full-mutex follow-up if the race is observed in practice. Do NOT re-flag the absence of a per-parent mutex as a blocking issue in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 353
File: packages/app/e2e/session/session-composer-dock.spec.ts:53-70
Timestamp: 2026-05-01T02:06:33.069Z
Learning: In `packages/app/e2e/session/session-composer-dock.spec.ts` (Astro-Han/pawwork), local question-dataset fixtures (`defaultQuestions`, `multiQuestions`, etc.) intentionally use camelCase, not SCREAMING_SNAKE_CASE. The SCREAMING_SNAKE_CASE guideline for `packages/app/e2e/**/*.spec.ts` is not applied uniformly to local fixture objects in this file. Do NOT re-flag camelCase question-fixture names here as style violations.
📚 Learning: 2026-04-29T04:31:25.068Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 0
File: :0-0
Timestamp: 2026-04-29T04:31:25.068Z
Learning: In `packages/app/src/pages/layout/pawwork-sidebar.tsx` (Astro-Han/pawwork), the session-row pin button container must be sized `w-[14px] h-[14px]` to match the 14×14 leading slot declared in `sidebar-items.tsx`. The Icon glyph inside is auto-shrunk to 14×14 by the `[data-leading-slot]` CSS rule in `sidebar.css`, so all three dimensions (button wrapper, icon content, slot frame) align at 14×14. Do NOT suggest widening the pin button container beyond 14×14 in this context.

Applied to files:

  • packages/app/src/pages/session/message-timeline.tsx
📚 Learning: 2026-04-30T14:25:19.902Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.

Applied to files:

  • packages/app/src/pages/session/message-timeline.tsx
📚 Learning: 2026-04-30T10:26:59.483Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/app/src/components/dialog-select-model.tsx:168-170
Timestamp: 2026-04-30T10:26:59.483Z
Learning: In `packages/app/src/components/dialog-select-model.tsx` (Astro-Han/pawwork), the `Kobalte.Content` for `ModelSelectorPopover` intentionally uses inline `style={{ "border-radius": "14px", "box-shadow": "var(--shadow-floating)" }}` rather than the shared `data-component="popover-content"` class from `packages/ui/src/components/popover.css`. Reason: this is a custom dropdown anatomy (`w-72 h-80 flex flex-col`), not a Popover composition — adopting `popover-content` would silently import min/max-width constraints, nested slot styles (`popover-header`, `popover-body`, `popover-arrow`), and `popover-open`/`popover-close` keyframe animations driven by Kobalte's `data-expanded`/`data-closed` attributes. The one-line inline style expresses exactly the two tokens needed without the anatomy/animation contract. Do NOT suggest replacing it with the shared popover-content class.

Applied to files:

  • packages/app/src/pages/session/message-timeline.tsx
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/message-timeline.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/message-timeline.tsx
🔇 Additional comments (2)
packages/app/src/pages/session/message-timeline.tsx (2)

707-714: Good dock-height compensation hook-up on the timeline content wrapper.

Using padding-bottom: calc(var(--composer-dock-height, 0px) + 16px) here keeps latest-turn visibility tied to the measured composer height and matches the centralized scroll-dock ownership design.


718-718: Nice spacing adjustment for the final turn.

Dropping the list container to pb-8 is a sensible trim now that bottom-safe space is handled by the outer dock-compensated wrapper.


📝 Walkthrough

Walkthrough

Refactors the session page into a composition root and extracts multiple session controllers; centralizes composer-dock height ownership and bottom-scroll compensation; adds scroll-dock, timeline/history, active-message, followups, revert, review, desktop-context, comment-context, keyboard-focus controllers and tests (including an E2E verifying dock expansion behavior).

Changes

Cohort / File(s) Summary
Composer Dock Scroll
packages/app/src/pages/session/use-session-scroll-dock.ts, packages/app/src/pages/session/use-session-scroll-dock.test.ts, packages/app/e2e/session/session-composer-dock.spec.ts
New scroll-dock controller owns --composer-dock-height, computes scroll state (overflow/bottom/jump), decides stickiness after dock resize, applies bottom-scroll compensation, and includes unit and E2E tests (screenshot artifact).
Session Composition & UI Split
packages/app/src/pages/session.tsx, packages/app/src/pages/session/session-main-view.tsx, packages/app/src/pages/session/session-composer-region.tsx
Converted session.tsx into a composition root; extracted UI into SessionMainView and SessionPageComposerRegion; wiring now delegates behavior to composables.
Timeline / Active Message / History
packages/app/src/pages/session/message-timeline.tsx, packages/app/src/pages/session/use-session-active-message.ts, packages/app/src/pages/session/use-session-history-window.ts, packages/app/src/pages/session/use-session-history-window.test.ts, packages/app/src/pages/session/use-session-history-backfill.ts
Added active-message selection, history-window slicing/backfill, load-and-reveal logic, scroll-preserve behavior, and adjusted final-turn spacing with accompanying tests.
Scroll/Timeline Interaction Composition
packages/app/src/pages/session/use-session-timeline-interaction.ts, packages/app/src/pages/session/use-session-history-backfill.ts, packages/app/src/pages/session/use-session-active-message.ts
New composition that wires scroll-dock, active-message, history-window, hash scrolling, and navigation helpers into a single interaction surface.
Followups & Comment Context
packages/app/src/pages/session/use-session-followups.ts, packages/app/src/pages/session/use-session-followups.test.ts, packages/app/src/pages/session/use-session-comment-context.ts, packages/app/src/pages/session/use-session-comment-context.test.ts
Followup queue manager with persistence, preview, auto-send gating and mutation handling; comment-context controller syncing comments with prompt-context; added tests.
Revert / Restore
packages/app/src/pages/session/use-session-revert.ts, packages/app/src/pages/session/use-session-revert.test.ts
Revert helpers (rolledRevertItems, nextRestoreTarget) and createSessionRevert mutation wiring with snapshot/rollback semantics and tests.
Review / VCS Panel & View
packages/app/src/pages/session/use-session-review-state.ts, packages/app/src/pages/session/use-session-review-state.test.ts, packages/app/src/pages/session/use-session-review-panel.tsx, packages/app/src/pages/session/review-panel-scroll.ts, packages/app/src/pages/session/review-panel-view.tsx, packages/app/src/pages/session/use-session-route-tabs.ts
Review-state for artifact derivation and per-mode VCS diff fetching (dedupe/stale suppression), review panel factory/view, diff auto-scroll, and route-tabs normalization/handoff; includes tests.
Desktop Context & Refresh Effects
packages/app/src/pages/session/use-session-desktop-context.ts, packages/app/src/pages/session/use-session-desktop-context.test.ts, packages/app/src/pages/session/use-session-vcs-refresh.ts, packages/app/src/pages/session/use-session-refresh-effects.ts
Desktop-context sync with dedupe and retry/backoff; session/todo refresh orchestration using RAF/timeouts and unit tests.
Keyboard Focus & Utilities
packages/app/src/pages/session/use-session-keyboard-focus.ts, packages/app/src/pages/session/use-session-new-worktree.ts
Keyboard-focus handler with editable/terminal heuristics and shadow-DOM-aware deep active element detection; new-worktree selection helper.
Timeline Data & Other Utilities
packages/app/src/pages/session/use-session-timeline-data.ts, packages/app/src/pages/session/use-session-vcs-refresh.ts, packages/app/src/pages/session/use-session-review-panel.tsx
Route/session timeline derivation (visible messages, revert ID, sync triggers), VCS refresh wiring and review-panel orchestration.
Tests & Minor
packages/app/src/components/prompt-input/submit.test.ts, packages/app/src/shell-frame-contract.test.ts, many packages/app/src/pages/session/*/*.test.ts
Added/updated unit tests across session modules, extended a test mock (encode.checksum), updated shell-frame expectations, and added multiple new tests including Bun suites and E2E.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ScrollDock as ScrollDock
    participant DOM
    participant RAF as RAF/Timer

    User->>ScrollDock: prompt dock height changes
    ScrollDock->>ScrollDock: measure previousDockHeight, compute nextDockHeight
    ScrollDock->>ScrollDock: shouldStickToBottomAfterDockResize(userScrolled?)
    alt user at bottom
        ScrollDock->>DOM: setCssHeight(nextDockHeight)
        ScrollDock->>RAF: forceScrollToBottom()
        RAF->>DOM: set scrollTop = scrollHeight
    else user scrolled up
        ScrollDock->>DOM: setCssHeight(nextDockHeight)
        ScrollDock->>DOM: scheduleScrollState() (no forced scroll)
    end
    DOM->>ScrollDock: report clientHeight, scrollHeight, scrollTop
    ScrollDock->>ScrollDock: calculateSessionScrollState(...) -> update store (overflow/bottom/jump)
Loading
sequenceDiagram
    participant Page as Session Page
    participant Followups as Followups Hook
    participant Storage as Workspace/Sync
    participant Client as SDK Client

    Page->>Followups: createSessionFollowups(session context)
    Followups->>Storage: persist queued followup
    Followups->>Followups: evaluate shouldAutoSendFollowup(...)
    alt auto-send allowed
        Followups->>Client: sendFollowupDraft()
        Client->>Server: POST /followup
        Server-->>Client: response
        alt success
            Client-->>Followups: ack -> remove persisted item
        else failure
            Client-->>Followups: error -> mark failed, optionally retry
        end
    else gated
        Followups->>Followups: defer until conditions clear
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

bug

Poem

🐇
I nibbled gaps where messages hide,
Measured docks and eased the tide.
Now padding moves and scrolls agree,
No lost last line for you or me.
Hooray — the timeline hops with glee.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.78% 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(app): stabilize session composer dock scrolling' directly describes the main bug fix, which aligns with the PR's primary objective.
Description check ✅ Passed The PR description covers all key template sections: Summary, Why, Related Issue, Review Focus, Risk Notes, Verification steps, and Checklist. It is well-structured and mostly complete.
Linked Issues check ✅ Passed The PR fully addresses the core coding objectives from #351: centralizing dock measurement/compensation ownership, extracting scroll-dock controller with unit tests, fixing composer dock scrolling, and reducing latest-turn spacing.
Out of Scope Changes check ✅ Passed All changes are in-scope: controller extraction, scroll-dock ownership fix, latest-turn spacing adjustment, new unit tests, and session.tsx refactoring per #351. SessionSidePanel complexity and right-panel redesign remain out of scope.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-i351-scroll-dock

Review rate limit: 8/10 reviews remaining, refill in 7 minutes and 17 seconds.

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

Copy link
Copy Markdown

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

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 refactors the session page by extracting complex logic into modular hooks, including management for desktop context, followups, history windows, revert actions, review states, and scroll dock behavior. It also introduces a new E2E test for composer dock scrolling. The review feedback highlights several improvement opportunities: ensuring SolidJS ref callbacks handle null/undefined values to prevent runtime errors, clearing global CSS variables during component cleanup to avoid side effects, and replacing 'any' types with concrete TypeScript interfaces in the new hooks to restore type safety.

Comment thread packages/app/src/pages/session/use-session-scroll-dock.ts Outdated
Comment thread packages/app/src/pages/session/use-session-scroll-dock.ts Outdated
Comment thread packages/app/src/pages/session/use-session-scroll-dock.ts
Comment thread packages/app/src/pages/session/use-session-review-state.ts
Comment thread packages/app/src/pages/session/use-session-revert.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app/e2e/session/session-composer-dock.spec.ts`:
- Around line 969-988: The test reads `afterUserScroll` immediately after
`dock.open(...)`, which can race with dock resize/scroll; change the direct
`page.evaluate` call to poll until the viewport metrics settle using
Playwright's `expect.poll` (or `expect(...).poll`) to repeatedly evaluate the
same function that queries '[data-component="scroll-viewport"]' and returns `{
scrollTop, distanceFromBottom }`, and assert/poll for `scrollTop` to be <
`before` and `distanceFromBottom` to be >= `distanceBeforeExpansion - 40` (or
poll `distanceFromBottom` first then assert `scrollTop`) so the test waits for
the dock's resize/scroll compensation before making final assertions on
`afterUserScroll`.

In `@packages/app/src/pages/session/use-session-revert.test.ts`:
- Around line 10-18: The test and implementation rely on lexicographic
comparison of message IDs causing incorrect slicing; update the
rolledRevertItems implementation to find the index of the message whose id
equals revertMessageID and slice the messages array by index (i.e., include
items after that index) instead of using string comparison (remove any `id >=
revertMessageID` style logic in rolledRevertItems), and update the
use-session-revert.test.ts case to use non-lexical IDs (e.g., "x1","a2","b3" or
random GUID-like strings) to assert index-based behavior matches expected order.

In `@packages/app/src/pages/session/use-session-review-state.ts`:
- Around line 45-47: The state uses createStore for a single independent field
"changes"; replace the createStore/createStore-set pattern with a
createSignal<ReviewChangeMode>("turn") to simplify state: remove createStore and
setStore, introduce a signal (e.g., const [changes, setChanges] =
createSignal<ReviewChangeMode>("turn")) and update all usages of store.changes
and setStore to use changes() and setChanges(...) respectively; keep the
ReviewChangeMode type and behavior unchanged.
- Around line 179-191: The two createEffect blocks (watching
input.sessionID()/input.turnDiffs() and input.sync.data.session_diff[id]) can
both call refetchArtifactHistory() and cause duplicate fetches; consolidate them
into a single createEffect that reads input.sessionID(), input.turnDiffs(), and
input.sync.data.session_diff[id] together and only calls
refetchArtifactHistory() once when the combined guard conditions pass, or add a
short debounce/one-shot guard (e.g., a local lastRefetchSessionID or a timeout)
inside that unified effect to ensure back-to-back triggers for the same session
do not call refetchArtifactHistory() twice. Ensure you reference the existing
symbols createEffect, input.sessionID(), input.turnDiffs(),
input.sync.data.session_diff and refetchArtifactHistory when making the change.

In `@packages/app/src/pages/session/use-session-scroll-dock.test.ts`:
- Around line 109-137: The test "syncs composer height through one path and
scrolls once when sticky" modifies the global document.documentElement.style by
setting the "--composer-dock-height" CSS property, but does not clean up after
itself. Add a cleanup step after the test assertions to remove the
"--composer-dock-height" property using removeProperty method on
document.documentElement.style to prevent this mutation from affecting other
tests. Consider using an afterEach hook if this pattern is repeated across
multiple tests, or add cleanup directly at the end of this test function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8fe63b45-8370-43a1-992a-755e5a2a0ea0

📥 Commits

Reviewing files that changed from the base of the PR and between 4d6e7e5 and 765de25.

📒 Files selected for processing (16)
  • packages/app/e2e/session/session-composer-dock.spec.ts
  • packages/app/src/components/prompt-input/submit.test.ts
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/message-timeline.tsx
  • packages/app/src/pages/session/use-session-desktop-context.test.ts
  • packages/app/src/pages/session/use-session-desktop-context.ts
  • packages/app/src/pages/session/use-session-followups.test.ts
  • packages/app/src/pages/session/use-session-followups.ts
  • packages/app/src/pages/session/use-session-history-window.test.ts
  • packages/app/src/pages/session/use-session-history-window.ts
  • packages/app/src/pages/session/use-session-revert.test.ts
  • packages/app/src/pages/session/use-session-revert.ts
  • packages/app/src/pages/session/use-session-review-state.test.ts
  • packages/app/src/pages/session/use-session-review-state.ts
  • packages/app/src/pages/session/use-session-scroll-dock.test.ts
  • packages/app/src/pages/session/use-session-scroll-dock.ts

Comment thread packages/app/e2e/session/session-composer-dock.spec.ts Outdated
Comment thread packages/app/src/pages/session/use-session-revert.test.ts
Comment thread packages/app/src/pages/session/use-session-review-state.ts Outdated
Comment thread packages/app/src/pages/session/use-session-review-state.ts
Comment thread packages/app/src/pages/session/use-session-scroll-dock.test.ts
@Astro-Han
Copy link
Copy Markdown
Owner Author

Astro-Han commented May 1, 2026

Updated with the follow-up session page split pass.

What changed:

  • session.tsx is now 573 lines, down from 1386 after the first split pass.
  • Extracted comment context, review panel wiring, keyboard focus, refresh effects, active message state, composer region wrapper, main view shell, route tab handoff, VCS refresh, and timeline data.
  • Kept right-panel redesign out of scope; this is still behavior-preserving extraction plus the original composer dock scroll fix.

Verification rerun locally:

  • bun typecheck: passed.
  • Focused unit suite excluding side-panel mock-conflict file: 47 passed, 0 failed.
  • bun test --preload ./happydom.ts src/pages/session/session-side-panel.test.tsx: 9 passed, 0 failed.
  • bun test:e2e:local -- --grep "composer dock keeps latest turn visible": 1 passed.
  • git diff --check: passed.

Note: session-side-panel.test.tsx was run separately because its Bun module mocks conflict with other session tests when batched in the same process.

@Astro-Han
Copy link
Copy Markdown
Owner Author

Follow-up split pushed.

Additional changes:

  • Extracted use-session-history-backfill.ts for the history auto-fill frame/effect logic.
  • Extracted use-session-new-worktree.ts for new-session worktree selection/reset.
  • session.tsx is now 526 lines.

Verification rerun locally:

  • bun typecheck: passed.
  • Focused session unit suite: 47 passed, 0 failed.
  • bun test --preload ./happydom.ts src/pages/session/session-side-panel.test.tsx: 9 passed, 0 failed.
  • bun test:e2e:local -- --grep "composer dock keeps latest turn visible": 1 passed.
  • git diff --check: passed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app/src/pages/session.tsx`:
- Around line 253-275: The code uses a forward-reference where
createSessionScrollDock is passed clearActiveMessage: () =>
activeMessage?.clearActiveMessage() before activeMessage is assigned, which
relies on the callback being called later; add a concise inline comment next to
the scrollDock creation (near scrollDock, createSessionScrollDock, and the
clearActiveMessage argument) explaining that activeMessage is intentionally
assigned after scrollDock via createSessionActiveMessage and that the closure is
safe because the callback is only invoked after activeMessage is initialized;
reference activeMessage, createSessionScrollDock, and createSessionActiveMessage
in the comment for clarity.

In `@packages/app/src/pages/session/session-main-view.tsx`:
- Around line 86-119: The Switch branch currently gates only on
props.activeSessionID so it can win before props.timelineSessionID is set,
leaving the nested Show empty and preventing NewSessionView from rendering;
change the condition that selects the MessageTimeline branch so it requires both
props.activeSessionID and props.timelineSessionID (e.g. make the Match predicate
check both) and/or move the timeline branch to explicitly check
props.timelineSessionID before rendering MessageTimeline (referencing Match,
Show, MessageTimeline, props.activeSessionID, props.timelineSessionID, and
NewSessionView) so the timeline branch only wins when both IDs are present.

In `@packages/app/src/pages/session/use-session-comment-context.ts`:
- Around line 59-62: The update payload omits intentionally empty-string
previews because the current truthy check on comment.preview skips "" — change
the guard to check for undefined instead of falsiness so updateComment receives
preview when it's an empty string; specifically update the call to
input.promptContext.updateComment (the object merged from comment.comment and
preview) to include preview whenever comment.preview !== undefined (or use
hasOwnProperty) rather than using a truthy check.

In `@packages/app/src/pages/session/use-session-review-panel.tsx`:
- Around line 219-229: The effect on input.activeFileTab that unconditionally
calls showAllFiles() when fileTreeTab() === "changes" is too broad and should be
removed or narrowed; instead, rely on createOpenReviewFile() to trigger
showAllFiles() for the explicit open-from-review path. Locate the
createEffect/on block that references input.activeFileTab, fileTreeTab(), and
showAllFiles() and delete it (or replace its logic with a more specific guard
tied to the review-open flow), ensuring that only createOpenReviewFile() calls
showAllFiles() for review opens and normal tab switches do not flip the explorer
to "all".

In `@packages/app/src/pages/session/use-session-route-tabs.ts`:
- Around line 7-8: The types for layout and tabs are currently declared as loose
any; replace them with explicit interfaces and use those in useSessionRouteTabs:
define an interface (e.g., SessionLayout) for layout that includes the handoff
object with methods like tabs: () => ReturnType and clearTabs: () => void, and a
tabs accessor signature (e.g., TabsAccessor) that maps keys to a specific Tab
type or structure; then change the declarations from "layout: any" and "tabs:
any" to "layout: SessionLayout" and "tabs: TabsAccessor" (or appropriate names)
and update any call sites in useSessionRouteTabs to match the narrowed
method/return types so the compiler enforces the expected structure.

In `@packages/app/src/pages/session/use-session-timeline-data.ts`:
- Around line 16-17: The loose any types for the variables sync and local reduce
type-safety; replace them with narrow structural interfaces that reflect the
properties actually used (for example declare an interface like SyncRecord {
session: Session; data: Record<string, unknown> } and a LocalRecord { /* fields
used from local */ } or reuse existing Session/Timeline types), update the
declarations of sync and local to those interfaces, and adjust any consumers of
useSessionTimelineData, getSyncData, or related functions to accept/return the
new types so TypeScript catches mismatches at compile time.

In `@packages/app/src/pages/session/use-session-vcs-refresh.ts`:
- Around line 4-15: The input types to useSessionVcsRefresh are too loose:
tighten branch and defaultBranch from () => unknown to () => string (or a
specific BranchName type) and replace load(mode: any, force: true) with
load(mode: Mode | undefined, force: true) where Mode is a union of the allowed
string modes (e.g., "view" | "edit" | "review") or string if more generic; also
change mode(): string | undefined to mode(): Mode | undefined so all callers and
the useSessionVcsRefresh signature share the same explicit Mode type. Update the
function signature and any call sites inside useSessionVcsRefresh that use
branch, defaultBranch, mode, or load to use these narrowed types and adjust any
runtime guards if necessary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9b2b9ebe-c42b-4928-94dc-0b17c6955711

📥 Commits

Reviewing files that changed from the base of the PR and between 765de25 and dd0b7f8.

📒 Files selected for processing (12)
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/session-composer-region.tsx
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-active-message.ts
  • packages/app/src/pages/session/use-session-comment-context.test.ts
  • packages/app/src/pages/session/use-session-comment-context.ts
  • packages/app/src/pages/session/use-session-keyboard-focus.ts
  • packages/app/src/pages/session/use-session-refresh-effects.ts
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/use-session-route-tabs.ts
  • packages/app/src/pages/session/use-session-timeline-data.ts
  • packages/app/src/pages/session/use-session-vcs-refresh.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-desktop
  • GitHub Check: unit-opencode
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: analyze-js-ts
  • GitHub Check: e2e-artifacts
🧰 Additional context used
📓 Path-based instructions (1)
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/use-session-timeline-data.ts
  • packages/app/src/pages/session/session-composer-region.tsx
  • packages/app/src/pages/session/use-session-keyboard-focus.ts
  • packages/app/src/pages/session/use-session-comment-context.test.ts
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/use-session-comment-context.ts
  • packages/app/src/pages/session/use-session-vcs-refresh.ts
  • packages/app/src/pages/session/use-session-route-tabs.ts
  • packages/app/src/pages/session/use-session-refresh-effects.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-active-message.ts
  • packages/app/src/pages/session.tsx
🧠 Learnings (35)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:58.329Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/patch/index.ts:337-346
Timestamp: 2026-04-28T04:38:05.946Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/patch/index.ts`), the BOM-transition surfacing gap — where `Bom.split` strips BOM before building `unified_diff`/`new_content` but `Bom.join` later re-attaches BOM on disk write, meaning BOM changes are not reflected in the diff payload — is intentionally deferred. PR `#270` is an upstream-sync graft; fixing the issue here would mix refactor + bugfix intents and drift the diff from the upstream baseline needed for clean future grafts. A dedicated follow-up PR (or upstream report) will address this. Do NOT re-flag the missing BOM-change surfacing in `ApplyPatchFileUpdate`/`ApplyPatchFileChange` as a blocking issue in PR `#270` or in future sync PRs that carry the same upstream baseline.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.
📚 Learning: 2026-04-29T13:27:28.494Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/session/session-view-controller.test.ts` and related Solid + Bun test files), the Bun + Solid test environment does NOT reliably exercise `createMemo((current) => ...)` signal recomputation the way a browser runtime does. Adding signal-driven transition tests (e.g., using `createSignal` + `createRoot` to flip reactive inputs) is misleading in this environment because the reactive invalidation/recomputation path is not faithfully replicated. The correct strategy is: cover pure transition logic with plain unit tests (e.g., `nextSessionViewState`), and cover the browser reactive path with E2E tests (e.g., session-switch E2E spec). Do NOT re-flag the absence of signal-driven `createSessionViewController` tests in this environment as a gap.

Applied to files:

  • packages/app/src/pages/session/use-session-timeline-data.ts
  • packages/app/src/pages/session/session-composer-region.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/use-session-vcs-refresh.ts
  • packages/app/src/pages/session/use-session-route-tabs.ts
  • packages/app/src/pages/session/use-session-refresh-effects.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-active-message.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T18:53:46.234Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 308
File: packages/opencode/test/server/global-session-list.test.ts:224-259
Timestamp: 2026-04-28T18:53:46.234Z
Learning: In `packages/opencode/test/server/global-session-list.test.ts` (Astro-Han/pawwork), all `it.live(...)` test bodies intentionally use `Effect.promise(async () => { ... })` rather than `Effect.gen(function* () { ... })`. This is the established local convention for the route/pagination tests in this file (e.g., "supports cursor pagination", "session route orders by creation time", "session routes omit undefined optional fields"). Do NOT flag individual `it.live` cases in this file as needing conversion to `Effect.gen`; any harness-style migration would need to cover the entire file.

Applied to files:

  • packages/app/src/pages/session/use-session-timeline-data.ts
  • packages/app/src/pages/session/use-session-comment-context.test.ts
  • packages/app/src/pages/session/use-session-route-tabs.ts
  • packages/app/src/pages/session/use-session-refresh-effects.ts
  • packages/app/src/pages/session/use-session-active-message.ts
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/use-session-timeline-data.ts
  • packages/app/src/pages/session/session-composer-region.tsx
  • packages/app/src/pages/session/use-session-keyboard-focus.ts
  • packages/app/src/pages/session/use-session-comment-context.test.ts
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/use-session-comment-context.ts
  • packages/app/src/pages/session/use-session-vcs-refresh.ts
  • packages/app/src/pages/session/use-session-route-tabs.ts
  • packages/app/src/pages/session/use-session-refresh-effects.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-active-message.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/use-session-timeline-data.ts
  • packages/app/src/pages/session/session-composer-region.tsx
  • packages/app/src/pages/session/use-session-keyboard-focus.ts
  • packages/app/src/pages/session/use-session-comment-context.test.ts
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/use-session-comment-context.ts
  • packages/app/src/pages/session/use-session-vcs-refresh.ts
  • packages/app/src/pages/session/use-session-route-tabs.ts
  • packages/app/src/pages/session/use-session-refresh-effects.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-active-message.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-24T05:39:58.329Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:58.329Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/app/src/pages/session/session-composer-region.tsx
  • packages/app/src/pages/session/use-session-keyboard-focus.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-30T14:25:19.902Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.

Applied to files:

  • packages/app/src/pages/session/session-composer-region.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/app/src/pages/session/use-session-keyboard-focus.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use `modKey` from utils for cross-platform keyboard shortcuts (Meta on Mac, Control on Linux/Windows)

Applied to files:

  • packages/app/src/pages/session/use-session-keyboard-focus.ts
📚 Learning: 2026-04-24T03:51:56.211Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:56.211Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/app/src/pages/session/use-session-keyboard-focus.ts
  • packages/app/src/pages/session/use-session-comment-context.test.ts
📚 Learning: 2026-04-26T16:34:57.130Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:57.130Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/app/src/pages/session/use-session-keyboard-focus.ts
  • packages/app/src/pages/session/use-session-active-message.ts
📚 Learning: 2026-04-29T13:27:28.494Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In pawwork’s Bun+Solid test environment (under packages/app/src/pages/session/**.test.ts), do not expect `createMemo((current) => ...)` to be recomputed/invalidated the same way it would in a real browser runtime. Avoid adding signal-driven transition tests that rely on Solid reactive invalidation paths in this environment (e.g., flipping inputs via `createSignal`/`createRoot` just to observe `createMemo` behavior), since it can be misleading. Instead, review/author tests as follows: (1) cover pure transition/state-update logic with plain unit tests (e.g., `nextSessionViewState`), and (2) cover the actual browser reactive behavior with E2E tests (e.g., the session-switch E2E spec). When reviewing this Bun+Solid test suite, do not treat the absence of signal-driven `createSessionViewController` tests as a missing-coverage gap.

Applied to files:

  • packages/app/src/pages/session/use-session-comment-context.test.ts
📚 Learning: 2026-04-28T11:24:35.312Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/test/session/subagent-lifecycle-integration.test.ts:41-47
Timestamp: 2026-04-28T11:24:35.312Z
Learning: In `packages/opencode/test/session/subagent-lifecycle-integration.test.ts` (Astro-Han/pawwork, PR `#287`), the test suite intentionally uses a manual `run` helper (`Effect.runPromise(program.pipe(Effect.provide(Layer.mergeAll(SubagentRun.defaultLayer, Session.defaultLayer)), Effect.orDie))`) and raw `bun:test` test cases rather than the `testEffect(...)` harness. All 27 lifecycle tests pass with this pattern. Migration to `testEffect` + `it.live(...)` is deferred to a dedicated test-harness sweep PR to avoid fixture drift before merge. Do NOT re-flag the manual `Effect.runPromise` runner in this file as needing harness migration until that sweep PR lands.

Applied to files:

  • packages/app/src/pages/session/use-session-comment-context.test.ts
  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-29T13:27:25.687Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/e2e/session/session-scroll-position.spec.ts:171-230
Timestamp: 2026-04-29T13:27:25.687Z
Learning: In `packages/app/e2e/session/session-scroll-position.spec.ts`, the `describe()` formatter is intentionally duplicated in `installPageErrorProbe` (inside `page.addInitScript`, runs in browser context) and `collectPageErrors` (runs in Node.js/Playwright context). These are separate execution contexts — `addInitScript` serializes its closure to a string for injection — so extracting a shared helper would add cross-context indirection. Do NOT flag this duplication as a maintenance issue.

Applied to files:

  • packages/app/src/pages/session/use-session-comment-context.test.ts
  • packages/app/src/pages/session/use-session-active-message.ts
📚 Learning: 2026-04-29T10:34:22.399Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 327
File: packages/opencode/test/pty/pty-session.test.ts:124-136
Timestamp: 2026-04-29T10:34:22.399Z
Learning: In `packages/opencode/test/pty/pty-session.test.ts` (Astro-Han/pawwork, PR `#327`), the `bun-pty` backend re-merges the parent process environment internally after `withoutInternalServerAuthEnv` strips auth values. This means `OPENCODE_SERVER_USERNAME` and `OPENCODE_SERVER_PASSWORD` will be **present-but-empty** (not fully unset) inside the PTY session. Using a `-unset` shell default expansion (e.g. `${OPENCODE_SERVER_USERNAME-unset}`) would never trigger, so asserting `username=unset` / `password=unset` would always fail. The correct assertion strategy is to check `expect(text).not.toContain("secret")` and `expect(text).not.toContain("PawWork")`. Do NOT suggest converting to `-unset` expansion or asserting `username=unset` / `password=unset` for PTY auth-env tests in this file.

Applied to files:

  • packages/app/src/pages/session/use-session-comment-context.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/app/src/pages/session/use-session-comment-context.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/app/src/pages/session/use-session-comment-context.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/app/src/pages/session/use-session-comment-context.test.ts
📚 Learning: 2026-04-28T04:56:21.338Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/test/tool/tool-define.test.ts:2-9
Timestamp: 2026-04-28T04:56:21.338Z
Learning: In `packages/opencode/test/tool/tool-define.test.ts` (Astro-Han/pawwork, PR `#270`), the test suite intentionally uses a `ManagedRuntime.make(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer))` wrapper and raw `bun:test` `test(...)` cases rather than the `testEffect(...)` harness. This code was adopted wholesale from upstream via the graft strategy (per `project_upstream_strategy.md`). Migrating to `testEffect` is deferred to a follow-up PR so as not to mix refactor + harness-migration intents and to avoid drifting the diff from the upstream baseline needed for clean future grafts. Do NOT re-flag the `ManagedRuntime.make` usage in this file as needing harness migration until that follow-up lands.

Applied to files:

  • packages/app/src/pages/session/use-session-comment-context.test.ts
📚 Learning: 2026-04-30T10:41:33.826Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/desktop-electron/src/main/ipc.ts:342-344
Timestamp: 2026-04-30T10:41:33.826Z
Learning: In Astro-Han/pawwork, the `export-session` IPC handler in `packages/desktop-electron/src/main/ipc.ts` accepts an optional localized `title` argument for `dialog.showSaveDialog`, keeping `"Export session"` as an English fallback. The localized title is plumbed from the renderer through `platform.exportSession` → `window.api.exportSession` → preload (`preload/types.ts`, `preload/index.ts`, `renderer/index.tsx`) → main, mirroring the same shape as the `save-file-picker` handler. Both renderer callsites (`layout.tsx` and `message-timeline.tsx`) pass `language.t("session.export.action.export")`. Do NOT flag the hard-coded fallback as unlocalized — the renderer is responsible for supplying the localized title.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/use-session-comment-context.ts
  • packages/app/src/pages/session/session-main-view.tsx
📚 Learning: 2026-04-28T04:38:11.727Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/tool/edit.ts:35-45
Timestamp: 2026-04-28T04:38:11.727Z
Learning: In `packages/opencode/src/tool/edit.ts` (Astro-Han/pawwork, PR `#270`), the module-scoped `Map<string, Semaphore.Semaphore>` (`locks`) and `lock(filePath)` helper are intentionally left as a global, never-cleaned registry. PR `#270` is a pure upstream-sync graft and mixing in a bugfix would drift the diff from the upstream baseline. The fix (moving per-file semaphores into `InstanceState`/`ScopedCache` for per-instance lifecycle management) is tracked as a follow-up to land in its own PR or be reported upstream. Do NOT re-flag the absence of InstanceState wiring for the lock map in this file until the follow-up PR is opened.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `InstanceState` from `src/effect/instance-state.ts` for per-directory or per-project state that needs per-instance cleanup; do work directly in the `InstanceState.make` closure where `ScopedCache` handles run-once semantics

Applied to files:

  • packages/app/src/pages/session/use-session-vcs-refresh.ts
  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : When validating routing, assert against canonical or resolved workspace slugs using shared helpers from `../actions` to account for Windows canonicalization

Applied to files:

  • packages/app/src/pages/session/use-session-route-tabs.ts
📚 Learning: 2026-04-28T05:36:22.128Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/session/compaction.ts:169-191
Timestamp: 2026-04-28T05:36:22.128Z
Learning: In `packages/opencode/src/session/compaction.ts` (Astro-Han/pawwork), the internal helper `splitTurn` intentionally returns a raw `Effect.gen(...)` rather than being wrapped with `Effect.fnUntraced`. Converting `splitTurn` (and similar plain-Effect.gen internal helpers in this file, e.g. `turns`, `completedCompactions`, `buildPrompt`) to `Effect.fnUntraced` is a repo-wide convention sweep deferred to a dedicated follow-up PR. The same sweep covers `tool/tool.ts` and other helpers flagged in the same sync. Do NOT re-flag the absence of `Effect.fnUntraced` on `splitTurn` or other internal helpers in `compaction.ts` during upstream-sync PRs; flag it only in the dedicated convention-sweep PR.

Applied to files:

  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-25T12:52:49.735Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:49.735Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.addFinalizer` or `Effect.acquireRelease` inside the `InstanceState.make` closure for cleanup (subscriptions, process teardown, etc.)

Applied to files:

  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-25T12:52:37.850Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:24-31
Timestamp: 2026-04-25T12:52:37.850Z
Learning: In `packages/opencode/src/session/export.ts`, the `hashFile` async helper intentionally uses raw `node:fs/promises` (`fs.readFile`) rather than `FileSystem.FileSystem`. It is a small, self-contained helper called inside `Effect.promise(...)` with broad failure tolerance (try/catch returning `undefined`). Using `FileSystem.FileSystem` here would add unnecessary layers without payoff. The `FileSystem.FileSystem` preference applies to Effect-service-style operations, not thin async boundary helpers like `hashFile`.

Applied to files:

  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually

Applied to files:

  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.gen(function* () { ... })` for Effect composition

Applied to files:

  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.forkScoped` inside the `InstanceState.make` closure for background stream consumers — the fiber is interrupted when the instance is disposed

Applied to files:

  • packages/app/src/pages/session/use-session-refresh-effects.ts
📚 Learning: 2026-04-27T11:18:47.298Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/websearch.test.ts:21-78
Timestamp: 2026-04-27T11:18:47.298Z
Learning: In `packages/opencode/test/tool/websearch.test.ts`, the tests intentionally use manual `Effect.runPromise` with explicit `Effect.provide(...)` chains (including `Layer.succeed(Auth.Service, ...)`, `Layer.succeed(HttpClient.HttpClient, http)`, `WebSearchAuth.layer`, `Truncate.defaultLayer`, and `Agent.defaultLayer`) rather than the `testEffect(...)` harness. This is by design: the fake Auth and HTTP recovery-metadata layers must be explicitly injected and kept visible/scoped at the test site. Do NOT suggest migrating these tests to `testEffect` or removing the manual layer provides.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-25T12:52:35.631Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:35.631Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T13:10:01.345Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/src/tool/agent.ts:89-102
Timestamp: 2026-04-28T13:10:01.345Z
Learning: In `packages/opencode/src/tool/agent.ts` (Astro-Han/pawwork, PR `#287`), `makeReadLastCompletedAssistantText` intentionally scans the full child session message history (no `limit`) to find the latest completed assistant text part for `partial_result` on cancellation. Cancellation is a rare path so the extra read cost is acceptable; tool-heavy children can push the last stable assistant text far back. Do NOT suggest re-adding a fixed small limit (e.g., `limit: 5`) to this function.

Applied to files:

  • packages/app/src/pages/session.tsx
🔇 Additional comments (17)
packages/app/src/pages/session/use-session-comment-context.test.ts (1)

4-84: Coverage for add/update/remove synchronization looks solid

The tests validate the core dual-write behavior (comments + promptContext) and preview derivation path in a focused, readable way.

packages/app/src/pages/session/use-session-timeline-data.ts (2)

101-122: LGTM!

The effects are well-structured: the first syncs the local model when lastUserMessage changes (with proper on tracking), and the second handles session reset on route changes with appropriate deferral and identity checks.


70-78: No action needed — message IDs are lexicographically sortable.

MessageIDs are generated using a timestamp-prefixed format (msg_ + 12-character hex encoding of timestamp+counter + random suffix). The hex portion encodes a monotonically increasing value, ensuring that string comparison (m.id < revert) correctly orders messages by creation time. The filter logic works as intended.

packages/app/src/pages/session/session-composer-region.tsx (1)

7-26: LGTM!

Clean typed wrapper that constrains the SessionComposerRegion prop interface for the session page context. The prop types are well-defined with appropriate optionality.

packages/app/src/pages/session/use-session-route-tabs.ts (1)

34-68: LGTM!

The tab handoff effect is well-guarded: it validates freshness (60s TTL), ensures identity match, avoids overwriting existing tabs, and properly cleans up both handoff and workspace state after transfer.

packages/app/src/pages/session/use-session-keyboard-focus.ts (2)

14-66: LGTM!

Well-structured keyboard focus handler with proper considerations:

  • Shadow DOM traversal for active element detection
  • Multiple protection layers (data-prevent-autofocus, editable targets, dialog state)
  • Clean separation of scroll gesture keys vs. printable character focus
  • Terminal focus prioritization when opened

68-70: LGTM!

Using makeEventListener from @solid-primitives/event-listener inside onMount ensures automatic cleanup when the component unmounts.

packages/app/src/pages/session/use-session-refresh-effects.ts (2)

20-51: LGTM!

The session refresh effect properly handles:

  • Cancellation of pending work on dependency change
  • Staleness detection via prefetch TTL
  • Deferred forced sync using RAF + setTimeout(0) to batch with frame timing
  • Identity check before executing stale refresh

53-88: LGTM!

Todo refresh effect mirrors the session refresh pattern with appropriate gating on session status and blocked state. The onCleanup properly cancels all four scheduled operations.

packages/app/src/pages/session/use-session-active-message.ts (3)

13-20: LGTM!

The store groups related active-message state (messageId, pendingMessage, scrollGesture). The mutable scrollMark and messageMark variables appropriately track gesture timing without triggering reactivity.


27-56: LGTM!

The geometry-based cursor detection is well-implemented:

  • Queries all [data-message-id] elements within the scroller
  • Filters to visible elements intersecting the viewport
  • Prioritizes elements crossing the reference line, then nearest, then falls back gracefully

58-75: LGTM!

Navigation correctly determines the current index based on whether the user has scrolled since the last message selection, handles boundary cases (past-end triggers resumeScroll), and pauses auto-scroll when navigating to a specific message.

packages/app/src/pages/session.tsx (3)

140-144: LGTM!

The page-level store consolidates UI state appropriately. Per project learnings, createStore is preferred when fields share a logical domain (page-level view state), even if they're not always updated together.


97-130: LGTM!

Clean integration of createSessionTimelineData. The destructuring of timeline accessors into local constants improves readability while maintaining reactivity.


536-572: LGTM!

The SessionMainView integration cleanly passes composed state and handlers. The prop drilling is explicit and traceable, which is appropriate for a composition root.

packages/app/src/pages/session/use-session-vcs-refresh.ts (2)

45-55: LGTM!

The file watcher integration properly:

  • Filters to file.watcher.updated events
  • Safely extracts the file path from properties
  • Ignores .git/ internal file changes
  • Cleans up the listener via onCleanup

16-43: LGTM!

The refresh logic correctly uses untrack to read mode and wantsReview without creating dependencies, and the branch-change effect uses structural equality via same() to avoid spurious refreshes.

Comment thread packages/app/src/pages/session.tsx Outdated
Comment thread packages/app/src/pages/session/session-main-view.tsx Outdated
Comment thread packages/app/src/pages/session/use-session-comment-context.ts
Comment thread packages/app/src/pages/session/use-session-review-panel.tsx Outdated
Comment thread packages/app/src/pages/session/use-session-route-tabs.ts Outdated
Comment thread packages/app/src/pages/session/use-session-timeline-data.ts Outdated
Comment thread packages/app/src/pages/session/use-session-vcs-refresh.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app/src/pages/session.tsx`:
- Around line 141-144: The current use of createStore for independent fields
mobileTab and deferRender can be changed to separate signals to follow the
guideline; replace the createStore call initializing mobileTab and deferRender
with two createSignal calls (e.g., const [mobileTab, setMobileTab] =
createSignal<"session" | "changes">("session") and const [deferRender,
setDeferRender] = createSignal(false)), then update every reference and setter
usage of store.mobileTab and store.deferRender (and any setStore({...}) calls)
to use the new signals and setters (e.g., mobileTab(), setMobileTab(...),
deferRender(), setDeferRender(...)) in the handlers and components that
currently access mobileTab and deferRender.

In `@packages/app/src/pages/session/use-session-new-worktree.ts`:
- Around line 8-10: The code uses createStore for a single field ({ value:
"main" })—replace it with a createSignal to simplify state: remove
createStore/setStore and instead create a signal (e.g., const [value, setValue]
= createSignal("main")), then update all usages of store.value to call the
signal getter (value()) and replace setStore updates with setValue(newValue);
keep createStore only if you later need coupled object state updated in one
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2f657671-c223-4b57-9a24-feb9017cc5d0

📥 Commits

Reviewing files that changed from the base of the PR and between dd0b7f8 and a23fbd7.

📒 Files selected for processing (3)
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-history-backfill.ts
  • packages/app/src/pages/session/use-session-new-worktree.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-opencode
  • GitHub Check: unit-desktop
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
  • GitHub Check: smoke-macos-arm64
🧰 Additional context used
📓 Path-based instructions (1)
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
  • packages/app/src/pages/session/use-session-history-backfill.ts
  • packages/app/src/pages/session.tsx
🧠 Learnings (24)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/patch/index.ts:337-346
Timestamp: 2026-04-28T04:38:05.946Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/patch/index.ts`), the BOM-transition surfacing gap — where `Bom.split` strips BOM before building `unified_diff`/`new_content` but `Bom.join` later re-attaches BOM on disk write, meaning BOM changes are not reflected in the diff payload — is intentionally deferred. PR `#270` is an upstream-sync graft; fixing the issue here would mix refactor + bugfix intents and drift the diff from the upstream baseline needed for clean future grafts. A dedicated follow-up PR (or upstream report) will address this. Do NOT re-flag the missing BOM-change surfacing in `ApplyPatchFileUpdate`/`ApplyPatchFileChange` as a blocking issue in PR `#270` or in future sync PRs that carry the same upstream baseline.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:58.329Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/session/compaction.ts:169-191
Timestamp: 2026-04-28T05:36:22.128Z
Learning: In `packages/opencode/src/session/compaction.ts` (Astro-Han/pawwork), the internal helper `splitTurn` intentionally returns a raw `Effect.gen(...)` rather than being wrapped with `Effect.fnUntraced`. Converting `splitTurn` (and similar plain-Effect.gen internal helpers in this file, e.g. `turns`, `completedCompactions`, `buildPrompt`) to `Effect.fnUntraced` is a repo-wide convention sweep deferred to a dedicated follow-up PR. The same sweep covers `tool/tool.ts` and other helpers flagged in the same sync. Do NOT re-flag the absence of `Effect.fnUntraced` on `splitTurn` or other internal helpers in `compaction.ts` during upstream-sync PRs; flag it only in the dedicated convention-sweep PR.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 353
File: packages/app/e2e/session/session-composer-dock.spec.ts:53-70
Timestamp: 2026-05-01T02:06:33.069Z
Learning: In `packages/app/e2e/session/session-composer-dock.spec.ts` (Astro-Han/pawwork), local question-dataset fixtures (`defaultQuestions`, `multiQuestions`, etc.) intentionally use camelCase, not SCREAMING_SNAKE_CASE. The SCREAMING_SNAKE_CASE guideline for `packages/app/e2e/**/*.spec.ts` is not applied uniformly to local fixture objects in this file. Do NOT re-flag camelCase question-fixture names here as style violations.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/app/src/components/dialog-delete-session.tsx:22-31
Timestamp: 2026-04-30T10:26:58.545Z
Learning: In `packages/app/src/components/dialog-delete-session.tsx` (Astro-Han/pawwork, PR `#342`), `handleDelete` uses a `deleting` signal with `try/finally` but intentionally omits an explicit `catch` block. The caller (`deleteSession` in `packages/app/src/pages/layout.tsx` around line 1083) already swallows errors with a toast, so `onConfirm` does not reject in practice. The `try/finally` is sufficient to keep the Delete button usable if a future caller throws. Do NOT re-flag the missing explicit catch in this component.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/session/session-view-controller.test.ts` and related Solid + Bun test files), the Bun + Solid test environment does NOT reliably exercise `createMemo((current) => ...)` signal recomputation the way a browser runtime does. Adding signal-driven transition tests (e.g., using `createSignal` + `createRoot` to flip reactive inputs) is misleading in this environment because the reactive invalidation/recomputation path is not faithfully replicated. The correct strategy is: cover pure transition logic with plain unit tests (e.g., `nextSessionViewState`), and cover the browser reactive path with E2E tests (e.g., session-switch E2E spec). Do NOT re-flag the absence of signal-driven `createSessionViewController` tests in this environment as a gap.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/test/session/subagent-lifecycle-integration.test.ts:41-47
Timestamp: 2026-04-28T11:24:35.312Z
Learning: In `packages/opencode/test/session/subagent-lifecycle-integration.test.ts` (Astro-Han/pawwork, PR `#287`), the test suite intentionally uses a manual `run` helper (`Effect.runPromise(program.pipe(Effect.provide(Layer.mergeAll(SubagentRun.defaultLayer, Session.defaultLayer)), Effect.orDie))`) and raw `bun:test` test cases rather than the `testEffect(...)` harness. All 27 lifecycle tests pass with this pattern. Migration to `testEffect` + `it.live(...)` is deferred to a dedicated test-harness sweep PR to avoid fixture drift before merge. Do NOT re-flag the manual `Effect.runPromise` runner in this file as needing harness migration until that sweep PR lands.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/e2e/session/session-scroll-position.spec.ts:171-230
Timestamp: 2026-04-29T13:27:25.687Z
Learning: In `packages/app/e2e/session/session-scroll-position.spec.ts`, the `describe()` formatter is intentionally duplicated in `installPageErrorProbe` (inside `page.addInitScript`, runs in browser context) and `collectPageErrors` (runs in Node.js/Playwright context). These are separate execution contexts — `addInitScript` serializes its closure to a string for injection — so extracting a shared helper would add cross-context indirection. Do NOT flag this duplication as a maintenance issue.
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
📚 Learning: 2026-04-29T13:27:28.494Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/session/session-view-controller.test.ts` and related Solid + Bun test files), the Bun + Solid test environment does NOT reliably exercise `createMemo((current) => ...)` signal recomputation the way a browser runtime does. Adding signal-driven transition tests (e.g., using `createSignal` + `createRoot` to flip reactive inputs) is misleading in this environment because the reactive invalidation/recomputation path is not faithfully replicated. The correct strategy is: cover pure transition logic with plain unit tests (e.g., `nextSessionViewState`), and cover the browser reactive path with E2E tests (e.g., session-switch E2E spec). Do NOT re-flag the absence of signal-driven `createSessionViewController` tests in this environment as a gap.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
  • packages/app/src/pages/session/use-session-history-backfill.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-24T03:51:56.211Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:56.211Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `InstanceState` from `src/effect/instance-state.ts` for per-directory or per-project state that needs per-instance cleanup; do work directly in the `InstanceState.make` closure where `ScopedCache` handles run-once semantics

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
📚 Learning: 2026-04-25T12:52:37.850Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:24-31
Timestamp: 2026-04-25T12:52:37.850Z
Learning: In `packages/opencode/src/session/export.ts`, the `hashFile` async helper intentionally uses raw `node:fs/promises` (`fs.readFile`) rather than `FileSystem.FileSystem`. It is a small, self-contained helper called inside `Effect.promise(...)` with broad failure tolerance (try/catch returning `undefined`). Using `FileSystem.FileSystem` here would add unnecessary layers without payoff. The `FileSystem.FileSystem` preference applies to Effect-service-style operations, not thin async boundary helpers like `hashFile`.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
📚 Learning: 2026-04-26T16:34:57.130Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:57.130Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
  • packages/app/src/pages/session/use-session-history-backfill.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T04:38:11.727Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/tool/edit.ts:35-45
Timestamp: 2026-04-28T04:38:11.727Z
Learning: In `packages/opencode/src/tool/edit.ts` (Astro-Han/pawwork, PR `#270`), the module-scoped `Map<string, Semaphore.Semaphore>` (`locks`) and `lock(filePath)` helper are intentionally left as a global, never-cleaned registry. PR `#270` is a pure upstream-sync graft and mixing in a bugfix would drift the diff from the upstream baseline. The fix (moving per-file semaphores into `InstanceState`/`ScopedCache` for per-instance lifecycle management) is tracked as a follow-up to land in its own PR or be reported upstream. Do NOT re-flag the absence of InstanceState wiring for the lock map in this file until the follow-up PR is opened.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
📚 Learning: 2026-04-30T10:41:33.826Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/desktop-electron/src/main/ipc.ts:342-344
Timestamp: 2026-04-30T10:41:33.826Z
Learning: In Astro-Han/pawwork, the `export-session` IPC handler in `packages/desktop-electron/src/main/ipc.ts` accepts an optional localized `title` argument for `dialog.showSaveDialog`, keeping `"Export session"` as an English fallback. The localized title is plumbed from the renderer through `platform.exportSession` → `window.api.exportSession` → preload (`preload/types.ts`, `preload/index.ts`, `renderer/index.tsx`) → main, mirroring the same shape as the `save-file-picker` handler. Both renderer callsites (`layout.tsx` and `message-timeline.tsx`) pass `language.t("session.export.action.export")`. Do NOT flag the hard-coded fallback as unlocalized — the renderer is responsible for supplying the localized title.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
📚 Learning: 2026-04-28T05:36:22.128Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/session/compaction.ts:169-191
Timestamp: 2026-04-28T05:36:22.128Z
Learning: In `packages/opencode/src/session/compaction.ts` (Astro-Han/pawwork), the internal helper `splitTurn` intentionally returns a raw `Effect.gen(...)` rather than being wrapped with `Effect.fnUntraced`. Converting `splitTurn` (and similar plain-Effect.gen internal helpers in this file, e.g. `turns`, `completedCompactions`, `buildPrompt`) to `Effect.fnUntraced` is a repo-wide convention sweep deferred to a dedicated follow-up PR. The same sweep covers `tool/tool.ts` and other helpers flagged in the same sync. Do NOT re-flag the absence of `Effect.fnUntraced` on `splitTurn` or other internal helpers in `compaction.ts` during upstream-sync PRs; flag it only in the dedicated convention-sweep PR.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
  • packages/app/src/pages/session/use-session-history-backfill.ts
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
  • packages/app/src/pages/session/use-session-history-backfill.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/use-session-new-worktree.ts
  • packages/app/src/pages/session/use-session-history-backfill.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T13:10:01.345Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/src/tool/agent.ts:89-102
Timestamp: 2026-04-28T13:10:01.345Z
Learning: In `packages/opencode/src/tool/agent.ts` (Astro-Han/pawwork, PR `#287`), `makeReadLastCompletedAssistantText` intentionally scans the full child session message history (no `limit`) to find the latest completed assistant text part for `partial_result` on cancellation. Cancellation is a rare path so the extra read cost is acceptable; tool-heavy children can push the last stable assistant text far back. Do NOT suggest re-adding a fixed small limit (e.g., `limit: 5`) to this function.

Applied to files:

  • packages/app/src/pages/session/use-session-history-backfill.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-29T13:27:25.687Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/e2e/session/session-scroll-position.spec.ts:171-230
Timestamp: 2026-04-29T13:27:25.687Z
Learning: In `packages/app/e2e/session/session-scroll-position.spec.ts`, the `describe()` formatter is intentionally duplicated in `installPageErrorProbe` (inside `page.addInitScript`, runs in browser context) and `collectPageErrors` (runs in Node.js/Playwright context). These are separate execution contexts — `addInitScript` serializes its closure to a string for injection — so extracting a shared helper would add cross-context indirection. Do NOT flag this duplication as a maintenance issue.

Applied to files:

  • packages/app/src/pages/session/use-session-history-backfill.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T18:53:46.234Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 308
File: packages/opencode/test/server/global-session-list.test.ts:224-259
Timestamp: 2026-04-28T18:53:46.234Z
Learning: In `packages/opencode/test/server/global-session-list.test.ts` (Astro-Han/pawwork), all `it.live(...)` test bodies intentionally use `Effect.promise(async () => { ... })` rather than `Effect.gen(function* () { ... })`. This is the established local convention for the route/pagination tests in this file (e.g., "supports cursor pagination", "session route orders by creation time", "session routes omit undefined optional fields"). Do NOT flag individual `it.live` cases in this file as needing conversion to `Effect.gen`; any harness-style migration would need to cover the entire file.

Applied to files:

  • packages/app/src/pages/session/use-session-history-backfill.ts
📚 Learning: 2026-04-30T14:25:19.902Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-30T10:26:58.545Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/app/src/components/dialog-delete-session.tsx:22-31
Timestamp: 2026-04-30T10:26:58.545Z
Learning: In `packages/app/src/components/dialog-delete-session.tsx` (Astro-Han/pawwork, PR `#342`), `handleDelete` uses a `deleting` signal with `try/finally` but intentionally omits an explicit `catch` block. The caller (`deleteSession` in `packages/app/src/pages/layout.tsx` around line 1083) already swallows errors with a toast, so `onConfirm` does not reject in practice. The `try/finally` is sufficient to keep the Delete button usable if a future caller throws. Do NOT re-flag the missing explicit catch in this component.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-27T10:33:12.228Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:12.228Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/prompt.ts` and `packages/opencode/src/session/processor.ts`, PR `#264`), the loop-gate race condition between `buildLoopContext()` and `recordSyntheticBlock`/`recordSyntheticStop` is intentionally handled via idempotence guards (re-check sigKey presence / `hasStopped` inside the record helpers) rather than a full per-parent `Effect.Mutex`. Threading a `Map<MessageID, Mutex>` through the processor was considered too large a surface change for this edge case; the residual TOCTOU window only produces extra synthetic parts with no behavioral drift on the "turn ends" contract. A code comment documents the trade-off and points to a full-mutex follow-up if the race is observed in practice. Do NOT re-flag the absence of a per-parent mutex as a blocking issue in future reviews.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-25T12:52:49.735Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:49.735Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-27T11:18:47.298Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/websearch.test.ts:21-78
Timestamp: 2026-04-27T11:18:47.298Z
Learning: In `packages/opencode/test/tool/websearch.test.ts`, the tests intentionally use manual `Effect.runPromise` with explicit `Effect.provide(...)` chains (including `Layer.succeed(Auth.Service, ...)`, `Layer.succeed(HttpClient.HttpClient, http)`, `WebSearchAuth.layer`, `Truncate.defaultLayer`, and `Agent.defaultLayer`) rather than the `testEffect(...)` harness. This is by design: the fake Auth and HTTP recovery-metadata layers must be explicitly injected and kept visible/scoped at the test site. Do NOT suggest migrating these tests to `testEffect` or removing the manual layer provides.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-25T12:52:35.631Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:35.631Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-24T05:39:58.329Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:58.329Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/app/src/pages/session.tsx
🔇 Additional comments (6)
packages/app/src/pages/session.tsx (4)

240-260: Forward reference pattern flagged in previous review.

This pattern where scrollDock receives callbacks referencing activeMessage and historyBackfill (assigned later) works correctly because callbacks are invoked after initialization, not during construction. A previous review already suggested adding a clarifying comment.


488-525: LGTM!

The refactored SessionMainView wiring correctly passes all 34 props from the various composables. The evaluated accessors (e.g., timelineSessionID(), centered()) follow standard Solid JSX patterns where the expressions become property getters for reactivity.


425-486: LGTM!

The renderComposerRegion function correctly wires the followup and revert props from their respective composables. The conditional logic properly gates followups to non-child sessions and revert to sessions with rolled items.


54-103: Well-structured composition root.

The refactored Page component successfully delegates to focused composables while maintaining clear data flow. The createSessionTimelineData composable cleanly encapsulates timeline state derivation, and the route tabs handoff via createSessionRouteTabs properly separates concerns.

packages/app/src/pages/session/use-session-history-backfill.ts (1)

1-62: LGTM!

The composable correctly implements history backfill with proper safeguards:

  • The fillFrame guard prevents concurrent rAF scheduling
  • Conditions are re-checked inside the rAF callback to handle state changes during the async delay
  • The onCleanup properly cancels pending frames
  • The double-checking pattern (effect filters + fill() filters) is appropriate defensive programming
packages/app/src/pages/session/use-session-new-worktree.ts (1)

12-33: LGTM!

The selected memo correctly handles the three cases (create, worktree directory, main), and the directory-change effect properly resets state. The defer: true option ensures the reset doesn't fire on initial mount.

Comment thread packages/app/src/pages/session.tsx Outdated
Comment thread packages/app/src/pages/session/use-session-new-worktree.ts Outdated
@Astro-Han
Copy link
Copy Markdown
Owner Author

Follow-up timeline interaction split pushed.

Additional changes:

  • Extracted use-session-timeline-interaction.ts to own scroll dock, active message state, history window, history backfill, and hash scroll wiring.
  • Completed the standalone followup test encode mock with base64Decode.
  • session.tsx is now 472 lines.

Verification rerun locally:

  • bun typecheck: passed.
  • Focused session unit suite: 47 passed, 0 failed.
  • Target composer dock E2E: 1 passed.
  • git diff --check: passed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app/src/pages/session.tsx`:
- Around line 142-149: The createComputed block that sets deferRender uses
requestAnimationFrame/setTimeout callbacks that can be executed out of order;
guard against stale callbacks by tracking and cancelling pending handles or
using a monotonic version token: in the createComputed where
timelineSessionKey() is compared to prev, store the raf and timeout handles
(from requestAnimationFrame and setTimeout) or a numeric version id scoped to
that createComputed, cancel previous handles with
cancelAnimationFrame/clearTimeout or ignore callbacks whose version doesn't
match before calling setStore("deferRender", false), and also clear any
remaining pending handles in onCleanup to avoid leaks; reference createComputed,
timelineSessionKey, setStore("deferRender", ...), requestAnimationFrame,
setTimeout, onCleanup, cancelAnimationFrame, and clearTimeout when making the
fix.

In `@packages/app/src/pages/session/use-session-followups.test.ts`:
- Around line 60-78: The test omits assertions for the two required positive
prerequisites; add checks that shouldAutoSendFollowup returns false when
hasSession is false and when hasItem is false (and optionally when both are
false) to ensure regressions in hasSession/hasItem are caught; update the test
in use-session-followups.test.ts around the existing shouldAutoSendFollowup
assertions to include expect(shouldAutoSendFollowup({ ...base, hasSession: false
})).toBe(false) and expect(shouldAutoSendFollowup({ ...base, hasItem: false
})).toBe(false) (and optionally expect(shouldAutoSendFollowup({ ...base,
hasSession: false, hasItem: false })).toBe(false)).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5a0145d8-5898-4c2f-b34f-b53060457339

📥 Commits

Reviewing files that changed from the base of the PR and between a23fbd7 and 9bed311.

📒 Files selected for processing (3)
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-followups.test.ts
  • packages/app/src/pages/session/use-session-timeline-interaction.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: analyze-js-ts
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: typecheck
  • GitHub Check: unit-opencode
  • GitHub Check: unit-desktop
🧰 Additional context used
📓 Path-based instructions (1)
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/use-session-followups.test.ts
  • packages/app/src/pages/session/use-session-timeline-interaction.ts
  • packages/app/src/pages/session.tsx
🧠 Learnings (29)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/session/session-view-controller.test.ts` and related Solid + Bun test files), the Bun + Solid test environment does NOT reliably exercise `createMemo((current) => ...)` signal recomputation the way a browser runtime does. Adding signal-driven transition tests (e.g., using `createSignal` + `createRoot` to flip reactive inputs) is misleading in this environment because the reactive invalidation/recomputation path is not faithfully replicated. The correct strategy is: cover pure transition logic with plain unit tests (e.g., `nextSessionViewState`), and cover the browser reactive path with E2E tests (e.g., session-switch E2E spec). Do NOT re-flag the absence of signal-driven `createSessionViewController` tests in this environment as a gap.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/patch/index.ts:337-346
Timestamp: 2026-04-28T04:38:05.946Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/patch/index.ts`), the BOM-transition surfacing gap — where `Bom.split` strips BOM before building `unified_diff`/`new_content` but `Bom.join` later re-attaches BOM on disk write, meaning BOM changes are not reflected in the diff payload — is intentionally deferred. PR `#270` is an upstream-sync graft; fixing the issue here would mix refactor + bugfix intents and drift the diff from the upstream baseline needed for clean future grafts. A dedicated follow-up PR (or upstream report) will address this. Do NOT re-flag the missing BOM-change surfacing in `ApplyPatchFileUpdate`/`ApplyPatchFileChange` as a blocking issue in PR `#270` or in future sync PRs that carry the same upstream baseline.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/session/compaction.ts:169-191
Timestamp: 2026-04-28T05:36:22.128Z
Learning: In `packages/opencode/src/session/compaction.ts` (Astro-Han/pawwork), the internal helper `splitTurn` intentionally returns a raw `Effect.gen(...)` rather than being wrapped with `Effect.fnUntraced`. Converting `splitTurn` (and similar plain-Effect.gen internal helpers in this file, e.g. `turns`, `completedCompactions`, `buildPrompt`) to `Effect.fnUntraced` is a repo-wide convention sweep deferred to a dedicated follow-up PR. The same sweep covers `tool/tool.ts` and other helpers flagged in the same sync. Do NOT re-flag the absence of `Effect.fnUntraced` on `splitTurn` or other internal helpers in `compaction.ts` during upstream-sync PRs; flag it only in the dedicated convention-sweep PR.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/test/session/subagent-lifecycle-integration.test.ts:41-47
Timestamp: 2026-04-28T11:24:35.312Z
Learning: In `packages/opencode/test/session/subagent-lifecycle-integration.test.ts` (Astro-Han/pawwork, PR `#287`), the test suite intentionally uses a manual `run` helper (`Effect.runPromise(program.pipe(Effect.provide(Layer.mergeAll(SubagentRun.defaultLayer, Session.defaultLayer)), Effect.orDie))`) and raw `bun:test` test cases rather than the `testEffect(...)` harness. All 27 lifecycle tests pass with this pattern. Migration to `testEffect` + `it.live(...)` is deferred to a dedicated test-harness sweep PR to avoid fixture drift before merge. Do NOT re-flag the manual `Effect.runPromise` runner in this file as needing harness migration until that sweep PR lands.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:58.329Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 353
File: packages/app/e2e/session/session-composer-dock.spec.ts:53-70
Timestamp: 2026-05-01T02:06:33.069Z
Learning: In `packages/app/e2e/session/session-composer-dock.spec.ts` (Astro-Han/pawwork), local question-dataset fixtures (`defaultQuestions`, `multiQuestions`, etc.) intentionally use camelCase, not SCREAMING_SNAKE_CASE. The SCREAMING_SNAKE_CASE guideline for `packages/app/e2e/**/*.spec.ts` is not applied uniformly to local fixture objects in this file. Do NOT re-flag camelCase question-fixture names here as style violations.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/app/src/components/dialog-delete-session.tsx:22-31
Timestamp: 2026-04-30T10:26:58.545Z
Learning: In `packages/app/src/components/dialog-delete-session.tsx` (Astro-Han/pawwork, PR `#342`), `handleDelete` uses a `deleting` signal with `try/finally` but intentionally omits an explicit `catch` block. The caller (`deleteSession` in `packages/app/src/pages/layout.tsx` around line 1083) already swallows errors with a toast, so `onConfirm` does not reject in practice. The `try/finally` is sufficient to keep the Delete button usable if a future caller throws. Do NOT re-flag the missing explicit catch in this component.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/e2e/session/session-scroll-position.spec.ts:171-230
Timestamp: 2026-04-29T13:27:25.687Z
Learning: In `packages/app/e2e/session/session-scroll-position.spec.ts`, the `describe()` formatter is intentionally duplicated in `installPageErrorProbe` (inside `page.addInitScript`, runs in browser context) and `collectPageErrors` (runs in Node.js/Playwright context). These are separate execution contexts — `addInitScript` serializes its closure to a string for injection — so extracting a shared helper would add cross-context indirection. Do NOT flag this duplication as a maintenance issue.
📚 Learning: 2026-04-29T13:27:28.494Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In pawwork’s Bun+Solid test environment (under packages/app/src/pages/session/**.test.ts), do not expect `createMemo((current) => ...)` to be recomputed/invalidated the same way it would in a real browser runtime. Avoid adding signal-driven transition tests that rely on Solid reactive invalidation paths in this environment (e.g., flipping inputs via `createSignal`/`createRoot` just to observe `createMemo` behavior), since it can be misleading. Instead, review/author tests as follows: (1) cover pure transition/state-update logic with plain unit tests (e.g., `nextSessionViewState`), and (2) cover the actual browser reactive behavior with E2E tests (e.g., the session-switch E2E spec). When reviewing this Bun+Solid test suite, do not treat the absence of signal-driven `createSessionViewController` tests as a missing-coverage gap.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-28T11:24:35.312Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/test/session/subagent-lifecycle-integration.test.ts:41-47
Timestamp: 2026-04-28T11:24:35.312Z
Learning: In `packages/opencode/test/session/subagent-lifecycle-integration.test.ts` (Astro-Han/pawwork, PR `#287`), the test suite intentionally uses a manual `run` helper (`Effect.runPromise(program.pipe(Effect.provide(Layer.mergeAll(SubagentRun.defaultLayer, Session.defaultLayer)), Effect.orDie))`) and raw `bun:test` test cases rather than the `testEffect(...)` harness. All 27 lifecycle tests pass with this pattern. Migration to `testEffect` + `it.live(...)` is deferred to a dedicated test-harness sweep PR to avoid fixture drift before merge. Do NOT re-flag the manual `Effect.runPromise` runner in this file as needing harness migration until that sweep PR lands.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-24T03:51:56.211Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:56.211Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-28T18:53:46.234Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 308
File: packages/opencode/test/server/global-session-list.test.ts:224-259
Timestamp: 2026-04-28T18:53:46.234Z
Learning: In `packages/opencode/test/server/global-session-list.test.ts` (Astro-Han/pawwork), all `it.live(...)` test bodies intentionally use `Effect.promise(async () => { ... })` rather than `Effect.gen(function* () { ... })`. This is the established local convention for the route/pagination tests in this file (e.g., "supports cursor pagination", "session route orders by creation time", "session routes omit undefined optional fields"). Do NOT flag individual `it.live` cases in this file as needing conversion to `Effect.gen`; any harness-style migration would need to cover the entire file.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-29T13:27:25.687Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/e2e/session/session-scroll-position.spec.ts:171-230
Timestamp: 2026-04-29T13:27:25.687Z
Learning: In `packages/app/e2e/session/session-scroll-position.spec.ts`, the `describe()` formatter is intentionally duplicated in `installPageErrorProbe` (inside `page.addInitScript`, runs in browser context) and `collectPageErrors` (runs in Node.js/Playwright context). These are separate execution contexts — `addInitScript` serializes its closure to a string for injection — so extracting a shared helper would add cross-context indirection. Do NOT flag this duplication as a maintenance issue.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
  • packages/app/src/pages/session/use-session-timeline-interaction.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Test one feature per test file

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-28T04:56:21.338Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/test/tool/tool-define.test.ts:2-9
Timestamp: 2026-04-28T04:56:21.338Z
Learning: In `packages/opencode/test/tool/tool-define.test.ts` (Astro-Han/pawwork, PR `#270`), the test suite intentionally uses a `ManagedRuntime.make(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer))` wrapper and raw `bun:test` `test(...)` cases rather than the `testEffect(...)` harness. This code was adopted wholesale from upstream via the graft strategy (per `project_upstream_strategy.md`). Migrating to `testEffect` is deferred to a follow-up PR so as not to mix refactor + harness-migration intents and to avoid drifting the diff from the upstream baseline needed for clean future grafts. Do NOT re-flag the `ManagedRuntime.make` usage in this file as needing harness migration until that follow-up lands.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-23T15:25:31.118Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-27T11:19:24.963Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/websearch-auth.test.ts:0-0
Timestamp: 2026-04-27T11:19:24.963Z
Learning: In `packages/opencode/test/tool/websearch-auth.test.ts` (Astro-Han/pawwork), the tests intentionally use a small local `runWith` runner with raw `bun:test` and `Effect.runPromise` rather than the `testEffect` harness. Each test case injects a custom in-memory `Auth.Service` layer; switching to `testEffect` would be style-only churn without changing risk coverage. Do not flag these tests as needing harness migration.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-27T12:59:49.844Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/test/session/prompt-effect.test.ts:0-0
Timestamp: 2026-04-27T12:59:49.844Z
Learning: In `packages/opencode/test/session/prompt-effect.test.ts` and `packages/opencode/src/session/diagnostics.ts` (PR `#264`), the recovery reminder copy differs between signature kinds: the input-repeat variant says "repeated the same tool input 3 times" (uses a literal count), while the target-repeat variant says "failed against the same target multiple times" (uses "multiple times" with no count). Assertions that check for injected reminder text in LLM inputs must accept both phrasings when a scenario produces both `input:` and `target:` signatures (e.g., `read` tool with a `filePath` parameter). Do NOT narrow the assertion to only the input-variant phrasing.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
  • packages/app/src/pages/session/use-session-timeline-interaction.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/use-session-followups.test.ts
  • packages/app/src/pages/session/use-session-timeline-interaction.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-29T13:27:28.494Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/session/session-view-controller.test.ts` and related Solid + Bun test files), the Bun + Solid test environment does NOT reliably exercise `createMemo((current) => ...)` signal recomputation the way a browser runtime does. Adding signal-driven transition tests (e.g., using `createSignal` + `createRoot` to flip reactive inputs) is misleading in this environment because the reactive invalidation/recomputation path is not faithfully replicated. The correct strategy is: cover pure transition logic with plain unit tests (e.g., `nextSessionViewState`), and cover the browser reactive path with E2E tests (e.g., session-switch E2E spec). Do NOT re-flag the absence of signal-driven `createSessionViewController` tests in this environment as a gap.

Applied to files:

  • packages/app/src/pages/session/use-session-timeline-interaction.ts
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-30T10:41:33.826Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/desktop-electron/src/main/ipc.ts:342-344
Timestamp: 2026-04-30T10:41:33.826Z
Learning: In Astro-Han/pawwork, the `export-session` IPC handler in `packages/desktop-electron/src/main/ipc.ts` accepts an optional localized `title` argument for `dialog.showSaveDialog`, keeping `"Export session"` as an English fallback. The localized title is plumbed from the renderer through `platform.exportSession` → `window.api.exportSession` → preload (`preload/types.ts`, `preload/index.ts`, `renderer/index.tsx`) → main, mirroring the same shape as the `save-file-picker` handler. Both renderer callsites (`layout.tsx` and `message-timeline.tsx`) pass `language.t("session.export.action.export")`. Do NOT flag the hard-coded fallback as unlocalized — the renderer is responsible for supplying the localized title.

Applied to files:

  • packages/app/src/pages/session/use-session-timeline-interaction.ts
📚 Learning: 2026-04-30T14:25:19.902Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-26T16:34:57.130Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:57.130Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-30T10:26:58.545Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/app/src/components/dialog-delete-session.tsx:22-31
Timestamp: 2026-04-30T10:26:58.545Z
Learning: In `packages/app/src/components/dialog-delete-session.tsx` (Astro-Han/pawwork, PR `#342`), `handleDelete` uses a `deleting` signal with `try/finally` but intentionally omits an explicit `catch` block. The caller (`deleteSession` in `packages/app/src/pages/layout.tsx` around line 1083) already swallows errors with a toast, so `onConfirm` does not reject in practice. The `try/finally` is sufficient to keep the Delete button usable if a future caller throws. Do NOT re-flag the missing explicit catch in this component.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-27T10:33:12.228Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:12.228Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/prompt.ts` and `packages/opencode/src/session/processor.ts`, PR `#264`), the loop-gate race condition between `buildLoopContext()` and `recordSyntheticBlock`/`recordSyntheticStop` is intentionally handled via idempotence guards (re-check sigKey presence / `hasStopped` inside the record helpers) rather than a full per-parent `Effect.Mutex`. Threading a `Map<MessageID, Mutex>` through the processor was considered too large a surface change for this edge case; the residual TOCTOU window only produces extra synthetic parts with no behavioral drift on the "turn ends" contract. A code comment documents the trade-off and points to a full-mutex follow-up if the race is observed in practice. Do NOT re-flag the absence of a per-parent mutex as a blocking issue in future reviews.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T13:10:01.345Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/src/tool/agent.ts:89-102
Timestamp: 2026-04-28T13:10:01.345Z
Learning: In `packages/opencode/src/tool/agent.ts` (Astro-Han/pawwork, PR `#287`), `makeReadLastCompletedAssistantText` intentionally scans the full child session message history (no `limit`) to find the latest completed assistant text part for `partial_result` on cancellation. Cancellation is a rare path so the extra read cost is acceptable; tool-heavy children can push the last stable assistant text far back. Do NOT suggest re-adding a fixed small limit (e.g., `limit: 5`) to this function.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-25T12:52:49.735Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:49.735Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T05:36:22.128Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/session/compaction.ts:169-191
Timestamp: 2026-04-28T05:36:22.128Z
Learning: In `packages/opencode/src/session/compaction.ts` (Astro-Han/pawwork), the internal helper `splitTurn` intentionally returns a raw `Effect.gen(...)` rather than being wrapped with `Effect.fnUntraced`. Converting `splitTurn` (and similar plain-Effect.gen internal helpers in this file, e.g. `turns`, `completedCompactions`, `buildPrompt`) to `Effect.fnUntraced` is a repo-wide convention sweep deferred to a dedicated follow-up PR. The same sweep covers `tool/tool.ts` and other helpers flagged in the same sync. Do NOT re-flag the absence of `Effect.fnUntraced` on `splitTurn` or other internal helpers in `compaction.ts` during upstream-sync PRs; flag it only in the dedicated convention-sweep PR.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-24T05:39:58.329Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:58.329Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T04:38:11.727Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/tool/edit.ts:35-45
Timestamp: 2026-04-28T04:38:11.727Z
Learning: In `packages/opencode/src/tool/edit.ts` (Astro-Han/pawwork, PR `#270`), the module-scoped `Map<string, Semaphore.Semaphore>` (`locks`) and `lock(filePath)` helper are intentionally left as a global, never-cleaned registry. PR `#270` is a pure upstream-sync graft and mixing in a bugfix would drift the diff from the upstream baseline. The fix (moving per-file semaphores into `InstanceState`/`ScopedCache` for per-instance lifecycle management) is tracked as a follow-up to land in its own PR or be reported upstream. Do NOT re-flag the absence of InstanceState wiring for the lock map in this file until the follow-up PR is opened.

Applied to files:

  • packages/app/src/pages/session.tsx
🔇 Additional comments (1)
packages/app/src/pages/session.tsx (1)

160-168: Use timelineSessionKey for all three controller instantiations to ensure consistent reset boundaries.

useSessionLayout().sessionKey follows route params, while createSessionTimelineData() exposes timelineSessionKey which tracks visibleSessionID. During new-session flows or route handoffs, these diverge while messages are loading: visibleSessionID lags behind routeSessionID until routeMessagesReady is true. Using sessionKey for reviewState (line 162), reviewPanel (line 224), and timelineInteraction (line 236) means these controllers reset on a different boundary than the timeline data they depend on.

Pass timelineSessionKey to all three instead:

  • Line 162: reviewState constructor
  • Line 224: reviewPanel constructor
  • Line 236: timelineInteraction constructor

Comment thread packages/app/src/pages/session.tsx
Comment thread packages/app/src/pages/session/use-session-followups.test.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/app/src/pages/session.tsx (1)

140-149: ⚠️ Potential issue | 🟡 Minor | 💤 Low value

Guard deferRender against stale scheduled callbacks.

The deferRender transition guard can race if the user rapidly switches sessions. An earlier callback may flip deferRender back to false during a subsequent transition, causing a brief render flash.

Suggested fix with epoch tracking
+  let deferRenderEpoch = 0
+
   createComputed((prev) => {
     const key = timelineSessionKey()
     if (key !== prev) {
+      const epoch = ++deferRenderEpoch
       setDeferRender(true)
       requestAnimationFrame(() => {
-        setTimeout(() => setDeferRender(false), 0)
+        setTimeout(() => {
+          if (epoch === deferRenderEpoch) setDeferRender(false)
+        }, 0)
       })
     }
     return key
   }, timelineSessionKey())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/pages/session.tsx` around lines 140 - 149, The deferRender
toggle in createComputed can be flipped by stale callbacks when sessions change
rapidly; modify the logic in the createComputed that reads timelineSessionKey()
to use an epoch/token (incremented whenever you schedule a defer) and capture
that token when calling requestAnimationFrame/setTimeout, then only call
setDeferRender(false) if the captured token matches the current epoch; update
references in this block (timelineSessionKey, setDeferRender,
requestAnimationFrame, setTimeout) so stale callbacks are ignored and
deferRender cannot be reset by older transitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/app/src/pages/session.tsx`:
- Around line 140-149: The deferRender toggle in createComputed can be flipped
by stale callbacks when sessions change rapidly; modify the logic in the
createComputed that reads timelineSessionKey() to use an epoch/token
(incremented whenever you schedule a defer) and capture that token when calling
requestAnimationFrame/setTimeout, then only call setDeferRender(false) if the
captured token matches the current epoch; update references in this block
(timelineSessionKey, setDeferRender, requestAnimationFrame, setTimeout) so stale
callbacks are ignored and deferRender cannot be reset by older transitions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 828afa18-1c62-4a29-9f56-7e6c3666b5ae

📥 Commits

Reviewing files that changed from the base of the PR and between 9bed311 and 79fa5e9.

📒 Files selected for processing (15)
  • packages/app/e2e/session/session-composer-dock.spec.ts
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-comment-context.test.ts
  • packages/app/src/pages/session/use-session-comment-context.ts
  • packages/app/src/pages/session/use-session-new-worktree.ts
  • packages/app/src/pages/session/use-session-revert.test.ts
  • packages/app/src/pages/session/use-session-revert.ts
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/use-session-review-state.ts
  • packages/app/src/pages/session/use-session-route-tabs.ts
  • packages/app/src/pages/session/use-session-scroll-dock.test.ts
  • packages/app/src/pages/session/use-session-scroll-dock.ts
  • packages/app/src/pages/session/use-session-timeline-data.ts
  • packages/app/src/pages/session/use-session-vcs-refresh.ts

Copy link
Copy Markdown
Owner Author

@Astro-Han Astro-Han left a comment

Choose a reason for hiding this comment

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

PR #354 Review

Overall this is a solid refactor. The scroll dock bug fix is correct — centralizing dock height measurement into a single path eliminates the duplicate measurement that caused the original issue. Module extraction is clean, test coverage is comprehensive.

Two things need attention before merge:

P0 — Restore mutation skips API call

In use-session-revert.ts, the restoreMutation has an early return inside batch() when next exists. This means the API call is never awaited and merge() is never called for that code path. The revertMutation correctly awaits the API and merges the response — restore should do the same.

P1 — use-session-review-panel.tsx exceeds 200-line target

The issue states a soft 200-line target. At 374 lines this is the largest extracted module and was essentially copied from the old session.tsx without further decomposition. The JSX builders (reviewContent, reviewPanel, mobileFallback) and the DOM scroll helpers (scrollToReviewDiff, reviewDiffTop) could be split into a separate review-panel-view.ts.

P2 — as never[] bypasses type safety

In use-session-review-panel.tsx line 28, source as never[] suppresses the type mismatch between the diff arrays. Should use a proper union type or adapter.

P2 — Missing paddingBottom in setScrollRef

The old session.tsx set el.style.paddingBottom = "calc(var(--composer-dock-height, 0px) + 16px)" in setScrollRef. The new use-session-scroll-dock.ts omits this. If this was intentional (handled elsewhere), no issue — but worth confirming.

P3 — 26-line pass-through component

session-composer-region.tsx is a pure prop-forwarding wrapper with no added logic. Fine if kept for future extension, but could be inlined.

Recommendation: Fix P0 before merge. P1/P2 can be follow-up PRs.

Comment thread packages/app/src/pages/session/use-session-revert.ts
Comment thread packages/app/src/pages/session/use-session-review-panel.tsx
Comment thread packages/app/src/pages/session/use-session-review-panel.tsx
Comment thread packages/app/src/pages/session/use-session-scroll-dock.ts
Comment thread packages/app/src/pages/session/session-composer-region.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
packages/app/src/pages/session/use-session-revert.ts (1)

64-66: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore target selection should be index-based, not id comparison.

Line 65 uses item.id > request.id, which is not safe when IDs are not lexically ordered. Compute the next message by array position from the matched ID.

💡 Proposed fix
-      const next = readUserMessages(readSessionMessages(input.sync.data.message[request.sessionID])).find(
-        (item) => item.id > request.id,
-      )
+      const timeline = readUserMessages(readSessionMessages(input.sync.data.message[request.sessionID]))
+      const start = timeline.findIndex((item) => item.id === request.id)
+      const next = start >= 0 ? timeline[start + 1] : undefined
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/pages/session/use-session-revert.ts` around lines 64 - 66,
The selection of the "next" message must be index-based rather than by comparing
IDs; instead of using
readUserMessages(readSessionMessages(input.sync.data.message[request.sessionID])).find((item)
=> item.id > request.id), first build the array via
readSessionMessages/readUserMessages, locate the index of the message whose id
=== request.id, then pick the element at index+1 (or undefined if out of
bounds). Update the logic around readUserMessages/readSessionMessages and
request.sessionID/request.id to use this index lookup and handle the
no-next-message case gracefully.
packages/app/src/pages/session/use-session-timeline-data.ts (1)

72-77: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid lexicographic ID filtering for reverted visibility.

Line 76 uses m.id < revert, which can produce incorrect timeline visibility when IDs are not lexically ordered. Use index-based slicing in timeline order instead.

💡 Proposed fix
   const visibleUserMessages = createMemo(
     () => {
       const revert = revertMessageID()
-      if (!revert) return userMessages()
-      return userMessages().filter((m) => m.id < revert)
+      const items = userMessages()
+      if (!revert) return items
+      const index = items.findIndex((m) => m.id === revert)
+      if (index < 0) return items
+      return items.slice(0, index)
     },
     emptyUserMessages,
     { equals: same },
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/pages/session/use-session-timeline-data.ts` around lines 72
- 77, The visibleUserMessages memo uses lexicographic comparison (m.id < revert)
which fails for non-lexical IDs; instead, compute the index of the revert
message in the timeline returned by userMessages() (e.g., findIndex(m => m.id
=== revert)) and return a slice of userMessages() up to that index (or the full
array if not found), preserving timeline order; update the createMemo block that
references revertMessageID, userMessages, and visibleUserMessages to use
findIndex + slice rather than filter by id comparison.
packages/app/src/pages/session/use-session-review-state.ts (1)

142-162: ⚠️ Potential issue | 🟠 Major

Avoid the extra artifact fetch on every session change.

createResource(input.sessionID, ...) already loads artifacts when the session ID changes. The effect on lines 201–206 schedules refetchArtifactHistory() for that same transition, so opening a session performs two session.artifacts(...) requests before any later diff-triggered refresh.

💡 Suggested fix
-  createEffect(() => {
-    const id = input.sessionID()
-    if (!id) return
-    input.turnDiffs()
-    queueArtifactHistoryRefetch()
-  })
+  createEffect((prevID) => {
+    const id = input.sessionID()
+    if (!id) return id
+    input.turnDiffs()
+    if (prevID !== undefined && id === prevID) {
+      queueArtifactHistoryRefetch()
+    }
+    return id
+  })

Also applies to: 201–206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/pages/session/use-session-review-state.ts` around lines 142
- 162, The code schedules a refetch via queueArtifactHistoryRefetch on session
changes even though createResource(input.sessionID, ...) already fetches
artifacts, causing duplicate requests; update queueArtifactHistoryRefetch (and
the places that call it) to skip scheduling a refetch when the current
artifactHistory.sessionID does not match input.sessionID (meaning the
createResource initial load is already fetching the new session), e.g., check
artifactHistory()?.sessionID === input.sessionID before setting
artifactHistoryPending and calling requestAnimationFrame so
refetchArtifactHistory is only queued for subsequent updates, not the initial
session change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app/src/pages/session.tsx`:
- Around line 79-88: The effect currently reads params.id and
searchParams.prompt inside untrack(), which prevents createEffect from tracking
them as dependencies and breaks later prefill behavior; update the createEffect
so that params.id and searchParams.prompt are read outside of untrack (or
assigned to local constants before calling untrack) and only the side-effectful
calls (prompt.set and setSearchParams) run inside untrack; in other words,
ensure createEffect explicitly depends on params.id and searchParams.prompt by
referencing them before the untrack call and then call prompt.set(...) and
setSearchParams(...) inside untrack to perform the updates.

In `@packages/app/src/pages/session/session-main-view.tsx`:
- Around line 86-124: The fallback Match currently uses when={true} which
renders NewSessionView even while a session route is active but
timelineSessionID is still resolving; change that Match to only render when
there is no active session (e.g., when={!props.activeSessionID}) so
NewSessionView does not mount during session switches—update the Match that
renders <NewSessionView composer={props.composerHome} /> accordingly and keep
the existing Show when={props.activeSessionID} for the session composer.

In `@packages/app/src/pages/session/use-session-review-panel.tsx`:
- Around line 78-82: The review panel never sets
tree.activeDiff/tree.pendingDiff, so diff focus/scroll restore never runs; wire
these fields to the currently selected review file by updating the logic that
manages selection (e.g., when input.activeFileTab() changes or during the
review-open flow) to compute the diff id/path via input.file.pathFromTab(...)
and call setTree({ activeDiff: <selectedDiffId>, pendingDiff: <selectedDiffId>
}) (or set pendingDiff first then activeDiff when initiating scroll retry).
Ensure any existing clear-to-undefined paths (where
tree.activeDiff/tree.pendingDiff are reset) still occur only when closing the
review or switching away, and reference the tree and setTree usage in
use-session-review-panel.tsx to attach this assignment where
focusedFile/select-tab changes are handled.

---

Duplicate comments:
In `@packages/app/src/pages/session/use-session-revert.ts`:
- Around line 64-66: The selection of the "next" message must be index-based
rather than by comparing IDs; instead of using
readUserMessages(readSessionMessages(input.sync.data.message[request.sessionID])).find((item)
=> item.id > request.id), first build the array via
readSessionMessages/readUserMessages, locate the index of the message whose id
=== request.id, then pick the element at index+1 (or undefined if out of
bounds). Update the logic around readUserMessages/readSessionMessages and
request.sessionID/request.id to use this index lookup and handle the
no-next-message case gracefully.

In `@packages/app/src/pages/session/use-session-review-state.ts`:
- Around line 142-162: The code schedules a refetch via
queueArtifactHistoryRefetch on session changes even though
createResource(input.sessionID, ...) already fetches artifacts, causing
duplicate requests; update queueArtifactHistoryRefetch (and the places that call
it) to skip scheduling a refetch when the current artifactHistory.sessionID does
not match input.sessionID (meaning the createResource initial load is already
fetching the new session), e.g., check artifactHistory()?.sessionID ===
input.sessionID before setting artifactHistoryPending and calling
requestAnimationFrame so refetchArtifactHistory is only queued for subsequent
updates, not the initial session change.

In `@packages/app/src/pages/session/use-session-timeline-data.ts`:
- Around line 72-77: The visibleUserMessages memo uses lexicographic comparison
(m.id < revert) which fails for non-lexical IDs; instead, compute the index of
the revert message in the timeline returned by userMessages() (e.g., findIndex(m
=> m.id === revert)) and return a slice of userMessages() up to that index (or
the full array if not found), preserving timeline order; update the createMemo
block that references revertMessageID, userMessages, and visibleUserMessages to
use findIndex + slice rather than filter by id comparison.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ac531ec5-38a8-435a-8714-dc3ad88cfd60

📥 Commits

Reviewing files that changed from the base of the PR and between 9bed311 and 1415aae.

📒 Files selected for processing (18)
  • packages/app/e2e/session/session-composer-dock.spec.ts
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/message-timeline.tsx
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-comment-context.test.ts
  • packages/app/src/pages/session/use-session-comment-context.ts
  • packages/app/src/pages/session/use-session-followups.test.ts
  • packages/app/src/pages/session/use-session-new-worktree.ts
  • packages/app/src/pages/session/use-session-revert.test.ts
  • packages/app/src/pages/session/use-session-revert.ts
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/use-session-review-state.ts
  • packages/app/src/pages/session/use-session-route-tabs.ts
  • packages/app/src/pages/session/use-session-scroll-dock.test.ts
  • packages/app/src/pages/session/use-session-scroll-dock.ts
  • packages/app/src/pages/session/use-session-timeline-data.ts
  • packages/app/src/pages/session/use-session-vcs-refresh.ts
  • packages/app/src/shell-frame-contract.test.ts

Comment thread packages/app/src/pages/session.tsx Outdated
Comment thread packages/app/src/pages/session/session-main-view.tsx
Comment thread packages/app/src/pages/session/use-session-review-panel.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app/src/pages/session/use-session-revert.ts`:
- Around line 64-66: The current logic selects the next restore target by
comparing opaque message ids with '>' which assumes lexicographic order; instead
use the ordered timeline returned by readUserMessages(readSessionMessages(...))
and pick the message by position: find the index of the message whose id ===
request.id and take the element at index+1 as the next target (handle not-found
and end-of-array cases). Update the variable construction around
readUserMessages/readSessionMessages/input.sync.data.message[request.sessionID]
and request.id to use findIndex + index+1 rather than an id '>' comparison.
- Around line 46-58: The SDK call input.client.session.revert(request) (and the
other SDK calls in the same file around lines 79-98) must be invoked with
throwOnError: true so HTTP errors are thrown instead of returning { error } and
bypassing the .catch; update the call sites (e.g., where
input.client.session.revert(request) is used) to pass the option (throwOnError:
true) to the generated SDK method so that failures trigger the existing .catch
block and the rollback code (input.roll, input.prompt.set, input.fail) runs as
intended.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b8f01e71-6118-4b91-9436-9490c76903c7

📥 Commits

Reviewing files that changed from the base of the PR and between 1415aae and 48ee1c9.

📒 Files selected for processing (6)
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/review-panel-scroll.ts
  • packages/app/src/pages/session/review-panel-view.tsx
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-revert.ts
  • packages/app/src/pages/session/use-session-review-panel.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: analyze-js-ts
  • GitHub Check: e2e-artifacts
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-desktop
  • GitHub Check: smoke-macos-arm64
🧰 Additional context used
📓 Path-based instructions (1)
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/review-panel-view.tsx
  • packages/app/src/pages/session/review-panel-scroll.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
🧠 Learnings (36)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/patch/index.ts:337-346
Timestamp: 2026-04-28T04:38:05.946Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/patch/index.ts`), the BOM-transition surfacing gap — where `Bom.split` strips BOM before building `unified_diff`/`new_content` but `Bom.join` later re-attaches BOM on disk write, meaning BOM changes are not reflected in the diff payload — is intentionally deferred. PR `#270` is an upstream-sync graft; fixing the issue here would mix refactor + bugfix intents and drift the diff from the upstream baseline needed for clean future grafts. A dedicated follow-up PR (or upstream report) will address this. Do NOT re-flag the missing BOM-change surfacing in `ApplyPatchFileUpdate`/`ApplyPatchFileChange` as a blocking issue in PR `#270` or in future sync PRs that carry the same upstream baseline.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:58.329Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/session/compaction.ts:169-191
Timestamp: 2026-04-28T05:36:22.128Z
Learning: In `packages/opencode/src/session/compaction.ts` (Astro-Han/pawwork), the internal helper `splitTurn` intentionally returns a raw `Effect.gen(...)` rather than being wrapped with `Effect.fnUntraced`. Converting `splitTurn` (and similar plain-Effect.gen internal helpers in this file, e.g. `turns`, `completedCompactions`, `buildPrompt`) to `Effect.fnUntraced` is a repo-wide convention sweep deferred to a dedicated follow-up PR. The same sweep covers `tool/tool.ts` and other helpers flagged in the same sync. Do NOT re-flag the absence of `Effect.fnUntraced` on `splitTurn` or other internal helpers in `compaction.ts` during upstream-sync PRs; flag it only in the dedicated convention-sweep PR.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/app/src/components/dialog-delete-session.tsx:22-31
Timestamp: 2026-04-30T10:26:58.545Z
Learning: In `packages/app/src/components/dialog-delete-session.tsx` (Astro-Han/pawwork, PR `#342`), `handleDelete` uses a `deleting` signal with `try/finally` but intentionally omits an explicit `catch` block. The caller (`deleteSession` in `packages/app/src/pages/layout.tsx` around line 1083) already swallows errors with a toast, so `onConfirm` does not reject in practice. The `try/finally` is sufficient to keep the Delete button usable if a future caller throws. Do NOT re-flag the missing explicit catch in this component.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/session/session-view-controller.test.ts` and related Solid + Bun test files), the Bun + Solid test environment does NOT reliably exercise `createMemo((current) => ...)` signal recomputation the way a browser runtime does. Adding signal-driven transition tests (e.g., using `createSignal` + `createRoot` to flip reactive inputs) is misleading in this environment because the reactive invalidation/recomputation path is not faithfully replicated. The correct strategy is: cover pure transition logic with plain unit tests (e.g., `nextSessionViewState`), and cover the browser reactive path with E2E tests (e.g., session-switch E2E spec). Do NOT re-flag the absence of signal-driven `createSessionViewController` tests in this environment as a gap.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/test/session/subagent-lifecycle-integration.test.ts:41-47
Timestamp: 2026-04-28T11:24:35.312Z
Learning: In `packages/opencode/test/session/subagent-lifecycle-integration.test.ts` (Astro-Han/pawwork, PR `#287`), the test suite intentionally uses a manual `run` helper (`Effect.runPromise(program.pipe(Effect.provide(Layer.mergeAll(SubagentRun.defaultLayer, Session.defaultLayer)), Effect.orDie))`) and raw `bun:test` test cases rather than the `testEffect(...)` harness. All 27 lifecycle tests pass with this pattern. Migration to `testEffect` + `it.live(...)` is deferred to a dedicated test-harness sweep PR to avoid fixture drift before merge. Do NOT re-flag the manual `Effect.runPromise` runner in this file as needing harness migration until that sweep PR lands.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:12.228Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/prompt.ts` and `packages/opencode/src/session/processor.ts`, PR `#264`), the loop-gate race condition between `buildLoopContext()` and `recordSyntheticBlock`/`recordSyntheticStop` is intentionally handled via idempotence guards (re-check sigKey presence / `hasStopped` inside the record helpers) rather than a full per-parent `Effect.Mutex`. Threading a `Map<MessageID, Mutex>` through the processor was considered too large a surface change for this edge case; the residual TOCTOU window only produces extra synthetic parts with no behavioral drift on the "turn ends" contract. A code comment documents the trade-off and points to a full-mutex follow-up if the race is observed in practice. Do NOT re-flag the absence of a per-parent mutex as a blocking issue in future reviews.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/src/tool/agent.ts:89-102
Timestamp: 2026-04-28T13:10:01.345Z
Learning: In `packages/opencode/src/tool/agent.ts` (Astro-Han/pawwork, PR `#287`), `makeReadLastCompletedAssistantText` intentionally scans the full child session message history (no `limit`) to find the latest completed assistant text part for `partial_result` on cancellation. Cancellation is a rare path so the extra read cost is acceptable; tool-heavy children can push the last stable assistant text far back. Do NOT suggest re-adding a fixed small limit (e.g., `limit: 5`) to this function.
📚 Learning: 2026-04-29T13:27:28.494Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/src/pages/session/session-view-controller.test.ts:5-45
Timestamp: 2026-04-29T13:27:28.494Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/session/session-view-controller.test.ts` and related Solid + Bun test files), the Bun + Solid test environment does NOT reliably exercise `createMemo((current) => ...)` signal recomputation the way a browser runtime does. Adding signal-driven transition tests (e.g., using `createSignal` + `createRoot` to flip reactive inputs) is misleading in this environment because the reactive invalidation/recomputation path is not faithfully replicated. The correct strategy is: cover pure transition logic with plain unit tests (e.g., `nextSessionViewState`), and cover the browser reactive path with E2E tests (e.g., session-switch E2E spec). Do NOT re-flag the absence of signal-driven `createSessionViewController` tests in this environment as a gap.

Applied to files:

  • packages/app/src/pages/session/review-panel-view.tsx
  • packages/app/src/pages/session/review-panel-scroll.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-30T14:25:19.902Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 346
File: packages/app/src/pages/session.tsx:2071-2071
Timestamp: 2026-04-30T14:25:19.902Z
Learning: In `packages/app/src/pages/session.tsx` (Astro-Han/pawwork), the session-panel container uses `min-w-[24rem]` unconditionally (no `md:` prefix). This is intentional: PawWork is Electron-only and PR `#346` raises `minWidth` in `packages/desktop-electron/src/main/windows.ts` from 480 → 768px, which matches the Tailwind `md` breakpoint. Windows narrower than 768px are unreachable in production, so there is no narrow-viewport flex-col case to guard against. Do NOT suggest scoping this `min-w-[24rem]` to `md:` in future reviews.

Applied to files:

  • packages/app/src/pages/session/review-panel-view.tsx
  • packages/app/src/pages/session/review-panel-scroll.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/review-panel-view.tsx
  • packages/app/src/pages/session/review-panel-scroll.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/review-panel-view.tsx
  • packages/app/src/pages/session/review-panel-scroll.ts
  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-29T13:27:25.687Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 329
File: packages/app/e2e/session/session-scroll-position.spec.ts:171-230
Timestamp: 2026-04-29T13:27:25.687Z
Learning: In `packages/app/e2e/session/session-scroll-position.spec.ts`, the `describe()` formatter is intentionally duplicated in `installPageErrorProbe` (inside `page.addInitScript`, runs in browser context) and `collectPageErrors` (runs in Node.js/Playwright context). These are separate execution contexts — `addInitScript` serializes its closure to a string for injection — so extracting a shared helper would add cross-context indirection. Do NOT flag this duplication as a maintenance issue.

Applied to files:

  • packages/app/src/pages/session/review-panel-scroll.ts
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-30T10:41:33.826Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/desktop-electron/src/main/ipc.ts:342-344
Timestamp: 2026-04-30T10:41:33.826Z
Learning: In Astro-Han/pawwork, the `export-session` IPC handler in `packages/desktop-electron/src/main/ipc.ts` accepts an optional localized `title` argument for `dialog.showSaveDialog`, keeping `"Export session"` as an English fallback. The localized title is plumbed from the renderer through `platform.exportSession` → `window.api.exportSession` → preload (`preload/types.ts`, `preload/index.ts`, `renderer/index.tsx`) → main, mirroring the same shape as the `save-file-picker` handler. Both renderer callsites (`layout.tsx` and `message-timeline.tsx`) pass `language.t("session.export.action.export")`. Do NOT flag the hard-coded fallback as unlocalized — the renderer is responsible for supplying the localized title.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
📚 Learning: 2026-04-27T10:33:12.228Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/prompt.ts:108-169
Timestamp: 2026-04-27T10:33:12.228Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/prompt.ts` and `packages/opencode/src/session/processor.ts`, PR `#264`), the loop-gate race condition between `buildLoopContext()` and `recordSyntheticBlock`/`recordSyntheticStop` is intentionally handled via idempotence guards (re-check sigKey presence / `hasStopped` inside the record helpers) rather than a full per-parent `Effect.Mutex`. Threading a `Map<MessageID, Mutex>` through the processor was considered too large a surface change for this edge case; the residual TOCTOU window only produces extra synthetic parts with no behavioral drift on the "turn ends" contract. A code comment documents the trade-off and points to a full-mutex follow-up if the race is observed in practice. Do NOT re-flag the absence of a per-parent mutex as a blocking issue in future reviews.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-26T16:34:57.130Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 247
File: packages/ui/src/components/message-part.tsx:1322-1324
Timestamp: 2026-04-26T16:34:57.130Z
Learning: In Astro-Han/pawwork (`packages/ui/src/components/message-part.tsx`), the `taskId` createMemo and `childSessionId` createMemo both intentionally read only from `partMetadata().sessionId` (populated post-execution), not from `input.task_id` / `input.subagent_session_id`. This has always been the case — the original code never read the input field either. Adding an `input.subagent_session_id` fallback would be a new capability, not a bug fix. Do NOT flag the absence of this fallback as a regression in PR `#247` or future PRs unless there is a concrete case where metadata is not populated.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-28T04:38:11.771Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/tool/agent.ts:23-27
Timestamp: 2026-04-28T04:38:11.771Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/tool/agent.ts`), the `subagent_session_id` field in the `Parameters` schema accepts any `Schema.String` rather than a branded `SessionID`. This is inherited upstream behavior (adopted in PR `#270`, an upstream-sync graft of upstream PR `#23244`). The fix — validating `subagent_session_id` as a `SessionID` brand up front so malformed/typo'd IDs fail explicitly rather than silently forking a new subagent session — is intentionally deferred to a follow-up PR or upstream report. Do NOT re-flag this as a blocking issue in PR `#270` or in future upstream-sync PRs that carry the same schema; flag it only in a PawWork-authored PR that touches `agent.ts` parameter validation.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-24T05:39:58.329Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:58.329Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T11:24:35.312Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/test/session/subagent-lifecycle-integration.test.ts:41-47
Timestamp: 2026-04-28T11:24:35.312Z
Learning: In `packages/opencode/test/session/subagent-lifecycle-integration.test.ts` (Astro-Han/pawwork, PR `#287`), the test suite intentionally uses a manual `run` helper (`Effect.runPromise(program.pipe(Effect.provide(Layer.mergeAll(SubagentRun.defaultLayer, Session.defaultLayer)), Effect.orDie))`) and raw `bun:test` test cases rather than the `testEffect(...)` harness. All 27 lifecycle tests pass with this pattern. Migration to `testEffect` + `it.live(...)` is deferred to a dedicated test-harness sweep PR to avoid fixture drift before merge. Do NOT re-flag the manual `Effect.runPromise` runner in this file as needing harness migration until that sweep PR lands.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-30T10:26:58.545Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 342
File: packages/app/src/components/dialog-delete-session.tsx:22-31
Timestamp: 2026-04-30T10:26:58.545Z
Learning: In `packages/app/src/components/dialog-delete-session.tsx` (Astro-Han/pawwork, PR `#342`), `handleDelete` uses a `deleting` signal with `try/finally` but intentionally omits an explicit `catch` block. The caller (`deleteSession` in `packages/app/src/pages/layout.tsx` around line 1083) already swallows errors with a toast, so `onConfirm` does not reject in practice. The `try/finally` is sufficient to keep the Delete button usable if a future caller throws. Do NOT re-flag the missing explicit catch in this component.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-28T12:01:16.559Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/src/session/session.ts:518-542
Timestamp: 2026-04-28T12:01:16.559Z
Learning: In `packages/opencode/src/session/session.ts` (Astro-Han/pawwork, PR `#287`), the `updatePart` subtask guard intentionally allows first writes (where `existing === undefined`) to pass through without calling `lifecycleFieldsChanged`. This is required for `Session.fork()`, migration, and import paths that replay historical `SubtaskPart` rows via `updatePart()` outside any `SubagentRunWriterContext`. Only mutations of an already-persisted row (`existing` is defined) are policed. Do NOT suggest adding a lifecycle check on first-write in this guard.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-24T03:51:56.211Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:56.211Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/app/src/pages/session/session-main-view.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-28T05:36:22.128Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/session/compaction.ts:169-191
Timestamp: 2026-04-28T05:36:22.128Z
Learning: In `packages/opencode/src/session/compaction.ts` (Astro-Han/pawwork), the internal helper `splitTurn` intentionally returns a raw `Effect.gen(...)` rather than being wrapped with `Effect.fnUntraced`. Converting `splitTurn` (and similar plain-Effect.gen internal helpers in this file, e.g. `turns`, `completedCompactions`, `buildPrompt`) to `Effect.fnUntraced` is a repo-wide convention sweep deferred to a dedicated follow-up PR. The same sweep covers `tool/tool.ts` and other helpers flagged in the same sync. Do NOT re-flag the absence of `Effect.fnUntraced` on `splitTurn` or other internal helpers in `compaction.ts` during upstream-sync PRs; flag it only in the dedicated convention-sweep PR.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-28T08:29:02.858Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/file/ripgrep.ts:469-470
Timestamp: 2026-04-28T08:29:02.858Z
Learning: In `packages/opencode/src/file/ripgrep.ts` (Astro-Han/pawwork, PR `#270`), the `tree()` function filters files with `if (file.includes(".opencode")) continue`, which is a raw substring match that would also drop unrelated paths whose names merely contain `.opencode` (e.g., `.opencode-backup`). The correct fix is `if (file.split(path.sep).includes(".opencode")) continue` to match only the exact path segment. However, upstream `dev:packages/opencode/src/file/ripgrep.ts` carries the identical false-positive filter. PR `#270` is an upstream-sync graft; fixing it here would diverge from the baseline. The fix is deferred to a PawWork ripgrep filter cleanup PR or an upstream-first fix, to land alongside the other ripgrep deferrals (Schema.Class, bytes-union, abort short-circuit, mixed-separator). Do NOT re-flag the `.opencode` substring filter in upstream-sync PRs.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
📚 Learning: 2026-04-28T04:38:21.935Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/test/file/ripgrep.test.ts:9-10
Timestamp: 2026-04-28T04:38:21.935Z
Learning: In `packages/opencode/test/file/ripgrep.test.ts` (Astro-Han/pawwork, PR `#270`), the tests intentionally use a local `run` helper (`effect.pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise)`) rather than the `testEffect(...)` harness. This file was adopted from upstream wholesale as part of an upstream-sync graft; migrating to `testEffect` + `it.live(...)` was deferred to a separate follow-up PR to avoid mixing refactor and harness-migration intents within the sync. Do NOT re-flag the local `run` wrapper as needing harness migration until that follow-up lands.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T18:53:46.234Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 308
File: packages/opencode/test/server/global-session-list.test.ts:224-259
Timestamp: 2026-04-28T18:53:46.234Z
Learning: In `packages/opencode/test/server/global-session-list.test.ts` (Astro-Han/pawwork), all `it.live(...)` test bodies intentionally use `Effect.promise(async () => { ... })` rather than `Effect.gen(function* () { ... })`. This is the established local convention for the route/pagination tests in this file (e.g., "supports cursor pagination", "session route orders by creation time", "session routes omit undefined optional fields"). Do NOT flag individual `it.live` cases in this file as needing conversion to `Effect.gen`; any harness-style migration would need to cover the entire file.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-28T04:38:05.946Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/patch/index.ts:337-346
Timestamp: 2026-04-28T04:38:05.946Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/patch/index.ts`), the BOM-transition surfacing gap — where `Bom.split` strips BOM before building `unified_diff`/`new_content` but `Bom.join` later re-attaches BOM on disk write, meaning BOM changes are not reflected in the diff payload — is intentionally deferred. PR `#270` is an upstream-sync graft; fixing the issue here would mix refactor + bugfix intents and drift the diff from the upstream baseline needed for clean future grafts. A dedicated follow-up PR (or upstream report) will address this. Do NOT re-flag the missing BOM-change surfacing in `ApplyPatchFileUpdate`/`ApplyPatchFileChange` as a blocking issue in PR `#270` or in future sync PRs that carry the same upstream baseline.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T04:56:18.533Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/test/file/ripgrep.test.ts:172-175
Timestamp: 2026-04-28T04:56:18.533Z
Learning: In `packages/opencode/test/file/ripgrep.test.ts` (Astro-Han/pawwork, PR `#270`), the `files dies on nonexistent directory` test hardcodes `/tmp/nonexistent-dir-12345` as the missing-directory path. This is upstream-inherited behaviour adopted wholesale via the graft strategy (`project_upstream_strategy.md`). The fix — replacing the hardcoded path with a platform-neutral guaranteed-nonexistent child path derived from the `tmpdir` fixture — is intentionally deferred to a follow-up PR or upstream report, to avoid mixing bugfix + refactor intents and drifting the diff from the upstream baseline future syncs need. Do NOT re-flag the hardcoded `/tmp/nonexistent-dir-12345` path in this file as a blocking or actionable issue until the follow-up PR lands.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
📚 Learning: 2026-04-28T04:38:11.727Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/tool/edit.ts:35-45
Timestamp: 2026-04-28T04:38:11.727Z
Learning: In `packages/opencode/src/tool/edit.ts` (Astro-Han/pawwork, PR `#270`), the module-scoped `Map<string, Semaphore.Semaphore>` (`locks`) and `lock(filePath)` helper are intentionally left as a global, never-cleaned registry. PR `#270` is a pure upstream-sync graft and mixing in a bugfix would drift the diff from the upstream baseline. The fix (moving per-file semaphores into `InstanceState`/`ScopedCache` for per-instance lifecycle management) is tracked as a follow-up to land in its own PR or be reported upstream. Do NOT re-flag the absence of InstanceState wiring for the lock map in this file until the follow-up PR is opened.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T07:27:59.666Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/file/ripgrep.ts:364-368
Timestamp: 2026-04-28T07:27:59.666Z
Learning: In `packages/opencode/src/file/ripgrep.ts` (Astro-Han/pawwork, PR `#270`), both `files()` and `search()` do not short-circuit on an already-aborted `AbortSignal` before `spawner.spawn(...)`. The `check(input.cwd)` effect before spawn already provides a natural cancellation point for interrupted fibers. Upstream `dev:packages/opencode/src/file/ripgrep.ts` has identical ordering with no pre-spawn `signal.aborted` guard; adding one would diverge from the graft baseline. The fix (explicit pre-spawn `signal.aborted` check) is deferred to a dedicated PawWork ripgrep cancellation pass or upstream PR. Do NOT re-flag the missing pre-spawn cancellation short-circuit in `files()`/`search()` in upstream-sync PRs.

Applied to files:

  • packages/app/src/pages/session/use-session-review-panel.tsx
📚 Learning: 2026-04-28T13:10:01.345Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 287
File: packages/opencode/src/tool/agent.ts:89-102
Timestamp: 2026-04-28T13:10:01.345Z
Learning: In `packages/opencode/src/tool/agent.ts` (Astro-Han/pawwork, PR `#287`), `makeReadLastCompletedAssistantText` intentionally scans the full child session message history (no `limit`) to find the latest completed assistant text part for `partial_result` on cancellation. Cancellation is a rare path so the extra read cost is acceptable; tool-heavy children can push the last stable assistant text far back. Do NOT suggest re-adding a fixed small limit (e.g., `limit: 5`) to this function.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-27T10:33:02.677Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/src/session/diagnostics.ts:245-252
Timestamp: 2026-04-27T10:33:02.677Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/diagnostics.ts`, PR `#264`), `loopLastError` is stored as raw (unscrubbed) error text in loop metadata intentionally. The trust boundary for sensitive data scrubbing is the renderer (`LoopRenderer.render` already applies `scrubErrorText` before any user/model-facing output), not the storage layer. Failed tool parts also already store raw errors in `state.error` (PR `#204` default behavior), so scrubbing only `loopLastError` would not close that pre-existing leak and would add lossy storage. If export-side hardening is needed, the correct approach is a single sanitizer pass over both `state.error` and metadata in the export layer. Do NOT suggest scrubbing `loopLastError` at the `observeToolError` call site.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-22T09:32:58.310Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/ui/src/theme/context.tsx:11-16
Timestamp: 2026-04-22T09:32:58.310Z
Learning: In Astro-Han/pawwork (`packages/ui/src/theme/context.tsx` and related files), the renaming of localStorage theme keys from `opencode-*` to `pawwork-*` (THEME_ID, COLOR_SCHEME, THEME_CSS_LIGHT, THEME_CSS_DARK) is intentional and should NOT include a migration path from the old keys. Migrating would re-couple PawWork and OpenCode browser storage namespaces, which the PR is explicitly designed to avoid. A reset to the PawWork default theme on upgrade is acceptable by design.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T06:51:54.812Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/tool/todo.ts:9-18
Timestamp: 2026-04-28T06:51:54.812Z
Learning: In `packages/opencode/src/tool/todo.ts` (Astro-Han/pawwork), `TodoItem.status` and `TodoItem.priority` are intentionally declared as plain `Schema.String` rather than closed literal unions. This matches the upstream opencode baseline (`dev:packages/opencode/src/tool/todo.ts`). The tightening — `Schema.Literals(["pending","in_progress","completed","cancelled"])` for `status` and `Schema.Literals(["high","medium","low"])` for `priority` — is tracked as a follow-up under the harness/tool-set-v1 series (issue `#129`) to land either as part of a tool-schema tightening sweep or upstream-first. Do NOT re-flag the free-form strings for these fields in upstream-sync PRs; flag it only in a PawWork-authored PR or the dedicated sweep that touches `TodoItem` schema.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-24T17:08:46.780Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:46.780Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-27T11:18:47.298Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/websearch.test.ts:21-78
Timestamp: 2026-04-27T11:18:47.298Z
Learning: In `packages/opencode/test/tool/websearch.test.ts`, the tests intentionally use manual `Effect.runPromise` with explicit `Effect.provide(...)` chains (including `Layer.succeed(Auth.Service, ...)`, `Layer.succeed(HttpClient.HttpClient, http)`, `WebSearchAuth.layer`, `Truncate.defaultLayer`, and `Agent.defaultLayer`) rather than the `testEffect(...)` harness. This is by design: the fake Auth and HTTP recovery-metadata layers must be explicitly injected and kept visible/scoped at the test site. Do NOT suggest migrating these tests to `testEffect` or removing the manual layer provides.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-28T06:47:20.342Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/tool/question.ts:6-16
Timestamp: 2026-04-28T06:47:20.342Z
Learning: In Astro-Han/pawwork, `packages/opencode/src/tool/question.ts` is a permanent PawWork carve-out: `Question.Prompt` (defined in `packages/opencode/src/question/index.ts`) remains a Zod schema, NOT an Effect Schema. The `ts-expect-error` boundary in `tool/registry.ts` marks the Zod ↔ Effect Schema seam. The dropped `packages/opencode/test/tool/question.test.ts` covered the old Zod-mixed surface. Schema-bound regression tests for this tool are intentionally deferred until `question/index.ts` migrates to Effect Schema in its own dedicated PR. Do NOT request restoring question tool tests in upstream-sync PRs; flag it only in the PR that migrates `question/index.ts` to Effect Schema.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-27T11:19:24.963Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 271
File: packages/opencode/test/tool/websearch-auth.test.ts:0-0
Timestamp: 2026-04-27T11:19:24.963Z
Learning: In `packages/opencode/test/tool/websearch-auth.test.ts` (Astro-Han/pawwork), the tests intentionally use a small local `runWith` runner with raw `bun:test` and `Effect.runPromise` rather than the `testEffect` harness. Each test case injects a custom in-memory `Auth.Service` layer; switching to `testEffect` would be style-only churn without changing risk coverage. Do not flag these tests as needing harness migration.

Applied to files:

  • packages/app/src/pages/session.tsx
📚 Learning: 2026-04-27T12:59:49.844Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 264
File: packages/opencode/test/session/prompt-effect.test.ts:0-0
Timestamp: 2026-04-27T12:59:49.844Z
Learning: In `packages/opencode/test/session/prompt-effect.test.ts` and `packages/opencode/src/session/diagnostics.ts` (PR `#264`), the recovery reminder copy differs between signature kinds: the input-repeat variant says "repeated the same tool input 3 times" (uses a literal count), while the target-repeat variant says "failed against the same target multiple times" (uses "multiple times" with no count). Assertions that check for injected reminder text in LLM inputs must accept both phrasings when a scenario produces both `input:` and `target:` signatures (e.g., `read` tool with a `filePath` parameter). Do NOT narrow the assertion to only the input-variant phrasing.

Applied to files:

  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-28T04:56:13.350Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 270
File: packages/opencode/src/tool/tool.ts:8-10
Timestamp: 2026-04-28T04:56:13.350Z
Learning: In `packages/opencode/src/tool/tool.ts` (Astro-Han/pawwork, PR `#270`), the `Metadata` interface uses `[key: string]: any` as its index signature. This is upstream-inherited code adopted wholesale via the graft strategy (per `project_upstream_strategy.md`). The fix — tightening to `Record<string, unknown>` and explicitly narrowing framework-owned fields like `truncated` — is intentionally deferred to a follow-up PR or upstream report. Do NOT re-flag the `any` index signature on `Metadata` in PR `#270` or in future upstream-sync PRs that carry the same upstream baseline.

Applied to files:

  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-28T01:24:29.645Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 280
File: packages/opencode/src/session/projectors.ts:0-0
Timestamp: 2026-04-28T01:24:29.645Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/session/projectors.ts`), `SyncEvent.Definition.schema` preserves the literal Zod type through `z.infer<Def["schema"]>`, so `data.info` inside a `SyncEvent.project(Session.Event.Created, ...)` callback is already typed as `Session.Info` by the TypeScript compiler. No `as Session.Info` cast or `Session.Info.parse()` call is needed at this persistence boundary — a typecheck-clean removal of any such cast is sufficient.

Applied to files:

  • packages/app/src/pages/session/use-session-revert.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/app/src/pages/session/use-session-revert.ts

Comment thread packages/app/src/pages/session/use-session-revert.ts
Comment thread packages/app/src/pages/session/use-session-revert.ts Outdated
@Astro-Han Astro-Han merged commit 299c1a7 into dev May 1, 2026
27 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request May 1, 2026
11 tasks
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 P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Split session page controllers and fix composer dock scrolling

1 participant