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
53 changes: 47 additions & 6 deletions plugins/docs/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1273,31 +1273,72 @@ describe("Docs nav panel", () => {
});
});

it("opens arbitrary Markdown files without exposing composer actions", async () => {
it("preserves an explicit host for file opener reads and autosaves", async () => {
const slot = renderSlot(
app.fileOpeners[0]!,
{
path: "notes/plan.mdx",
path: "/Users/shared/notes/plan.mdx",
source: {
kind: "workspace",
kind: "host",
threadId: "thr_1",
environmentId: "env_1",
environmentId: null,
projectId: "project_1",
experimental_hostId: "host_remote",
},
experimental_Original: () => null,
},
{
rpc: {
openFile: () => ({
file: { content: "# Workspace plan", sha256: "sha" },
file: { content: "# Remote plan", sha256: "sha" },
preview,
previewPath: "notes/plan.mdx",
}),
saveOpenedFile: () => ({
outcome: "written",
sha256: "updated-sha",
}),
},
},
);

await slot.findByText("Workspace plan");
const body = await slot.findByText("Remote plan");
expect(slot.rpcCalls).toContainEqual({
method: "openFile",
input: {
source: {
kind: "host",
threadId: "thr_1",
environmentId: null,
projectId: "project_1",
experimental_hostId: "host_remote",
},
path: "/Users/shared/notes/plan.mdx",
},
});

body.textContent = "Updated remote plan";
fireEvent.input(body);
await waitFor(
() => {
expect(slot.rpcCalls).toContainEqual({
method: "saveOpenedFile",
input: {
source: {
kind: "host",
threadId: "thr_1",
environmentId: null,
projectId: "project_1",
experimental_hostId: "host_remote",
},
path: "/Users/shared/notes/plan.mdx",
content: "# Updated remote plan",
expectedSha256: "sha",
},
});
},
{ timeout: 2_000 },
);
expect(slot.queryByRole("button", { name: "Add to chat" })).toBeNull();
expect(slot.queryByRole("button", { name: "Mention in chat" })).toBeNull();
});
Expand Down
11 changes: 10 additions & 1 deletion plugins/docs/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1172,8 +1172,17 @@ function DocsFileOpener({ path: filePath, source }: PluginFileOpenerProps) {
threadId: source.threadId,
environmentId: source.environmentId,
projectId: source.projectId,
...(source.experimental_hostId === undefined
? {}
: { experimental_hostId: source.experimental_hostId }),
}),
[source.environmentId, source.kind, source.projectId, source.threadId],
[
source.environmentId,
source.experimental_hostId,
source.kind,
source.projectId,
source.threadId,
],
);
const [state, setState] = useState<
| { content: string; lease: PreviewLease; previewPath: string }
Expand Down
92 changes: 92 additions & 0 deletions plugins/docs/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ describe("Docs RPC contract", () => {
threadId: string | null;
environmentId: string | null;
projectId: string | null;
experimental_hostId?: string;
};
path: string;
}>();
Expand Down Expand Up @@ -1059,6 +1060,97 @@ describe("Docs vault operations", () => {
]);
});

it("routes explicit host opener reads, previews, and saves to that host", async () => {
const rootPath = "/shared";
const filePath = "/shared/plan.md";
const host = createFakePluginHost({
pluginId: "simple-notes",
sdk: {
files: {
mkdir: async () => ({ ok: true as const }),
read: async ({ hostId, path: openedPath }) => ({
path: openedPath,
content:
hostId === "host_remote" ? "# Remote plan" : "# Primary plan",
contentEncoding: "utf8" as const,
mimeType: "text/markdown",
sizeBytes: 13,
modifiedAtMs: 1,
sha256: hostId === "host_remote" ? "remote-sha" : "primary-sha",
}),
write: async ({ hostId }) => ({
outcome: "written" as const,
sha256:
hostId === "host_remote"
? "remote-written-sha"
: "primary-written-sha",
sizeBytes: 21,
}),
createPreview: async ({ hostId }) => ({
baseUrl:
hostId === "host_remote" ? "/remote-preview" : "/primary-preview",
expiresAtMs: Date.now() + 60_000,
}),
},
},
});
await simpleNotes(host.bb);
host.harness.sdk.calls.length = 0;
const source = {
kind: "host",
threadId: "thread_1",
environmentId: null,
projectId: "project_1",
experimental_hostId: "host_remote",
};

await expect(
host.harness.callRpc("openFile", { source, path: filePath }),
).resolves.toEqual({
file: {
path: filePath,
content: "# Remote plan",
contentEncoding: "utf8",
mimeType: "text/markdown",
sizeBytes: 13,
modifiedAtMs: 1,
sha256: "remote-sha",
},
preview: expect.objectContaining({ baseUrl: "/remote-preview" }),
previewPath: "plan.md",
});
await expect(
host.harness.callRpc("saveOpenedFile", {
source,
path: filePath,
content: "# Updated remote plan",
expectedSha256: "remote-sha",
}),
).resolves.toEqual({
outcome: "written",
sha256: "remote-written-sha",
sizeBytes: 21,
});

expect(host.harness.sdk.callsTo("files.read")).toEqual([
[{ hostId: "host_remote", path: filePath, rootPath }],
]);
expect(host.harness.sdk.callsTo("files.createPreview")).toEqual([
[{ hostId: "host_remote", rootPath }],
]);
expect(host.harness.sdk.callsTo("files.write")).toEqual([
[
{
hostId: "host_remote",
path: filePath,
rootPath,
content: "# Updated remote plan",
expectedSha256: "remote-sha",
},
],
]);
});

it("opens and saves thread-storage Markdown files on the thread's host", async () => {
const storageRootPath = String.raw`C:\bb\thread-storage\thread_1`;
const openedPath = String.raw`C:\bb\thread-storage\thread_1\reports\plan.md`;
Expand Down
7 changes: 6 additions & 1 deletion plugins/docs/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ interface OpenerSource {
threadId: string | null;
environmentId: string | null;
projectId: string | null;
experimental_hostId?: string;
}

interface ResolvedOpenerFile {
Expand Down Expand Up @@ -171,6 +172,7 @@ const openerSourceSchema = z
threadId: z.string().nullable(),
environmentId: z.string().nullable(),
projectId: z.string().nullable(),
experimental_hostId: z.string().min(1).optional(),
})
.strict();
const fileReadSchema = z
Expand Down Expand Up @@ -500,6 +502,9 @@ function parseOpenerSource(value: unknown): OpenerSource {
environmentId:
typeof source.environmentId === "string" ? source.environmentId : null,
projectId: typeof source.projectId === "string" ? source.projectId : null,
...(typeof source.experimental_hostId === "string"
? { experimental_hostId: source.experimental_hostId }
: {}),
};
}

Expand Down Expand Up @@ -1012,7 +1017,7 @@ export default async function plugin(
return {
path: normalized,
rootPath: pathApi.dirname(normalized),
hostId: null,
hostId: source.experimental_hostId ?? null,
};
}
if (source.kind === "workspace" && source.environmentId) {
Expand Down
Loading