Part V · The work lifecycleWorkbench services
Chapter 28source checked

Terminals, files, previews, MCP, VCS, and pull requests

T3 Code's chat surrounds itself with server-owned workbench services. Their transports differ deliberately: terminal and preview state stream, assets use expiring signed HTTP paths, pull-request diffs use authenticated HTTP slices, and desktop owns the only in-app browser host.

What this chapter resolves
  • Trace the ownership and transport boundary for terminals, workspace files, signed assets, preview, MCP, VCS, and pull requests.
  • Distinguish server metadata from the desktop-only processes and webviews that enact a workbench action.
  • Read surface parity as capability-specific rather than assuming every web, desktop, and mobile client has the same tool.
  • Identify where authorization is re-checked instead of trusting a visible client control.

Chat is the control surface, not the whole application. A running T3 environment also owns processes, workspace paths, Git state, provider credentials, and source- control integrations. The book has already traced turn orchestration, worktree creation, context, and checkpoints. This chapter follows the adjacent workbench services without retelling those internal sagas.

The rule that prevents most incorrect diagrams is simple: a visible panel does not move authority into the client. In the pinned implementation, server services keep the filesystem, PTY, VCS, asset-signing, and remote-host integrations. Web, desktop, and mobile clients ask one selected environment for a capability and render the result through surface-specific code.

1. The service map: owner before UI

Service Primary authority Main data movement A tempting but false shortcut
Terminal server PTY manager RPC commands; attach snapshot then live events “The browser owns the shell.”
Files server workspace services authenticated project RPC “The file tree is a local checkout.”
Assets server asset access issue URL by RPC, fetch bytes by signed HTTP “A file URL exposes the workspace.”
Preview server metadata plus desktop Electron host RPC/events plus desktop IPC/webview “The server is the browser renderer.”
T3 MCP preview tools provider-session registry and MCP server scoped bearer HTTP, then broker stream “T3 controls every MCP server.”
VCS / worktrees server driver and filesystem RPC snapshots/commands “A client branch selector creates a checkout locally.”
Pull requests server host-provider service RPC controls; HTTP diff slices “A rendered action proves the user may write.”
Figure 28.1 · Workbench capabilities cross different transport boundariesfollow authority, not panel chrome
Workbench service owners and transportsDiagram loading

Zoom with the controls, +/, or Ctrl/ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.

Workbench service owners and transports
Text equivalent

Web, desktop, and mobile clients call authenticated server workbench services. The terminal returns a snapshot and live stream. Asset RPC returns an expiring signed HTTP URL. Pull-request RPC handles controls while authenticated HTTP carries diff slices. Server preview state is metadata; desktop Electron main owns the Chromium guest. A provider can call the bearer-authenticated T3 MCP preview toolkit, which brokers work to a compatible desktop host.

Figure 28.1. Clients use authenticated environment RPC for terminal commands, workspace files, preview metadata, VCS, and most pull-request controls. Terminal state also streams. Assets and large pull-request diffs take separate HTTP paths. A provider may receive a scoped T3 MCP bearer credential whose preview requests are brokered to an available desktop automation host; Electron main owns the guest Chromium WebContents.
Interactive capability and path matrix

Choose a workbench service; then inspect its owner, transport, and surface edge

Each path is an architecture summary of the pinned source, not a live T3 environment.

Terminal. Server PTY manager. RPC command + attach stream.

Terminal
Primary owner
Server PTY manager
Transport model
RPC command + attach stream
  1. 1Client names thread + terminal id
  2. 2Server opens or restores PTY history
  3. 3Attach receives snapshot, then live output / exit / activity events

Surface edge

web
Ghostty web surface
desktop
web renderer inside Electron
mobile
native terminal surface
Provider
No provider terminal endpoint
Static matrix and reading rules
ServiceOwnerTransportImportant edge
TerminalServer PTY managerRPC command + attach streamThe client renders a terminal; the process and its environment stay with the server.
FilesServer workspace servicesAuthenticated project RPCA file pane is not a local filesystem mirror; operations address the selected server environment.
AssetsServer asset accessRPC URL issue → signed HTTP GETA signed URL grants only the claimed resource/path until expiry; it is not a filesystem browse token.
Preview & browserDesktop Electron host + server preview metadataServer RPC/event model + desktop IPC/webviewThe server tracks collaboration state; it does not itself render or automate a browser page.
MCP preview toolkitProvider-session credential registry + MCP HTTP serverProvider-scoped bearer HTTP + broker streamThis is T3's preview toolkit, not a universal inventory or controller for every MCP server a provider may know.
VCS & worktreesServer VCS driverAuthenticated RPC commands and snapshotsVCS capability is environment-local. A UI view is not a promise that a remote checkout, branch, or worktree exists everywhere.
Pull requestsServer host-provider serviceRPC controls + authenticated HTTP diff slicesA visible action is never the authorization decision. The server re-checks host capability and current viewer permission before writes.

“Desktop uses the web renderer path” does not erase Electron-only authority: preview’s actual webview and automation host are desktop-owned. “No matching route” records this source audit; it is not a product roadmap claim.

The lab’s surface rows intentionally say which edge is evidenced, not which feature is “supported” in the abstract. Desktop often shares the web renderer, but its Electron main/preload boundary adds authority that a hosted browser does not receive. Conversely, mobile has its own native terminal and file paths even though the server protocol is shared.

2. A terminal is a server process with a streamed screen

The terminal contract names a thread and a client-chosen terminal id; it carries a cwd, optional worktree path, dimensions, and bounded environment overrides. The server manager opens/reuses a PTY session, exposes lifecycle commands (open, write, resize, clear, restart, close), and offers two different subscriptions:

  • attachStream begins with the selected terminal’s snapshot and follows it with output/lifecycle events. This is the high-fidelity path used to reconstruct a visible screen.
  • metadata subscription is a cheaper stream of terminal summaries for a broader shell/session view.
apps/server/src/terminal/Manager.ts:2351–2402 ↗verbatim · typescript · b271e39c
  const attachStream: TerminalManager["Service"]["attachStream"] = (input, listener) => {
    let unsubscribe: (() => void) | null = null;
 
    return Effect.gen(function* () {
      const bufferedEvents: TerminalEvent[] = [];
      let deliverLive = false;
 
      unsubscribe = yield* subscribe((event) => {
        if (event.threadId !== input.threadId || event.terminalId !== input.terminalId) {
          return Effect.void;
        }
 
        if (!deliverLive) {
          bufferedEvents.push(event);
          return Effect.void;
        }
 
        const attachEvent = terminalEventToAttachEvent(event);
        return attachEvent ? listener(attachEvent) : Effect.void;
      });
 
      const initialSnapshot = yield* openOrAttachForStream(input);
 
      yield* listener({
        type: "snapshot",
        snapshot: initialSnapshot,
      });
 
      for (const event of bufferedEvents) {
        if (isDuplicateAttachSnapshotEvent(event, initialSnapshot)) {
          continue;
        }
 
        const attachEvent = terminalEventToAttachEvent(event);
        if (attachEvent) {
          yield* listener(attachEvent);
        }
      }
 
      deliverLive = true;
      return () => {
        unsubscribe?.();
        unsubscribe = null;
      };
    }).pipe(
      Effect.catchCause((cause) =>
        Effect.flatMap(
          Effect.sync(() => {
            unsubscribe?.();
            unsubscribe = null;
          }),
          () => Effect.failCause(cause),
Read this as: Attach subscribes before reading the initial snapshot, buffers racing events, emits the snapshot first, and then releases nonduplicate live events.

Terminal history is maintained in memory, persisted with a debounce/cap, and then replayed on a later first open before a new process is started. That makes a reopened screen useful after a client disconnect; it does not turn terminal history into a durable replay of every process state or side effect. The manager’s finalizer kills its sessions, and thread deletion performs best-effort terminal and provider cleanup. Treat a terminal as a live server resource with an inspectable text history, not as a durable orchestration event stream.

Web renders that stream through its Ghostty-based surface. Mobile drives the same open/attach/write/resize/close protocol but renders through its native terminal module; it also explicitly reopens a stale, ended subscription only when the panel is shown. Desktop uses the web renderer path for the terminal panel. Those are rendering differences around one server-owned PTY boundary.

3. Files and assets are deliberately different paths

Workspace files use authenticated project operations: list/search entries, read a file, and write a file. The web file browser, preview, and editor are therefore views of an environment’s workspace service; they are not a sync engine that copies the project into the client. Mobile has a tree/file-preview path and obtains a workspace-file asset URL using the same thread/environment address.

An asset request is narrower. Its resource union names a workspace file in a thread, an attachment, or a project favicon. The server resolves the workspace context, normalizes the root, rejects paths outside that root, restricts previewable workspace resource types, resolves real paths, and emits an expiring signed URL. On retrieval it verifies the signature and expiry again; workspace claims also enforce exact-name or constrained relative-path rules, extension allow-lists, and canonical containment.

apps/server/src/assets/AssetAccess.ts:410–451 ↗verbatim · typescript · 27cc3e4c
          cause,
        }),
    ),
  );
  if (claims.kind === "project-favicon" || claims.kind === "project-favicon-external") {
    const issuedAt = yield* Clock.currentTimeMillis;
    expiresAt =
      (Math.floor(issuedAt / PROJECT_FAVICON_TOKEN_BUCKET_MS) + 2) *
      PROJECT_FAVICON_TOKEN_BUCKET_MS;
    claims = { ...claims, expiresAt };
  }
  const encodedPayload = base64UrlEncode(encodeAssetClaims(claims));
  const token = `${encodedPayload}.${signPayload(encodedPayload, signingSecret)}`;
  return {
    relativeUrl: `${ASSET_ROUTE_PREFIX}/${token}/${encodeURIComponent(fileName)}`,
    expiresAt,
    ...(sourcePath !== undefined ? { sourcePath } : {}),
  };
});
 
export const resolveAsset = Effect.fn("AssetAccess.resolveAsset")(function* (
  token: string,
  relativePath: string,
) {
  const [encodedPayload, signature] = token.split(".");
  if (!encodedPayload || !signature) return null;
 
  const secretStore = yield* ServerSecretStore.ServerSecretStore;
  const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32).pipe(
    Effect.tapError((cause) => Effect.logError("Failed to load the asset signing key.", { cause })),
    Effect.orElseSucceed(() => null),
  );
  if (!signingSecret) return null;
  if (!timingSafeEqualBase64Url(signature, signPayload(encodedPayload, signingSecret))) return null;
 
  const claims = decodeClaims(encodedPayload);
  if (!claims || claims.expiresAt <= (yield* Clock.currentTimeMillis)) return null;
 
  if (claims.kind === "attachment") {
    const config = yield* ServerConfig.ServerConfig;
    const attachmentPath = resolveAttachmentPathById({
      attachmentsDir: config.attachmentsDir,
Read this as: Issuance signs narrow claims into a relative URL. Retrieval independently verifies the signature and expiry before resolving the claimed resource.

This split also explains why “file preview” has more than one meaning. Source text can arrive by a project read operation; an image, browser document, or nested asset can arrive through a signed HTTP path; and the interactive application preview in the next section is neither of those things.

4. Preview state streams; Chromium authority stays on desktop

The server PreviewManager holds small per-thread/tab snapshots: URL/navigation state, history affordances, viewport setting, timestamps, a server epoch, and a monotonic revision. It serializes state mutation and publishes ordered lightweight events, so reconnecting clients can list metadata and converge on a tab’s recorded state. The manager’s refresh operation only confirms that a session exists; the actual reload is performed by the desktop bridge and later reported back.

The actual in-app browser is intentionally desktop-only. The Electron preload exposes a curated preview IPC API for tab creation, navigation, zoom, screenshot, recording, element picking, and automation actions. The web route rejects the preview keybinding outside a supported desktop runtime with a “Preview is desktop-only” message. Thus the server can coordinate preview metadata across subscribers, but it cannot make a plain web client or phone into a Chromium webview host.

This is a useful two-layer model:

  1. Server coordination: tab identity, state/revision, and typed preview RPC.
  2. Desktop enactment: the renderer requests actions through curated preload IPC; Electron main’s preview manager validates and owns the guest Chromium WebContents, then reports navigation, loading, and failures over the bridge.

That division prevents a remote client from assuming it can inspect a page merely because a server has a preview record. A record may exist while no compatible desktop host is connected.

5. T3’s MCP service gives a provider a scoped preview toolkit

T3 can attach its own t3-code MCP server to a provider session when the agent browser-access setting permits it. This is not the same as discovering or managing every external MCP server configured by Codex, Claude, Cursor, Grok, or OpenCode. It is a narrowly scoped T3 endpoint for preview operations.

At provider-session preparation, the server either revokes/clears prior MCP state when access is disabled or issues a fresh random bearer credential. Its scope is bound to the environment, thread, provider instance, and a registry-generated MCP session id—the latter is not an adapter-native Codex, Claude, Cursor, Grok, or OpenCode session identity. The registry stores a hash, not the raw token, and only advertises the preview capability. The /mcp route sits outside ordinary environment auth, so it requires that provider-scoped bearer credential. Registry liveness is refreshed by both MCP traffic and active provider turns; stop/revoke paths remove credentials eagerly, while abandoned records age out after the liveness window.

apps/server/src/mcp/McpSessionRegistry.ts:112–168 ↗verbatim · typescript · 1c72aab2
    const next = new Map(
      Array.from(records).filter(
        ([, record]) => timestamp - record.lastAliveAt <= livenessWindowMs,
      ),
    );
    return next.size === records.size ? records : next;
  };
 
  const issue: McpSessionRegistryShape["issue"] = Effect.fn("McpSessionRegistry.issue")(
    function* (request) {
      const issuedAt = yield* currentTimeMillis;
      const providerSessionId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
      const rawToken = yield* crypto.randomBytes(32).pipe(Effect.map(tokenFromBytes), Effect.orDie);
      const tokenHash = yield* hashToken(rawToken);
      const scope: McpInvocationContext.McpInvocationScope = {
        environmentId,
        threadId: ThreadId.make(request.threadId),
        providerSessionId,
        providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
        capabilities: new Set(["preview"]),
        issuedAt,
      };
      yield* SynchronizedRef.update(state, ({ records }) => {
        const next = new Map(pruneDead(records, issuedAt));
        next.set(tokenHash, { tokenHash, scope, lastAliveAt: issuedAt });
        return { records: next };
      });
      return {
        config: {
          environmentId,
          threadId: scope.threadId,
          providerSessionId,
          providerInstanceId: scope.providerInstanceId,
          endpoint,
          authorizationHeader: `Bearer ${rawToken}`,
        },
      };
    },
  );
 
  const resolve: McpSessionRegistryShape["resolve"] = Effect.fn("McpSessionRegistry.resolve")(
    function* (rawToken) {
      if (rawToken.length === 0) return undefined;
      const tokenHash = yield* hashToken(rawToken);
      const timestamp = yield* currentTimeMillis;
      return yield* SynchronizedRef.modify(state, ({ records }) => {
        const current = pruneDead(records, timestamp);
        const record = current.get(tokenHash);
        if (!record) return [undefined, { records: current }] as const;
        const next = new Map(current);
        next.set(tokenHash, { ...record, lastAliveAt: timestamp });
        return [record.scope, { records: next }] as const;
      });
    },
  );
 
  const touch: McpSessionRegistryShape["touch"] = Effect.fn("McpSessionRegistry.touch")(
Read this as: The registry generates both the bearer token and its own MCP session id, hashes the token, and binds the resulting preview-only scope to environment, thread, and provider instance.

All five adapter paths can consume the T3-owned MCP configuration, but their native injection mechanisms differ: Codex uses app-server arguments plus a bearer-token environment variable; Claude supplies an HTTP MCP server; Cursor and Grok pass ACP MCP server definitions; OpenCode registers the remote server only when T3 owns the local OpenCode server (!server.external). This is adapter integration for one T3 toolkit, not shared management of each harness’s external MCP inventory.

The MCP server authenticates the request, provides the resulting invocation scope to preview toolkit handlers, and sends automation through PreviewAutomationBroker. The broker selects a compatible desktop host in the same environment, keeps a provider-session-to-host lease while the connection remains live, and returns an unavailable error when no suitable host can perform an operation. It does not silently transfer an active browser automation session to another desktop because a credential timer expired.

6. VCS remains an environment service; worktrees remain a lifecycle saga

The VCS contract exposes a driver kind and capability flags such as worktree, bookmark, atomic-snapshot, and default-push support. Client views query or command the server driver against the selected workspace; they do not obtain raw local Git access merely by rendering a branch, diff, or worktree picker.

Two earlier boundaries matter here:

  • Chapter 22 traces the start-turn worktree saga, including its server filesystem effects and failure seam.
  • Chapter 27 traces hidden checkpoint refs, comparison modes, and the destructive restore/revert path.

The workbench is where those results become visible: a terminal’s cwd can be the thread worktree, a file panel reads it, and review/VCS panels query its Git state. That co-location is not a transaction. A terminal command, a Git operation, and a provider file edit still have their own ordering and error channels.

7. Pull requests add host authorization and a second transport lane

For each eligible project with a repository identity and supported host provider, the pull-request service builds a repository target. Projects without the required identity/provider/repository are skipped. It deduplicates worktrees of one repository for listing purposes, but retains alternate roots for host-viewer lookup so one unreadable checkout does not make a whole host look signed out. Host support and account identity are therefore server-side facts, not client guesses.

For a write, the service checks two distinct gates in order:

  1. Host/provider capability: does the selected provider support this action, merge strategy, update strategy, comment/review feature, or other requested operation?
  2. Fresh viewer permission: does the currently signed-in account have the necessary host-reported right now?

Only then does it call the host provider. The server deliberately does not try to invent permissions it cannot query, such as whether a person may edit their own already-posted text; that decision stays with the host. Successful mutations bump server-side invalidation epochs so subsequent list/detail reads refresh rather than pretend cached rows are authoritative.

apps/server/src/pullRequest/PullRequestService.ts:1300–1367 ↗verbatim · typescript · a76b5450
  const runAction: PullRequestService["Service"]["runAction"] = (input) =>
    requireProject(input).pipe(
      Effect.flatMap((project): Effect.Effect<void, PullRequestError> => {
        // The surface hides what a host cannot do, and this refuses it as well: a request that
        // reached here anyway must not be handed to a provider that never claimed the action.
        if (!project.api.capabilities.actions.includes(input.action)) {
          return Effect.fail(
            new PullRequestOperationError({
              operation: "runAction",
              detail: `This host cannot ${input.action} a change request.`,
            }),
          );
        }
        // A strategy the host does not offer must be refused rather than passed on: every
        // provider maps an unrecognised method to its own default, so asking Azure DevOps to
        // rebase would quietly merge instead of failing.
        if (
          input.mergeMethod !== undefined &&
          !project.api.capabilities.mergeMethods.includes(input.mergeMethod)
        ) {
          return Effect.fail(
            new PullRequestOperationError({
              operation: "runAction",
              detail: `This host cannot merge with the ${input.mergeMethod} strategy.`,
            }),
          );
        }
        // The same for the way a stale branch is brought up to date: a host that only merges
        // must not be asked to rebase and left to pick something else.
        if (
          input.updateMethod !== undefined &&
          !(project.api.capabilities.updateMethods ?? []).includes(input.updateMethod)
        ) {
          return Effect.fail(
            new PullRequestOperationError({
              operation: "runAction",
              detail: `This host cannot update a branch by ${input.updateMethod}.`,
            }),
          );
        }
        // What the host can do and what this account may ask of it are two questions, and both
        // have to say yes. The second is asked last, because it costs a request and the checks
        // above do not.
        return viewerPermissionsOf(project, input, "runAction").pipe(
          Effect.flatMap((viewer): Effect.Effect<void, PullRequestError> => {
            if (!viewer.actions.includes(input.action)) {
              return Effect.fail(
                new PullRequestOperationError({
                  operation: "runAction",
                  detail: ACTION_ACCESS_REFUSALS[input.action],
                }),
              );
            }
            if (
              input.updateMethod !== undefined &&
              !(viewer.updateMethods ?? []).includes(input.updateMethod)
            ) {
              return Effect.fail(
                new PullRequestOperationError({
                  operation: "runAction",
                  detail: ACTION_ACCESS_REFUSALS["update-branch"],
                }),
              );
            }
            return project.api
              .runAction({
                cwd: project.project.workspaceRoot,
                repository: project.repository,
Read this as: A PR action first passes provider capability and strategy checks, then fetches current viewer permissions before invoking the host provider.

Most PR controls use typed RPC. Diff content is exceptional: large, compressible diff slices travel through an authenticated HTTP endpoint, with a host-opaque cursor and truncation/next-cursor fields. The web route checks each connected environment’s advertised pull-request capability and selects a capable source. Desktop inherits that web route. This pinned mobile source audit found terminal, file, and VCS paths (including a control that can open an existing PR externally), but no corresponding in-app pull-request list/detail route; that observation is not a promise about a future mobile release.

What a surface can honestly claim

Surface Strongly evidenced workbench roles Important non-equivalence
Web terminal renderer, files/editor, signed assets, VCS views, capability-gated PR route cannot host the in-app Chromium preview
Desktop web roles plus Electron preview/webview and preview automation host the web renderer alone does not acquire Electron IPC authority
Mobile native terminal rendering, file tree/preview, signed workspace assets, server-backed VCS state/actions no matching desktop-style preview host or in-app PR list/detail route was found in this audit
Provider session provider harness work plus optional T3-scoped MCP preview toolkit no generic ownership of client filesystem, terminal, or arbitrary MCP inventory

The durable lesson is to locate authority before adding an arrow: environment server for resources and host integrations, desktop for browser enactment, provider adapter/session for harness-native behavior, and client for the interactive view of a selected environment.

T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.