Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- `bun run dev:web` — Starts just the Vite dev server for the web app.
- Dev commands default `T3CODE_STATE_DIR` to `~/.t3/dev` to keep dev state isolated from desktop/prod state.
- Override server CLI-equivalent flags from root dev commands with `--`, for example:
`bun run dev -- --state-dir ~/.t3/another-dev-state`
`bun run dev -- --base-dir ~/.t3-2`
- `bun run start` — Runs the production server (serves built web app as static files).
- `bun run build` — Builds contracts, web app, and server through Turbo.
- `bun run typecheck` — Strict TypeScript checks for all packages.
Expand Down
2 changes: 1 addition & 1 deletion REMOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The T3 Code CLI accepts the following configuration options, available either as
| `--mode <web\|desktop>` | `T3CODE_MODE` | Runtime mode. |
| `--port <number>` | `T3CODE_PORT` | HTTP/WebSocket port. |
| `--host <address>` | `T3CODE_HOST` | Bind interface/address. |
| `--state-dir <path>` | `T3CODE_STATE_DIR` | State directory. |
| `--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. |
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ 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 STATE_DIR =
process.env.T3CODE_STATE_DIR?.trim() || Path.join(OS.homedir(), ".t3", "userdata");
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";
const ROOT_DIR = Path.resolve(__dirname, "../../..");
const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
Expand Down Expand Up @@ -924,7 +924,7 @@ function backendEnv(): NodeJS.ProcessEnv {
T3CODE_MODE: "desktop",
T3CODE_NO_BROWSER: "1",
T3CODE_PORT: String(backendPort),
T3CODE_STATE_DIR: STATE_DIR,
T3CODE_HOME: BASE_DIR,
T3CODE_AUTH_TOKEN: backendAuthToken,
};
}
Expand Down
48 changes: 24 additions & 24 deletions apps/server/integration/OrchestrationEngineHarness.integration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";

import * as NodeServices from "@effect/platform-node/NodeServices";
Expand All @@ -13,9 +10,11 @@ import {
import {
Effect,
Exit,
FileSystem,
Layer,
ManagedRuntime,
Option,
Path,
Ref,
Schedule,
Schema,
Expand Down Expand Up @@ -66,7 +65,7 @@ import {
makeTestProviderAdapterHarness,
type TestProviderAdapterHarness,
} from "./TestProviderAdapter.integration.ts";
import { ServerConfig } from "../src/config.ts";
import { deriveServerPaths, ServerConfig } from "../src/config.ts";

function runGit(cwd: string, args: ReadonlyArray<string>) {
return execFileSync("git", args, {
Expand All @@ -76,14 +75,16 @@ function runGit(cwd: string, args: ReadonlyArray<string>) {
});
}

function initializeGitWorkspace(cwd: string) {
const initializeGitWorkspace = Effect.fn(function* (cwd: string) {
runGit(cwd, ["init", "--initial-branch=main"]);
runGit(cwd, ["config", "user.email", "test@example.com"]);
runGit(cwd, ["config", "user.name", "Test User"]);
fs.writeFileSync(path.join(cwd, "README.md"), "v1\n", "utf8");
const fileSystem = yield* FileSystem.FileSystem;
const { join } = yield* Path.Path;
yield* fileSystem.writeFileString(join(cwd, "README.md"), "v1\n");
runGit(cwd, ["add", "."]);
runGit(cwd, ["commit", "-m", "Initial"]);
}
});

export function gitRefExists(cwd: string, ref: string): boolean {
try {
Expand Down Expand Up @@ -214,7 +215,9 @@ export const makeOrchestrationIntegrationHarness = (
options?: MakeOrchestrationIntegrationHarnessOptions,
) =>
Effect.gen(function* () {
const sleep = (ms: number) => Effect.sleep(ms);
const path = yield* Path.Path;
const fileSystem = yield* FileSystem.FileSystem;

const provider = options?.provider ?? "codex";
const useRealCodex = options?.realCodex === true;
const adapterHarness = useRealCodex
Expand All @@ -231,13 +234,16 @@ export const makeOrchestrationIntegrationHarness = (
listProviders: () => Effect.succeed([adapterHarness.provider]),
} as typeof ProviderAdapterRegistry.Service)
: null;
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-orchestration-integration-"));
const rootDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-orchestration-integration-",
});
const workspaceDir = path.join(rootDir, "workspace");
const stateDir = path.join(rootDir, "state");
const dbPath = path.join(stateDir, "state.sqlite");
fs.mkdirSync(workspaceDir, { recursive: true });
fs.mkdirSync(stateDir, { recursive: true });
initializeGitWorkspace(workspaceDir);
const { stateDir, dbPath } = yield* deriveServerPaths(rootDir, undefined).pipe(
Effect.provideService(Path.Path, path),
);
yield* fileSystem.makeDirectory(workspaceDir, { recursive: true });
yield* fileSystem.makeDirectory(stateDir, { recursive: true });
yield* initializeGitWorkspace(workspaceDir);

const persistenceLayer = makeSqlitePersistenceLive(dbPath);
const orchestrationLayer = OrchestrationEngineLive.pipe(
Expand All @@ -262,7 +268,7 @@ export const makeOrchestrationIntegrationHarness = (
}),
).pipe(
Layer.provide(makeCodexAdapterLive()),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, stateDir)),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)),
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(providerSessionDirectoryLayer),
);
Expand Down Expand Up @@ -312,7 +318,7 @@ export const makeOrchestrationIntegrationHarness = (
);
const layer = orchestrationReactorLayer.pipe(
Layer.provide(persistenceLayer),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, stateDir)),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)),
Layer.provideMerge(NodeServices.layer),
);

Expand Down Expand Up @@ -352,7 +358,7 @@ export const makeOrchestrationIntegrationHarness = (
yield* Stream.runForEach(runtimeReceiptBus.stream, (receipt) =>
Ref.update(receiptHistory, (history) => [...history, receipt]).pipe(Effect.asVoid),
).pipe(Effect.forkIn(scope));
yield* sleep(10);
yield* Effect.sleep(10);

const waitForThread: OrchestrationIntegrationHarness["waitForThread"] = (
threadId,
Expand Down Expand Up @@ -469,13 +475,7 @@ export const makeOrchestrationIntegrationHarness = (
}
});

yield* shutdown.pipe(
Effect.ensuring(
Effect.sync(() => {
fs.rmSync(rootDir, { recursive: true, force: true });
}),
),
);
yield* shutdown;
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
CheckpointDiffFinalizedReceipt,
TurnProcessingQuiescedReceipt,
} from "../src/orchestration/Services/RuntimeReceiptBus.ts";
import * as NodeServices from "@effect/platform-node/NodeServices";

const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value);
const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value);
Expand All @@ -51,8 +52,6 @@ class IntegrationWaitTimeoutError extends Schema.TaggedErrorClass<IntegrationWai
},
) {}

const sleep = (ms: number) => Effect.sleep(ms);

function waitForSync<A>(
read: () => A,
predicate: (value: A) => boolean,
Expand All @@ -70,7 +69,7 @@ function waitForSync<A>(
if (Date.now() >= deadline) {
return yield* Effect.die(new IntegrationWaitTimeoutError({ description }));
}
yield* sleep(10);
yield* Effect.sleep(10);
}
});
}
Expand All @@ -91,7 +90,7 @@ function withHarness<A, E>(
makeOrchestrationIntegrationHarness({ provider }),
use,
(harness) => harness.dispose,
);
).pipe(Effect.provide(NodeServices.layer));
}

function withRealCodexHarness<A, E>(
Expand All @@ -101,7 +100,7 @@ function withRealCodexHarness<A, E>(
makeOrchestrationIntegrationHarness({ provider: "codex", realCodex: true }),
use,
(harness) => harness.dispose,
);
).pipe(Effect.provide(NodeServices.layer));
}

const seedProjectAndThread = (harness: OrchestrationIntegrationHarness) =>
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/attachmentPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ export function normalizeAttachmentRelativePath(rawRelativePath: string): string
}

export function resolveAttachmentRelativePath(input: {
readonly stateDir: string;
readonly attachmentsDir: string;
readonly relativePath: string;
}): string | null {
const normalizedRelativePath = normalizeAttachmentRelativePath(input.relativePath);
if (!normalizedRelativePath) {
return null;
}

const attachmentsRoot = path.resolve(path.join(input.stateDir, "attachments"));
const attachmentsRoot = path.resolve(input.attachmentsDir);
const filePath = path.resolve(path.join(attachmentsRoot, normalizedRelativePath));
if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) {
return null;
Expand Down
14 changes: 6 additions & 8 deletions apps/server/src/attachmentStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,34 +44,32 @@ describe("attachmentStore", () => {
});

it("resolves attachment path by id using the extension that exists on disk", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-"));
const attachmentsDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-"));
try {
const attachmentId = "thread-1-attachment";
const attachmentsDir = path.join(stateDir, "attachments");
fs.mkdirSync(attachmentsDir, { recursive: true });
const pngPath = path.join(attachmentsDir, `${attachmentId}.png`);
fs.writeFileSync(pngPath, Buffer.from("hello"));

const resolved = resolveAttachmentPathById({
stateDir,
attachmentsDir,
attachmentId,
});
expect(resolved).toBe(pngPath);
} finally {
fs.rmSync(stateDir, { recursive: true, force: true });
fs.rmSync(attachmentsDir, { recursive: true, force: true });
}
});

it("returns null when no attachment file exists for the id", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-"));
const attachmentsDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-"));
try {
const resolved = resolveAttachmentPathById({
stateDir,
attachmentsDir,
attachmentId: "thread-1-missing",
});
expect(resolved).toBeNull();
} finally {
fs.rmSync(stateDir, { recursive: true, force: true });
fs.rmSync(attachmentsDir, { recursive: true, force: true });
}
});
});
8 changes: 4 additions & 4 deletions apps/server/src/attachmentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,17 @@ export function attachmentRelativePath(attachment: ChatAttachment): string {
}

export function resolveAttachmentPath(input: {
readonly stateDir: string;
readonly attachmentsDir: string;
readonly attachment: ChatAttachment;
}): string | null {
return resolveAttachmentRelativePath({
stateDir: input.stateDir,
attachmentsDir: input.attachmentsDir,
relativePath: attachmentRelativePath(input.attachment),
});
}

export function resolveAttachmentPathById(input: {
readonly stateDir: string;
readonly attachmentsDir: string;
readonly attachmentId: string;
}): string | null {
const normalizedId = normalizeAttachmentRelativePath(input.attachmentId);
Expand All @@ -85,7 +85,7 @@ export function resolveAttachmentPathById(input: {
}
for (const extension of ATTACHMENT_FILENAME_EXTENSIONS) {
const maybePath = resolveAttachmentRelativePath({
stateDir: input.stateDir,
attachmentsDir: input.attachmentsDir,
relativePath: `${normalizedId}${extension}`,
});
if (maybePath && existsSync(maybePath)) {
Expand Down
Loading