Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d40f774
chore: add sandbox dev runner
YoruSamaOG Jun 19, 2026
907c86b
docs: plan harness enhancements
YoruSamaOG Jun 19, 2026
e060c29
fix(desktop): ignore broken pipe logger writes
YoruSamaOG Jun 19, 2026
efeb461
fix(workspace): preserve restored worktree identity
YoruSamaOG Jun 19, 2026
a2c8ea4
feat(web): group chats by workspace
YoruSamaOG Jun 19, 2026
507b1a6
fix(web): clear workspace context for new project chats
YoruSamaOG Jun 19, 2026
4870a49
docs: sync workspace chat behavior
YoruSamaOG Jun 19, 2026
822c03e
feat(web): describe chat keybindings
YoruSamaOG Jun 19, 2026
f148276
docs: mark workspace compatibility complete
YoruSamaOG Jun 20, 2026
e30db97
docs: add durable workspace persistence task
YoruSamaOG Jun 20, 2026
457e7c4
Merge pull request #1 from h4rzx/t3code/faca466b
YoruSamaOG Jun 20, 2026
bbe5114
docs: track source-control PR target resolution
YoruSamaOG Jun 20, 2026
a9d9aa1
Merge remote-tracking branch 'origin/t3code/faca466b'
YoruSamaOG Jun 21, 2026
a4d8ae2
Merge upstream main
YoruSamaOG Jun 21, 2026
5e92010
Format ChatView merge resolution
YoruSamaOG Jun 21, 2026
8f17d31
chore: add upstream sync tooling
YoruSamaOG Jun 21, 2026
9aba964
Merge remote-tracking branch 'upstream/main'
YoruSamaOG Jun 21, 2026
6186d75
docs: update harness sync baseline
YoruSamaOG Jun 21, 2026
152e75f
fix(workspaces): persist durable workspace identity
YoruSamaOG Jun 22, 2026
9731579
docs(harness): align workspace row behavior
YoruSamaOG Jun 22, 2026
dd93610
Merge pull request #2 from h4rzx/t3code/c78d602a
YoruSamaOG Jun 22, 2026
9f29894
Merge remote-tracking branch 'upstream/main'
YoruSamaOG Jun 22, 2026
18517c8
docs: align harness plan with workspace foundation
YoruSamaOG Jun 22, 2026
baf55e2
docs: add fork release policy
YoruSamaOG Jun 23, 2026
c7f63e3
feat(server): add workspace context harness
YoruSamaOG Jul 20, 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
95 changes: 95 additions & 0 deletions .github/workflows/upstream-sync-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Upstream Sync Check

on:
schedule:
# Four times a day. The job only opens/updates an issue; it does not merge.
- cron: "0 */6 * * *"
workflow_dispatch:

permissions:
contents: read
issues: write

jobs:
check:
name: Check upstream drift
runs-on: ubuntu-latest
steps:
- name: Checkout fork
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Fetch upstream
run: |
git remote add upstream https://github.com/pingdotgg/t3code.git || git remote set-url upstream https://github.com/pingdotgg/t3code.git
git fetch --no-tags upstream main

- name: Compute drift
id: drift
run: |
set -euo pipefail
read -r ahead behind < <(git rev-list --left-right --count HEAD...upstream/main)
upstream_sha="$(git rev-parse upstream/main)"
head_sha="$(git rev-parse HEAD)"
echo "ahead=$ahead" >> "$GITHUB_OUTPUT"
echo "behind=$behind" >> "$GITHUB_OUTPUT"
echo "upstream_sha=$upstream_sha" >> "$GITHUB_OUTPUT"
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
git log --oneline --max-count=20 HEAD..upstream/main > /tmp/upstream-commits.txt

- name: Open or update sync issue
if: steps.drift.outputs.behind != '0'
uses: actions/github-script@v8
with:
script: |
const fs = require("node:fs");
const owner = context.repo.owner;
const repo = context.repo.repo;
const title = "Sync fork with upstream";
const ahead = "${{ steps.drift.outputs.ahead }}";
const behind = "${{ steps.drift.outputs.behind }}";
const upstreamSha = "${{ steps.drift.outputs.upstream_sha }}";
const headSha = "${{ steps.drift.outputs.head_sha }}";
const commits = fs.readFileSync("/tmp/upstream-commits.txt", "utf8").trim();
const body = [
`The fork is currently ${ahead} commits ahead of and ${behind} commits behind \`pingdotgg/t3code:main\`.`,
"",
`Fork head: \`${headSha}\``,
`Upstream head: \`${upstreamSha}\``,
"",
"Run the local sync command from a clean checkout:",
"",
"```bash",
"pnpm run sync:upstream",
"```",
"",
"Recent upstream commits:",
"",
commits ? `\`\`\`text\n${commits}\n\`\`\`` : "_No commit list available._",
"",
"This scheduled workflow only reports drift. It does not auto-merge because upstream syncs can conflict with local harness work.",
].join("\n");

const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: "open",
per_page: 100,
});
const existing = issues.find((issue) => issue.title === title && !issue.pull_request);
if (existing) {
await github.rest.issues.update({
owner,
repo,
issue_number: existing.number,
body,
});
} else {
await github.rest.issues.create({
owner,
repo,
title,
body,
});
}
51 changes: 37 additions & 14 deletions apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@ await waitForResources({
tcpPort: port,
});

const childEnv = { ...process.env };
delete childEnv.ELECTRON_RUN_AS_NODE;
const devProtocolClient = resolveDevProtocolClient();
if (devProtocolClient) {
childEnv.T3CODE_DESKTOP_APP_USER_MODEL_ID = devProtocolClient.appBundleId;
childEnv.T3CODE_DESKTOP_PROTOCOL_REGISTRATION_MANAGED = "1";
}

let shuttingDown = false;
let restartTimer = null;
let currentApp = null;
Expand All @@ -75,6 +67,37 @@ function cleanupStaleDevApps() {
NodeChildProcess.spawnSync("pkill", ["-f", "--", `--t3code-dev-root=${desktopDir}`], {
stdio: "ignore",
});
NodeChildProcess.spawnSync(
"pkill",
["-f", "--", `${NodePath.join(desktopDir, ".electron-runtime")}/T3 Code (Dev).app`],
{
stdio: "ignore",
},
);
}

function isShellScript(path) {
try {
const buffer = Buffer.alloc(2);
const fd = NodeFS.openSync(path, "r");
try {
NodeFS.readSync(fd, buffer, 0, buffer.length, 0);
return buffer[0] === 0x23 && buffer[1] === 0x21;
} finally {
NodeFS.closeSync(fd);
}
} catch {
return false;
}
}

cleanupStaleDevApps();
const childEnv = { ...process.env };
delete childEnv.ELECTRON_RUN_AS_NODE;
const devProtocolClient = resolveDevProtocolClient();
if (devProtocolClient) {
childEnv.T3CODE_DESKTOP_APP_USER_MODEL_ID = devProtocolClient.appBundleId;
childEnv.T3CODE_DESKTOP_PROTOCOL_REGISTRATION_MANAGED = "1";
}

function startApp() {
Expand All @@ -85,11 +108,12 @@ function startApp() {
const electronArgs = remoteDebuggingPort
? [`--remote-debugging-port=${remoteDebuggingPort}`]
: [];
const launchArgs = devProtocolClient
? electronArgs
: [...electronArgs, `--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"];
const electronCommand = resolveElectronLaunchCommand(launchArgs);
const app = NodeChildProcess.spawn(electronCommand.electronPath, electronCommand.args, {
const electronCommand = resolveElectronLaunchCommand(electronArgs);
const launchArgs =
devProtocolClient && isShellScript(electronCommand.electronPath)
? electronCommand.args
: [...electronCommand.args, `--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"];
const app = NodeChildProcess.spawn(electronCommand.electronPath, launchArgs, {
cwd: desktopDir,
env: childEnv,
stdio: "inherit",
Expand Down Expand Up @@ -233,7 +257,6 @@ async function shutdown(exitCode) {
}

startWatchers();
cleanupStaleDevApps();
startApp();

process.once("SIGINT", () => {
Expand Down
15 changes: 11 additions & 4 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const APP_BUNDLE_ID = isDevelopment
? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
: "com.t3tools.t3code";
const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
const LAUNCHER_VERSION = 12;
const LAUNCHER_VERSION = 13;
const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
const developmentMacIconPngPath = NodePath.join(
repoRoot,
Expand Down Expand Up @@ -100,9 +100,8 @@ function shellSingleQuote(value) {
return `'${value.replaceAll("'", "'\\''")}'`;
}

function writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath) {
const mainEntryPath = NodePath.join(desktopDir, "dist-electron", "main.cjs");
const envEntries = [
function resolveDevelopmentLauncherEnvEntries() {
return [
["VITE_DEV_SERVER_URL", process.env.VITE_DEV_SERVER_URL],
["T3CODE_PORT", process.env.T3CODE_PORT],
["T3CODE_HOME", process.env.T3CODE_HOME],
Expand All @@ -111,6 +110,11 @@ function writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath) {
["T3CODE_OTLP_EXPORT_INTERVAL_MS", process.env.T3CODE_OTLP_EXPORT_INTERVAL_MS],
["T3CODE_DESKTOP_APP_USER_MODEL_ID", APP_BUNDLE_ID],
].filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0);
}

function writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath) {
const mainEntryPath = NodePath.join(desktopDir, "dist-electron", "main.cjs");
const envEntries = resolveDevelopmentLauncherEnvEntries();
NodeFS.writeFileSync(
targetBinaryPath,
[
Expand Down Expand Up @@ -278,6 +282,9 @@ function buildMacLauncher(electronBinaryPath) {
iconMtimeMs: NodeFS.statSync(iconPath).mtimeMs,
appBundleId: APP_BUNDLE_ID,
appProtocolSchemes: APP_PROTOCOL_SCHEMES,
...(isDevelopment
? { launcherEnvEntries: Object.fromEntries(resolveDevelopmentLauncherEnvEntries()) }
: {}),
};

const currentMetadata = readJson(metadataPath);
Expand Down
15 changes: 11 additions & 4 deletions apps/desktop/src/app/DesktopBackendOutputLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import * as Semaphore from "effect/Semaphore";

import { ensureConsoleStreamGuard, isIgnorableConsoleStreamError } from "./DesktopConsole.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";

export const DESKTOP_LOG_FILE_MAX_BYTES = 10 * 1024 * 1024;
Expand Down Expand Up @@ -267,12 +268,18 @@ const writeDevelopmentConsoleOutput = (
streamName: "stdout" | "stderr",
chunk: Uint8Array,
): Effect.Effect<void> =>
Effect.try({
try: () => {
Effect.suspend(() => {
try {
const output = streamName === "stderr" ? process.stderr : process.stdout;
ensureConsoleStreamGuard(output);
if (!output.writable || output.destroyed || output.writableEnded) return Effect.void;
output.write(chunk);
},
catch: (cause) => new DesktopBackendConsoleWriteError({ streamName, cause }),
return Effect.void;
} catch (cause) {
return isIgnorableConsoleStreamError(cause)
? Effect.void
: Effect.fail(new DesktopBackendConsoleWriteError({ streamName, cause }));
}
}).pipe(
Effect.catchTags({
DesktopBackendConsoleWriteError: (error) => Effect.logError(error.message, { error }),
Expand Down
39 changes: 39 additions & 0 deletions apps/desktop/src/app/DesktopConsole.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { assert, describe, it } from "@effect/vitest";

import "./DesktopConsole.ts";

describe("DesktopConsole", () => {
it("ignores EPIPE thrown by console stdout writes", () => {
const originalWrite = process.stdout.write;
process.stdout.write = function () {
throw Object.assign(new Error("write EPIPE"), { code: "EPIPE" });
} as typeof process.stdout.write;

try {
const guardedLog: (...data: Array<unknown>) => void = console["log"].bind(console);
assert.doesNotThrow(() => guardedLog("ignored broken stdout"));
} finally {
process.stdout.write = originalWrite;
}
});

it("ignores EPIPE emitted by console stdout writes", async () => {
const originalWrite = process.stdout.write;
process.stdout.write = function (...args: Parameters<typeof process.stdout.write>) {
const error = Object.assign(new Error("write EPIPE"), { code: "EPIPE" });
queueMicrotask(() => process.stdout.emit("error", error));
for (const arg of args) {
if (typeof arg === "function") arg();
}
return false;
} as typeof process.stdout.write;

try {
const guardedLog: (...data: Array<unknown>) => void = console["log"].bind(console);
guardedLog("ignored broken stdout");
await new Promise<void>((resolve) => setImmediate(resolve));
} finally {
process.stdout.write = originalWrite;
}
});
});
40 changes: 40 additions & 0 deletions apps/desktop/src/app/DesktopConsole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const guardedConsoleStreams = new WeakSet<NodeJS.WriteStream>();

export function isIgnorableConsoleStreamError(cause: unknown): boolean {
if (!(cause instanceof Error)) return false;
const errorCode = "code" in cause && typeof cause.code === "string" ? cause.code : undefined;
return errorCode === "EPIPE" || errorCode === "ERR_STREAM_DESTROYED";
}

export function ensureConsoleStreamGuard(output: NodeJS.WriteStream): void {
if (guardedConsoleStreams.has(output)) return;
guardedConsoleStreams.add(output);
output.on("error", (cause) => {
if (isIgnorableConsoleStreamError(cause)) return;
throw cause;
});
}

function guardConsoleMethod<T extends (...args: Array<unknown>) => void>(method: T): T {
return ((...args: Parameters<T>) => {
try {
method(...args);
} catch (cause) {
if (!isIgnorableConsoleStreamError(cause)) {
throw cause;
}
}
}) as T;
}

export function installDesktopConsoleGuards(): void {
ensureConsoleStreamGuard(process.stdout);
ensureConsoleStreamGuard(process.stderr);
console.log = guardConsoleMethod(console.log.bind(console));
console.info = guardConsoleMethod(console.info.bind(console));
console.warn = guardConsoleMethod(console.warn.bind(console));
console.error = guardConsoleMethod(console.error.bind(console));
console.debug = guardConsoleMethod(console.debug.bind(console));
}

installDesktopConsoleGuards();
48 changes: 48 additions & 0 deletions apps/desktop/src/app/DesktopObservability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,52 @@ describe("DesktopObservability", () => {
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
),
);

it.effect("ignores a broken development console pipe while persisting backend child output", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-backend-output-epipe-test-",
});
const environmentLayer = makeEnvironmentLayer(baseDir);
const logPath = yield* Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
return environment.path.join(environment.logDir, "server-child.log");
}).pipe(Effect.provide(environmentLayer));

const originalWrite = process.stdout.write;
process.stdout.write = function (...args: Parameters<typeof process.stdout.write>) {
const error = Object.assign(new Error("write EPIPE"), { code: "EPIPE" });
queueMicrotask(() => process.stdout.emit("error", error));
for (const arg of args) {
if (typeof arg === "function") arg();
}
return false;
} as typeof process.stdout.write;

try {
yield* Effect.gen(function* () {
const outputLog = yield* DesktopObservability.DesktopBackendOutputLog;
yield* outputLog.writeOutputChunk("stdout", new TextEncoder().encode("hello server\n"));
yield* Effect.promise(() => new Promise<void>((resolve) => setImmediate(resolve)));
}).pipe(
Effect.annotateLogs({ runId: "test-run" }),
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
);
} finally {
process.stdout.write = originalWrite;
}

const log = yield* fileSystem.readFileString(logPath);
const lines = log.trimEnd().split("\n");
const output = yield* decodeDesktopBackendChildLogRecord(lines[0] ?? "");

assert.equal(output.message, "backend child process output");
assert.equal(output.annotations.stream, "stdout");
assert.equal(output.annotations.text, "hello server\n");
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
),
);
});
Loading
Loading