This repository is a fork of pingdotgg/t3code. This page lists what the fork adds or changes, so each difference can be found, kept working across upstream syncs, or offered back upstream. Everything not listed here matches upstream.
Remotes: origin is this fork, upstream is pingdotgg/t3code. Upstream is merged in periodically
(t3code/sync-upstream-* branches). To see the fork-only commits:
git fetch upstream
git log --oneline --no-merges upstream/main..HEADAgents update this page in the same change that adds, changes, or removes a difference. The rule is in AGENTS.md.
The server's built-in browser stays available when an individual automation request times out,
preserving other sessions and their tabs. Expired or cancelled queued actions cannot execute
later. Snapshot and evaluation reads have bounded deadlines, and preview_snapshot with
includeImage: false skips capture unless save: true needs a screenshot file.
This ports upstream PR #16941 ahead of its merge. External browser hosts keep upstream's disconnect-on-timeout behavior.
Code: PreviewAutomationBroker.ts, ServerBrowser.ts, and ServerBrowserPage.ts.
Updating the desktop app or a connected server pauses running threads before the process restarts. After T3 Code is back, each paused thread offers Resume. Continue threads after restarts still resumes threads after a crash or machine restart, and does not auto-start a thread an update already paused.
Code: UpdateThreadPause.ts.
User guide: updating.md.
Settings → Integrations → Open in can add, edit, and remove applications for one environment, choose a default for chat file links, and override it by file extension. Each custom application can use an application logo or a general icon in the workspace Open button and picker. Custom applications appear in the workspace Open in picker and file Open with menus, and can run through the user's interactive login shell so shell functions and aliases work. The workspace Open button can be pinned to any application instead of following the last-used pick. Commands run on the environment hosting the file. Remote workspace menus offer custom applications on that host alongside editors that support SSH links.
See the user guide, settings, and launcher.
Adds a dedicated devin provider driven through the local devin CLI's ACP server
(devin acp). Upstream can also run Devin as a generic ACP Registry agent; the dedicated driver
uses the same shared ACP adapter and Devin protocol handling (subagent markers, message grouping,
client-owned terminals) and adds the rest.
- Each instance has its own binary path and, when added, a private
XDG_DATA_HOME(see Provider sign-in methods). T3 runtime modes map onto Devin's--permission-modeat spawn and onto its session modes; plan turns switch to Devin's Plan mode. - Models come from
devin models list. Devin encodes effort, speed, and context in each model id, so the catalog groups variants into one picker row per model with effort/speed/context options, and the session is switched to the matching variant. Fusion is one row: lead and sidekick are chosen by model family, lead effort is the one Devin advertises for that lead, and sidekick effort is selectable. The context meter uses the catalog's window when Devin does not report one. - T3's MCP tools reach Devin through the shared ACP stdio bridge.
devin skills listfeeds the skill picker, and$skillmentions are sent as Devin's@skills:name. - The usage page covers Devin from the CLI's
sessions.db, which includes sessions run outside T3, priced from the provider snapshot. The page can export the current window as CSV. Acog_...service key withViewOrgConsumptionandDEVIN_ORG_IDcan show organization ACUs in a separate section; those are not mixed into token-cost estimates. Conversation rewind is not supported. - Devin also generates commit messages, PR content, branch names, and thread titles, updates
through
devin update, and signs in by browser, saved login, or a pasted API key. - The welcome wizard lists Devin with an Enable action, since the provider is opt-in.
Code: DevinDriver.ts,
DevinAdapterV2.ts,
DevinAcpSupport.ts,
apps/server/src/provider/**/Devin*, apps/server/src/provider/devinModelCatalog.ts,
apps/server/src/textGeneration/DevinTextGeneration.ts, apps/server/src/usage/devinAccountUsage.ts,
apps/server/src/usage/devinUsageReader.ts, apps/web/src/components/usage/usageExport.ts, and
DevinSettings in packages/contracts/src/settings.ts. User guides:
providers-devin.md and usage.md.
Settings → Providers → Add provider → Custom ACP runs any stdio Agent Client Protocol agent from an executable and arguments, with credentials in the instance's environment variables. Each added instance is one agent; there is no default instance.
- The health check opens a short ACP session and lists what the agent advertises: its models
(the
modelconfig option, else session models), its other config options as model options, and the plan toggle only when it has aplanorarchitectmode. Slash commands come from its session updates, per workspace. - Turns run over the shared ACP adapter, so T3's MCP tools (stdio bridge), approvals with the
agent's own option ids, form elicitations as questions, and fresh-session recovery when a
session cannot be resumed work as for upstream's ACP agents. The selected model is applied
through the
modelconfig option orsession/set_model, and T3 runtime modes map onto the agent's matching session modes. - No sign-in flow, updates, text generation, rewind, or usage accounting.
Code: CustomAcpDriver.ts,
CustomAcpAdapterV2.ts,
apps/server/src/provider/**/CustomAcp*, apps/server/src/provider/acp/AcpCommandCatalog.ts,
and CustomAcpSettings in packages/contracts/src/settings.ts. User guide:
providers-custom-acp.md.
Each provider's Settings > Providers card gets a sign-in section with a method picker: the provider's CLI login flow, saved credentials, or a pasted key. The chosen method is persisted. Codex defaults to the browser flow.
An added instance with a blank home gets a private directory under T3's data (provider-homes),
so a second login does not replace the default. The default instance keeps the CLI's normal home.
Grok uses GROK_HOME; Devin and OpenCode use XDG_DATA_HOME. Cursor is not covered: upstream signs each
Cursor instance in through the Cursor SDK and keeps every instance's sign-in separately.
Cursor usage follows each instance's own sign-in. Upstream reads limits only from the host's shared
CLI login and shows none for an SDK sign-in. In the fork, each instance trades its SDK key (or
CURSOR_API_KEY) for an access token, as the SDK does, and reads its own limits; only the default
instance may fall back to the CLI login. The usage page reads every instance's account as well as the
CLI login, and one account counts once. Code:
usageLimits.ts,
readCursorSdkCredential in
credentialStore.ts, and the Cursor
scan in UsageService.ts. User guide:
usage.md.
Cursor turns also record their token usage. Upstream's Cursor adapter ignores the usage the SDK
returns with each finished run, so Cursor provider turns carry none. The fork records reported
usage while a run is working, updating totals at each model turn's end, and replaces those partial
totals with the finished run's cumulative usage. Interrupted or failed runs retain the partial
counts already reported. The fork maps it onto the
turn's turnTokenUsage (cache reads and writes counted inside input, as for Claude), which feeds
turn analytics and the live usage of delegated tasks run on Cursor
(turnTokenUsage.ts). It is a sum
over the run's model calls, not context occupancy, so Cursor threads still have no context meter.
The usage page keeps reading Cursor's account history, so nothing is counted twice.
Native Grok turns also record the CLI's prompt-wide input, output, cache, and reasoning usage,
feeding turn analytics and app-owned delegated task totals. Counts cover the prompt's model calls,
with incomplete reports marked partial; older builds and interrupted prompts that report no counts
remain unavailable. The context meter stays separate. See
turnTokenUsage.ts.
Code: apps/web/src/components/settings/ProviderAuthSection.tsx,
apps/server/src/provider/ProviderAuthService.ts,
packages/provider-core/src/server/cliAuth.ts,
packages/provider-core/src/server/instanceEnvironment.ts, and
packages/contracts/src/providerSetup.ts. User guides:
providers-devin.md and
providers-opencode.md.
Web, desktop, and mobile show processed thread tokens separately from context occupancy. Main turn counts exclude provider threads owned by subagents; reported subagent totals appear separately. Missing or partial main-turn counts mark the token total as a lower bound. Providers that report session costs also show a cost subtotal, grouped by currency. Sessions without cost reports are excluded; no model-price estimate or subscription charge is inferred.
At 80% of a reported context window, the composer suggests reducing context. Web and desktop offer
the existing Compact action when the provider advertises it and Continue in new tab when tabs are
available. Mobile can insert /compact into an empty composer when supported. Web and desktop remember dismissal until the provider reports real usage below 80%; mobile
hides it for the current thread view, also rearming below 80%. Providers without context occupancy reports,
such as native Cursor and Grok, show token totals without a context notice.
Code: threadUsage.ts,
ContextWindowMeter.tsx, and
ThreadUsageNotice.tsx.
User guide: composer.md.
Changing the model in an existing thread leaves a divider in the transcript, the same kind of marker a fork leaves in the new thread. It names the model the conversation was on and the model the next message uses, and it appears as soon as the composer selection differs, before that message is sent. A provider switch that already shows a context handoff keeps that handoff instead of a second divider. Effort and other options on the same model do not add one.
Code: modelChangeMarker.ts,
V2LifecycleRow.tsx, and
thread-handoff-row.tsx.
When more than one enabled instance of a provider can serve the thread, the composer has an account picker separate from the model picker. The model list stays one row per provider and model. Codex only switches in place between instances that share the thread's home. After the first message on a server thread, other accounts stay listed and are marked New tab; choosing one forks the chat into a new tab on that account (see Chat tabs). Where forking is unavailable, an account that can no longer be switched stays visible as a label. This is web and desktop.
Code: apps/web/src/components/chat/ProviderAccountPicker.tsx and
apps/web/src/components/chat/providerAccountSelection.ts. User guide:
providers-codex.md.
Choosing a Cursor context tier above 300K (Grok 4.7 at 500K, or 1M on other models) turns on Cursor's Max Mode for the request, as Cursor's own CLI does. Upstream sends these tiers without Max Mode, so Grok 4.7 at 500K fails with "Invalid parameters for registry model" and 1M tiers silently run at about 300K. Max Mode is billed per token on usage-based Cursor plans.
This is a port of upstream PR #15884 for
#15788. Remove this section when upstream merges
it. Code: packages/provider-cursor/src/server/sdk.ts.
Cursor gets every model option the picker shows, not only the ones the user changed. Cursor treats an omitted option as the model's standard tier, so upstream runs an untouched composer, a delegated task, a launched thread, or a scheduled task at about 300K while the picker shows 1M (500K for Grok 4.7). Fast is the exception: it stays off unless chosen, as in the composer. With the long-context change above, these defaults run in Max Mode.
delegate_task on the parent's own provider and model keeps the parent's options and overrides
only the ones it names, for every provider. Upstream replaces them, so an effort-only child loses
the parent's context window.
Fixes upstream #16149; remove this section when
upstream fixes it. Code: withCursorDefaultParameters in
packages/provider-cursor/src/server/sdkModel.ts, applied in
packages/provider-cursor/src/server/driver.ts, and resolveTarget in
apps/server/src/mcp/OrchestratorMcpService.ts.
A follow-up sent into a running Cursor turn is injected into that turn. Delegated task completions wake the parent the same way, while the turn is still going. Upstream interrupts the turn and starts it again for a follow-up, and holds a delegated completion until the turn ends. Queuing a message still waits, and an explicit restart still stops the turn and starts a new one. When Cursor does not accept the injection, a finished turn receives the message as its next turn.
Code: steerTurn in
adapter.ts and
Run.steer in
CursorAgentSdk.ts.
Upstream makes every subagent a child thread, lists a thread's subagents under Lineage in the
thread details panel, one row each, newest first, and removed the right-panel Agents surface
(its store migration dropped persisted agents tabs). The fork keeps Lineage and brings the
Agents panel back beside it, both over the child threads:
- Agents panel. A right-panel surface with the thread's whole fleet: one line per agent with a
status dot, the provider's icon (
AgentFleetEntry.providerInstanceId), token total, elapsed time, and start time, a second line with a working agent's latest tool call (a static…while it runs, awaitingbadge when the agent waits on the user), a finished agent's first result line (subagentResultSummaryLine) or a failed agent's error (the toolbar's details toggle hides these second lines, remembered per device), and hover previews (status, provider, compact model with reasoning effort andrun N, prompt, result or error, the latest five tool calls, usage). Clicking a tool call in a preview opens the agent on that call, expanded and scrolled into view (agentDrillStore.tsfocusToolCall). Agents spawned by an agent sit indented under it, found through child-thread lineage in the thread shells (deriveThreadAgentFleet); a filter keeps a non-matching agent as context when one below it matches. An agent recorded before its child thread exists still opens (its prompt, result, and usage, without activity) and has the right-click menu. Spawn order is the child thread's creation, so a resumed agent never moves (subagentSpawnedAt). The footer counts agents by status and sums the usage they reported (input, cached share, output, reasoning, tool calls;summarizeAgentFleet). Agents spawned by agents add no usage there: their records live on their owners' projections, which the list does not subscribe to. Open the panel from the right panel's launcher or + menu (badged with working agents, nested agents and live follow-up runs included), the command palette (Show agents), the Lineage header, an agent tab, Details or the bot button on an agent row in the conversation, or Show in Agents panel in an agent's right-click menu. Clicking a row or its preview opens the agent's detail. The panel links back to Lineage. Upstream's v14 right-panel migration droppedagents; v15 keeps it, so the surface is restored on restart again. Rows page in 50 at a time, and only working rows on screen and open previews read a child thread. - Lineage. With two or more subagents, Lineage gets the same search, status filter, and sorting
by spawn order (the default, so rows never jump while they work), status, tokens, or duration,
and the same compact rows. Lineage and the panel share one filter and sort, kept per device
(
agentListViewStore.ts), so switching between them shows the same list. - Conversation rows. Upstream's agent rows open the agent's thread. The fork adds Details,
which opens the Agents panel on that agent, and a bot button that opens the fleet
(
V2LifecycleRow.tsx). - Record fields.
OrchestrationV2Subagentgains optional fork fields kept in the record's payload JSON (no migration):usage(for a Claude subagent, the input, cached, and output tokens summed over its own calls, since the SDK'stotal_tokensis only the latest call, with the SDK's tool calls and time added up across resumed runs; the running token total of a Codex child thread; and, for app-owned tasks such asdelegate_taskchildren on any provider, the sum of their own thread's provider-turn usage plus its tool calls, written when the task finishes;subagentUsageFromChildTurnsinsubagentProjection.ts),outputFile(Claude's task output file), andsessionUrl(the http(s) remote session link a Claude Workflow tool result names for its task). Every agent view (detail footer, rows, hover cards, fleet footer) reads this one field. A running delegated task shows usage once it finishes, and tasks that finished before this existed show none. Other native subagents leave these empty. - Agent detail. Clicking an agent in the Agents panel inspects it in place, with Back to
the fleet. The header has the agent's status, compact model with effort (from its child thread's
model selection),
run Npast its first run, elapsed time, the prompt clamped to four lines with Show all, the result or error, Artifacts (output file, Open remote session), and agents it started (click one to drill a level further; Back returns one level). Below it is the agent's activity from its child thread: its Tools (the default), with status and kind filters and sorting, or a live Transcript of its messages, reasoning summaries, tool calls, and notices in order, each with its time. Both can be searched, and a narrowed view says Showing N of M. Paths in both read relative to the checkout the agent works in: a Claude.claude/worktrees/agent-<id>worktree or a sibling checkout its calls use (subagentWorkspaceRoot). An empty Tools view says whether the agent made no calls or its provider records none, showing progress while it works (subagentEmptyToolCallsText). The usage breakdown adds Runs and Attempt from the child thread's runs and run attempts (subagentRunStats). The transcript reuses the chat's timeline derivation (deriveTimelineEntriesFromVisibleTurnItemsWithState), work-log rows, andV2ItemInspectorfor expanded calls (output, diffs). It is virtualized and follows new activity only while scrolled to the end. Stop agent appears when the agent's own thread has an interruptible run. It is the same interrupt that thread offers in chat; native Claude subagents have none. The drill-in is per thread and session-only (agentDrillStore.ts). Show in Agents panel opens the panel on the agent. The child thread is read only while its detail is shown. - Agent tab. Open in new tab in the detail view, or right-click an agent in the panel, in Lineage, or in the conversation, keeps it in a thread-scoped right-panel tab beside the fleet, with the same detail view. Agent tabs close like other tabs, reopen the same way, and are restored when the app restarts.
- Attach to chat (right-click or the agent detail) puts the agent's
@handlechip in the composer; the chip carries its task and result (see Attach agents to chat by handle). - Continue in chat (right-click or the agent detail) opens a new chat tab of the thread whose
draft carries the agent's task, result or error, and latest tool calls as a chat-summary chip;
subagentContinuationContextbuilds the text. It starts a fresh conversation rather than resuming the agent's provider session.
Tool previews include bounded unified edit diffs and line counts, read ranges, search arguments,
and exit codes when the provider supplies them, in the agent detail and in chat tool expansions on
web, desktop, and mobile. Timelines carry an edit without its diff, so an expanded edit or its hover card fetches the
stored item for its preview (fileChangePreviewText); the fork's projectTurnItemForDetail returns
an edit's stored diff, bounded, where upstream withholds it there too. Claude's Edit results are only a
success message, so the fork builds a Claude edit's diff and line counts from the tool's input
(claudeFileChangeDiff), where upstream stores the message as the diff. Codex sends a new or deleted
file as raw contents and an update as bare hunks; the fork stores every change in the item as one
unified patch with line counts (codexFileChangeDiff), where upstream kept the first change's raw
text. A file read shows the file itself, syntax-highlighted on web and desktop and numbered from
the line the read started at, instead of the JSON, wrapper tags or line-number prefixes each
provider reports it in (turnItemReadFile, ReadFileView.tsx);
the agent views fetch a read's stored output when its call is hovered or expanded. In the agent views a tool's preview also
carries what it reported back, such as an Error: line, when its output came with the timeline;
v2 command items record no working directory, so none is shown. On web and desktop, collapsed tool calls in
the main chat also preview on hover; clicking still expands them inline. The card shows the tool
heading, the workspace-relative syntax-highlighted command or full label, then the same details the
row expands to (output loads only once the card opens, and a non-zero exit code shows with it), with
time and status in a compact footer. Syntax highlighting for commands and tool arguments is
limited to hover previews; expanded chat rows show plain text. Thoughts and answered questions do not preview
(MessagesTimeline.tsx,
toolCallPreview.ts).
A tool call's expanded row, and its hover card on web and desktop, show common tools as what
they did rather than their arguments and JSON result, with the raw call one click away on web: T3
thread and task tools as the thread's title, status, model and latest items, or as what the call
did to the thread (interrupted, forked, renamed, reconfigured, organized), with a link to the
thread; thread search as matches by thread title; pull request tools as PR rows, with the
title, state and branch from the thread's linked-PR snapshot when the result omits them; scheduled
task tools as each task's title and schedule; browser preview actions as their target, page, and
evaluated code and value; Slack thread reads and searches as messages; and Claude's ToolSearch,
AskUserQuestion, SendMessage, Skill, Monitor and ScheduleWakeup and HTML pages by what they carry
(resolveToolPreview, ToolPreviewCard.tsx,
mobile). Third-party MCP tools get
cards too: Notion page edits from what the call changed, fetched pages and queries; Datadog logs,
aggregates and monitors with a link to Datadog; Calendar events and calendars; Slack sends, drafts,
channel reads and searches; Granola meetings; Linear issues and lists; Drive files; the T3 history
server's threads, turns, messages, activities, plans and usage; Forge questionnaires, projects,
orders, respondents, quotas and weights; PostHog SQL and feature flags; LangSmith runs; Gmail
threads; and Codex's GitHub app. T3's own utility tools (capabilities, worktrees, queues, projects,
devices, environment) have cards as well. Any other server's JSON result shows as records or
properties picked from its title, status, time and link fields
(integrationToolPreview.ts).
On web, results no card describes show as a collapsible JSON tree with long IDs shortened, and
markdown results rendered. On every
client, server metadata such as an MCP _meta block or a browser row's toolIcon is left out of a
tool's output, and a Cursor MCP call shows its own arguments rather than Cursor's envelope.
Code: agentListView.ts,
agentFleet.ts,
AgentsPanel.tsx,
ThreadRelationshipsControl.tsx,
AgentDetailPanel.tsx,
agentTranscript.ts,
agentDrillStore.ts,
agentChatActions.ts, the agents and
agent surfaces in rightPanelStore.ts, the record
field mapping in ClaudeAdapterV2.ts and CodexAdapterV2.ts, and finalizeAppOwnedSubagent in
Orchestrator.ts. User guide:
thread-sidebar.md.
Every subagent has an @handle: its title as a slug, numbered -2, -3 in spawn order when
agents started by the same thread slug alike
(subagentHandles.ts). Every client
derives it from thread shells, so nothing is stored. Agents panel rows, the agent detail header,
conversation agent rows, agent hover cards (Lineage's included) and the Subagent of divider on
an agent's own thread show it.
- Composer. When the thread has agents, the
@menu adds an Agents tab next to Files and Chats. A query that starts an agent's handle opens on Agents. Rows show status, provider, title, handle, model, tokens, elapsed time and a working agent's latest tool call, and hover to preview the agent like an Agents panel row. - Chip. Picking an agent inserts a
subagentcontext chip (SubagentContextRecord) that shows the handle and the agent's live status and previews the agent on hover. What the provider receives depends on how the agent can be reached: a T3 delegated task carriestask_statusandt3_thread_sendinstructions, a Claude subagent itsSendMessageagent id, and other native subagents are marked as unable to take messages. Each also carries its task and its outcome at the time it was attached (formatSubagentPayloadincomposerContextReferences.ts). An agent whose record the client has not loaded (an older run's) is attached by its child thread alone. Chip payloads live in memory like issue chips, so a reloaded draft shows them as unavailable. - Other ways in. Attach to chat and Copy @handle in the agent right-click menu,
Alt-click on an Agents panel row, the
@button on the detail header and on conversation agent rows, and Attach to parent chat on an agent's own thread, which puts the chip in the parent's draft and opens the parent. On mobile, the Agents sheet rows show the handle and offer the same actions on long-press. The mobile composer's@menu has the same tabs.
In chat Markdown (messages, plans, agent results), clicking an inline `code` span copies its
text and marks it copied for a moment. A click that ends a text selection does not copy, and inline
code that is a file path still opens the file (CopyableInlineCode in
ChatMarkdown.tsx).
A thread can have several chat tabs that share one workspace (same checkout and worktree). Each tab is its own conversation and provider. A group can have an optional name, set from the chat-tab menu with Name thread group. Leave it blank to use the most recently opened tab’s title. Group names appear in the sidebar and match sidebar search; tab titles remain independently editable. Names are stored by the ThreadTabs service.
- Storage. Every tab is a normal thread. Tab membership lives in the fork-owned
fork_thread_tabstable, created byensureThreadTabsSchemainapps/server/src/threadTabs/schema.ts. It is versioned in a separatefork_schema_migrationstable so upstream's numbered migrations are never touched. - Server. The
ThreadTabsservice (apps/server/src/threadTabs/ThreadTabs.ts) lists a group and all memberships, creates a tab, forks a response into a tab, and summarizes other chats;http.tsserves it as thethreadTabsHTTP group. A new tab is an orchestrationthread.createon the source's branch and worktree. Each tab keeps its own checkpoints in the shared worktree. Orchestration v2 does not follow a worktree's checked-out branch for any thread, so tabs keep the branch they were created with. - Settlement. The group shows as one sidebar row, so it settles as a unit while upstream's
settle commands and auto-settle policy stay per thread.
settlement.tsfollows the stored orchestration events and mirrors them: a new run or unsettle in any tab wakes the group, and a settle in any tab settles the rest. A settle is undone while another tab is working, waiting on you, or holding background work, or, for an automatic settle, while another tab has an open pull request. Hiding works the same way:hiding.tsmirrors a hide or unhide across the group's live tabs, so the row leaves the list whichever tab the menu was opened on. Snoozing any tab parks all live tabs until the same wake time; waking a tab (including Undo or sending a message), pinning it, or a fresh completion, failure, or pending request returns the group. A sibling waiting on you or holding a queued turn prevents group snooze (apps/server/src/threadTabs/settlement.ts). - Sidebars. By default child tabs are hidden from the web sidebar, the legacy project sidebar,
and both mobile thread lists (
useHiddenTabThreads). The group's row stays highlighted while any of its tabs is open. Opening it from another thread returns to the tab last left open; clicking it while one of its tabs is already open leaves that tab selected (threadTabGroupHeaderTargetinpackages/client-runtime/src/threadTabs.ts,apps/web/src/threadTabRecencyStore.ts). With tabs hidden on web and desktop, the row shows that tab’s title, model, and details (apps/web/src/components/Sidebar.tsx). On web and desktop, Settings → General → Tabs in sidebar, the sidebar header's tabs button (click to toggle, right-click for a menu;sidebar/SidebarTabsMenu.tsx), the command palette, andCmd+Option+Ton macOS orCtrl+Alt+Ton Windows and Linux list each tab under that row: all of them, or up to a chosen number with the rest behind a more row that keeps the open tab listed. Hovering the collapsed row lists those tabs, and clicking one opens it. Sort tabs by orders them by latest response (a tab with no response counts from its creation), creation, or last opened, either way, or manually by dragging, which switches to Manual. Manual order is client-local, because the server's tab positions also pick the group's row. The row's tab count opens or folds just that group; those choices stay in the browser and reset when the setting changes. While folded, hovering the count lists the group's tabs in that order, and clicking one opens it. The row's hover actions add a + that opens a new tab, and each listed tab has a hover × that closes it. - Menus. The thread right-click menu in both sidebars and the header's thread menu offer
New tab. The sidebar's menu also offers Close tab on a thread that has a sibling tab.
Closing archives the tab's thread and lands on its neighbour (
useThreadTabActionsinapps/web/src/components/chat/ThreadTabs.tsx). Upstream restarts an agent session only from the command palette, for the open chat; the tab crumb's menu and both sidebars' right-click menus add Restart agent session for a tab (a row standing for a tab group restarts the tab it opens). The header's thread menu leaves it out because it acts on the group's original thread (apps/web/src/hooks/useRestartAgentSession.ts). - Web header. The breadcrumb reads
project / thread / tab. The thread crumb keeps the thread action menu and acts on the original thread. The tab crumb switches, creates, and closes tabs. A chat with one tab shows a New tab button instead of repeating the title. A tab can also be closed from the hover control on its menu row (apps/web/src/components/chat/ThreadTabs.tsx,ChatHeader.tsx). - Right panel. Each tab keeps its own right-panel surfaces, but the panel stays open or closed
as you move between tabs, including new, forked, and closed-into tabs
(
useRightPanelFollowsTabSwitchinThreadTabs.tsx). - Context from other chats. Type
@in the composer and pick a sibling tab from the Chats tab, which a query with chat matches but no file matches opens on, or, before the first message, pick one under Include context from (hovering one previews its summary). Those pills follow the sidebar's tab order and limit and show each tab's provider, status, and time, with the rest behind a more pill that lists them on hover (apps/web/src/components/sidebar/SidebarTabSummary.tsx). A pill's menu attaches the summary or continues the conversation: a native fork from the sibling's latest finished response, resolved by the server when the tab fork omitsrunId, replaces the empty tab and takes its draft (onContinueFromTabinChatView.tsx,continueFrominapps/mobile/src/features/threads/ThreadTabs.tsx). The Chats tab also lists other unarchived threads in the environment, newest first and matched by title (apps/web/src/components/chat/composerThreadReferences.ts), and works in a new draft thread too. On web and desktop, Attach → Thread and Attach thread in the command palette open a searchable picker of those threads. A thread's tabs sit together behind a side rule, and searching a thread title finds its tabs too. Rows show the project, provider, status, and last activity as the sidebar does. It has project and provider filters and sorting by updated time, creation time, or title. Hovering a row previews how the chat started, the latest exchange, and changed files; attaching reuses that snapshot (ThreadAttachPicker.tsx). These paths insert athread-tabcontext chip at the caret, which can be moved like any other chip. The kind lives inpackages/contracts/src/composerContext.tsand is formatted for providers inpackages/shared/src/composerContextReferences.ts. The summary covers the recent conversation, tools, reasoning, errors, changed files, and the latest plan (apps/server/src/threadTabs/summary.ts). It is captured when chosen (or when first previewed), so later changes in that chat do not change it. - Forking. On web and desktop, a started chat can fork into a new tab. Every fork uses
upstream's native
thread.forkfrom a completed response (forkResponseIntoTabinThreadTabs.tsx, theforkendpoint), so the tab carries the conversation itself. The fork point comes fromlatestThreadForkPointandthreadForkPointBeforeMessageinpackages/client-runtime/src/threadTabs.ts. When there is no finished response to fork from, the tab falls back to athread-tabsummary chip in its draft (forkThreadTab).- A user message's hover actions include Fork into new tab. The fork stops at the response before that message, and the message's text and attachments wait in the new tab's composer.
- A completed agent response's Fork from this response forks at that response. Against a server without tabs it forks a separate thread, as upstream does.
- Each model picker row has a hover fork button. The new tab forks at the latest finished
response and runs that model: the fork request's
modelSelectionswitches it before its first message, so upstream's fork hands the conversation to the new provider on that send. The current draft (text, attachments, and context chips) is copied into it. Other providers, and models the provider cannot switch to mid-chat, stay listed instead of being hidden. Their rows show a not-allowed cursor and a tooltip, and only the fork button opens them, so a stray click never forks (matchesModelPickerLockinModelPickerContent.tsx). - The account picker forks the same way when the chosen account cannot take over the tab.
- Agents. Over MCP,
t3_thread_tabslists a thread's group andt3_thread_tab_openopens a tab in it: empty, or a native fork of a tab's latest (or chosen) response, on any model, with an optional first message. One service call,ThreadTabs.open, does what New tab or a model-picker fork does, then sends the message. The tab inherits its group's modes, so its group (and fork source) must sit within the caller's, and it recordsstartedByand counts toward the caller's spawn limits like a launched thread (toolkits/thread/handlers.ts). - Mobile. A switcher menu switches, creates, and closes tabs, and restarts the open tab's
agent session (
apps/mobile/src/features/threads/ThreadTabs.tsx). An empty tab can attach sibling context when sending. A started chat's Hand off menu forks it into a new tab on any provider's model, the same way as the web model picker's fork button. Mobile does not have the header crumb, the@chip, forking from a message, or the sidebar tab list.
User guide: thread-sidebar.md.
On web and desktop, two chats can be shown side by side. Each pane is a full ChatView with its
own timeline, composer, right panel, and terminal.
- State.
apps/web/src/splitViewStore.tsholds the pair of thread keys, in memory only. The split shows while the routed thread is one of the pair, and the routed thread is the focused pane, so the header, sidebar, command palette, and global shortcuts follow focus unchanged. Focusing a pane replaces the route. Panes are keyed by thread, so moving focus never remounts a timeline. - Focus. Window-level shortcuts, paste-to-composer, digit answers, right-panel launcher
letters, and composer focus grabs skip the unfocused pane (
useSplitPaneFocusinapps/web/src/components/chat/splitPane.ts). Each pane uses CSS layout containment so its fixed title-bar controls stay inside it (SplitChatPanes.tsx). - Entry points. The tab crumb menu's per-tab split button, Open in split view / Close
split view in both sidebars' thread menus, the command palette,
splitView.toggle(mod+\), andsplitView.focusOther(mod+alt+\). Each pane header has a close button. In a sidebar tab list, dragging a tab onto the near half of another and holding it there for half a second pairs them, iOS home-screen style: the list holds still while aiming, the target row lights up, and releasing opens the split (resolveSidebarTabPairTargetinSidebar.logic.ts). - Swap and resize. Dragging the handle at the top of a pane onto the other pane swaps their
sides, and each keeps its width. Panes keep a fixed DOM order and are placed with CSS
order, so a swap never moves a scrolled timeline. Dragging the divider resizes the panes (at least 320px each); double-clicking it evens them out. The width ratio lives in the split store and resets when the split closes. - Sidebar. Both chats' rows carry a side-by-side icon, and the one beside the routed chat gets
a lighter version of the active highlight. A tab list's more fold keeps both listed
(
limitSidebarTabsinSidebar.logic.ts). The legacy project sidebar does not mark the split. - Mobile. Not supported. Windows at or below the right-panel sheet breakpoint also show one chat.
User guide: thread-sidebar.md.
On web and desktop, Settings → General → Open pull requests in chooses where the thread's own pull request opens: the thread details panel's pull request rows, the composer's pull request badge, and the View PR button on the toast after a pull request is created. Side panel (the default) is upstream's behaviour, where Cmd/Ctrl-click opens the browser. Browser swaps them: a click opens the browser that Open links in picks (see below), and Cmd/Ctrl-click opens the side panel. Other pull request links are unaffected.
Code: usePreferredOpenPrLink in
openPullRequestLink.ts,
BranchToolbarBranchSelector.tsx, and
pullRequestOpenTarget in packages/contracts/src/settings.ts.
Upstream's pull request panel always sends its links to the system browser. In the fork, those links follow Settings → Integrations → Browser → Open links in when the panel is next to a thread. That covers "Open on GitHub", the PR number, the repository name, author profiles, comment timestamps, activity links, attachments, and the "Open on GitHub" button on the load-error screen. Cmd/Ctrl-click still opens the system browser. The pull requests page has no thread, so its links still open in the system browser.
Code: useLinkClickHandler in apps/web/src/browser/useOpenLink.ts and
apps/web/src/components/pullRequest/PullRequestMarkdownContext.ts.
Automatic worktree cleanup still refuses to delete a checkout that has uncommitted work.
Regenerable caches such as node_modules, virtualenvs, and __pycache__ no longer count as that
work. Settings → Storage → Ignored names that do not block cleanup adds more file and directory
names, applied to every project on that machine. Suggest from projects asks the selected model,
or the environment's text-generation model when that control is off, to propose ignored directories
that appear in more than one project. Credential paths such as .env and .ssh stay blocking.
Code: apps/server/src/storageCleanup.ts and
apps/web/src/components/settings/StorageSettings.tsx. User guide:
project-settings.md.
New worktree branches use upstream's naming (Settings → Source Control → Worktree branch
naming), but the name is generated while the worktree is checked out, so the setup script and
agent start on the final branch; upstream renames the t3/<id> placeholder after the first
turn has started. The placeholder remains only when naming outlasts checkout by more than a few
seconds, and is then renamed in the background as upstream does. A static prefix the model repeats
in its answer is not doubled. The fork's earlier Branch prefix setting (global and per project)
moves into the static prefix once, the first time the server loads its settings.
Code: prepareInBackground in apps/server/src/orchestration-v2/ThreadLaunchService.ts,
formatGeneratedBranchName in packages/shared/src/git.ts, and foldLegacyWorktreeBranchPrefix
in apps/server/src/serverSettings.ts. User guide:
project-settings.md.
The sidebar titlebar shows how much memory T3 uses on the primary environment's machine. That covers the server, provider and terminal processes, and on desktop the Electron processes. Clicking the pill opens a process tree with per-process CPU and memory, plus a link to Settings > Diagnostics. The pill is hidden when the resource monitor has no sample and when the sidebar is narrow. This is web and desktop.
The pill polls server.getResourceUsage, which reads the monitor's background history, so an
always-visible pill does not raise the sampling rate. The popover holds the live
resource-telemetry stream only while it is open.
Code: apps/web/src/components/sidebar/SidebarResourcePill.tsx and readUsage in
apps/server/src/resourceTelemetry/ResourceTelemetry.ts.
The sidebar titlebar has back and forward buttons after the resource pill. They step through the
app's navigation history, the same as the navigation.back and navigation.forward shortcuts
(Mod+[ and Mod+] by default). This is web and desktop. The mobile app uses its own navigation.
Code: SidebarHistoryNavigation in apps/web/src/components/sidebar/SidebarChrome.tsx.
On web and desktop, Settings → General → Thread views chooses detailed cards or compact rows independently for active, pinned, working, grouped, hidden, snoozed, and settled threads. Hidden threads default to detailed cards; snoozed and settled threads default to compact rows. These client preferences also apply when several categories share the sidebar and when chat tabs are expanded.
Code: Sidebar.tsx,
SettingsPanels.tsx, and
settings.ts. User guide:
thread-sidebar.md.
On web and desktop, one filter button in the sidebar header replaces upstream's project picker.
Each row opens a multi-select submenu, like the pull request filters: Show picks which of
Threads, your thread groups, Snoozed, Hidden, and Settled the list holds; Organisations scopes
it to repository owners, read per checkout from repositoryIdentity (a fork counts under its own
remote); and Projects scopes it to some projects. Only, on the highlighted row, picks just that one. Threads alone is the
default; other picks list as titled sections below live threads, so snoozed and settled threads no longer sit in shelves under
live work, and the drag targets for settling and waking are gone. On web and desktop, clicking a
section's title collapses it to a count, remembered per client; the open thread stays listed under a
collapsed title. Dragging to pin or reorder
works while only Threads is shown. The button shows a count of narrowings off their default,
and hovering it lists the selected pages, organisations, and projects. Under the header, each
of those selections shows as a pill with an × that drops it, plus Clear all once there are
several. Settings → Organisations gives an organisation a display name and icon, saved as the
shared organisations server setting so every environment and mobile's filter use them. A thread's Filter by
project menu item still narrows the list to that one project. Mobile's
thread list filter menu has the same Show submenu.
Code: SidebarFilterMenu.tsx,
OrganisationsSettings.tsx,
sidebarPages in Sidebar.tsx,
sidebarProjectScopeKeys in uiStateStore.ts, and mobile's
home-list-filter-menu.ts. User
guide: thread-sidebar.md.
Web and desktop always show Projects in the settings sidebar, including when no project is selected. Open it and choose a project to manage its name, icon, checkouts and actions.
Code: SettingsSidebarNav.tsx and
ProjectsSettings.tsx.
Diagnostics has its own entry in the settings sidebar, between Connections and Archive. Upstream only reaches it from the View diagnostics button in General. Failed-turn callouts also offer View diagnostics, selecting the thread's environment so its process information, recent logs, and Open logs folder action are reachable from the error. This is web and desktop; mobile's Diagnostics screen reports native app startup crashes rather than environment logs.
Code: SETTINGS_SECTION_LABELS in apps/web/src/components/settings/settingsSearch.ts and
SETTINGS_SECTION_ICONS in apps/web/src/components/settings/SettingsSidebarNav.tsx, and
WorkEntryLogRow in MessagesTimeline.tsx.
New worktrees honor a repository's Conductor (conductor.build) settings from the project
checkout: gitignored Files to copy (.worktreeinclude, file_include_globs, default .env*),
scripts.setup as the setup script when the project has no setup action of its own, and
scripts.archive before a worktree is removed, by the user or by storage cleanup. Scripts get
Conductor's CONDUCTOR_* variables and environment_variables; CONDUCTOR_PORT is derived from
the worktree path. This runs on the server, so every client gets it.
Run scripts (scripts.run) show in the chat header's actions menu on web and desktop, where the
default one is the header's Run button when the project has no actions of its own, and in the
thread's terminal menu on mobile. Each runs in its own terminal and toggles to Stop while running;
run_mode = "nonconcurrent" applies within a thread. The project's Conductor settings section
(web, desktop, and mobile's project overview) edits the setup and archive scripts, Files to copy,
and environment variables in settings.local.toml or settings.toml.
Code: packages/shared/src/conductorSettings.ts and conductorSettingsEditor.ts,
packages/client-runtime/src/conductorRunScripts.ts, apps/server/src/project/ConductorWorkspace.ts
(called from ProjectSetupScriptRunner.ts, GitWorkflowService.removeWorktree, and
storageCleanup.ts), apps/web/src/components/settings/ConductorSettings.tsx, the run button in
apps/web/src/components/ProjectScriptsControl.tsx, and on mobile
apps/mobile/src/features/settings/components/SettingsConductorSection.tsx and the terminal menu in
apps/mobile/src/features/threads/ThreadGitControls.tsx. User guide:
project-settings.md.
When a Claude subagent starts in a worktree of its own (isolation: "worktree"), the server
prepares that worktree the way it prepares a thread's: Files to copy, then the project's setup
script (the T3 setup action or Conductor's scripts.setup) with the Conductor variables and
environment_variables, in a subagent-setup-<agent> terminal on the parent thread. A non-async
script holds the subagent until it exits. Claude Code still creates and removes the worktree itself,
so no archive script runs. Only the Claude adapter does this.
Code: makeSubagentWorktreeSetupHooks in
ClaudeAdapterV2.ts and
SubagentWorktreeSetup.ts. User guide:
project-settings.md.
Settings > Integrations > Linear connects a Linear account to the environment with OAuth
(PKCE, read-only scope, no client secret). The server holds the credential and refreshes it, so
every client of that environment can use it. The browser's redirect lands on a loopback listener
on fixed port 47831, which must match the redirect URI registered on the fork's Linear OAuth app.
When the browser is on another device, the user pastes the redirect URL back instead.
T3CODE_LINEAR_CLIENT_ID points a fork at its own Linear app.
Issues attach to messages as a linear-issue context chip: from the composer's attach menu (which
now asks between files, a Linear issue, and a pull request), the # menu beside pull requests, the command palette,
and the mobile attach menu. The web picker filters by assignee, team, project, milestone, status,
priority, and label, and sorts by the fields Linear's API offers. The server renders the issue to
capped markdown when it is attached, and that snapshot is inlined into the prompt for every
provider. An Open Linear links in setting can send issue links to the Linear desktop app
(linear://), which the desktop shell's external-URL allowlist permits.
A thread's chat-tab group can also be linked to one issue, from the thread menu or the command
palette, and starting a thread from an issue links it. The link lives in the fork-owned
fork_linear_thread_links table, keyed by tab group, and streams to clients over
linear.subscribeThreadLinks. Only the issue's identity is stored; the chat header chip reads its
status live, and offers open, change, and unlink. Mobile shows the same chip beside the tab
switcher (apps/mobile/src/features/threads/ThreadLinearLink.tsx). Agents link and unlink it
through the thread MCP's link_linear_issue / unlink_linear_issue, and read the thread's Linear
and GitHub issue links with list_thread_issues
(toolkits/issueLinks/). Like the pull request
tools, they default to the caller's thread, need the pull-requests MCP capability, and write
through the same thread access checks.
linearAssignmentTriggers in server settings (off when empty) holds rules that start a thread
when an issue is newly assigned to the connected account. The server polls the read-only API
every 2 minutes, since a webhook would need a public address. It launches like a scheduled task
(new worktree, rule's model and prompt) with the issue attached as context and linked to the
thread. Dedupe lives in fork-owned tables: an issue row means the issue never starts another
thread, and the command id is derived from the issue so a retry returns the same thread. A rule's
baseline is keyed by its id, filters, and the Linear account, so adding a rule, editing its
filters, or switching accounts records the issues already assigned instead of starting them.
Changing the rules needs the orchestration:operate scope. Mobile lists and removes rules but
cannot add them. Code: LinearAssignmentTriggers.ts,
LinearAssignmentTriggerSettings.tsx.
The attach menu's pull request option, also in the web command palette, opens a searchable picker
(PullRequestAttachPicker.tsx) and
inserts the same context chip as picking the pull request from the # menu. It is not a thread
link. Pull request chips include the repository and number, such as owner/repository#271
(web/desktop labels,
mobile labels). Right-clicking a row offers
Attach a comment…, which lists that pull request's comments
(Backspace on an empty search goes back) and attaches the picked one as the same chip a pasted
comment link makes.
Code: apps/server/src/linear/, packages/contracts/src/linear.ts, LinearIssueContextRecord in
packages/contracts/src/composerContext.ts, packages/client-runtime/src/state/linear.ts,
apps/web/src/components/settings/LinearSettings.tsx,
apps/web/src/components/chat/LinearIssuePicker.tsx,
apps/web/src/components/chat/ComposerAttachMenu.tsx,
apps/web/src/components/chat/useComposerLinearIssueItems.ts,
apps/web/src/components/chat/LinearIssueFilters.tsx,
apps/web/src/components/chat/LinearThreadLink.tsx,
apps/server/src/linear/LinearThreadLinks.ts,
apps/mobile/src/components/LinearIssuePickerSheet.tsx, and
apps/mobile/src/features/settings/SettingsLinearRouteScreen.tsx. User guide:
linear.md.
Linear, Slack, and Notion sign-in can return directly to the environment from another device using an explicitly registered HTTPS URL at /oauth/<integration>/callback and the corresponding T3CODE_<INTEGRATION>_REDIRECT_URI server setting. Local sign-in and paste-back remain available by default. Each provider must allow the exact URL on the user's OAuth app; Linear requires a custom client ID for custom URLs. This works with a reachable HTTPS server or tunnel forwarding the callback, rather than assuming the client's origin is the environment. Callback state is checked against the active login, and cancelled or completed flows cannot be reused.
Code: redirect configuration, Linear auth, Slack auth, and Notion auth. Setup: Linear, Slack, and Notion.
Settings > Integrations > Slack connects a Slack account to the environment with OAuth (PKCE,
read-only user scopes, no client secret). There is no built-in Slack app: Slack limits apps that
are distributed without Marketplace approval to one conversations.replies call a minute, 15
messages each. Each workspace makes its own app from a manifest the settings section copies
(slackAppManifest in packages/contracts/src/slack.ts), and the user enters its client ID. The
server keeps that client ID across disconnects, and T3CODE_SLACK_CLIENT_ID can supply one. The
redirect is http://localhost:47832/callback, since Slack only treats localhost as a desktop
redirect by default, with the same paste-back path as Linear for remote browsers. A registered
HTTPS callback set through T3CODE_SLACK_REDIRECT_URI receives approval on the environment directly.
A message or its whole thread attaches as a slack-thread context chip: from a pasted or typed
permalink, the attach menu's picker (Slack search syntax, right-click for the message alone), a
# menu tab shown once Slack is connected, the command palette, and the mobile attach menu. The
server renders the thread to capped markdown when it is attached. That markdown always keeps the
first message and the linked one, fills the rest newest first, and resolves mentions to names.
The snapshot is inlined into the prompt for every provider.
A thread's chat-tab group can also be linked to one Slack thread, from the thread menu, the web
command palette, or the mobile tab menu, which open the same picker in link mode. Picking a reply links its whole thread.
The link lives in the fork-owned fork_slack_thread_links table, keyed by tab group, and streams
over slack.subscribeThreadLinks. Slack threads have no status, so the channel, author, and the
picked message's first line are copied when linked. The web chat header chip shows the channel and
offers open, change, and unlink; mobile shows the same chip beside the tab switcher. Linking and
unlinking are client-guarded RPCs that need the orchestration-operate grant. Unlinking stays
available with the integration turned off.
An off-by-default mention trigger in the same setting polls once a minute for explicit @mentions
of the connected account in selected channels, optionally including direct messages and requiring
a keyword. It starts threads in a selected project's root workspace with configured instructions,
a provider/model override or project defaults, the project's permission mode, and the same Slack
snapshot and link. Web and desktop configure it; all clients can use the resulting threads. It
requires both settings-write and orchestration-operate permissions. Older mentions are baselined
on enable or account change, processed mentions persist across restarts, and retried launches use
a stable command id. Polls read the latest 50 search results; Slack's visibility, indexing and rate
limits apply. Disabling the trigger or integration stops new launches. Code:
SlackMentionTrigger.ts.
A bare Slack message link left in any rendered message on web or desktop (an agent's reply, a
paste-as-text, a message sent from mobile) shows #channel · Author once slack.getLinkPreview
reads it, with the message's first line on hover. It shows its URL while Slack is off, disconnected,
or the read fails. The server keeps each preview as a file under the caches directory
(slack-link-previews), so a link is read from Slack once; it holds no reply count, since that
would go stale. Mobile shows the URL.
Turning off Enable Slack integration in that setting keeps pasted Slack links as links without setup prompts and hides Slack attachment actions on web, desktop, and mobile for the environment. The connected account and existing attachments are kept, so turning it back on needs no new login.
Code: apps/server/src/slack/, packages/contracts/src/slack.ts, SlackThreadContextRecord in
packages/contracts/src/composerContext.ts, packages/client-runtime/src/state/slack.ts,
apps/web/src/components/settings/SlackSettings.tsx,
apps/web/src/components/chat/SlackMessagePicker.tsx,
apps/web/src/components/chat/SlackThreadLink.tsx,
apps/web/src/components/chat/useComposerSlackItems.ts,
apps/mobile/src/components/SlackMessagePickerSheet.tsx, and
apps/mobile/src/features/threads/ThreadSlackNotionLinks.tsx. User guide: slack.md.
GitHub issues attach to messages as a github-issue context chip, the same way Linear issues do:
from the composer's attach menu, the # menu's GitHub issues tab, the web command palette, and
the mobile attach menu. They are offered for projects whose remote is on GitHub and read with the
environment's gh CLI, listing the repository the
project or worktree is checked out from; a number, #123, or issue URL is looked up directly. The
server renders the issue to capped markdown when it is attached: the description, then comments
newest first, dropping hidden (minimized), empty, "+1", and reaction-only comments and marking the
issue author's and maintainers' comments. That snapshot is inlined into the prompt for every
provider.
Starting a thread from a GitHub issue links it to the thread's chat-tab group, in the fork-owned
fork_github_issue_thread_links table, streamed over githubIssues.subscribeThreadLinks. A group
can hold one GitHub link beside its Linear link. The web chat header and the mobile tab switcher
show #123 with its live open or closed state, and offer open, change, and unlink. An existing
thread links or changes its issue from the thread menu (web sidebar and chat header), the web
command palette (which also unlinks), or the mobile tab menu and Link issue pill, through the
same picker in link mode, listing issues from the thread's own checkout. Pickers mark a linked
issue In use; the web attach picker's hover preview lists those threads and opens one on click.
Agents link and unlink it with link_github_issue / unlink_github_issue, beside the Linear tools
in toolkits/issueLinks/.
Code: apps/server/src/githubIssues/, packages/contracts/src/githubIssues.ts,
GitHubIssueContextRecord in packages/contracts/src/composerContext.ts,
packages/client-runtime/src/state/githubIssues.ts,
apps/web/src/components/chat/GitHubIssuePicker.tsx,
apps/web/src/components/chat/GitHubIssueThreadLink.tsx,
apps/mobile/src/components/GitHubIssuePickerSheet.tsx, and
apps/mobile/src/features/threads/ThreadGitHubIssueLink.tsx. User guide:
source-control.md.
On web and desktop, a new thread's composer has a ⋯ button in its top-right corner. It opens a
picker with PRs, Branches, Issues, and Linear tabs. The same picker opens from the command palette and from
chat.startFrom (mod+shift+b); both of those start a new thread first. The PRs tab filters and
sorts with the pull requests page's filter menu, the Issues tab lists the project's GitHub issues
by state, and the Linear tab uses the Linear attach picker's filter bar, whose view it shares.
- A pull request is checked out in a new worktree, or the worktree it is already checked out in, and the draft is pointed there. Upstream's checkout dialog, with its Local and Worktree choice, is skipped. A branch already checked out in the project's own checkout is refused.
- A branch is worked on where it is already checked out. Any other branch becomes the base of a new worktree.
- A GitHub or Linear issue is attached as a context chip and linked to the new thread (see GitHub issues and Linear integration).
A pull request or branch that a live thread is already on, or an issue linked to a live thread, is marked In use. Picking it asks whether to open that thread or start a second one. The default branch never counts as in use. Hovering a row previews it: a pull request's description, a branch's full name and where it would run (both listing threads already on it), or the issue snapshot an attached chip would carry.
On mobile, a new thread's ⋯ button offers Pull request, Linear issue, and GitHub issue, with the same checkout, linking, and In use rules; the existing branch picker covers branches. The pull request's worktree is checked out before the thread exists, so mobile does not run the project's setup script in it. The issue is linked once the draft sends.
Code: apps/web/src/components/chat/StartFromPicker.tsx, StartFromPicker.logic.ts and
StartFromPreviews.tsx, apps/mobile/src/features/threads/StartFromPullRequestSheet.tsx, the
startFrom targets in apps/mobile/src/components/LinearIssuePickerSheet.tsx and
GitHubIssuePickerSheet.tsx, the button
in ChatComposer.tsx, and chat.startFrom in packages/contracts/src/keybindings.ts. User guide:
source-control.md.
Link pull request, from the linked pull requests panel or the command palette, opens a
multi-select picker instead of a URL field. It lists the project's pull requests with the start-from
picker's filters, puts the thread's branch first, and pins the thread's current links at the top
with their boxes checked. Pasting a URL or #123 adds that pull request as a row. Nothing changes
until Link (or ⌘/Ctrl+Enter) applies every check and uncheck at once. An environment that holds
a single link keeps one row checked. See
LinkPullRequestDialog.tsx and
Linked pull requests.
Upstream's pull request watch (watch_pull_request, or the row menu) wakes the agent with news.
The fork adds to it:
- The wake tells the agent what to do about failing checks, a merge conflict, and requested changes (fix and push, rebase, address the review), with how to inspect the pull request. A "changes requested" review decision is news of its own, raised once until it clears.
- Wakes that ask for a fix spend a budget of 3 follow-ups, alongside upstream's limit on comment-only wakes. When a fourth is needed the watch pauses instead.
- Pause watching and Resume watching sit beside Stop watching. A paused watch reads nothing; resuming restores the budget and reports what changed meanwhile.
- Settled threads stay watched, and a wake brings the thread back; upstream ends a watch when its thread settles. Archiving the thread or pressing Stop still ends it.
- Each watched row on web shows a status line ("Waiting for checks", "Checks failed", "Changes requested", ...) and the follow-ups used. Mobile's Git overview shows "Watching" or "Watch paused".
The state rides on upstream's ThreadPullRequestWatch (changesRequested, followUps, paused)
and thread.pull-request.watch takes paused, behind the threadPullRequestWatchPause capability.
Watches from the fork's old fork_pull_request_watches table move onto their links at startup,
without their follow-up counts. Code:
pullRequestWatch.ts,
PullRequestWatchReactor.ts,
pullRequestWatch.ts (status line), and
ThreadPullRequestsPanel.tsx.
See the user guide.
On web and desktop, a new thread's empty composer shows Create worktree (or Create thread
in Local mode) in place of the send arrow; Enter does the same. It launches the thread without a
message: the thread opens right away with an empty composer and the setup card while the worktree
is checked out and the setup script runs, and no turn starts. Sending waits until that setup is
done, and the server also holds a first message that arrives earlier, from any client. The first
real message starts the turn, names the thread, and renames the temporary t3/<id> branch.
Chat tabs already share a workspace, so they never offer it. A selection of several models, or a
server without the deferredBootstrapTurn capability, keeps the plain send arrow. Mobile does not
offer it.
Code: bootstrap.deferTurn in packages/client-runtime/src/operations/commands.ts;
prepareMessageWorkspace and nameTemporaryBranch in
apps/server/src/orchestration-v2/ThreadLaunchService.ts, called from dispatchCommand in
apps/server/src/orchestration-v2/ThreadMessageIntake.ts; createThreadWithoutMessage in
apps/web/src/components/ChatView.tsx and the pill in ComposerPrimaryActions.tsx. User guide:
thread-sidebar.md.
On web and desktop, Export transcript… in a thread's menu (sidebar row, sidebar tab row, chat
header) or the command palette opens a dialog that previews the thread as Markdown. A thread with
several chat tabs gets a tab picker, starting on the tab it was opened from. Concise keeps
the prompts and replies; Full adds the work log (tool calls, commands, file edits) and proposed
plans in the order the thread's timeline shows them. Reasoning is always left out. Header adds front matter with the project,
branch, provider, model, and dates. Save… writes a .md file on the device running the client:
the native save dialog on desktop, the browser's save picker in Chromium, and a download elsewhere.
Copy puts the same Markdown on the clipboard. The dialog loads the whole thread projection over
HTTP (loadFullThreadSnapshot), so it works for threads that are not open. Mobile does not offer
it.
Code: threadTranscript.ts,
TranscriptExportDialog.tsx, and
saveTextFile in window.ts. User guide:
thread-sidebar.md.
Upstream imports recent Claude Code and Codex history only in bulk, from the welcome wizard. The
fork adds a picker on web and desktop: Import conversation… in the command palette, which
lists every project, and in the legacy sidebar's project menu, which starts on that project. Its
Filters menu narrows the list to one project or one source. Search matches conversation titles,
prompts, and full or partial Claude Code and Codex conversation IDs. agentSessions.list
returns the project's conversations from the last 30 days (newest 50, with first prompt, message
count, and dates, and truncated when there are more). Each is marked with the thread already
holding it, so the picker opens that thread instead: an earlier import, found by upstream's
import:<instance>:<session> thread id, or any unarchived thread whose active provider thread
resumes that session, including threads T3 started itself. agentSessions.import takes an
optional session to import just one through upstream's importer, from a fresh scan, and returns
its thread in threadIds. The thread binds to the original session exactly like the wizard's
import, so the next turn resumes it. Upstream matches a conversation only to the project whose
root is its exact working directory; the fork gives one started in a subfolder to the nearest
project at or above it, for both the picker and the wizard's bulk import. The server advertises
the picker with the
agentSessionPicker capability. Cursor, Grok, OpenCode, Antigravity, Devin, and mobile have no
import.
Code: listProjectAgentSessions in
AgentSessionImporter.ts, the
recentThreads options in AgentSessionScanner.ts,
and ImportConversationDialog.tsx.
User guide: thread-sidebar.md.
The import picker also lists the project's active Conductor (conductor.build) workspaces: those
whose Conductor repository has the project's root or origin remote, whose worktree still exists,
and that have at least one sent prompt. Importing one turns each open tab into a thread on the
workspace's branch and worktree, grouped as chat tabs, with the workspace's pin on
the first tab. The threads land active, as if un-settled, because the workspace is still in use.
History comes from Conductor's own database, read-only, so it matches what Conductor showed:
prompts and reply text, without tool activity. Images and files sent with a prompt are copied into
T3's attachment store from the workspace's .context/attachments; Conductor deletes some of those,
so a missing one is named in the message instead. Diff comments sent to the agent appear in their
prompt as quoted review comments. Claude Code and Codex tabs bind to their agent session and resume
it. Conductor keeps Cursor sessions in a private store, so a Cursor tab's history is handed to its
next turn instead. Thread ids are conductor:<session>, so the picker opens an earlier import.
Only macOS hosts have the database; mobile has no import.
Import all Conductor imports every listed active workspace. Include archived also imports
workspaces Conductor has archived (workspaces.state = 'archived'), even when the worktree
directory is gone. Those threads are settled at the workspace's last update, and they are not
pinned. The picker list itself stays limited to active workspaces; archived ids are returned beside
it for the bulk import. Running it again skips tabs that already have a thread, but rewrites their
imported messages where the importer now reads Conductor differently, matched by import message
id. Turns sent in T3 since the import are untouched, and Conductor prompts sent since are not added.
Some workspace state stays where it is. Notes (.context/notes.md, todos.md) are files in the
worktree the threads run in. Pull request review comments, which make up almost all of Conductor's
diff comments, come from the pull request T3 finds for the branch. Conductor encrypts terminal
scrollback, so terminal history is not imported, and neither are unsent diff comments.
Imported Claude sessions resume on their first turn because the provider thread records
nativeMetadata.importedNativeId; Claude rejects a new session under an id that already exists.
Code: ConductorImporter.ts,
conductorDatabase.ts, adopt in
ThreadTabs.ts, and
ImportConversationDialog.tsx.
User guide: thread-sidebar.md.
The composer's attach menu (and Attach repository in the web command palette) picks other
repositories to clone into the workspace's context folder, .context/ by default, the way the
ctxclone shell tool does. The picker lists the GitHub repositories (via gh repo list) of an
ordered list of owners, most recently attached first, then by owner order. A name more than one
owner has is labelled with its owner, like api (acme). The server keeps each owner's list in
memory and serves it at once, refetching in the background once it is five minutes old. Typing
org/ lists another owner, and a pasted
owner/repo or clone URL also works. Rows for repositories already cloned into the thread's
workspace show their branch, ahead/behind, and changed-file count.
On web and desktop the composer's # menu also has a Repositories tab over the same list:
#name searches the configured owners and #owner/name another one. #owner/name, and a
hyphenated name once an owner is configured, open that tab by default instead of pull requests.
A picked repository becomes a repository context chip. When the message sends, the server
clones what is missing before the turn starts. It leaves an existing clone of the same remote
alone (fetching only, so ahead/behind are current) and never overwrites a folder that belongs to
something else. On a new launch this runs as a step of the setup card, before the setup script,
and the outcomes reach the already recorded message as its run is released. Otherwise it runs
before the message is recorded, with a one-step setup card showing progress. Each record's clone
outcome and git status are written back onto the message. The chip shows them, and the agent's prompt includes them, so the
agent knows what is there. A failed clone is a warning and the agent still starts. The server adds
the folder to the repository's info/exclude so checkpoints and diffs ignore the clones. The
owners and the folder are server settings in Settings > General. Mobile's attach menu
has the same picker, without the recently attached ranking.
A project's Settings > Storage lists the clones in each of its checkouts' context folder, with their remote and git status, and removes one after a confirmation that names any unpushed commits or changed files it throws away. The server refuses a name that is not one folder, a folder that is not a git repository, one that resolves outside the context folder through a symlink, and any removal while a turn in that checkout is running or waiting on the user. A turn that starts during a removal, or a removal that meets a clone in progress, waits for the other to finish. Removal needs the source control write grant and is on web and desktop only; it is not an MCP tool, since thread agents cannot delete.
Code: apps/server/src/contextRepositories/ContextRepositories.ts,
packages/contracts/src/contextRepositories.ts, RepositoryContextRecord in
packages/contracts/src/composerContext.ts,
apps/server/src/contextRepositories/messageContextRepositories.ts (called from
apps/server/src/orchestration-v2/ThreadLaunchService.ts), the context of prepared-run.release
in packages/contracts/src/orchestrationV2.ts and its message restatement in
apps/server/src/orchestration-v2/Orchestrator.ts,
packages/client-runtime/src/contextRepositories.ts,
apps/web/src/components/chat/RepositoryAttachPicker.tsx,
apps/web/src/components/settings/ContextRepositoriesSettings.tsx,
apps/web/src/components/chat/useComposerRepositoryItems.ts, and
apps/mobile/src/components/RepositoryPickerSheet.tsx. User guide:
composer.md.
Pasting a Linear issue, GitHub issue, pull request, GitHub repository root, or Slack message link
into the web or desktop composer, or typing one followed by whitespace, turns it into the chip its attach picker
makes. A pull request link to one comment (#issuecomment-…, #discussion_r…, #r…) becomes a
chip for that comment instead, anchored to its line with the earlier replies when it is in a review
thread; a comment the read did not return falls back to the pull request chip. A link to lines of
one file (#diff-<hash>L4-L14) becomes a chip for those lines of the diff, and stays a link when
the diff does not show them. The text goes in as
usual, then each link becomes a chip once its object loads. A link that
cannot be read, or that was edited away meanwhile, stays as text. Paste-as-text (Cmd+Shift+V)
skips it, and typing never retries a link the draft already converted or tried, so undo and a
deleted chip stick. Mobile does not convert links.
A bare link of one of those kinds that is still a link when the message renders, in any message on
web, desktop, or mobile, shows its short name instead of the URL: owner/repo#162 for a pull
request or GitHub issue (with L4-L14 appended for a link to lines), ENG-123 for a Linear issue, and
owner/repo for a repository. It still opens, previews, and copies as the full URL. Link text the
writer chose is left alone. A bare Slack link is not one of these: it names the message only when
Slack is on and connected (see Slack messages and threads).
Code: packages/shared/src/composerObjectLinks.ts,
apps/web/src/components/chat/useResolveComposerObjectLink.ts, and convertObjectLinks in
apps/web/src/components/chat/ChatComposer.tsx. User guide:
composer.md.
On web and desktop, Cmd-click (Ctrl-click on Windows and Linux) a row in the composer's # menu
to check it instead of attaching it. While anything is checked, every row shows a checkbox and a
plain click toggles it too. Picks hold across the menu's tabs and searches, so pull requests, issues,
Notion pages, Slack messages, and repositories can go in together. Enter or Attach inserts
them all where the # was, and Esc drops the picks. Mobile has no # menu.
Code: attachReferenceItems in apps/web/src/components/chat/ChatComposer.tsx and
apps/web/src/components/chat/ComposerCommandMenu.tsx. User guide:
composer.md.
Pasting text that is only file paths, one per line, into the web or desktop composer turns each
path into the same file chip the @ picker makes. Absolute, ~/, Windows drive, and ./ or
../ paths count, as does a relative path whose last part has an extension; quoted paths and
shell-escaped spaces work. A paste with any other text in it, such as a sentence or a command that
mentions a path, stays as text, and so does a paste with Cmd+Shift+V. Mobile does not convert
paths.
Code: pastedFilePathsAsComposerFileLinks in packages/shared/src/composerTrigger.ts, called from
apps/web/src/components/composerInlineTokenPaste.ts.
A Makefile at the repository root drives a local auto-update test loop for the desktop app:
create a trusted self-signed certificate once, build signed mock-feed payloads, serve the feed, and
install the first build into /Applications. T3CODE_DESKTOP_IDENTITY makes
scripts/build-desktop-artifact.ts sign with any keychain identity, because the updater refuses to
install ad hoc–signed builds. Run make targets from the repository root, starting with
make update-cert.
Tool calls in the web, desktop, and mobile timelines, and subagent tool calls in the web and
desktop agent tabs and agents list, show paths inside the thread's working directory as relative.
For commands, a leading cd to that directory is dropped. Rewriting a
command stops at the first cd somewhere else, because relative paths after it would point
somewhere else. File, image, and other tool labels (Read: src/index.ts) get the same path
treatment. A subagent working in a sibling checkout of that directory — another folder next to
the thread's worktree, which is where Cursor runs a task while still passing absolute file
paths — is shown relative to the sibling once more than one of its calls uses it. Claude Code's
private agent worktree (.claude/worktrees/agent-<id>) is recognized from the path even when
the agent never reports it. Cross-worktree file targets render as a plain wrapping path whose
worktree label has a dotted underline, with a full-path copy tooltip on web and desktop and a
tap-to-copy prompt on mobile. A label shortened to the checkout directory (worktree/Dockerfile)
keeps that name underlined too, including a branch that contains a slash. Roots are resolved from
that environment's thread records, with T3 and Claude private-worktree layouts as fallbacks;
unknown roots are labeled External. A repository linked to
a message and cloned into the default .context/<name> folder gets its own label named after the
clone. Shell command rows
and approval prompts still show the exact command, on one truncated line. The shared runtime
instructions also tell every provider that shell commands already start in that directory.
Code: packages/client-runtime/src/work-log/commandDisplay.ts, used by
apps/web/src/components/chat/MessagesTimeline.logic.ts and by buildThreadFeed and
workEntryRowLabel in apps/mobile/src/lib/threadActivity.ts;
agentListView.ts;
toolPaths.ts;
ToolPathText.tsx; and
packages/provider-core/src/server/runtimeInstructions.ts.
Two more MCP servers serve agents outside T3 Code. /mcp/query gives read-only access to the
environment's history: projects, threads, turns, messages, the work log, plans, turn diffs, linked
pull requests, and usage. Its tools page with cursors, take since/until bounds, and cap text.
It reads the orchestration v2 projections; threads imported from before v2 carry only their
messages. /mcp/operate adds upstream's orchestrator, thread, project, and environment tools,
acting as a client caller labelled with the token's name, plus t3_approval_respond for
answering a thread's approvals (upstream's t3_pending_request_respond answers questions but
refuses approvals) and t3_thread_delete, which sends the app's own thread.delete command. It acts as the user, in any permission mode, without the spawn limits, and
threads it starts with t3_thread_launch record the token's label.
Both authenticate with an environment bearer token, never a cookie: /mcp/query needs
orchestration:read, /mcp/operate also orchestration:operate.
Tokens come from Settings → Connections → Agent access (POST /api/auth/agent-access-tokens,
access: "read" | "operate") or t3 auth session issue --read-only / --operate. Each token is a
normal client session, so revoking it works like revoking any client. Web and desktop only.
Code: AgentAccessMcpServer.ts,
mcp/query/,
toolkits/approval/,
toolkits/threadDelete/, the agentAccessToken
handler in auth/http.ts, AuthReadOnlyClientScopes and
AuthAgentOperateScopes in contracts/src/auth.ts, and
AgentAccessSettings.tsx.
User guide: agent-access.md.
Upstream gives every thread's agent its orchestration tools (create_threads, delegate_task,
t3_thread_launch, t3_thread_send, and the rest). The fork adds limits for an agent inside a
thread: chains of agent-started threads stop two levels deep, a thread keeps at most five started
threads going at once (a started thread counts until it settles or is archived, a delegated task
while its child runs), and t3_thread_send, t3_thread_wait, and t3_thread_interrupt refuse
the caller's own thread. Metadata tools such as t3_thread_update still default to the caller's
own thread, as upstream intends. A thread's agent never answers another thread's approvals and
never deletes a thread; it archives instead. t3_thread_organize also takes mark_read, the
reverse of upstream's mark_unread, which marks a thread read as opening it in the app does
(thread/handlers.ts).
The fork also adds t3_thread_rollback, which reverts another thread to the checkpoint after one of
its runs (runOrdinal from t3_thread_read's recentRuns, 0 for before the first run), like the
app's revert: later runs are discarded and, unless restoreFiles is false, the files are restored.
It follows t3_thread_interrupt's rules (never the caller's own thread, a live caller, the target
within the caller's modes) and also refuses a thread with a turn running. /mcp/operate tokens get
it too. The tool returns rollback_requested and a command ID when accepted; provider and
file restoration run afterward and can fail. t3_thread_read exposes the latest rollback
request ID and terminal failure.
A thread an agent starts with create_threads, t3_thread_launch, or t3_thread_tab_open
records startedBy (the starting thread, or the agent access token's label). The chat header on web, desktop, and mobile
names the starting thread (and opens it) or the token, and web sidebar rows mark the thread with a
bot icon. A client cannot set startedBy; only the server's MCP paths do.
Code: spawnPolicy.ts, its callers in
OrchestratorMcpService.ts (which also holds
rollbackThread) and
toolkits/project/handlers.ts,
OrchestrationV2ThreadStartedBy in
orchestrationV2.ts,
state/startedBy.ts,
StartedByChip.tsx, and
ThreadStartedByChip.tsx. User
guide: agent-access.md.
t3_thread_send and t3_thread_launch take contextLinks: Slack message permalinks, Notion
pages, Linear issues, GitHub issues, and GitHub repository roots. The server reads each one through
the same integration the composer uses and attaches the same chip record, so the message reads as
if a user had pasted the links: a link already in the message text becomes its chip there, the rest
are appended. Repositories are cloned before the message lands. An unrecognized link, a pull
request link, a disconnected or turned-off integration, or an unreadable object fails the call with
the reason; nothing is sent. Links are read only once the caller is allowed to send, and a
clientRequestId retry of a message that already landed does not read them again. Agents cannot
search Slack or Notion over MCP; they attach links they already have.
Code: McpContextLinks.ts, the link parser in
composerObjectLinks.ts, and the chip records in
integrationContextRecords.ts. User guide:
agent-access.md.
Upstream's delegate_task always runs the child in the calling thread's checkout; only
t3_thread_launch can choose a worktree, and it makes an ordinary top-level thread. The fork adds
an optional workspaceStrategy to delegate_task with t3_thread_launch's worktree (from
baseRef, optional branch and startFromOrigin) and existing_worktree shapes; there is no
root option, and omitting it still shares the caller's checkout. The child stays the caller's
subagent (Lineage, the Agents panel, completion delivery, spawn limits, and usage are unchanged),
but its first run waits in preparing while the server prepares the workspace the way a launch does:
worktree creation, the branch named before setup, Files to copy and the setup script, and setup
progress on the child thread. The child thread is then bound to the worktree and its agent starts
there. A preparation failure fails the child run and reaches the caller as a failed task whose
summary carries the reason, and a worktree the child never recorded is removed. Like
t3_thread_launch, it requires a full-access/default calling thread.
Code: delegateTask in OrchestratorMcpService.ts,
dispatchDelegatedTaskRequest in Orchestrator.ts,
prepareDeferredRun in ThreadLaunchService.ts,
and OrchestrationV2DelegatedTaskWorkspaceStrategy in
orchestrationV2.ts. User guide:
agent-access.md.
Upstream's Stop always stops everything: the turn, every delegated task under the thread, their completion wakes, and the thread's pull request watches. In the fork, Stop on a running turn that has delegated subagents still working asks first. Stop turn only interrupts the turn and holds the queue, but the subagents keep running, still report back when they finish, and pull request watches stay on. Stop everything is upstream's Stop. Work the provider runs in its own process, such as Claude's background shells and agents, ends with the turn either way. Once the turn has stopped, the existing "Waiting on" strip lists the subagents and its Stop ends them. On mobile the same choice is a native alert, and tapping the background-work pill offers to stop what is left.
Code: keepBackgroundWork on run.interrupt in
orchestrationV2.ts, dispatchRunInterrupt and
settleBackgroundWork in Orchestrator.ts,
runningDelegatedTasks in
orchestrationV2PendingBackgroundWork.ts,
and the Stop handlers in ChatView.tsx and
ThreadRouteScreen.tsx.
Upstream's t3_worktree_handoff only moves a thread out of the project checkout into a new
worktree, and fails once the thread is in one. An agent whose work then belonged on another branch,
such as a PR into a different base, ran git worktree add and prefixed every command with cd,
so T3 neither started commands there nor tracked that checkout's diffs. In the fork the handoff
also moves a thread that is already in a worktree, and moves it into an existing worktree when
branch is checked out there and path names that checkout. Nothing is created or set up for an
existing worktree unless runSetupScript asks, a failed move never removes it, and the worktree
the thread leaves stays on disk. The agent instructions point at the handoff instead of
git worktree add or cd.
Code: performHandoff in WorktreeMcpService.ts,
the tool in toolkits/worktree/tools.ts, and
orchestrationInstructions.ts.
Hide thread in the thread menu takes a thread out of the thread list until Unhide thread;
Hidden in the filter menu's Show submenu lists hidden threads. Unlike settling, activity
does not bring a hidden thread back; unlike archiving, it stays live. The state is the
server-owned hiddenAt field, set by the thread.hidden.set command and gated on the
threadHiding capability. A chat-tab group hides as one sidebar row: hiding or unhiding any
tab mirrors across the group's other live tabs. Agents reach it through t3_thread_organize's
hide and unhide actions.
Code: thread.hidden.set in Orchestrator.ts,
hiding.ts,
the hidden section in Sidebar.logic.ts and
mobile's threadListV2.ts. User guide:
thread-sidebar.md.
Move to group in the thread menu (web, desktop, mobile) files a thread under a user-made
group, which takes it out of the live thread list; each group is an entry in the filter menu's
Show submenu, which can also create an empty group. Groups are the shared server setting
threadGroups (name to icon and accent), written to every environment, so they persist when empty
and are removed only by Delete group, which returns their threads to the live list.
Membership is the server-owned groupName field, set by thread.group.set and gated on the
threadGroups capability; a name a thread holds but the setting lacks (an agent can create one)
still lists. Renaming moves each thread and the setting's key. The project-scoped setting
defaultThreadGroup files a project's new threads: ThreadManagementService.dispatch fills
groupName on every thread.create that does not choose one, and a fork inherits its source's
group. Hiding outranks a group. Agents
reach membership through t3_thread_organize's move_to_group and remove_from_group.
Code: thread.group.set in Orchestrator.ts,
resolveSidebarPages in Sidebar.logic.ts, the
menu in threadActionMenu.logic.ts,
useThreadGroups.ts, and
mobile's threadListV2.ts. User guide:
thread-sidebar.md.
On web and desktop, an inbox button appears in the sidebar header while any thread needs you, with
a count. Its popover lists those threads across every environment, project, and chat tab, one
entry per thread with its most urgent reason: an approval, a question for you, a failed turn, or
finished work you have not opened yet. Clicking an entry opens that exact tab, not the tab its
sidebar row would return to. Failures and completions leave the inbox once you open the thread or
mark them read; Mark unread in the thread menu brings a completion back. Approvals and
questions stay until answered. Archived threads are left out, and so are snoozed ones until they
wake or raise their hand. The same list is Needs attention in the command palette
(mod+alt+shift+n, attentionInbox.open).
The inbox is derived on the client from thread shells and the client's existing read markers, so it adds no server state or payload. Mobile does not show it, because the mobile client does not track which threads you have read.
Code: attentionInbox.ts,
SidebarAttentionInbox.tsx, and
the attention-inbox view in CommandPalette.tsx.
User guide: thread-sidebar.md.
Upstream notifies about every thread event or none. The fork lets each device choose:
- Web and desktop: Settings → General → Notify about mutes individual events (approval,
question, failed turn, usage limit, finished turn, pull request watch news), and a
Notifications switch on each project's settings page mutes every checkout of that project.
Both are client settings (
mutedNotificationEvents,mutedNotificationProjects), stored as mutes so new events default on. When several events land at once, the most urgent unmuted one alerts. Pull request watch news is new to the fork: a newly failed check on the head commit, a requested change, or a merge conflict on a watched PR alerts even if the agent is mid-turn. - Mobile: Settings → Notifications → Notify me about sends the relay's existing per-device
notifyOn*flags, which upstream always sends as on. The relay classifies by turn phase, so usage limits count as failures and pull request wakes as completions. Mobile has no per-project mute.
Notifications carry no approve or reply actions on any surface; they open the thread.
Code: notificationRules.ts,
ThreadNotificationCoordinator.tsx,
NotificationSettings.tsx, and
SettingsNotificationsRouteScreen.tsx.
User guides: project-settings.md and
mobile-notifications.md.
Usage → Limits reads the public status page for Claude, Codex, Cursor, Devin, and Grok. A page that is not fully operational shows its own wording, such as Partially Degraded Service, on web, desktop, and mobile, and opens the status page. The same line appears in the composer's usage-limits result for that provider. OpenCode, Antigravity, Muse, and Pi do not publish a status feed this can read. Grok's page often refuses an automated read; a refused read shows nothing rather than a false all-clear.
Code: providerServiceStatus.ts and
collectDegradedServices in usageLimits.ts. User
guide: usage.md.
Upstream shows a recovery banner when Codex or Claude ends a turn on a usage limit, with Resume at reset, Cancel auto-resume, and Snooze until reset. The fork adds:
- Resume now, on web, desktop, and mobile, which sends the same "Continue where you left off."
continuation at once, on the composer's model, so picking another provider first continues there
instead of into the same limit. It is the
thread.usage-limit.resume-nowcommand, so the server owns the continuation. Servers advertise it with theusageLimitResumeNowcapability; clients hide the button without it. - Agents see the stopped state as
usageLimit(stopped run and reset time) int3_thread_readandt3_thread_wait, and send the same command witht3_thread_usage_limit_resume, under the usual rules for writing to another thread. It continues on the thread's current model, so an agent switches models witht3_thread_configurefirst. - Continue in new tab on web and desktop, which forks the chat onto another ready account or model through the Chat tabs fork, with the continuation as the new tab's prompt.
- A one-minute grace after the reported reset before an armed auto-resume is sent, since a request at the reset can still be refused.
- A notice in the stopped turn when auto-resume is scheduled or cancelled.
- Cursor stops count too. Cursor reports an exhausted allowance only as run error text ("You're out of usage…"), which upstream shows as a generic provider error; the fork recognizes it as a usage limit, so Cursor threads get the same banner. Cursor gives no reset time, so there is no auto-resume for them.
Code: cursorRunFailure in
Cursor adapter, the thread.usage-limit.resume-now case and limit-recovery notice in
Orchestrator, the grace in
UsageLimitRecoveryWorker,
resumeUsageLimitedThread in OrchestratorMcpService,
UsageLimitRecoveryBanner, and
UsageLimitRecoveryCard. User guides:
providers-codex.md and
providers-claude.md.
Web and desktop support Notion OAuth sign-in in Integrations settings, where the user enters their own Notion connection's client ID and secret (kept on the server, with T3CODE_NOTION_CLIENT_ID and T3CODE_NOTION_CLIENT_SECRET as a fallback), page attachments from the paperclip picker and command palette, a Notion tab in the # menu, and conversion of pasted notion.so, notion.com, and notion.site page links to context chips. A pasted link to a page the connection cannot read stays as text, with a notice that opens the page in Notion and retries the conversion once the user shares it. Turning off Enable Notion integration in those settings keeps pasted Notion links as links without setup prompts and hides Notion attachment actions on web, desktop, and mobile for the environment, keeping the connected account. Mobile can pick pages using the environment's connection and inspect captured page contents. Pages are captured as bounded Markdown snapshots and sent through the shared context projection to every provider. Remote sign-in can use a registered HTTPS callback through T3CODE_NOTION_REDIRECT_URI, or paste the OAuth redirect URL back into settings.
A thread's chat-tab group can also be linked to one Notion page, from the thread menu, the web command palette, or the mobile tab menu, which open the page picker in link mode. The link lives in the fork-owned fork_notion_thread_links table, keyed by tab group, and streams over notion.subscribeThreadLinks; the page's title and URL are copied when linked. The web chat header chip shows the title and offers open, change, and unlink; mobile shows the same chip beside the tab switcher. Linking and unlinking are client-guarded RPCs that need the orchestration-operate grant. Unlinking stays available with the integration turned off.
Code: NotionAuth, NotionApi, NotionPagePicker, NotionThreadLinks, NotionThreadLink, and NotionPagePickerSheet. User guide: Notion.
Themes carry the terminal's 16 ANSI colors. A VS Code import reads terminal.ansi* and fills unset
ones with VS Code's defaults, and the theme editor's advanced view edits them under Terminal
colors. Built-in themes keep Ghostty's stock palette. Appearance settings add Terminal cursor
(block, bar, or underline) and Terminal line height, and terminal text is centred on the
font's line box rather than its ink. This is web and desktop; mobile keeps its own terminal palette
and metrics.
Code: TERMINAL_ANSI_ROLES in themePalettes.ts,
applyPalette and setDefaultCursorStyle in
core.ts, measureGhosttyCell in
renderer.ts, and the ANSI mapping in
vscodeThemeImport.ts. User guide:
Appearance.
Activate Python environment in terminals (off by default) and Python interpreter path make
new and restarted terminals activate a Python environment, like VS Code's
python.terminal.activateEnvironment and python.defaultInterpreterPath. An empty path finds a
.venv or venv in the terminal's working directory; a configured interpreter or environment
folder can be a virtual environment or a conda environment (conda activate). Both are
project-scoped settings, exposed in web, desktop, and mobile settings and in
t3_environment_preferences_update, and a repository can set the path with
"pythonInterpreterPath" in its t3.json.
Code: pythonEnvironment.ts, called from
startSession in Manager.ts, which resolves the path with
resolveTerminalPythonEnvironment. User guide:
Terminal.
An expanded tool call in the web and desktop timeline shows its call and output inside a rounded, tinted panel, so the output stays visually separate from the assistant text around it. Upstream renders them flush with the timeline. Inner cards and Input/Output labels stay removed as upstream has them. Mobile follows upstream.
Code: the panel variant of WorkLogDetails in
WorkLog.tsx.
The file viewer header has a refresh button, and mod+r (command file.refresh, when
fileFocus) does the same while the viewer has focus. It reads the file again and refetches
media and rendered pages, so a file outside the workspace that never triggers a workspace
mutation can still be brought up to date. See the user guide
and FilePreviewPanel.tsx.
Typing > at the start of the file picker's (mod+p) search switches to the command palette's
actions-only search with the query kept, and deleting the > returns to file search. Upstream keeps
the two surfaces separate. See the user guide,
ProjectFilePicker.tsx, and
CommandPalette.tsx.
Update this page in the same change that adds, changes, or removes a user-visible fork-only behavior. Rewrite the affected section so it describes the behavior as it is now. Link the code, and the user guide when one exists. Do not append a changelog entry or describe the implementation line by line.
Remove a section when upstream adopts the change or the fork drops it. An upstream sync includes this check. A bugfix or refactor that leaves the described behavior the same does not need an entry.
README.md only points here. Do not add a second feature list there.
Settings → Storage → Managed worktrees inventories managed checkouts per selected environment with linked threads, branch, local changes, and last synced pull request state. Sizes are measured on demand. Individual and bulk removal rechecks local changes and running work under the workspace lease, refuses project checkouts and unmanaged paths, and keeps branches and thread history. Mobile offers the same inventory and safe cleanup in each environment’s Settings page.
Code: WorktreeInventory.ts and WorktreeInventory.tsx. User guide: Review worktree storage.