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 apps/server/src/cli/sshLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ function readDarwinProcessIdentity(pid: number): ProcessIdentity | undefined {
}
}

export function readProcessIdentity(pid: number): ProcessIdentity | undefined {
function readProcessIdentity(pid: number): ProcessIdentity | undefined {
if (!Number.isInteger(pid) || pid <= 0 || !processExists(pid)) return undefined;
const platform = HostProcessPlatform.defaultValue();
if (platform === "linux") return readLinuxProcessIdentity(pid);
Expand Down
9 changes: 5 additions & 4 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2359,9 +2359,10 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
Effect.asSome,
// Settling a thread the server never stored has nothing to settle. Accept
// it without events so a stale client shell does not surface an error.
Effect.catchTag("ProjectionStoreThreadNotFoundError", (error) =>
command.type === "thread.settle" ? Effect.succeedNone : Effect.fail(error),
),
Effect.catchTags({
ProjectionStoreThreadNotFoundError: (error) =>
command.type === "thread.settle" ? Effect.succeedNone : Effect.fail(error),
}),
Effect.mapError(
(cause) =>
new OrchestratorProjectionError({
Expand Down Expand Up @@ -10147,7 +10148,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
])
.pipe(
Effect.asSome,
Effect.catchTag("ProjectionStoreThreadNotFoundError", () => Effect.succeedNone),
Effect.catchTags({ ProjectionStoreThreadNotFoundError: () => Effect.succeedNone }),
Effect.mapError(
(cause) => new OrchestratorProjectionError({ threadId: command.threadId, cause }),
),
Expand Down
36 changes: 32 additions & 4 deletions apps/server/src/process/externalLauncher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as Fiber from "effect/Fiber";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import * as TestClock from "effect/testing/TestClock";
Expand Down Expand Up @@ -66,6 +67,30 @@ const layerTest = (input: {
readonly onUnref?: () => void;
readonly spawnResult?: (command: ChildProcess.StandardCommand) => MockSpawnResult | undefined;
}) => {
const layerFileSystem = Layer.effect(
FileSystem.FileSystem,
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
if (input.platform !== "darwin") return fs;
// Editor discovery must see fixture bundles rather than host applications.
return {
...fs,
stat: (file: string) =>
file.startsWith("/Applications/")
? Effect.fail(
PlatformError.systemError({
_tag: "NotFound",
module: "FileSystem",
method: "stat",
pathOrDescriptor: file,
}),
)
: fs.stat(file),
readDirectory: (directory: string) =>
directory === "/Applications" ? Effect.succeed([]) : fs.readDirectory(directory),
};
}),
);
const layerSpawner = Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) =>
Expand All @@ -84,7 +109,10 @@ const layerTest = (input: {
);

return Layer.mergeAll(
ExternalLauncher.layer.pipe(Layer.provide(Layer.merge(NodeServices.layer, layerSpawner))),
ExternalLauncher.layer.pipe(
Layer.provide(Layer.merge(layerFileSystem, layerSpawner)),
Layer.provide(NodeServices.layer),
),
Layer.succeed(HostProcessPlatform, input.platform),
Layer.succeed(
SpawnExecutableResolution,
Expand Down Expand Up @@ -1068,7 +1096,7 @@ it.effect.each([
yield* launcher.launchEditor({ editor, cwd: "/workspace with spaces/file.ts:12:4" });
}).pipe(
Effect.provide(
testLayer({
layerTest({
platform: "darwin",
env: { HOME: home, PATH: path.join(home, "empty") },
onSpawn: (command) => {
Expand Down Expand Up @@ -1111,7 +1139,7 @@ it.effect("matches macOS bundles on the name boundary only", () =>
return yield* launcher.resolveAvailableEditors();
}).pipe(
Effect.provide(
testLayer({ platform: "darwin", env: { HOME: home, PATH: path.join(home, "bin") } }),
layerTest({ platform: "darwin", env: { HOME: home, PATH: path.join(home, "bin") } }),
),
);

Expand Down Expand Up @@ -1155,7 +1183,7 @@ it.effect("keeps the macOS VS Code Insiders bundle out of stable VS Code", () =>
return available;
}).pipe(
Effect.provide(
testLayer({
layerTest({
platform: "darwin",
env: { HOME: home, PATH: path.join(home, "empty") },
onSpawn: (command) => {
Expand Down
26 changes: 14 additions & 12 deletions apps/server/src/workspace/WorkspaceFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,18 +224,20 @@ export const make = Effect.gen(function* () {
}),
}).pipe(
Effect.map((realTargetPath) => ({ kind: "workspace" as const, realTargetPath })),
Effect.catchTag("WorkspaceFileSystemOperationError", (error) => {
if (!isEnoentCause(error.cause)) {
return Effect.fail(error);
}
const siblingPath = siblingHostAbsolutePath(path, input.cwd, requestedPath);
if (siblingPath === null || siblingPath === target.absolutePath) {
return Effect.fail(error);
}
return Effect.tryPromise({
try: () => NodeFSP.realpath(siblingPath),
catch: () => error,
}).pipe(Effect.map((realTargetPath) => ({ kind: "host" as const, realTargetPath })));
Effect.catchTags({
WorkspaceFileSystemOperationError: (error) => {
if (!isEnoentCause(error.cause)) {
return Effect.fail(error);
}
const siblingPath = siblingHostAbsolutePath(path, input.cwd, requestedPath);
if (siblingPath === null || siblingPath === target.absolutePath) {
return Effect.fail(error);
}
return Effect.tryPromise({
try: () => NodeFSP.realpath(siblingPath),
catch: () => error,
}).pipe(Effect.map((realTargetPath) => ({ kind: "host" as const, realTargetPath })));
},
}),
);
if (resolvedTarget.kind === "host") {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/addons/terminal-shell/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function TerminalShellStatus({
return (
<div
aria-live="polite"
className="flex items-center gap-2 px-1 pb-1.5 text-[11px] text-muted-foreground"
className="flex items-center gap-2 px-1 pb-1.5 text-2xs text-muted-foreground"
data-terminal-shell-status="true"
>
<span data-terminal-shell-status-state={isWorking ? "working" : "ready"}>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/PlanPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function PlanStep({ step }: { step: ActivePlanState["steps"][number] }) {
</span>
<span className={cn("min-w-0 flex-1", completed && "line-through")}>{step.step}</span>
{step.durationMs !== undefined ? (
<span className="shrink-0 text-[11px] tabular-nums text-muted-foreground/60">
<span className="shrink-0 text-2xs tabular-nums text-muted-foreground/60">
{formatDuration(step.durationMs)}
</span>
) : null}
Expand Down Expand Up @@ -153,7 +153,7 @@ export function PlanPanel({ activePlan, proposedPlan, cwd, threadRef }: PlanPane
/>
))}
</div>
<div className="mt-1.5 flex justify-between gap-2 text-[11px] text-muted-foreground">
<div className="mt-1.5 flex justify-between gap-2 text-2xs text-muted-foreground">
<span className="min-w-0 truncate">
{currentStep ? currentStep.step : allComplete ? "All steps complete" : "Plan ready"}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ function toV2ThreadShell(input: typeof thread) {
? "completed"
: latestTurn.state === "error" || input.session?.status === "error"
? "failed"
: "running",
: latestTurn.state === "waiting"
? "waiting"
: "running",
pendingRuntimeRequest: input.hasPendingUserInput
? { id: "request-1", kind: "user_input", createdAt: SHELL_NOW }
: input.hasPendingApprovals
Expand Down Expand Up @@ -279,7 +281,16 @@ it.each(["hasPendingApprovals", "hasPendingUserInput"] as const)(
"badges %s and clears when notifications are disabled",
async (flag) => {
await render();
state.shells.set("one", shell({ [flag]: true }));
state.shells.set(
"one",
shell({
[flag]: true,
latestTurn: {
...thread.latestTurn,
state: flag === "hasPendingUserInput" ? "waiting" : "running",
},
}),
);
await render();
expect(state.badge).toHaveBeenLastCalledWith(1);
const notification = TestNotification.sent[0]!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ function mockThreadShell() {
? "completed"
: state.sessionError || state.turnError || state.limited
? "failed"
: "running",
: state.input
? "waiting"
: "running",
lastErrorClass: state.limited ? "usage_limit" : null,
pendingRuntimeRequest: state.input
? { id: "request-1", kind: "user_input", createdAt: SHELL_NOW }
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ function buildProps() {
}

it("mounts the empty timeline without throwing from layout effects", async () => {
vi.unstubAllGlobals();
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
const container = document.createElement("div");
const root = createRoot(container);

Expand Down
35 changes: 21 additions & 14 deletions packages/ssh/src/runnerProcess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/process";
import * as NodeNet from "node:net";

import { buildRemoteStopScript, buildRemoteT3RunnerScript } from "./tunnel.ts";
import { remoteStateKey } from "./command.ts";

const Started = Schema.Struct({
pid: Schema.Number,
Expand Down Expand Up @@ -183,26 +184,32 @@ server.listen(0, "127.0.0.1", () => {
port: `${started.port}\n`,
managed: mode === "external" ? "external\n" : "managed\n",
};
for (const [name, contents] of Object.entries(savedState)) {
yield* fs.writeFileString(path.join(fixture, name), contents);
}
const script = buildRemoteStopScript({
const target = {
alias: "fixture",
hostname: "fixture",
username: null,
port: null,
});
// Redirect only the state directory. Never use the developer's SSH state.
const isolatedScript = script.replace(
/^STATE_DIR=.*$/mu,
'STATE_DIR="$T3_TEST_STATE_DIR"',
);
assert.notEqual(isolatedScript, script);
};
const stateRoot = path.join(fixture, "ssh-state");
const legacyStateDir = path.join(stateRoot, remoteStateKey(target));
yield* fs.makeDirectory(legacyStateDir, { recursive: true });
for (const [name, contents] of Object.entries(savedState)) {
yield* fs.writeFileString(path.join(legacyStateDir, name), contents);
}
const script = buildRemoteStopScript(target);
// Exercise legacy ownership in the fixture without a host CLI or home.
const isolatedScript = script
.replace(/^STATE_ROOT=.*$/mu, 'STATE_ROOT="$T3_TEST_STATE_ROOT"')
.replace(/^DEFAULT_SERVER_HOME=.*$/mu, 'DEFAULT_SERVER_HOME="$T3_TEST_STATE_ROOT/home"')
.replace(/^PATH_T3=.*$/mu, 'PATH_T3=""');
assert.include(isolatedScript, 'STATE_ROOT="$T3_TEST_STATE_ROOT"');
assert.include(isolatedScript, 'DEFAULT_SERVER_HOME="$T3_TEST_STATE_ROOT/home"');
assert.include(isolatedScript, 'PATH_T3=""');
const runStop = Effect.fn("test.remoteStop")(function* () {
const stop = yield* spawner.spawn(
ChildProcess.make("/bin/sh", ["-s"], {
cwd: fixture,
env: { T3_TEST_STATE_DIR: fixture },
env: { T3_TEST_STATE_ROOT: stateRoot },
stdin: Stream.make(new TextEncoder().encode(isolatedScript)),
}),
);
Expand All @@ -226,11 +233,11 @@ server.listen(0, "127.0.0.1", () => {
return Effect.sync(() => connection.destroy());
});
}
assert.equal(result.exitCode, 0);
assert.equal(result.exitCode, 0, result.stderr);
assert.equal(result.stdout, '{"stopped":true}\n');
assert.equal(result.stderr, "");
for (const name of Object.keys(savedState)) {
assert.isFalse(yield* fs.exists(path.join(fixture, name)));
assert.isFalse(yield* fs.exists(path.join(legacyStateDir, name)));
}
if (mode === "external") {
assert.isFalse(yield* fs.exists(signalPath));
Expand Down
Loading