Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3d6b9af
feat(web): restore user prompt to composer input on revert
Mar 21, 2026
8399fcf
fix: only restore prompt to composer on successful revert
Mar 21, 2026
2b6640a
fix(claude): load Claude SDK filesystem setting sources (#1334)
harshit97 Mar 23, 2026
754cded
Load PTY adapter at runtime (#1311)
shivamhwp Mar 23, 2026
843d6d8
fix: add license field to npm package (#1272)
gameroman Mar 23, 2026
d415558
Flatten Git service layer and switch server paths to base dir (#1255)
juliusmarminge Mar 24, 2026
e11fb6e
fix(web): avoid false draft attachment persistence warnings (#1153)
shivamhwp Mar 24, 2026
2b08d86
Add resizable chat sidebar (#1347)
shivamhwp Mar 24, 2026
e823f8f
ci(github): exclude test files from mixed PR size calculation (#1105)
binbandit Mar 24, 2026
d6408a2
refactor(settings): simplify settings layout and controls (#1288)
maria-rcks Mar 24, 2026
1371ce2
Run draft-thread project scripts from resolved project/worktree cwd (…
juliusmarminge Mar 24, 2026
384f350
Stream git hook progress events for stacked git actions (#1214)
juliusmarminge Mar 24, 2026
be1abc8
feat: add terminal toggle button to chat header (#633)
antiisaint Mar 24, 2026
d4a7e1a
feat: add word wrapping setting and in-panel button (#1326)
raphaelluethy Mar 24, 2026
0a503d0
fix(provider,claude): handle prompt stream interrupt on session stop …
keyzou Mar 24, 2026
529315b
Preserve terminal history across ANSI control sequences (#1367)
juliusmarminge Mar 24, 2026
8f35155
Surface context window usage in the UI (#1351)
juliusmarminge Mar 24, 2026
28afb14
Sort sidebar projects and threads by recency (#1372)
juliusmarminge Mar 24, 2026
2a435ae
chore(release): prepare v0.0.14
t3-code[bot] Mar 24, 2026
56d13fa
Preserve git action progress until HTTP completion (#1388)
juliusmarminge Mar 24, 2026
a542a3b
Persist provider-aware model selections (#1371)
juliusmarminge Mar 25, 2026
bf71e0b
Pick sidebar fallback thread after delete (#1395)
juliusmarminge Mar 25, 2026
b3bca04
fix(security): send sensitive config over a bootstrap fd (#1398)
juliusmarminge Mar 25, 2026
df5bde7
docs: properly linked CLAUDE.md to AGENTS.md (#1328)
jakob1379 Mar 25, 2026
3289a25
fix(web): prevent provider model submenu overlap (#1403)
ifBars Mar 25, 2026
f10ab6f
Merge branch 'main' into revert-prompt-restore-clean
MaximIJ Mar 25, 2026
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
75 changes: 64 additions & 11 deletions .github/workflows/pr-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,32 @@ jobs:
{
name: "size:XS",
color: "0e8a16",
description: "0-9 changed lines (additions + deletions).",
description: "0-9 effective changed lines (test files excluded in mixed PRs).",
},
{
name: "size:S",
color: "5ebd3e",
description: "10-29 changed lines (additions + deletions).",
description: "10-29 effective changed lines (test files excluded in mixed PRs).",
},
{
name: "size:M",
color: "fbca04",
description: "30-99 changed lines (additions + deletions).",
description: "30-99 effective changed lines (test files excluded in mixed PRs).",
},
{
name: "size:L",
color: "fe7d37",
description: "100-499 changed lines (additions + deletions).",
description: "100-499 effective changed lines (test files excluded in mixed PRs).",
},
{
name: "size:XL",
color: "d93f0b",
description: "500-999 changed lines (additions + deletions).",
description: "500-999 effective changed lines (test files excluded in mixed PRs).",
},
{
name: "size:XXL",
color: "b60205",
description: "1,000+ changed lines (additions + deletions).",
description: "1,000+ effective changed lines (test files excluded in mixed PRs).",
},
];

Expand Down Expand Up @@ -131,12 +131,18 @@ jobs:
with:
script: |
const issueNumber = context.payload.pull_request.number;
const additions = context.payload.pull_request.additions ?? 0;
const deletions = context.payload.pull_request.deletions ?? 0;
const changedLines = additions + deletions;
const managedLabels = JSON.parse(process.env.PR_SIZE_LABELS_JSON ?? "[]");

const managedLabelNames = new Set(managedLabels.map((label) => label.name));
// Keep this aligned with the repo's test entrypoints and test-only support files.
const testFilePatterns = [
/(^|\/)__tests__(\/|$)/,
/(^|\/)tests?(\/|$)/,
/^apps\/server\/integration\//,
/\.(test|spec|browser|integration)\.[^.\/]+$/,
];

const isTestFile = (filename) =>
testFilePatterns.some((pattern) => pattern.test(filename));

const resolveSizeLabel = (totalChangedLines) => {
if (totalChangedLines < 10) {
Expand All @@ -162,6 +168,42 @@ jobs:
return "size:XXL";
};

const files = await github.paginate(
github.rest.pulls.listFiles,
{
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: issueNumber,
per_page: 100,
},
(response) => response.data,
);

if (files.length >= 3000) {
core.warning(
"The GitHub pull request files API may truncate results at 3,000 files; PR size may be undercounted.",
);
}

let testChangedLines = 0;
let nonTestChangedLines = 0;

for (const file of files) {
const changedLinesForFile = (file.additions ?? 0) + (file.deletions ?? 0);

if (changedLinesForFile === 0) {
continue;
}

if (isTestFile(file.filename)) {
testChangedLines += changedLinesForFile;
continue;
}

nonTestChangedLines += changedLinesForFile;
}

const changedLines = nonTestChangedLines === 0 ? testChangedLines : nonTestChangedLines;
const nextLabelName = resolveSizeLabel(changedLines);

const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
Expand Down Expand Up @@ -199,4 +241,15 @@ jobs:
});
}

core.info(`PR #${issueNumber}: ${changedLines} changed lines -> ${nextLabelName}`);
const classification =
nonTestChangedLines === 0
? testChangedLines > 0
? "test-only PR"
: "no line changes"
: testChangedLines > 0
? "test lines excluded"
: "all non-test changes";

core.info(
`PR #${issueNumber}: ${nonTestChangedLines} non-test lines, ${testChangedLines} test lines, ${changedLines} effective lines -> ${nextLabelName} (${classification})`,
);
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ apps/web/.playwright
apps/web/playwright-report
apps/web/src/components/__screenshots__
.vitest-*
__screenshots__/
__screenshots__/
.tanstack
2 changes: 1 addition & 1 deletion CLAUDE.md
21 changes: 12 additions & 9 deletions REMOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,24 @@ Use this when you want to open T3 Code from another device (phone, tablet, anoth

The T3 Code CLI accepts the following configuration options, available either as CLI flags or environment variables:

| CLI flag | Env var | Notes |
| ----------------------- | --------------------- | ---------------------------------- |
| `--mode <web\|desktop>` | `T3CODE_MODE` | Runtime mode. |
| `--port <number>` | `T3CODE_PORT` | HTTP/WebSocket port. |
| `--host <address>` | `T3CODE_HOST` | Bind interface/address. |
| `--base-dir <path>` | `T3CODE_HOME` | Base directory. |
| `--dev-url <url>` | `VITE_DEV_SERVER_URL` | Dev web URL redirect/proxy target. |
| `--no-browser` | `T3CODE_NO_BROWSER` | Disable auto-open browser. |
| `--auth-token <token>` | `T3CODE_AUTH_TOKEN` | WebSocket auth token. |
| CLI flag | Env var | Notes |
| ----------------------- | --------------------- | ------------------------------------------------------------------------------------ |
| `--mode <web\|desktop>` | `T3CODE_MODE` | Runtime mode. |
| `--port <number>` | `T3CODE_PORT` | HTTP/WebSocket port. |
| `--host <address>` | `T3CODE_HOST` | Bind interface/address. |
| `--base-dir <path>` | `T3CODE_HOME` | Base directory. |
| `--dev-url <url>` | `VITE_DEV_SERVER_URL` | Dev web URL redirect/proxy target. |
| `--no-browser` | `T3CODE_NO_BROWSER` | Disable auto-open browser. |
| `--auth-token <token>` | `T3CODE_AUTH_TOKEN` | WebSocket auth token. Use this for standard CLI and remote-server flows. |
| `--bootstrap-fd <fd>` | `T3CODE_BOOTSTRAP_FD` | Read a one-shot bootstrap envelope from an inherited file descriptor during startup. |

> TIP: Use the `--help` flag to see all available options and their descriptions.

## Security First

- Always set `--auth-token` before exposing the server outside localhost.
- When you control the process launcher, prefer sending the auth token in a JSON envelope via `--bootstrap-fd <fd>`.
With `--bootstrap-fd <fd>`, the launcher starts the server first, then sends a one-shot JSON envelope over the inherited file descriptor. This allows the auth token to be delivered without putting it in process environment or command line arguments.
- Treat the token like a password.
- Prefer binding to trusted interfaces (LAN IP or Tailnet IP) instead of opening all interfaces unless needed.

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/desktop",
"version": "0.0.13",
"version": "0.0.14",
"private": true,
"main": "dist-electron/main.js",
"scripts": {
Expand Down
59 changes: 42 additions & 17 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state";
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
const GET_WS_URL_CHANNEL = "desktop:get-ws-url";
const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
const STATE_DIR = Path.join(BASE_DIR, "userdata");
const DESKTOP_SCHEME = "t3";
Expand Down Expand Up @@ -113,6 +114,17 @@ function sanitizeLogValue(value: string): string {
return value.replace(/\s+/g, " ").trim();
}

function backendChildEnv(): NodeJS.ProcessEnv {
const env = { ...process.env };
delete env.T3CODE_PORT;
delete env.T3CODE_AUTH_TOKEN;
delete env.T3CODE_MODE;
delete env.T3CODE_NO_BROWSER;
delete env.T3CODE_HOST;
delete env.T3CODE_DESKTOP_WS_URL;
return env;
}

function writeDesktopLogHeader(message: string): void {
if (!desktopLogSink) return;
desktopLogSink.write(`[${logTimestamp()}] [${logScope("desktop")}] ${message}\n`);
Expand Down Expand Up @@ -918,17 +930,6 @@ function configureAutoUpdater(): void {
}, AUTO_UPDATE_POLL_INTERVAL_MS);
updatePollTimer.unref();
}
function backendEnv(): NodeJS.ProcessEnv {
return {
...process.env,
T3CODE_MODE: "desktop",
T3CODE_NO_BROWSER: "1",
T3CODE_PORT: String(backendPort),
T3CODE_HOME: BASE_DIR,
T3CODE_AUTH_TOKEN: backendAuthToken,
};
}

function scheduleBackendRestart(reason: string): void {
if (isQuitting || restartTimer) return;

Expand All @@ -952,16 +953,35 @@ function startBackend(): void {
}

const captureBackendLogs = app.isPackaged && backendLogSink !== null;
const child = ChildProcess.spawn(process.execPath, [backendEntry], {
const child = ChildProcess.spawn(process.execPath, [backendEntry, "--bootstrap-fd", "3"], {
cwd: resolveBackendCwd(),
// In Electron main, process.execPath points to the Electron binary.
// Run the child in Node mode so this backend process does not become a GUI app instance.
env: {
...backendEnv(),
...backendChildEnv(),
ELECTRON_RUN_AS_NODE: "1",
},
stdio: captureBackendLogs ? ["ignore", "pipe", "pipe"] : "inherit",
stdio: captureBackendLogs
? ["ignore", "pipe", "pipe", "pipe"]
: ["ignore", "inherit", "inherit", "pipe"],
});
const bootstrapStream = child.stdio[3];
if (bootstrapStream && "write" in bootstrapStream) {
bootstrapStream.write(
`${JSON.stringify({
mode: "desktop",
noBrowser: true,
port: backendPort,
t3Home: BASE_DIR,
authToken: backendAuthToken,
})}\n`,
);
bootstrapStream.end();
} else {
child.kill("SIGTERM");
scheduleBackendRestart("missing desktop bootstrap pipe");
return;
}
backendProcess = child;
let backendSessionClosed = false;
const closeBackendSession = (details: string) => {
Expand Down Expand Up @@ -1072,6 +1092,11 @@ async function stopBackendAndWaitForExit(timeoutMs = 5_000): Promise<void> {
}

function registerIpcHandlers(): void {
ipcMain.removeAllListeners(GET_WS_URL_CHANNEL);
ipcMain.on(GET_WS_URL_CHANNEL, (event) => {
event.returnValue = backendWsUrl;
});

ipcMain.removeHandler(PICK_FOLDER_CHANNEL);
ipcMain.handle(PICK_FOLDER_CHANNEL, async () => {
const owner = BrowserWindow.getFocusedWindow() ?? mainWindow;
Expand Down Expand Up @@ -1320,9 +1345,9 @@ async function bootstrap(): Promise<void> {
);
writeDesktopLogHeader(`reserved backend port via NetService port=${backendPort}`);
backendAuthToken = Crypto.randomBytes(24).toString("hex");
backendWsUrl = `ws://127.0.0.1:${backendPort}/?token=${encodeURIComponent(backendAuthToken)}`;
process.env.T3CODE_DESKTOP_WS_URL = backendWsUrl;
writeDesktopLogHeader(`bootstrap resolved websocket url=${backendWsUrl}`);
const baseUrl = `ws://127.0.0.1:${backendPort}`;
backendWsUrl = `${baseUrl}/?token=${encodeURIComponent(backendAuthToken)}`;
writeDesktopLogHeader(`bootstrap resolved websocket endpoint baseUrl=${baseUrl}`);

registerIpcHandlers();
writeDesktopLogHeader("bootstrap ipc handlers registered");
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state";
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
const wsUrl = process.env.T3CODE_DESKTOP_WS_URL ?? null;
const GET_WS_URL_CHANNEL = "desktop:get-ws-url";

contextBridge.exposeInMainWorld("desktopBridge", {
getWsUrl: () => wsUrl,
getWsUrl: () => {
const result = ipcRenderer.sendSync(GET_WS_URL_CHANNEL);
return typeof result === "string" ? result : null;
},
pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL),
confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message),
setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {

import { CheckpointStoreLive } from "../src/checkpointing/Layers/CheckpointStore.ts";
import { CheckpointStore } from "../src/checkpointing/Services/CheckpointStore.ts";
import { GitCoreLive } from "../src/git/Layers/GitCore.ts";
import { GitCore, type GitCoreShape } from "../src/git/Services/GitCore.ts";
import { TextGeneration, type TextGenerationShape } from "../src/git/Services/TextGeneration.ts";
import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/Layers/OrchestrationCommandReceipts.ts";
Expand Down Expand Up @@ -284,12 +285,13 @@ export const makeOrchestrationIntegrationHarness = (
Layer.provide(AnalyticsService.layerTest),
);

const checkpointStoreLayer = CheckpointStoreLive.pipe(Layer.provide(GitCoreLive));
const runtimeServicesLayer = Layer.mergeAll(
orchestrationLayer,
OrchestrationProjectionSnapshotQueryLive,
ProjectionCheckpointRepositoryLive,
ProjectionPendingApprovalRepositoryLive,
CheckpointStoreLive,
checkpointStoreLayer,
providerLayer,
RuntimeReceiptBusLive,
);
Expand Down
Loading