@datalayer/agent-runtimes 1.3.48 → 1.3.50

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.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * An application's computer, beside its page (LOOP R-23).
3
+ *
4
+ * An application whose files and shell are on, drawn as the Studio's Preview
5
+ * draws it — `AppRenderer` with its `sidebar` — on the Local target: its
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*.
9
+ *
10
+ * @module examples/LoopAppComputerExample
11
+ */
12
+ import React from 'react';
13
+ declare const LoopAppComputerExample: React.FC;
14
+ export default LoopAppComputerExample;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ /**
7
+ * An application's computer, beside its page (LOOP R-23).
8
+ *
9
+ * An application whose files and shell are on, drawn as the Studio's Preview
10
+ * draws it — `AppRenderer` with its `sidebar` — on the Local target: its
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*.
14
+ *
15
+ * @module examples/LoopAppComputerExample
16
+ */
17
+ import { useMemo } from 'react';
18
+ import { Box, setupPrimerPortals } from '@datalayer/primer-addons';
19
+ import { ThemedProvider } from './utils/themedProvider';
20
+ import { resolveExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
21
+ import { AppRenderer } from '../loop/apps/AppRenderer';
22
+ import { APP_CATALOGUE } from '../specs/apps';
23
+ setupPrimerPortals();
24
+ /** Web Research, given a computer with its files and its shell on. */
25
+ function computerDesk() {
26
+ const base = APP_CATALOGUE['web-research'];
27
+ return {
28
+ ...base,
29
+ id: 'computer-desk',
30
+ name: 'Computer Desk',
31
+ emoji: '🖥️',
32
+ description: 'Works on its own computer: runs code, reads and writes files.',
33
+ connections: [],
34
+ rules: [],
35
+ instructions: 'You have a computer: run Python on it with your tools, and keep what you make in files there.',
36
+ interface: {
37
+ ...base.interface,
38
+ welcome: 'I work on my own computer. Ask me to compute something.',
39
+ starters: [
40
+ {
41
+ label: 'Write a file',
42
+ message: 'Run Python that writes the squares of 1 to 5 in squares.txt, then print the file.',
43
+ },
44
+ ],
45
+ },
46
+ permissions: {
47
+ ...base.permissions,
48
+ computer: { browse: false, files: true, shell: true },
49
+ },
50
+ };
51
+ }
52
+ const LoopAppComputerExample = () => {
53
+ const app = useMemo(computerDesk, []);
54
+ return (_jsx(ThemedProvider, { children: _jsx(Box, { sx: { height: '100vh', minHeight: 0 }, children: _jsx(AppRenderer, { app: app,
55
+ // The examples Vite server has no /api proxy.
56
+ serverUrl: resolveExampleAgentRuntimesUrl('local'), target: "local", sidebar: true }) }) }));
57
+ };
58
+ export default LoopAppComputerExample;
@@ -106,6 +106,7 @@ export const EXAMPLE_ENTRIES = [
106
106
  // the shell hides the one in its header rather than showing two controls
107
107
  // for one sandbox — and a second one that would not agree with the first.
108
108
  ['example', 'loop', 'workspace', 'sandbox', 'owns-sandbox-control']),
109
+ makeEntry('LoopAppComputerExample', () => import('./LoopAppComputerExample'), 'An application beside its computer: what its agent ran on its sandbox, its files, and Take over and Hand back (LOOP R-23).', ['example', 'loop', 'app', 'computer', 'sandbox', 'owns-sandbox-control']),
109
110
  makeEntry('LoopShellExample', () => import('./LoopShellExample'), 'The Loop shell at its most naked: a blank canvas, a floating draggable prompt, and an editor selector in the corner — none, notebook or document.',
110
111
  // `owns-sandbox-control`: the shell is pinned to the browser sandbox, so
111
112
  // the page's selector would offer three targets it cannot move to.
@@ -67,6 +67,9 @@ const EXAMPLE_GROUP_ORDER = [
67
67
  */
68
68
  const ANONYMOUS_EXAMPLES = new Set([
69
69
  'LoopWorkspaceExample',
70
+ // An application beside its computer, on the local agent-runtimes server:
71
+ // nothing allocated on Datalayer, and the machine itself needs no token.
72
+ 'LoopAppComputerExample',
70
73
  // Runs on the browser sandbox: nothing to allocate, nothing to sign into.
71
74
  'LoopShellExample',
72
75
  // Temporarily anonymous so the Jupyter output surface can be driven and
@@ -29,9 +29,9 @@
29
29
  * - its **frame**, when the host asks (`frame`): the `window-frame` plugin's
30
30
  * window, its title the application's face and name;
31
31
  * - its **sidebar**, when the host asks (`sidebar`, R-01b): its rules and
32
- * the approvals waiting for the person (`app-rules`), and what it did
33
- * (`app-activity`), beside its page — what its builder reads in the
34
- * Studio's Preview.
32
+ * the approvals waiting for the person (`app-rules`), what it did
33
+ * (`app-activity`), and its computer, live (`app-computer`, R-23), beside
34
+ * its page — what its builder reads in the Studio's Preview.
35
35
  *
36
36
  * Which plugins a kind needs, and how its workspace is laid out, is one
37
37
  * function: `appPreset`, the preset per kind, as `loopPlugins` is for the
@@ -153,7 +153,8 @@ export type AppPreset = {
153
153
  * but those of the UI plugins its organization turned off (`pluginsOff`),
154
154
  * whose contributions are the blocks it may draw (R-01b); a thumb and a
155
155
  * comment on each answer when its record keeps feedback (V-18); with
156
- * `sidebar`, its rules and approvals card and its activity feed (R-01b);
156
+ * `sidebar`, its rules and approvals card, its activity feed and its
157
+ * computer (R-01b, R-23);
157
158
  * laid out as `interface.layout` says;
158
159
  * - a **decision**: the page its host draws (`page`), as the one view of a
159
160
  * workspace without a conversation (R-02).
@@ -164,7 +165,7 @@ export type AppPreset = {
164
165
  export declare function appPreset(app: AppSpec, options?: {
165
166
  page?: ComponentType<AppHostPageProps>;
166
167
  pluginsOff?: readonly string[];
167
- /** Its rules and approvals, and its activity, beside its page. */
168
+ /** Its rules and approvals, its activity and its computer, beside its page. */
168
169
  sidebar?: boolean;
169
170
  /** What its record is kept under: its activity's. */
170
171
  appUid?: string;
@@ -34,9 +34,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
34
34
  * - its **frame**, when the host asks (`frame`): the `window-frame` plugin's
35
35
  * window, its title the application's face and name;
36
36
  * - its **sidebar**, when the host asks (`sidebar`, R-01b): its rules and
37
- * the approvals waiting for the person (`app-rules`), and what it did
38
- * (`app-activity`), beside its page — what its builder reads in the
39
- * Studio's Preview.
37
+ * the approvals waiting for the person (`app-rules`), what it did
38
+ * (`app-activity`), and its computer, live (`app-computer`, R-23), beside
39
+ * its page — what its builder reads in the Studio's Preview.
40
40
  *
41
41
  * Which plugins a kind needs, and how its workspace is laid out, is one
42
42
  * function: `appPreset`, the preset per kind, as `loopPlugins` is for the
@@ -52,6 +52,7 @@ import { CANVAS_BLOCK_PLUGINS, canvasBlocksPluginName, } from '../plugins/canvas
52
52
  import { LoopEmbed } from '../embed/LoopEmbed';
53
53
  import { dumpAppspec } from './appspec';
54
54
  import { defineAppActivityPlugin } from '../plugins/app-activity';
55
+ import { defineAppComputerPlugin } from '../plugins/app-computer';
55
56
  import { defineAppRulesPlugin } from '../plugins/app-rules';
56
57
  import { defineAppFeedbackPlugin } from './AppFeedback';
57
58
  import { keepsFeedback } from './feedback';
@@ -88,6 +89,9 @@ export function defineAppPlugin(app) {
88
89
  createPayload: {
89
90
  name: app.id,
90
91
  app_spec: dumpAppspec(app),
92
+ // Its shell is its code: Codemode's `execute_code`, only when it is on
93
+ // (LOOP R-23), as on Datalayer (`appDatalayerCreatePayload`).
94
+ enable_codemode: Boolean(app.permissions?.computer?.shell),
91
95
  ...(app.model ? { model: app.model } : {}),
92
96
  },
93
97
  suggestions: app.interface.starters.map(starter => ({
@@ -141,7 +145,8 @@ export function appLayoutOptions(app) {
141
145
  * but those of the UI plugins its organization turned off (`pluginsOff`),
142
146
  * whose contributions are the blocks it may draw (R-01b); a thumb and a
143
147
  * comment on each answer when its record keeps feedback (V-18); with
144
- * `sidebar`, its rules and approvals card and its activity feed (R-01b);
148
+ * `sidebar`, its rules and approvals card, its activity feed and its
149
+ * computer (R-01b, R-23);
145
150
  * laid out as `interface.layout` says;
146
151
  * - a **decision**: the page its host draws (`page`), as the one view of a
147
152
  * workspace without a conversation (R-02).
@@ -180,6 +185,8 @@ export function appPreset(app, options = {}) {
180
185
  ? [
181
186
  defineAppRulesPlugin(app),
182
187
  defineAppActivityPlugin(app, options.appUid),
188
+ // Its computer, live (R-23).
189
+ defineAppComputerPlugin(app),
183
190
  ]
184
191
  : []),
185
192
  ],
@@ -0,0 +1,134 @@
1
+ /**
2
+ * An application's computer (LOOP R-23, I-01): the sandbox its agent runs on,
3
+ * as the computer view shows it — its three parts, each off until it is
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
6
+ * runtime's `/api/v1/apps/agents/{agent}/computer` routes.
7
+ *
8
+ * Pure but for `fetch`: no React.
9
+ *
10
+ * @module loop/apps/computer
11
+ */
12
+ import type { AppSpec } from '../../types/agentspecs';
13
+ import type { ConversationEntry } from '../core';
14
+ /** The parts of a computer, in the order a person reads them. */
15
+ export declare const COMPUTER_PARTS: readonly ["browse", "files", "shell"];
16
+ export type ComputerPart = (typeof COMPUTER_PARTS)[number];
17
+ export type ComputerParts = Record<ComputerPart, boolean>;
18
+ /** The tools of each part, as the runtime gives them (`loop/apps/computer.py`). */
19
+ export declare const COMPUTER_PART_TOOLS: Record<ComputerPart, readonly string[]>;
20
+ /** What the computer view says. */
21
+ export declare const COMPUTER_WORDS: {
22
+ readonly title: "Computer";
23
+ readonly part: Record<ComputerPart, string>;
24
+ readonly on: "on";
25
+ readonly off: "off";
26
+ readonly none: "It has no computer: browse, files and shell are all off. Turn one on in its permissions to give it one.";
27
+ readonly offSaid: (parts: string[]) => string;
28
+ readonly noBrowser: "Browser: no browser runs on its computer yet, so there is nothing to show.";
29
+ readonly agentHas: "Its agent has the computer.";
30
+ readonly youHave: "You have the computer: its agent waits for it until you hand it back.";
31
+ readonly someoneHas: "Somebody else has taken this computer over.";
32
+ readonly takeOver: "Take over";
33
+ readonly handBack: "Hand back";
34
+ readonly run: "Run";
35
+ readonly yourCode: "Python to run on its computer";
36
+ readonly terminal: "What ran on it";
37
+ readonly nothingRan: "Nothing has run on it yet in this conversation.";
38
+ readonly files: "Files";
39
+ readonly notStarted: "Its computer starts when its agent first uses it: no file yet.";
40
+ readonly emptyDirectory: "Empty.";
41
+ readonly up: "Up";
42
+ readonly download: "Download";
43
+ readonly signedOut: "Its computer is shown once you are signed in to Datalayer.";
44
+ };
45
+ /** Each part of an application's computer, on or off, as its Appspec says. */
46
+ export declare function computerParts(app: AppSpec): ComputerParts;
47
+ /** Whether its agent uses a computer at all. */
48
+ export declare const hasComputer: (parts: ComputerParts) => boolean;
49
+ /** The parts turned off, in words. */
50
+ export declare const partsOff: (parts: ComputerParts) => string[];
51
+ /** The part of the computer a tool uses, or undefined. */
52
+ export declare function partOf(toolName: string): ComputerPart | undefined;
53
+ /** One thing its agent did on its computer: the command, and what it said. */
54
+ export type TerminalEntry = {
55
+ tool: string;
56
+ part: ComputerPart;
57
+ /** The code run, or the file's path. */
58
+ command: string;
59
+ /** What came back; undefined while it runs. */
60
+ output?: string;
61
+ };
62
+ /** The most of an output the view shows. */
63
+ export declare const OUTPUT_LIMIT = 4000;
64
+ /**
65
+ * What a tool of its computer returned, as a terminal shows it: the printed
66
+ * output and the error of code run (Codemode answers with an object, or its
67
+ * JSON), anything else as it came.
68
+ */
69
+ export declare function outputOf(value: unknown): string;
70
+ /**
71
+ * What its agent ran on its computer, in order: the conversation's calls to
72
+ * the tools of its shell and of its files, each with what it returned —
73
+ * streamed, as the conversation is.
74
+ */
75
+ export declare function terminalEntries(conversation: readonly ConversationEntry[]): TerminalEntry[];
76
+ /** Who has its computer, as the runtime says. */
77
+ export type ComputerHolder = {
78
+ kind: string;
79
+ uid: string;
80
+ since: number;
81
+ };
82
+ /** Its computer, as the runtime describes it. */
83
+ export type ComputerState = {
84
+ agent: string;
85
+ app: string;
86
+ parts: ComputerParts;
87
+ browser: boolean;
88
+ started: boolean;
89
+ held: ComputerHolder | null;
90
+ /** Whether the person asking is who has it. */
91
+ yours: boolean;
92
+ };
93
+ /** One entry of a directory of its computer. */
94
+ export type ComputerFile = {
95
+ name: string;
96
+ path: string;
97
+ type: 'file' | 'directory';
98
+ size: number;
99
+ modified: number;
100
+ };
101
+ /** What a person's code said on its computer. */
102
+ export type RunOutput = {
103
+ stdout: string;
104
+ stderr: string;
105
+ error: string;
106
+ };
107
+ /** Where its computer is reached, and as whom. */
108
+ export type ComputerContext = {
109
+ /** The agent-runtimes server its agent runs on. */
110
+ serverUrl: string;
111
+ /** Its agent, as the runtime names it. */
112
+ agentId: string;
113
+ /** The person's token; none on the machine itself. */
114
+ token?: string;
115
+ };
116
+ /** The computer's routes, for an agent. */
117
+ export declare const computerUrl: (context: ComputerContext, tail?: string) => string;
118
+ /** Its parts, whether it started, who has it. */
119
+ export declare const readComputer: (context: ComputerContext) => Promise<ComputerState>;
120
+ /** A directory of its working directory. */
121
+ export declare const listComputerFiles: (context: ComputerContext, path?: string) => Promise<ComputerFile[]>;
122
+ /** One of its files, to save. */
123
+ export declare const downloadComputerFile: (context: ComputerContext, path: string) => Promise<Blob>;
124
+ /** Take it over: what runs is interrupted, its agent's calls wait. */
125
+ export declare const takeOverComputer: (context: ComputerContext) => Promise<{
126
+ held: ComputerHolder;
127
+ interrupted: boolean;
128
+ }>;
129
+ /** Hand it back to its agent. */
130
+ export declare const handBackComputer: (context: ComputerContext) => Promise<void>;
131
+ /** Run code on it, as the person who took it over. */
132
+ export declare const runOnComputer: (context: ComputerContext, code: string) => Promise<RunOutput>;
133
+ /** The directory above a path of its working directory. */
134
+ export declare function parentOf(path: string): string;
@@ -0,0 +1,190 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The parts of a computer, in the order a person reads them. */
6
+ export const COMPUTER_PARTS = ['browse', 'files', 'shell'];
7
+ /** The tools of each part, as the runtime gives them (`loop/apps/computer.py`). */
8
+ export const COMPUTER_PART_TOOLS = {
9
+ // No sandbox Datalayer runs has a browser yet.
10
+ browse: [],
11
+ files: ['list_computer_files', 'read_computer_file', 'write_computer_file'],
12
+ shell: ['execute_code', 'run_skill_script'],
13
+ };
14
+ /** What the computer view says. */
15
+ export const COMPUTER_WORDS = {
16
+ title: 'Computer',
17
+ part: {
18
+ browse: 'Browse',
19
+ files: 'Files',
20
+ shell: 'Shell',
21
+ },
22
+ on: 'on',
23
+ off: 'off',
24
+ none: 'It has no computer: browse, files and shell are all off. Turn one on in its permissions to give it one.',
25
+ 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.',
27
+ agentHas: 'Its agent has the computer.',
28
+ youHave: 'You have the computer: its agent waits for it until you hand it back.',
29
+ someoneHas: 'Somebody else has taken this computer over.',
30
+ takeOver: 'Take over',
31
+ handBack: 'Hand back',
32
+ run: 'Run',
33
+ yourCode: 'Python to run on its computer',
34
+ terminal: 'What ran on it',
35
+ nothingRan: 'Nothing has run on it yet in this conversation.',
36
+ files: 'Files',
37
+ notStarted: 'Its computer starts when its agent first uses it: no file yet.',
38
+ emptyDirectory: 'Empty.',
39
+ up: 'Up',
40
+ download: 'Download',
41
+ signedOut: 'Its computer is shown once you are signed in to Datalayer.',
42
+ };
43
+ /** Each part of an application's computer, on or off, as its Appspec says. */
44
+ export function computerParts(app) {
45
+ const computer = app.permissions?.computer;
46
+ return {
47
+ browse: Boolean(computer?.browse),
48
+ files: Boolean(computer?.files),
49
+ shell: Boolean(computer?.shell),
50
+ };
51
+ }
52
+ /** Whether its agent uses a computer at all. */
53
+ export const hasComputer = (parts) => COMPUTER_PARTS.some(part => parts[part]);
54
+ /** The parts turned off, in words. */
55
+ export const partsOff = (parts) => COMPUTER_PARTS.filter(part => !parts[part]).map(part => COMPUTER_WORDS.part[part].toLowerCase());
56
+ /** The part of the computer a tool uses, or undefined. */
57
+ export function partOf(toolName) {
58
+ return COMPUTER_PARTS.find(part => COMPUTER_PART_TOOLS[part].includes(toolName));
59
+ }
60
+ /** The most of an output the view shows. */
61
+ export const OUTPUT_LIMIT = 4000;
62
+ /**
63
+ * What a tool of its computer returned, as a terminal shows it: the printed
64
+ * output and the error of code run (Codemode answers with an object, or its
65
+ * JSON), anything else as it came.
66
+ */
67
+ export function outputOf(value) {
68
+ let found = value;
69
+ if (typeof found === 'string') {
70
+ try {
71
+ const parsed = JSON.parse(found);
72
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
73
+ found = parsed;
74
+ }
75
+ }
76
+ catch {
77
+ // Text, as it came.
78
+ }
79
+ }
80
+ let text;
81
+ if (found && typeof found === 'object' && !Array.isArray(found)) {
82
+ const ran = found;
83
+ const printed = [
84
+ typeof ran.stdout === 'string' ? ran.stdout : ran.output,
85
+ ran.stderr,
86
+ ran.error ?? ran.execution_error ?? ran.code_error,
87
+ ].filter((part) => typeof part === 'string' && part.trim() !== '');
88
+ text =
89
+ printed.length > 0
90
+ ? printed.join('\n')
91
+ : 'stdout' in ran || 'output' in ran
92
+ ? ''
93
+ : JSON.stringify(found, null, 2);
94
+ }
95
+ else {
96
+ text =
97
+ typeof found === 'string'
98
+ ? found
99
+ : found === null || found === undefined
100
+ ? ''
101
+ : JSON.stringify(found, null, 2);
102
+ }
103
+ return text.length > OUTPUT_LIMIT
104
+ ? `${text.slice(0, OUTPUT_LIMIT)}\n… (${(text.length - OUTPUT_LIMIT).toLocaleString('en')} characters more)`
105
+ : text;
106
+ }
107
+ function commandOf(args) {
108
+ for (const key of ['code', 'path', 'script_name']) {
109
+ const value = args[key];
110
+ if (typeof value === 'string' && value.trim()) {
111
+ return value;
112
+ }
113
+ }
114
+ // A call read back from history may come without its arguments.
115
+ return Object.keys(args).length ? JSON.stringify(args) : '';
116
+ }
117
+ /**
118
+ * What its agent ran on its computer, in order: the conversation's calls to
119
+ * the tools of its shell and of its files, each with what it returned —
120
+ * streamed, as the conversation is.
121
+ */
122
+ export function terminalEntries(conversation) {
123
+ const entries = [];
124
+ for (const entry of conversation) {
125
+ if (entry.role !== 'tool') {
126
+ continue;
127
+ }
128
+ const part = partOf(entry.name);
129
+ if (!part) {
130
+ continue;
131
+ }
132
+ entries.push({
133
+ tool: entry.name,
134
+ part,
135
+ command: commandOf(entry.args ?? {}),
136
+ output: entry.result === undefined ? undefined : outputOf(entry.result),
137
+ });
138
+ }
139
+ return entries;
140
+ }
141
+ /** The computer's routes, for an agent. */
142
+ export const computerUrl = (context, tail = '') => `${context.serverUrl.replace(/\/+$/, '')}/api/v1/apps/agents/${encodeURIComponent(context.agentId)}/computer${tail}`;
143
+ async function asked(context, tail, init = {}) {
144
+ const response = await fetch(computerUrl(context, tail), {
145
+ ...init,
146
+ headers: {
147
+ ...(init.body ? { 'Content-Type': 'application/json' } : {}),
148
+ ...(context.token ? { Authorization: `Bearer ${context.token}` } : {}),
149
+ },
150
+ });
151
+ if (!response.ok) {
152
+ let detail = `${response.status}`;
153
+ try {
154
+ const body = (await response.json());
155
+ detail = typeof body.detail === 'string' ? body.detail : detail;
156
+ }
157
+ catch {
158
+ // A refusal without a sentence: its status says it.
159
+ }
160
+ throw new Error(detail);
161
+ }
162
+ return response;
163
+ }
164
+ /** Its parts, whether it started, who has it. */
165
+ export const readComputer = async (context) => (await asked(context, '')).json();
166
+ /** A directory of its working directory. */
167
+ export const listComputerFiles = async (context, path = '.') => {
168
+ const response = await asked(context, `/files?path=${encodeURIComponent(path)}`);
169
+ const body = (await response.json());
170
+ return body.entries;
171
+ };
172
+ /** One of its files, to save. */
173
+ export const downloadComputerFile = async (context, path) => (await asked(context, `/file?path=${encodeURIComponent(path)}`)).blob();
174
+ /** Take it over: what runs is interrupted, its agent's calls wait. */
175
+ export const takeOverComputer = async (context) => (await asked(context, '/take-over', { method: 'POST' })).json();
176
+ /** Hand it back to its agent. */
177
+ export const handBackComputer = async (context) => {
178
+ await asked(context, '/hand-back', { method: 'POST' });
179
+ };
180
+ /** Run code on it, as the person who took it over. */
181
+ export const runOnComputer = async (context, code) => (await asked(context, '/run', {
182
+ method: 'POST',
183
+ body: JSON.stringify({ code }),
184
+ })).json();
185
+ /** The directory above a path of its working directory. */
186
+ export function parentOf(path) {
187
+ const parts = path.split('/').filter(part => part && part !== '.');
188
+ parts.pop();
189
+ return parts.length ? parts.join('/') : '.';
190
+ }
@@ -8,6 +8,7 @@ export * from './AppFeedback';
8
8
  export * from './AppRenderer';
9
9
  export * from './appspec';
10
10
  export * from './checks';
11
+ export * from './computer';
11
12
  export * from './feedback';
12
13
  export * from './records';
13
14
  export * from './rules';
@@ -12,6 +12,7 @@ export * from './AppFeedback';
12
12
  export * from './AppRenderer';
13
13
  export * from './appspec';
14
14
  export * from './checks';
15
+ export * from './computer';
15
16
  export * from './feedback';
16
17
  export * from './records';
17
18
  export * from './rules';
@@ -53,6 +53,7 @@ export { defineA2uiScenePlugin, type A2uiSceneOptions, } from './plugins/a2ui-sc
53
53
  export { APP_KIND_PATHS, APP_PAGE_PLUGIN_NAME, APP_PAGE_SURFACE, AppPage, appKindPaths, appPageAction, appPageData, defineAppPagePlugin, hasAppPage, surfaceUnshown, type AppKindPaths, type AppPageAction, type AppPageKind, type AppPagePath, } from './plugins/app-page';
54
54
  export { APP_RULES_PLUGIN_NAME, APP_RULES_WORDS, AppRulesCard, defineAppRulesPlugin, } from './plugins/app-rules';
55
55
  export { APP_ACTIVITY_PLUGIN_NAME, APP_ACTIVITY_WORDS, AppActivity, defineAppActivityPlugin, } from './plugins/app-activity';
56
+ export { APP_COMPUTER_PLUGIN_NAME, AppComputer, defineAppComputerPlugin, } from './plugins/app-computer';
56
57
  export { GraphViewPlugin, GRAPH_PLUGIN_NAME, GRAPH_VIEW_TYPE, } from './plugins/graph';
57
58
  export { PluginsPanelPlugin, PLUGINS_PANEL_PLUGIN_NAME, PluginsPanel, } from './plugins/plugins-panel';
58
59
  export { AGENTS_PLUGIN_NAME, AgentsPlugin, SandboxSelector, browserSource, createBrowserSandboxService, createServerSandboxService, createSwitchableSandboxService, suppliedSource, useOptionalSandboxService, useSandboxService, type AgentsConfig, type AgentsOutput, type SandboxExecution, type SandboxService, type SandboxTarget, type ServiceManagerSource, type SwitchableSandboxService, } from './plugins/agents';
package/lib/loop/index.js CHANGED
@@ -66,6 +66,7 @@ export { defineA2uiScenePlugin, } from './plugins/a2ui-scene';
66
66
  export { APP_KIND_PATHS, APP_PAGE_PLUGIN_NAME, APP_PAGE_SURFACE, AppPage, appKindPaths, appPageAction, appPageData, defineAppPagePlugin, hasAppPage, surfaceUnshown, } from './plugins/app-page';
67
67
  export { APP_RULES_PLUGIN_NAME, APP_RULES_WORDS, AppRulesCard, defineAppRulesPlugin, } from './plugins/app-rules';
68
68
  export { APP_ACTIVITY_PLUGIN_NAME, APP_ACTIVITY_WORDS, AppActivity, defineAppActivityPlugin, } from './plugins/app-activity';
69
+ export { APP_COMPUTER_PLUGIN_NAME, AppComputer, defineAppComputerPlugin, } from './plugins/app-computer';
69
70
  export { GraphViewPlugin, GRAPH_PLUGIN_NAME, GRAPH_VIEW_TYPE, } from './plugins/graph';
70
71
  export { PluginsPanelPlugin, PLUGINS_PANEL_PLUGIN_NAME, PluginsPanel, } from './plugins/plugins-panel';
71
72
  export { AGENTS_PLUGIN_NAME, AgentsPlugin, SandboxSelector, browserSource, createBrowserSandboxService, createServerSandboxService, createSwitchableSandboxService, suppliedSource, useOptionalSandboxService, useSandboxService, } from './plugins/agents';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * An application's computer, beside its page (LOOP R-23, R-01b): the sandbox
3
+ * its agent runs on, live.
4
+ *
5
+ * - its **parts** — browse, files, shell — on or off, as its permissions say;
6
+ * a part off gives its agent no tool, and is said to be off;
7
+ * - **what ran on it**: the code its agent ran and the files it read and
8
+ * wrote, each with what came back, streamed from the conversation's tool
9
+ * calls;
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;
12
+ * - **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.
15
+ *
16
+ * @module loop/plugins/app-computer/AppComputer
17
+ */
18
+ import type { JSX } from 'react';
19
+ import type { AppSpec } from '../../../types/agentspecs';
20
+ import { type LoopWorkspaceContext } from '../../core';
21
+ export declare function AppComputer({ app, workspace, }: {
22
+ app: AppSpec;
23
+ workspace?: LoopWorkspaceContext;
24
+ }): JSX.Element;
25
+ export default AppComputer;
@@ -0,0 +1,179 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Button, Heading, Label, Spinner, Text, Textarea } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { signal } from '@datalayer/reactor';
6
+ import { useContributions, useSignalValue } from '@datalayer/reactor/react';
7
+ import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
8
+ import { LoopChatTurn, } from '../../core';
9
+ import { COMPUTER_PARTS, COMPUTER_WORDS, computerParts, downloadComputerFile, handBackComputer, hasComputer, listComputerFiles, parentOf, partsOff, readComputer, runOnComputer, takeOverComputer, terminalEntries, } from '../../apps/computer';
10
+ const NO_TURN = signal({ id: 0, status: 'idle' });
11
+ const NO_CONVERSATION = signal([]);
12
+ const MONO_FACE = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
13
+ const MONO = {
14
+ fontFamily: MONO_FACE,
15
+ fontSize: 0,
16
+ whiteSpace: 'pre-wrap',
17
+ wordBreak: 'break-word',
18
+ m: 0,
19
+ };
20
+ function Parts({ app }) {
21
+ const parts = computerParts(app);
22
+ const off = partsOff(parts);
23
+ return (_jsxs(Box, { "data-testid": "computer-parts", children: [_jsx(Box, { sx: { display: 'flex', gap: 1, flexWrap: 'wrap', mb: 1 }, children: COMPUTER_PARTS.map(part => (_jsxs(Label, { variant: parts[part] ? 'success' : 'secondary', "data-part": part, "data-on": parts[part] ? 'true' : 'false', children: [COMPUTER_WORDS.part[part], ":", ' ', parts[part] ? COMPUTER_WORDS.on : COMPUTER_WORDS.off] }, part))) }), off.length > 0 && off.length < COMPUTER_PARTS.length ? (_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0 }, children: COMPUTER_WORDS.offSaid(off) })) : null] }));
24
+ }
25
+ function Terminal({ conversation, }) {
26
+ const entries = terminalEntries(conversation);
27
+ return (_jsxs(Box, { "data-testid": "computer-terminal", children: [_jsx(Heading, { as: "h4", sx: { fontSize: 1, mt: 3, mb: 1 }, children: COMPUTER_WORDS.terminal }), entries.length === 0 ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: COMPUTER_WORDS.nothingRan })) : (_jsx(Box, { sx: {
28
+ bg: 'canvas.inset',
29
+ border: '1px solid',
30
+ borderColor: 'border.muted',
31
+ borderRadius: 2,
32
+ p: 2,
33
+ maxHeight: 320,
34
+ overflow: 'auto',
35
+ }, children: entries.map((entry, index) => (_jsxs(Box, { sx: { mb: 2 }, children: [_jsx(Text, { as: "p", sx: { ...MONO, fontWeight: 'semibold' }, children: `$ ${entry.tool}${entry.command ? `: ${entry.command}` : ''}` }), entry.output === undefined ? (_jsx(Spinner, { size: "small" })) : (_jsx(Text, { as: "pre", sx: { ...MONO, color: 'fg.muted' }, children: entry.output }))] }, index))) }))] }));
36
+ }
37
+ function save(blob, name) {
38
+ const url = URL.createObjectURL(blob);
39
+ const anchor = document.createElement('a');
40
+ anchor.href = url;
41
+ anchor.download = name;
42
+ anchor.click();
43
+ URL.revokeObjectURL(url);
44
+ }
45
+ function Files({ context, state, ended, }) {
46
+ const [path, setPath] = useState('.');
47
+ const [entries, setEntries] = useState();
48
+ const [error, setError] = useState('');
49
+ useEffect(() => {
50
+ if (!state.started) {
51
+ return undefined;
52
+ }
53
+ let cancelled = false;
54
+ listComputerFiles(context, path).then(found => {
55
+ if (!cancelled) {
56
+ setEntries(found);
57
+ setError('');
58
+ }
59
+ }, (failed) => !cancelled &&
60
+ setError(failed instanceof Error ? failed.message : String(failed)));
61
+ return () => {
62
+ cancelled = true;
63
+ };
64
+ // eslint-disable-next-line react-hooks/exhaustive-deps
65
+ }, [
66
+ context.serverUrl,
67
+ context.agentId,
68
+ context.token,
69
+ path,
70
+ state.started,
71
+ ended,
72
+ ]);
73
+ return (_jsxs(Box, { "data-testid": "computer-files", children: [_jsxs(Heading, { as: "h4", sx: { fontSize: 1, mt: 3, mb: 1 }, children: [COMPUTER_WORDS.files, path !== '.' ? (_jsx(Text, { sx: { fontWeight: 'normal', color: 'fg.muted' }, children: ` · ${path}` })) : null] }), !state.started ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: COMPUTER_WORDS.notStarted })) : error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0 }, children: error })) : !entries ? (_jsx(Spinner, { size: "small" })) : (_jsxs(Box, { as: "ul", sx: { listStyle: 'none', p: 0, m: 0 }, children: [path !== '.' ? (_jsx(Box, { as: "li", children: _jsx(Button, { size: "small", variant: "invisible", onClick: () => setPath(parentOf(path)), children: COMPUTER_WORDS.up }) })) : null, entries.length === 0 ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: COMPUTER_WORDS.emptyDirectory })) : null, entries.map(entry => (_jsx(Box, { as: "li", "data-path": entry.path, sx: {
74
+ display: 'flex',
75
+ alignItems: 'center',
76
+ gap: 2,
77
+ fontSize: 1,
78
+ py: 1,
79
+ borderTop: '1px solid',
80
+ borderColor: 'border.muted',
81
+ }, children: entry.type === 'directory' ? (_jsx(Button, { size: "small", variant: "invisible", onClick: () => setPath(entry.path), children: `${entry.name}/` })) : (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { flex: 1, fontFamily: MONO_FACE, fontSize: 0 }, children: entry.name }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: `${entry.size.toLocaleString('en')} bytes` }), _jsx(Button, { size: "small", "aria-label": `${COMPUTER_WORDS.download} ${entry.name}`, onClick: () => {
82
+ downloadComputerFile(context, entry.path).then(blob => save(blob, entry.name), (failed) => setError(failed instanceof Error
83
+ ? failed.message
84
+ : String(failed)));
85
+ }, children: COMPUTER_WORDS.download })] })) }, entry.path)))] }))] }));
86
+ }
87
+ function YourTerminal({ context }) {
88
+ const [code, setCode] = useState('');
89
+ const [running, setRunning] = useState(false);
90
+ const [outputs, setOutputs] = useState([]);
91
+ const run = () => {
92
+ setRunning(true);
93
+ runOnComputer(context, code).then(output => {
94
+ setOutputs(previous => [...previous, { code, output }]);
95
+ setCode('');
96
+ setRunning(false);
97
+ }, (failed) => {
98
+ setOutputs(previous => [
99
+ ...previous,
100
+ {
101
+ code,
102
+ output: {
103
+ stdout: '',
104
+ stderr: '',
105
+ error: failed instanceof Error ? failed.message : String(failed),
106
+ },
107
+ },
108
+ ]);
109
+ setRunning(false);
110
+ });
111
+ };
112
+ return (_jsxs(Box, { "data-testid": "computer-your-terminal", sx: { mt: 2 }, children: [outputs.map((each, index) => (_jsxs(Box, { sx: { mb: 2 }, children: [_jsx(Text, { as: "p", sx: { ...MONO, fontWeight: 'semibold' }, children: `>>> ${each.code}` }), _jsx(Text, { as: "pre", sx: { ...MONO, color: 'fg.muted' }, children: [each.output.stdout, each.output.stderr, each.output.error]
113
+ .filter(Boolean)
114
+ .join('\n') })] }, index))), _jsx(Textarea, { "aria-label": COMPUTER_WORDS.yourCode, placeholder: COMPUTER_WORDS.yourCode, value: code, onChange: event => setCode(event.target.value), sx: { width: '100%', fontFamily: MONO_FACE }, rows: 3 }), _jsx(Button, { size: "small", sx: { mt: 1 }, disabled: running || !code.trim(), onClick: run, children: COMPUTER_WORDS.run })] }));
115
+ }
116
+ export function AppComputer({ app, workspace, }) {
117
+ const token = useIAMStore(state => state.token) ?? '';
118
+ const turnEntries = useContributions(LoopChatTurn);
119
+ const turn = useSignalValue(turnEntries[0]?.value.turn ?? NO_TURN);
120
+ const conversation = useSignalValue(turnEntries[0]?.value.conversation ?? NO_CONVERSATION);
121
+ const settled = turn.status !== 'thinking' && turn.status !== 'streaming';
122
+ const [ended, setEnded] = useState(0);
123
+ useEffect(() => {
124
+ if (settled) {
125
+ setEnded(turn.id);
126
+ }
127
+ }, [settled, turn.id]);
128
+ const serverUrl = workspace?.sandbox.agentBaseUrl || workspace?.serverUrl || '';
129
+ const agentId = workspace?.agentId || app.id;
130
+ const context = useMemo(() => ({ serverUrl, agentId, token: token || undefined }), [serverUrl, agentId, token]);
131
+ const parts = computerParts(app);
132
+ const [state, setState] = useState();
133
+ const [error, setError] = useState('');
134
+ const [busy, setBusy] = useState(false);
135
+ // Nothing is set once it is gone: an answer may come back after.
136
+ const alive = useRef(true);
137
+ useEffect(() => {
138
+ alive.current = true;
139
+ return () => {
140
+ alive.current = false;
141
+ };
142
+ }, []);
143
+ const read = useCallback(() => {
144
+ readComputer(context).then(found => {
145
+ if (alive.current) {
146
+ setState(found);
147
+ setError('');
148
+ }
149
+ }, (failed) => alive.current &&
150
+ setError(failed instanceof Error ? failed.message : String(failed)));
151
+ }, [context]);
152
+ const used = hasComputer(parts);
153
+ useEffect(() => {
154
+ if (used && serverUrl) {
155
+ read();
156
+ }
157
+ }, [used, serverUrl, read, ended]);
158
+ const act = (step) => {
159
+ setBusy(true);
160
+ step(context).then(() => {
161
+ if (alive.current) {
162
+ setBusy(false);
163
+ read();
164
+ }
165
+ }, (failed) => {
166
+ if (alive.current) {
167
+ setBusy(false);
168
+ setError(failed instanceof Error ? failed.message : String(failed));
169
+ }
170
+ });
171
+ };
172
+ const me = Boolean(state?.yours);
173
+ 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 }), error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 2 }, children: error })) : null, state ? (_jsxs(Box, { sx: { mt: 2 }, "data-testid": "computer-holder", children: [_jsx(Text, { as: "p", sx: { fontSize: 1, m: 0, mb: 1 }, children: !state.held
174
+ ? COMPUTER_WORDS.agentHas
175
+ : me
176
+ ? COMPUTER_WORDS.youHave
177
+ : 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 ? (_jsx(Files, { context: context, state: state, ended: ended })) : null] }))] }));
178
+ }
179
+ export default AppComputer;
@@ -0,0 +1,8 @@
1
+ import { type ReactorPlugin } from '@datalayer/reactor';
2
+ import type { AppSpec } from '../../../types/agentspecs';
3
+ export declare const APP_COMPUTER_PLUGIN_NAME = "@datalayer/loop-plugin-app-computer";
4
+ /** The plugin that shows an application's computer beside its page. */
5
+ export declare function defineAppComputerPlugin(app: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
6
+ export { AppComputer } from './AppComputer';
7
+ export { COMPUTER_WORDS } from '../../apps/computer';
8
+ export default defineAppComputerPlugin;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { definePlugin } from '@datalayer/reactor';
3
+ import { LoopSlots } from '../../core';
4
+ import { AppComputer } from './AppComputer';
5
+ export const APP_COMPUTER_PLUGIN_NAME = '@datalayer/loop-plugin-app-computer';
6
+ /** The plugin that shows an application's computer beside its page. */
7
+ export function defineAppComputerPlugin(app) {
8
+ // One component per plugin: the slot keeps it, and the application with it.
9
+ function Computer({ workspace, }) {
10
+ return _jsx(AppComputer, { app: app, workspace: workspace });
11
+ }
12
+ const plugin = definePlugin({
13
+ name: `${APP_COMPUTER_PLUGIN_NAME}-${app.id}`,
14
+ displayName: `${app.name}: computer`,
15
+ description: `The computer ${app.name} runs on: what ran on it, its files, and taking it over.`,
16
+ octicon: 'device-desktop',
17
+ build: () => ({
18
+ components: [
19
+ {
20
+ id: 'app-computer',
21
+ slot: LoopSlots.sidebar,
22
+ order: 30,
23
+ Component: Computer,
24
+ },
25
+ ],
26
+ }),
27
+ });
28
+ return plugin;
29
+ }
30
+ export { AppComputer } from './AppComputer';
31
+ export { COMPUTER_WORDS } from '../../apps/computer';
32
+ export default defineAppComputerPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.3.48",
3
+ "version": "1.3.50",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",