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/coderWs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export const makeHandlers = Effect.fn("makeHandlers")(function* ({
path.dirname(currentParent) !== worktreesRoot
) {
return yield* renameThreadBranchError(
"Only worktree folders managed by T3 Code can be renamed from the UI.",
"Only worktree folders managed by T3 Coder can be renamed from the UI.",
);
}

Expand Down
6 changes: 3 additions & 3 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2157,7 +2157,7 @@ const layerWsRpc = () =>
workspaceSnapshotsOnDemand ? withoutWorkspaceSnapshots(providers) : providers,
),
// Compare initial paired values with the snapshot; blindly dropping them loses changes between snapshot and subscription.
(updates) => Stream.concat(Stream.make(config.providers), updates),
(updates) => Stream.concat(rpcInitialItems([config.providers]), updates),
Stream.changesWith(
(previous, next) => JSON.stringify(previous) === JSON.stringify(next),
),
Expand Down Expand Up @@ -2249,7 +2249,7 @@ const layerWsRpc = () =>
Stream.flatMap((event) => Stream.fromIterable(migrationEvent(event))),
);
return Stream.concat(
Stream.make(
rpcInitialItems([
{
version: 1 as const,
sequence: 1,
Expand All @@ -2269,7 +2269,7 @@ const layerWsRpc = () =>
payload: { at: yield* nowIso, environment: environment.descriptor },
},
...snapshotEvents,
),
]),
liveEvents,
);
}),
Expand Down
18 changes: 0 additions & 18 deletions apps/web/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,24 +146,6 @@ export const TraeIcon: Icon = (props) => (
</svg>
);

export const KiroIcon: Icon = (props) => (
<svg {...props} viewBox="0 0 1200 1200" fill="none">
<rect width="1200" height="1200" rx="260" fill="#9046FF" />
<path
d="M398.554 818.914C316.315 1001.03 491.477 1046.74 620.672 940.156C658.687 1059.66 801.052 970.473 852.234 877.795C964.787 673.567 919.318 465.357 907.64 422.374C827.637 129.443 427.623 128.946 358.8 423.865C342.651 475.544 342.402 534.18 333.458 595.051C328.986 625.86 325.507 645.488 313.83 677.785C306.873 696.424 297.68 712.819 282.773 740.645C259.915 783.881 269.604 867.113 387.87 823.883L399.051 818.914H398.554Z"
fill="#fff"
/>
<path
d="M636.123 549.353C603.328 549.353 598.359 510.097 598.359 486.742C598.359 465.623 602.086 448.977 609.293 438.293C615.504 428.852 624.697 424.131 636.123 424.131C647.555 424.131 657.492 428.852 664.447 438.541C672.398 449.474 676.623 466.12 676.623 486.742C676.623 525.998 661.471 549.353 636.375 549.353H636.123Z"
fill="#000"
/>
<path
d="M771.24 549.353C738.445 549.353 733.477 510.097 733.477 486.742C733.477 465.623 737.203 448.977 744.41 438.293C750.621 428.852 759.814 424.131 771.24 424.131C782.672 424.131 792.609 428.852 799.564 438.541C807.516 449.474 811.74 466.12 811.74 486.742C811.74 525.998 796.588 549.353 771.492 549.353H771.24Z"
fill="#000"
/>
</svg>
);

export const VisualStudioCode: Icon = (props) => {
const id = useId();
const maskId = `${id}-vscode-a`;
Expand Down
8 changes: 7 additions & 1 deletion docs/internals/coder-only.md
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,9 @@ checks that every name here still exists and that every modified upstream file i
its primary-environment factory, is not carried.
- client-runtime's `threadSnapshotHttp.ts`, `shellSnapshotHttp.ts`, and `threadHistoryHttp.ts` keep
upstream's paths but load over helper stdio; shell snapshots use upstream's socket fallback.
- client-runtime's `rpc/session.ts` opens the socket without upstream's socket frame parts
(#23758) and `shared/socketFrameParts.ts` is not carried: the gateway forwards whole helper stdio
frames, and the split lives on upstream's uncarried `/ws` upgrade.
- client-runtime's `rpc/protocol.ts` builds its client from `CoderWsRpcGroup`, and `rpc/index.ts`
exports it without the HTTP client; `client-runtime/environment/index.ts` and
`client-runtime/platform/index.ts` export no endpoint, descriptor, capability, or storage document
Expand Down Expand Up @@ -862,6 +865,9 @@ checks that every name here still exists and that every modified upstream file i

### Web chat, composer, and files

- `browser/useOpenLink.ts` is the only carried file of web's cut `browser/`: upstream's link opener
without the in-app browser preview, sending every link to the system browser through the
HTTP(S)-only `shell.openExternal`.
- `ChatView.tsx`: no browser or device panels, mini-player, automatic machine placement,
self-update or version-skew banners, usage-limit panel, feedback upload, `OpenInPicker`, or
favicon store; the active workspace stands in for the primary environment and drafts read their
Expand Down Expand Up @@ -1125,7 +1131,7 @@ them, the file is named here.
`window.location.origin` where upstream calls `configuredHostedAppUrl`, and shared
`threadLinks.ts` declares `DEFAULT_HOSTED_APP_URL` (upstream's `connectAuth.ts` is cut), so
upstream's hosted links still parse. Cloud link state (`cloud/cloudLinkState.ts`) is cut from
`ScheduledTasksSettings.tsx` and `useAvailableSettingsSearchItems.ts`. `Icons.tsx` drops `OnePasswordIcon`.
`ScheduledTasksSettings.tsx` and `useAvailableSettingsSearchItems.ts`. `Icons.tsx` drops `OnePasswordIcon` and `KiroIcon`.
`media/mediaContent.ts`'s `fetchMediaBlob` has no desktop native read after a failed fetch
(#29079).
- **Auth, pairing, and relay.** No HTTP/WebSocket listener, session scopes, pairing, DPoP, relay,
Expand Down
4 changes: 2 additions & 2 deletions docs/internals/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ mobile's `microphonePriority.ts` into `packages/shared`) arrives as an added fil
(top-level directories in brackets):

- Auth and pairing (`apps/server/src/auth`, web `components/auth`, client-runtime `authorization`).
- Preview and browser (`apps/server/src/preview` except `PortScanner.ts`, web `browser`,
`components/preview`).
- Preview and browser (`apps/server/src/preview` except `PortScanner.ts`, web `browser` except
`useOpenLink.ts`, `components/preview`).
- Device (`apps/server/src/device`, web `components/device`, client-runtime `device`).
- HTTP routes (`http.ts` route files other than the server's signed asset route in
`apps/server/src/http.ts`, and the uncarried `assets/` modules).
Expand Down
11 changes: 8 additions & 3 deletions docs/user/composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ conversation.

## Rich text and multiple models

The composer formats supported Markdown as you type. Turn off **Rich text composer** in
**Settings → Chat** to show formatting markers literally. File mentions, skills, images,
terminal context, and review comments remain editable context chips in either mode.
The composer formats supported Markdown as you type. When your cursor is on styled text, its
Markdown shows instead, such as `**bold**`, and the markers edit like any other text: delete them
to remove the style, or change `**` to `*` for italic. The style updates when your cursor leaves.
Some styled text containing or touching literal Markdown characters, such as a bold `path\` or
`2*3`, keeps its styling without showing markers; use formatting shortcuts or turn off rich text to
edit those ranges. Turn off **Rich text composer** in **Settings → Chat** to show formatting
markers literally. File mentions, skills, images, terminal context, and review comments remain
editable context chips in either mode.

In a new draft, select multiple models to send the same prompt to separate background threads.
Each selected model gets its own new worktree in the current Coder workspace. Select a base branch
Expand Down
1 change: 0 additions & 1 deletion knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@
"packages/shared/src/orchestrationV2PendingBackgroundWork.ts": ["exports", "nsExports"],
"packages/shared/src/serverSettings.ts": ["exports", "nsExports"],
"packages/shared/src/shell.ts": ["exports", "nsExports"],
"packages/shared/src/socketFrameParts.ts": ["exports", "nsExports"],
"packages/shared/src/sourceControl.ts": ["exports", "nsExports"],
"packages/shared/src/t3McpToolPresentation.ts": ["exports", "nsExports"],
"packages/shared/src/themePalettes.ts": ["exports", "nsExports"],
Expand Down
90 changes: 89 additions & 1 deletion packages/client-runtime/src/rpc/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { EnvironmentId, GitManagerError, WS_METHODS } from "@t3tools/contracts";
import { describe, expect, it } from "@effect/vitest";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Deferred from "effect/Deferred";
import * as Fiber from "effect/Fiber";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import * as Stream from "effect/Stream";
import * as SubscriptionRef from "effect/SubscriptionRef";
import * as TestClock from "effect/testing/TestClock";

import {
AVAILABLE_CONNECTION_STATE,
Expand All @@ -17,7 +19,13 @@ import {
import * as EnvironmentSupervisor from "../connection/supervisor.ts";
import type { WsRpcProtocolClient } from "./protocol.ts";
import type * as RpcSession from "./session.ts";
import { requestGuarded, RpcPermissionGuard, runStreamGuarded, subscribe } from "./client.ts";
import {
requestGuarded,
RpcPermissionGuard,
runStreamGuarded,
subscribe,
subscribeDynamicWithSession,
} from "./client.ts";
import { setErrorDiagnosticReporter, type ErrorDiagnostic } from "../errors/diagnostics.ts";

const TARGET = new ConnectionTarget({
Expand All @@ -27,6 +35,18 @@ const TARGET = new ConnectionTarget({
wsBaseUrl: "wss://environment.example.test",
});

function session(client: WsRpcProtocolClient): RpcSession.RpcSession {
return {
client,
initialConfig: Effect.never,
subscribeServerConfig: (input = {}) => client.subscribeServerConfig(input),
ready: Effect.void,
probe: Effect.void,
inboundFrames: Effect.succeed(0),
closed: Effect.never,
};
}

const makeHarness = Effect.fn("TestEnvironmentRpc.makeHarness")(function* () {
const state = yield* SubscriptionRef.make<SupervisorConnectionState>(AVAILABLE_CONNECTION_STATE);
const activeSession = yield* SubscriptionRef.make<Option.Option<RpcSession.RpcSession>>(
Expand Down Expand Up @@ -121,4 +141,72 @@ describe("environment RPC", () => {
expect(JSON.stringify(diagnostics)).not.toContain("/private-repo");
}),
);

it.effect.each(["input", "stream"] as const)(
"reports %s subscription defects and resubscribes on the next session",
(where) =>
Effect.gen(function* () {
const defect = new Error("subscription invariant failed");
let expectedFailureCount = 0;
let inputs = 0;
let streams = 0;
const observedDefects: unknown[] = [];
const defectObserved = yield* Deferred.make<void>();
const resubscribed = yield* Deferred.make<void>();
const firstClient = {
[WS_METHODS.subscribeTerminalMetadata]: () => {
streams += 1;
return where === "stream" ? Stream.die(defect) : Stream.never;
},
} as unknown as WsRpcProtocolClient;
const secondClient = {
[WS_METHODS.subscribeTerminalMetadata]: () => {
Deferred.doneUnsafe(resubscribed, Effect.void);
return Stream.never;
},
} as unknown as WsRpcProtocolClient;
const { activeSession, supervisor } = yield* makeHarness();

yield* SubscriptionRef.set(activeSession, Option.some(session(firstClient)));
const subscriptionFiber = yield* subscribeDynamicWithSession(
WS_METHODS.subscribeTerminalMetadata,
() =>
Effect.suspend(() => {
inputs += 1;
return where === "input" && inputs === 1 ? Effect.die(defect) : Effect.succeed({});
}),
{
onDefect: (cause) =>
Effect.sync(() => {
observedDefects.push(Cause.squash(cause));
Deferred.doneUnsafe(defectObserved, Effect.void);
}),
onExpectedFailure: () =>
Effect.sync(() => {
expectedFailureCount += 1;
}),
retryExpectedFailureAfter: "250 millis",
},
).pipe(
Stream.runDrain,
Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
Effect.forkChild,
);
yield* Deferred.await(defectObserved);

expect(inputs).toBe(1);
expect(streams).toBe(where === "input" ? 0 : 1);
expect(expectedFailureCount).toBe(0);
expect(observedDefects).toEqual([defect]);

// Let the defect settle before the next session arrives, as it does on a real device.
yield* TestClock.adjust("1 second");
expect(subscriptionFiber.pollUnsafe()).toBeUndefined();
yield* SubscriptionRef.set(activeSession, Option.some(session(secondClient)));
yield* Deferred.await(resubscribed);
yield* Fiber.interrupt(subscriptionFiber);

expect(inputs).toBe(2);
}),
);
});
31 changes: 0 additions & 31 deletions packages/client-runtime/src/rpc/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
type ServerConfig as ServerConfigType,
WS_METHODS,
} from "@t3tools/contracts";
import { splitSocketFrame } from "@t3tools/shared/socketFrameParts";
import { describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
Expand Down Expand Up @@ -183,36 +182,6 @@ const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialCo
});

describe("RpcSessionFactory", () => {
it.effect("connects while a large config snapshot downloads at 4000 bytes per second", () =>
Effect.gen(function* () {
const { factory, sockets } = yield* makeFactory();
const session = yield* factory.connect(PREPARED);
const readyFiber = yield* Effect.forkChild(session.ready);
const socket = yield* awaitSocket(sockets);
socket.open();
const request = yield* awaitRequest(socket);
// Multi-byte text, so parts must be sized in UTF-8 bytes and never split a surrogate pair.
const config = { ...SERVER_CONFIG, cwd: "/" + "é€😀x".repeat(32 * 1024) };
const frame = encodeJson({
_tag: "Chunk",
requestId: request.id,
values: [{ version: 1, type: "snapshot", config: encodeServerConfig(config) }],
});
const parts = new URL(socket.url).searchParams.has("frameParts")
? splitSocketFrame(frame)
: [frame];

for (const part of parts) {
const bytes = new TextEncoder().encode(part);
yield* TestClock.adjust((bytes.length / 4000) * 1000);
socket.serverMessage(new TextDecoder().decode(bytes));
}
yield* Fiber.join(readyFiber);

expect(yield* session.initialConfig).toEqual(config);
}),
);

it.effect("uses one config subscription for initial sync", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
21 changes: 5 additions & 16 deletions packages/client-runtime/src/rpc/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import {
WsSubscribeServerConfigRpc,
WS_METHODS,
} from "@t3tools/contracts";
import {
joinSocketFrameParts,
SOCKET_FRAME_PARTS_QUERY_PARAM,
} from "@t3tools/shared/socketFrameParts";
import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
import * as Deferred from "effect/Deferred";
Expand Down Expand Up @@ -122,7 +118,7 @@ function mapSessionRpcError(
case "KeybindingsConfigParseError":
case "ServerSettingsError":
return new ConnectionTransientErrorClass({
reason: "transport",
reason: "remote-unavailable",
detail: error.message,
});
case "RpcClientError":
Expand Down Expand Up @@ -180,17 +176,10 @@ const make = Effect.gen(function* () {
});
return socket;
};
const socketUrl = new URL(connection.socketUrl);
socketUrl.searchParams.set(SOCKET_FRAME_PARTS_QUERY_PARAM, "1");
const layerSocket = Layer.effect(
Socket.Socket,
Effect.map(Effect.service(Socket.Socket), joinSocketFrameParts),
).pipe(
Layer.provide(
Socket.layerWebSocket(socketUrl.toString(), {
openTimeout: SOCKET_OPEN_TIMEOUT,
}),
),
// Coder: no socket frame parts (#23758); the gateway forwards whole helper stdio frames.
const layerSocket = Socket.layerWebSocket(connection.socketUrl, {
openTimeout: SOCKET_OPEN_TIMEOUT,
}).pipe(
Layer.provide(Layer.succeed(Socket.WebSocketConstructor, countingWebSocketConstructor)),
);
const layerProtocol = Layer.effect(
Expand Down
4 changes: 0 additions & 4 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,6 @@
"types": "./src/delimitedPreview.ts",
"import": "./src/delimitedPreview.ts"
},
"./socketFrameParts": {
"types": "./src/socketFrameParts.ts",
"import": "./src/socketFrameParts.ts"
},
"./thirdPartyLicenses": {
"types": "./src/thirdPartyLicenses.ts",
"import": "./src/thirdPartyLicenses.ts"
Expand Down
Loading