Skip to content
Closed
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
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"test:events": "node scripts/events.mjs",
"test:todos": "node scripts/todos.mjs",
"test:permissions": "node scripts/permissions.mjs",
"test:openwork-selection": "bun scripts/openwork-selection.ts",
"test:session-scope": "bun scripts/session-scope.ts",
"test:session-switch": "node scripts/session-switch.mjs",
"test:fs-engine": "node scripts/fs-engine.mjs",
Expand Down
97 changes: 97 additions & 0 deletions apps/app/scripts/openwork-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import assert from "node:assert/strict";

const { selectOpenworkWorkspace } = await import("../src/app/workspace/openwork-selection.ts");

const workspace = (id: string, directory?: string) => ({
id,
name: id,
path: directory ?? "",
preset: "remote",
workspaceType: "remote" as const,
remoteType: "openwork" as const,
baseUrl: `https://example.com/w/${id}/opencode`,
directory: directory ?? null,
opencode: directory ? { directory } : undefined,
});

const results = {
ok: true,
steps: [] as Array<Record<string, unknown>>,
};

async function step(name: string, fn: () => void | Promise<void>) {
results.steps.push({ name, status: "running" });
const index = results.steps.length - 1;

try {
await fn();
results.steps[index] = { name, status: "ok" };
} catch (error) {
results.ok = false;
results.steps[index] = {
name,
status: "error",
error: error instanceof Error ? error.message : String(error),
};
throw error;
}
}

try {
await step("explicit workspace id wins", () => {
const result = selectOpenworkWorkspace({
items: [workspace("ws_alpha", "/repo/a"), workspace("ws_beta", "/repo/b")],
workspaceId: "ws_beta",
});

assert.deepEqual(result, { ok: true, workspace: workspace("ws_beta", "/repo/b") });
});

await step("directory hint resolves normalized matches", () => {
const result = selectOpenworkWorkspace({
items: [workspace("ws_alpha", "/repo/a"), workspace("ws_beta", "/repo/b")],
directoryHint: "/repo/b/",
});

assert.deepEqual(result, { ok: true, workspace: workspace("ws_beta", "/repo/b") });
});

await step("single-workspace hosts still connect without extra selectors", () => {
const result = selectOpenworkWorkspace({
items: [workspace("ws_only", "/repo/a")],
});

assert.deepEqual(result, { ok: true, workspace: workspace("ws_only", "/repo/a") });
});

await step("multi-workspace hosts reject ambiguous host-only connects", () => {
const result = selectOpenworkWorkspace({
items: [workspace("ws_alpha", "/repo/a"), workspace("ws_beta", "/repo/b")],
});

assert.deepEqual(result, {
ok: false,
reason: "ambiguous",
message:
"OpenWork host returned multiple workspaces. Use a workspace-scoped URL (/w/ws_*) or reconnect from the specific workspace.",
});
});

await step("stale directory hints no longer silently fall back to the first workspace", () => {
const result = selectOpenworkWorkspace({
items: [workspace("ws_alpha", "/repo/a"), workspace("ws_beta", "/repo/b")],
directoryHint: "/repo/missing",
});

assert.deepEqual(result, {
ok: false,
reason: "not-found",
message: "OpenWork worker directory not found on that host.",
});
});

console.log(JSON.stringify(results));
} catch {
console.log(JSON.stringify(results));
process.exitCode = 1;
}
4 changes: 3 additions & 1 deletion apps/app/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,9 @@ export default function App() {
if (!id) return false;
const ready = await workspaceStore.activateWorkspace(id);
if (ready) {
await refreshSidebarWorkspaceSessions(id).catch(() => undefined);
// Keep workspace activation on the critical path for correctness, but do
// not block session creation or prompt send on a sidebar refresh.
void refreshSidebarWorkspaceSessions(id).catch(() => undefined);
}
return ready;
};
Expand Down
40 changes: 12 additions & 28 deletions apps/app/src/app/context/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
type OpenworkServerClient,
type OpenworkWorkspaceInfo,
} from "../lib/openwork-server";
import { selectOpenworkWorkspace } from "../workspace/openwork-selection";
import { downloadDir, homeDir } from "@tauri-apps/api/path";
import {
engineDoctor,
Expand Down Expand Up @@ -816,32 +817,15 @@ export function createWorkspaceStore(options: {
}

const response = await client.listWorkspaces();
const items = Array.isArray(response.items) ? response.items : [];
const hint = normalizeDirectoryPath(input.directoryHint ?? "");
const selectByHint = (entry: OpenworkWorkspaceInfo) => {
if (!hint) return false;
const entryPath = normalizeDirectoryPath(
(entry.opencode?.directory as string | undefined) ?? (entry.path as string | undefined) ?? "",
);
return Boolean(entryPath && entryPath === hint);
};
const selectById = (entry: OpenworkWorkspaceInfo) => Boolean(requestedWorkspaceId && entry?.id === requestedWorkspaceId);

const workspaceById = requestedWorkspaceId
? (items.find((item) => item?.id && selectById(item as any)) as OpenworkWorkspaceInfo | undefined)
: undefined;
if (requestedWorkspaceId && !workspaceById) {
throw new Error("OpenWork worker not found on that host.");
}

const workspaceByHint = hint
? (items.find((item) => item?.id && selectByHint(item as any)) as OpenworkWorkspaceInfo | undefined)
: undefined;

const workspace = (workspaceById ?? workspaceByHint ?? items[0]) as OpenworkWorkspaceInfo | undefined;
if (!workspace?.id) {
throw new Error("OpenWork server did not return a worker.");
const selected = selectOpenworkWorkspace({
items: Array.isArray(response.items) ? response.items : [],
workspaceId: requestedWorkspaceId,
directoryHint: input.directoryHint ?? null,
});
if (!selected.ok) {
throw new Error(selected.message);
}
const workspace = selected.workspace;
const opencodeUpstreamBaseUrl = workspace.opencode?.baseUrl?.trim() ?? workspace.baseUrl?.trim() ?? "";
if (!opencodeUpstreamBaseUrl) {
throw new Error("OpenWork server did not provide an OpenCode URL.");
Expand Down Expand Up @@ -1201,7 +1185,7 @@ export function createWorkspaceStore(options: {
const resolved = await resolveOpenworkHost({
hostUrl,
token,
workspaceId: workspace.openworkWorkspaceId ?? null,
workspaceId: resolveRuntimeWorkspaceLookup(workspace)?.workspaceId ?? null,
});
if (resolved.kind !== "openwork") {
updateWorkspaceConnectionState(id, {
Expand Down Expand Up @@ -1509,7 +1493,7 @@ export function createWorkspaceStore(options: {
const resolved = await resolveOpenworkHost({
hostUrl,
token,
workspaceId: next.openworkWorkspaceId ?? null,
workspaceId: resolveRuntimeWorkspaceLookup(next)?.workspaceId ?? null,
directoryHint: next.directory ?? null,
});
if (resolved.kind !== "openwork") {
Expand Down Expand Up @@ -2792,7 +2776,7 @@ export function createWorkspaceStore(options: {
const resolved = await resolveOpenworkHost({
hostUrl,
token,
workspaceId: workspace.openworkWorkspaceId ?? null,
workspaceId: resolveRuntimeWorkspaceLookup(workspace)?.workspaceId ?? null,
directoryHint: directory || null,
});
if (resolved.kind !== "openwork") {
Expand Down
80 changes: 80 additions & 0 deletions apps/app/src/app/workspace/openwork-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { OpenworkWorkspaceInfo } from "../lib/openwork-server";
import { normalizeDirectoryPath } from "../utils";

type OpenworkWorkspaceSelectionReason = "missing" | "not-found" | "ambiguous";

export type OpenworkWorkspaceSelectionResult =
| {
ok: true;
workspace: OpenworkWorkspaceInfo;
}
| {
ok: false;
reason: OpenworkWorkspaceSelectionReason;
message: string;
};

const workspaceDirectory = (entry: OpenworkWorkspaceInfo) =>
normalizeDirectoryPath((entry.opencode?.directory as string | undefined) ?? entry.directory ?? entry.path ?? "");

export function selectOpenworkWorkspace(input: {
items: OpenworkWorkspaceInfo[];
workspaceId?: string | null;
directoryHint?: string | null;
}): OpenworkWorkspaceSelectionResult {
const items = Array.isArray(input.items) ? input.items.filter((entry): entry is OpenworkWorkspaceInfo => Boolean(entry?.id)) : [];
const explicitWorkspaceId = input.workspaceId?.trim() ?? "";
const directoryHint = normalizeDirectoryPath(input.directoryHint ?? "");

if (!items.length) {
return {
ok: false,
reason: "missing",
message: "OpenWork server did not return a worker.",
};
}

if (explicitWorkspaceId) {
const exact = items.find((entry) => entry.id === explicitWorkspaceId);
if (exact) {
return { ok: true, workspace: exact };
}
return {
ok: false,
reason: "not-found",
message: "OpenWork worker not found on that host.",
};
}

if (directoryHint) {
const matches = items.filter((entry) => workspaceDirectory(entry) === directoryHint);
if (matches.length === 1) {
return { ok: true, workspace: matches[0] };
}
if (matches.length > 1) {
return {
ok: false,
reason: "ambiguous",
message: "OpenWork host returned multiple workspaces for that directory.",
};
}
if (items.length === 1) {
return { ok: true, workspace: items[0] };
}
return {
ok: false,
reason: "not-found",
message: "OpenWork worker directory not found on that host.",
};
}

if (items.length === 1) {
return { ok: true, workspace: items[0] };
}

return {
ok: false,
reason: "ambiguous",
message: "OpenWork host returned multiple workspaces. Use a workspace-scoped URL (/w/ws_*) or reconnect from the specific workspace.",
};
}
Loading