Skip to content

feat(web): show a fork preview dialog before forking a conversation - #1673

Open
junmo-kim wants to merge 6 commits into
tiann:mainfrom
junmo-kim:feat/fork-preview-dialog
Open

feat(web): show a fork preview dialog before forking a conversation#1673
junmo-kim wants to merge 6 commits into
tiann:mainfrom
junmo-kim:feat/fork-preview-dialog

Conversation

@junmo-kim

Copy link
Copy Markdown
Contributor

Summary

Forking a conversation currently gives no indication of what will happen:
which messages stay in the original session and where the new session
starts. This adds a confirmation dialog that renders the transcript
boundary before the fork executes.

  • Selecting Fork on any message now opens a preview dialog instead of
    forking immediately.
  • The dialog shows the last turns kept by the original session above a
    highlighted "Fork point" divider, and the selected message quoted as the
    first message of the new session below it. The boundary semantics mirror
    the hub's existing prefix selection (selectForkTranscriptPrefix).
  • Confirm runs the existing fork flow; cancel leaves the session untouched.
  • Localized (en / zh-CN). Rewind behavior is unchanged.
Desktop Mobile
desktop mobile

Testing

  • New unit tests: web/src/lib/forkPreview.test.ts (boundary slicing,
    turn merging/truncation, current-fork semantics) — 5 cases.
  • New e2e: e2e/fork-preview.spec.ts drives the production
    ForkPreviewDialog via the standalone fixture page (scratchlist
    pattern): rendering, boundary marker, cancel/confirm callbacks, zh-CN
    localization. RED was verified before the implementation commit.
  • Full suite green locally: web vitest 2850 passed, root bun run test
    pass, bun typecheck clean, Playwright 52 passed.

Fork now opens a confirm dialog that renders the transcript boundary:
kept turns above the fork point, the selected message starting the new
session below it. Confirming executes the existing fork flow; cancel
leaves the session untouched.
@junmo-kim
junmo-kim marked this pull request as ready for review August 23, 2026 07:10

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

Findings

  • [Major] Fork still requires the old generic confirmation before the new preview opens - the added handler only runs after MessageActions confirms, producing two dialogs. Evidence: web/src/components/SessionChat.tsx:584. Suggested fix: invoke the preview callback directly from the Fork action and remove the fork-specific generic ConfirmDialog.
  • [Major] The preview reverses historical-fork semantics - the hub and native Codex fork exclude the selected boundary, but the dialog says the new session starts with it. Evidence: web/src/components/AssistantChat/ForkPreviewDialog.tsx:51. Suggested fix: label the prefix as copied into the child and the selected message as excluded/the cutoff.
  • [Major] Pending queued rows are rendered as part of the fork preview even though the hub never copies invokedAt == null rows. Evidence: web/src/lib/forkPreview.ts:55. Suggested fix: skip blocks whose invokedAt === null and add a queued-row test.

Summary

Review mode: initial

Three Major issues found in the confirmation flow and preview/backend semantic parity.

Testing

Not run (automation; PR-provided code was not executed). Coverage gap: no integration test enters the preview through MessageActions, and no preview test covers queued/uninvoked rows.

HAPI Bot

Comment thread web/src/components/SessionChat.tsx
Comment thread web/src/components/AssistantChat/ForkPreviewDialog.tsx Outdated
Comment thread web/src/lib/forkPreview.ts
… semantics

- Open the preview directly from the Fork action; drop MessageActions'
  fork ConfirmDialog so the preview is a single confirmation.
- Correct boundary wording: the hub copies the prefix before the selected
  message into the child, so label kept turns as copied and the selected
  message as excluded.
- Skip never-invoked (queued) user blocks in the preview, matching
  selectForkTranscriptPrefix's invokedAt filter.

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

Findings

  • [Major] Failed fork requests close the preview before completion and surface no error — setForkPreviewRequest(null) unmounts the dialog before the API call, while the click handler discards the returned promise. The added pending UI therefore never renders, and backend/network failures become an unhandled rejection with no retry context. Evidence: web/src/components/SessionChat.tsx:569.
    Suggested fix:

    const executeForkConversation = useCallback(async (messageLocalId?: string) => {
        setHistoryActionPending(true)
        try {
            const result = await props.api.forkConversation(props.session.id, messageLocalId)
            await navigate({ to: '/sessions/$sessionId', params: { sessionId: result.sessionId } })
            setForkPreviewRequest(null)
        } finally {
            setHistoryActionPending(false)
        }
    }, [navigate, props.api, props.session.id])

    Make the dialog await onConfirm and render caught errors, matching ConfirmDialog.

  • [Major] Current-tail forks render a historical cutoff that does not exist — current fork actions pass no messageLocalId, so the hub copies the complete transcript and boundaryText is null. The dialog still renders “Fork point” and always says the child resumes before “this message,” which is false for this common path. Evidence: web/src/components/AssistantChat/ForkPreviewDialog.tsx:41.
    Suggested fix:

    {previewKind === 'historical' ? <ForkBoundary text={boundaryText} /> : null}
    <p>{t(previewKind === 'historical'
        ? 'forkPreview.below'
        : 'forkPreview.currentTail')}</p>

    Return an explicit 'current' | 'historical' kind from buildForkPreview rather than inferring it from nullable text.

  • [Minor] Attachment-only messages disappear from the preview — fork actions are available for user messages without text, and the API accepts attachment-only messages, but the preview extractor reads only block.text. A selected attachment-only boundary is shown with no quoted message, and copied attachment-only turns are omitted. Evidence: web/src/lib/forkPreview.ts:28.
    Suggested fix:

    if (block.kind === 'user-text') {
        const attachments = block.attachments?.map(({ filename }) => filename).join(', ')
        text = [block.text, attachments].filter(Boolean).join(' ')
    }

Summary

Review mode: follow-up after new commits

The three findings from the prior bot pass are no longer present. The current head has two Major correctness/UX regressions and one Minor preview-parity gap.

Testing

Not run (automation; PR-provided code was not executed). GitHub integration check passed; the main test check was still in progress when reviewed. Missing coverage: a SessionChat-level failed-fork case, current-tail dialog semantics, and attachment-only preview input.

HAPI Bot

Comment thread web/src/components/SessionChat.tsx Outdated
Comment thread web/src/components/AssistantChat/ForkPreviewDialog.tsx Outdated
Comment thread web/src/lib/forkPreview.ts Outdated
… historical forks

- The dialog owns pending/error state: a failed fork request keeps it
  open and shows the error instead of dismissing with an unhandled
  rejection.
- Only historical forks render the cutoff divider and excluded-message
  quote; current-tail forks show a dedicated note since nothing is
  excluded.
- Attachment-only user messages appear in the preview via their
  filenames.

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

Findings

  • [Major] The empty-prefix state can misrepresent a paginated fork — the preview is built only from the currently loaded message window, but keptTurns.length === 0 is rendered as “The new session starts empty.” When the selected boundary is the oldest loaded message and hasMoreMessages is true, the hub still copies all older invoked messages. Evidence: web/src/components/SessionChat.tsx:1362, web/src/components/AssistantChat/ForkPreviewDialog.tsx:54.
    Suggested fix:
    // Thread through whether older copied history is outside this window.
    const prefixMayContainUnshownContent = props.hasMoreMessages
    
    {keptTurns.length === 0 ? (
        <div>{t(prefixMayContainUnshownContent
            ? "forkPreview.noTextPreview"
            : "forkPreview.emptyPrefix")}</div>
    ) : null}
  • [Minor] The new Playwright coverage is not run by PR CI — .github/workflows/test.yml invokes only terminal-wrap-fidelity.spec.ts and composer-copy.spec.ts, so this added spec never executes in automation. Evidence: e2e/fork-preview.spec.ts:1.
    Suggested fix:
    - run: bun run test:e2e -- terminal-wrap-fidelity.spec.ts composer-copy.spec.ts fork-preview.spec.ts

Summary

Review mode: follow-up after new commits

The previous bot findings are addressed. Two fresh-head issues remain: one preview-correctness bug for paginated history and one CI coverage gap.

Testing

Not run (automation): Bun is unavailable in this review environment. GitHub integration check passed; the main test check was still pending when reviewed.

HAPI Bot

Comment thread web/src/components/AssistantChat/ForkPreviewDialog.tsx Outdated
Comment thread e2e/fork-preview.spec.ts
When the fork boundary sits in older history that is not loaded yet
(hasMoreMessages), the child inherits those rows from the hub even
though the preview window shows no turns. Show a dedicated note instead
of claiming the session starts empty, and include the new spec in the
CI e2e workflow so it runs on every PR.

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

Findings

No high-confidence issues found.

Summary

Review mode: follow-up after new commits

The previous HAPI Bot findings are addressed on the current head. The full PR diff was reviewed; no Blocker/Major/Minor/Nit findings met the confidence threshold. Residual risk: the pagination-specific copy wording and the complete SessionChat → preview dialog → fork API wiring are not directly exercised by the added tests.

Testing

Not run locally (automation/review policy). GitHub integration and test checks pass at the reviewed head.

HAPI Bot

The hardcoded text-white became invisible in dark themes where
--app-link resolves to a light color. Use --app-bg for contrast,
matching the QuickKeys active-tab convention.

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

Findings

No high-confidence issues found.

Summary

Review mode: follow-up after new commits

The full current diff was reviewed. The commits since the prior HAPI Bot pass only change the fork-point badge and confirm-button foreground colors. Residual risk: the complete SessionChat → preview dialog → fork API wiring and pending/error behavior are not directly exercised; the added browser test mounts the dialog in an isolated fixture.

Testing

Not run locally (automation/security policy). Current-head CI typecheck, targeted Playwright (including fork-preview.spec.ts), and root tests passed. The separate CLI runner integration job failed in unchanged CLI tests; no CLI or integration source files are changed by this PR, so it is not attributed to this diff.

HAPI Bot

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant