Merge upstream/main into fork (batch 5)#9
Conversation
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
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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.
derivePendingApprovalsandderivePendingUserInputsare called inside a render loop for every visible thread. Both functions copy, sort, iterate, and return full arrays—yet only.length > 0is used here.For a sidebar that re-renders on many interactions, this could become a hot path. A lightweight
hasPendingApprovals(activities): booleanhelper 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
!activePromptis technically redundant since line 28 already ensurespendingUserInputs.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.providerwill render internal ids likeclaudeCodeandgeminiCliin 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 additionaltriggerKindvalues in empty state message.The empty state message defaults to
"No matching command."for anytriggerKindthat isn't"path". If the menu supports model selection (as suggested by themodeltype inComposerCommandItem), 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 withallDirectoriesExpandedprop.The
useStateinitializer hardcodestrue(line 32), while theuseEffectsyncs withallDirectoryExpansionStatewhich respects theallDirectoriesExpandedprop. If the prop is initiallyfalse, 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
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (30)
.github/VOUCHED.td.github/workflows/release.ymlapps/desktop/package.jsonapps/server/package.jsonapps/web/package.jsonapps/web/src/components/ChatView.logic.tsapps/web/src/components/ChatView.tsxapps/web/src/components/Sidebar.tsxapps/web/src/components/chat/ChangedFilesTree.tsxapps/web/src/components/chat/ChatHeader.tsxapps/web/src/components/chat/ClaudeCodeTraitsPicker.tsxapps/web/src/components/chat/CodexTraitsPicker.tsxapps/web/src/components/chat/CompactComposerControlsMenu.tsxapps/web/src/components/chat/ComposerCommandMenu.tsxapps/web/src/components/chat/ComposerPendingApprovalActions.tsxapps/web/src/components/chat/ComposerPendingApprovalPanel.tsxapps/web/src/components/chat/ComposerPendingUserInputPanel.tsxapps/web/src/components/chat/ComposerPlanFollowUpBanner.tsxapps/web/src/components/chat/CursorTraitsPicker.tsxapps/web/src/components/chat/DiffStatLabel.tsxapps/web/src/components/chat/ExpandedImagePreview.tsxapps/web/src/components/chat/MessageCopyButton.tsxapps/web/src/components/chat/MessagesTimeline.tsxapps/web/src/components/chat/OpenInPicker.tsxapps/web/src/components/chat/ProposedPlanCard.tsxapps/web/src/components/chat/ProviderHealthBanner.tsxapps/web/src/components/chat/ProviderModelPicker.tsxapps/web/src/components/chat/ThreadErrorBanner.tsxapps/web/src/components/chat/VscodeEntryIcon.tsxpackages/contracts/package.json
| <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> |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
🧹 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
useStateinitializer duplicates the computation already performed inallDirectoryExpansionState. 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
📒 Files selected for processing (8)
.github/workflows/release.ymlapps/web/src/components/chat/ChangedFilesTree.tsxapps/web/src/components/chat/ComposerCommandMenu.tsxapps/web/src/components/chat/ComposerPlanFollowUpBanner.tsxapps/web/src/components/chat/CursorTraitsPicker.tsxapps/web/src/components/chat/MessageCopyButton.tsxapps/web/src/components/chat/OpenInPicker.tsxapps/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
There was a problem hiding this comment.
♻️ Duplicate comments (1)
apps/web/src/components/chat/ProposedPlanCard.tsx (1)
57-75:⚠️ Potential issue | 🟡 MinorKeep 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
workspaceRootexists, andhandleSaveToWorkspace()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 usenode.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.mdis 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
📒 Files selected for processing (2)
apps/web/src/components/chat/ChangedFilesTree.tsxapps/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
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
apps/web/src/components/chat/ChangedFilesTree.tsxapps/web/src/components/chat/ProposedPlanCard.tsx
Use segment-based check (split on '/') so filenames like 'README..md' are not incorrectly rejected.
Summary
pingdotgg/t3codemain branchChatView.tsx(~2400 lines) into ~20 modular sub-componentsChatView Modular Split
Upstream extracted components into
apps/web/src/components/chat/:MessagesTimeline,ProviderModelPicker,ChatHeader,CodexTraitsPicker,CompactComposerControlsMenu,ComposerCommandMenu,ComposerPendingApprovalPanel,ComposerPendingUserInputPanel,ProposedPlanCard,ChangedFilesTree, and moreMulti-Provider Additions Ported
CursorTraitsPicker.tsx,ClaudeCodeTraitsPicker.tsx— extracted as standalone components matching upstream's modular patternProviderModelPicker.tsx— expanded with all 8 provider icons, model discovery, pricing tiers, sub-provider groupingChatView.tsx— multi-provider queries, state, and callbacks integrated into slimmed-down orchestratorChatView.logic.ts— provider-aware draft thread defaultsOther Upstream Changes
Verification
bun typecheckpasses (7/7 packages)bun lintpasses (0 errors)Summary by CodeRabbit