Skip to content

Latest commit

 

History

History
1581 lines (1340 loc) · 113 KB

File metadata and controls

1581 lines (1340 loc) · 113 KB

Fork differences

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

Agents update this page in the same change that adds, changes, or removes a difference. The rule is in AGENTS.md.

Browser automation survives request timeouts

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.

Pause threads on update

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.

Custom file applications

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.

Devin provider

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-mode at 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 list feeds the skill picker, and $skill mentions 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. A cog_... service key with ViewOrgConsumption and DEVIN_ORG_ID can 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.

Custom ACP provider

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 model config option, else session models), its other config options as model options, and the plan toggle only when it has a plan or architect mode. 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 model config option or session/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.

Provider sign-in methods

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.

Thread usage and context notices

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.

Model change marker

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.

Provider account picker

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.

Cursor long-context tiers use Max Mode

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 turns send the picker's defaults

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.

Cursor follow-ups steer the running turn

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.

Agents panel drilldowns

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, a waiting badge 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 and run 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.ts focusToolCall). 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 dropped agents; 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. OrchestrationV2Subagent gains 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's total_tokens is 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 as delegate_task children on any provider, the sum of their own thread's provider-turn usage plus its tool calls, written when the task finishes; subagentUsageFromChildTurns in subagentProjection.ts), outputFile (Claude's task output file), and sessionUrl (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 N past 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, and V2ItemInspector for 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 @handle chip 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; subagentContinuationContext builds 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.

Attach agents to chat by handle

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 subagent context 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 carries task_status and t3_thread_send instructions, a Claude subagent its SendMessage agent 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 (formatSubagentPayload in composerContextReferences.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.

Click inline code to copy it

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

Chat tabs

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_tabs table, created by ensureThreadTabsSchema in apps/server/src/threadTabs/schema.ts. It is versioned in a separate fork_schema_migrations table so upstream's numbered migrations are never touched.
  • Server. The ThreadTabs service (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.ts serves it as the threadTabs HTTP group. A new tab is an orchestration thread.create on 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.ts follows 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.ts mirrors 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 (threadTabGroupHeaderTarget in packages/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, and Cmd+Option+T on macOS or Ctrl+Alt+T on 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 (useThreadTabActions in apps/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 (useRightPanelFollowsTabSwitch in ThreadTabs.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 omits runId, replaces the empty tab and takes its draft (onContinueFromTab in ChatView.tsx, continueFrom in apps/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 a thread-tab context chip at the caret, which can be moved like any other chip. The kind lives in packages/contracts/src/composerContext.ts and is formatted for providers in packages/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.fork from a completed response (forkResponseIntoTab in ThreadTabs.tsx, the fork endpoint), so the tab carries the conversation itself. The fork point comes from latestThreadForkPoint and threadForkPointBeforeMessage in packages/client-runtime/src/threadTabs.ts. When there is no finished response to fork from, the tab falls back to a thread-tab summary 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 modelSelection switches 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 (matchesModelPickerLock in ModelPickerContent.tsx).
    • The account picker forks the same way when the chosen account cannot take over the tab.
  • Agents. Over MCP, t3_thread_tabs lists a thread's group and t3_thread_tab_open opens 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 records startedBy and 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.

Split view

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.ts holds 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 (useSplitPaneFocus in apps/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+\), and splitView.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 (resolveSidebarTabPairTarget in Sidebar.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 (limitSidebarTabs in Sidebar.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.

Open the thread's pull request in the browser

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.

Pull request host links follow Open links in

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.

Worktree cleanup ignored names

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.

Worktree branch named before setup

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.

Sidebar resource pill

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.

Sidebar back and forward buttons

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.

Sidebar thread views

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.

Sidebar filter menu

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.

Projects settings sidebar entry

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 settings tab

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.

Conductor workspace settings

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.

Claude subagent worktrees get the project setup

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.

Linear integration

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.

Slack messages and threads

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

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.

Start a thread from a pull request, branch, or issue

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 requests from a picker

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.

Watch a pull request

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.

Create a thread before writing its first message

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.

Export a transcript

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.

Import a CLI conversation

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.

Import a Conductor workspace

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.

Attach repositories as context

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.

Pasted and typed links become chips

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.

Pick several items from the # menu

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.

Pasted file paths become chips

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.

Desktop mock-update loop

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.

Commands and file paths shown relative to the workspace

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.

Agent access over MCP

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.

Agents start and drive threads

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.

Agents attach integration context to messages

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.

Delegated subagents in their own worktree

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.

Stop keeps delegated subagents running on request

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.

Agents move their thread between worktrees

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 a thread

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.

Thread groups

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.

Attention inbox

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.

Per-event notification rules

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.

Provider service status

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.

Usage-limit recovery

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-now command, so the server owns the continuation. Servers advertise it with the usageLimitResumeNow capability; clients hide the button without it.
  • Agents see the stopped state as usageLimit (stopped run and reset time) in t3_thread_read and t3_thread_wait, and send the same command with t3_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 with t3_thread_configure first.
  • 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.

Notion page context

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.

Terminal colors, cursor, and line height

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.

Terminals activate Python environments

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.

Expanded tool calls sit in a panel

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.

Refresh the open file

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.

Type > in the file picker for commands

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.

Keeping this page current

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.

Managed worktree storage

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.