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
2 changes: 1 addition & 1 deletion apps/server/src/mcp/toolkits/preview/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const readonlyBrowserTool = <T extends Tool.Any>(tool: T): T =>

const PreviewStatusTool = Tool.make("preview_status", {
description:
"Report preview tab state: owner, pending dialog, URL, title, loading, viewport, and size. `tabs` lists this thread's tabs. `profiles` lists logins for `preview_open`. Read any listed tab. Act only on your tab, or an unclaimed tab with no human in control.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (P3) clarity: SessionControl.agent() rejects when owner !== null even if the agent owns the tab, and agentCanActNow() also requires owner === null. Please state explicitly that no human may control either an agent-owned or unclaimed tab before acting; keep the read-any-tab rule. This confirms the existing bot note, not a new behavior finding.

"Report tab state: owner, dialog, URL, title, loading and viewport. No tab (`tabId:null`): try `preview_open`. `tabs`: thread tabs; `profiles`: logins. Read any tab; act on yours or unclaimed tabs without human control.",
parameters: PreviewAutomationTabTargetInput,
success: PreviewAutomationStatus,
failure: PreviewToolFailure,
Expand Down
33 changes: 33 additions & 0 deletions apps/server/src/preview/ServerBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2035,6 +2035,39 @@ it.live("agents read a human's tab with no arguments and act on it only while no
).pipe(Effect.provide(layer)),
);

it.live("browser automation stays available without an open tab", () =>
Effect.scoped(
Effect.gen(function* () {
const { broker, tabId } = yield* ready;
const emptyScope = {
...scope,
thread: { ...scope.thread, threadId: ThreadId.make("empty-browser-thread") },
};
const empty = yield* broker.invoke<PreviewAutomationStatus>({
scope: emptyScope,
operation: "status",
input: {},
});
expect(empty).toMatchObject({ available: true, visible: false, tabId: null });
yield* broker.invoke({ scope, tabId, operation: "close", input: {} });
const closed = yield* broker.invoke<PreviewAutomationStatus>({
scope,
operation: "status",
input: {},
});
expect(closed).toMatchObject({ available: true, visible: false, tabId: null });
expect(contexts).toHaveLength(1);
const reopened = yield* broker.invoke<PreviewAutomationStatus>({
scope,
operation: "open",
input: { show: false },
});
expect(reopened.available).toBe(true);
expect(reopened.tabId).not.toBeNull();
}),
).pipe(Effect.provide(layer)),
);

it.live("a session's own tab stays its default while other sessions' tabs are listed", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/preview/ServerBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ const make = Effect.gen(function* () {
): Promise<PreviewAutomationStatus> => {
if (!tab) {
return {
available: false,
available: true,
visible: false,
tabId: null,
url: null,
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/agentContextBudget.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"claudeSystemAppendBytes": 5133,
"codexAdditionalContextBytes": 6863,
"mcpToolListJsonBytes": 47498
"mcpToolListJsonBytes": 47461
}
Loading