Skip to content

Refresh desktop and chat UI styling#1

Open
vkiooz wants to merge 7 commits into
mainfrom
feature/ui-refresh
Open

Refresh desktop and chat UI styling#1
vkiooz wants to merge 7 commits into
mainfrom
feature/ui-refresh

Conversation

@vkiooz

@vkiooz vkiooz commented Mar 25, 2026

Copy link
Copy Markdown
  • Move app state to a single userdata directory
  • Add thinking indicator and branch toolbar integration
  • Update sidebar, diff, and message layouts with new typography

Summary by CodeRabbit

  • New Features

    • Animated "Thinking" status indicator
    • Project detail page with direct project navigation
  • Style

    • Updated font stack and added shimmer text animation
    • Refined sidebar, toolbar, chat, diff, and message layouts and spacing
    • Improved color palette and theme tokens
  • Behavior

    • Relative time now shows full pluralized units (e.g., "2 hours ago")

- Move app state to a single userdata directory
- Add thinking indicator and branch toolbar integration
- Update sidebar, diff, and message layouts with new typography
@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 297 files, which is 147 over the limit of 150.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 863e2848-1bf6-4a07-8004-a1cbf3525310

📥 Commits

Reviewing files that changed from the base of the PR and between 8864012 and 8ffad40.

⛔ Files ignored due to path filters (2)
  • apps/web/public/empty-state.svg is excluded by !**/*.svg
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (297)
  • .gitattributes
  • .github/VOUCHED.td
  • .github/workflows/pr-size.yml
  • .gitignore
  • CLAUDE.md
  • README.md
  • REMOTE.md
  • apps/desktop/package.json
  • apps/desktop/scripts/dev-electron.mjs
  • apps/desktop/scripts/wait-for-resources.mjs
  • apps/desktop/src/confirmDialog.ts
  • apps/desktop/src/main.ts
  • apps/desktop/src/preload.ts
  • apps/desktop/src/syncShellEnvironment.test.ts
  • apps/desktop/src/syncShellEnvironment.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/integration/orchestrationEngine.integration.test.ts
  • apps/server/integration/providerService.integration.test.ts
  • apps/server/package.json
  • apps/server/src/bootstrap.test.ts
  • apps/server/src/bootstrap.ts
  • apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts
  • apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
  • apps/server/src/codexAppServerManager.test.ts
  • apps/server/src/codexAppServerManager.ts
  • apps/server/src/config.ts
  • apps/server/src/git/Layers/ClaudeTextGeneration.test.ts
  • apps/server/src/git/Layers/ClaudeTextGeneration.ts
  • apps/server/src/git/Layers/CodexTextGeneration.test.ts
  • apps/server/src/git/Layers/CodexTextGeneration.ts
  • apps/server/src/git/Layers/GitCore.test.ts
  • apps/server/src/git/Layers/GitCore.ts
  • apps/server/src/git/Layers/GitManager.test.ts
  • apps/server/src/git/Layers/GitManager.ts
  • apps/server/src/git/Layers/RoutingTextGeneration.ts
  • apps/server/src/git/Prompts.test.ts
  • apps/server/src/git/Prompts.ts
  • apps/server/src/git/Services/GitCore.ts
  • apps/server/src/git/Services/GitManager.ts
  • apps/server/src/git/Services/TextGeneration.ts
  • apps/server/src/git/Utils.ts
  • apps/server/src/git/isRepo.ts
  • apps/server/src/keybindings.test.ts
  • apps/server/src/keybindings.ts
  • apps/server/src/main.test.ts
  • apps/server/src/main.ts
  • apps/server/src/open.test.ts
  • apps/server/src/open.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.ts
  • apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationEngine.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.ts
  • apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts
  • apps/server/src/orchestration/Layers/ProjectionPipeline.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
  • apps/server/src/orchestration/Schemas.ts
  • apps/server/src/orchestration/Services/CheckpointReactor.ts
  • apps/server/src/orchestration/Services/OrchestrationReactor.ts
  • apps/server/src/orchestration/Services/ProviderCommandReactor.ts
  • apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts
  • apps/server/src/orchestration/commandInvariants.test.ts
  • apps/server/src/orchestration/commandInvariants.ts
  • apps/server/src/orchestration/decider.projectScripts.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/orchestration/projector.test.ts
  • apps/server/src/orchestration/projector.ts
  • apps/server/src/os-jank.test.ts
  • apps/server/src/os-jank.ts
  • apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts
  • apps/server/src/persistence/Layers/OrchestrationEventStore.ts
  • apps/server/src/persistence/Layers/ProjectionProjects.ts
  • apps/server/src/persistence/Layers/ProjectionRepositories.test.ts
  • apps/server/src/persistence/Layers/ProjectionTasks.ts
  • apps/server/src/persistence/Layers/ProjectionThreads.ts
  • apps/server/src/persistence/Layers/Sqlite.ts
  • apps/server/src/persistence/Migrations.ts
  • apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts
  • apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.ts
  • apps/server/src/persistence/Migrations/016_ProjectionTasks.ts
  • apps/server/src/persistence/Migrations/017_ProjectionThreadsArchivedAt.ts
  • apps/server/src/persistence/Migrations/018_ProjectionThreadsArchivedAtIndex.ts
  • apps/server/src/persistence/Migrations/020_BackfillProjectCreatedDefaultModelSelection.test.ts
  • apps/server/src/persistence/Migrations/020_BackfillProjectCreatedDefaultModelSelection.ts
  • apps/server/src/persistence/Migrations/021_BackfillThreadCreatedModelSelection.test.ts
  • apps/server/src/persistence/Migrations/021_BackfillThreadCreatedModelSelection.ts
  • apps/server/src/persistence/Migrations/022_RepairCanonicalModelSelections.test.ts
  • apps/server/src/persistence/Migrations/022_RepairCanonicalModelSelections.ts
  • apps/server/src/persistence/NodeSqliteClient.ts
  • apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts
  • apps/server/src/persistence/Services/ProjectionProjects.ts
  • apps/server/src/persistence/Services/ProjectionTasks.ts
  • apps/server/src/persistence/Services/ProjectionThreads.ts
  • apps/server/src/processRunner.ts
  • apps/server/src/project/Layers/ProjectFaviconResolver.test.ts
  • apps/server/src/project/Layers/ProjectFaviconResolver.ts
  • apps/server/src/project/Services/ProjectFaviconResolver.ts
  • apps/server/src/projectFaviconRoute.test.ts
  • apps/server/src/projectFaviconRoute.ts
  • apps/server/src/provider/Layers/ClaudeAdapter.test.ts
  • apps/server/src/provider/Layers/ClaudeAdapter.ts
  • apps/server/src/provider/Layers/ClaudeProvider.ts
  • apps/server/src/provider/Layers/CodexAdapter.test.ts
  • apps/server/src/provider/Layers/CodexAdapter.ts
  • apps/server/src/provider/Layers/CodexProvider.ts
  • apps/server/src/provider/Layers/ProviderHealth.test.ts
  • apps/server/src/provider/Layers/ProviderHealth.ts
  • apps/server/src/provider/Layers/ProviderRegistry.test.ts
  • apps/server/src/provider/Layers/ProviderRegistry.ts
  • apps/server/src/provider/Layers/ProviderService.test.ts
  • apps/server/src/provider/Layers/ProviderService.ts
  • apps/server/src/provider/Services/ClaudeProvider.ts
  • apps/server/src/provider/Services/CodexProvider.ts
  • apps/server/src/provider/Services/ProviderHealth.ts
  • apps/server/src/provider/Services/ProviderRegistry.ts
  • apps/server/src/provider/Services/ServerProvider.ts
  • apps/server/src/provider/codexAccount.ts
  • apps/server/src/provider/codexAppServer.ts
  • apps/server/src/provider/makeManagedServerProvider.ts
  • apps/server/src/provider/providerSnapshot.ts
  • apps/server/src/serverLayers.ts
  • apps/server/src/serverSettings.test.ts
  • apps/server/src/serverSettings.ts
  • apps/server/src/terminal/Layers/Manager.test.ts
  • apps/server/src/terminal/Layers/Manager.ts
  • apps/server/src/terminal/Services/Manager.ts
  • apps/server/src/workspace/Layers/WorkspaceEntries.test.ts
  • apps/server/src/workspace/Layers/WorkspaceEntries.ts
  • apps/server/src/workspace/Layers/WorkspaceFileSystem.test.ts
  • apps/server/src/workspace/Layers/WorkspaceFileSystem.ts
  • apps/server/src/workspace/Layers/WorkspacePaths.test.ts
  • apps/server/src/workspace/Layers/WorkspacePaths.ts
  • apps/server/src/workspace/Services/WorkspaceEntries.ts
  • apps/server/src/workspace/Services/WorkspaceFileSystem.ts
  • apps/server/src/workspace/Services/WorkspacePaths.ts
  • apps/server/src/workspaceEntries.chunking.test.ts
  • apps/server/src/workspaceEntries.test.ts
  • apps/server/src/workspaceEntries.ts
  • apps/server/src/wsServer.test.ts
  • apps/server/src/wsServer.ts
  • apps/server/src/wsServer/pushBus.test.ts
  • apps/web/index.html
  • apps/web/package.json
  • apps/web/src/appSettings.test.ts
  • apps/web/src/appSettings.ts
  • apps/web/src/components/AppSidebarLayout.tsx
  • apps/web/src/components/BranchToolbar.logic.test.ts
  • apps/web/src/components/BranchToolbar.logic.ts
  • apps/web/src/components/BranchToolbarBranchSelector.tsx
  • apps/web/src/components/ChatMarkdown.tsx
  • apps/web/src/components/ChatView.browser.tsx
  • apps/web/src/components/ChatView.logic.test.ts
  • apps/web/src/components/ChatView.logic.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/DiffPanel.tsx
  • apps/web/src/components/GitActionsControl.tsx
  • apps/web/src/components/KeybindingsToast.browser.tsx
  • apps/web/src/components/PlanSidebar.tsx
  • apps/web/src/components/ProjectFavicon.tsx
  • apps/web/src/components/ProjectScriptsControl.tsx
  • apps/web/src/components/PullRequestThreadDialog.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/components/ThreadTerminalDrawer.tsx
  • apps/web/src/components/chat/ChangedFilesTree.tsx
  • apps/web/src/components/chat/ChatHeader.tsx
  • apps/web/src/components/chat/ClaudeTraitsPicker.browser.tsx
  • apps/web/src/components/chat/ClaudeTraitsPicker.tsx
  • apps/web/src/components/chat/CodexTraitsPicker.browser.tsx
  • apps/web/src/components/chat/CodexTraitsPicker.tsx
  • apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx
  • apps/web/src/components/chat/ComposerCommandMenu.tsx
  • apps/web/src/components/chat/ComposerPrimaryActions.tsx
  • apps/web/src/components/chat/ContextWindowMeter.tsx
  • apps/web/src/components/chat/MessagesTimeline.test.tsx
  • apps/web/src/components/chat/MessagesTimeline.tsx
  • apps/web/src/components/chat/OpenInPicker.tsx
  • apps/web/src/components/chat/ProviderModelPicker.browser.tsx
  • apps/web/src/components/chat/ProviderModelPicker.tsx
  • apps/web/src/components/chat/ProviderStatusBanner.tsx
  • apps/web/src/components/chat/TraitsPicker.browser.tsx
  • apps/web/src/components/chat/TraitsPicker.tsx
  • apps/web/src/components/chat/composerProviderRegistry.test.tsx
  • apps/web/src/components/chat/composerProviderRegistry.tsx
  • apps/web/src/components/composerFooterLayout.test.ts
  • apps/web/src/components/composerFooterLayout.ts
  • apps/web/src/components/desktopUpdate.logic.test.ts
  • apps/web/src/components/desktopUpdate.logic.ts
  • apps/web/src/components/settings/SettingsPanels.tsx
  • apps/web/src/components/settings/SettingsSidebarNav.tsx
  • apps/web/src/components/sidebar/SidebarUpdatePill.tsx
  • apps/web/src/components/ui/autocomplete.tsx
  • apps/web/src/components/ui/combobox.tsx
  • apps/web/src/components/ui/select.tsx
  • apps/web/src/components/ui/sidebar.tsx
  • apps/web/src/components/ui/thinking-indicator.tsx
  • apps/web/src/components/ui/toast.tsx
  • apps/web/src/composerDraftStore.test.ts
  • apps/web/src/composerDraftStore.ts
  • apps/web/src/contextMenuFallback.ts
  • apps/web/src/hooks/useHandleNewThread.ts
  • apps/web/src/hooks/useSettings.test.ts
  • apps/web/src/hooks/useSettings.ts
  • apps/web/src/hooks/useThreadActions.ts
  • apps/web/src/index.css
  • apps/web/src/keybindings.test.ts
  • apps/web/src/keybindings.ts
  • apps/web/src/lib/contextWindow.test.ts
  • apps/web/src/lib/contextWindow.ts
  • apps/web/src/lib/desktopUpdateReactQuery.test.ts
  • apps/web/src/lib/desktopUpdateReactQuery.ts
  • apps/web/src/lib/gitReactQuery.test.ts
  • apps/web/src/lib/gitReactQuery.ts
  • apps/web/src/lib/serverReactQuery.ts
  • apps/web/src/lib/storage.ts
  • apps/web/src/lib/terminalFocus.test.ts
  • apps/web/src/lib/terminalFocus.ts
  • apps/web/src/lib/utils.ts
  • apps/web/src/modelSelection.ts
  • apps/web/src/orchestrationEventEffects.test.ts
  • apps/web/src/orchestrationEventEffects.ts
  • apps/web/src/orchestrationRecovery.test.ts
  • apps/web/src/orchestrationRecovery.ts
  • apps/web/src/projectScripts.test.ts
  • apps/web/src/projectScripts.ts
  • apps/web/src/providerModels.ts
  • apps/web/src/pullRequestReference.test.ts
  • apps/web/src/pullRequestReference.ts
  • apps/web/src/routeTree.gen.ts
  • apps/web/src/router.ts
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/_chat.$threadId.tsx
  • apps/web/src/routes/_chat.index.tsx
  • apps/web/src/routes/_chat.project.$projectId.tsx
  • apps/web/src/routes/_chat.settings.tsx
  • apps/web/src/routes/_chat.tsx
  • apps/web/src/routes/settings.archived.tsx
  • apps/web/src/routes/settings.general.tsx
  • apps/web/src/routes/settings.tsx
  • apps/web/src/session-logic.test.ts
  • apps/web/src/session-logic.ts
  • apps/web/src/store.test.ts
  • apps/web/src/store.ts
  • apps/web/src/storeSelectors.ts
  • apps/web/src/terminal-links.test.ts
  • apps/web/src/terminal-links.ts
  • apps/web/src/terminalStateStore.test.ts
  • apps/web/src/terminalStateStore.ts
  • apps/web/src/timestampFormat.ts
  • apps/web/src/truncateTitle.test.ts
  • apps/web/src/types.ts
  • apps/web/src/uiStateStore.test.ts
  • apps/web/src/uiStateStore.ts
  • apps/web/src/worktreeCleanup.test.ts
  • apps/web/src/wsNativeApi.test.ts
  • apps/web/src/wsNativeApi.ts
  • apps/web/src/wsTransport.test.ts
  • apps/web/src/wsTransport.ts
  • docs/effect-fn-checklist.md
  • package.json
  • packages/contracts/package.json
  • packages/contracts/src/baseSchemas.ts
  • packages/contracts/src/editor.ts
  • packages/contracts/src/git.test.ts
  • packages/contracts/src/git.ts
  • packages/contracts/src/index.ts
  • packages/contracts/src/ipc.ts
  • packages/contracts/src/keybindings.test.ts
  • packages/contracts/src/keybindings.ts
  • packages/contracts/src/model.ts
  • packages/contracts/src/orchestration.test.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/provider.test.ts
  • packages/contracts/src/provider.ts
  • packages/contracts/src/providerRuntime.test.ts
  • packages/contracts/src/providerRuntime.ts
  • packages/contracts/src/server.ts
  • packages/contracts/src/settings.ts
  • packages/contracts/src/terminal.ts
  • packages/contracts/src/ws.test.ts
  • packages/contracts/src/ws.ts
  • packages/shared/package.json
  • packages/shared/src/DrainableWorker.ts
  • packages/shared/src/KeyedCoalescingWorker.test.ts
  • packages/shared/src/KeyedCoalescingWorker.ts
  • packages/shared/src/String.test.ts
  • packages/shared/src/String.ts
  • packages/shared/src/Struct.ts
  • packages/shared/src/model.test.ts
  • packages/shared/src/model.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Unified user-data directory behavior for desktop and server; updated web fonts and theme variables; multiple UI layout and styling adjustments across chat, sidebar, diff, and branch toolbar components; added a reusable ThinkingIndicator component; and introduced a new project detail route at /_chat/project/$projectId.

Changes

Cohort / File(s) Summary
App data paths
apps/desktop/src/main.ts, apps/server/src/config.ts
Desktop now always sets userData dir to "t3code". Server deriveServerPaths always sets stateDir = join(baseDir, "userdata"); derived paths (db, logs, attachments, etc.) consistently resolve under userdata.
Web fonts & global styles
apps/web/index.html, apps/web/src/index.css
Replaced DM Sans with Instrument Serif, Inter, Nunito Sans, JetBrains Mono; added new theme color tokens (volt/signal/coral/iris, sidebar), updated radius math, new shimmer animation, .inverse-border-radius-mask, and swapped body/code fonts.
Chat layout & header
apps/web/src/components/ChatView.tsx, apps/web/src/components/chat/ChatHeader.tsx
Moved BranchToolbar into ChatHeader via new optional branchToolbar?: ReactNode prop; adjusted container backgrounds, wrapped content in new masked/bg wrappers, and increased messages vertical padding.
Branch toolbar & selector
apps/web/src/components/BranchToolbar.tsx, apps/web/src/components/BranchToolbarBranchSelector.tsx
Simplified outer layout classes of BranchToolbar; BranchSelector now uses cn helper for active item classes and tweaks to input nested selector (!font-sans).
Messages timeline & thinking indicator
apps/web/src/components/chat/MessagesTimeline.tsx, apps/web/src/components/ui/thinking-indicator.tsx
Added ThinkingIndicator component (forwardRef) and replaced in-place working UI with it; adjusted virtual row height when turn diffs exist, refactored message bubble and grouping styles, and changed user message typography to font-sans.
Sidebar & related UI
apps/web/src/components/Sidebar.tsx, apps/web/src/components/ui/sidebar.tsx
formatRelativeTime now returns full pluralized units; project activation navigates to /project/$projectId; expand/collapse toggles reworked into tooltip-wrapped SidebarMenuAction; removed SidebarSeparator and adjusted submenu/side border utilities; thread status label removal on larger screens.
Diff panel & changed files tree
apps/web/src/components/DiffPanel.tsx, apps/web/src/components/chat/ChangedFilesTree.tsx
Adjusted separator spacing and updated edge fade overlays to be flush and wider; directory node folder-icon conditional commented out (only chevron + name shown).
Routing: project detail
apps/web/src/routeTree.gen.ts, apps/web/src/routes/_chat.project.$projectId.tsx
Added generated route entry for /_chat/project/$projectId and new route file rendering project details, threads list, scripts, and favicon fetch logic (with Electron/WebSocket handling helper).
Layout wrapper changes
apps/web/src/routes/_chat.tsx
Sidebar border utility tweaks; main content now wrapped in nested flex containers with rounded/bordered bg-background wrappers and overflow constraints.
Misc
.gitignore
Added .t3-opensoftware to ignore patterns.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through fonts and tidy lanes,

Paths now one — no dev-domain strains.
Indicators hum while changes cheer,
Toolbars nestle, sidebars steer,
A rabbit applauds the polished scene! 🥕✨

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is sparse bullet points that lack the detailed context required by the template. Required sections like 'What Changed', 'Why', 'UI Changes', and checklist items are completely absent or unfilled. Fill the template with detailed explanations: expand 'What Changed' section, explain 'Why' these changes solve a problem, add before/after screenshots for UI changes, and complete all checklist items.
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.
Title check ❓ Inconclusive The title 'Refresh desktop and chat UI styling' is partially related to the changeset—it accurately describes UI styling updates to chat components but overlooks the significant backend changes to user data directory handling and server state paths. Consider a more comprehensive title like 'Unify userdata directory and refresh UI styling' to capture both the backend state management and UI changes.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ui-refresh

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

❤️ Share

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

- Add `.t3-opensoftware` to `.gitignore`
- Remove tracked local state and keybinding files

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/server/src/config.ts (1)

49-72: Remove unused devUrl parameter from deriveServerPaths function.

The devUrl parameter (line 51) is no longer used in the function body after the change to always use "userdata" for stateDir. This dead code can be cleaned up by removing the parameter and updating all call sites (in main.ts, config.ts, and test files).

♻️ Proposed cleanup
-export const deriveServerPaths = Effect.fn(function* (
-  baseDir: ServerConfigShape["baseDir"],
-  devUrl: ServerConfigShape["devUrl"],
-): Effect.fn.Return<ServerDerivedPaths, never, Path.Path> {
+export const deriveServerPaths = Effect.fn(function* (
+  baseDir: ServerConfigShape["baseDir"],
+): Effect.fn.Return<ServerDerivedPaths, never, Path.Path> {
   const { join } = yield* Path.Path;
   const stateDir = join(baseDir, "userdata");

Call sites requiring updates: main.ts (line 156), config.ts (line 91), test files in wsServer.test.ts, ProviderCommandReactor.test.ts, and OrchestrationEngineHarness.integration.ts.

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

In `@apps/server/src/config.ts` around lines 49 - 72, The deriveServerPaths
function contains an unused devUrl parameter—remove devUrl from the
deriveServerPaths signature (including its Effect.fn.Return generic) and update
every call site that passes devUrl (e.g., callers in main.ts, config.ts, and
tests: wsServer.test.ts, ProviderCommandReactor.test.ts,
OrchestrationEngineHarness.integration.ts) to call deriveServerPaths with only
the baseDir argument; also update any related type annotations
(ServerConfigShape usage) and imports so the function signature and all
references compile cleanly.
🤖 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/MessagesTimeline.tsx`:
- Around line 377-412: The image grid forces two columns via the fixed
"grid-cols-2" class causing a single attachment in userImages to render too
small; update the container class (in MessagesTimeline.tsx where userImages is
mapped) to conditionally use "grid-cols-1" when userImages.length === 1 and
"grid-cols-2" otherwise (use whatever className/utility helper you use in the
project, e.g., template literal or clsx), leaving the rest of the logic
(buildExpandedImagePreview, onImageExpand, onTimelineImageLoad handlers)
unchanged.

In `@apps/web/src/components/ui/thinking-indicator.tsx`:
- Around line 49-97: The live region currently uses role="status" and includes
the continuously-updating timer text (displayText/longestWord) which causes
repeated screen-reader announcements; keep a static accessible label inside the
role="status" wrapper and mark the animated/timer spans aria-hidden="true" so
they are ignored by AT. Concretely, in the ThinkingIndicator component locate
the outer element with role="status" and the two inner spans (the invisible
spacer showing longestWord and the animating span showing displayText) and add
aria-hidden="true" to both timer/animated spans while ensuring a short static
text node (e.g., "Thinking...") remains in the live region for status
announcements.
- Around line 23-46: Ensure the component treats the words prop as non-empty: if
words is an empty array, fallback to defaultWords before using it (use a local
variable like effectiveWords) and use effectiveWords everywhere (interval,
reduce, displayText). When effectiveWords changes (or its length changes) clamp
or reset the index state (setIndex to 0 or index % effectiveWords.length) inside
the same effect or a separate effect to avoid NaN/out-of-bounds. Also use
effectiveWords for the longestWord reduce to avoid calling reduce on an empty
array.

In `@apps/web/src/index.css`:
- Line 121: Stylelint is flagging Tailwind v4 at-rules in apps/web/src/index.css
(e.g., `@custom-variant`, `@theme`, `@variant`) because the project only extends
stylelint-config-standard-scss; update the Stylelint config (.stylelintrc.json)
to include a Tailwind-aware config (add
"@dreamsicle.io/stylelint-config-tailwindcss" to "extends") or relax the SCSS
at-rule rule by adding the Tailwind directives to "ignoreAtRules" (include
import, custom-variant, theme, variant, apply, layer) so those at-rules
validate, and also remove unnecessary quotes around the SFMono-Regular
font-family entry and address the deprecated "word-break: break-word" and the
missing blank line before a declaration as reported.

---

Nitpick comments:
In `@apps/server/src/config.ts`:
- Around line 49-72: The deriveServerPaths function contains an unused devUrl
parameter—remove devUrl from the deriveServerPaths signature (including its
Effect.fn.Return generic) and update every call site that passes devUrl (e.g.,
callers in main.ts, config.ts, and tests: wsServer.test.ts,
ProviderCommandReactor.test.ts, OrchestrationEngineHarness.integration.ts) to
call deriveServerPaths with only the baseDir argument; also update any related
type annotations (ServerConfigShape usage) and imports so the function signature
and all references compile cleanly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af0ffadf-eba8-4356-82c2-a1ff29aa5993

📥 Commits

Reviewing files that changed from the base of the PR and between e823f8f and 1024f7c.

📒 Files selected for processing (17)
  • .t3-opensoftware/userdata/keybindings.json
  • .t3-opensoftware/userdata/state.sqlite
  • .t3-opensoftware/userdata/state.sqlite-shm
  • .t3-opensoftware/userdata/state.sqlite-wal
  • apps/desktop/src/main.ts
  • apps/server/src/config.ts
  • apps/web/index.html
  • apps/web/src/components/BranchToolbar.tsx
  • apps/web/src/components/BranchToolbarBranchSelector.tsx
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/DiffPanel.tsx
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/components/chat/ChatHeader.tsx
  • apps/web/src/components/chat/MessagesTimeline.tsx
  • apps/web/src/components/ui/sidebar.tsx
  • apps/web/src/components/ui/thinking-indicator.tsx
  • apps/web/src/index.css

Comment on lines +377 to +412
{userImages.length > 0 && (
<div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">
{userImages.map(
(image: NonNullable<TimelineMessage["attachments"]>[number]) => (
<div
key={image.id}
className="overflow-hidden rounded-lg border border-border/80 bg-background/70"
>
{image.previewUrl ? (
<button
type="button"
className="h-full w-full cursor-zoom-in"
aria-label={`Preview ${image.name}`}
onClick={() => {
const preview = buildExpandedImagePreview(userImages, image.id);
if (!preview) return;
onImageExpand(preview);
}}
>
<img
src={image.previewUrl}
alt={image.name}
className="h-full max-h-[220px] w-full object-cover"
onLoad={onTimelineImageLoad}
onError={onTimelineImageLoad}
/>
</button>
) : (
<div className="flex min-h-[72px] items-center justify-center px-2 py-3 text-center text-[11px] text-muted-foreground/70">
{image.name}
</div>
)}
</div>
),
)}
</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 | 🟡 Minor

Use a single column when there’s only one attachment.

With a fixed grid-cols-2, a single uploaded image only occupies half of the available bubble width and renders much smaller than necessary. Switch to one column for userImages.length === 1.

🖼️ Possible fix
-                    <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">
+                    <div
+                      className={cn(
+                        "mb-2 grid max-w-[420px] gap-2",
+                        userImages.length === 1 ? "grid-cols-1" : "grid-cols-2",
+                      )}
+                    >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/MessagesTimeline.tsx` around lines 377 - 412,
The image grid forces two columns via the fixed "grid-cols-2" class causing a
single attachment in userImages to render too small; update the container class
(in MessagesTimeline.tsx where userImages is mapped) to conditionally use
"grid-cols-1" when userImages.length === 1 and "grid-cols-2" otherwise (use
whatever className/utility helper you use in the project, e.g., template literal
or clsx), leaving the rest of the logic (buildExpandedImagePreview,
onImageExpand, onTimelineImageLoad handlers) unchanged.

Comment on lines +23 to +46
({ className, elapsed, words = defaultWords, ...props }, ref) => {
const [index, setIndex] = useState(0);
const [animating, setAnimating] = useState(false);

useEffect(() => {
const interval = setInterval(() => {
setAnimating(true);
// Small delay to allow exit animation before changing word
setTimeout(() => {
setIndex((i) => (i + 1) % words.length);
setAnimating(false);
}, 160);
}, 4000);
return () => clearInterval(interval);
}, [words.length]);

// Find the longest word for invisible spacer
const longestWord = words.reduce((a, b) =>
a.length >= b.length ? a : b,
);

const displayText = elapsed
? `${words[index]} for ${elapsed}`
: `${words[index]}...`;

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

Treat words as a non-empty list before cycling it.

Line 32 assumes words.length > 0, and Line 40 calls reduce() with no fallback. Passing words={[]} will either throw during render or drive index to NaN. Fallback to defaultWords when the array is empty, and clamp/reset index when the list changes.

🛠️ Possible fix
 const ThinkingIndicator = forwardRef<HTMLDivElement, ThinkingIndicatorProps>(
   ({ className, elapsed, words = defaultWords, ...props }, ref) => {
+    const cycleWords = words.length > 0 ? words : defaultWords;
     const [index, setIndex] = useState(0);
     const [animating, setAnimating] = useState(false);
 
+    useEffect(() => {
+      setIndex((current) => current % cycleWords.length);
+    }, [cycleWords.length]);
+
     useEffect(() => {
       const interval = setInterval(() => {
         setAnimating(true);
         // Small delay to allow exit animation before changing word
         setTimeout(() => {
-          setIndex((i) => (i + 1) % words.length);
+          setIndex((i) => (i + 1) % cycleWords.length);
           setAnimating(false);
         }, 160);
       }, 4000);
       return () => clearInterval(interval);
-    }, [words.length]);
+    }, [cycleWords.length]);
 
     // Find the longest word for invisible spacer
-    const longestWord = words.reduce((a, b) =>
+    const longestWord = cycleWords.reduce((a, b) =>
       a.length >= b.length ? a : b,
     );
 
     const displayText = elapsed
-      ? `${words[index]} for ${elapsed}`
-      : `${words[index]}...`;
+      ? `${cycleWords[index] ?? cycleWords[0]} for ${elapsed}`
+      : `${cycleWords[index] ?? cycleWords[0]}...`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
({ className, elapsed, words = defaultWords, ...props }, ref) => {
const [index, setIndex] = useState(0);
const [animating, setAnimating] = useState(false);
useEffect(() => {
const interval = setInterval(() => {
setAnimating(true);
// Small delay to allow exit animation before changing word
setTimeout(() => {
setIndex((i) => (i + 1) % words.length);
setAnimating(false);
}, 160);
}, 4000);
return () => clearInterval(interval);
}, [words.length]);
// Find the longest word for invisible spacer
const longestWord = words.reduce((a, b) =>
a.length >= b.length ? a : b,
);
const displayText = elapsed
? `${words[index]} for ${elapsed}`
: `${words[index]}...`;
({ className, elapsed, words = defaultWords, ...props }, ref) => {
const cycleWords = words.length > 0 ? words : defaultWords;
const [index, setIndex] = useState(0);
const [animating, setAnimating] = useState(false);
useEffect(() => {
setIndex((current) => current % cycleWords.length);
}, [cycleWords.length]);
useEffect(() => {
const interval = setInterval(() => {
setAnimating(true);
// Small delay to allow exit animation before changing word
setTimeout(() => {
setIndex((i) => (i + 1) % cycleWords.length);
setAnimating(false);
}, 160);
}, 4000);
return () => clearInterval(interval);
}, [cycleWords.length]);
// Find the longest word for invisible spacer
const longestWord = cycleWords.reduce((a, b) =>
a.length >= b.length ? a : b,
);
const displayText = elapsed
? `${cycleWords[index] ?? cycleWords[0]} for ${elapsed}`
: `${cycleWords[index] ?? cycleWords[0]}...`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ui/thinking-indicator.tsx` around lines 23 - 46,
Ensure the component treats the words prop as non-empty: if words is an empty
array, fallback to defaultWords before using it (use a local variable like
effectiveWords) and use effectiveWords everywhere (interval, reduce,
displayText). When effectiveWords changes (or its length changes) clamp or reset
the index state (setIndex to 0 or index % effectiveWords.length) inside the same
effect or a separate effect to avoid NaN/out-of-bounds. Also use effectiveWords
for the longestWord reduce to avoid calling reduce on an empty array.

Comment on lines +49 to +97
<div
ref={ref}
role="status"
className={cn("flex items-center gap-2 px-1.5 py-1", className)}
{...props}
>
<svg
aria-hidden
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="text-muted-foreground/60 shrink-0"
>
<path className="thinking-morph" d={circleA}>
<animate
attributeName="d"
values={`${circleA};${infinity};${circleB};${infinity};${circleA}`}
dur="6s"
repeatCount="indefinite"
calcMode="spline"
keySplines="0.4 0 0.2 1; 0.4 0 0.2 1; 0.4 0 0.2 1; 0.4 0 0.2 1"
keyTimes="0; 0.25; 0.5; 0.75; 1"
/>
</path>
</svg>
<span className="inline-grid text-[11px] overflow-hidden">
{/* Invisible spacer for stable width */}
<span
className="col-start-1 row-start-1 invisible shimmer-text"
aria-hidden="true"
>
{longestWord} for 99m 59s
</span>
<span
className={cn(
"col-start-1 row-start-1 shimmer-text transition-all duration-200",
animating
? "opacity-0 -translate-y-2"
: "opacity-100 translate-y-0",
)}
>
{displayText}
</span>
</span>

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

Keep the ticking timer out of the live region.

This wrapper is role="status", and MessagesTimeline feeds it an elapsed string that changes continuously. That ends up re-announcing the status on every timer update for screen-reader users. Keep a static accessible label here and mark the animated/timer text aria-hidden.

♿ Possible fix
       <div
         ref={ref}
         role="status"
+        aria-label="Assistant is working"
         className={cn("flex items-center gap-2 px-1.5 py-1", className)}
         {...props}
       >
@@
-        <span className="inline-grid text-[11px] overflow-hidden">
+        <span aria-hidden="true" className="inline-grid overflow-hidden text-[11px]">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ui/thinking-indicator.tsx` around lines 49 - 97, The
live region currently uses role="status" and includes the continuously-updating
timer text (displayText/longestWord) which causes repeated screen-reader
announcements; keep a static accessible label inside the role="status" wrapper
and mark the animated/timer spans aria-hidden="true" so they are ignored by AT.
Concretely, in the ThinkingIndicator component locate the outer element with
role="status" and the two inner spans (the invisible spacer showing longestWord
and the animating span showing displayText) and add aria-hidden="true" to both
timer/animated spans while ensuring a short static text node (e.g.,
"Thinking...") remains in the live region for status announcements.

Comment thread apps/web/src/index.css
--sidebar-border: oklch(0.922 0.005 34.3);
--sidebar-ring: oklch(0.714 0.014 41.2);

@variant dark {

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant sections
wc -l apps/web/src/index.css

Repository: emphoni/t3code

Length of output: 84


🏁 Script executed:

# Read around line 121 to see the `@variant` context
sed -n '115,130p' apps/web/src/index.css

Repository: emphoni/t3code

Length of output: 667


🏁 Script executed:

# Read around line 203 to see the font-family issue
sed -n '197,210p' apps/web/src/index.css

Repository: emphoni/t3code

Length of output: 299


🏁 Script executed:

# Check for Stylelint configuration files
fd -e '.stylelintrc*' -e 'stylelint.config.*' | head -20

Repository: emphoni/t3code

Length of output: 40


🏁 Script executed:

# Look for Stylelint config in package.json or other config locations
find . -maxdepth 2 -type f \( -name "stylelint.config.js" -o -name ".stylelintrc*" -o -name "package.json" \) | xargs grep -l "stylelint" 2>/dev/null | head -10

Repository: emphoni/t3code

Length of output: 77


🏁 Script executed:

# Read the Stylelint configuration
cat .stylelintrc.json

Repository: emphoni/t3code

Length of output: 1064


🏁 Script executed:

# Check if there are any Tailwind CSS plugins configured in Stylelint
cat .stylelintrc.json | jq '.plugins' 2>/dev/null || cat .stylelintrc.json

Repository: emphoni/t3code

Length of output: 62


🏁 Script executed:

# Search for any Tailwind CSS Stylelint plugin setup
rg "tailwindcss|@tailwind|tailwind-css" --type json -A 2 -B 2

Repository: emphoni/t3code

Length of output: 1321


🏁 Script executed:

# Check package.json for Stylelint configuration or Tailwind plugin
cat apps/web/package.json 2>/dev/null || cat package.json | head -100

Repository: emphoni/t3code

Length of output: 2020


🏁 Script executed:

# Check if there's a way to run Stylelint or if there are any lint reports/error files
find . -type f \( -name "*.json" -o -name "*.txt" -o -name "*.log" \) | xargs grep -l "stylelint\|@variant\|SFMono" 2>/dev/null | head -10

Repository: emphoni/t3code

Length of output: 119


🏁 Script executed:

# Check if `@variant` is a standard Tailwind CSS directive
web_search "Tailwind CSS `@variant` at-rule Tailwind 4.0"

Repository: emphoni/t3code

Length of output: 108


🏁 Script executed:

# Let's verify the exact context around both lines to understand the structure
sed -n '119,125p' apps/web/src/index.css | cat -n

Repository: emphoni/t3code

Length of output: 315


🏁 Script executed:

# Check the full context of line 203 to see what the actual rule is
sed -n '200,207p' apps/web/src/index.css | cat -n

Repository: emphoni/t3code

Length of output: 303


🏁 Script executed:

# See if there's a Tailwind CSS Stylelint plugin configured anywhere
rg "stylelint-tailwindcss|@tailwindcss.*stylelint|tailwind.*stylelint"

Repository: emphoni/t3code

Length of output: 40


🌐 Web query:

Tailwind CSS 4.0 @variant at-rule syntax documentation

💡 Result:

The @variant at-rule in Tailwind CSS 4.0 is used within custom CSS to apply Tailwind variants (like dark, hover, responsive) to your custom styles. Basic syntax: .my-element { background: white; @variant dark { background: black; } } This compiles to: .my-element { background: white; } @media (prefers-color-scheme: dark) { .my-element { background: black; } } For multiple variants, nest them: .foo { @variant hover { @variant focus { color: red; } } } Note: @custom-variant is for defining new custom variants (e.g., @custom-variant my-variant (&:where(.my-class *));), while @variant applies existing Tailwind variants to custom CSS rules. Official documentation references this in Functions and directives (https://tailwindcss.com/docs/functions-and-directives) and Adding custom styles (https://tailwindcss.com/docs/adding-custom-styles), with implementation details from the merged PR #15663 (tailwindlabs/tailwindcss#15663).

Citations:


🌐 Web query:

Stylelint Tailwind CSS @variant plugin configuration

💡 Result:

To configure Stylelint for Tailwind CSS @variant and @custom-variant directives (Tailwind CSS v4+), use a dedicated Tailwind-aware config that defines these as valid syntax in languageOptions. Recommended approach: Install and extend @dreamsicle.io/stylelint-config-tailwindcss (latest v1.2.2 as of Jan 2026), designed specifically for Tailwind v4+ and Stylelint v16.17+. It implements all Tailwind directives including @variant, @custom-variant, @theme, @utility, @apply, @layer as proper syntax, enabling validation instead of just ignoring. Installation: npm install --save-dev @dreamsicle.io/stylelint-config-tailwindcss stylelint stylelint-config-standard Configuration (stylelint.config.mjs): / * @type {import("stylelint").Config} */ const stylelintConfig = { extends: [ "stylelint-config-standard", "@dreamsicle.io/stylelint-config-tailwindcss", ], }; export default stylelintConfig; This config spreads Tailwind's syntax into languageOptions.atRules, types, properties, etc., so Stylelint recognizes: @import "tailwindcss"; @custom-variant js (.js &); @variant dark { @slot; } Alternative for Tailwind v3 or minimal setup: Manually override rules in stylelint.config.js: module.exports = { extends: ['stylelint-config-standard'], rules: { 'at-rule-no-unknown': [true, { ignoreAtRules: ['tailwind', 'apply', 'layer', 'variants', 'responsive', 'screen', 'theme', 'utility', 'variant', 'custom-variant', 'plugin', 'source', 'config'] }], 'no-descending-specificity': null, 'declaration-block-trailing-semicolon': null, }, }; This ignores the directives but doesn't validate their syntax/content. The @dreamsicle.io config is preferred for Tailwind v4 as it was discussed in Tailwind's GitHub and generates syntax from Tailwind's at-rules for full support.

Citations:


🏁 Script executed:

# Let's check if there's any CSS nesting or understanding of the file structure
head -130 apps/web/src/index.css | tail -20

Repository: emphoni/t3code

Length of output: 832


🏁 Script executed:

# Check the broader context - is `@variant` inside something or standalone?
sed -n '110,135p' apps/web/src/index.css

Repository: emphoni/t3code

Length of output: 1074


🏁 Script executed:

# Look for any other `@variant` usages in the file
rg "@variant" apps/web/src/index.css

Repository: emphoni/t3code

Length of output: 75


🏁 Script executed:

# Check if there are any lint configuration overrides or plugin references
find . -maxdepth 3 -type f -name "tailwind.config.*" -o -name "vite.config.*" | head -5

Repository: emphoni/t3code

Length of output: 83


🏁 Script executed:

# Check if package.json has the Tailwind Stylelint plugin listed
rg "@dreamsicle|stylelint.*tailwind|tailwind.*stylelint" apps/web/package.json

Repository: emphoni/t3code

Length of output: 40


🏁 Script executed:

# Verify the exact content and context - is `@variant` at the root or nested in a rule?
sed -n '105,140p' apps/web/src/index.css | cat -A

Repository: emphoni/t3code

Length of output: 1510


🏁 Script executed:

# Check the full file to understand if it's using CSS nesting
sed -n '1,50p' apps/web/src/index.css

Repository: emphoni/t3code

Length of output: 2139


🏁 Script executed:

# Let's check the actual line count to verify line 203 corresponds to the font-family
sed -n '200,210p' apps/web/src/index.css | cat -n

Repository: emphoni/t3code

Length of output: 368


🏁 Script executed:

# Try to run Stylelint to see the actual errors reported
cd apps/web && npx stylelint src/index.css 2>&1 | head -50

Repository: emphoni/t3code

Length of output: 839


🏁 Script executed:

# Check if the specific `@variant` dark block is at root level or nested
grep -n "@variant\|@theme\|@custom-variant\|^}" apps/web/src/index.css | head -20

Repository: emphoni/t3code

Length of output: 243


🏁 Script executed:

# Check the root cause more clearly - confirm these are Tailwind v4 directives
head -10 apps/web/src/index.css

Repository: emphoni/t3code

Length of output: 390


🏁 Script executed:

# Verify package.json has tailwindcss v4
grep "tailwindcss" apps/web/package.json

Repository: emphoni/t3code

Length of output: 121


Configure Stylelint for Tailwind CSS v4 directives instead of rewriting valid code.

This hunk fails Stylelint checks due to Tailwind v4 at-rules (@custom-variant, @theme, @variant) not being recognized. The .stylelintrc.json only extends stylelint-config-standard-scss without Tailwind support.

Add @dreamsicle.io/stylelint-config-tailwindcss to your Stylelint config:

npm install --save-dev `@dreamsicle.io/stylelint-config-tailwindcss`

Then update .stylelintrc.json:

{
  "extends": [
    "stylelint-config-standard-scss",
    "@dreamsicle.io/stylelint-config-tailwindcss"
  ],
  ...
}

Alternatively, disable the at-rule check for Tailwind directives by adding to the rules section:

"scss/at-rule-no-unknown": [true, { "ignoreAtRules": ["import", "custom-variant", "theme", "variant", "apply", "layer"] }]

Also remove unnecessary quotes from SFMono-Regular at line 203:

Font family fix
-  font-family: "JetBrains Mono", "SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
+  font-family: "JetBrains Mono", "SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;

Note: Stylelint also reports deprecated word-break: break-word at line 312 and a missing empty line before declaration at line 503.

🧰 Tools
🪛 Stylelint (17.4.0)

[error] 121-121: Unexpected unknown at-rule "@variant" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

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

In `@apps/web/src/index.css` at line 121, Stylelint is flagging Tailwind v4
at-rules in apps/web/src/index.css (e.g., `@custom-variant`, `@theme`, `@variant`)
because the project only extends stylelint-config-standard-scss; update the
Stylelint config (.stylelintrc.json) to include a Tailwind-aware config (add
"@dreamsicle.io/stylelint-config-tailwindcss" to "extends") or relax the SCSS
at-rule rule by adding the Tailwind directives to "ignoreAtRules" (include
import, custom-variant, theme, variant, apply, layer) so those at-rules
validate, and also remove unnecessary quotes around the SFMono-Regular
font-family entry and address the deprecated "word-break: break-word" and the
missing blank line before a declaration as reported.

vkiooz and others added 2 commits March 25, 2026 11:51
Wrap the chat panel in a rounded card container with subtle border
and shadow. Add inverse/negative border radius CSS mask effect on
the content area below the header for a polished card-like appearance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/web/src/components/chat/MessagesTimeline.tsx (1)

377-379: ⚠️ Potential issue | 🟡 Minor

Let a lone attachment use the full bubble width.

A single uploaded image still renders in a two-column grid, so it only gets half of the available bubble width. Switching to one column when userImages.length === 1 keeps single-image messages readable.

🖼️ Suggested fix
-                    <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">
+                    <div
+                      className={cn(
+                        "mb-2 grid max-w-[420px] gap-2",
+                        userImages.length === 1 ? "grid-cols-1" : "grid-cols-2",
+                      )}
+                    >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/MessagesTimeline.tsx` around lines 377 - 379,
The image attachments grid in MessagesTimeline renders all userImages in two
columns, causing a single image to occupy only half the bubble; update the
rendering logic where userImages.map is used to conditionally set the grid
columns based on the array length (e.g., use a class or conditional that applies
"grid-cols-1" when userImages.length === 1 and "grid-cols-2" otherwise) so a
lone attachment uses the full bubble width.
apps/web/src/index.css (1)

121-121: ⚠️ Potential issue | 🟠 Major

This stylesheet still fails lint as written.

@variant is valid Tailwind v4 syntax here, but the current Stylelint setup still flags it, and "SFMono-Regular" also trips font-family-name-quotes. Please carry the Tailwind-aware Stylelint config change and unquote that family name before merge.

Also applies to: 203-203

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

In `@apps/web/src/index.css` at line 121, Update the Stylelint config to recognize
Tailwind v4 directives so the `@variant` dark at-rule is allowed (add
Tailwind-aware at-rule whitelist or use stylelint-config-tailwindcss), and
remove the unnecessary quotes around the SFMono-Regular font-family entry
(change "SFMono-Regular" to SFMono-Regular) to satisfy font-family-name-quotes;
ensure these changes are applied alongside the existing lint config so the
`@variant` rule and unquoted font family no longer cause errors.
🧹 Nitpick comments (4)
apps/web/src/index.css (1)

299-303: Make inverse-border-radius-mask self-contained.

The pseudo-elements only anchor correctly because the caller adds relative separately. Folding that into the utility will keep the effect from breaking the next time this class is reused.

♻️ Suggested refactor
 .inverse-border-radius-mask {
   --ibr-size: 12px;
+
+  position: relative;
   border-top-left-radius: var(--ibr-size);
   border-top-right-radius: var(--ibr-size);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/index.css` around lines 299 - 303, The
.inverse-border-radius-mask utility is not self-contained because it relies on
callers to set position: relative for its pseudo-elements to anchor correctly;
update the .inverse-border-radius-mask rule to include position: relative (and
any necessary positioning/display like overflow: hidden if the mask expects
clipping) so the pseudo-elements tie to this element without requiring callers
to add relative.
apps/web/src/routes/_chat.project.$projectId.tsx (3)

58-61: Consider sorting threads for consistent UX.

The sidebar sorts threads by createdAt descending (most recent first), but here threads are rendered in their default store order. This may lead to inconsistent ordering between the sidebar and this detail view.

♻️ Suggested refactor
 const threads = useMemo(
-  () => allThreads.filter((t) => t.projectId === projectId),
+  () =>
+    allThreads
+      .filter((t) => t.projectId === projectId)
+      .toSorted((a, b) => {
+        const byDate = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
+        if (byDate !== 0) return byDate;
+        return b.id.localeCompare(a.id);
+      }),
   [allThreads, projectId],
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_chat.project`.$projectId.tsx around lines 58 - 61, The
threads useMemo currently filters allThreads by projectId but doesn't sort,
causing inconsistent order with the sidebar; update the useMemo (the threads
constant created via useMemo that references allThreads and projectId) to return
allThreads.filter(t => t.projectId === projectId).sort(...) where you sort by
t.createdAt descending (newest first) so the detail view matches the sidebar
ordering; ensure the dependency array remains [allThreads, projectId].

9-45: Extract duplicated code to shared utilities.

getServerHttpOrigin() and ProjectFavicon are duplicated from Sidebar.tsx. Consider extracting these to shared modules:

  • getServerHttpOrigin()~/lib/serverOrigin.ts or similar utility
  • ProjectFavicon~/components/ProjectFavicon.tsx

This will improve maintainability and ensure consistent behavior across both usages.

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

In `@apps/web/src/routes/_chat.project`.$projectId.tsx around lines 9 - 45,
Extract the duplicated getServerHttpOrigin function and ProjectFavicon component
into shared modules and import them here: move getServerHttpOrigin (and the
serverHttpOrigin constant if you prefer) into a utility file like
~/lib/serverOrigin.ts exporting the function (and optionally a derived
serverHttpOrigin constant), and move ProjectFavicon into
~/components/ProjectFavicon.tsx exporting the React component; then replace the
local definitions in _chat.project.$projectId.tsx with imports of
getServerHttpOrigin (or serverHttpOrigin) and ProjectFavicon and remove the
duplicated code so both this file and Sidebar.tsx consume the shared
implementations.

73-80: Move scriptIconMap outside the component.

This constant object is recreated on every render. Move it to module scope since it doesn't depend on props or state.

♻️ Suggested refactor
+const SCRIPT_ICON_MAP: Record<string, string> = {
+  play: "▶",
+  test: "🧪",
+  lint: "🔍",
+  build: "🔨",
+  debug: "🐛",
+  configure: "⚙",
+};
+
 function ProjectDetailRouteView() {
   // ...
-  const scriptIconMap: Record<string, string> = {
-    play: "▶",
-    test: "🧪",
-    lint: "🔍",
-    build: "🔨",
-    debug: "🐛",
-    configure: "⚙",
-  };

Then update the usage on line 142:

-<span className="text-base">{scriptIconMap[script.icon] ?? "▶"}</span>
+<span className="text-base">{SCRIPT_ICON_MAP[script.icon] ?? "▶"}</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_chat.project`.$projectId.tsx around lines 73 - 80, The
scriptIconMap constant is being recreated on every render; move the declaration
of scriptIconMap (currently a Record<string,string> with keys
play,test,lint,build,debug,configure) out of the React component to module scope
(top-level of the file) so it is allocated once, keep the same name and type,
and leave its usage inside the component (where it's referenced around line 142)
unchanged so consumers still access scriptIconMap by name.
🤖 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/ChangedFilesTree.tsx`:
- Around line 64-68: Directory labels shifted because the folder icon was
removed in ChangedFilesTree (the commented FolderIcon/FolderClosedIcon block),
so add a fixed-width spacer where the icons used to render when icons are
intentionally hidden; e.g., in the same place as FolderIcon/FolderClosedIcon
render an empty element with the same sizing classes (the "size-3.5 shrink-0
text-muted-foreground/75" styles or equivalent) so directory names align with
file names at the same depth and the tree columns line up.

---

Duplicate comments:
In `@apps/web/src/components/chat/MessagesTimeline.tsx`:
- Around line 377-379: The image attachments grid in MessagesTimeline renders
all userImages in two columns, causing a single image to occupy only half the
bubble; update the rendering logic where userImages.map is used to conditionally
set the grid columns based on the array length (e.g., use a class or conditional
that applies "grid-cols-1" when userImages.length === 1 and "grid-cols-2"
otherwise) so a lone attachment uses the full bubble width.

In `@apps/web/src/index.css`:
- Line 121: Update the Stylelint config to recognize Tailwind v4 directives so
the `@variant` dark at-rule is allowed (add Tailwind-aware at-rule whitelist or
use stylelint-config-tailwindcss), and remove the unnecessary quotes around the
SFMono-Regular font-family entry (change "SFMono-Regular" to SFMono-Regular) to
satisfy font-family-name-quotes; ensure these changes are applied alongside the
existing lint config so the `@variant` rule and unquoted font family no longer
cause errors.

---

Nitpick comments:
In `@apps/web/src/index.css`:
- Around line 299-303: The .inverse-border-radius-mask utility is not
self-contained because it relies on callers to set position: relative for its
pseudo-elements to anchor correctly; update the .inverse-border-radius-mask rule
to include position: relative (and any necessary positioning/display like
overflow: hidden if the mask expects clipping) so the pseudo-elements tie to
this element without requiring callers to add relative.

In `@apps/web/src/routes/_chat.project`.$projectId.tsx:
- Around line 58-61: The threads useMemo currently filters allThreads by
projectId but doesn't sort, causing inconsistent order with the sidebar; update
the useMemo (the threads constant created via useMemo that references allThreads
and projectId) to return allThreads.filter(t => t.projectId ===
projectId).sort(...) where you sort by t.createdAt descending (newest first) so
the detail view matches the sidebar ordering; ensure the dependency array
remains [allThreads, projectId].
- Around line 9-45: Extract the duplicated getServerHttpOrigin function and
ProjectFavicon component into shared modules and import them here: move
getServerHttpOrigin (and the serverHttpOrigin constant if you prefer) into a
utility file like ~/lib/serverOrigin.ts exporting the function (and optionally a
derived serverHttpOrigin constant), and move ProjectFavicon into
~/components/ProjectFavicon.tsx exporting the React component; then replace the
local definitions in _chat.project.$projectId.tsx with imports of
getServerHttpOrigin (or serverHttpOrigin) and ProjectFavicon and remove the
duplicated code so both this file and Sidebar.tsx consume the shared
implementations.
- Around line 73-80: The scriptIconMap constant is being recreated on every
render; move the declaration of scriptIconMap (currently a Record<string,string>
with keys play,test,lint,build,debug,configure) out of the React component to
module scope (top-level of the file) so it is allocated once, keep the same name
and type, and leave its usage inside the component (where it's referenced around
line 142) unchanged so consumers still access scriptIconMap by name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 81f71c5e-cdbe-4880-a072-4d8a9322765c

📥 Commits

Reviewing files that changed from the base of the PR and between d5f80ec and 8864012.

📒 Files selected for processing (9)
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/components/chat/ChangedFilesTree.tsx
  • apps/web/src/components/chat/MessagesTimeline.tsx
  • apps/web/src/components/ui/sidebar.tsx
  • apps/web/src/index.css
  • apps/web/src/routeTree.gen.ts
  • apps/web/src/routes/_chat.project.$projectId.tsx
  • apps/web/src/routes/_chat.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/components/ui/sidebar.tsx

Comment on lines +64 to +68
{/* {isExpanded ? (
<FolderIcon className="size-3.5 shrink-0 text-muted-foreground/75" />
) : (
<FolderClosedIcon className="size-3.5 shrink-0 text-muted-foreground/75" />
)}
)} */}

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 | 🟡 Minor

Keep directory labels aligned if the folder glyphs stay hidden.

With the folder icon removed here, directory names now start one icon-width earlier than file names at the same depth, so the tree columns no longer line up. If hiding the icon is intentional, keep a fixed-width spacer in its place.

🎯 Suggested fix
-            {/* {isExpanded ? (
-              <FolderIcon className="size-3.5 shrink-0 text-muted-foreground/75" />
-            ) : (
-              <FolderClosedIcon className="size-3.5 shrink-0 text-muted-foreground/75" />
-            )} */}
+            <span aria-hidden="true" className="size-3.5 shrink-0" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* {isExpanded ? (
<FolderIcon className="size-3.5 shrink-0 text-muted-foreground/75" />
) : (
<FolderClosedIcon className="size-3.5 shrink-0 text-muted-foreground/75" />
)}
)} */}
<span aria-hidden="true" className="size-3.5 shrink-0" />
🤖 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 64 - 68,
Directory labels shifted because the folder icon was removed in ChangedFilesTree
(the commented FolderIcon/FolderClosedIcon block), so add a fixed-width spacer
where the icons used to render when icons are intentionally hidden; e.g., in the
same place as FolderIcon/FolderClosedIcon render an empty element with the same
sizing classes (the "size-3.5 shrink-0 text-muted-foreground/75" styles or
equivalent) so directory names align with file names at the same depth and the
tree columns line up.

vkiooz and others added 3 commits April 1, 2026 10:57
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	apps/desktop/src/main.ts
#	apps/server/src/orchestration/Layers/ProjectionPipeline.ts
#	apps/server/src/persistence/Migrations.ts
#	apps/web/src/components/BranchToolbarBranchSelector.tsx
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/Sidebar.tsx
#	apps/web/src/routeTree.gen.ts
#	apps/web/src/routes/_chat.tsx
#	apps/web/src/store.test.ts
#	apps/web/src/store.ts
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