Add preferred Pi model settings and pickers - #2211
Conversation
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
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
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
| }), | ||
| ); | ||
|
|
||
| export const legacyServerEnvironmentRouteLayer = HttpRouter.add( |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit f0912ed. Configure here.
|
Closing this because the branch should be reviewed in jlmcmich/workbench rather than upstream. |
| const changed = changedPaths.has(entry.path); |
There was a problem hiding this comment.
🟡 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)
| removeItem: (name: string) => { | ||
| localStorage.removeItem(name); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🟢 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.
| 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"`
| const code = await new Promise<number>((resolve, reject) => { | ||
| child.once("error", reject); | ||
| child.once("exit", (exitCode) => resolve(exitCode ?? 0)); | ||
| }); |
There was a problem hiding this comment.
🟡 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
| expect(page.getByLabelText("Edit file contents")).toBeDefined(); | ||
| expect(document.body.textContent ?? "").toContain("Save"); | ||
| expect(document.body.textContent ?? "").toContain("Cancel"); | ||
| }); |
There was a problem hiding this comment.
🟢 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.
| 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} | ||
| /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🟠 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.
ApprovabilityVerdict: 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. |


Summary
Validation
Notes
bun run test --filter=@workbench/webstill hits an unrelated existing failure inapps/web/src/components/chat/MessagesTimeline.test.tsxNote
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
t3toworkbench, updates build/release filters, and adds backward-compatible config/env handling (e.g.,WORKBENCH_*with fallback to legacyT3CODE_*, default base dir migration from~/.t3to~/.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 aapps/serverbundle watcher alongside Electron dev. Also adds a new/.well-known/workbench/environmentendpoint 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
piprovider throughout the stack: contracts (PiSettings,PiModelOptions,PiModelSelection), server adapter (PiAdapter.ts) that spawns thepiCLI in JSON mode to stream runtime events, and a PiProvider.ts that checks install/auth/version status and lists models.piin SettingsPanels.tsx: aPiLoginPanelthat 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.PlanSidebarin ChatView.tsx with a resizableConsoleRailthat renders Files, Recent Changes, and Tasks panes, supports per-pane collapse/visibility, workspace file preview/edit/diff, and selection-to-chat insertion.@t3tools/*to@workbench/*across all packages, renames the server CLI binary fromt3toworkbench, updates localStorage keys (with legacy fallback reads), and updates all branding strings, icons, and well-known paths (e.g./.well-known/workbench/environment).NetServicecontext 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