Skip to content

fix(ui): clarify safe-recovery failure and pre-progress turn status (#1358) - #1366

Merged
Astro-Han merged 7 commits into
devfrom
claude/recovery-presentation-1358
Jun 18, 2026
Merged

fix(ui): clarify safe-recovery failure and pre-progress turn status (#1358)#1366
Astro-Han merged 7 commits into
devfrom
claude/recovery-presentation-1358

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

Two presentation fixes for the safe-recovery / pre-progress turn (runtime behavior was already correct). Three independent review passes (Codex + two subagents) drove three further correctness fixes — the last a P1 cross-message topology bug (see Review).

  1. Terminal safe_retry_failed notice — replaced the weak single-line caption with a separated, titled status row (stroked icon + ink title + explanation). Copy adapts to context, driven by a backend-stamped sideEffect flag on the notice:

    • a completed side-effecting tool ran earlier this turn → Action completed / 操作已完成. The external action already landed, so the copy tells the user not to repeat it and points the next step at regenerating the reply (or switching models), never a plain retry: "上一项操作已执行,无需重复。当前网络或模型服务商连接异常,可稍后重新生成回复,或更换模型。"
    • no side-effecting tool (read-only-only, or no tool) → Reply incomplete / 回复未完成, where a plain retry is safe: "当前网络或模型服务商存在连接问题,请稍后重试,或换一个模型。"

    The classification lives in one place on the backend (turnHasCompletedSideEffectRunObservability.toolEffect): bash/apply_patch/unknown count as side effects, the read-only set (read/glob/grep/webfetch/tool_info) does not, erring toward reassurance so a real side effect is never under-warned. The UI just reads part.sideEffect.

  2. Pre-first-provider-progress status — the turn no longer says Thinking / 思考中 while it is still connecting. Split by whether any provider-output part (text / reasoning / tool — the UI mirror of the backend isProviderProgressEvent set) exists yet:

    • none yet → Connecting / 连接中 (data-phase="connecting")
    • first one arrived → Thinking / 思考中 (data-phase="thinking")
      Safe recovery now names the attempt: Recovering... attempt #N / 正在恢复…第 N 次.

Why

#1358: after a side-effecting tool (e.g. a posted GitHub comment) completed, the model continuation failed before first provider progress and safe recovery exhausted its budget. The runtime state was correct, but the UI made it look like the previous tool card was still stuck, and the pre-progress wait was mislabeled "Thinking" even though no provider progress had been observed. The goal is honest, reassuring presentation: say the operation already happened, attribute the failure outside the harness (network / provider), don't nudge a redo of the side effect, and don't render a connection wait as model reasoning.

Cross-message topology (the P1 fix): the turn loop creates a new assistant message per step, so the side-effecting tool completes in one step's message while the trailing safe_retry_failed notice lands on the next (failed-continuation) message. A UI scan of the notice's own message can therefore never see the tool — it would always fall back to the default copy. Only the backend, which can scan the whole turn at notice-write time, can classify this reliably; it stamps sideEffect onto the notice and the UI trusts the field.

A step-start part is intentionally excluded from the "provider started" signal: it can precede the first provider chunk. The assistant message carries zero provider parts until the first progress event creates one, so no backend status change is needed for fix #2.

Related Issue

Closes #1358

Human Review Status

Pending

Review

Three independent review passes (Codex codex exec + two separate subagents), all run against the branch. They converged on three real issues, now fixed:

  • (P1) Side-effect detection could never fire in production: the first implementation scanned the notice's own message in the UI, but the side-effecting tool lives on an earlier assistant message of the turn (one message per step). Moved classification to the backend (turnHasCompletedSideEffect scans the whole turn) and stamped a sideEffect flag on the NoticePart; the UI now reads the field — no useData, no part scan, no READ_ONLY_TOOLS duplication. The recovery snap was rebuilt to the real cross-message topology (tool on message A, notice on message B) to prove it.
  • Side-effect detection over-claimed: the predicate was "any completed tool", so a completed read-only grep falsely showed "操作已完成". Fixed to side-effecting tools only (read-only set excluded), with a read-only column in the snap proving the fallback to "回复未完成".
  • Side-effect copy nudged a redo: it said "请稍后重试" after "操作已完成". Rewritten to "无需重复 … 重新生成回复" (default body keeps the safe retry).
  • (P3) Brittle source-grep test: the contract test asserted the notice's behavior by grepping its source (part().sideEffect, "no useData", "no READ_ONLY_TOOLS") without rendering anything. Converged into a real render (notice-render.test.tsx, the Vite-SSR fixture pattern): the notice is mounted with only the sideEffect flag — no tool part, no DataProvider — and the rendered copy is asserted (true → 操作已完成 no-redo, false/undefined → 回复未完成, plus the zh locale). Because nothing tool-shaped is ever mounted, a correct copy proves the UI reads the field alone and never classifies tools. The contract file keeps only locale-content/parity guards.

All reviewers confirmed the connecting↔thinking equivalence is airtight (multi-step reconnect, reasoning-only, tool-only all correct) and that the 4th #1358 state ("model did not start responding") is adequately carried by the "Reply incomplete" notice. Consciously accepted: zht is not loaded at runtime but kept consistent.

Review Focus

  • turnHasCompletedSideEffect (packages/opencode/src/session/safe-retry-notice.ts): the whole-turn scan, the read-only exclusion, and the "err toward reassurance for unknown tools" choice. Stamped in processor.ts at notice-write time.
  • providerStarted in session-turn.tsx: mirrors the backend isProviderProgressEvent set; excludes step-start.
  • The side-effect vs default copy split (no redo nudge on the side-effect branch).

Risk Notes

Fix #1 now spans backend + SDK + UI: an optional sideEffect field on NoticePart (schema + scoped 1-line SDK regen), a pure backend helper, and the processor stamping it. The field is optional, so existing notices and persisted sessions parse unchanged. No recovery-policy, status-schema, or provider-behavior change. Fix #2 is UI-only. The pre-progress and side-effect signals are validated against the backend's own classifications and exercised by the @smoke W1 hang path, the new backend unit tests, and the recovery snap.

How To Verify

```text
opencode unit (processor-effect end-to-end + message-v2 schema + safe-retry-notice helper): 122 pass, 0 fail
└ safe-retry-notice.test.ts: 6 pass (cross-message side-effect=true, read-only=false, no-tool=false, still-running=false, unknown-tool=true, different-turn=false)
└ processor-effect.test.ts still writes the notice through the real processor path with the new field — green
packages/ui typecheck: clean (tsgo --noEmit)
packages/app typecheck: clean (tsgo -b)
packages/opencode typecheck: clean
packages/ui unit suite: 735 pass, 0 fail (notice-render.test.tsx renders the notice from the sideEffect field alone — no tool/DataProvider — and asserts the en+zh copy per variant; contract file keeps locale-parity guards only)
lint (changed src): 0 errors
e2e @smoke W1 connecting indicator (real session-turn memo, assistant.hang path): 1 passed
snap recovery-presentation: rebuilt to real cross-message topology, 中英对照 (zh + en), 3 scenarios each (side-effect 操作已完成 no-redo / read-only 回复未完成 / no-tool 回复未完成) reviewed
snap turn-status-phase: 连接中 / 思考中 / 正在恢复…第 2 次 reviewed
```

Screenshots or Recordings

recovery-presentation

Checklist

  • Type labelbug
  • Routing labelsapp, ui
  • Priority labelP2
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason>
  • I linked the related issue, or stated in Summary why there is no issue.
  • I described the review focus and any meaningful risks.
  • I replaced the example block in How To Verify with the real verification steps and the key result for each.
  • I did not introduce unrelated refactors, dependencies, or file changes beyond the stated scope. The one generated file (sdk/.../types.gen.ts) is a scoped 1-line regen for the new optional field.
  • (conditional) I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes. Leave unticked only if no platform/packaging surface was touched.
  • (conditional) I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant.
  • I reviewed the final diff for unrelated changes and suspicious dependency changes.
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English.

https://claude.ai/code/session_01UUsFz2KqaDQpF8TzBJoBZF

Summary by CodeRabbit

  • New Features

    • Added "Connecting" status phase displayed before the first provider response, distinct from "Thinking" phase.
    • Enhanced recovery failure UI with structured layout including title, body, and warning icon.
    • Improved retry messaging that reflects attempt count when recovering from failures.
    • Side-effect-aware recovery copy that adapts messaging based on whether a tool has already completed.
  • Tests

    • Added snapshot tests validating session status phase transitions and recovery presentation across languages.

Part of #1358. When safe recovery exhausts its budget after a completed
side-effecting tool (e.g. a posted GitHub comment), the turn previously
ended with a weak grey caption while the prominent item stayed the
completed tool card — reading as if the tool were still stuck.

- notice.tsx renders a separated, titled status row (stroked icon + ink
  title + explanation), not the old single-line caption. No semantic
  colour flood: the operation usually succeeded, so it must not read as
  an error.
- Copy adapts to context: a completed tool earlier in the turn means an
  external side effect already landed, so the notice reassures it is done
  ("Action completed" / 操作已完成) and does not nudge a redo; otherwise
  the reply simply never started ("Reply incomplete" / 回复未完成). Both
  attribute the failure to the network or model provider (not PawWork)
  and give a next step (retry / switch models).
- i18n: split the flat safeRetryFailed key into sideEffect/default
  title+body for en/zh/zht; drop the stale flat key from the dormant
  locales (runtime loads en + zh, others fall back to en).
- session-safe-retry-contract.test.ts rewritten to lock the new
  presentation and copy intent.
- recovery-presentation snap (fixture + target) renders both cases
  through the real AssistantParts → tool card + notice pipeline as
  durable regression coverage.

Visual check: bun run snap recovery-presentation
(docs/design/preview/screenshots/recovery-presentation.png).

Claude-Session: https://claude.ai/code/session_01UUsFz2KqaDQpF8TzBJoBZF
Before the provider sends its first chunk, the turn showed "Thinking" /
"思考中" even though the model had not started responding — building the
request, connecting, or waiting for the stream to be accepted all read as
reasoning (#1358).

Split the turn status by whether any provider-output part (text / reasoning /
tool — the UI mirror of the backend isProviderProgressEvent set) exists yet:
- no such part: "Connecting" / "连接中" (data-phase="connecting")
- after the first one: "Thinking" / "思考中" (data-phase="thinking")
A step-start part is intentionally excluded; it can precede the first provider
chunk and would otherwise make a connection wait read as reasoning.

Safe recovery now names the retry attempt ("Recovering... attempt #N" /
"正在恢复…第 N 次") so the row reads as model-recovery progress, not a stuck
tool. Terminal failure before first progress is already covered by the
safe_retry_failed notice ("Reply incomplete" / "回复未完成").

UI-only: the assistant message carries zero parts until the first provider
progress event creates one, so no backend status/schema change is needed.

Verify:
- packages/ui: typecheck, full unit suite (732 pass), lint clean
- packages/app: typecheck
- e2e @smoke W1 connecting indicator (real session-turn memo, hang path) passed
- snap turn-status-phase grid reviewed (连接中 / 思考中 / 正在恢复…第 2 次)

Claude-Session: https://claude.ai/code/session_01UUsFz2KqaDQpF8TzBJoBZF
@Astro-Han Astro-Han added bug Something isn't working ui Design system and user interface app Application behavior and product flows P2 Medium priority labels Jun 18, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested priority: P2 (includes non-doc, non-test paths outside the low-risk bucket).

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

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR splits the session turn "connecting" phase from "thinking" by adding a providerStarted computed signal and data-phase attribute. It also adds a backend sideEffect flag to the safe_retry_failed notice part (computed by scanning sibling messages for completed unsafe tools), redesigns the notice component with variant-aware structured slots, updates session-retry to show attempt counts, adds status.connecting/recoveryAttempt i18n keys to primary locales, removes the flat legacy safeRetryFailed key from all other 13 locale files, and adds contract tests, snap fixtures, and Playwright snap tests to cover all new states.

Changes

Connecting Phase Split + Side-Effect-Aware Safe-Retry Notice

Layer / File(s) Summary
NoticePart sideEffect schema extension
packages/opencode/src/session/message-v2.ts
Adds optional sideEffect: boolean to the NoticePart zod schema as the data contract for the processor and UI.
Backend sideEffect computation and notice writing
packages/opencode/src/session/safe-retry-notice.ts, packages/opencode/src/session/safe-retry-notice.test.ts, packages/opencode/src/session/processor.ts
Adds turnHasCompletedSideEffect to scan sibling messages for completed unsafe tools; wires it into writeSafeRetryFailedNotice to persist the field; covered by a Bun test suite.
SessionTurn connecting vs thinking phase split
packages/ui/src/components/session-turn.tsx, packages/ui/src/i18n/en.ts, packages/ui/src/i18n/zh.ts, packages/ui/src/i18n/zht.ts
Adds providerStarted memo; updates session-turn-thinking to expose data-phase and switch shimmer label between "connecting" and "thinking"; adds status.connecting i18n key to en/zh/zht.
Notice component redesign for safe_retry_failed
packages/ui/src/components/message-part/parts/notice.tsx, packages/ui/src/components/message-part/parts/notice.css
Replaces single-caption rendering with a structured warning-icon + title/body slot layout; selects sideEffect vs default i18n copy variants; adds CSS slot rules.
SessionRetry attempt-count label
packages/ui/src/components/session-retry.tsx
Conditionally shows recoveryAttempt label with attempt number when attempt > 0, falling back to the generic recovery label.
i18n: new keys in en/zh/zht; legacy key removal across all locales
packages/ui/src/i18n/en.ts, packages/ui/src/i18n/zh.ts, packages/ui/src/i18n/zht.ts, packages/ui/src/i18n/ar.ts, packages/ui/src/i18n/br.ts, packages/ui/src/i18n/bs.ts, packages/ui/src/i18n/da.ts, packages/ui/src/i18n/de.ts, packages/ui/src/i18n/es.ts, packages/ui/src/i18n/fr.ts, packages/ui/src/i18n/ja.ts, packages/ui/src/i18n/ko.ts, packages/ui/src/i18n/no.ts, packages/ui/src/i18n/pl.ts, packages/ui/src/i18n/ru.ts, packages/ui/src/i18n/th.ts, packages/ui/src/i18n/tr.ts
Adds retry.recoveryAttempt, structured notice.safeRetryFailed.sideEffect.* / .default.* keys and status.connecting to en/zh/zht; removes the flat legacy ui.sessionTurn.notice.safeRetryFailed key from all 13 other locales.
UI contract and thinking-phase contract tests
packages/ui/src/components/session-safe-retry-contract.test.ts, packages/ui/src/components/session-thinking-phase-contract.test.ts
Replaces old safe-retry assertions with tests for titled notice structure, sideEffect copy variants, locale coverage, and legacy key removal; adds new tests asserting connecting/thinking phase split and recoveryAttempt label.
Snap fixtures for turn-status phases and recovery presentation
packages/app/e2e/snap/fixtures/turn-status-phase-snap-fixture.tsx, packages/app/e2e/snap/fixtures/recovery-presentation-snap-fixture.tsx
Adds two SolidJS snap fixtures rendering connecting/thinking/recovery states and side-effect/read-only/no-tool notice scenarios across Chinese and English.
Playwright snap tests
packages/app/e2e/snap/turn-status-phase.snap.ts, packages/app/e2e/snap/recovery-presentation.snap.ts
Adds two Playwright snapshot tests that mount fixtures, assert phase/text/visibility for each scenario, compose screenshot grids, and print output paths.
W1 e2e contract and smoke-tagging updates
packages/app/e2e/session/session-w1-contracts.spec.ts, packages/opencode/test/config/e2e-smoke-tagging.test.ts
Updates W1 hung-reply test to assert data-phase="connecting" and verify thinking-phase element count is zero; reconciles smoke-tagging inventory.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #1358 — [Bug] Safe recovery failure looks like the previous tool is still stuck: This PR directly implements the suggested fixes: splitting "connecting" from "thinking" in the turn UI, adding sideEffect-aware copy to the safe_retry_failed notice, showing recovery attempt counts, and adding coverage for the completed-tool + safe-retry-failed UX sequence.

Possibly related PRs

  • Astro-Han/pawwork#922: Modifies the same safe-retry/processor flow around attempt-scoped retry gating; this PR adds the sideEffect-driven notice on top of that same code path.
  • Astro-Han/pawwork#1008: Changes the safe-recovery retry scheduling/budget in processor.ts; this PR extends the same writeSafeRetryFailedNotice function to persist sideEffect.
  • Astro-Han/pawwork#1125: Both PRs modify session-w1-contracts.spec.ts around the thinking/shimmer state during hung/no-progress turns.

Poem

🐇 Hop hop, the bunny checks the wire,
"Connecting!" it chirps — no model on fire.
A tool ran through, the side effect's done,
No need to redo what was already won.
The notice now speaks with a title and care:
"Recovery failed — but your comment's still there!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.83% 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 accurately reflects the main changes: fixing UI presentation for safe-recovery failure and pre-progress turn status, matching the core objectives.
Linked Issues check ✅ Passed All requirements from issue #1358 are met: terminal safe_retry_failed notice redesigned with side-effect-aware copy, pre-first-provider-progress status split into connecting/thinking phases, recovery shown as recovery attempt, and test coverage added.
Out of Scope Changes check ✅ Passed All file changes are scoped to the stated objectives: backend side-effect detection, UI notice/turn redesign, i18n updates, snapshot tests, and contract tests. No unrelated refactors or unexpected dependencies.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all required sections from the template including Summary, Why, Related Issue, Human Review Status, Review Focus, Risk Notes, How to Verify, Screenshots, and a complete Checklist.

✏️ 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 claude/recovery-presentation-1358

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

❤️ Share

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

…pleted"

Independent review (#1358) found the `afterToolRun` predicate over-claimed:
any completed tool — including a read-only read/glob/grep/webfetch/tool_info —
flipped the notice to "Action completed / 操作已完成". So a turn that only ran a
grep before the stream dropped would falsely assert an external action landed,
the exact honesty defect the side-effect copy exists to avoid.

Gate on side-effecting tools only: exclude the backend READ_ONLY_TOOLS set
(mirrors run-observability/sanitize.ts). Everything else — bash, apply_patch,
or an unknown/custom tool — still counts, erring toward reassurance so a real
side effect is never under-warned (a redo would repeat it, which #1358 calls
out). Renamed the memo to `afterSideEffectingTool` for accuracy.

The recovery-presentation snap gains a read-only column (completed grep) that
proves the notice falls back to the default "回复未完成", not "操作已完成".

Verify:
- packages/ui: typecheck clean, full unit suite 732 pass, lint clean
- packages/app: typecheck clean
- snap recovery-presentation: 3 columns reviewed (side-effect 操作已完成 /
  read-only 回复未完成 / no-tool 回复未完成)

Claude-Session: https://claude.ai/code/session_01UUsFz2KqaDQpF8TzBJoBZF
Independent review (#1358) found the side-effect body said "Try again later /
请稍后重试" right after "the previous action already went through". After a
real side effect, that nudges the user to repeat it (re-comment, re-run a
command) — the exact thing #1358 says to avoid.

Side-effect copy now says the action already ran and must not be repeated, and
points the next step at regenerating the *reply* (or switching models), never a
plain retry. The default body is unchanged: nothing landed there, so "请稍后重试"
stays safe.

zh copy is the user's wording; en/zht mirror it. Full-width CJK punctuation.

Verify:
- packages/ui: typecheck clean, full unit suite 732 pass (contract test now
  locks the no-redo side-effect copy and the safe default retry)
- snap recovery-presentation: side-effect column reads "无需重复 … 可稍后重新生成回复"

Claude-Session: https://claude.ai/code/session_01UUsFz2KqaDQpF8TzBJoBZF
… test

The e2e-smoke-tagging inventory locks the exact set of @smoke test titles.
Renaming the W1 test ("thinking" → "connecting indicator", #1358) left the
inventory stale, failing unit-opencode (and the unit-windows-opencode-* and the
`check` aggregate gate). Updated the entry to the new title and kept the list
in sorted order (connecting sorts before rendered).

Verify: packages/opencode `bun test test/config/e2e-smoke-tagging.test.ts` —
2 pass, 0 fail.

Claude-Session: https://claude.ai/code/session_01UUsFz2KqaDQpF8TzBJoBZF
@github-actions github-actions Bot added the harness Model harness, prompts, tool descriptions, and session mechanics label Jun 18, 2026
…1358)

The post-tool model continuation runs as a NEW assistant message (the turn
loop creates one message per step), so a completed side-effecting tool and
the trailing safe_retry_failed notice land on DIFFERENT messages of the same
turn. The UI sees one part at a time and cannot scan sibling messages, so the
earlier UI-side scan of the notice's own message could never see the tool —
it would always fall back to the default copy even after bash/apply_patch ran.

Make the backend the single source of truth: when writing the notice, scan the
whole turn via turnHasCompletedSideEffect() and stamp a `sideEffect` flag on
the NoticePart. The UI now just reads the field — no useData, no part scan, no
READ_ONLY_TOOLS duplication. Side-effect classification stays in one place
(RunObservability.toolEffect): bash/apply_patch/unknown count as side effects,
read-only tools do not.

- opencode: new pure helper safe-retry-notice.ts (+6 unit tests covering the
  cross-message, read-only, no-tool, still-running, unknown-tool, and
  different-turn cases); processor stamps sideEffect; NoticePart schema gains
  the optional field.
- sdk: regenerate NoticePart.sideEffect (scoped 1-line).
- ui: notice.tsx reads part().sideEffect; contract test asserts the field
  drives the copy and the old UI scan is gone.
- snap: recovery-presentation rebuilt as a real cross-message topology, zh/en
  side by side (中英对照).

Claude-Session: https://claude.ai/code/session_01UUsFz2KqaDQpF8TzBJoBZF

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/app/e2e/snap/recovery-presentation.snap.ts (1)

14-19: 💤 Low value

Consider extracting the repeated 30-second timeout to a named constant.

The timeout value 30_000 appears four times across the helper functions and assertions. Extracting it would improve maintainability if the timeout needs adjustment in the future.

♻️ Optional refactor
+const SNAPSHOT_READY_TIMEOUT = 30_000
+
 async function waitForThemeBoot(page: Page): Promise<void> {
   await page.waitForFunction(
     () => getComputedStyle(document.documentElement).getPropertyValue("--bg-base").trim().length > 0,
     null,
-    { timeout: 30_000 },
+    { timeout: SNAPSHOT_READY_TIMEOUT },
   )
 }
 
 async function capture(name: string, block: Locator): Promise<Shot> {
-  await expect(block).toBeVisible({ timeout: 30_000 })
+  await expect(block).toBeVisible({ timeout: SNAPSHOT_READY_TIMEOUT })
   return { name, buf: await block.screenshot() }
 }

Then use SNAPSHOT_READY_TIMEOUT in the remaining assertions on lines 44 and 61.

Also applies to: 44-61

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

In `@packages/app/e2e/snap/recovery-presentation.snap.ts` around lines 14 - 19,
Extract the hardcoded timeout value 30_000 into a named constant at the top of
the file (for example, SNAPSHOT_READY_TIMEOUT) and replace all four occurrences
of the timeout value throughout the helper functions (in the capture function's
expect call and in other timeout assertions) with this constant reference. This
will make the code more maintainable and allow for easier adjustments to the
timeout value in the future.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/app/e2e/snap/recovery-presentation.snap.ts`:
- Around line 14-19: Extract the hardcoded timeout value 30_000 into a named
constant at the top of the file (for example, SNAPSHOT_READY_TIMEOUT) and
replace all four occurrences of the timeout value throughout the helper
functions (in the capture function's expect call and in other timeout
assertions) with this constant reference. This will make the code more
maintainable and allow for easier adjustments to the timeout value in the
future.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de903660-6aa8-4575-a019-6c129425c95a

📥 Commits

Reviewing files that changed from the base of the PR and between 6c8519d and bd9d505.

⛔ Files ignored due to path filters (1)
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (33)
  • packages/app/e2e/session/session-w1-contracts.spec.ts
  • packages/app/e2e/snap/fixtures/recovery-presentation-snap-fixture.tsx
  • packages/app/e2e/snap/fixtures/turn-status-phase-snap-fixture.tsx
  • packages/app/e2e/snap/recovery-presentation.snap.ts
  • packages/app/e2e/snap/turn-status-phase.snap.ts
  • packages/opencode/src/session/message-v2.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/safe-retry-notice.test.ts
  • packages/opencode/src/session/safe-retry-notice.ts
  • packages/opencode/test/config/e2e-smoke-tagging.test.ts
  • packages/ui/src/components/message-part/parts/notice.css
  • packages/ui/src/components/message-part/parts/notice.tsx
  • packages/ui/src/components/session-retry.tsx
  • packages/ui/src/components/session-safe-retry-contract.test.ts
  • packages/ui/src/components/session-thinking-phase-contract.test.ts
  • packages/ui/src/components/session-turn.tsx
  • packages/ui/src/i18n/ar.ts
  • packages/ui/src/i18n/br.ts
  • packages/ui/src/i18n/bs.ts
  • packages/ui/src/i18n/da.ts
  • packages/ui/src/i18n/de.ts
  • packages/ui/src/i18n/en.ts
  • packages/ui/src/i18n/es.ts
  • packages/ui/src/i18n/fr.ts
  • packages/ui/src/i18n/ja.ts
  • packages/ui/src/i18n/ko.ts
  • packages/ui/src/i18n/no.ts
  • packages/ui/src/i18n/pl.ts
  • packages/ui/src/i18n/ru.ts
  • packages/ui/src/i18n/th.ts
  • packages/ui/src/i18n/tr.ts
  • packages/ui/src/i18n/zh.ts
  • packages/ui/src/i18n/zht.ts
💤 Files with no reviewable changes (14)
  • packages/ui/src/i18n/pl.ts
  • packages/ui/src/i18n/th.ts
  • packages/ui/src/i18n/ko.ts
  • packages/ui/src/i18n/es.ts
  • packages/ui/src/i18n/no.ts
  • packages/ui/src/i18n/ja.ts
  • packages/ui/src/i18n/ru.ts
  • packages/ui/src/i18n/tr.ts
  • packages/ui/src/i18n/ar.ts
  • packages/ui/src/i18n/de.ts
  • packages/ui/src/i18n/da.ts
  • packages/ui/src/i18n/fr.ts
  • packages/ui/src/i18n/bs.ts
  • packages/ui/src/i18n/br.ts

… grep (#1358)

Address the P3 review note: the contract test asserted the notice's behavior by
grepping its source for `part().sideEffect`, "no useData", "no READ_ONLY_TOOLS",
etc. — brittle, and it never actually rendered anything.

Replace that block with a real render (the repo's Vite-SSR fixture pattern):
mount the notice with ONLY the backend `sideEffect` flag — no tool part, no
DataProvider, no turn context — and assert the rendered copy:
- sideEffect=true  → "Action completed" / 操作已完成, variant=side-effect, no redo
- sideEffect=false → "Reply incomplete" / 回复未完成, variant=default
- sideEffect=undefined (older notices) → safe default
Because nothing tool-shaped is ever mounted, a correct copy proves the UI reads
the field alone and never scans or classifies tools. The same render covers the
zh locale, so a copy regression in either variant fails the test.

The contract file keeps only the locale-content guards (no-redo wording, en/zh/
zht parity, old flat key removed) and the retry status-row check; the brittle
component source grep is gone.

Claude-Session: https://claude.ai/code/session_01UUsFz2KqaDQpF8TzBJoBZF
@Astro-Han
Astro-Han merged commit e441480 into dev Jun 18, 2026
40 checks passed
@Astro-Han
Astro-Han deleted the claude/recovery-presentation-1358 branch August 21, 2026 00:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows bug Something isn't working harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Safe recovery failure looks like the previous tool is still stuck

1 participant