Skip to content
Open
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
48 changes: 45 additions & 3 deletions apps/server/src/wsServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
ProjectId,
ThreadId,
type WsError,
WS_CHANNELS,
WS_ERROR_CODES,
WS_METHODS,
WebSocketRequest,
type WsResponse as WsResponseMessage,
Expand Down Expand Up @@ -231,6 +233,40 @@ class RouteRequestError extends Schema.TaggedErrorClass<RouteRequestError>()("Ro
message: Schema.String,
}) {}

function buildWsErrorForClient(cause: Cause.Cause<unknown>): WsError {
const defect = Cause.squash(cause);
const rawMessage = defect instanceof Error ? defect.message : Cause.pretty(cause);
const firstLine = rawMessage.split("\n")[0]?.trim() ?? rawMessage;
const message = firstLine.replace(/^Error:\s*/i, "");

const dirNotExistPrefix = "Project directory does not exist: ";
const pathNotDirPrefix = "Project path is not a directory: ";
if (message.startsWith(dirNotExistPrefix)) {
return {
code: WS_ERROR_CODES.projectDirNotExist,
message: "Project directory does not exist",
path: message.slice(dirNotExistPrefix.length).trim() || undefined,
};
}
if (message.startsWith(pathNotDirPrefix)) {
return {
code: WS_ERROR_CODES.projectPathNotDirectory,
message: "Project path is not a directory",
path: message.slice(pathNotDirPrefix.length).trim() || undefined,
};
}
if (Schema.is(RouteRequestError)(defect)) {
return {
code: WS_ERROR_CODES.routeRequest,
message,
};
}
return {
code: WS_ERROR_CODES.unknown,
message,
};
}

export const createServer = Effect.fn(function* (): Effect.fn.Return<
http.Server,
ServerLifecycleError,
Expand Down Expand Up @@ -903,23 +939,29 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
if (messageText === null) {
return yield* sendWsResponse({
id: "unknown",
error: { message: "Invalid request format: Failed to read message" },
error: {
code: WS_ERROR_CODES.invalidRequest,
message: "Invalid request format: Failed to read message",
},
});
}

const request = decodeWebSocketRequest(messageText);
if (Result.isFailure(request)) {
return yield* sendWsResponse({
id: "unknown",
error: { message: `Invalid request format: ${formatSchemaError(request.failure)}` },
error: {
code: WS_ERROR_CODES.invalidRequest,
message: `Invalid request format: ${formatSchemaError(request.failure)}`,
},
});
}

const result = yield* Effect.exit(routeRequest(request.success));
if (Exit.isFailure(result)) {
return yield* sendWsResponse({
id: request.success.id,
error: { message: Cause.pretty(result.cause) },
error: buildWsErrorForClient(result.cause),
});
}

Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1215,7 +1215,7 @@ export default function Sidebar() {
</div>

{shouldShowProjectPathEntry && (
<div className="mb-2 px-1">
<div className="mb-2 min-w-0 px-1 overflow-hidden">
{isElectron && (
<button
type="button"
Expand All @@ -1227,7 +1227,7 @@ export default function Sidebar() {
{isPickingFolder ? "Picking folder..." : "Browse for folder"}
</button>
)}
<div className="flex gap-1.5">
<div className="flex min-w-0 gap-1.5">
<input
ref={addProjectInputRef}
className={`min-w-0 flex-1 rounded-md border bg-secondary px-2 py-1 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:outline-none ${
Expand Down Expand Up @@ -1260,7 +1260,7 @@ export default function Sidebar() {
</button>
</div>
{addProjectError && (
<p className="mt-1 px-0.5 text-[11px] leading-tight text-red-400">
<p className="mt-1 min-w-0 break-words px-0.5 text-[11px] leading-tight text-red-400">
{addProjectError}
</p>
)}
Expand Down
22 changes: 17 additions & 5 deletions packages/contracts/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,26 @@ export const WebSocketRequest = Schema.Struct({
});
export type WebSocketRequest = typeof WebSocketRequest.Type;

export const WS_ERROR_CODES = {
invalidRequest: "INVALID_REQUEST",
projectDirNotExist: "PROJECT_DIR_NOT_EXIST",
projectPathNotDirectory: "PROJECT_PATH_NOT_DIRECTORY",
routeRequest: "ROUTE_REQUEST_ERROR",
unknown: "UNKNOWN",
} as const;
export type WsErrorCode = (typeof WS_ERROR_CODES)[keyof typeof WS_ERROR_CODES];

export const WsError = Schema.Struct({
code: Schema.String,
message: Schema.String,
path: Schema.optional(Schema.String),
});
export type WsError = typeof WsError.Type;

export const WebSocketResponse = Schema.Struct({
id: TrimmedNonEmptyString,
result: Schema.optional(Schema.Unknown),
error: Schema.optional(
Schema.Struct({
message: Schema.String,
}),
),
error: Schema.optional(WsError),
});
export type WebSocketResponse = typeof WebSocketResponse.Type;

Expand Down