Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# T3 Code

T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs (Codex, Claude Code, Cursor, Grok, OpenCode) and serves web, desktop, and mobile clients.
T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs (Codex, Claude Code, Cline, Cursor, Grok, OpenCode) and serves web, desktop, and mobile clients.

You can think of T3 Code as an open source "bring-your-own-subscription" alternative to apps like Claude Desktop, Codex App, Cursor Glass and Conductor.

Expand Down Expand Up @@ -48,7 +48,7 @@ We need to be on the same page with terminology. When communicating, use this la
- **we, us, and maintainers** mean Theo, Julius and the people building T3 Code. These are who you are talking to now.
- **user** means the person using T3 Code to direct coding agents.
- **agent** means the coding agent a user runs inside T3 Code. Depending on context, that may also include you.
- **provider** means the agent runtime or harness T3 Code talks to, such as Codex, Claude, Cursor, or OpenCode.
- **provider** means the agent runtime or harness T3 Code talks to, such as Codex, Claude, Cline, Cursor, or OpenCode.
- **client** means the web, desktop, or mobile UI.
- **environment** means one running T3 server and the machine, filesystem, provider credentials, and state it owns.
- **project** means an environment-local workspace record rooted at a directory.
Expand All @@ -68,7 +68,7 @@ The most common defect in this repo is a change that works on the path you teste

- **Entry points.** A behavior reachable from the chat view is usually also reachable from Settings, the command palette, and a keybinding. Fixing one is not fixing the feature.
- **Clients.** Web, desktop (wraps web, adds Electron shell/IPC), and mobile (React Native, separate navigation). Shared logic lives in `packages/client-runtime`
- **Providers.** Codex, Claude, Cursor, Grok, and OpenCode each have an adapter. Provider-shaped features need a decision per adapter, even if the decision is "not supported here".
- **Providers.** Codex, Claude, Cline, Cursor, Grok, and OpenCode each have an adapter. Provider-shaped features need a decision per adapter, even if the decision is "not supported here".
- **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow.
- **Reverse states.** If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug.
- **Connection modes.** Local, remote/relay, and tunnel behave differently. Multi-device and multi-environment cases are real.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

T3 Code is an "agent harness control surface". It enables control of the agents on your machine with a best-in-class mobile app ([iOS](https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824), [Android](https://play.google.com/store/apps/details?id=com.t3tools.t3code)), [web app](https://app.t3.codes) and [Electron-based desktop app](https://t3.codes).

Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, and OpenCode. If they're set up on your computer, T3 Code can control them.
Works with your subscriptions on Claude Code, Codex, Cline, Cursor, Grok Build, and OpenCode. If they're set up on your computer, T3 Code can control them.

## "Wait, what are you selling me?"

Expand All @@ -13,10 +13,11 @@ We wanted something performant, remote-ready, and truly open. If we ever go the
## Installation

> [!WARNING]
> T3 Code currently supports Codex, Claude, Cursor, Grok Build and OpenCode. Install and authenticate at least one provider before use:
> T3 Code currently supports Codex, Claude, Cline, Cursor, Grok Build, and OpenCode. Install and authenticate at least one provider before use:
>
> - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login`
> - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login`
> - Cline: install [Cline CLI](https://docs.cline.bot/usage/cli-overview) and run `cline auth`
> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `agent login`
> - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login`
> - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login`
Expand Down
9 changes: 9 additions & 0 deletions apps/marketing/public/harnesses/cline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 12 additions & 8 deletions apps/marketing/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const mobileEndorsementRows = [
</h1>

<p class="hero-sub">
Orchestrate Claude Code, Codex, OpenCode, Cursor, and Grok from one surface.
Orchestrate Claude Code, Codex, Cline, OpenCode, Cursor, and Grok from one surface.
Bring your own subscription. Fork the whole thing.
</p>

Expand Down Expand Up @@ -186,8 +186,8 @@ const mobileEndorsementRows = [
<div class="section-head">
<h2>Bring your own sub</h2>
<p>
T3 Code doesn't resell tokens. Plug in Claude Code, Codex, OpenCode,
Cursor, or Grok with the credentials you already have — we orchestrate
T3 Code doesn't resell tokens. Plug in Claude Code, Codex, Cline,
OpenCode, Cursor, or Grok with the credentials you already have — we orchestrate
them, you keep your plan.
</p>
</div>
Expand All @@ -207,6 +207,13 @@ const mobileEndorsementRows = [
<div class="harness-tag">codex login</div>
</div>
</div>
<div class="harness">
<div class="harness-mark"><img src="/harnesses/cline.svg" alt="" /></div>
<div class="harness-meta">
<div class="harness-name">Cline</div>
<div class="harness-tag">cline auth</div>
</div>
</div>
<div class="harness">
<div class="harness-mark"><img src="/harnesses/opencode-dark.svg" alt="" /></div>
<div class="harness-meta">
Expand Down Expand Up @@ -811,7 +818,7 @@ const mobileEndorsementRows = [

.harness-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-columns: repeat(6, 1fr);
gap: 0;
border: 1px solid var(--border);
border-radius: var(--radius);
Expand Down Expand Up @@ -1266,10 +1273,7 @@ const mobileEndorsementRows = [
.harness-grid { grid-template-columns: 1fr 1fr; }
.harness { border-right: 0; border-bottom: 1px solid var(--border); }
.harness:nth-child(odd) { border-right: 1px solid var(--border); }
.harness:nth-child(5):last-child {
grid-column: 1 / -1;
border-right: 0;
}
.harness:nth-last-child(-n + 2) { border-bottom: 0; }
.git-inner { grid-template-columns: 1fr; gap: 40px; }
.open-grid { grid-template-columns: 1fr; }
}
Expand Down
37 changes: 36 additions & 1 deletion apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Path, Svg } from "react-native-svg";
import { Circle, Path, Rect, Svg } from "react-native-svg";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";

type ProviderIconProps = {
Expand Down Expand Up @@ -39,6 +39,41 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "cline") {
const fill = isDarkMode ? "#F5F5F5" : "#0F0F0F";
return (
<Svg width={size} height={size} viewBox="0 0 92 96" fill="none">
<Path
fill={fill}
transform="translate(0 0.5)"
fillRule="nonzero"
d="M65.4492701,16.3 C76.3374701,16.3 85.1635558,25.16479 85.1635558,36.1 L85.1635558,42.7 L90.9027661,54.1647464 C91.4694141,55.2966923 91.4668177,56.6300535 90.8957658,57.7597839 L85.1635558,69.1 L85.1635558,75.7 C85.1635558,86.63554 76.3374701,95.5 65.4492701,95.5 L26.0206986,95.5 C15.1328272,95.5 6.30641291,86.63554 6.30641291,75.7 L6.30641291,69.1 L0.448507752,57.7954874 C-0.14693501,56.6464093 -0.149634367,55.2802504 0.441262896,54.1288283 L6.30641291,42.7 L6.30641291,36.1 C6.30641291,25.16479 15.1328272,16.3 26.0206986,16.3 L65.4492701,16.3 Z M62.9301895,22 L29.189529,22 C19.8723267,22 12.3191987,29.5552188 12.3191987,38.875 L12.3191987,44.5 L7.44288578,53.9634655 C6.84794449,55.1180686 6.85066096,56.4896598 7.45017099,57.6418974 L12.3191987,67 L12.3191987,72.625 C12.3191987,81.9450625 19.8723267,89.5 29.189529,89.5 L62.9301895,89.5 C72.2476729,89.5 79.8005198,81.9450625 79.8005198,72.625 L79.8005198,67 L84.5682187,57.6061395 C85.1432011,56.473244 85.1458141,55.1345713 84.5752587,53.9994398 L79.8005198,44.5 L79.8005198,38.875 C79.8005198,29.5552188 72.2476729,22 62.9301895,22 Z"
/>
<Circle cx="45.7349843" cy="11.5" r="11" fill={fill} />
<Rect
x="31"
y="45"
width="5"
height="22"
rx="2.5"
fill={fill}
stroke={fill}
strokeWidth="8"
/>
<Rect
x="55"
y="45"
width="5"
height="22"
rx="2.5"
fill={fill}
stroke={fill}
strokeWidth="8"
/>
</Svg>
);
}

if (props.provider === "cursor") {
return (
<Svg width={size} height={size} viewBox="0 0 466.73 532.09" fill="none">
Expand Down
72 changes: 63 additions & 9 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ import {
type ComposerDraft,
} from "../../state/use-composer-drafts";
import { useEnvironmentServerConfig, useProjects } from "../../state/entities";
import { resolveSelectableModelSelection } from "../../lib/modelOptions";
import {
getUnsupportedProviderAttachmentReason,
getUnsupportedProviderModeReason,
getUnavailableProviderModelReason,
providerSupportsImageAttachments,
resolveSelectableModelSelection,
shouldShowProviderInteractionModeToggle,
} from "../../lib/modelOptions";
import { deriveThreadTitleFromPrompt } from "../../lib/projectThreadStartTurn";
import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration";
import { enqueueThreadOutboxMessage, removeThreadOutboxMessage } from "../../state/thread-outbox";
Expand Down Expand Up @@ -613,6 +620,15 @@ export function NewTaskDraftScreen(props: {
if (isIncomingShareTransferPending) {
return;
}
const unsupportedAttachmentReason = getUnsupportedProviderAttachmentReason({
config: selectedEnvironmentServerConfig,
selection: flow.selectedModel,
attachmentCount: 1,
});
if (unsupportedAttachmentReason !== null) {
Alert.alert("Attachments unavailable", unsupportedAttachmentReason);
return;
}
const result = await pickComposerImages({ existingCount: flow.attachments.length });
if (result.images.length > 0) {
flow.appendAttachments(result.images);
Expand Down Expand Up @@ -661,6 +677,33 @@ export function NewTaskDraftScreen(props: {
? (draft.interactionMode ?? flow.interactionMode)
: "default";
const initialMessageText = draft.text.trim();
const unavailableModelReason = getUnavailableProviderModelReason({
config: selectedEnvironmentServerConfig,
selection: modelSelection,
});
if (unavailableModelReason !== null) {
Alert.alert("Provider still checking", unavailableModelReason);
return;
}
const unsupportedProviderModeReason = getUnsupportedProviderModeReason({
config: selectedEnvironmentServerConfig,
selection: modelSelection,
runtimeMode,
interactionMode,
});
if (unsupportedProviderModeReason !== null) {
Alert.alert("Change provider mode", unsupportedProviderModeReason);
return;
}
const unsupportedAttachmentReason = getUnsupportedProviderAttachmentReason({
config: selectedEnvironmentServerConfig,
selection: modelSelection,
attachmentCount: draft.attachments.length,
});
if (unsupportedAttachmentReason !== null) {
Alert.alert("Remove attachments", unsupportedAttachmentReason);
return;
}

if (
!modelSelection ||
Expand Down Expand Up @@ -805,6 +848,7 @@ export function NewTaskDraftScreen(props: {
isIncomingShareReady &&
!isImportingShare &&
!flow.submitting &&
flow.unsupportedProviderModeReason === null &&
!(flow.workspaceMode === "worktree" && !flow.selectedBranchName);
const promptEditor = (
<ComposerEditor
Expand Down Expand Up @@ -989,13 +1033,18 @@ export function NewTaskDraftScreen(props: {
fadeTransparent={sheetFadeTransparent}
contentPaddingRight={8}
>
<ComposerToolbarButton
accessibilityLabel="Add attachment"
disabled={isIncomingShareTransferPending}
icon="plus"
onPress={() => void handlePickImages()}
showChevron={false}
/>
{providerSupportsImageAttachments({
config: selectedEnvironmentServerConfig,
selection: flow.selectedModel,
}) ? (
<ComposerToolbarButton
accessibilityLabel="Add attachment"
disabled={isIncomingShareTransferPending}
icon="plus"
onPress={() => void handlePickImages()}
showChevron={false}
/>
) : null}
<ComposerInlineControl
accessibilityLabel="Model and reasoning settings"
disabled={isIncomingShareTransferPending}
Expand All @@ -1007,7 +1056,12 @@ export function NewTaskDraftScreen(props: {
maxWidth={152}
onPress={settingsSheetPresentation.open}
/>
{flow.planModeEnabled ? (
{flow.planModeEnabled &&
shouldShowProviderInteractionModeToggle({
config: selectedEnvironmentServerConfig,
selection: flow.selectedModel,
interactionMode: flow.interactionMode,
}) ? (
<ComposerInlineControl
accessibilityHint={`Switches to ${flow.interactionMode === "plan" ? "Build" : "Plan"} mode`}
accessibilityLabel={`Interaction mode: ${flow.interactionMode === "plan" ? "Plan" : "Build"}`}
Expand Down
Loading
Loading