Skip to content

Add preferred Pi model settings and pickers - #2211

Closed
jlmcmich wants to merge 47 commits into
pingdotgg:mainfrom
jlmcmich:feat/pi-preferred-models
Closed

Add preferred Pi model settings and pickers#2211
jlmcmich wants to merge 47 commits into
pingdotgg:mainfrom
jlmcmich:feat/pi-preferred-models

Conversation

@jlmcmich

@jlmcmich jlmcmich commented Apr 20, 2026

Copy link
Copy Markdown

Summary

  • add Pi settings for favorite models and a configurable default model
  • use those preferences in the splash screen and chat composer model pickers
  • fix provider switching so Pi falls back to Pi defaults instead of reusing another provider model slug

Validation

  • bun fmt
  • bun typecheck
  • bun run test src/modelSelection.test.ts src/composerDraftStore.model.test.ts src/composerDraftStore.test.ts (from apps/web)

Notes

  • bun run test --filter=@workbench/web still hits an unrelated existing failure in apps/web/src/components/chat/MessagesTimeline.test.tsx

Note

Medium Risk
Moderate risk because it changes CLI/package identifiers, filesystem base directories, environment variable names, and Electron protocol/userData handling, which can affect upgrades and runtime startup across platforms.

Overview
Rebrands the product from T3 Code to Workbench across docs, marketing, release workflow metadata, package names, and user-facing strings.

Renames the server CLI/package from t3 to workbench, updates build/release filters, and adds backward-compatible config/env handling (e.g., WORKBENCH_* with fallback to legacy T3CODE_*, default base dir migration from ~/.t3 to ~/.workbench).

Updates the desktop app identity (bundle id, protocol scheme workbench://, Linux desktop entry/userData dir naming) while keeping legacy support (t3:// scheme registration and legacy userData dir detection), and improves dev DX by starting a apps/server bundle watcher alongside Electron dev. Also adds a new /.well-known/workbench/environment endpoint alongside the legacy /.well-known/t3/environment, and extends git text-generation provider typing to include "pi".

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

Note

Add 'pi' provider settings, model pickers, and Console rail to Workbench

  • Introduces a new pi provider throughout the stack: contracts (PiSettings, PiModelOptions, PiModelSelection), server adapter (PiAdapter.ts) that spawns the pi CLI in JSON mode to stream runtime events, and a PiProvider.ts that checks install/auth/version status and lists models.
  • Adds settings UI for pi in SettingsPanels.tsx: a PiLoginPanel that fetches backends from /api/provider/pi/backends, triggers terminal login via /api/provider/pi/login, and a preferred-models picker with favorite/default model controls.
  • Replaces PlanSidebar in ChatView.tsx with a resizable ConsoleRail that renders Files, Recent Changes, and Tasks panes, supports per-pane collapse/visibility, workspace file preview/edit/diff, and selection-to-chat insertion.
  • Renames the monorepo from @t3tools/* to @workbench/* across all packages, renames the server CLI binary from t3 to workbench, updates localStorage keys (with legacy fallback reads), and updates all branding strings, icons, and well-known paths (e.g. /.well-known/workbench/environment).
  • Risk: localStorage key migration is one-shot and transparent, but NetService context identifier changed (@workbench/shared/Net/NetService), which could break any external code resolving the service by string key.
📊 Macroscope summarized f0912ed. 73 files reviewed, 7 issues evaluated, 0 issues filtered, 5 comments posted

🗂️ Filtered Issues

Atelier and others added 30 commits April 18, 2026 11:17
Adds workspace panel, cowork shell, workspace artifacts tracking,
and Atelier design/architecture docs on top of the upstream t3code
baseline. 40 files touched (30 modified, 1 deleted, 6 new files,
1 new docs dir).
Mints a fresh /pair URL via the t3 CLI with the correct --dev-url flag
so agents (or a second browser) can get into the running dev server
without re-using tokens bound to the primary session.

Tokens are still one-time-use and short-lived; script just wraps the
existing auth CLI.
Adds "pi" to ProviderKind, PiModelOptions, PiModelSelection, ModelSelection
union, ProviderModelOptions, DEFAULT_MODEL_BY_PROVIDER,
DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER, MODEL_SLUG_ALIASES_BY_PROVIDER,
PROVIDER_DISPLAY_NAMES, PROVIDER_CACHE_IDS, TextGenerationProvider,
ProvidersSettings (both static + patch), and all web-layer
Record<ProviderKind, ...> maps that require exhaustive coverage.

No runtime behavior yet — pi is recognized by the type system and schemas
but no PiProvider or PiAdapter is wired in. Next slice adds the provider
snapshot (install/auth/models detection).
Adds a PiProvider layer wired into ProviderRegistry that:
- runs `pi --version` (reading stderr since pi prints version there)
- detects auth by checking env vars keyed on PiSettings.defaultProvider;
  explicitly does not inherit Codex/Claude credentials so users can point
  pi at a different account or backend
- surfaces a default built-in model list (anthropic/claude-*, openai/gpt-5,
  google/gemini-2.5-pro) that custom slugs from settings extend
- emits a helpful status message listing backend options when no env var
  is present ("Choose one in settings and export the matching key")
- warns when the detected pi version is below the recommended floor

Wires the new provider into the settings panel's PROVIDER_SETTINGS list
so it renders in General → Providers with the same UX as the others.

Slice 3 will add the PiAdapter runtime. A later slice adds a dedicated
"pi backend" dropdown in settings that drives PiSettings.defaultProvider.
Previously PiProvider only recognized env-var-based credentials, so
users who ran \`pi\` → \`/login\` (the expected flow for Claude
Pro/Max and ChatGPT Plus/Pro subscriptions) appeared as Not
authenticated in the UI even though pi itself was ready.

Now PiProvider reads \`\$PI_CODING_AGENT_DIR/auth.json\` (default
\`~/.pi/agent/auth.json\`), inspects top-level provider keys only
(never tokens), and marks the backend authenticated if either an env
var or an OAuth entry is present. The authenticated message names the
active backend, the auth source (env var vs pi login), and lists any
other backends with detected credentials.

Also expands PI_BACKEND_OPTIONS to cover GitHub Copilot and Antigravity
(OAuth-only backends pi supports via \`/login\`). Each backend
declares both its env-var names and its OAuth-file keys so detection
stays declarative.
Collapses the per-backend login buttons into a single unified panel in
the pi provider section:

- one "Open pi to log in" button at the top of the panel. Clicking it
  shells out to osascript to launch pi in a new Terminal window, where
  the user runs /login to start pi's OAuth flow. On non-macOS platforms
  the endpoint returns a copy-paste fallback command.
- status grid listing every pi backend (Anthropic, ChatGPT, Google CCA,
  GitHub Copilot, Antigravity, Groq, OpenRouter, xAI, Mistral) with a
  SIGNED IN / NOT SIGNED IN pill pulled from pi's own auth.json.
- a per-row "Use as default" button that writes to
  settings.providers.pi.defaultProvider. The currently-default backend
  shows as pressed with a highlighted row; rows that aren't logged in
  can't be picked as default.

Server additions:
- POST /api/provider/pi/login spawns the terminal (via osascript on
  darwin, with a platform-aware fallback message otherwise)
- GET /api/provider/pi/backends returns per-backend login state so the
  panel renders without needing a full provider refresh cycle
- PiProvider now wires an fs.watch on ~/.pi/agent/auth.json through a
  Queue into managed.refresh so the settings UI flips from NOT SIGNED IN
  to SIGNED IN within a second of the user completing /login, without
  waiting for the 60s provider refresh tick
…bprocess

MVP pi runtime adapter. Each sendTurn spawns a fresh `pi -p --mode
json --model <model> <prompt>` subprocess and pipes stdout
line-by-line as NDJSON, mapping events onto the canonical
ProviderRuntimeEvent stream.

Scope (intentionally minimal for this slice):
- startSession: register thread, generate session id, emit
  session-started event. No pi process is spawned at startSession time.
- sendTurn: spawn pi subprocess, parse NDJSON line-by-line, emit a
  content.delta with the full assistant text from the first assistant
  message_end, then turn.completed on turn_end (or turn.completed with
  state=failed if pi reports stopReason=error/failed).
- interruptTurn / stopSession / stopAll: SIGKILL the tracked
  subprocess.
- readThread: returns empty snapshot (pi doesn't persist across our
  session boundary in this slice).
- rollbackThread / respondToRequest / respondToUserInput: fail with
  ProviderAdapterRequestError and a clear "pi does not yet support X"
  message. These are separate future slices.
- capabilities: { sessionModelSwitch: "unsupported" } — pi takes the
  model via --model per invocation, no in-session switch.

Wiring:
- New Services/PiAdapter.ts (12 lines) — Context.Service wrapper.
- New Layers/PiAdapter.ts (~660 lines) — the adapter. Includes a
  fallback DEFAULT_MODEL (anthropic/claude-sonnet-4-6) if no model is
  supplied, since pi requires --model to be non-empty.
- ProviderAdapterRegistry.ts registers PiAdapter in the adapter array.
- ProviderAdapterRegistry.test.ts adds fakePiAdapter coverage and the
  updated expected listProviders order.
- server.ts threads makePiAdapterLive into ProviderLayerLive alongside
  the other four adapters.

Tested live: pi appears in the composer provider picker with all five
default built-in models selectable (anthropic/claude-sonnet-4-6,
claude-opus-4-7, claude-haiku-4-5, openai/gpt-5,
google/gemini-2.5-pro). `bun run typecheck` passes with zero new
errors; `bun run lint` passes (22 pre-existing warnings, no new
ones).

Known limitations in this slice to follow up on:
- Raw native-event payloads aren't attached to emitted runtime events
  yet; providerRuntime.ts's RuntimeEventRaw.source union needs a
  "pi.cli.json" literal first.
- Tool calls, plan/todo updates, approvals and attachments aren't
  wired through the canonical event stream; pi's native log still
  captures them via writeNativeEventBestEffort for debugging.
- No session resume across server restarts (no --continue plumbing).
Two fixes stacked on Slice 3's adapter MVP:

Part A \u2014 real pi model slugs
============================

The initial snapshot exposed invented `provider/model` slugs
(`openai/gpt-5`, `anthropic/claude-sonnet-4-6`, etc.) that are not
valid pi --model arguments. Selecting a pi model was silently being
rejected by the UI's `resolveSelectableModel` because those slugs
didn't exist in the provider snapshot's model list, leaving the
composer stuck on the previous Codex default. Even when a selection
did commit, `pi --model openai/gpt-5` would not match any real pi
catalog entry and fail at runtime.

Fix:
- `piRuntime.ts`: add `parsePiListModels`, `loadPiModelCatalog`,
  `piCatalogToServerModels`, and `normalizePiModelSlug`. Parse the
  fixed-column table pi --list-models emits (32 real models on a
  logged-in machine), skip the header and (n/m) pagination marker,
  and expose each entry as a ServerProviderModel with pi's real bare
  slug as `slug` and `{backend}/{model}` as the human-readable
  `name`. pi --list-models only surfaces models the user has keys for,
  so this also gives us free filtering-by-auth-state.
- `PiProvider.ts`: call `loadPiModelCatalog` during
  `checkPiProviderStatus`, fall through to the (now-corrected)
  `DEFAULT_PI_BUILTIN_MODELS` fallback list only when enumeration
  returns empty (no configured backends). Fallback list now uses
  real pi slugs (`gpt-5.4`, `claude-sonnet-4-6`, `claude-haiku-4-5`).
- `PiAdapter.sendTurn`: pass the slug through `normalizePiModelSlug`
  before invoking pi, so any lingering `openai/gpt-5` style slugs in
  persisted composer state are stripped to bare `gpt-5` form. Default
  model changed from `anthropic/claude-sonnet-4-6` to `gpt-5.4`
  (the Codex subscription is broadly usable; Claude Pro/Max tokens are
  blocked by Anthropic for third-party apps).

Part C \u2014 pi logo
===============

Adds a `PiIcon` component (stylised P-with-dot, `currentColor` fill
so it matches the surrounding text color like the other provider
icons) and registers it in `PROVIDER_ICON_BY_PROVIDER`. Pi now shows
a distinct glyph in the composer's provider picker instead of the
generic `BotIcon` fallback, giving a visual cue that turns really
are routed through the pi harness.
When the user switched providers in the composer model picker,
`onProviderModelSelect` was writing the new ModelSelection to
`scopeThreadRef(activeThread.environmentId, activeThread.id)`. But
`composerActiveProvider` (which the composer reads to decide which
provider to route turns through) was reading from
`getComposerDraft(composerDraftTarget)` — and for draft threads,
`composerDraftTarget` is the DraftId string, not the ScopedThreadRef.

The two keys didn't resolve to the same draft bucket, so picking pi in
the picker updated a bucket nobody was reading, and the composer kept
sending turns with the previously-selected provider (codex). The
server-side pi-debug log confirmed the client was shipping
`modelSelection.provider = "codex"` even after a pi click.

Fix: use `composerDraftTarget` for the write, matching the read. Now
provider selection actually commits, and `pi -p --mode json` gets
invoked when the user picks a pi model.

Also drops the temporary [pi-debug] log in ProviderCommandReactor
(job done).
normalizeProviderKind was silently returning null for pi (only codex/
claudeAgent/cursor/opencode matched), so activeProvider never updated
after a pi click. Composer fell back to codex, turns dispatched with
provider=codex, server-side pi-debug log confirmed this.

Also adds pi to two model-options-iteration loops in composerDraftStore
and to PROVIDER_ORDER in serverSettings.
Pi's `--model gpt-5.4` is ambiguous — the same model name exists
under multiple pi backends (openai-codex, azure-openai-responses, etc)
and pi picks whichever backend it enumerates first. On the user's
machine that was azure-openai-responses, which they're not authed for,
so turns failed with "No API key found for azure-openai-responses"
even though their actual ChatGPT Plus login via openai-codex was fine.

Fix: make ServerProviderModel.slug = `<backend>/<model>` (both slug
and name). Pi's --model flag accepts `<provider>/<id>` patterns per
its help text, so passing the qualified slug routes deterministically.
normalizePiModelSlug is now a pass-through (kept as the adapter's
single reshaping hook for future needs).
- Assistant chip moves to the far right of the composer footer
  (ms-auto on the pill so wrapping still works on narrow widths) and
  renders the provider's own glyph in place of the generic BotIcon.
  The dropdown rows also get per-provider icons so you can tell pi
  from Codex from Claude at a glance.
- Adds PROVIDER_ICON_BY_PROVIDER mapping to match ProviderModelPicker
  (codex \u2192 OpenAI, claudeAgent \u2192 ClaudeAI, opencode \u2192 OpenCodeIcon,
  cursor \u2192 CursorIcon, pi \u2192 PiIcon, fallback \u2192 BotIcon).
- Folder chip reads "Folder: <name>" instead of the bare project
  name, so the chip's role is obvious at first glance.
- Replaces "No active task" header text with "Start a task" in the
  two places it rendered (Electron titlebar variant + web sidebar
  variant).
Adds a bottom-of-menu action in the Folder chip dropdown on the
landing page that:
1. Prompts the OS folder picker via LocalApi.dialogs.pickFolder
   (desktop only \u2014 web users get a "install desktop app" hint).
2. Creates a new project at that path through
   project.create on the currently-active environment's
   orchestration channel, using the same Codex defaults the command
   palette's add-project flow uses.
3. Selects the new project as the active folder so the user can
   send a task right away.

No "Recent projects" tile row added for now \u2014 the folder chip
dropdown already lists every project with its path, which covers
"ways to get started" without duplicating affordances. We can add
tiles later if the dropdown approach doesn't surface enough.
Adds 7 knowledge-worker-flavored shortcuts the composer expands into
structured prompt templates at composition time, backend-agnostic:

  /summarize   - summarize current folder or a mentioned file
  /research    - research a topic with sources
  /draft       - draft a document (email, memo, report, ...)
  /rewrite     - rewrite text more clearly
  /organize    - suggest folder structure; asks before moving files
  /compare     - compare docs, options, plans, versions
  /nextsteps   - interview-style next-step planning

Each template is written to invite the assistant to ask 1-2 short
clarifying questions when details are missing (tone, scope, criteria,
etc.) rather than front-loading everything onto the user.

Implementation:
- New `atelierSlashCommands.ts` holds the registry and prompt text.
- `ComposerCommandItem` gains an `atelier-slash-command` variant and
  the menu groups them under a "Shortcuts" header above the existing
  Built-in / Provider groups.
- ChatComposer replaces the typed `/name` with the full template and
  places the cursor at the trailing space so the user can keep typing
  specifics (filename, topic, etc.).
- Search ranking is extended to score the new item type the same way
  built-in and provider slash commands are scored.

/research flags up front when no web tool is available, since not
every backend ships one by default. Skills integration and a proper
interview UI for /nextsteps are follow-ups.
…nch)

Captures what's shipped (PR1\u2013PR3), uncommitted WorkspacePanel
refactor sitting in the working tree, ordered backlog, and a detailed
rebrand spec covering the Workbench rename and the sidebar
"Workspaces" \u2192 "Consoles" terminology shift.

Flags one live ambiguity: "workspace" is used in three places in the
codebase (sidebar grouping, right-side file panel, server filesystem
services). Only the sidebar grouping should become "Console" \u2014
the other two are infra names that describe the active project's
working directory, not the grouping.

Intended as a resume-where-you-left-off doc; re-read it before
starting the next session.
…acing surfaces

User-facing rename pass for the next phase of the Atelier → Workbench
identity shift. The app now presents as Workbench end-to-end and the
sidebar/grouping language reads as Console/Consoles. Internal identity
layers — t3 package names, CLI names, repo/release URLs, data dirs,
env vars, and the workspace* component plumbing — are intentionally
left untouched in this pass to keep the change low-risk; a coordinated
deeper rename can follow once the brand has settled.

Touched surfaces:
- App brand: AGENTS.md, README, marketing pages, desktop launcher +
  electron app metadata, splash, sidebar header, settings copy.
- Workspaces → Consoles: sidebar grouping headers, project chips,
  composer plan-sidebar label, related branding tests, settings
  connections + panels.
- Docs: drop superseded atelier/* planning docs; refresh next-steps
  with the rebrand spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…akeover and quick edit

Replaces the legacy WorkspacePanel with a vertical stack of pane cards
(Cowork-style), a takeover viewer for previewing/editing files, and a
small set of UX affordances designed for non-technical users. The rail's
internal plumbing is still named workspace*; the user-facing surfaces
follow the Console rebrand from the previous commit.

Phase 1 — vertical-stack architecture
- New `components/workspace/` directory: registry types, persisted pane
  visibility + collapsed state in localStorage, PaneCard primitive, and
  a WorkspaceRail shell that hosts the cards.
- Cards extracted from the old WorkspacePanel: TreePane (file browser),
  TaskPane (active plan + work log), RecentChangesPane (recent agent
  edits). Each card is independently visible + collapsible via the
  Workspace badge dropdown menu in the rail header.
- Pane registry is extension-ready — adding a card (e.g. Instructions,
  Context, Scheduled) is a new descriptor + a render arm.

Phase 1.5 — viewer takeover + expand
- Selecting a file in the tree (or any markdown file mention) opens
  ViewerPane as a full-bleed overlay covering the rail.
- Closing the viewer X returns to the stack underneath; if the rail was
  expanded, it auto-collapses so the chat column reappears in one move.
- Expand button (in the viewer header, not the rail header) widens the
  rail to cover the chat column entirely. A `transform` on the chat
  row makes it the containing block for the Sidebar's `position: fixed`
  inner container, capping the expanded rail at the left sidebar's
  right edge instead of letting it stretch behind the left sidebar.
- Manual drag is capped at 50% viewport — drag is for "roomier stack",
  expand is for "real space to read or edit".

Phase 2 — DocumentMarkdown + Quick edit
- DocumentMarkdown wraps ChatMarkdown with a `.document-markdown` class
  so all of its behavior (file links, syntax-highlighted code blocks,
  GFM, url transforms) carries over while the typography overrides in
  index.css render markdown like a document — proper heading sizes,
  generous prose spacing, max-width: 72ch for readability.
- Inline-code file paths in chat (e.g. `docs/plan.md`) are now promoted
  to clickable file links via a new `code` handler in ChatMarkdown that
  runs resolveMarkdownFileLinkMeta. Routes through onOpenWorkspaceFile
  so backtick'd mentions reach the viewer just like explicit `[](path)`
  links do.
- Quick edit mode: textarea-backed source editor for note/document/data
  files, with Save/Cancel inline next to the Quick edit button (same
  toolbar slot, no detached footer). Save calls projects.writeFile via
  a new saveWorkspaceFile callback in WorkspaceRail; success toasts and
  refetches the on-disk text, failure preserves the unsaved buffer.
- Show-in-Folder split-button replaces the Open-in-app + Open-in-editor
  pair: default click reveals in file manager; chevron opens a menu
  with Open in editor today and is the future home for connector
  exports (Google Drive, Notion, …).

Tests
- `WorkspaceRail.browser.tsx`: 26 tests covering pane visibility,
  collapse persistence, viewer takeover open/close, doc-tuned markdown
  preview, Quick edit toggling, and Save → projects.writeFile roundtrip
  with mocked environment API + per-test useQuery override for
  read-file responses.
- `ChatMarkdown.browser.tsx`: 2 new tests for inline-code file-link
  promotion (positive + negative).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the inherited T3 Code README and REMOTE.md with Workbench-focused
content:
- README.md: tagline, quick-start, repo layout, acknowledgement of the
  pingdotgg/t3code fork
- REMOTE.md: Workbench server pairing flow (desktop app + headless CLI),
  same network/security guidance, refreshed examples

Drop documentation that no longer reflects the current direction:
- docs/effect-fn-checklist.md
- docs/observability.md
- docs/release.md
- docs/workbench/next-steps.md
- assets/dev/blueprint-icon-composer.icon/Assets/T3.svg (T3 wordmark
  source, replaced by Workbench mark)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure mechanical changes from running the formatter across the repo —
no behavior changes. Long imports broken into multi-line form,
inline objects wrapped, package.json devDependencies + bin entries
re-sorted alphabetically.

Touches: 3 package.json files (desktop, server, web), 8 server source
files, 6 web component/lib files, and 2 build scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top bar symmetry
- Replace ChatHeader's "Console" pill with a chevron toggle (PanelRight*).
- Sidebar trigger always visible: inside the sidebar chrome when the
  sidebar is open, in the chat/splash/settings header (with macOS
  traffic-light inset) when collapsed.
- Trigger icon switches between PanelLeft (open) and Menu hamburger
  (collapsed), matching the Cowork / Claude Code pattern.
- Workbench wordmark + logo bumped from size-4/text-sm to size-6/text-base.
- Add FolderKanban icon to the rail header's Console pill so the brand
  mark moves with the toggle.

Right rail (console)
- Drop the 40vh cap on the file tree so it fills the rail; outer
  ScrollArea handles overflow.
- workspaceFileTree.ts now renders trailing-slash entries (submodules /
  nested git repos like `workbench/`, `workbench-pi-fix/`) as proper
  folder nodes with names + folder icons, not empty-name file rows. New
  regression test.
- TaskPane: drop the placeholder "Plan mode stays visible" card; replace
  colored-dot status indicators with proper green-check / blue-spinner /
  outline-circle icons; filter raw "Tool call" entries out of the work
  log via a new isMeaningfulWorkEntry guard; rename section to
  "Recent activity".

Splash composer (NoActiveThreadState)
- Collapse the nested max-w-5xl + double max-w-4xl wrappers into a
  single max-w-[min(56rem,100%)] container.
- Title fluid via clamp(); textarea text + padding + submit button +
  min-height all scale at the sm: breakpoint instead of fighting at
  narrow widths.
- New transient pendingAutoSubmitStore so the splash can request
  "auto-send on next mount". ChatView consumes the flag once the draft
  prompt loads, then setTimeout(0)s onSend — type into splash, hit
  enter, chat starts immediately (no second enter).
- Splash sidebar trigger inset matches ChatView's pattern.

Branding
- Add AnimatedWorkbenchLogo (block-float + soft drop-shadow) to
  Icons.tsx; SplashScreen now uses the animated variant at size-24.

AGENTS.md
- Replace the inherited "Project Snapshot" intro with a Workbench
  Mission section: folder-first AI workbench, locked product
  vocabulary (Workbench / Console / Task / Workspace), six core
  product principles. Original task-completion + maintainability
  content preserved further down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Group A change added a chevron toggle in the chat top bar that
collapses the right rail. The original close-rail chevron inside the
RailHeader (PanelRightCloseIcon next to the Console pill) was left in
place, so users now saw two stacked chevrons doing the same thing.

Drop the inside-rail close button. Toggle now lives only in the chat
header, matching the parallel left-sidebar trigger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs: rewrite README/REMOTE for Workbench + remove stale docs
jlmcmich and others added 17 commits April 19, 2026 14:56
chore: apply formatter wrap + dep alphabetization
feat(web): UI refinements + product mission merge
Replace the old black-rounded-square "T3" wordmark with the bare workbench
mark (bench top + 2 legs + 3 blocks, currentColor on transparent) and
regenerate every committed raster — production launchers, dev/nightly
blueprint variants, web public favicons, and desktop committed fallbacks.

Adds scripts/regenerate-icons.mjs (driven by sharp + png-to-ico, with
sips/iconutil for ICNS and a pure-JS ICNS encoder fallback) so the whole
raster set can be rebuilt from assets/prod/logo.svg with one command.
The dev/nightly variants composite the white workbench mark over the
existing blueprint-blue gradient + paper texture from the icon-composer
source layers — replacing the prior "T3" letters, not painting over them.

Renames assets/prod/{t3-black-*,black-*-1024}.png|ico to workbench-*.png|ico
and updates scripts/lib/brand-assets.ts to point at the new filenames.
Drops the orphan assets/prod/black-ios-1024.png that no callsite read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rename every persisted localStorage key (and the in-page CustomEvent name)
to the workbench:* namespace. Each store now reads the new key first and
falls back to its t3code:* predecessor on first load, writes only to the
new key thereafter, so existing users keep their preferences across the
rebrand.

Touched stores: useTheme + index.html boot script (theme), client
settings + saved-environment registry, last-editor preference,
terminal state, ui state (added new tier above the existing
renderer-state legacy chain), composer drafts, ChatView's
last-invoked-script-by-project (one-shot copy at module load).

The DOM CustomEvent useLocalStorage dispatches is renamed without
back-compat — it has no persistence, only listeners-from-this-tab.

Updates the three ChatView.browser test fixtures that wrote to the old
keys directly so they continue exercising the production code path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…th Workbench

Sweep every user-visible "T3 Code" / "T3 Server" string and the
`t3-code-git-text` ACP client identifier across the server runtime,
git layer, provider probe messages, environment label fallback, server
CLI description, and the websocket connection surface on the web side.
Updates matching test/snapshot assertions in the same commit so the
suite passes.

Also relabels the checkpoint git author/committer from "T3 Code" to
"Workbench" (existing checkpoints keep their historical author per
the user's call) and renames the Codex desktop client `title`. Effect
Service tag IDs, the `t3-code-provider-probe` Cursor ACP client name,
and the desktop main.ts `LEGACY_USER_DATA_DIR_NAMES` back-compat list
are intentionally left untouched per the rebrand brief.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the legacy `t3` entry from apps/server/package.json — the published
binary is now exclusively `workbench`. The fallback used during the
desktop migration is no longer needed since the server itself is
already on the new identifier.

Refresh REMOTE.md to use `workbench serve` / `workbench auth`
throughout (and remove the "still uses `t3` during the migration"
caveat). Refresh KEYBINDINGS.md to point at `~/.workbench/keybindings.json`
with `~/.t3/...` listed as a legacy fallback during the migration —
matching the resolver in apps/server/src/os-jank.ts and
scripts/dev-runner.ts which already check both directories.

Renames the devcontainer label, the ignored fork-baseline
directory in .gitignore, and updates the .docs/ collection
(architecture, encyclopedia, codex-prerequisites, runtime-modes,
remote-architecture, scripts) to drop "T3 Code" branding. The
`T3CODE_*` environment variable names are preserved for now —
they're still the names the runtime reads. Updates the stale
`T3-Code-*.zip|exe|dmg` artifact-name fixtures in
scripts/release-smoke.ts and scripts/merge-update-manifests.test.ts
to match the actual `Workbench-*` template emitted by
build-desktop-artifact.ts.

AGENTS.md was already T3-free in this branch and is owned by another
stream, so it's intentionally untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous dev/nightly icons composited the workbench mark over the
icon-composer GPT-generated blueprint background, but the source PNG had
the old "T3" letters baked into it as a single flattened layer — the
result was the workbench mark sitting on top of ghost T3 letters.

Drop the blueprint background entirely. Dev/nightly variants are now the
same workbench mark as prod, rendered in #2563eb (Tailwind blue-600,
the UI's accent color used for the active Console pill and other
highlight states), on a transparent background.

Removed ~80 lines of unused composite/mask helpers from
regenerate-icons.mjs. File sizes for the 1024px dev/nightly PNGs
dropped from ~6MB to ~7KB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T3 Code → Workbench rebrand sweep
Without an upper bound the textarea grew with content and pushed the
toolbar (folder picker, plan, full-access, assistant) off-screen for
long prompts, plus eating the page title. Add max-h-[40vh] and
overflow-y-auto so the input scrolls internally past that point and
the surrounding chrome stays visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(web): cap splash composer textarea height
The previous attempt set min-h/max-h via [&_textarea]: child-selector
classes on the wrapper span, but they lost a CSS specificity battle
against the Textarea component's own inner classes (`min-h-17.5`,
etc.) — net effect was a textarea stuck at the min, with content
overflowing into a scrollbar instead of growing.

Move height controls to inline `style` (passes through mergeProps to
the inner textarea, beats every stylesheet rule). Also drop the
`sm:min-h-[184px]` desktop override so the box starts at 140px on all
screens. `field-sizing-content` (already on the inner textarea) does
the actual auto-grow up to maxHeight, then `overflow-y-auto` scrolls
inside past the cap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(web): splash textarea starts small + grows to cap
Bring the splash composer to feature parity with the in-thread chat
composer's image handling:

- Paperclip + button next to the submit button opens a hidden file
  input (image/* multi-select).
- Paste handler on the textarea picks up clipboard images and adds them.
- Drag-and-drop anywhere on the form card adds files; the card border
  tints blue while dragging.
- Thumbnail strip above the textarea, each thumbnail with a remove X.
- Validation: image MIME types only, per-file PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
  total PROVIDER_SEND_TURN_MAX_ATTACHMENTS. Errors render in the
  existing error message slot.
- Object URLs revoked on remove + on unmount so we don't leak.
- Submit button enables when prompt OR images are present (was: prompt
  only). + button disables at the attachment limit.
- On submit: attached images flow into the new draft via
  draftStore.addImages(draftId, ...) before the auto-submit signal —
  ChatView's onSend then picks them up via composerImagesRef like
  any other draft attachment.

The reusable-hook extract from ChatComposer was deferred — the splash
state lives locally (until the draft exists) while the chat composer
state is store-backed, so a single shared hook would have to bridge
both. Splash-side is self-contained for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(web): splash composer image attachment (drag, paste, +)
When a workspace root is itself a git repo AND contains nested git
repos at depth 1 (submodules, sibling project checkouts, etc.), the
git-ls-files path returns those nested dirs as opaque entries
(`workbench/`, `workbench-pi-fix/`, etc.) and the file tree can't
expose what's inside them. Users see an unbrowsable folder.

Add a fast detectNestedGitRepos probe that scans only the immediate
subdirectories for a `.git` entry (skipping the standard ignored
directory names). When it finds any, route the index build through
the filesystem walker instead — which already respects gitignore
+ IGNORED_DIRECTORY_NAMES and walks every directory.

Depth is capped at 1 because the routing decision doesn't need to
traverse deeper: the filesystem walker handles the rest. Cost is
one readdir of the workspace root per cache miss.

Updated the concurrent-build test (rootReadCount goes from 1 → 2:
one readdir for the probe + one for the walk's root scan; the
build dedup behavior is unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(server): walk into nested git repos in the workspace tree
@coderabbitai

coderabbitai Bot commented Apr 20, 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: c31ce3e0-d5ae-4074-8155-af0cd921ca7c

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

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Apr 20, 2026

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f0912ed. Configure here.

Comment thread apps/server/src/http.ts
}),
);

export const legacyServerEnvironmentRouteLayer = HttpRouter.add(

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.

Legacy environment route layer likely never mounted

Medium Severity

The original serverEnvironmentRouteLayer export was split into two: a new serverEnvironmentRouteLayer (serving /.well-known/workbench/environment) and a new legacyServerEnvironmentRouteLayer (serving /.well-known/t3/environment). If the server composition code only references serverEnvironmentRouteLayer, the legacy route layer is exported but never mounted, silently breaking backward compatibility for any existing clients hitting the old /.well-known/t3/environment endpoint.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f0912ed. Configure here.

@jlmcmich

Copy link
Copy Markdown
Author

Closing this because the branch should be reviewed in jlmcmich/workbench rather than upstream.

@jlmcmich jlmcmich closed this Apr 20, 2026
const changed = changedPaths.has(entry.path);

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 lib/workspaceFileTree.ts:114

At line 114, changedPaths.has(entry.path) uses the original entry.path which may have a trailing slash (e.g., "workbench/"), but changedPaths contains artifact paths without trailing slashes. Entries for submodules or nested git repos will never be marked as changed even when their normalized path exists in changedPaths. The check should use normalizedPath instead.

-    const changed = changedPaths.has(entry.path);
+    const changed = changedPaths.has(normalizedPath);
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/lib/workspaceFileTree.ts around line 114:

At line 114, `changedPaths.has(entry.path)` uses the original `entry.path` which may have a trailing slash (e.g., `"workbench/"`), but `changedPaths` contains artifact paths without trailing slashes. Entries for submodules or nested git repos will never be marked as changed even when their normalized path exists in `changedPaths`. The check should use `normalizedPath` instead.

Evidence trail:
apps/web/src/lib/workspaceFileTree.ts lines 82-87 (comment explaining trailing slash case), lines 85-86 (hasTrailingSlash and normalizedPath logic), line 114 (changedPaths.has(entry.path) using entry.path instead of normalizedPath), apps/web/src/components/console/ConsoleRail.tsx lines 233-236 (changedPaths populated from artifacts without trailing slashes), apps/web/src/lib/workspaceFileTree.test.ts lines 32-45 (test confirming trailing-slash entries exist but no test combining trailing-slash entries with changedPaths lookup)

Comment on lines +75 to +78
removeItem: (name: string) => {
localStorage.removeItem(name);
},
};

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.

🟢 Low src/composerDraftStore.ts:75

removeItem only deletes the new key, so stale legacy data remains in localStorage. When a user clears drafts and then reopens the composer, getItem falls back to the legacy key and resurrects the old draft. Consider also removing LEGACY_COMPOSER_DRAFT_STORAGE_KEY in removeItem to prevent zombie data.

Suggested change
removeItem: (name: string) => {
localStorage.removeItem(name);
},
};
removeItem: (name: string) => {
localStorage.removeItem(name);
+ if (name === COMPOSER_DRAFT_STORAGE_KEY) {
+ localStorage.removeItem(LEGACY_COMPOSER_DRAFT_STORAGE_KEY);
+ }
},
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/composerDraftStore.ts around lines 75-78:

`removeItem` only deletes the new key, so stale legacy data remains in `localStorage`. When a user clears drafts and then reopens the composer, `getItem` falls back to the legacy key and resurrects the old draft. Consider also removing `LEGACY_COMPOSER_DRAFT_STORAGE_KEY` in `removeItem` to prevent zombie data.

Evidence trail:
apps/web/src/composerDraftStore.ts lines 60-78 (REVIEWED_COMMIT): `composerBaseStorage` IIFE defines:
- `getItem` (lines 65-71): fallback to `LEGACY_COMPOSER_DRAFT_STORAGE_KEY` when new key returns null
- `removeItem` (lines 75-77): only removes the passed `name`, not the legacy key

Constants at lines 44-46:
- `COMPOSER_DRAFT_STORAGE_KEY = "workbench:composer-drafts:v1"`
- `LEGACY_COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1"`

Comment on lines +43 to +46
const code = await new Promise<number>((resolve, reject) => {
child.once("error", reject);
child.once("exit", (exitCode) => resolve(exitCode ?? 0));
});

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 provider/piRuntime.ts:43

Using child.once("exit", ...) returns the exit code before stdout and stderr streams finish draining, so the final chunks of output are lost in the returned PiCommandResult. The close event fires only after all stdio streams are fully closed. Consider changing "exit" to "close" to capture complete output.

+  const code = await new Promise<number>((resolve, reject) => {
+    child.once("error", reject);
+    child.once("close", (exitCode) => resolve(exitCode ?? 0));
+  });
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/provider/piRuntime.ts around lines 43-46:

Using `child.once("exit", ...)` returns the exit code before `stdout` and `stderr` streams finish draining, so the final chunks of output are lost in the returned `PiCommandResult`. The `close` event fires only after all stdio streams are fully closed. Consider changing `"exit"` to `"close"` to capture complete output.

Evidence trail:
apps/server/src/provider/piRuntime.ts lines 43-53 at REVIEWED_COMMIT - shows `child.once("exit", ...)` pattern followed by immediately returning the collected chunks. Node.js child_process documentation confirms that `exit` fires when the process terminates while `close` fires after all stdio streams are fully closed: https://nodejs.org/api/child_process.html#event-close

Comment on lines +435 to +438
expect(page.getByLabelText("Edit file contents")).toBeDefined();
expect(document.body.textContent ?? "").toContain("Save");
expect(document.body.textContent ?? "").toContain("Cancel");
});

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.

🟢 Low console/ConsoleRail.browser.tsx:435

Line 435 uses expect(page.getByLabelText("Edit file contents")).toBeDefined(), which always passes because page.getByLabelText() returns a Locator object that is always truthy regardless of whether the element exists in the DOM. This test cannot fail even when the textarea is missing. Consider using await expect(...).toBeVisible() or a direct DOM query like line 517 to verify actual element presence.

-    await vi.waitFor(() => {
-      expect(page.getByLabelText("Edit file contents")).toBeDefined();
-      expect(document.body.textContent ?? "").toContain("Save");
-      expect(document.body.textContent ?? "").toContain("Cancel");
-    });
+    await vi.waitFor(() => {
+      expect(document.querySelector('textarea[aria-label="Edit file contents"]')).not.toBeNull();
+      expect(document.body.textContent ?? "").toContain("Save");
+      expect(document.body.textContent ?? "").toContain("Cancel");
+    });
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/console/ConsoleRail.browser.tsx around lines 435-438:

Line 435 uses `expect(page.getByLabelText("Edit file contents")).toBeDefined()`, which always passes because `page.getByLabelText()` returns a Locator object that is always truthy regardless of whether the element exists in the DOM. This test cannot fail even when the textarea is missing. Consider using `await expect(...).toBeVisible()` or a direct DOM query like line 517 to verify actual element presence.

Evidence trail:
apps/web/src/components/console/ConsoleRail.browser.tsx lines 430-438 (showing the defective assertion at line 435: `expect(page.getByLabelText("Edit file contents")).toBeDefined();`), apps/web/src/components/console/ConsoleRail.browser.tsx lines 510-525 (showing correct approach at line 517: `expect(document.querySelector('textarea[aria-label="Edit file contents"]')).not.toBeNull();`), Playwright documentation confirms getByLabelText() returns a Locator object which is always truthy.

Comment on lines +162 to +174
function LandingPillButton(props: React.ComponentProps<typeof Button>) {
return (
<Button
size="sm"
variant="ghost"
className={cn(
"h-10 rounded-full border border-border/60 bg-background/84 px-4 text-[15px] text-foreground shadow-[0_10px_30px_-24px_rgba(0,0,0,0.55)] backdrop-blur hover:bg-background/96",
props.className,
)}
{...props}
/>
);
}

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.

🟠 High components/NoActiveThreadState.tsx:162

LandingPillButton spreads {...props} after the computed className, so when callers pass className="justify-start" the default pill styles (h-10 rounded-full border...) are completely replaced instead of merged. The spread should come before the explicit className prop, or className should be omitted from the spread and merged explicitly with cn().

+function LandingPillButton(props: React.ComponentProps<typeof Button>) {
+  const { className, ...rest } = props;
   return (
     <Button
       size="sm"
       variant="ghost"
       className={cn(
         "h-10 rounded-full border border-border/60 bg-background/84 px-4 text-[15px] text-foreground shadow-[0_10px_30px_-24px_rgba(0,0,0,0.55)] backdrop-blur hover:bg-background/96",
-        props.className,
+        className,
       )}
-      {...props}
+      {...rest}
     />
   );
 }
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/NoActiveThreadState.tsx around lines 162-174:

`LandingPillButton` spreads `{...props}` after the computed `className`, so when callers pass `className="justify-start"` the default pill styles (`h-10 rounded-full border...`) are completely replaced instead of merged. The spread should come before the explicit `className` prop, or `className` should be omitted from the spread and merged explicitly with `cn()`.

Evidence trail:
apps/web/src/components/NoActiveThreadState.tsx lines 162-176 at REVIEWED_COMMIT: Shows `LandingPillButton` component with `className={cn(..., props.className)}` followed by `{...props}` spread. In JSX, props spread after an explicit attribute will overwrite matching properties, so caller-provided className will replace the computed className.

@macroscopeapp

macroscopeapp Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

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

@jlmcmich
jlmcmich deleted the feat/pi-preferred-models branch April 20, 2026 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant