Skip to content

Pure black dark theme - #4068

Closed
t3dotgg wants to merge 16 commits into
mainfrom
t3code/cool-black-redesign
Closed

Pure black dark theme#4068
t3dotgg wants to merge 16 commits into
mainfrom
t3code/cool-black-redesign

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 17, 2026

Copy link
Copy Markdown
Member

Dark canvas goes from #161616 to pure black with raised card surfaces, a light primary, and a rounder composer.

Before

before

After

after


Note

Low Risk
Visual and CSS token changes only; no auth, data, or business logic affected.

Overview
Dark mode shifts from charcoal (#161616) to a pure black canvas, with boot splash, theme-color, and CSS tokens updated so --background is black and card/popover surfaces use raised oklch grays instead of neutral mixes.

Primary actions in dark mode flip to a light primary on dark text (--primary near white), and focus rings move to a white alpha instead of the old blue primary.

UI polish includes a rounder chat composer (20px radii), stronger composer glass (82% card mix), sidebar v2 using dark:bg-card on the black canvas, user message bubbles with translucent white fill and no border in dark, terminal fallback background black, and a split-button border tweak on Implement actions in dark.

Reviewed by Cursor Bugbot for commit 70d022d. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add pure black dark theme and Sidebar v2 beta with thread settling

  • Changes the dark mode background to pure black (#000000) across index.html and index.css, updating CSS variables for contrast, opacity, and composer glass opacity.
  • Introduces a new SidebarV2 component (SidebarV2.tsx) behind a sidebarV2Enabled client setting, rendering animated card-based thread rows with visual status indicators ordered by creation time.
  • Adds thread settling logic: effectiveSettled in threadSettled.ts marks threads as settled based on archive state, PR merge/close state, or configurable inactivity (sidebarAutoSettleAfterDays, default 3, range 1–90).
  • Adds a /settings/beta route with controls for toggling sidebar v2 and configuring the auto-settle inactivity period; mobile gets a matching Beta section in settings.
  • On mobile, HomeScreen renders a merged live + archived thread list when threadListV2Enabled is set, with a settled tail behind a divider, optimistic hold for newly settled threads, and settle/unsettle swipe actions.
  • startNewThreadInProjectFromContext no longer passes branch/worktree context when the target project differs from the current context.
  • Behavioral Change: dark mode backgrounds are now pure black by default, which may affect users with system-level dim/contrast adjustments.
📊 Macroscope summarized 70d022d. 31 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

t3dotgg and others added 16 commits July 15, 2026 02:30
…ifecycle

A new thread sidebar behind a Settings → Beta toggle (sidebarV2Enabled,
default off). One flat recency-sorted list across projects: active threads
render as two-line cards (favicon, title, time, status word · branch ·
provider glyph + model · machine), settled threads collapse to slim
one-liners. Approval-blocked threads pin to the top with wait time; failed
sessions (session.lastError) finally get a visible state.

Settled is an event-sourced lifecycle state mirroring the archived pattern:
- settledOverride ("settled" | "active" | null) + settledAt on the thread,
  thread.settle/thread.unsettle commands, thread.settled/thread.unsettled
  events, projection columns via migration 033.
- effectiveSettled() computes the visible state: manual override beats
  auto rules (PR merged/closed, N days of inactivity — client setting,
  default 3, nullable). Idempotency is by event re-emission since the
  engine rejects zero-event commands.
- Activity auto-unsettles server-side: turn start, live session start,
  approval/user-input requests. Session stop/error status writes do not.

Manually settling a thread whose worktree is orphaned offers a one-click
worktree removal (re-validated at click time, never forced).

The v2 component is a sibling swapped at the single AppSidebarLayout mount
point; shared chrome moved to sidebar/SidebarChrome.tsx. The data model is
flag-independent and ships dark for v1 users, so toggling is a pure view
swap.

Plan: .plans/21-sidebar-v2-beta.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reation picker

Design iteration after testing the beta over Tailscale:

- Settled is the only collapse rule. Reverted the "seen Ready threads render
  slim" heuristic — it silently turned settled back into a derived state.
  Every unsettled thread is a full card; density is earned by settling.
- Cards are bordered islands (rounded-xl, state-colored border: amber
  approval / sky working / red failed), status dot replaces the favicon on
  stateful cards, live elapsed timer on working. Slim rows grew to 34px with
  13px titles; a hairline divider separates cards from the settled tail.
- Harness identity is the provider glyph only (model name in tooltip). The
  full-row harness tint experiment is removed.
- Hover actions (Settle/Un-settle) are absolutely-positioned overlays that
  fade over the timestamp — no layout shift.
- Project scope chips above the list (All / per-project / + add): filters
  the flat list and informs the new-thread default. Session-local.
- New thread opens a project picker dialog: contextual default preselected
  and focused (Enter confirms), all projects with workspace paths, Add
  project row. Single-project setups skip the dialog. chat.new (mod+shift+o)
  routes through the picker via newThreadPickerBus since the shortcut is
  handled in the _chat route layout. Arrow/Home/End navigation in the list.
- chat.newLocal still bypasses the picker as the explicit escape hatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI: add settledOverride/settledAt + new client settings to desktop and
mobile test fixtures and the mobile threadDetailToShell mapping.

Review fixes (cursor bugbot + macroscope):
- failed status now requires session.status === "error" instead of any
  lingering lastError on a stopped/ready session
- effectiveSettled: pending approvals or user input always win — blocked
  work can never collapse into the settled tail, even manually settled
- decider unsettle-on-activity guards check settledOverride === "settled"
  so activity no longer erases a user's explicit keep-active override
- repeat thread.settle re-emits with updatedAt = original settledAt, so
  double-settles no longer bump recency ordering
- sidebarAutoSettleAfterDays bounded 1..90 in schema and patch
- approval wait sort/label use shell.updatedAt (bumped by the approval
  activity in the projection pipeline) instead of latestUserMessageAt /
  turn start, with the approximation documented
- v2 rows: Enter/Space keyboard activation; double-click inline rename
  ported from v1 plus a context-menu Rename entry
- new-thread picker: cross-project creation no longer inherits the active
  thread's branch/worktree; preselection consults the active draft via
  resolveThreadActionProjectRef; stale project scope resets to All
- settle worktree prompt: vcs status checked before offering (and again
  at click time) — dirty or ahead worktrees are never offered

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…led tail

- Cards are square hard-edged blocks on a darker shell; active row reads
  by a near-white border instead of a blue-adjacent wash
- Status moves from tinted border + dot to a solid 3px left edge strip
  (duty-cycled pulse while working), so the project favicon always shows
- Card titles clamp to two lines instead of truncating one
- Settled rows grey out (desaturated favicon, dimmed title) and restore
  on hover; section gets an explicit SETTLED label
- Project chips are square mono tabs; selected All chip inverts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Each card is a tiny window: a thin title bar carries the project
  favicon + mono uppercase name with the timestamp, separated from the
  thread content by a hairline — project identity reads as chrome
- Active card brightens its title bar on top of the near-white border
- Thread list is statically ordered by creation time, newest first;
  activity never reorders rows — only settling moves them
- Project identity mirrored on the open thread: favicon+name chip leads
  the chat header and the composer's run-context strip (drafts included)
- Settled rows keep dimmed favicons; chips/picker favicons restored
- Composer squared to match: 12px radius, higher-contrast border,
  neutral focus ring

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the web sidebar v2 design to React Native behind a device-local
Settings → Beta toggle (threadListV2Enabled — mobile has no
client-settings sync, so the flag lives in mobile preferences):

- threadListV2.ts: v2 status model (approval/working/failed/ready),
  static creation-order sort, and the effectiveSettled partition into
  an active card block + settled recency tail (shared
  @t3tools/client-runtime/state/thread-settled, web-default 3-day
  auto-settle)
- thread-list-v2-items.tsx: square title-bar cards — project favicon +
  mono uppercase name as chrome, status edge strip, two-line titles,
  branch/status meta — and dimmed slim rows under a SETTLED divider
- Settle (cards) / un-settle (slim) as the primary swipe action and in
  the long-press menu; useThreadListActions grows settle/unsettle via
  the shared threadEnvironment atoms
- HomeScreen renders the flat v2 list (no grouping, rows never reorder
  on activity) when the toggle is on; v1 list untouched otherwise

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first mobile pass copied the web card anatomy (outlined translucent
box, thin header strip) — which is exactly iOS push-banner anatomy, so
rows read as information rather than buttons. Rebuild the card around
native list-app touch signals:

- Solid raised bg-card surface, no border, 18pt continuous corners
- Leading 38pt favicon tile as the touch anchor; mono project name
  becomes an eyebrow line above the title
- Trailing chevron affordance; timestamp joins the eyebrow row
- Spring press feedback (scale to 97%) via reanimated
- Status edge strip survives as a full-height bar on the left edge

Settled rows, swipe actions, and long-press menus unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hanges

Makes the new sidebar/thread-list testable against existing unupgraded
servers by removing every backend dependency of the settled feature:

- apps/server, contracts orchestration schemas, and client-runtime
  command/reducer plumbing revert to main baseline (no thread.settle
  commands, no settled events/fields, no migration 033)
- effectiveSettled() is now archive-based: archivedAt is the explicit
  settled signal; PR merged/closed and inactivity auto-settle keep
  working client-side. Blocked work (pending approval/input, live
  session) still never settles
- settle/unsettle actions ride threadEnvironment.archive/unarchive on
  web and mobile — UI and UX unchanged
- Archived threads stay visible as the settled tail: the live shell
  stream drops archived threads (thread.archived → thread-removed), so
  both clients merge them back from the archived shell snapshot and
  refresh it after settle/unsettle/archive actions
- Client settings (sidebarV2Enabled, sidebarAutoSettleAfterDays) are
  kept — they persist client-side only

Trade-offs vs the event-sourced model (still on t3code/sidebar-v2-mobile):
no activity-driven auto-un-settle for archived threads, and no "keep
active" override distinct from unarchiving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- settleThread inherits archive's running-turn guard on web and mobile:
  settling can never interrupt an active turn
- unsettleThread no-ops on auto-settled (unarchived) threads instead of
  sending an unarchive the server would reject
- Mobile v2 partition now receives per-row PR state (rows report up via
  onChangeRequestState, mirroring web) so merged/closed PRs auto-settle
- Mobile empty state accounts for archived-only workspaces when v2 is on
- Optimistic settled hold on web + mobile: the shell being settled is kept
  visible (marked archived) between the live stream dropping it and the
  archived snapshot returning it — no flicker, and holds are created only
  at settle time so deletes are never resurrected
- Hover-only settle buttons in web SidebarV2 get focus-visible:opacity-100
- v2 full swipe commits the advertised primary action (Settle/Un-settle),
  with the stretch animation following; delete remains a tap target only.
  v1 keeps full-swipe delete via the new fullSwipeAction prop default
- Android long-press on v2 cards works: PressableScaleCard is the menu's
  direct child and forwards the injected onLongPress to its Pressable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Worktree-removal offer works after settle: archived threads leave the
  live shell store, so the orphan check re-includes the settled shell and
  click-time revalidation treats absence-from-live as still-settled
- Un-settle is only offered when there is an archive to undo: auto-settled
  rows (inactivity / merged PR, archivedAt null) get Settle instead, on
  web (hover button + context menu) and mobile (swipe + long-press menu)
- Mobile optimistic settled hold rolls back when the settle is blocked or
  fails (settleThread now reports success); web and mobile both drop holds
  on delete and un-settle so ghosts can't linger
- Archived snapshot refreshes after delete too, clearing stale settled
  rows sourced from the snapshot
- v2 mobile list renders queued offline tasks above the cards (they were
  dropped entirely, leaving a blank screen for pending-task-only projects)

On the chat.new/settings finding: not fixed here — the shortcut routes to
the v2 picker only inside chat routes by design; on /settings it falls
back to contextual create, matching v1's behavior on those routes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Settling is a high-frequency lifecycle action; announcing it with a
notification (plus a disk-cleanup offer) was noise. Settle is silent now.
Worktree cleanup still exists on the delete path, which asks explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Settling the active thread on web navigates to the next remaining card
  (wrapping, never a settled row), or to the new-thread screen when it
  was the last active one. Background settles never navigate.
- The settled tail renders 10 rows, then an explicit Show more reveals
  25 at a time (web + mobile). Expansion resets when scope, environment,
  or search changes. Mobile's builder counts hidden rows instead of
  building them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 884b49b7-44cc-4e0a-a502-44e868c0bf11

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/cool-black-redesign

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

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 17, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jul 17, 2026
@macroscopeapp

macroscopeapp Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@chiqors

chiqors commented Jul 18, 2026

Copy link
Copy Markdown

It would be great to add a Project Filter dropdown to the new sidebar v2 to help manage and isolate threads (i might be wrong to put comment in this PR, but just let you know).

Right now, as the number of threads grows, it gets difficult to focus on specific workstreams. Adding the ability to filter by project (e.g., project-a, project-b, project-c) would significantly improve navigation and reduce noise.

CleanShot 2026-07-18 at 17 19 22 CleanShot 2026-07-18 at 17 17 22

Base automatically changed from t3code/new-sidebar-client-only to main July 22, 2026 10:36
@macroscopeapp
macroscopeapp Bot dismissed their stale review July 22, 2026 10:36

Dismissing prior approval to re-evaluate 70d022d

: () => void router.navigate({ to: "/" });
}
const result = await settleThread(threadRef);
if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium components/SidebarV2.tsx:991

attemptSettle treats an interrupted settleThread result as a success: when result._tag === "Failure" and isAtomCommandInterrupted(result) is true, the failure branch is skipped, so the optimistic settledHolds entry stays in place, refreshArchivedThreadsForEnvironment runs, and navigateAfterSettle fires — navigating the user away from a thread whose settle was interrupted, not completed. Every other stateful call site in this file returns before its success-side effects when the result is an interrupted failure. The attemptSettle callback should remove the hold and skip navigation for any failure, while still suppressing the toast for interruptions.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/SidebarV2.tsx around line 991:

`attemptSettle` treats an interrupted `settleThread` result as a success: when `result._tag === "Failure"` and `isAtomCommandInterrupted(result)` is true, the failure branch is skipped, so the optimistic `settledHolds` entry stays in place, `refreshArchivedThreadsForEnvironment` runs, and `navigateAfterSettle` fires — navigating the user away from a thread whose settle was interrupted, not completed. Every other stateful call site in this file returns before its success-side effects when the result is an interrupted failure. The `attemptSettle` callback should remove the hold and skip navigation for any failure, while still suppressing the toast for interruptions.

const v2ListHeader = (
<>
{listHeader}
{props.pendingTasks.map((pendingTask, index) => (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium home/HomeScreen.tsx:655

The v2 list header renders every queued task in props.pendingTasks without filtering by selectedEnvironmentId or searchQuery, so when Thread List v2 is enabled, selecting a single environment still shows pending tasks from all environments, and pending tasks that don't match an active search stay visible. The grouped v1 list filters both ways via buildHomeThreadGroups, but the v2 header maps props.pendingTasks directly. Consider applying the same environmentId and search-query filters to props.pendingTasks before rendering the v2 header rows.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/home/HomeScreen.tsx around line 655:

The v2 list header renders every queued task in `props.pendingTasks` without filtering by `selectedEnvironmentId` or `searchQuery`, so when Thread List v2 is enabled, selecting a single environment still shows pending tasks from all environments, and pending tasks that don't match an active search stay visible. The grouped v1 list filters both ways via `buildHomeThreadGroups`, but the v2 header maps `props.pendingTasks` directly. Consider applying the same `environmentId` and search-query filters to `props.pendingTasks` before rendering the v2 header rows.

@t3dotgg

t3dotgg commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

Closing because this branch bundles the old sidebar-v2 stack with the theme experiment. Sidebar v2 has landed through #4026 and later work; the remaining focused true-black direction is tracked in #3980.

@t3dotgg t3dotgg closed this Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants