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.” |
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.
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.
packages/contracts/src/terminal.ts:1–234 ↗apps/server/src/terminal/Manager.ts:123–198 ↗packages/contracts/src/assets.ts:1–180 ↗apps/server/src/assets/AssetAccess.ts:186–425 ↗packages/contracts/src/preview.ts:145–289 ↗apps/desktop/src/preview/Manager.ts:1875–2028 ↗apps/server/src/mcp/McpSessionRegistry.ts:59–201 ↗apps/server/src/mcp/PreviewAutomationBroker.ts:420–587 ↗packages/contracts/src/vcs.ts:1–36 ↗packages/contracts/src/rpc.ts:289–603 ↗packages/contracts/src/environmentHttp.ts:533–547 ↗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.
- Primary owner
- Server PTY manager
- Transport model
- RPC command + attach stream
- 1Client names thread + terminal id
- 2Server opens or restores PTY history
- 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
| Service | Owner | Transport | Important edge |
|---|---|---|---|
| Terminal | Server PTY manager | RPC command + attach stream | The client renders a terminal; the process and its environment stay with the server. |
| Files | Server workspace services | Authenticated project RPC | A file pane is not a local filesystem mirror; operations address the selected server environment. |
| Assets | Server asset access | RPC URL issue → signed HTTP GET | A signed URL grants only the claimed resource/path until expiry; it is not a filesystem browse token. |
| Preview & browser | Desktop Electron host + server preview metadata | Server RPC/event model + desktop IPC/webview | The server tracks collaboration state; it does not itself render or automate a browser page. |
| MCP preview toolkit | Provider-session credential registry + MCP HTTP server | Provider-scoped bearer HTTP + broker stream | This is T3's preview toolkit, not a universal inventory or controller for every MCP server a provider may know. |
| VCS & worktrees | Server VCS driver | Authenticated RPC commands and snapshots | VCS capability is environment-local. A UI view is not a promise that a remote checkout, branch, or worktree exists everywhere. |
| Pull requests | Server host-provider service | RPC controls + authenticated HTTP diff slices | A 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:
attachStreambegins 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.
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),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.
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,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:
- Server coordination: tab identity, state/revision, and typed preview RPC.
- 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.
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")(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:
- Host/provider capability: does the selected provider support this action, merge strategy, update strategy, comment/review feature, or other requested operation?
- 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.
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,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.