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
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const formSchema = z
scheduleTime: z.string().optional(),
scheduleInterval: z.number().int().min(1).max(60).optional(),
providerId: z.string().optional(),
runSilently: z.boolean(),
enabled: z.boolean(),
})
.superRefine((data, ctx) => {
Expand Down Expand Up @@ -107,6 +108,7 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
scheduleTime: '09:00',
scheduleInterval: 1,
providerId: undefined,
runSilently: true,
enabled: true,
},
})
Expand Down Expand Up @@ -144,6 +146,7 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
scheduleTime: initialValues.scheduleTime || '09:00',
scheduleInterval: initialValues.scheduleInterval || 1,
providerId: initialValues.providerId,
runSilently: initialValues.runSilently ?? true,
enabled: initialValues.enabled,
})
} else {
Expand All @@ -154,6 +157,7 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
scheduleTime: '09:00',
scheduleInterval: 1,
providerId: undefined,
runSilently: true,
enabled: true,
})
}
Expand Down Expand Up @@ -253,6 +257,7 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
scheduleInterval:
values.scheduleType !== 'daily' ? values.scheduleInterval : undefined,
providerId: values.providerId,
runSilently: values.runSilently,
enabled: values.enabled,
})
form.reset()
Expand Down Expand Up @@ -458,6 +463,28 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
)}
</div>

<FormField
control={form.control}
name="runSilently"
render={({ field }) => (
<FormItem className="flex flex-row items-start gap-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1">
<FormLabel className="font-normal">Run silently</FormLabel>
<FormDescription>
Use a hidden background page without opening or focusing a
browser window.
</FormDescription>
</div>
</FormItem>
)}
/>

<FormField
control={form.control}
name="enabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ export const ScheduledTaskCard: FC<ScheduledTaskCardProps> = ({
</span>
</>
)}
<span>•</span>
<span>
{job.runSilently === false ? 'Visible run' : 'Silent run'}
</span>
{job.lastRunAt && (
<>
<span>•</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const ScheduledTasksPage: FC = () => {
'daily',
scheduleTime: searchParams.get('scheduleTime') ?? '09:00',
scheduleInterval: 1,
runSilently: true,
enabled: true,
createdAt: '',
updatedAt: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export const scheduledJobRuns = async () => {
message: job.query,
signal: abortController.signal,
providerId: job.providerId,
runSilently: job.runSilently ?? true,
})

await updateJobRun(jobRun.id, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const TIPS: Tip[] = [
},
{
id: 'background-tasks',
text: 'Scheduled tasks run in a separate window so they never interrupt your browsing.',
text: 'Scheduled tasks can run silently in the background without opening a new browser window.',
},
{
id: 'claude-code-mcp',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,16 @@ describe('buildChatRequestBody', () => {

expect(body.toolApprovalConfig).toBeUndefined()
})

it('passes scheduled task silent mode through to the server', () => {
const body = buildChatRequestBody({
conversationId: '6ff46e3b-e45a-40a4-9157-ca520e800f43',
provider,
isScheduledTask: true,
runSilently: false,
})

expect(body.isScheduledTask).toBe(true)
expect(body.runSilently).toBe(false)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ interface ChatRequestBodyParams {
toolApprovalConfig?: ToolApprovalConfig
toolApprovalResponses?: ApprovalResponseData[]
isScheduledTask?: boolean
runSilently?: boolean
}

export const toRequestToolApprovalConfig = (
Expand Down Expand Up @@ -81,6 +82,7 @@ export const buildChatRequestBody = ({
toolApprovalConfig,
toolApprovalResponses,
isScheduledTask,
runSilently,
}: ChatRequestBodyParams) => ({
message,
provider: provider.type,
Expand Down Expand Up @@ -112,4 +114,5 @@ export const buildChatRequestBody = ({
toolApprovalConfig: toRequestToolApprovalConfig(toolApprovalConfig),
toolApprovalResponses,
isScheduledTask,
runSilently,
})
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface ChatServerRequest {
activeTab?: ActiveTab
signal?: AbortSignal
providerId?: string
runSilently?: boolean
}

interface ChatServerResponse {
Expand Down Expand Up @@ -137,6 +138,7 @@ export async function getChatServerResponse(
userSystemPrompt: `${personalization}\n${scheduleSystemPrompt}`,
supportsImages: provider.supportsImages,
isScheduledTask: true,
runSilently: request.runSilently ?? true,
}),
}),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ScheduledJob {
scheduleTime?: string
scheduleInterval?: number
enabled: boolean
runSilently?: boolean
providerId?: string
createdAt: string
updatedAt: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type RemoteScheduledJob = {
lastRunAt: string | null
}

const IGNORED_FIELDS = ['id', 'createdAt', 'lastRunAt'] as const
const IGNORED_FIELDS = ['id', 'createdAt', 'lastRunAt', 'runSilently'] as const

function toComparable(job: ScheduledJob) {
const data = omit(job, IGNORED_FIELDS)
Expand Down Expand Up @@ -63,6 +63,7 @@ function remoteToLocal(remote: RemoteScheduledJob): ScheduledJob {
scheduleTime: remote.scheduleTime ?? undefined,
scheduleInterval: remote.scheduleInterval ?? undefined,
enabled: remote.enabled,
runSilently: true,
providerId: remote.llmProviderId ?? undefined,
createdAt: normalizeTimestamp(remote.createdAt),
updatedAt: normalizeTimestamp(remote.updatedAt),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export class AiSdkAgent {
userSystemPrompt: config.resolvedConfig.userSystemPrompt,
exclude: excludeSections,
isScheduledTask: config.resolvedConfig.isScheduledTask,
scheduledTaskRunSilently: config.resolvedConfig.scheduledTaskRunSilently,
scheduledTaskPageId: config.browserContext?.activeTab?.pageId,
workspaceDir: config.resolvedConfig.workingDir,
soulContent,
Expand Down
39 changes: 27 additions & 12 deletions packages/browseros-agent/apps/server/src/agent/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ You do not have a filesystem workspace in this session. Return all results direc

// Mode-aware framing
if (options?.isScheduledTask) {
role +=
'\n\nYou are running as a scheduled background task on a system-managed hidden page. Complete the task autonomously and report results.'
const location =
options.scheduledTaskRunSilently !== false
? ' on a system-managed hidden page'
: ''
role += `\n\nYou are running as a scheduled background task${location}. Complete the task autonomously and report results.`
} else if (options?.chatMode) {
role +=
'\n\nYou are in read-only chat mode. You can observe pages but cannot interact with them, modify files, or store memories.'
Expand Down Expand Up @@ -659,9 +662,13 @@ function getUserContext(
if (!options?.chatMode) {
let pageCtx = '<page_context>'

const isSilentScheduledTask =
options?.isScheduledTask && options.scheduledTaskRunSilently !== false

if (options?.isScheduledTask) {
pageCtx +=
'\nYou are running as a **scheduled background task** on a system-managed hidden page.'
pageCtx += isSilentScheduledTask
? '\nYou are running as a **scheduled background task** on a system-managed hidden page.'
: '\nYou are running as a **scheduled background task**.'
}

pageCtx +=
Expand All @@ -671,14 +678,21 @@ function getUserContext(
const pageRef = options.scheduledTaskPageId
? `\`${options.scheduledTaskPageId}\``
: 'the page ID from the Browser Context'
pageCtx += `\n2. **Use starting page ID ${pageRef} directly.** For additional browsing, prefer \`new_hidden_page\` so the work stays invisible to the user.`
pageCtx +=
'\n3. **Do NOT close your starting hidden page** (via `close_page` on that page ID). It is managed by the system and will be cleaned up automatically.'
pageCtx +=
'\n4. **Do NOT create new windows** (via `create_window` or `create_hidden_window`). Use hidden pages instead.'
pageCtx +=
'\n5. **Close extra hidden pages when you are done with them** unless you explicitly reveal them with `show_page`.'
pageCtx += '\n6. Complete the task end-to-end and report results.'
if (isSilentScheduledTask) {
pageCtx += `\n2. **Use starting page ID ${pageRef} directly.** For additional browsing, prefer \`new_hidden_page\` so the work stays invisible to the user.`
pageCtx +=
'\n3. **Do NOT close your starting hidden page** (via `close_page` on that page ID). It is managed by the system and will be cleaned up automatically.'
pageCtx +=
'\n4. **Do NOT create new windows** (via `create_window` or `create_hidden_window`). Use hidden pages instead.'
pageCtx +=
'\n5. **Close extra hidden pages when you are done with them** unless you explicitly reveal them with `show_page`.'
pageCtx += '\n6. Complete the task end-to-end and report results.'
} else {
pageCtx += `\n2. **Use starting page ID ${pageRef} directly.** For additional browsing, prefer \`new_page\` with background mode so the task does not steal focus.`
pageCtx +=
'\n3. **Do NOT create new windows** unless the scheduled task explicitly requires a separate window.'
pageCtx += '\n4. Complete the task end-to-end and report results.'
}
}

pageCtx += '\n</page_context>'
Expand Down Expand Up @@ -739,6 +753,7 @@ export interface BuildSystemPromptOptions {
userSystemPrompt?: string
exclude?: string[]
isScheduledTask?: boolean
scheduledTaskRunSilently?: boolean
scheduledTaskPageId?: number
workspaceDir?: string
soulContent?: string
Expand Down
2 changes: 2 additions & 0 deletions packages/browseros-agent/apps/server/src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export interface ResolvedAgentConfig {
chatMode?: boolean
/** Scheduled task mode - disables tab grouping. Defaults to false. */
isScheduledTask?: boolean
/** Scheduled task silent mode - runs on hidden pages without visible windows. Defaults to true. */
scheduledTaskRunSilently?: boolean
/** Apps the user previously declined to connect via MCP (chose "do it manually"). */
declinedApps?: string[]
/** Where the chat session originates from — determines navigation behavior. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class ChatService {
supportsImages: request.supportsImages,
chatMode: request.mode === 'chat',
isScheduledTask: request.isScheduledTask,
scheduledTaskRunSilently: request.runSilently,
origin: request.origin,
declinedApps: request.declinedApps,
browserosId: this.deps.browserosId,
Expand Down Expand Up @@ -186,7 +187,7 @@ export class ChatService {
this.deps.browser,
request.browserContext,
)
if (request.isScheduledTask) {
if (request.isScheduledTask && request.runSilently !== false) {
try {
hiddenPageId = await this.deps.browser.newPage('about:blank', {
hidden: true,
Expand Down
1 change: 1 addition & 0 deletions packages/browseros-agent/apps/server/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const ChatRequestSchema = AgentLLMConfigSchema.extend({
browserContext: BrowserContextSchema.optional(),
userSystemPrompt: z.string().optional(),
isScheduledTask: z.boolean().optional().default(false),
runSilently: z.boolean().optional().default(true),
userWorkingDir: z.string().min(1).optional(),
supportsImages: z.boolean().optional().default(true),
mode: z.enum(['chat', 'agent']).optional().default('agent'),
Expand Down
36 changes: 3 additions & 33 deletions packages/browseros-agent/apps/server/src/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,45 +517,15 @@ export class Browser {
return null
}

private async resolveWindowIdForNewPage(opts?: {
hidden?: boolean
windowId?: number
}): Promise<number | undefined> {
if (!opts?.hidden) {
return opts?.windowId
}

if (opts.windowId !== undefined) {
const windows = await this.listWindows()
const targetWindow = windows.find(
(window) => window.windowId === opts.windowId,
)
if (targetWindow && !targetWindow.isVisible) {
return targetWindow.windowId
}
if (targetWindow?.isVisible) {
logger.warn(
'Requested hidden page target window is visible, creating a new hidden window instead',
{
requestedWindowId: opts.windowId,
},
)
}
}

const hiddenWindow = await this.createWindow({ hidden: true })
return hiddenWindow.windowId
}

async newPage(
url: string,
opts?: { hidden?: boolean; background?: boolean; windowId?: number },
): Promise<number> {
const windowId = await this.resolveWindowIdForNewPage(opts)
const createResult = await this.cdp.Browser.createTab({
url,
...(opts?.background !== undefined && { background: opts.background }),
...(windowId !== undefined && { windowId }),
...(opts?.hidden !== undefined && { hidden: opts.hidden }),
...(opts?.windowId !== undefined && { windowId: opts.windowId }),
})

const tabId = (createResult.tab as TabInfo).tabId
Expand Down Expand Up @@ -583,7 +553,7 @@ export class Browser {
loadProgress: tabInfo.loadProgress,
isPinned: tabInfo.isPinned,
isHidden: tabInfo.isHidden,
windowId: tabInfo.windowId ?? windowId,
windowId: tabInfo.windowId ?? opts?.windowId,
index: tabInfo.index,
groupId: tabInfo.groupId,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@ describe('mode-aware framing', () => {
expect(prompt).toContain('Do NOT create new windows')
expect(prompt).toContain('Close extra hidden pages')
})

it('visible scheduled task mode excludes hidden page rules', () => {
const prompt = buildScheduled({ scheduledTaskRunSilently: false })
expect(prompt).toContain('scheduled background task')
expect(prompt).not.toContain('system-managed hidden page')
expect(prompt).not.toContain('Do NOT close your starting hidden page')
expect(prompt).toContain('new_page')
})
})

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading