Skip to content

Merge upstream/main into fork (batch 5)#9

Merged
aaditagrawal merged 10 commits intomainfrom
merge/upstream-main-5
Mar 12, 2026
Merged

Merge upstream/main into fork (batch 5)#9
aaditagrawal merged 10 commits intomainfrom
merge/upstream-main-5

Conversation

@aaditagrawal
Copy link
Copy Markdown
Owner

@aaditagrawal aaditagrawal commented Mar 12, 2026

Summary

  • Integrates 5 upstream commits from pingdotgg/t3code main branch
  • Major change: upstream split monolithic ChatView.tsx (~2400 lines) into ~20 modular sub-components
  • All fork multi-provider additions ported into the new modular structure

ChatView Modular Split

Upstream extracted components into apps/web/src/components/chat/:

  • MessagesTimeline, ProviderModelPicker, ChatHeader, CodexTraitsPicker, CompactComposerControlsMenu, ComposerCommandMenu, ComposerPendingApprovalPanel, ComposerPendingUserInputPanel, ProposedPlanCard, ChangedFilesTree, and more

Multi-Provider Additions Ported

  • New files: CursorTraitsPicker.tsx, ClaudeCodeTraitsPicker.tsx — extracted as standalone components matching upstream's modular pattern
  • Enhanced: ProviderModelPicker.tsx — expanded with all 8 provider icons, model discovery, pricing tiers, sub-provider grouping
  • Updated: ChatView.tsx — multi-provider queries, state, and callbacks integrated into slimmed-down orchestrator
  • Updated: ChatView.logic.ts — provider-aware draft thread defaults

Other Upstream Changes

Verification

  • bun typecheck passes (7/7 packages)
  • bun lint passes (0 errors)

Summary by CodeRabbit

  • New Features
    • Rich chat header with project/editor actions and diff toggle
    • Virtualized messages timeline with diffs, file tree, image preview, copy & revert controls
    • Numerous composer UIs: command menu, trait pickers, compact controls, approval & user-input panels
    • Model/provider pickers, provider health banner, plan card, plan follow-up banner, open-in editor picker
  • Chores
    • Package version bumps, CI release step tweak, added trusted contributor to vouching list

github-actions bot and others added 6 commits March 11, 2026 17:27
Co-authored-by: Christian Smith <chris@Christians-MacBook-Air.local>
Integrates 5 upstream commits including the ChatView.tsx split into
~20 sub-components, sidebar status pills, release prep, and contributor
updates.

Ports all multi-provider additions into the new modular structure:
- Created CursorTraitsPicker.tsx and ClaudeCodeTraitsPicker.tsx
- Enhanced ProviderModelPicker.tsx with all 8 provider icons, model
  discovery, pricing tiers, and sub-provider grouping
- Updated ChatView.tsx with multi-provider queries, state, and callbacks
- Updated ChatView.logic.ts for provider-aware draft thread defaults
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7ed53c02-2c5d-4956-8b30-31be23aa5f43

📥 Commits

Reviewing files that changed from the base of the PR and between c98163d and 07c3366.

📒 Files selected for processing (1)
  • apps/web/src/components/chat/ProposedPlanCard.tsx

📝 Walkthrough

Walkthrough

Adds a trusted contributor to vouch list, updates release workflow to align package versions and switch a script to bun, bumps several package versions, and introduces numerous chat-related utilities and ~20+ new React UI components and helpers across the web app.

Changes

Cohort / File(s) Summary
Release infra
/.github/VOUCHED.td, /.github/workflows/release.yml
Add github:maria-rcks to vouch list; insert "Align package versions to release version" workflow step(s); change final release step to use bun run scripts/update-release-package-versions.ts.
Version bumps
apps/desktop/package.json, apps/server/package.json, apps/web/package.json, packages/contracts/package.json
Bump package versions from 0.0.90.0.10.
Chat core utilities
apps/web/src/components/ChatView.logic.ts
Add storage helpers, draft-thread builder, blob URL management, file→dataURL reader, temp worktree branch naming, image clone helper, SendPhase type, and PullRequestDialogState.
Timeline & diffs
apps/web/src/components/chat/MessagesTimeline.tsx, apps/web/src/components/chat/ChangedFilesTree.tsx, apps/web/src/components/chat/DiffStatLabel.tsx, apps/web/src/components/chat/ExpandedImagePreview.tsx
Add a virtualized MessagesTimeline, ChangedFilesTree with directory expansion, DiffStatLabel helper, and expanded-image preview model + builder.
Composer controls & approvals
apps/web/src/components/chat/ComposerCommandMenu.tsx, apps/web/src/components/chat/ComposerPendingApprovalActions.tsx, .../ComposerPendingApprovalPanel.tsx, .../ComposerPendingUserInputPanel.tsx, .../MessageCopyButton.tsx
Add composer command menu, pending-approval actions and panel, pending-user-input panel with keyboard/auto-advance, and MessageCopyButton.
Provider / model / traits pickers
apps/web/src/components/chat/ProviderModelPicker.tsx, ClaudeCodeTraitsPicker.tsx, CodexTraitsPicker.tsx, CursorTraitsPicker.tsx, CompactComposerControlsMenu.tsx
Add ProviderModelPicker with model merging/grouping utilities and multiple trait picker components plus a compact composer controls menu.
Header, plans & editor integration
apps/web/src/components/chat/ChatHeader.tsx, ProposedPlanCard.tsx, ComposerPlanFollowUpBanner.tsx, OpenInPicker.tsx
Add ChatHeader, ProposedPlanCard (download/save plan flows including workspace save dialog), follow-up banner, and OpenInPicker (editor selection with persistence and shortcut handling).
Status, errors & icons
apps/web/src/components/chat/ProviderHealthBanner.tsx, ThreadErrorBanner.tsx, VscodeEntryIcon.tsx
Add provider health banner, thread error banner, and a VSCode-style entry icon component with fallback handling.
Sidebar tweak
apps/web/src/components/Sidebar.tsx
Remove per-thread useMemo caches for pending approvals/user inputs; compute pending state inline during render.
Other UI utilities
multiple apps/web/src/components/chat/*.tsx
Add various smaller UI pieces (Composer menus, copy button, diff stats, plan follow-up banner, expanded image preview builder, message copy, icons, etc.).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through diffs with tiny feet,
Vouched a friend and tuned the release beat.
Timelines, pickers, plans in a row—
New widgets sprout where conversations grow.
A soft-code nibble — onward, we go!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the PR as a merge of upstream commits (batch 5) into the fork, which aligns with the major refactoring work to integrate the new modular component structure.
Description check ✅ Passed The PR description provides comprehensive detail on what changed, why, and verification status. While it exceeds the template sections, it fully explains the monolithic ChatView split, port of multi-provider additions, and verification steps—addressing the intent of the template.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch merge/upstream-main-5

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

@github-actions github-actions bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Mar 12, 2026
Copy link
Copy Markdown

@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: 9

🧹 Nitpick comments (6)
apps/web/src/components/Sidebar.tsx (1)

2020-2026: Consider a short-circuit "hasAny" variant for better render performance.

derivePendingApprovals and derivePendingUserInputs are called inside a render loop for every visible thread. Both functions copy, sort, iterate, and return full arrays—yet only .length > 0 is used here.

For a sidebar that re-renders on many interactions, this could become a hot path. A lightweight hasPendingApprovals(activities): boolean helper that returns early on the first match would avoid building the full result array when only presence is needed.

This is not a functional issue, but worth considering if profiling reveals render lag with many threads.

💡 Example early-exit helper (in session-logic.ts)
export function hasPendingApprovals(
  activities: ReadonlyArray<OrchestrationThreadActivity>,
): boolean {
  const resolved = new Set<string>();
  for (const activity of activities) {
    const payload =
      activity.payload && typeof activity.payload === "object"
        ? (activity.payload as Record<string, unknown>)
        : null;
    const requestId = payload && typeof payload.requestId === "string" ? payload.requestId : null;
    if (!requestId) continue;

    if (activity.kind === "approval.resolved") {
      resolved.add(requestId);
    } else if (activity.kind === "approval.requested" && !resolved.has(requestId)) {
      return true; // early exit
    }
  }
  return false;
}

Then in Sidebar.tsx:

-hasPendingApprovals:
-  derivePendingApprovals(thread.activities).length > 0,
+hasPendingApprovals: hasPendingApprovals(thread.activities),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/Sidebar.tsx` around lines 2020 - 2026, The render
currently calls derivePendingApprovals(...) and derivePendingUserInputs(...) and
only uses .length > 0, which forces full array work for every visible thread;
add lightweight short-circuit helpers (e.g., hasPendingApprovals(activities:
ReadonlyArray<OrchestrationThreadActivity>): boolean and
hasPendingUserInputs(...): boolean) that iterate and return true on first match,
place them alongside existing session-logic helpers, and replace the calls in
resolveThreadStatusPill invocation (the call site using derivePendingApprovals
and derivePendingUserInputs in the Sidebar component) to use the new boolean
helpers so the render no longer allocates full result arrays.
apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx (1)

16-18: Remove the commented-out JSX before merging.

This dead copy makes the component harder to scan and can drift from the shipped UX. If it's intentionally deferred, track it outside the render tree instead.

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

In `@apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx` around lines 16
- 18, In ComposerPlanFollowUpBanner.tsx remove the commented-out JSX block (the
commented div containing "Review the plan") from the component render so no
dead/commented UI remains; if the markup must be preserved for later, move it to
a separate note or comment outside the render tree (e.g., developer TODO)
instead of leaving JSX commented inline.
apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx (1)

28-30: Minor: Redundant null check.

Line 30's check for !activePrompt is technically redundant since line 28 already ensures pendingUserInputs.length > 0. However, this is defensive and helps TypeScript narrow the type, so it's acceptable.

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

In `@apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx` around lines
28 - 30, The second null-check is redundant: after verifying
pendingUserInputs.length > 0, remove the extra if (!activePrompt) return null; —
keep the first guard (if (pendingUserInputs.length === 0) return null;), assign
const activePrompt = pendingUserInputs[0]; and proceed; if you prefer defensive
typing instead, replace the redundant check with a short comment explaining it's
for TypeScript narrowing and leave activePrompt as-is (symbols:
pendingUserInputs, activePrompt, ComposerPendingUserInputPanel).
apps/web/src/components/chat/ProviderHealthBanner.tsx (1)

15-29: Use the shared provider display label here.

status.provider will render internal ids like claudeCode and geminiCli in the title/default message. Pulling the same label source used by the picker keeps these banners consistent with the rest of the provider UI.

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

In `@apps/web/src/components/chat/ProviderHealthBanner.tsx` around lines 15 - 29,
The banner currently uses raw internal ids (status.provider) in AlertTitle and
defaultMessage; replace those usages with the shared display label function
(e.g. getProviderDisplayLabel/statusProviderLabel) used by the provider picker
so the UI shows the human-friendly name. Specifically, import and call the
shared label helper (getProviderDisplayLabel) with status.provider when building
defaultMessage and when rendering AlertTitle and AlertDescription, keeping the
same error/limited wording and fallback to status.message as before. Ensure you
only change the displayed label and not the underlying status logic in
ProviderHealthBanner or defaultMessage construction.
apps/web/src/components/chat/ComposerCommandMenu.tsx (1)

65-73: Consider handling additional triggerKind values in empty state message.

The empty state message defaults to "No matching command." for any triggerKind that isn't "path". If the menu supports model selection (as suggested by the model type in ComposerCommandItem), a "model" trigger kind might benefit from a more specific message like "No matching models."

💡 Possible enhancement
           {props.isLoading
             ? "Searching workspace files..."
             : props.triggerKind === "path"
               ? "No matching files or folders."
-              : "No matching command."}
+              : props.triggerKind === "model"
+                ? "No matching models."
+                : "No matching command."}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ComposerCommandMenu.tsx` around lines 65 - 73,
The empty-state in ComposerCommandMenu currently falls back to "No matching
command." for any triggerKind other than "path"; update the conditional logic
that renders the message (where props.items.length === 0 is checked) to
explicitly handle a "model" triggerKind (used by ComposerCommandItem's model
type) and show "No matching models." when props.triggerKind === "model", keep
the existing "No matching files or folders." for "path" and preserve the default
fallback for other kinds; ensure props.isLoading still takes precedence to show
"Searching workspace files..." when loading.
apps/web/src/components/chat/ChangedFilesTree.tsx (1)

31-36: Initial state inconsistent with allDirectoriesExpanded prop.

The useState initializer hardcodes true (line 32), while the useEffect syncs with allDirectoryExpansionState which respects the allDirectoriesExpanded prop. If the prop is initially false, users will see a brief flash where directories appear expanded then collapse.

Consider initializing with the prop value directly:

♻️ Proposed fix
-  const [expandedDirectories, setExpandedDirectories] = useState<Record<string, boolean>>(() =>
-    buildDirectoryExpansionState(directoryPathsKey ? directoryPathsKey.split("\u0000") : [], true),
-  );
+  const [expandedDirectories, setExpandedDirectories] = useState<Record<string, boolean>>(() =>
+    buildDirectoryExpansionState(
+      directoryPathsKey ? directoryPathsKey.split("\u0000") : [],
+      allDirectoriesExpanded,
+    ),
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ChangedFilesTree.tsx` around lines 31 - 36, The
state initializer for expandedDirectories uses buildDirectoryExpansionState(...,
true) which hardcodes expansion and causes a flash when allDirectoriesExpanded
is false; update the useState initializer to derive the initial value from
allDirectoryExpansionState (or use allDirectoriesExpanded to build the initial
expansion state) so the initial expandedDirectories matches the prop-driven
allDirectoryExpansionState, keeping the existing useEffect that calls
setExpandedDirectories(allDirectoryExpansionState); ensure you reference
buildDirectoryExpansionState(directoryPathsKey ?
directoryPathsKey.split("\u0000") : [], ...) when constructing the initial
value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 128-130: Replace the step that runs the TypeScript release script
using node with Bun execution: change the run command from `node
scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version
}}"` to `bun run scripts/update-release-package-versions.ts "${{
needs.preflight.outputs.version }}"` so the TypeScript file
`scripts/update-release-package-versions.ts` is executed with Bun's native TS
support in the workflow.

In `@apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx`:
- Around line 9-15: The banner in ComposerPlanFollowUpBanner (the outer div
wrapping the "Plan ready" text and planTitle) should be made into a polite
status region so screen readers announce the state change; update that container
(the div that currently has className="px-4 py-3.5 sm:px-5 sm:py-4") to include
accessibility attributes such as role="status", aria-live="polite" and
aria-atomic="true" so when the component mounts/updates the "Plan ready" message
(and planTitle) is announced to assistive tech.

In `@apps/web/src/components/chat/CursorTraitsPicker.tsx`:
- Around line 84-89: The MenuRadioItem in CursorTraitsPicker is rendering the
raw reasoning key from CURSOR_REASONING_OPTIONS instead of the human-readable
label; update the MenuRadioItem children to use
reasoningLabelByOption[reasoning] (and keep the " (default)" suffix when
reasoning === "normal") so the options match the trigger label and other pickers
(refer to CURSOR_REASONING_OPTIONS, reasoningLabelByOption, MenuRadioItem,
CursorTraitsPicker).

In `@apps/web/src/components/chat/MessageCopyButton.tsx`:
- Around line 8-12: handleCopy currently calls navigator.clipboard.writeText()
without awaiting errors and starts timers that can overlap and persist after
unmount; change handleCopy to await navigator.clipboard.writeText(text) and only
call setCopied(true) on successful resolution, catch and handle errors (e.g., do
not setCopied or surface an error). Use a ref (e.g., copyTimerRef) to store the
timeout id, clear any existing timer before creating a new one, and set
copyTimerRef.current = setTimeout(...). Add a useEffect cleanup that clears
copyTimerRef.current on unmount so timers don't fire after unmount. Reference:
handleCopy, navigator.clipboard.writeText, setCopied, and the timeout behavior.

In `@apps/web/src/components/chat/OpenInPicker.tsx`:
- Line 114: The MenuTrigger's Button currently uses an incorrect aria-label
"Copy options"; update the render prop on MenuTrigger (the Button component
inside OpenInPicker.tsx) to use an accurate aria-label reflecting its purpose
(e.g., "Open selected text" or "Open editor selection") so it describes the
editor selection action; ensure the aria-label is updated on the Button inside
the MenuTrigger component to improve accessibility.
- Line 100: The Group aria-label is incorrect (copy-pasted as "Subscription
actions") in the OpenInPicker component; update the aria-label on the Group
element to a descriptive label matching the editor picker (e.g., "Editor picker"
or "Open in editor actions") so assistive tech correctly describes this control;
locate the Group JSX in the OpenInPicker component and replace the aria-label
string accordingly.
- Around line 23-26: The state initializer for lastEditor in the OpenInPicker
component reads localStorage directly, which breaks SSR; change this to be
client-safe by either initializing lastEditor to a safe default (e.g.,
EDITORS[0].id) and then in a useEffect (within OpenInPicker) read
localStorage.getItem(LAST_EDITOR_KEY) and call setLastEditor only if the stored
value matches EDITORS, or keep the lazy initializer but guard it with typeof
window !== 'undefined' before accessing localStorage; reference LAST_EDITOR_KEY,
EDITORS, lastEditor and setLastEditor when making the change.
- Around line 46-55: The code uses navigator.platform directly inside the
OpenInPicker component when building the label (via isMacPlatform and
isWindowsPlatform), which breaks SSR; wrap the platform-dependent logic in a
client-only guard: move the platform detection into a useEffect or compute it
lazily only when window/navigator is defined (e.g., check typeof navigator !==
"undefined" or use a useState/useEffect inside OpenInPicker) and then set the
label (or value for the file-manager item) from that state; ensure isMacPlatform
and isWindowsPlatform are called only after the guard so SSR renders a safe
fallback (e.g., "Files") initially and updates on the client.

In `@apps/web/src/components/chat/ProposedPlanCard.tsx`:
- Around line 57-67: The "Save to workspace" flow should be gated by native API
availability as well as workspaceRoot: in openSaveDialog (and the other entry
points at the same spots around the second dialog show and the save trigger near
lines ~131-133), call/read the native availability check (readNativeApi() or a
shared isNativeAvailable helper) and if it's absent, show a toastManager.add
error and return instead of opening the dialog; otherwise proceed to
setSavePath(downloadFilename) and setIsSaveDialogOpen(true). Also update
handleSaveToWorkspace to rely on the same shared availability check so the UI
and handler use identical gating logic.

---

Nitpick comments:
In `@apps/web/src/components/chat/ChangedFilesTree.tsx`:
- Around line 31-36: The state initializer for expandedDirectories uses
buildDirectoryExpansionState(..., true) which hardcodes expansion and causes a
flash when allDirectoriesExpanded is false; update the useState initializer to
derive the initial value from allDirectoryExpansionState (or use
allDirectoriesExpanded to build the initial expansion state) so the initial
expandedDirectories matches the prop-driven allDirectoryExpansionState, keeping
the existing useEffect that calls
setExpandedDirectories(allDirectoryExpansionState); ensure you reference
buildDirectoryExpansionState(directoryPathsKey ?
directoryPathsKey.split("\u0000") : [], ...) when constructing the initial
value.

In `@apps/web/src/components/chat/ComposerCommandMenu.tsx`:
- Around line 65-73: The empty-state in ComposerCommandMenu currently falls back
to "No matching command." for any triggerKind other than "path"; update the
conditional logic that renders the message (where props.items.length === 0 is
checked) to explicitly handle a "model" triggerKind (used by
ComposerCommandItem's model type) and show "No matching models." when
props.triggerKind === "model", keep the existing "No matching files or folders."
for "path" and preserve the default fallback for other kinds; ensure
props.isLoading still takes precedence to show "Searching workspace files..."
when loading.

In `@apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx`:
- Around line 28-30: The second null-check is redundant: after verifying
pendingUserInputs.length > 0, remove the extra if (!activePrompt) return null; —
keep the first guard (if (pendingUserInputs.length === 0) return null;), assign
const activePrompt = pendingUserInputs[0]; and proceed; if you prefer defensive
typing instead, replace the redundant check with a short comment explaining it's
for TypeScript narrowing and leave activePrompt as-is (symbols:
pendingUserInputs, activePrompt, ComposerPendingUserInputPanel).

In `@apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx`:
- Around line 16-18: In ComposerPlanFollowUpBanner.tsx remove the commented-out
JSX block (the commented div containing "Review the plan") from the component
render so no dead/commented UI remains; if the markup must be preserved for
later, move it to a separate note or comment outside the render tree (e.g.,
developer TODO) instead of leaving JSX commented inline.

In `@apps/web/src/components/chat/ProviderHealthBanner.tsx`:
- Around line 15-29: The banner currently uses raw internal ids
(status.provider) in AlertTitle and defaultMessage; replace those usages with
the shared display label function (e.g.
getProviderDisplayLabel/statusProviderLabel) used by the provider picker so the
UI shows the human-friendly name. Specifically, import and call the shared label
helper (getProviderDisplayLabel) with status.provider when building
defaultMessage and when rendering AlertTitle and AlertDescription, keeping the
same error/limited wording and fallback to status.message as before. Ensure you
only change the displayed label and not the underlying status logic in
ProviderHealthBanner or defaultMessage construction.

In `@apps/web/src/components/Sidebar.tsx`:
- Around line 2020-2026: The render currently calls derivePendingApprovals(...)
and derivePendingUserInputs(...) and only uses .length > 0, which forces full
array work for every visible thread; add lightweight short-circuit helpers
(e.g., hasPendingApprovals(activities:
ReadonlyArray<OrchestrationThreadActivity>): boolean and
hasPendingUserInputs(...): boolean) that iterate and return true on first match,
place them alongside existing session-logic helpers, and replace the calls in
resolveThreadStatusPill invocation (the call site using derivePendingApprovals
and derivePendingUserInputs in the Sidebar component) to use the new boolean
helpers so the render no longer allocates full result arrays.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f0e667e-5cbb-4cb0-9b1d-313d404ebe27

📥 Commits

Reviewing files that changed from the base of the PR and between 83d5f9f and 31972e2.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (30)
  • .github/VOUCHED.td
  • .github/workflows/release.yml
  • apps/desktop/package.json
  • apps/server/package.json
  • apps/web/package.json
  • apps/web/src/components/ChatView.logic.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/components/chat/ChangedFilesTree.tsx
  • apps/web/src/components/chat/ChatHeader.tsx
  • apps/web/src/components/chat/ClaudeCodeTraitsPicker.tsx
  • apps/web/src/components/chat/CodexTraitsPicker.tsx
  • apps/web/src/components/chat/CompactComposerControlsMenu.tsx
  • apps/web/src/components/chat/ComposerCommandMenu.tsx
  • apps/web/src/components/chat/ComposerPendingApprovalActions.tsx
  • apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx
  • apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx
  • apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx
  • apps/web/src/components/chat/CursorTraitsPicker.tsx
  • apps/web/src/components/chat/DiffStatLabel.tsx
  • apps/web/src/components/chat/ExpandedImagePreview.tsx
  • apps/web/src/components/chat/MessageCopyButton.tsx
  • apps/web/src/components/chat/MessagesTimeline.tsx
  • apps/web/src/components/chat/OpenInPicker.tsx
  • apps/web/src/components/chat/ProposedPlanCard.tsx
  • apps/web/src/components/chat/ProviderHealthBanner.tsx
  • apps/web/src/components/chat/ProviderModelPicker.tsx
  • apps/web/src/components/chat/ThreadErrorBanner.tsx
  • apps/web/src/components/chat/VscodeEntryIcon.tsx
  • packages/contracts/package.json

Comment on lines +9 to +15
<div className="px-4 py-3.5 sm:px-5 sm:py-4">
<div className="flex flex-wrap items-center gap-2">
<span className="uppercase text-sm tracking-[0.2em]">Plan ready</span>
{planTitle ? (
<span className="min-w-0 flex-1 truncate text-sm font-medium">{planTitle}</span>
) : null}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Announce this state change to screen readers.

If this banner appears when plan generation completes, a plain div won't notify assistive tech that anything changed. Making the container a polite status region will announce "Plan ready" when it mounts/updates.

♿ Proposed fix
-    <div className="px-4 py-3.5 sm:px-5 sm:py-4">
+    <div
+      role="status"
+      aria-live="polite"
+      className="px-4 py-3.5 sm:px-5 sm:py-4"
+    >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx` around lines 9 -
15, The banner in ComposerPlanFollowUpBanner (the outer div wrapping the "Plan
ready" text and planTitle) should be made into a polite status region so screen
readers announce the state change; update that container (the div that currently
has className="px-4 py-3.5 sm:px-5 sm:py-4") to include accessibility attributes
such as role="status", aria-live="polite" and aria-atomic="true" so when the
component mounts/updates the "Plan ready" message (and planTitle) is announced
to assistive tech.

- Use bun instead of node for TS script execution in release workflow
- Add role="status" and aria-live="polite" to ComposerPlanFollowUpBanner
- Remove commented-out JSX in ComposerPlanFollowUpBanner
- Use human-readable labels in CursorTraitsPicker reasoning options
- Handle clipboard errors and clean up timers in MessageCopyButton
- Add SSR guards for localStorage and navigator.platform in OpenInPicker
- Fix incorrect aria-labels in OpenInPicker
- Gate workspace save on native API availability in ProposedPlanCard
- Fix initial state inconsistency in ChangedFilesTree
- Add "slash-model" triggerKind empty state message in ComposerCommandMenu
Copy link
Copy Markdown

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

🧹 Nitpick comments (2)
apps/web/src/components/chat/ProposedPlanCard.tsx (1)

114-121: Use .finally() for cleanup instead of dual-callback .then().

After .catch() handles the rejection, it returns a resolved promise, so the rejection handler (lines 118-120) is unreachable. The .finally() method is the idiomatic pattern for cleanup that must run in both success and failure cases.

♻️ Suggested refactor
       .catch((error) => {
         toastManager.add({
           type: "error",
           title: "Could not save plan",
           description: error instanceof Error ? error.message : "An error occurred while saving.",
         });
       })
-      .then(
-        () => {
-          setIsSavingToWorkspace(false);
-        },
-        () => {
-          setIsSavingToWorkspace(false);
-        },
-      );
+      .finally(() => {
+        setIsSavingToWorkspace(false);
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ProposedPlanCard.tsx` around lines 114 - 121,
The promise chain in ProposedPlanCard uses a dual-callback .then to reset
setIsSavingToWorkspace which makes the rejection handler unreachable after
.catch; replace the dual-callback .then(...) with a single .then(...) for
success if needed and append .finally(() => setIsSavingToWorkspace(false)) (or
move the cleanup into .finally) so setIsSavingToWorkspace(false) always runs
regardless of success or failure, referencing the existing
setIsSavingToWorkspace call in the component.
apps/web/src/components/chat/ChangedFilesTree.tsx (1)

31-36: Simplify state initialization by reusing the memoized value.

The useState initializer duplicates the computation already performed in allDirectoryExpansionState. Since hooks execute in order, you can pass the memoized value directly.

♻️ Proposed simplification
-  const [expandedDirectories, setExpandedDirectories] = useState<Record<string, boolean>>(() =>
-    buildDirectoryExpansionState(
-      directoryPathsKey ? directoryPathsKey.split("\u0000") : [],
-      allDirectoriesExpanded,
-    ),
-  );
+  const [expandedDirectories, setExpandedDirectories] =
+    useState<Record<string, boolean>>(allDirectoryExpansionState);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ChangedFilesTree.tsx` around lines 31 - 36, The
state initializer for expandedDirectories recomputes
buildDirectoryExpansionState even though allDirectoryExpansionState already
memoizes that result; replace the initializer usage so useState is called with
allDirectoryExpansionState directly (i.e., initialize expandedDirectories via
the memoized allDirectoryExpansionState) and keep setExpandedDirectories as-is;
update references to directoryPathsKey only where allDirectoryExpansionState is
computed so no duplicate splitting/processing occurs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/components/chat/ChangedFilesTree.tsx`:
- Around line 31-36: The state initializer for expandedDirectories recomputes
buildDirectoryExpansionState even though allDirectoryExpansionState already
memoizes that result; replace the initializer usage so useState is called with
allDirectoryExpansionState directly (i.e., initialize expandedDirectories via
the memoized allDirectoryExpansionState) and keep setExpandedDirectories as-is;
update references to directoryPathsKey only where allDirectoryExpansionState is
computed so no duplicate splitting/processing occurs.

In `@apps/web/src/components/chat/ProposedPlanCard.tsx`:
- Around line 114-121: The promise chain in ProposedPlanCard uses a
dual-callback .then to reset setIsSavingToWorkspace which makes the rejection
handler unreachable after .catch; replace the dual-callback .then(...) with a
single .then(...) for success if needed and append .finally(() =>
setIsSavingToWorkspace(false)) (or move the cleanup into .finally) so
setIsSavingToWorkspace(false) always runs regardless of success or failure,
referencing the existing setIsSavingToWorkspace call in the component.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0a14695-6003-417f-bceb-5aa7b0bb3dfb

📥 Commits

Reviewing files that changed from the base of the PR and between 31972e2 and cd95414.

📒 Files selected for processing (8)
  • .github/workflows/release.yml
  • apps/web/src/components/chat/ChangedFilesTree.tsx
  • apps/web/src/components/chat/ComposerCommandMenu.tsx
  • apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx
  • apps/web/src/components/chat/CursorTraitsPicker.tsx
  • apps/web/src/components/chat/MessageCopyButton.tsx
  • apps/web/src/components/chat/OpenInPicker.tsx
  • apps/web/src/components/chat/ProposedPlanCard.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/src/components/chat/OpenInPicker.tsx
  • apps/web/src/components/chat/ComposerCommandMenu.tsx
  • apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx
  • apps/web/src/components/chat/MessageCopyButton.tsx

- Use .finally() instead of dual-callback .then() in ProposedPlanCard
- Simplify ChangedFilesTree state initialization by reusing memoized value
Copy link
Copy Markdown

@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)
apps/web/src/components/chat/ProposedPlanCard.tsx (1)

57-75: ⚠️ Potential issue | 🟡 Minor

Keep the save action gated everywhere, not just in openSaveDialog().

The opener now shows a toast when native support is missing, but the menu item at Line 134 is still enabled whenever workspaceRoot exists, and handleSaveToWorkspace() at Line 81 still silently returns if that prerequisite disappears after the dialog opens. Reuse one availability check/helper for the trigger and the submit path so the UI and handler stay in sync.

Also applies to: 78-83, 134-136

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

In `@apps/web/src/components/chat/ProposedPlanCard.tsx` around lines 57 - 75, The
save action is inconsistently gated: openSaveDialog shows a toast when native
support is missing but the menu item and handleSaveToWorkspace still
allow/ignore the action; add a single helper (e.g., canSaveToWorkspace or
isSaveAvailable) that returns Boolean(workspaceRoot && readNativeApi()), use
that helper to disable the menu item/trigger (so the UI reflects availability),
and call the same helper at the start of handleSaveToWorkspace and
openSaveDialog to early-return with the same toast error message when
unavailable; update references to openSaveDialog and handleSaveToWorkspace to
use this helper so the UI and handler stay in sync.
🧹 Nitpick comments (2)
apps/web/src/components/chat/ChangedFilesTree.tsx (1)

71-75: Minor: Inconsistent stat display condition between directories and files.

Directory nodes use hasNonZeroStat(node.stat) (line 71) while file nodes use node.stat (line 104). This means files will show "+0 -0" when stats exist but are zero, while directories won't.

If this is intentional (e.g., to indicate file renames/mode changes), consider adding a brief comment. Otherwise, consider aligning the conditions:

♻️ Optional: Align stat display logic
-        {node.stat && (
+        {hasNonZeroStat(node.stat) && (
           <span className="ml-auto shrink-0 font-mono text-[10px] tabular-nums">
             <DiffStatLabel additions={node.stat.additions} deletions={node.stat.deletions} />
           </span>
         )}

Also applies to: 104-108

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

In `@apps/web/src/components/chat/ChangedFilesTree.tsx` around lines 71 - 75, The
directory branch currently guards the stat display with
hasNonZeroStat(node.stat) while the file branch uses a truthy node.stat check,
causing files with zeroed stats to render "+0 -0"; update the file node render
to use the same predicate (hasNonZeroStat(node.stat)) or, if the differing
behavior was intentional, add an inline comment near the file node render
explaining why node.stat alone is used; locate the file-specific block that
renders <DiffStatLabel additions={node.stat.additions}
deletions={node.stat.deletions} /> and change its condition to mirror the
directory branch (or add the explanatory comment) so the logic is consistent and
easy to understand.
apps/web/src/components/chat/ProposedPlanCard.tsx (1)

80-97: Client-side validation would improve UX, but server-side validation already prevents path escapes.

The server's resolveWorkspaceWritePath() properly canonicalizes the path and rejects absolute paths and parent-directory traversal (../) before writing. The test confirms ../escape.md is correctly rejected. However, consider adding client-side validation in ProposedPlanCard (around line 80) to provide immediate feedback before the request is sent, rather than only trimming the input.

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

In `@apps/web/src/components/chat/ProposedPlanCard.tsx` around lines 80 - 97, Add
client-side path validation in ProposedPlanCard before calling
api.projects.writeFile: after computing relativePath from savePath.trim(),
validate that relativePath is non-empty and does not start with "/" or contain
".." segments (e.g., "../") and if invalid call toastManager.add with a warning
and return (and ensure setIsSavingToWorkspace is not set). Keep existing
empty-check behavior, then proceed to call api.projects.writeFile (the same call
as in the diff) only when the client-side checks pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/src/components/chat/ProposedPlanCard.tsx`:
- Around line 57-75: The save action is inconsistently gated: openSaveDialog
shows a toast when native support is missing but the menu item and
handleSaveToWorkspace still allow/ignore the action; add a single helper (e.g.,
canSaveToWorkspace or isSaveAvailable) that returns Boolean(workspaceRoot &&
readNativeApi()), use that helper to disable the menu item/trigger (so the UI
reflects availability), and call the same helper at the start of
handleSaveToWorkspace and openSaveDialog to early-return with the same toast
error message when unavailable; update references to openSaveDialog and
handleSaveToWorkspace to use this helper so the UI and handler stay in sync.

---

Nitpick comments:
In `@apps/web/src/components/chat/ChangedFilesTree.tsx`:
- Around line 71-75: The directory branch currently guards the stat display with
hasNonZeroStat(node.stat) while the file branch uses a truthy node.stat check,
causing files with zeroed stats to render "+0 -0"; update the file node render
to use the same predicate (hasNonZeroStat(node.stat)) or, if the differing
behavior was intentional, add an inline comment near the file node render
explaining why node.stat alone is used; locate the file-specific block that
renders <DiffStatLabel additions={node.stat.additions}
deletions={node.stat.deletions} /> and change its condition to mirror the
directory branch (or add the explanatory comment) so the logic is consistent and
easy to understand.

In `@apps/web/src/components/chat/ProposedPlanCard.tsx`:
- Around line 80-97: Add client-side path validation in ProposedPlanCard before
calling api.projects.writeFile: after computing relativePath from
savePath.trim(), validate that relativePath is non-empty and does not start with
"/" or contain ".." segments (e.g., "../") and if invalid call toastManager.add
with a warning and return (and ensure setIsSavingToWorkspace is not set). Keep
existing empty-check behavior, then proceed to call api.projects.writeFile (the
same call as in the diff) only when the client-side checks pass.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a53fd12-80cb-4cd0-9aac-4c2233d4b628

📥 Commits

Reviewing files that changed from the base of the PR and between cd95414 and 5414eda.

📒 Files selected for processing (2)
  • apps/web/src/components/chat/ChangedFilesTree.tsx
  • apps/web/src/components/chat/ProposedPlanCard.tsx

- Unify save-to-workspace gating with canSaveToWorkspace helper in ProposedPlanCard
- Disable menu item when native API is unavailable
- Add client-side path validation for traversal and absolute paths
- Align file stat display condition with directory nodes in ChangedFilesTree
Copy link
Copy Markdown

@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: 1

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

Inline comments:
In `@apps/web/src/components/chat/ProposedPlanCard.tsx`:
- Around line 87-94: The current check rejects any substring '..' in
relativePath which misflags filenames like 'README..md'; update the validation
so it only rejects '..' as a full path segment: keep the existing absolute-path
check (relativePath.startsWith("/")), and replace relativePath.includes("..")
with a segment check such as splitting on '/' and ensuring no segment === '..'
(or use a normalized path and verify no segment equals '..'); when the check
fails still call toastManager.add with the same warning title/description and
return. Use the same symbols (relativePath, toastManager.add) inside the
ProposedPlanCard component to locate and replace the logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20428367-eae0-46c0-aa8d-43e39f84c49e

📥 Commits

Reviewing files that changed from the base of the PR and between 5414eda and c98163d.

📒 Files selected for processing (2)
  • apps/web/src/components/chat/ChangedFilesTree.tsx
  • apps/web/src/components/chat/ProposedPlanCard.tsx

Use segment-based check (split on '/') so filenames like 'README..md'
are not incorrectly rejected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants