@datalayer/agent-runtimes 1.3.106 → 1.3.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,6 +88,21 @@ const texts = (value) => Array.isArray(value)
88
88
  ? value.filter((item) => typeof item === 'string')
89
89
  : [];
90
90
  const records = (value) => Array.isArray(value) ? value.filter(isData) : [];
91
+ /**
92
+ * One turn of a test's conversation as its document says it (STUDIO E-01):
93
+ * what it says, chooses or presses, with a press's payload; nothing it does
94
+ * not say.
95
+ */
96
+ function parseTestTurn(turn) {
97
+ return {
98
+ ...(text(turn.say) ? { say: text(turn.say) } : {}),
99
+ ...(text(turn.choose) ? { choose: text(turn.choose) } : {}),
100
+ ...(text(turn.press) ? { press: text(turn.press) } : {}),
101
+ ...(isData(turn.payload) && Object.keys(turn.payload).length
102
+ ? { payload: { ...turn.payload } }
103
+ : {}),
104
+ };
105
+ }
91
106
  const oneOf = (value, allowed, otherwise) => (allowed.includes(value) ? value : otherwise);
92
107
  const RETENTION = /^([1-9]\d*)_(day|days|month|months|year|years)$/;
93
108
  const DAYS = { day: 1, month: 30, year: 365 };
@@ -691,6 +706,10 @@ export function parseAppspec(document) {
691
706
  evalset: text(tests.evalset),
692
707
  cases: records(tests.cases).map(testCase => ({
693
708
  ask: text(testCase.ask),
709
+ // A conversation, turn by turn, when it is one (STUDIO E-01).
710
+ ...(records(testCase.turns).length
711
+ ? { turns: records(testCase.turns).map(parseTestTurn) }
712
+ : {}),
694
713
  expect: text(testCase.expect),
695
714
  // Decided by its code (LOOP P-06), when it names the function.
696
715
  ...(text(testCase.code) ? { code: text(testCase.code) } : {}),
@@ -1018,7 +1037,12 @@ export function dumpAppspec(app) {
1018
1037
  .value('ready_at', app.tests.readyAt, DEFAULT_READY_AT)
1019
1038
  .text('evalset', app.tests.evalset)
1020
1039
  .list('cases', app.tests.cases.map(testCase => ({
1021
- ask: testCase.ask,
1040
+ // One message, or a conversation in its place (STUDIO E-01).
1041
+ ...(testCase.turns?.length
1042
+ ? {
1043
+ turns: testCase.turns.map(turn => parseTestTurn({ ...turn })),
1044
+ }
1045
+ : { ask: testCase.ask }),
1022
1046
  expect: testCase.expect,
1023
1047
  ...(testCase.code ? { code: testCase.code } : {}),
1024
1048
  ...(testCase.files?.length
@@ -1031,8 +1031,30 @@ export function documentShapeProblems(document) {
1031
1031
  }
1032
1032
  text(tests.evalset, 'tests.evalset');
1033
1033
  records(tests.cases, 'tests.cases', (item, where) => {
1034
- required(item, 'ask', where);
1034
+ // One message (`ask`) or a conversation (`turns`), never both (E-01).
1035
+ const asks = typeof item.ask === 'string' && item.ask.trim() !== '';
1036
+ const converses = Array.isArray(item.turns) && item.turns.length > 0;
1037
+ if (asks === converses) {
1038
+ at(where, 'asks one message (ask) or has a conversation (turns)');
1039
+ }
1040
+ text(item.ask, `${where}.ask`);
1035
1041
  required(item, 'expect', where);
1042
+ records(item.turns, `${where}.turns`, (turn, place) => {
1043
+ const does = ['say', 'choose', 'press'].filter(key => {
1044
+ const said = turn[key];
1045
+ return typeof said === 'string' && said.trim() !== '';
1046
+ });
1047
+ if (does.length !== 1) {
1048
+ at(place, 'says, chooses or presses one thing');
1049
+ }
1050
+ if (turn.payload !== undefined && !does.includes('press')) {
1051
+ at(`${place}.payload`, 'goes with an action pressed');
1052
+ }
1053
+ mapping(turn.payload, `${place}.payload`, () => undefined);
1054
+ });
1055
+ if (converses && item.files !== undefined) {
1056
+ at(`${where}.files`, "go with the message it asks: a conversation's turns take none");
1057
+ }
1036
1058
  records(item.files, `${where}.files`, (file, at) => {
1037
1059
  required(file, 'name', at);
1038
1060
  required(file, 'text', at);
@@ -2,7 +2,8 @@
2
2
  * An application's computer (LOOP R-23, I-01): the sandbox its agent runs on,
3
3
  * as the computer view shows it — its three parts, each off until it is
4
4
  * turned on; what its agent ran on it, from the conversation's tool calls;
5
- * its files, read-only; and *Take over* and *Hand back*, through the
5
+ * its files, read-only; its browser's page, live; and *Take over* and
6
+ * *Hand back* — the person then clicks and types in that page — through the
6
7
  * runtime's `/api/v1/apps/agents/{agent}/computer` routes.
7
8
  *
8
9
  * Pure but for `fetch`: no React.
@@ -25,7 +26,17 @@ export declare const COMPUTER_WORDS: {
25
26
  readonly off: "off";
26
27
  readonly none: "It has no computer: browse, files and shell are all off. Turn one on in its permissions to give it one.";
27
28
  readonly offSaid: (parts: string[]) => string;
28
- readonly noBrowser: "Browser: no browser runs on its computer yet, so there is nothing to show.";
29
+ readonly browser: "Browser";
30
+ readonly noBrowser: "No browser is installed on its computer.";
31
+ readonly noPage: "Its browser has opened no page yet.";
32
+ readonly pageAlt: (title: string) => string;
33
+ readonly clickToAct: "Click on the page to click there; with the page selected, what you type goes to it.";
34
+ readonly typeOnPage: "Text to type on the page";
35
+ readonly type: "Type";
36
+ readonly address: "An address to open";
37
+ readonly open: "Open";
38
+ readonly scrollUp: "Scroll up";
39
+ readonly scrollDown: "Scroll down";
29
40
  readonly agentHas: "Its agent has the computer.";
30
41
  readonly youHave: "You have the computer: its agent waits for it until you hand it back.";
31
42
  readonly someoneHas: "Somebody else has taken this computer over.";
@@ -84,7 +95,10 @@ export type ComputerState = {
84
95
  agent: string;
85
96
  app: string;
86
97
  parts: ComputerParts;
98
+ /** Whether a browser is installed where its agent runs, browse on. */
87
99
  browser: boolean;
100
+ /** The page its browser has open; none before its first, or browse off. */
101
+ page?: BrowserPage | null;
88
102
  started: boolean;
89
103
  held: ComputerHolder | null;
90
104
  /** Whether the person asking is who has it. */
@@ -96,6 +110,11 @@ export type ComputerState = {
96
110
  */
97
111
  servedFrom?: string;
98
112
  };
113
+ /** A page its browser has open. */
114
+ export type BrowserPage = {
115
+ url: string;
116
+ title: string;
117
+ };
99
118
  /** One entry of a directory of its computer. */
100
119
  export type ComputerFile = {
101
120
  name: string;
@@ -154,5 +173,39 @@ export declare const takeOverComputer: (context: ComputerContext) => Promise<{
154
173
  export declare const handBackComputer: (context: ComputerContext) => Promise<void>;
155
174
  /** Run code on it, as the person who took it over. */
156
175
  export declare const runOnComputer: (context: ComputerContext, code: string) => Promise<RunOutput>;
176
+ /** The keys a person presses on its page, as the runtime takes them. */
177
+ export declare const BROWSER_KEYS: readonly ["Enter", "Tab", "Backspace", "Escape", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown"];
178
+ /**
179
+ * The page its browser shows, as a PNG; undefined before its first page
180
+ * (the runtime answers 404).
181
+ */
182
+ export declare const readBrowserScreenshot: (context: ComputerContext) => Promise<Blob | undefined>;
183
+ /** Click on its page, at a point of its screenshot, as who took it over. */
184
+ export declare const clickOnBrowser: (context: ComputerContext, x: number, y: number) => Promise<BrowserPage>;
185
+ /** Type where its page's focus is. */
186
+ export declare const typeOnBrowser: (context: ComputerContext, text: string) => Promise<BrowserPage>;
187
+ /** Press one of `BROWSER_KEYS` on its page. */
188
+ export declare const pressOnBrowser: (context: ComputerContext, key: string) => Promise<BrowserPage>;
189
+ /** Scroll its page, down for a positive `dy`. */
190
+ export declare const scrollBrowser: (context: ComputerContext, dy: number) => Promise<BrowserPage>;
191
+ /** Open an address in its browser. */
192
+ export declare const openOnBrowser: (context: ComputerContext, url: string) => Promise<BrowserPage>;
193
+ /**
194
+ * Where a click on a screenshot drawn at another size lands on the page: its
195
+ * pixels are the page's (the runtime takes it at the page's own size).
196
+ */
197
+ export declare function pointOnPage(clicked: {
198
+ x: number;
199
+ y: number;
200
+ }, drawn: {
201
+ width: number;
202
+ height: number;
203
+ }, natural: {
204
+ width: number;
205
+ height: number;
206
+ }): {
207
+ x: number;
208
+ y: number;
209
+ };
157
210
  /** The directory above a path of its working directory. */
158
211
  export declare function parentOf(path: string): string;
@@ -6,8 +6,13 @@
6
6
  export const COMPUTER_PARTS = ['browse', 'files', 'shell'];
7
7
  /** The tools of each part, as the runtime gives them (`loop/apps/computer.py`). */
8
8
  export const COMPUTER_PART_TOOLS = {
9
- // No sandbox Datalayer runs has a browser yet.
10
- browse: [],
9
+ browse: [
10
+ 'open_page',
11
+ 'read_page',
12
+ 'page_screenshot',
13
+ 'click_on_page',
14
+ 'type_on_page',
15
+ ],
11
16
  files: ['list_computer_files', 'read_computer_file', 'write_computer_file'],
12
17
  shell: ['execute_code', 'run_skill_script'],
13
18
  };
@@ -23,7 +28,17 @@ export const COMPUTER_WORDS = {
23
28
  off: 'off',
24
29
  none: 'It has no computer: browse, files and shell are all off. Turn one on in its permissions to give it one.',
25
30
  offSaid: (parts) => `Off, so its agent is given no tool for it: ${parts.join(', ')}.`,
26
- noBrowser: 'Browser: no browser runs on its computer yet, so there is nothing to show.',
31
+ browser: 'Browser',
32
+ noBrowser: 'No browser is installed on its computer.',
33
+ noPage: 'Its browser has opened no page yet.',
34
+ pageAlt: (title) => `The page its browser shows: ${title}`,
35
+ clickToAct: 'Click on the page to click there; with the page selected, what you type goes to it.',
36
+ typeOnPage: 'Text to type on the page',
37
+ type: 'Type',
38
+ address: 'An address to open',
39
+ open: 'Open',
40
+ scrollUp: 'Scroll up',
41
+ scrollDown: 'Scroll down',
27
42
  agentHas: 'Its agent has the computer.',
28
43
  youHave: 'You have the computer: its agent waits for it until you hand it back.',
29
44
  someoneHas: 'Somebody else has taken this computer over.',
@@ -105,7 +120,7 @@ export function outputOf(value) {
105
120
  : text;
106
121
  }
107
122
  function commandOf(args) {
108
- for (const key of ['code', 'path', 'script_name']) {
123
+ for (const key of ['code', 'path', 'script_name', 'url', 'selector']) {
109
124
  const value = args[key];
110
125
  if (typeof value === 'string' && value.trim()) {
111
126
  return value;
@@ -225,6 +240,69 @@ export const runOnComputer = async (context, code) => (await asked(context, '/ru
225
240
  method: 'POST',
226
241
  body: JSON.stringify({ code }),
227
242
  })).json();
243
+ /** The keys a person presses on its page, as the runtime takes them. */
244
+ export const BROWSER_KEYS = [
245
+ 'Enter',
246
+ 'Tab',
247
+ 'Backspace',
248
+ 'Escape',
249
+ 'ArrowUp',
250
+ 'ArrowDown',
251
+ 'ArrowLeft',
252
+ 'ArrowRight',
253
+ 'PageUp',
254
+ 'PageDown',
255
+ ];
256
+ /**
257
+ * The page its browser shows, as a PNG; undefined before its first page
258
+ * (the runtime answers 404).
259
+ */
260
+ export const readBrowserScreenshot = async (context) => {
261
+ const response = await fetch(computerUrl(context, '/browser/screenshot'), {
262
+ headers: context.token ? { Authorization: `Bearer ${context.token}` } : {},
263
+ cache: 'no-store',
264
+ });
265
+ if (response.status === 404) {
266
+ return undefined;
267
+ }
268
+ if (!response.ok) {
269
+ let detail = `${response.status}`;
270
+ try {
271
+ const body = (await response.json());
272
+ detail = typeof body.detail === 'string' ? body.detail : detail;
273
+ }
274
+ catch {
275
+ // A refusal without a sentence: its status says it.
276
+ }
277
+ throw new Error(detail);
278
+ }
279
+ return response.blob();
280
+ };
281
+ const onBrowser = async (context, step, body) => (await (await asked(context, `/browser/${step}`, {
282
+ method: 'POST',
283
+ body: JSON.stringify(body),
284
+ })).json()).page;
285
+ /** Click on its page, at a point of its screenshot, as who took it over. */
286
+ export const clickOnBrowser = (context, x, y) => onBrowser(context, 'click', { x, y });
287
+ /** Type where its page's focus is. */
288
+ export const typeOnBrowser = (context, text) => onBrowser(context, 'type', { text });
289
+ /** Press one of `BROWSER_KEYS` on its page. */
290
+ export const pressOnBrowser = (context, key) => onBrowser(context, 'key', { key });
291
+ /** Scroll its page, down for a positive `dy`. */
292
+ export const scrollBrowser = (context, dy) => onBrowser(context, 'scroll', { dy });
293
+ /** Open an address in its browser. */
294
+ export const openOnBrowser = (context, url) => onBrowser(context, 'open', { url });
295
+ /**
296
+ * Where a click on a screenshot drawn at another size lands on the page: its
297
+ * pixels are the page's (the runtime takes it at the page's own size).
298
+ */
299
+ export function pointOnPage(clicked, drawn, natural) {
300
+ const scale = (value, from, to) => from > 0 ? Math.round((value * to) / from) : value;
301
+ return {
302
+ x: Math.max(0, Math.min(natural.width, scale(clicked.x, drawn.width, natural.width))),
303
+ y: Math.max(0, Math.min(natural.height, scale(clicked.y, drawn.height, natural.height))),
304
+ };
305
+ }
228
306
  /** The directory above a path of its working directory. */
229
307
  export function parentOf(path) {
230
308
  const parts = path.split('/').filter(part => part && part !== '.');
@@ -8,16 +8,20 @@
8
8
  * wrote, each with what came back, streamed from the conversation's tool
9
9
  * calls;
10
10
  * - its **files**, read-only, each to download, read again as each turn ends;
11
- * - its **browser**: none runs on any sandbox yet, and it says so;
11
+ * - its **browser**, when browse is on: the page it has open, live — a
12
+ * screenshot read again every moment;
12
13
  * - **Take over**: what runs on it is interrupted and its agent's calls to it
13
- * wait while the person runs code on it themselves; **Hand back** lets its
14
- * agent go on.
14
+ * wait while the person runs code on it themselves, and clicks and types
15
+ * in its page — a click on the screenshot is a click there; **Hand back**
16
+ * lets its agent go on.
15
17
  *
16
18
  * @module apps/plugins/app-computer/AppComputer
17
19
  */
18
20
  import type { JSX } from 'react';
19
21
  import type { AppSpec } from '../../../types/agentspecs';
20
22
  import { type LoopWorkspaceContext } from '../../core';
23
+ /** How often the page its browser shows is read again, in milliseconds. */
24
+ export declare const BROWSER_POLL_MS = 1500;
21
25
  export declare function AppComputer({ app, workspace, }: {
22
26
  app: AppSpec;
23
27
  workspace?: LoopWorkspaceContext;
@@ -1,13 +1,13 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { Button, Heading, Label, Spinner, Text, Textarea } from '@primer/react';
3
+ import { Button, Heading, Label, Spinner, Text, TextInput, Textarea, } from '@primer/react';
4
4
  import { Box } from '@datalayer/primer-addons';
5
5
  import { signal } from '@datalayer/reactor';
6
6
  import { useContributions, useSignalValue } from '@datalayer/reactor/react';
7
7
  import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
8
8
  import { useChatWords } from '../../../chat/ChatLanguage';
9
9
  import { LoopChatTurn, agentServerOf, noRuntimeSaid, IDLE_SANDBOX_SNAPSHOT, } from '../../core';
10
- import { COMPUTER_PARTS, COMPUTER_WORDS, computerParts, downloadComputerFile, handBackComputer, hasComputer, listComputerFiles, parentOf, partsOff, readComputer, runOnComputer, takeOverComputer, terminalEntries, } from '../../apps/computer';
10
+ import { BROWSER_KEYS, COMPUTER_PARTS, COMPUTER_WORDS, clickOnBrowser, computerParts, downloadComputerFile, handBackComputer, hasComputer, listComputerFiles, openOnBrowser, parentOf, partsOff, pointOnPage, pressOnBrowser, readBrowserScreenshot, readComputer, runOnComputer, scrollBrowser, takeOverComputer, terminalEntries, typeOnBrowser, } from '../../apps/computer';
11
11
  const NO_TURN = signal({ id: 0, status: 'idle' });
12
12
  const NO_CONVERSATION = signal([]);
13
13
  const MONO_FACE = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
@@ -71,6 +71,93 @@ function Files({ context, state, ended, }) {
71
71
  : String(failed)));
72
72
  }, children: COMPUTER_WORDS.download })] })) }, entry.path)))] }))] }));
73
73
  }
74
+ /** How often the page its browser shows is read again, in milliseconds. */
75
+ export const BROWSER_POLL_MS = 1500;
76
+ function BrowserView({ context, state, mine, }) {
77
+ const [shot, setShot] = useState();
78
+ const [none, setNone] = useState(false);
79
+ const [page, setPage] = useState(state.page);
80
+ const [error, setError] = useState('');
81
+ const [text, setText] = useState('');
82
+ const [address, setAddress] = useState('');
83
+ const [asked, setAsked] = useState(0);
84
+ const shown = useRef(undefined);
85
+ useEffect(() => setPage(state.page), [state.page]);
86
+ useEffect(() => {
87
+ if (!state.browser) {
88
+ return undefined;
89
+ }
90
+ let cancelled = false;
91
+ const look = () => readBrowserScreenshot(context).then(blob => {
92
+ if (cancelled) {
93
+ return;
94
+ }
95
+ setError('');
96
+ setNone(!blob);
97
+ if (shown.current) {
98
+ URL.revokeObjectURL(shown.current);
99
+ }
100
+ shown.current = blob ? URL.createObjectURL(blob) : undefined;
101
+ setShot(shown.current);
102
+ }, (failed) => !cancelled &&
103
+ setError(failed instanceof Error ? failed.message : String(failed)));
104
+ void look();
105
+ const every = setInterval(look, BROWSER_POLL_MS);
106
+ return () => {
107
+ cancelled = true;
108
+ clearInterval(every);
109
+ };
110
+ // eslint-disable-next-line react-hooks/exhaustive-deps
111
+ }, [context.serverUrl, context.agentId, context.token, state.browser, asked]);
112
+ useEffect(() => () => {
113
+ if (shown.current) {
114
+ URL.revokeObjectURL(shown.current);
115
+ }
116
+ }, []);
117
+ const act = (step) => step.then(found => {
118
+ setPage(found);
119
+ setError('');
120
+ setAsked(previous => previous + 1);
121
+ }, (failed) => setError(failed instanceof Error ? failed.message : String(failed)));
122
+ const onClick = (event) => {
123
+ const image = event.currentTarget;
124
+ const box = image.getBoundingClientRect();
125
+ const point = pointOnPage({ x: event.clientX - box.left, y: event.clientY - box.top }, { width: box.width, height: box.height }, { width: image.naturalWidth, height: image.naturalHeight });
126
+ image.focus();
127
+ void act(clickOnBrowser(context, point.x, point.y));
128
+ };
129
+ const onKey = (event) => {
130
+ if (event.metaKey || event.ctrlKey || event.altKey) {
131
+ return;
132
+ }
133
+ if (BROWSER_KEYS.includes(event.key)) {
134
+ event.preventDefault();
135
+ void act(pressOnBrowser(context, event.key));
136
+ }
137
+ else if (event.key.length === 1) {
138
+ event.preventDefault();
139
+ void act(typeOnBrowser(context, event.key));
140
+ }
141
+ };
142
+ return (_jsxs(Box, { "data-testid": "computer-browser", children: [_jsxs(Heading, { as: "h4", sx: { fontSize: 1, mt: 3, mb: 1 }, children: [COMPUTER_WORDS.browser, page ? (_jsx(Text, { sx: { fontWeight: 'normal', color: 'fg.muted' }, children: ` · ${page.title || page.url}` })) : null] }), !state.browser ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: COMPUTER_WORDS.noBrowser })) : none || !shot ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: none ? COMPUTER_WORDS.noPage : null })) : (_jsxs(_Fragment, { children: [page ? (_jsx(Text, { as: "p", sx: {
143
+ fontFamily: MONO_FACE,
144
+ fontSize: 0,
145
+ color: 'fg.muted',
146
+ m: 0,
147
+ mb: 1,
148
+ wordBreak: 'break-all',
149
+ }, children: page.url })) : null, _jsx("img", { src: shot, alt: COMPUTER_WORDS.pageAlt(page?.title || page?.url || ''), "data-testid": "computer-browser-page", tabIndex: mine ? 0 : -1, onClick: mine ? onClick : undefined, onKeyDown: mine ? onKey : undefined, style: {
150
+ width: '100%',
151
+ height: 'auto',
152
+ display: 'block',
153
+ border: '1px solid var(--borderColor-muted, #d0d7de)',
154
+ borderRadius: 6,
155
+ cursor: mine ? 'pointer' : 'default',
156
+ } })] })), error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 1 }, children: error })) : null, mine && state.browser ? (_jsxs(Box, { mt: 2, "data-testid": "computer-browser-controls", children: [_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0, mb: 1 }, children: COMPUTER_WORDS.clickToAct }), _jsxs(Box, { display: "flex", gap: 1, mb: 1, children: [_jsx(TextInput, { size: "small", "aria-label": COMPUTER_WORDS.typeOnPage, placeholder: COMPUTER_WORDS.typeOnPage, value: text, onChange: event => setText(event.target.value), sx: { flex: 1 } }), _jsx(Button, { size: "small", disabled: !text, onClick: () => {
157
+ void act(typeOnBrowser(context, text));
158
+ setText('');
159
+ }, children: COMPUTER_WORDS.type }), _jsx(Button, { size: "small", onClick: () => void act(pressOnBrowser(context, 'Enter')), children: "Enter" })] }), _jsxs(Box, { display: "flex", gap: 1, mb: 1, children: [_jsx(TextInput, { size: "small", "aria-label": COMPUTER_WORDS.address, placeholder: COMPUTER_WORDS.address, value: address, onChange: event => setAddress(event.target.value), sx: { flex: 1 } }), _jsx(Button, { size: "small", disabled: !address.trim(), onClick: () => void act(openOnBrowser(context, address.trim())), children: COMPUTER_WORDS.open })] }), _jsxs(Box, { display: "flex", gap: 1, children: [_jsx(Button, { size: "small", onClick: () => void act(scrollBrowser(context, -400)), children: COMPUTER_WORDS.scrollUp }), _jsx(Button, { size: "small", onClick: () => void act(scrollBrowser(context, 400)), children: COMPUTER_WORDS.scrollDown })] })] })) : null] }));
160
+ }
74
161
  function YourTerminal({ context }) {
75
162
  const [code, setCode] = useState('');
76
163
  const [running, setRunning] = useState(false);
@@ -193,10 +280,10 @@ export function AppComputer({ app, workspace, }) {
193
280
  });
194
281
  };
195
282
  const me = Boolean(state?.yours);
196
- return (_jsxs(Box, { "data-testid": "app-computer", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: COMPUTER_WORDS.title }), _jsx(Parts, { app: app }), !used ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.none })) : (_jsxs(_Fragment, { children: [_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.noBrowser }), noRuntime ? (_jsx(Text, { as: "p", role: "status", sx: { fontSize: 1, color: 'fg.default', m: 0, mt: 2 }, children: noRuntime })) : null, error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 2 }, children: error })) : null, state && context ? (_jsxs(Box, { mt: 2, "data-testid": "computer-holder", children: [_jsx(Text, { as: "p", sx: { fontSize: 1, m: 0, mb: 1 }, children: !state.held
283
+ return (_jsxs(Box, { "data-testid": "app-computer", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: COMPUTER_WORDS.title }), _jsx(Parts, { app: app }), !used ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.none })) : (_jsxs(_Fragment, { children: [noRuntime ? (_jsx(Text, { as: "p", role: "status", sx: { fontSize: 1, color: 'fg.default', m: 0, mt: 2 }, children: noRuntime })) : null, error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 2 }, children: error })) : null, state && context ? (_jsxs(Box, { mt: 2, "data-testid": "computer-holder", children: [_jsx(Text, { as: "p", sx: { fontSize: 1, m: 0, mb: 1 }, children: !state.held
197
284
  ? COMPUTER_WORDS.agentHas
198
285
  : me
199
286
  ? COMPUTER_WORDS.youHave
200
- : COMPUTER_WORDS.someoneHas }), !state.held ? (_jsx(Button, { size: "small", disabled: busy, onClick: () => act(takeOverComputer), children: COMPUTER_WORDS.takeOver })) : me ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "small", variant: "primary", disabled: busy, onClick: () => act(handBackComputer), children: COMPUTER_WORDS.handBack }), _jsx(YourTerminal, { context: context })] })) : null] })) : null, _jsx(Terminal, { conversation: conversation }), state && context ? (_jsx(Files, { context: context, state: state, ended: ended })) : null] }))] }));
287
+ : COMPUTER_WORDS.someoneHas }), !state.held ? (_jsx(Button, { size: "small", disabled: busy, onClick: () => act(takeOverComputer), children: COMPUTER_WORDS.takeOver })) : me ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "small", variant: "primary", disabled: busy, onClick: () => act(handBackComputer), children: COMPUTER_WORDS.handBack }), _jsx(YourTerminal, { context: context })] })) : null] })) : null, parts.browse && state && context ? (_jsx(BrowserView, { context: context, state: state, mine: me })) : null, _jsx(Terminal, { conversation: conversation }), state && context ? (_jsx(Files, { context: context, state: state, ended: ended })) : null] }))] }));
201
288
  }
202
289
  export default AppComputer;
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * An application's computer, beside its page (LOOP R-23).
3
3
  *
4
- * An application whose files and shell are on, drawn as the Studio's Preview
4
+ * An application whose browse, files and shell are on, drawn as the Studio's Preview
5
5
  * draws it — `AppRenderer` with its `sidebar` — on the Local target: its
6
6
  * agent runs on the local agent-runtimes server, and the computer view beside
7
- * it shows what ran on its sandbox, its files, and *Take over* and *Hand
8
- * back*.
7
+ * it shows the page its browser has open, what ran on its sandbox, its
8
+ * files, and *Take over* — clicks and typing then go to its page — and
9
+ * *Hand back*.
9
10
  *
10
11
  * @module examples/LoopAppComputerExample
11
12
  */
@@ -6,11 +6,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  /**
7
7
  * An application's computer, beside its page (LOOP R-23).
8
8
  *
9
- * An application whose files and shell are on, drawn as the Studio's Preview
9
+ * An application whose browse, files and shell are on, drawn as the Studio's Preview
10
10
  * draws it — `AppRenderer` with its `sidebar` — on the Local target: its
11
11
  * agent runs on the local agent-runtimes server, and the computer view beside
12
- * it shows what ran on its sandbox, its files, and *Take over* and *Hand
13
- * back*.
12
+ * it shows the page its browser has open, what ran on its sandbox, its
13
+ * files, and *Take over* — clicks and typing then go to its page — and
14
+ * *Hand back*.
14
15
  *
15
16
  * @module examples/LoopAppComputerExample
16
17
  */
@@ -29,14 +30,18 @@ function computerDesk() {
29
30
  id: 'computer-desk',
30
31
  name: 'Computer Desk',
31
32
  emoji: '🖥️',
32
- description: 'Works on its own computer: runs code, reads and writes files.',
33
+ description: 'Works on its own computer: opens web pages, runs code, reads and writes files.',
33
34
  connections: [],
34
35
  rules: [],
35
- instructions: 'You have a computer: run Python on it with your tools, and keep what you make in files there.',
36
+ instructions: 'You have a computer: open web pages in its browser, run Python on it with your tools, and keep what you make in files there.',
36
37
  interface: {
37
38
  ...base.interface,
38
39
  welcome: 'I work on my own computer. Ask me to compute something.',
39
40
  starters: [
41
+ {
42
+ label: 'Open a page',
43
+ message: 'Open https://example.com in your browser, tell me its title and take a screenshot.',
44
+ },
40
45
  {
41
46
  label: 'Write a file',
42
47
  message: 'Run Python that writes the squares of 1 to 5 in squares.txt, then print the file.',
@@ -45,7 +50,7 @@ function computerDesk() {
45
50
  },
46
51
  permissions: {
47
52
  ...base.permissions,
48
- computer: { browse: false, files: true, shell: true },
53
+ computer: { browse: true, files: true, shell: true },
49
54
  },
50
55
  };
51
56
  }