Skip to content

Commit 28449b0

Browse files
authored
Merge pull request #85 from aaditagrawal/codex/sync-upstream-traits-and-fork-rules
fix(web): scope traits by provider instance
2 parents 6cc364d + 394937a commit 28449b0

10 files changed

Lines changed: 127 additions & 25 deletions

AGENTS.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- **NEVER create PRs, push branches, post comments, or perform ANY write operation against `pingdotgg/t3code` or any upstream/third-party repo.**
77
- **NEVER run `gh pr create` without `--repo aaditagrawal/t3code`.** Always explicitly target the fork.
88
- **NEVER run `gh` write commands (pr create, issue create, pr comment, pr close, pr merge) against any repo other than `aaditagrawal/t3code`.**
9+
- Any request involving PRs, issues, GitHub Actions, workflows, checks, comments, labels, releases, or other GitHub repo operations is fork-only: use `aaditagrawal/t3code` explicitly and do not target upstream.
910
- The ONLY interaction with upstream is `git fetch upstream` to pull changes. Everything else targets `origin` (the fork).
1011
- When merging upstream changes, create a PR on `aaditagrawal/t3code` targeting the fork's `main` branch.
1112

@@ -14,23 +15,33 @@
1415
- The fork's `README.md` takes priority over upstream's. On merge conflicts, keep ours.
1516
- Do NOT commit scratch/analysis markdown files (e.g. `CONFLICT_ANALYSIS.md`, plan dumps) into the repo.
1617

18+
## Protected Fork Features
19+
20+
When syncing upstream, preserve these fork features unless the user explicitly asks to remove them:
21+
22+
1. Multi-provider runtime support for the built-in drivers: Codex CLI, Claude Code, Cursor, Droid, OpenCode, Amp, Copilot, Gemini CLI, and Kilo.
23+
2. Usage and limit monitoring, including token/context usage snapshots, provider usage events, Codex account rate-limit streams, and the web rate-limit banner/panel UX.
24+
3. Provider management UX, including custom provider instances, per-instance environment/config/model state, custom model slugs, and provider-scoped traits such as reasoning, context window, fast mode, and agent selection.
25+
4. Provider-neutral orchestration reliability, including SQLite event persistence, command receipts, replay/live stream ordering, session restart/reconnect behavior, and projection consistency.
26+
1727
## Task Completion Requirements
1828

1929
- All of `bun fmt`, `bun lint`, and `bun typecheck` must pass before considering tasks completed.
2030
- NEVER run `bun test`. Always use `bun run test` (runs Vitest).
2131

2232
## Project Snapshot
2333

24-
T3 Code is a multi-provider web GUI for coding agents. It supports 8 providers:
34+
T3 Code is a multi-provider web GUI for coding agents. This fork supports 9 built-in provider drivers:
2535

2636
- **Codex CLI** (v0.37.0+) — JSON-RPC over stdio
2737
- **Claude Code** — Claude Agent SDK with thinking tokens and permission modes
2838
- **Cursor** — ACP (Agent Communication Protocol) over stdio
39+
- **Droid** — Factory Droid SDK runtime
40+
- **OpenCode** — SDK CLI server
2941
- **Copilot** — GitHub Copilot CLI
3042
- **Gemini CLI** — Google Gemini CLI with persistent JSON
3143
- **Amp** — Amp Code headless mode (no `/mode free`)
3244
- **Kilo** — HTTP SSE transport
33-
- **OpenCode** — SDK CLI server
3445

3546
This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged.
3647

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
AGENTS.md
1+
AGENTS.md

CONTRIBUTING.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
First off, thank you for considering contributing to this fork! It's people like you that make the open-source community such an amazing place to learn, inspire, and create.
44

5-
This fork is maintained in [aaditagrawal/t3code](https://github.com/aaditagrawal/t3code) and focuses on expanding provider support while improving the core orchestration and persistence layers.
5+
This fork is maintained in [aaditagrawal/t3code](https://github.com/aaditagrawal/t3code) and focuses on expanding provider support, preserving usage and limit monitoring, and improving the core orchestration and persistence layers.
6+
7+
## Protected Fork Features
8+
9+
Pull requests and upstream syncs should preserve these four fork features unless the change explicitly replaces them with equivalent or better behavior:
10+
11+
1. Multi-provider runtime support across Codex CLI, Claude Code, Cursor, Droid, OpenCode, Amp, Copilot, Gemini CLI, and Kilo.
12+
2. Usage and limit monitoring, including token/context usage, provider usage events, and rate-limit UI.
13+
3. Provider management UX for custom instances, per-instance config/environment/model state, and provider-scoped traits.
14+
4. Provider-neutral orchestration reliability for persistence, replay/live ordering, reconnects, restarts, and projections.
615

716
## How Can I Contribute?
817

README.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,33 @@
22

33
T3 Code is a minimal web GUI for coding agents made by [Pingdotgg](https://github.com/pingdotgg). This project is a downstream fork of the original [T3 Code](https://github.com/pingdotgg/t3code), maintained in [aaditagrawal/t3code](https://github.com/aaditagrawal/t3code).
44

5-
This fork focuses on expanding provider support, improving persistence layers, and refining provider management across the app.
5+
This fork focuses on expanding provider support, keeping usage and limit monitoring visible, improving persistence layers, and refining provider management across the app.
66

7-
It supports Codex, Claude Code, Cursor, Copilot, Gemini CLI, Amp, Kilo, and OpenCode.
7+
It supports Codex, Claude Code, Cursor, Droid, OpenCode, Copilot, Gemini CLI, Amp, and Kilo.
88

99
(NOTE: Amp /mode free is not supported, as Amp Code doesn't support it in headless mode - since they need to show ads for that business model to work.)
1010

1111
## Why this fork?
1212

13-
This fork aims to provide a more robust and feature-rich multi-provider experience, with improved server management, more reliable persistence of orchestration events, and UI refinements for settings and model selection.
13+
This fork aims to provide a more robust and feature-rich multi-provider experience, with improved server management, visible usage/rate-limit monitoring, more reliable persistence of orchestration events, and UI refinements for settings and model selection.
14+
15+
The protected fork features are multi-provider runtime support, usage and limit monitoring, provider management UX, and provider-neutral orchestration reliability. Upstream syncs should preserve those unless a change explicitly replaces them with equivalent or better behavior.
1416

1517
### Multi-provider support (Enhanced)
1618

1719
Adds full provider adapters (server managers, service layers, runtime layers) for agents that are not yet on the upstream roadmap:
1820

19-
| Provider | What's included |
20-
| ----------- | ------------------------------------------------------------------------- |
21-
| Gemini CLI | **Enhanced:** Adapter + `geminiCliServerManager` with full test coverage |
22-
| Amp | Adapter + `ampServerManager` for headless Amp sessions |
23-
| Copilot | Adapter + CLI binary resolution + text generation layer |
24-
| Cursor | Adapter + ACP probe integration + usage tracking |
25-
| Kilo | Adapter + `kiloServerManager` + OpenCode-style server URL config |
26-
| OpenCode | Adapter + `opencodeServerManager` with hostname/port/workspace config |
27-
| Claude Code | Full adapter with permission mode, thinking token limits, and SDK typings |
21+
| Provider | What's included |
22+
| -------------- | ------------------------------------------------------------------------- |
23+
| Codex CLI | App-server JSON-RPC support with usage/rate-limit monitoring |
24+
| Claude Code | Full adapter with permission mode, thinking token limits, and SDK typings |
25+
| Cursor | ACP adapter, probe integration, permissions, and usage tracking |
26+
| Droid | Factory Droid SDK runtime integration |
27+
| OpenCode | Adapter with hostname/port/workspace config |
28+
| Amp | Adapter + `ampServerManager` for headless Amp sessions |
29+
| GitHub Copilot | Adapter + CLI binary resolution + text generation layer |
30+
| Gemini CLI | **Enhanced:** Adapter + `geminiCliServerManager` with full test coverage |
31+
| Kilo | Adapter + `kiloServerManager` + OpenCode-style server URL config |
2832

2933
### Persistence & Orchestration Improvements
3034

@@ -43,6 +47,7 @@ Adds full provider adapters (server managers, service layers, runtime layers) fo
4347
| Sidebar search | Normalized thread title search with instant filtering |
4448
| Plan sidebar | Dedicated panel for reviewing, downloading, or saving proposed agent plans |
4549
| Terminal drawer | Theme-aware integrated terminal with accent color styling |
50+
| Usage monitoring | Context window meter, token usage events, and account rate-limit banner/panel visibility |
4651

4752
## Getting started
4853

@@ -78,6 +83,7 @@ bun run dev
7883
- [Gemini CLI](https://github.com/google-gemini/gemini-cli)
7984
- [Claude Code](https://github.com/anthropics/claude-code)
8085
- [Cursor](https://cursor.sh)
86+
- [Droid](https://factory.ai)
8187
- [Codex CLI](https://github.com/openai/codex) (requires v0.37.0 or later)
8288
- [Copilot](https://github.com/features/copilot)
8389
- [Amp](https://ampcode.com)

apps/web/src/components/chat/ChatComposer.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -703,9 +703,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
703703
model: selectedModel,
704704
models: selectedProviderModels,
705705
prompt,
706-
modelOptions: composerModelOptions?.[selectedProvider],
706+
modelOptions: composerModelOptions?.[selectedInstanceId],
707707
}),
708-
[composerModelOptions, prompt, selectedModel, selectedProvider, selectedProviderModels],
708+
[
709+
composerModelOptions,
710+
prompt,
711+
selectedInstanceId,
712+
selectedModel,
713+
selectedProvider,
714+
selectedProviderModels,
715+
],
709716
);
710717

711718
const selectedPromptEffort = composerProviderState.promptEffort;
@@ -994,21 +1001,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
9941001

9951002
const providerTraitsMenuContent = renderProviderTraitsMenuContent({
9961003
provider: selectedProvider,
1004+
instanceId: selectedInstanceId,
9971005
...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),
9981006
...(routeKind === "draft" && draftId ? { draftId } : {}),
9991007
model: selectedModel,
10001008
models: selectedProviderModels,
1001-
modelOptions: composerModelOptions?.[selectedProvider],
1009+
modelOptions: composerModelOptions?.[selectedInstanceId],
10021010
prompt,
10031011
onPromptChange: setPromptFromTraits,
10041012
});
10051013
const providerTraitsPicker = renderProviderTraitsPicker({
10061014
provider: selectedProvider,
1015+
instanceId: selectedInstanceId,
10071016
...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),
10081017
...(routeKind === "draft" && draftId ? { draftId } : {}),
10091018
model: selectedModel,
10101019
models: selectedProviderModels,
1011-
modelOptions: composerModelOptions?.[selectedProvider],
1020+
modelOptions: composerModelOptions?.[selectedInstanceId],
10121021
prompt,
10131022
onPromptChange: setPromptFromTraits,
10141023
});

apps/web/src/components/chat/TraitsPicker.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
type ProviderDriverKind,
3+
type ProviderInstanceId,
34
type ProviderOptionDescriptor,
45
type ProviderOptionSelection,
56
type ScopedThreadRef,
@@ -196,6 +197,7 @@ export function shouldRenderTraitsControls(input: {
196197

197198
export interface TraitsMenuContentProps {
198199
provider: ProviderDriverKind;
200+
instanceId?: ProviderInstanceId;
199201
models: ReadonlyArray<ServerProviderModel>;
200202
model: string | null | undefined;
201203
prompt: string;
@@ -208,6 +210,7 @@ export interface TraitsMenuContentProps {
208210

209211
export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
210212
provider,
213+
instanceId,
211214
models,
212215
model,
213216
prompt,
@@ -228,11 +231,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
228231
return;
229232
}
230233
setProviderModelOptions(threadTarget, provider, nextOptions, {
234+
...(instanceId ? { instanceId } : {}),
231235
model,
232236
persistSticky: true,
233237
});
234238
},
235-
[model, persistence, provider, setProviderModelOptions],
239+
[instanceId, model, persistence, provider, setProviderModelOptions],
236240
);
237241
const {
238242
descriptors,
@@ -343,6 +347,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
343347

344348
export const TraitsPicker = memo(function TraitsPicker({
345349
provider,
350+
instanceId,
346351
models,
347352
model,
348353
prompt,
@@ -431,6 +436,7 @@ export const TraitsPicker = memo(function TraitsPicker({
431436
<MenuPopup align="start">
432437
<TraitsMenuContent
433438
provider={provider}
439+
{...(instanceId ? { instanceId } : {})}
434440
models={models}
435441
model={model}
436442
prompt={prompt}

apps/web/src/components/chat/composerProviderState.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
type ProviderDriverKind,
3+
type ProviderInstanceId,
34
type ProviderOptionSelection,
45
type ScopedThreadRef,
56
type ServerProviderModel,
@@ -35,6 +36,7 @@ export type ComposerProviderState = {
3536

3637
type TraitsRenderInput = {
3738
provider: ProviderDriverKind;
39+
instanceId?: ProviderInstanceId;
3840
threadRef?: ScopedThreadRef;
3941
draftId?: DraftId;
4042
model: string;
@@ -76,8 +78,17 @@ function renderTraitsControl(
7678
Component: typeof TraitsMenuContent | typeof TraitsPicker,
7779
input: TraitsRenderInput,
7880
): ReactNode {
79-
const { provider, threadRef, draftId, model, models, modelOptions, prompt, onPromptChange } =
80-
input;
81+
const {
82+
provider,
83+
instanceId,
84+
threadRef,
85+
draftId,
86+
model,
87+
models,
88+
modelOptions,
89+
prompt,
90+
onPromptChange,
91+
} = input;
8192
const hasTarget = threadRef !== undefined || draftId !== undefined;
8293
if (
8394
!hasTarget ||
@@ -88,6 +99,7 @@ function renderTraitsControl(
8899
return (
89100
<Component
90101
provider={provider}
102+
{...(instanceId ? { instanceId } : {})}
91103
models={models}
92104
{...(threadRef ? { threadRef } : {})}
93105
{...(draftId ? { draftId } : {})}

apps/web/src/composerDraftStore.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { createModelSelection } from "@t3tools/shared/model";
2121
// `stickyModelSelectionByProvider` maps are keyed by `ProviderInstanceId`
2222
// in production; these aliases keep the legacy-key migration tests concise.
2323
const CODEX_INSTANCE = ProviderInstanceId.make("codex");
24+
const CODEX_SECONDARY_INSTANCE = ProviderInstanceId.make("codex_secondary");
2425
const CLAUDE_AGENT_INSTANCE = ProviderInstanceId.make("claudeAgent");
2526
const CURSOR_INSTANCE = ProviderInstanceId.make("cursor");
2627
const CODEX_DRIVER = ProviderDriverKind.make("codex");
@@ -1201,6 +1202,43 @@ describe("composerDraftStore modelSelection", () => {
12011202
);
12021203
});
12031204

1205+
it("stores provider option changes on a selected custom instance", () => {
1206+
const store = useComposerDraftStore.getState();
1207+
1208+
store.setProviderModelOptions(
1209+
threadRef,
1210+
CODEX_DRIVER,
1211+
toSelections({ reasoningEffort: "low" }),
1212+
{
1213+
instanceId: CODEX_SECONDARY_INSTANCE,
1214+
model: "gpt-5-codex",
1215+
persistSticky: true,
1216+
},
1217+
);
1218+
1219+
expect(
1220+
draftFor(threadId, TEST_ENVIRONMENT_ID)?.modelSelectionByProvider[CODEX_SECONDARY_INSTANCE],
1221+
).toEqual(
1222+
expect.objectContaining({
1223+
instanceId: CODEX_SECONDARY_INSTANCE,
1224+
options: [{ id: "reasoningEffort", value: "low" }],
1225+
}),
1226+
);
1227+
expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.activeProvider).toBe(CODEX_SECONDARY_INSTANCE);
1228+
expect(useComposerDraftStore.getState().stickyActiveProvider).toBe(CODEX_SECONDARY_INSTANCE);
1229+
expect(useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_INSTANCE]).toBe(
1230+
undefined,
1231+
);
1232+
expect(
1233+
useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_SECONDARY_INSTANCE],
1234+
).toEqual(
1235+
expect.objectContaining({
1236+
instanceId: CODEX_SECONDARY_INSTANCE,
1237+
options: [{ id: "reasoningEffort", value: "low" }],
1238+
}),
1239+
);
1240+
});
1241+
12041242
it("updates only the draft when sticky persistence is disabled", () => {
12051243
const store = useComposerDraftStore.getState();
12061244

apps/web/src/composerDraftStore.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ interface ComposerDraftStoreState {
371371
provider: ProviderDriverKind,
372372
nextProviderOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined,
373373
options?: {
374+
instanceId?: ProviderInstanceId | null | undefined;
374375
model?: string | null | undefined;
375376
persistSticky?: boolean;
376377
},
@@ -2456,7 +2457,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
24562457
if (normalizedProvider === null) {
24572458
return;
24582459
}
2459-
const instanceKey = defaultInstanceIdForDriver(normalizedProvider);
2460+
const instanceKey = options?.instanceId ?? defaultInstanceIdForDriver(normalizedProvider);
24602461
const fallbackModel =
24612462
normalizeModelSlug(options?.model, normalizedProvider) ??
24622463
DEFAULT_MODEL_BY_PROVIDER[normalizedProvider] ??
@@ -2501,7 +2502,9 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
25012502
const { options: _, ...rest } = stickyBase;
25022503
nextStickyMap[instanceKey] = rest as ModelSelection;
25032504
}
2504-
nextStickyActiveProvider = base.activeProvider ?? instanceKey;
2505+
nextStickyActiveProvider = options.instanceId
2506+
? instanceKey
2507+
: (base.activeProvider ?? instanceKey);
25052508
}
25062509

25072510
if (
@@ -2514,6 +2517,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
25142517

25152518
const nextDraft: ComposerThreadDraftState = {
25162519
...base,
2520+
...(options?.instanceId ? { activeProvider: instanceKey } : {}),
25172521
modelSelectionByProvider: nextMap,
25182522
};
25192523
const nextDraftsByThreadKey = { ...state.draftsByThreadKey };

docs/custom-alpha-workflow.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,13 @@ git fetch upstream
195195
git merge --ff-only upstream/main
196196
```
197197

198+
Before accepting upstream removals or resolving conflicts, verify the protected fork features still work:
199+
200+
1. Multi-provider runtime support for Codex CLI, Claude Code, Cursor, Droid, OpenCode, Amp, Copilot, Gemini CLI, and Kilo.
201+
2. Usage and limit monitoring for token/context usage, provider usage events, and account rate-limit UI.
202+
3. Provider management UX for custom instances, per-instance config/environment/model state, and provider-scoped traits.
203+
4. Provider-neutral orchestration reliability for persistence, replay/live ordering, reconnects, restarts, and projections.
204+
198205
Then refresh the alpha branch:
199206

200207
```bash

0 commit comments

Comments
 (0)