Mike0021 commited on
Commit
98010fc
·
verified ·
1 Parent(s): 6e4bff0

Apply dark theme across workspace and load Pi from the first-message dialog

Browse files
README.md CHANGED
@@ -25,7 +25,7 @@ tags:
25
 
26
  Run **Pi's real agent core**, **just-bash**, and **MiniCPM5-2B** entirely in your browser. Chat with Pi, inspect and edit files, and execute shell commands in a persistent virtual workspace. A browser worker runs the agent loop, local WebGPU inference, and every tool. No inference API, API key, or shell server is involved.
27
 
28
- Click **Load model** once. The first download is about **1.84 GB**; files are SHA-256 verified and cached in OPFS. The loader uses bounded byte ranges, retries, cancellation, and separate download / GPU setup / warm-up phases. Chat and virtual files are saved in IndexedDB on this site. Cached weights can be reused after reloading.
29
 
30
  If your browser blocks storage inside Hugging Face's embedded view, use the app's **Open workspace in a new tab** button or [open the app directly](https://mike0021-minicpm5-2b-webgpu.static.hf.space/). This keeps inference and files in your browser and does not require changing browser privacy settings.
31
 
 
25
 
26
  Run **Pi's real agent core**, **just-bash**, and **MiniCPM5-2B** entirely in your browser. Chat with Pi, inspect and edit files, and execute shell commands in a persistent virtual workspace. A browser worker runs the agent loop, local WebGPU inference, and every tool. No inference API, API key, or shell server is involved.
27
 
28
+ Send your first message. If the model is not loaded, a **Load model & send** dialog explains the download and shows progress, then sends your message automatically. Cancelling keeps your draft; loading errors offer a retry. You can also click **Load model** ahead of time. The first download is about **1.84 GB**; files are SHA-256 verified and cached in OPFS. The loader uses bounded byte ranges, retries, cancellation, and separate download / GPU setup / warm-up phases. Chat and virtual files are saved in IndexedDB on this site. Cached weights can be reused after reloading. The entire workspace uses a dark theme.
29
 
30
  If your browser blocks storage inside Hugging Face's embedded view, use the app's **Open workspace in a new tab** button or [open the app directly](https://mike0021-minicpm5-2b-webgpu.static.hf.space/). This keeps inference and files in your browser and does not require changing browser privacy settings.
31
 
SPACE_README.md CHANGED
@@ -25,7 +25,7 @@ tags:
25
 
26
  Run **Pi's real agent core**, **just-bash**, and **MiniCPM5-2B** entirely in your browser. Chat with Pi, inspect and edit files, and execute shell commands in a persistent virtual workspace. A browser worker runs the agent loop, local WebGPU inference, and every tool. No inference API, API key, or shell server is involved.
27
 
28
- Click **Load model** once. The first download is about **1.84 GB**; files are SHA-256 verified and cached in OPFS. The loader uses bounded byte ranges, retries, cancellation, and separate download / GPU setup / warm-up phases. Chat and virtual files are saved in IndexedDB on this site. Cached weights can be reused after reloading.
29
 
30
  If your browser blocks storage inside Hugging Face's embedded view, use the app's **Open workspace in a new tab** button or [open the app directly](https://mike0021-minicpm5-2b-webgpu.static.hf.space/). This keeps inference and files in your browser and does not require changing browser privacy settings.
31
 
 
25
 
26
  Run **Pi's real agent core**, **just-bash**, and **MiniCPM5-2B** entirely in your browser. Chat with Pi, inspect and edit files, and execute shell commands in a persistent virtual workspace. A browser worker runs the agent loop, local WebGPU inference, and every tool. No inference API, API key, or shell server is involved.
27
 
28
+ Send your first message. If the model is not loaded, a **Load model & send** dialog explains the download and shows progress, then sends your message automatically. Cancelling keeps your draft; loading errors offer a retry. You can also click **Load model** ahead of time. The first download is about **1.84 GB**; files are SHA-256 verified and cached in OPFS. The loader uses bounded byte ranges, retries, cancellation, and separate download / GPU setup / warm-up phases. Chat and virtual files are saved in IndexedDB on this site. Cached weights can be reused after reloading. The entire workspace uses a dark theme.
29
 
30
  If your browser blocks storage inside Hugging Face's embedded view, use the app's **Open workspace in a new tab** button or [open the app directly](https://mike0021-minicpm5-2b-webgpu.static.hf.space/). This keeps inference and files in your browser and does not require changing browser privacy settings.
31
 
app/README.md CHANGED
@@ -9,7 +9,7 @@ npm run build
9
  npm run preview
10
  ```
11
 
12
- Open the printed localhost URL, then **Load model**. The default downloads our pinned model from Hugging Face. The app needs a secure context (HTTPS or localhost), WebGPU with `shader-f16` in a worker, and OPFS storage. Allow about 2 GB of free browser storage plus device memory for inference. Actual memory availability matters on phones.
13
 
14
  For development, use `npm run dev`. To use the already converted weights on this computer instead of downloading them, run `python scripts/serve.py` from the repository root in another terminal, then open `http://127.0.0.1:5173/?source=local`. The development and preview servers forward `/models` to that static file server. The local source option is accepted only on localhost/127.0.0.1.
15
 
@@ -26,6 +26,7 @@ For development, use `npm run dev`. To use the already converted weights on this
26
  - Visible download / WebGPU setup / shader warm-up phases. No image requests from model-generated Markdown.
27
  - A 4,096-token combined prompt/output budget, up to 768 output tokens per model call, and a 12-turn limit per user request. Older complete user turns can be omitted from inference context; the full saved conversation is retained. An oversized current turn produces an explicit error.
28
  - Responsive desktop and mobile layouts. The file workspace follows the chat on narrow screens.
 
29
 
30
  ## Runtime boundaries
31
 
@@ -52,6 +53,8 @@ node tests/terminal.mjs
52
  npm run test:render
53
  # Ten consecutive actual-model turns (local model server required):
54
  npm run test:conversation
 
 
55
  # Start npm run preview -- --port 4173 and the root static model server first:
56
  npm run test:browser
57
  ```
 
9
  npm run preview
10
  ```
11
 
12
+ Open the printed localhost URL and send your first message. If needed, a **Load model & send** dialog explains the one-time download, shows progress, and sends your message automatically after loading. Cancel returns the unsent message to the editor; loading errors support retry. You can also load ahead of time with **Load model**. The default downloads our pinned model from Hugging Face. The app needs a secure context (HTTPS or localhost), WebGPU with `shader-f16` in a worker, and OPFS storage. Allow about 2 GB of free browser storage plus device memory for inference. Actual memory availability matters on phones.
13
 
14
  For development, use `npm run dev`. To use the already converted weights on this computer instead of downloading them, run `python scripts/serve.py` from the repository root in another terminal, then open `http://127.0.0.1:5173/?source=local`. The development and preview servers forward `/models` to that static file server. The local source option is accepted only on localhost/127.0.0.1.
15
 
 
26
  - Visible download / WebGPU setup / shader warm-up phases. No image requests from model-generated Markdown.
27
  - A 4,096-token combined prompt/output budget, up to 768 output tokens per model call, and a 12-turn limit per user request. Older complete user turns can be omitted from inference context; the full saved conversation is retained. An oversized current turn produces an explicit error.
28
  - Responsive desktop and mobile layouts. The file workspace follows the chat on narrow screens.
29
+ - A consistent dark theme across the terminal, header, model controls, file editor, shell, and setup dialog, including native form controls and scrollbars.
30
 
31
  ## Runtime boundaries
32
 
 
53
  npm run test:render
54
  # Ten consecutive actual-model turns (local model server required):
55
  npm run test:conversation
56
+ # First message, modal cancellation/retry, and actual model auto-send:
57
+ npm run test:first-message
58
  # Start npm run preview -- --port 4173 and the root static model server first:
59
  npm run test:browser
60
  ```
app/index.html CHANGED
@@ -2,7 +2,8 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
5
- <meta name="theme-color" content="#f5f5f5">
 
6
  <link rel="icon" href="./favicon.svg" type="image/svg+xml">
7
  <meta name="description" content="Pi, MiniCPM5 and a bash workspace, running entirely in your browser.">
8
  <title>Pi · Browser workspace</title>
@@ -36,6 +37,24 @@ Try: ls -la
36
  <p class="workspace-note">Bash, text tools, and a virtual filesystem. Node.js, Python, and network commands are unavailable.</p>
37
  </aside>
38
  </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  <script type="module" src="./src/main.mjs"></script>
40
  </body>
41
  </html>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <meta name="theme-color" content="#18181e">
6
+ <meta name="color-scheme" content="dark">
7
  <link rel="icon" href="./favicon.svg" type="image/svg+xml">
8
  <meta name="description" content="Pi, MiniCPM5 and a bash workspace, running entirely in your browser.">
9
  <title>Pi · Browser workspace</title>
 
37
  <p class="workspace-note">Bash, text tools, and a virtual filesystem. Node.js, Python, and network commands are unavailable.</p>
38
  </aside>
39
  </main>
40
+ <dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
41
+ <div class="setup-body">
42
+ <div class="setup-icon" aria-hidden="true">π</div>
43
+ <h2 id="model-title">Let’s get Pi ready</h2>
44
+ <p id="model-description">Pi runs on your device. Load its model once, and we’ll send your message as soon as it’s ready.</p>
45
+ <div class="setup-facts"><strong>About 1.84 GB on the first download</strong><span>Saved in this browser for faster starts next time.</span></div>
46
+ <p class="setup-label">Your message</p><div id="model-preview"></div>
47
+ <div id="model-loading" hidden>
48
+ <p id="model-status" role="status"></p>
49
+ <progress id="model-progress" max="100" aria-label="Model setup progress"></progress>
50
+ <p id="model-detail"></p>
51
+ </div>
52
+ <p id="model-error" role="alert" hidden></p>
53
+ <a id="model-open-tab" class="button" target="_blank" rel="noopener noreferrer" hidden>Open workspace in a new tab ↗</a>
54
+ <p class="setup-privacy">Your message and the AI stay in your browser.</p>
55
+ </div>
56
+ <div class="setup-actions"><button id="model-cancel" type="button">Back to message</button><button id="model-confirm" type="button" class="primary" autofocus>Load model &amp; send</button></div>
57
+ </dialog>
58
  <script type="module" src="./src/main.mjs"></script>
59
  </body>
60
  </html>
app/package.json CHANGED
@@ -11,7 +11,8 @@
11
  "test:browser": "node tests/browser.mjs",
12
  "test:terminal": "node tests/terminal.mjs",
13
  "test:render": "node tests/render.mjs",
14
- "test:conversation": "node tests/conversation.mjs"
 
15
  },
16
  "dependencies": {
17
  "@earendil-works/pi-agent-core": "0.85.1",
 
11
  "test:browser": "node tests/browser.mjs",
12
  "test:terminal": "node tests/terminal.mjs",
13
  "test:render": "node tests/render.mjs",
14
+ "test:conversation": "node tests/conversation.mjs",
15
+ "test:first-message": "node tests/first-message.mjs"
16
  },
17
  "dependencies": {
18
  "@earendil-works/pi-agent-core": "0.85.1",
app/public/favicon.svg CHANGED
app/src/load-dialog.mjs ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Native modal dialog provides focus trapping, background inertness and Escape.
2
+ export function createLoadDialog(dialog, { load, stop, onClose }) {
3
+ const find = selector => dialog.querySelector(selector);
4
+ const confirm = find('#model-confirm'), cancel = find('#model-cancel');
5
+ const status = find('#model-status'), detail = find('#model-detail');
6
+ const progress = find('#model-progress'), error = find('#model-error');
7
+ let resolveRequest, loading = false, cancelled = false;
8
+
9
+ function finish(send) {
10
+ const resolve = resolveRequest; resolveRequest = undefined;
11
+ dialog.close(); resolve?.(send); onClose();
12
+ }
13
+ function cancelRequest() {
14
+ if (cancelled) return;
15
+ if (!loading) { finish(false); return; }
16
+ cancelled = true; cancel.disabled = true; cancel.textContent = 'Stopping…';
17
+ status.textContent = 'Stopping setup. Your message will be kept.';
18
+ // Wait for load() to settle before allowing another attempt.
19
+ void stop().catch(() => {});
20
+ }
21
+ cancel.onclick = cancelRequest;
22
+ dialog.addEventListener('cancel', event => { event.preventDefault(); cancelRequest(); });
23
+ confirm.onclick = async () => {
24
+ if (loading || !resolveRequest) return;
25
+ loading = true; cancelled = false;
26
+ confirm.disabled = true; confirm.textContent = 'Getting ready…';
27
+ cancel.textContent = 'Cancel loading'; error.hidden = true;
28
+ find('#model-loading').hidden = false;
29
+ status.textContent = 'Checking your browser and saved model…'; detail.textContent = '';
30
+ progress.hidden = false; progress.removeAttribute('value');
31
+ find('.setup-body').scrollTop = find('.setup-body').scrollHeight;
32
+ try {
33
+ await load();
34
+ finish(!cancelled);
35
+ } catch (cause) {
36
+ if (cancelled) finish(false);
37
+ else {
38
+ status.textContent = 'Pi could not finish loading.';
39
+ detail.textContent = 'Your message is safe. You can try again or return to editing.';
40
+ error.textContent = cause.message; error.hidden = false;
41
+ progress.hidden = true;
42
+ confirm.textContent = 'Try again';
43
+ const storageLink = document.querySelector('#open-tab');
44
+ find('#model-open-tab').hidden = storageLink.hidden;
45
+ find('#model-open-tab').href = storageLink.href;
46
+ find('.setup-body').scrollTop = find('.setup-body').scrollHeight;
47
+ }
48
+ } finally {
49
+ loading = false; confirm.disabled = false; cancel.disabled = false;
50
+ cancel.textContent = 'Back to message';
51
+ }
52
+ };
53
+ return {
54
+ request(text) {
55
+ if (resolveRequest) throw Error('Finish the current model setup before sending another message.');
56
+ cancelled = false; loading = false;
57
+ confirm.disabled = false; confirm.textContent = 'Load model & send';
58
+ cancel.disabled = false; cancel.textContent = 'Back to message';
59
+ find('#model-preview').textContent = text;
60
+ find('#model-loading').hidden = true; find('#model-open-tab').hidden = true;
61
+ error.hidden = true; progress.hidden = false;
62
+ const request = new Promise(resolve => { resolveRequest = resolve; });
63
+ dialog.showModal(); find('.setup-body').scrollTop = 0; confirm.focus();
64
+ return request;
65
+ },
66
+ update(data) {
67
+ if (!loading || cancelled) return;
68
+ progress.hidden = false;
69
+ if (data.phase === 'download') {
70
+ status.textContent = 'Downloading Pi to your device…';
71
+ const percent = Math.round(data.loaded / data.total * 100);
72
+ progress.value = percent;
73
+ detail.textContent = `${(data.loaded / 1e9).toFixed(2)} of ${(data.total / 1e9).toFixed(2)} GB · ${percent}%`;
74
+ } else {
75
+ progress.removeAttribute('value');
76
+ status.textContent = data.phase === 'compile' ? 'Preparing Pi on your device…' : 'Almost ready…';
77
+ detail.textContent = 'Your message will be sent automatically when Pi is ready.';
78
+ }
79
+ },
80
+ };
81
+ }
app/src/main.mjs CHANGED
@@ -1,5 +1,6 @@
1
  import './style.css';
2
  import { createTerminalUI } from './pi/terminal-ui.mjs';
 
3
 
4
  const $ = selector => document.querySelector(selector);
5
  const worker = new Worker(new URL('./agent.worker.mjs', import.meta.url), { type: 'module' });
@@ -12,6 +13,7 @@ const terminalUI = createTerminalUI($('#pi-terminal'), {
12
  newChat: () => ready.then(newChat), stop: () => rpc('stop'),
13
  shell: command => { if (dirty) throw Error('Save your file edits before running commands.'); return rpc('shell', { command }); },
14
  });
 
15
  function rpc(action, args) {
16
  return new Promise((resolve, reject) => {
17
  const id = nextId++; pending.set(id, { resolve, reject }); worker.postMessage({ id, action, args });
@@ -65,6 +67,7 @@ worker.onmessage = ({ data }) => {
65
  } else if (data.type === 'workspace') { files = data.files; renderFiles(); }
66
  else if (data.type === 'device') { diagnostics.device = data.device; }
67
  else if (data.type === 'load_progress') {
 
68
  if (data.phase === 'download') {
69
  $('#progress').hidden = false; $('#progress').value = data.loaded / data.total * 100;
70
  $('#load-status').textContent = data.loaded === data.total ? 'Download verified. Preparing model…' : 'Downloading and verifying model…';
@@ -104,9 +107,9 @@ async function load() {
104
  } catch (error) { error.message = storageHelp(error.message); $('#load-status').textContent = error.message; $('#progress').hidden = true; throw error; }
105
  }
106
  async function prompt(text) {
107
- if (!loaded) throw Error('Load the model to start chatting.');
108
  if (!text.trim()) return;
109
  if (dirty) throw Error('Save your file edits before asking Pi to work on them.');
 
110
  notice(''); const result = await rpc('prompt', { text }); messages = result.messages; renderMessages(); return result;
111
  }
112
  $('#load').onclick = guarded(load);
 
1
  import './style.css';
2
  import { createTerminalUI } from './pi/terminal-ui.mjs';
3
+ import { createLoadDialog } from './load-dialog.mjs';
4
 
5
  const $ = selector => document.querySelector(selector);
6
  const worker = new Worker(new URL('./agent.worker.mjs', import.meta.url), { type: 'module' });
 
13
  newChat: () => ready.then(newChat), stop: () => rpc('stop'),
14
  shell: command => { if (dirty) throw Error('Save your file edits before running commands.'); return rpc('shell', { command }); },
15
  });
16
+ const loadDialog = createLoadDialog($('#model-dialog'), { load, stop: () => rpc('stop'), onClose: () => terminalUI.focus() });
17
  function rpc(action, args) {
18
  return new Promise((resolve, reject) => {
19
  const id = nextId++; pending.set(id, { resolve, reject }); worker.postMessage({ id, action, args });
 
67
  } else if (data.type === 'workspace') { files = data.files; renderFiles(); }
68
  else if (data.type === 'device') { diagnostics.device = data.device; }
69
  else if (data.type === 'load_progress') {
70
+ loadDialog.update(data);
71
  if (data.phase === 'download') {
72
  $('#progress').hidden = false; $('#progress').value = data.loaded / data.total * 100;
73
  $('#load-status').textContent = data.loaded === data.total ? 'Download verified. Preparing model…' : 'Downloading and verifying model…';
 
107
  } catch (error) { error.message = storageHelp(error.message); $('#load-status').textContent = error.message; $('#progress').hidden = true; throw error; }
108
  }
109
  async function prompt(text) {
 
110
  if (!text.trim()) return;
111
  if (dirty) throw Error('Save your file edits before asking Pi to work on them.');
112
+ if (!loaded && !await loadDialog.request(text)) throw new DOMException('Message kept for later.', 'AbortError');
113
  notice(''); const result = await rpc('prompt', { text }); messages = result.messages; renderMessages(); return result;
114
  }
115
  $('#load').onclick = guarded(load);
app/src/pi/terminal-ui.mjs CHANGED
@@ -135,7 +135,7 @@ export function createTerminalUI(element, actions) {
135
  ui.requestRender();
136
  }
137
  function refreshStatus() {
138
- status.text = theme.fg(state.busy ? 'accent' : 'muted', terminalText(state.activity || (state.loaded ? 'Ready' : 'Load the model to chat · /help')));
139
  editor.disableSubmit = state.busy || inputPending;
140
  editor.borderColor = text => theme.fg(state.busy ? 'accent' : 'borderMuted', text);
141
  actions.onInput?.(editor.getText()); ui.requestRender();
@@ -157,7 +157,7 @@ export function createTerminalUI(element, actions) {
157
  addLocal(`$ ${command}\n${result.stdout}${result.stderr}\n[exit ${result.exitCode}]`, result.exitCode !== 0);
158
  } else { await actions.prompt(text); }
159
  } catch (error) {
160
- addLocal(error.message, true);
161
  // Preserve a failed submission without overwriting a draft typed meanwhile.
162
  if (!editor.getText()) editor.setText(text);
163
  } finally { inputPending = false; refreshStatus(); }
 
135
  ui.requestRender();
136
  }
137
  function refreshStatus() {
138
+ status.text = theme.fg(state.busy ? 'accent' : 'muted', terminalText(state.activity || (state.loaded ? 'Ready' : 'Send a message to get started · /help')));
139
  editor.disableSubmit = state.busy || inputPending;
140
  editor.borderColor = text => theme.fg(state.busy ? 'accent' : 'borderMuted', text);
141
  actions.onInput?.(editor.getText()); ui.requestRender();
 
157
  addLocal(`$ ${command}\n${result.stdout}${result.stderr}\n[exit ${result.exitCode}]`, result.exitCode !== 0);
158
  } else { await actions.prompt(text); }
159
  } catch (error) {
160
+ if (error.name !== 'AbortError') addLocal(error.message, true);
161
  // Preserve a failed submission without overwriting a draft typed meanwhile.
162
  if (!editor.getText()) editor.setText(text);
163
  } finally { inputPending = false; refreshStatus(); }
app/src/style.css CHANGED
@@ -2,38 +2,48 @@
2
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3
  font-size: 13px;
4
  font-synthesis: none;
5
- color: #262626;
6
- background: #f5f5f5;
7
- --surface: #ffffff;
8
- --panel: #fafafa;
9
- --line: #dedede;
10
- --muted: #686868;
11
- --accent: #303030;
 
 
 
 
 
 
 
 
12
  }
13
 
14
  * { box-sizing: border-box; }
15
  body { margin: 0; }
16
- button, input, textarea { font: inherit; }
 
 
17
  button, .button {
18
  cursor: pointer;
19
  min-height: 28px;
20
  padding: 5px 9px;
21
- border: 1px solid var(--line);
22
  border-radius: 4px;
23
- background: var(--surface);
24
  color: inherit;
25
  font-size: 12px;
26
  line-height: 1.4;
27
  white-space: nowrap;
28
  }
29
- button:hover, .button:hover { background: #ededed; }
30
  button:disabled { opacity: .45; cursor: default; }
31
- .primary { background: var(--accent); border-color: var(--accent); color: #fff; }
32
- .primary:hover { background: #171717; }
33
  .quiet { border-color: transparent; background: transparent; font-size: 11px; }
34
  textarea, input { outline: none; }
35
- textarea:focus, input:focus { box-shadow: inset 0 0 0 1px #888; }
36
- button:focus-visible, .button:focus-visible { outline: 2px solid #737373; outline-offset: 2px; }
37
  [hidden] { display: none !important; }
38
 
39
  header {
@@ -43,14 +53,14 @@ header {
43
  justify-content: space-between;
44
  padding: 0 16px;
45
  border-bottom: 1px solid var(--line);
46
- background: #f7f7f7;
47
  }
48
  .brand { display: flex; gap: 8px; align-items: center; text-decoration: none; color: inherit; font-size: 15px; font-weight: 600; }
49
  .pi { font-family: Georgia, serif; font-size: 28px; line-height: 1; }
50
  .brand small { display: inline; margin-left: 8px; font-size: 11px; font-weight: 400; color: var(--muted); }
51
  .header-right { display: flex; align-items: center; gap: 6px; font-size: 11px; }
52
  .header-right button { margin-left: 12px; }
53
- .local-dot { width: 5px; height: 5px; border-radius: 50%; background: #737373; }
54
 
55
  main { display: grid; grid-template-columns: minmax(420px, 1fr) minmax(370px, .83fr); height: calc(100dvh - 46px); max-width: 1800px; margin: auto; }
56
  .conversation { display: flex; flex-direction: column; min-height: 0; min-width: 0; padding: 12px 14px 8px; }
@@ -59,28 +69,22 @@ main { display: grid; grid-template-columns: minmax(420px, 1fr) minmax(370px, .8
59
  .model-heading > div { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
60
  .model-heading strong { font-size: 12px; font-weight: 600; }
61
  .model-panel p { margin: 5px 0 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
62
- .badge { padding: 2px 5px; border: 1px solid #d9d9d9; border-radius: 3px; background: #f0f0f0; color: #595959; font-size: 10px; }
63
  progress { width: 100%; height: 5px; margin-top: 8px; accent-color: var(--accent); }
64
  .muted { margin-top: 4px; color: var(--muted); font-size: 11px; }
65
  .muted:empty { display: none; }
66
 
67
- .terminal-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; margin-top: 10px; padding: 8px; border: 1px solid #393942; border-radius: 4px; color: #d4d4d4; background: #18181e; color-scheme: dark; }
68
- #pi-terminal { flex: 1; min-height: 120px; min-width: 0; overflow: hidden; background: #18181e; }
69
  #pi-terminal .pi-terminal-mount, #pi-terminal .xterm { height: 100%; }
70
- #pi-terminal .xterm-viewport { background-color: #18181e !important; }
71
  #pi-terminal textarea:focus { box-shadow: none; }
72
  .terminal-controls { display: flex; justify-content: flex-end; align-items: center; gap: 6px; padding-top: 8px; }
73
- .terminal-panel button { background: #282832; border-color: #50505c; color: #d4d4d4; }
74
- .terminal-panel button:hover { background: #343541; }
75
- .terminal-panel button.primary { background: #d4d4d4; border-color: #d4d4d4; color: #18181e; }
76
- .terminal-panel button.primary:hover { background: #f2f2f4; }
77
- .terminal-panel footer { color: #9696a4; }
78
- .terminal-panel #notice { background: #282832; border-color: #50505c; color: #d4d4d4; }
79
- #notice { margin: 6px 0; padding: 8px 10px; border: 1px solid #cfcfcf; border-radius: 4px; background: #f0f0f0; font-size: 12px; }
80
 
81
  footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-top: 7px; color: var(--muted); font-size: 10px; }
82
 
83
- .workspace { display: flex; flex-direction: column; min-width: 0; min-height: 0; border-left: 1px solid var(--line); background: #f1f1f1; }
84
  .workspace-heading { display: flex; align-items: center; justify-content: space-between; min-height: 40px; padding: 5px 10px; }
85
  .workspace-heading h2 { margin: 0; font-size: 12px; font-weight: 600; }
86
  .workspace-heading > div { display: flex; gap: 0; }
@@ -88,14 +92,14 @@ footer { display: flex; align-items: center; justify-content: space-between; gap
88
  .file-area { display: grid; grid-template-columns: 140px minmax(0, 1fr); flex: 1; min-height: 180px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
89
  #files { overflow: auto; padding: 5px 4px; border-right: 1px solid var(--line); }
90
  #files button { display: block; width: 100%; padding: 5px 7px; border: none; border-radius: 3px; background: transparent; text-align: left; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, monospace; font-size: 11px; line-height: 1.45; }
91
- #files button:hover { background: #e9e9e9; }
92
- #files button.selected { background: #dedede; color: #202020; }
93
  .editor-area { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--surface); }
94
  .editor-heading { display: flex; align-items: center; justify-content: space-between; height: 32px; padding: 0 6px 0 10px; gap: 6px; border-bottom: 1px solid var(--line); background: var(--panel); }
95
  #filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, monospace; font-size: 11px; }
96
- #editor { flex: 1; min-height: 140px; padding: 10px 12px; resize: none; border: 0; background: transparent; color: #333; font: 12px/1.6 ui-monospace, SFMono-Regular, Consolas, monospace; tab-size: 2; white-space: pre; overflow: auto; }
97
 
98
- .workspace > .terminal { padding: 0 10px; background: #f5f5f5; }
99
  .terminal summary { padding: 9px 0; cursor: pointer; font-size: 11px; font-weight: 500; }
100
  .terminal summary span { float: right; color: var(--muted); font-size: 10px; font-weight: 400; }
101
  #terminal-output { height: 112px; margin: 0; overflow: auto; font: 11px/1.5 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
@@ -105,6 +109,31 @@ footer { display: flex; align-items: center; justify-content: space-between; gap
105
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
106
 
107
  #open-tab { margin-top: 8px; font-size: 12px; color: var(--accent); text-decoration: none; white-space: normal; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  @media (max-width: 850px) {
109
  header { padding: 0 12px; }
110
  main { display: flex; flex-direction: column; height: auto; }
 
2
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3
  font-size: 13px;
4
  font-synthesis: none;
5
+ color-scheme: dark;
6
+ color: var(--text);
7
+ background: var(--background);
8
+ --background: #18181e;
9
+ --surface: #1e1e24;
10
+ --panel: #22222b;
11
+ --control: #282832;
12
+ --hover: #343541;
13
+ --selected: #3a3a4a;
14
+ --line: #393942;
15
+ --line-strong: #50505c;
16
+ --text: #d4d4d4;
17
+ --muted: #b0b0bd;
18
+ --accent: #8abeb7;
19
+ scrollbar-color: var(--line-strong) var(--background);
20
  }
21
 
22
  * { box-sizing: border-box; }
23
  body { margin: 0; }
24
+ button, input, textarea { font: inherit; color: inherit; }
25
+ ::selection { background: var(--selected); color: var(--text); }
26
+ input::placeholder, textarea::placeholder { color: var(--muted); opacity: 1; }
27
  button, .button {
28
  cursor: pointer;
29
  min-height: 28px;
30
  padding: 5px 9px;
31
+ border: 1px solid var(--line-strong);
32
  border-radius: 4px;
33
+ background: var(--control);
34
  color: inherit;
35
  font-size: 12px;
36
  line-height: 1.4;
37
  white-space: nowrap;
38
  }
39
+ button:hover, .button:hover { background: var(--hover); }
40
  button:disabled { opacity: .45; cursor: default; }
41
+ .primary { background: var(--hover); border-color: var(--accent); color: var(--text); }
42
+ .primary:hover { background: var(--selected); }
43
  .quiet { border-color: transparent; background: transparent; font-size: 11px; }
44
  textarea, input { outline: none; }
45
+ textarea:focus, input:focus { box-shadow: inset 0 0 0 1px var(--accent); }
46
+ button:focus-visible, .button:focus-visible, summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
47
  [hidden] { display: none !important; }
48
 
49
  header {
 
53
  justify-content: space-between;
54
  padding: 0 16px;
55
  border-bottom: 1px solid var(--line);
56
+ background: var(--surface);
57
  }
58
  .brand { display: flex; gap: 8px; align-items: center; text-decoration: none; color: inherit; font-size: 15px; font-weight: 600; }
59
  .pi { font-family: Georgia, serif; font-size: 28px; line-height: 1; }
60
  .brand small { display: inline; margin-left: 8px; font-size: 11px; font-weight: 400; color: var(--muted); }
61
  .header-right { display: flex; align-items: center; gap: 6px; font-size: 11px; }
62
  .header-right button { margin-left: 12px; }
63
+ .local-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
64
 
65
  main { display: grid; grid-template-columns: minmax(420px, 1fr) minmax(370px, .83fr); height: calc(100dvh - 46px); max-width: 1800px; margin: auto; }
66
  .conversation { display: flex; flex-direction: column; min-height: 0; min-width: 0; padding: 12px 14px 8px; }
 
69
  .model-heading > div { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
70
  .model-heading strong { font-size: 12px; font-weight: 600; }
71
  .model-panel p { margin: 5px 0 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
72
+ .badge { padding: 2px 5px; border: 1px solid var(--line-strong); border-radius: 3px; background: var(--control); color: var(--muted); font-size: 10px; }
73
  progress { width: 100%; height: 5px; margin-top: 8px; accent-color: var(--accent); }
74
  .muted { margin-top: 4px; color: var(--muted); font-size: 11px; }
75
  .muted:empty { display: none; }
76
 
77
+ .terminal-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; margin-top: 10px; padding: 8px; border: 1px solid var(--line); background: var(--background); border-radius: 4px; }
78
+ #pi-terminal { flex: 1; min-height: 120px; min-width: 0; overflow: hidden; background: var(--background); }
79
  #pi-terminal .pi-terminal-mount, #pi-terminal .xterm { height: 100%; }
80
+ #pi-terminal .xterm-viewport { background-color: var(--background) !important; }
81
  #pi-terminal textarea:focus { box-shadow: none; }
82
  .terminal-controls { display: flex; justify-content: flex-end; align-items: center; gap: 6px; padding-top: 8px; }
83
+ #notice { margin: 6px 0; padding: 8px 10px; border: 1px solid var(--line-strong); border-radius: 4px; background: var(--control); font-size: 12px; }
 
 
 
 
 
 
84
 
85
  footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-top: 7px; color: var(--muted); font-size: 10px; }
86
 
87
+ .workspace { display: flex; flex-direction: column; min-width: 0; min-height: 0; border-left: 1px solid var(--line); background: var(--panel); }
88
  .workspace-heading { display: flex; align-items: center; justify-content: space-between; min-height: 40px; padding: 5px 10px; }
89
  .workspace-heading h2 { margin: 0; font-size: 12px; font-weight: 600; }
90
  .workspace-heading > div { display: flex; gap: 0; }
 
92
  .file-area { display: grid; grid-template-columns: 140px minmax(0, 1fr); flex: 1; min-height: 180px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
93
  #files { overflow: auto; padding: 5px 4px; border-right: 1px solid var(--line); }
94
  #files button { display: block; width: 100%; padding: 5px 7px; border: none; border-radius: 3px; background: transparent; text-align: left; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, monospace; font-size: 11px; line-height: 1.45; }
95
+ #files button:hover { background: var(--hover); }
96
+ #files button.selected { background: var(--selected); color: var(--text); }
97
  .editor-area { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--surface); }
98
  .editor-heading { display: flex; align-items: center; justify-content: space-between; height: 32px; padding: 0 6px 0 10px; gap: 6px; border-bottom: 1px solid var(--line); background: var(--panel); }
99
  #filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, monospace; font-size: 11px; }
100
+ #editor { flex: 1; min-height: 140px; padding: 10px 12px; resize: none; border: 0; background: transparent; color: var(--text); font: 12px/1.6 ui-monospace, SFMono-Regular, Consolas, monospace; tab-size: 2; white-space: pre; overflow: auto; }
101
 
102
+ .workspace > .terminal { padding: 0 10px; background: var(--background); }
103
  .terminal summary { padding: 9px 0; cursor: pointer; font-size: 11px; font-weight: 500; }
104
  .terminal summary span { float: right; color: var(--muted); font-size: 10px; font-weight: 400; }
105
  #terminal-output { height: 112px; margin: 0; overflow: auto; font: 11px/1.5 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
 
109
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
110
 
111
  #open-tab { margin-top: 8px; font-size: 12px; color: var(--accent); text-decoration: none; white-space: normal; }
112
+
113
+ #model-dialog { width: min(460px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow: auto; padding: 24px; border: 1px solid var(--line-strong); border-radius: 12px; background: var(--surface); color: var(--text); box-shadow: 0 24px 80px #0008; }
114
+ #model-dialog[open] { display: flex; flex-direction: column; }
115
+ .setup-body { min-height: 0; overflow: auto; }
116
+ #model-dialog::backdrop { background: #08080dbd; backdrop-filter: blur(4px); }
117
+ .setup-icon { color: var(--accent); font: 36px/1 Georgia, serif; }
118
+ #model-title { margin: 16px 0 8px; font-size: 21px; line-height: 1.25; font-weight: 600; }
119
+ #model-description { margin: 0; line-height: 1.6; color: var(--muted); }
120
+ .setup-facts { display: flex; flex-direction: column; gap: 5px; margin: 18px 0; padding: 12px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); line-height: 1.45; }
121
+ .setup-facts strong { font-size: 12px; font-weight: 600; }
122
+ .setup-facts span, .setup-label, .setup-privacy, #model-detail { font-size: 11px; color: var(--muted); line-height: 1.5; }
123
+ .setup-label { margin: 16px 0 6px; }
124
+ #model-preview { max-height: 84px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; padding: 10px 12px; border-radius: 6px; background: var(--control); font-size: 12px; line-height: 1.5; }
125
+ #model-status { margin: 18px 0 0; line-height: 1.5; }
126
+ #model-detail { margin: 5px 0 0; }
127
+ #model-error { padding: 10px 12px; background: #3c2828; border: 1px solid #795454; border-radius: 6px; line-height: 1.5; overflow-wrap: anywhere; }
128
+ #model-open-tab { color: var(--accent); white-space: normal; text-decoration: none; }
129
+ .setup-privacy { margin: 18px 0; }
130
+ .setup-actions { display: flex; flex-shrink: 0; justify-content: flex-end; flex-wrap: wrap; gap: 8px; padding-top: 4px; }
131
+ .setup-actions button { min-height: 38px; padding: 8px 12px; }
132
+ @media (max-width: 420px) {
133
+ #model-dialog { padding: 20px; }
134
+ .setup-actions { flex-direction: column-reverse; }
135
+ .setup-actions button { width: 100%; }
136
+ }
137
  @media (max-width: 850px) {
138
  header { padding: 0 12px; }
139
  main { display: flex; flex-direction: column; height: auto; }
app/tests/first-message.mjs ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { chromium } from 'playwright';
2
+ import fs from 'node:fs/promises';
3
+ import assert from 'node:assert/strict';
4
+
5
+ const url = process.argv[2] ?? 'http://127.0.0.1:4173/?source=local';
6
+ const label = process.argv[3] ?? 'agent-first-message';
7
+ const result = { date: new Date().toISOString(), url, checks: [], errors: [] };
8
+ const gpuArgs = ['--enable-unsafe-webgpu', '--use-angle=vulkan', '--enable-features=Vulkan', '--disable-vulkan-surface'];
9
+ let context, browser, page;
10
+ async function open() {
11
+ page = await context.newPage(); page.on('pageerror', e => result.errors.push(e.stack));
12
+ context.setDefaultTimeout(180_000);
13
+ await page.goto(url); await page.waitForFunction(() => window.browserPi?.terminal);
14
+ await page.evaluate(() => window.browserPi.ready);
15
+ await page.evaluate(() => document.querySelector('#new-chat').onclick());
16
+ }
17
+ async function send(text) {
18
+ await page.locator('.xterm-helper-textarea').focus();
19
+ if (text) await page.keyboard.type(text);
20
+ await page.locator('#send').click();
21
+ await page.waitForFunction(() => document.querySelector('#model-dialog').open);
22
+ }
23
+ async function draft(text) {
24
+ await page.waitForFunction(text => !document.querySelector('#model-dialog').open && window.browserPi.terminal.text === text, text);
25
+ }
26
+ try {
27
+ // Real unsupported-GPU path: no fake model, worker, progress or failure events.
28
+ browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true, args: ['--disable-gpu', '--disable-software-rasterizer'] });
29
+ context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, colorScheme: 'light' });
30
+ const requests = []; context.on('request', r => requests.push(r.url()));
31
+ await open();
32
+ const prompt = 'Create a file called hello.txt containing Hello.';
33
+ await send(prompt);
34
+ assert.equal(await page.locator('#model-preview').textContent(), prompt);
35
+ assert(await page.locator('#model-confirm').evaluate(el => el === document.activeElement));
36
+ assert.equal(await page.evaluate(() => window.browserPi.loaded), false);
37
+ assert.equal(requests.filter(u => /\.onnx|manifest\.json|tokenizer/.test(u)).length, 0);
38
+ result.checks.push('First Send opens an accessible modal; no model download before confirmation');
39
+ await page.keyboard.press('Shift+Tab'); assert(await page.locator('#model-cancel').evaluate(el => el === document.activeElement));
40
+ await page.keyboard.press('Tab'); assert(await page.locator('#model-confirm').evaluate(el => el === document.activeElement));
41
+ for (const [width,height] of [[1440,1000],[850,640],[390,844],[320,568],[390,420]]) {
42
+ await page.setViewportSize({width,height});
43
+ const layout = await page.evaluate(() => {
44
+ const rect=document.querySelector('#model-dialog').getBoundingClientRect();
45
+ const actions=document.querySelector('.setup-actions').getBoundingClientRect();
46
+ const selectors=['html','header','.model-panel','.workspace','.editor-area','.workspace > .terminal','#model-dialog','#model-preview'];
47
+ return { inBounds:rect.x>=0 && rect.y>=0 && rect.right<=innerWidth && rect.bottom<=innerHeight && actions.top>=rect.top && actions.bottom<=rect.bottom,
48
+ overflow:document.documentElement.scrollWidth>innerWidth,
49
+ dark:getComputedStyle(document.documentElement).colorScheme==='dark',
50
+ surfaces:selectors.map(s=>({selector:s,bg:getComputedStyle(document.querySelector(s)).backgroundColor})) };
51
+ });
52
+ assert(layout.inBounds && !layout.overflow && layout.dark);
53
+ for(const surface of layout.surfaces) assert(surface.bg.match(/\d+/g).slice(0,3).every(n=>Number(n)<80),JSON.stringify(surface));
54
+ if([1440,390,320].includes(width)) await page.screenshot({path:`../validation/${label}-modal-${width}x${height}.png`,fullPage:true});
55
+ }
56
+ result.checks.push('Focus stays in the modal; entire app remains dark under a light OS preference; five viewport sizes fit');
57
+ await page.keyboard.press('Escape'); await draft(prompt);
58
+ assert.equal(await page.locator('#notice').textContent(),'');
59
+ await send(); await page.locator('#model-confirm').click();
60
+ await page.waitForFunction(()=>!document.querySelector('#model-error').hidden);
61
+ assert.match(await page.locator('#model-error').textContent(),/WebGPU/);
62
+ assert.equal(await page.locator('#model-confirm').textContent(),'Try again');
63
+ await page.locator('#model-confirm').click();
64
+ await page.waitForFunction(()=>!document.querySelector('#model-error').hidden && !document.querySelector('#model-confirm').disabled);
65
+ await page.screenshot({path:`../validation/${label}-error.png`,fullPage:true});
66
+ await page.locator('#model-cancel').click(); await draft(prompt);
67
+ assert.equal((await page.evaluate(()=>window.browserPi.rpc('snapshot'))).messages.length,0);
68
+ result.checks.push('Escape and Back to message restore the draft; real unsupported-GPU error supports retry without sending');
69
+ await context.close(); await browser.close(); context=undefined; browser=undefined;
70
+
71
+ context = await chromium.launchPersistentContext(new URL('../../validation/agent-browser-profile',import.meta.url).pathname,
72
+ { executablePath:'/usr/bin/chromium', headless:true, viewport:{width:1440,height:1000}, args:gpuArgs });
73
+ await open();
74
+ const nonce='FIRST-'+crypto.randomUUID().slice(0,8);
75
+ await page.evaluate(nonce=>window.browserPi.rpc('write',{path:'/workspace/first-message.txt',content:nonce}),nonce);
76
+ const task='Read first-message.txt using the read tool and tell me its exact contents.';
77
+ await send(task); await page.locator('#model-confirm').click();
78
+ await page.waitForFunction(()=>window.browserPi.busy && !document.querySelector('#model-loading').hidden);
79
+ await page.locator('#model-cancel').click(); await draft(task);
80
+ await page.waitForFunction(()=>!window.browserPi.busy);
81
+ assert.equal((await page.evaluate(()=>window.browserPi.rpc('snapshot'))).messages.length,0);
82
+ result.checks.push('Cancelling real model setup waits for cleanup and restores the unsent message');
83
+ const start=Date.now();
84
+ await send(); await page.locator('#model-confirm').click();
85
+ await page.waitForFunction(()=>window.browserPi.loaded && window.browserPi.diagnostics.events.includes('agent_end') && !window.browserPi.busy);
86
+ result.elapsedMs=Date.now()-start;
87
+ const state=await page.evaluate(()=>window.browserPi.rpc('snapshot'));
88
+ assert.equal(state.messages.filter(m=>m.role==='user').length,1);
89
+ assert.equal(state.messages.find(m=>m.role==='user').content[0].text,task);
90
+ assert(state.messages.some(m=>m.role==='toolResult' && m.toolName==='read' && !m.isError));
91
+ assert(state.messages.filter(m=>m.role==='assistant').at(-1).content.some(c=>c.text?.includes(nonce)));
92
+ assert.equal(await page.evaluate(()=>window.browserPi.terminal.text),'');
93
+ assert.equal(await page.locator('#model-dialog').evaluate(el=>el.open),false);
94
+ result.messages=state.messages;
95
+ result.checks.push('Confirm loads the actual model, sends exactly once, executes a tool and answers with an unseen file value');
96
+ await page.screenshot({path:`../validation/${label}-desktop.png`,fullPage:true});
97
+ await page.setViewportSize({width:390,height:844});
98
+ await page.screenshot({path:`../validation/${label}-mobile.png`,fullPage:true});
99
+ assert.equal(result.errors.length,0); result.passed=true;
100
+ console.log(JSON.stringify({passed:true,checks:result.checks,elapsedMs:result.elapsedMs}));
101
+ } catch(error) { result.failure=error.stack; await page?.screenshot({path:`../validation/${label}-failure.png`,fullPage:true}); throw error; }
102
+ finally { await fs.writeFile(`../validation/${label}.json`,JSON.stringify(result,null,2)); await context?.close(); await browser?.close(); }
app/tests/space-embedded.mjs CHANGED
@@ -12,7 +12,8 @@ const disabledFeatures = chromiumSwitches(false).find(arg => arg.startsWith('--d
12
 
13
  const url = 'https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU';
14
  const label = process.argv[2] ?? 'agent-space-embedded';
15
- const result = { date: new Date().toISOString(), url };
 
16
  const context = await chromium.launchPersistentContext('../validation/agent-browser-profile/space', {
17
  executablePath: '/usr/bin/chromium', headless: true, viewport: { width: 1440, height: 1080 },
18
  ignoreDefaultArgs: [disabledFeatures],
@@ -30,25 +31,41 @@ try {
30
  await app.waitForFunction(() => window.browserPi?.terminal);
31
  result.frameUrl = app.url();
32
  await app.evaluate(() => window.browserPi.ready);
 
 
 
 
 
 
 
 
 
 
33
  const start = Date.now();
34
- await app.locator('#load').click();
35
- await app.waitForFunction(() => window.browserPi.loaded || (!window.browserPi.busy && document.querySelector('#notice').textContent));
36
  assert.equal(await app.evaluate(() => window.browserPi.loaded), true, await app.locator('#load-status').textContent());
37
  result.loadMs = Date.now() - start;
38
  result.device = await app.evaluate(() => window.browserPi.diagnostics.device);
 
39
  await app.waitForFunction(() => !window.browserPi.busy);
40
  // Await the async UI handler: checking !busy immediately after a click can
41
  // precede the worker's busy event and race the next setup command.
42
  await app.evaluate(() => document.querySelector('#new-chat').onclick());
43
  await app.evaluate(() => window.browserPi.rpc('shell', { command: 'rm -f /workspace/report.md' }));
44
  await app.locator('.xterm-helper-textarea').focus();
45
- await page.keyboard.type('Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.');
46
  await app.locator('#send').click();
47
  await app.waitForFunction(() => window.browserPi.busy);
48
  await app.waitForFunction(() => !window.browserPi.busy);
 
 
 
 
49
  const state = await app.evaluate(() => window.browserPi.rpc('snapshot'));
50
  result.messages = state.messages; result.report = state.files['/workspace/report.md'];
51
  result.terminalScreen = await app.evaluate(() => window.browserPi.terminal.readScreen());
 
52
  assert.match(result.report ?? '', /144/);
53
  assert(state.messages.some(m => m.role === 'toolResult' && m.toolName === 'bash' && !m.isError));
54
  assert(state.messages.filter(m => m.role === 'assistant').at(-1).content.some(c => c.type === 'text' && c.text.includes('144')));
 
12
 
13
  const url = 'https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU';
14
  const label = process.argv[2] ?? 'agent-space-embedded';
15
+ const firstMessage = process.argv.includes('--first-message');
16
+ const result = { date: new Date().toISOString(), url, firstMessage };
17
  const context = await chromium.launchPersistentContext('../validation/agent-browser-profile/space', {
18
  executablePath: '/usr/bin/chromium', headless: true, viewport: { width: 1440, height: 1080 },
19
  ignoreDefaultArgs: [disabledFeatures],
 
31
  await app.waitForFunction(() => window.browserPi?.terminal);
32
  result.frameUrl = app.url();
33
  await app.evaluate(() => window.browserPi.ready);
34
+ const task = 'Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.';
35
+ if (firstMessage) {
36
+ await app.evaluate(() => document.querySelector('#new-chat').onclick());
37
+ await app.evaluate(() => window.browserPi.rpc('shell', { command: 'rm -f /workspace/report.md' }));
38
+ await app.locator('.xterm-helper-textarea').focus(); await page.keyboard.type(task);
39
+ await app.locator('#send').click();
40
+ await app.waitForFunction(() => document.querySelector('#model-dialog').open);
41
+ assert.equal(await app.locator('#model-preview').textContent(), task);
42
+ await page.screenshot({ path: `../validation/${label}-modal.png`, fullPage: true });
43
+ }
44
  const start = Date.now();
45
+ await app.locator(firstMessage ? '#model-confirm' : '#load').click();
46
+ await app.waitForFunction(() => window.browserPi.loaded || (!window.browserPi.busy && (document.querySelector('#notice').textContent || !document.querySelector('#model-error').hidden)));
47
  assert.equal(await app.evaluate(() => window.browserPi.loaded), true, await app.locator('#load-status').textContent());
48
  result.loadMs = Date.now() - start;
49
  result.device = await app.evaluate(() => window.browserPi.diagnostics.device);
50
+ if (!firstMessage) {
51
  await app.waitForFunction(() => !window.browserPi.busy);
52
  // Await the async UI handler: checking !busy immediately after a click can
53
  // precede the worker's busy event and race the next setup command.
54
  await app.evaluate(() => document.querySelector('#new-chat').onclick());
55
  await app.evaluate(() => window.browserPi.rpc('shell', { command: 'rm -f /workspace/report.md' }));
56
  await app.locator('.xterm-helper-textarea').focus();
57
+ await page.keyboard.type(task);
58
  await app.locator('#send').click();
59
  await app.waitForFunction(() => window.browserPi.busy);
60
  await app.waitForFunction(() => !window.browserPi.busy);
61
+ } else {
62
+ await app.waitForFunction(() => window.browserPi.diagnostics.events.includes('agent_end') && !window.browserPi.busy);
63
+ assert.equal(await app.locator('#model-dialog').evaluate(el => el.open), false);
64
+ }
65
  const state = await app.evaluate(() => window.browserPi.rpc('snapshot'));
66
  result.messages = state.messages; result.report = state.files['/workspace/report.md'];
67
  result.terminalScreen = await app.evaluate(() => window.browserPi.terminal.readScreen());
68
+ assert.equal(state.messages.filter(m => m.role === 'user').length, 1);
69
  assert.match(result.report ?? '', /144/);
70
  assert(state.messages.some(m => m.role === 'toolResult' && m.toolName === 'bash' && !m.isError));
71
  assert(state.messages.filter(m => m.role === 'assistant').at(-1).content.some(c => c.type === 'text' && c.text.includes('144')));
assets/index-9339vMLa.css ADDED
@@ -0,0 +1 @@
 
 
1
+ :root{font-synthesis:none;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;color:var(--text);background:var(--background);--background:#18181e;--surface:#1e1e24;--panel:#22222b;--control:#282832;--hover:#343541;--selected:#3a3a4a;--line:#393942;--line-strong:#50505c;--text:#d4d4d4;--muted:#b0b0bd;--accent:#8abeb7;scrollbar-color:var(--line-strong) var(--background);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px}*{box-sizing:border-box}body{margin:0}button,input,textarea{font:inherit;color:inherit}::selection{background:var(--selected);color:var(--text)}input::placeholder,textarea::placeholder{color:var(--muted);opacity:1}button,.button{cursor:pointer;border:1px solid var(--line-strong);background:var(--control);min-height:28px;color:inherit;white-space:nowrap;border-radius:4px;padding:5px 9px;font-size:12px;line-height:1.4}button:hover,.button:hover{background:var(--hover)}button:disabled{opacity:.45;cursor:default}.primary{background:var(--hover);border-color:var(--accent);color:var(--text)}.primary:hover{background:var(--selected)}.quiet{background:0 0;border-color:#0000;font-size:11px}textarea,input{outline:none}textarea:focus,input:focus{box-shadow:inset 0 0 0 1px var(--accent)}button:focus-visible,.button:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}[hidden]{display:none!important}header{border-bottom:1px solid var(--line);background:var(--surface);justify-content:space-between;align-items:center;height:46px;padding:0 16px;display:flex}.brand{color:inherit;align-items:center;gap:8px;font-size:15px;font-weight:600;text-decoration:none;display:flex}.pi{font-family:Georgia,serif;font-size:28px;line-height:1}.brand small{color:var(--muted);margin-left:8px;font-size:11px;font-weight:400;display:inline}.header-right{align-items:center;gap:6px;font-size:11px;display:flex}.header-right button{margin-left:12px}.local-dot{background:var(--accent);border-radius:50%;width:5px;height:5px}main{grid-template-columns:minmax(420px,1fr) minmax(370px,.83fr);max-width:1800px;height:calc(100dvh - 46px);margin:auto;display:grid}.conversation{flex-direction:column;min-width:0;min-height:0;padding:12px 14px 8px;display:flex}.model-panel{border:1px solid var(--line);background:var(--panel);border-radius:4px;padding:9px 10px}.model-heading{justify-content:space-between;align-items:center;gap:8px;display:flex}.model-heading>div{flex-wrap:wrap;align-items:center;gap:7px;display:flex}.model-heading strong{font-size:12px;font-weight:600}.model-panel p{color:var(--muted);margin:5px 0 0;font-size:11px;line-height:1.45}.badge{border:1px solid var(--line-strong);background:var(--control);color:var(--muted);border-radius:3px;padding:2px 5px;font-size:10px}progress{width:100%;height:5px;accent-color:var(--accent);margin-top:8px}.muted{color:var(--muted);margin-top:4px;font-size:11px}.muted:empty{display:none}.terminal-panel{border:1px solid var(--line);background:var(--background);border-radius:4px;flex-direction:column;flex:1;min-height:0;margin-top:10px;padding:8px;display:flex}#pi-terminal{background:var(--background);flex:1;min-width:0;min-height:120px;overflow:hidden}#pi-terminal .pi-terminal-mount,#pi-terminal .xterm{height:100%}#pi-terminal .xterm-viewport{background-color:var(--background)!important}#pi-terminal textarea:focus{box-shadow:none}.terminal-controls{justify-content:flex-end;align-items:center;gap:6px;padding-top:8px;display:flex}#notice{border:1px solid var(--line-strong);background:var(--control);border-radius:4px;margin:6px 0;padding:8px 10px;font-size:12px}footer{color:var(--muted);justify-content:space-between;align-items:center;gap:8px;padding-top:7px;font-size:10px;display:flex}.workspace{border-left:1px solid var(--line);background:var(--panel);flex-direction:column;min-width:0;min-height:0;display:flex}.workspace-heading{justify-content:space-between;align-items:center;min-height:40px;padding:5px 10px;display:flex}.workspace-heading h2{margin:0;font-size:12px;font-weight:600}.workspace-heading>div{gap:0;display:flex}.button{display:inline-block}.file-area{border-top:1px solid var(--line);border-bottom:1px solid var(--line);flex:1;grid-template-columns:140px minmax(0,1fr);min-height:180px;display:grid}#files{border-right:1px solid var(--line);padding:5px 4px;overflow:auto}#files button{text-align:left;white-space:normal;overflow-wrap:anywhere;background:0 0;border:none;border-radius:3px;width:100%;padding:5px 7px;font-family:ui-monospace,monospace;font-size:11px;line-height:1.45;display:block}#files button:hover{background:var(--hover)}#files button.selected{background:var(--selected);color:var(--text)}.editor-area{background:var(--surface);flex-direction:column;min-width:0;min-height:0;display:flex}.editor-heading{border-bottom:1px solid var(--line);background:var(--panel);justify-content:space-between;align-items:center;gap:6px;height:32px;padding:0 6px 0 10px;display:flex}#filename{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,monospace;font-size:11px;overflow:hidden}#editor{resize:none;min-height:140px;color:var(--text);tab-size:2;white-space:pre;background:0 0;border:0;flex:1;padding:10px 12px;font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:auto}.workspace>.terminal{background:var(--background);padding:0 10px}.terminal summary{cursor:pointer;padding:9px 0;font-size:11px;font-weight:500}.terminal summary span{float:right;color:var(--muted);font-size:10px;font-weight:400}#terminal-output{white-space:pre-wrap;overflow-wrap:anywhere;height:112px;margin:0;font:11px/1.5 ui-monospace,monospace;overflow:auto}#shell-form{border-top:1px solid var(--line);align-items:center;gap:6px;padding:4px 0;font-family:ui-monospace,monospace;display:flex}#command{background:0 0;border:0;width:100%;min-width:0;padding:6px 0;font-size:12px}.workspace-note{color:var(--muted);margin:8px 10px;font-size:10px;line-height:1.45}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;padding:0;position:absolute;overflow:hidden}#open-tab{color:var(--accent);white-space:normal;margin-top:8px;font-size:12px;text-decoration:none}#model-dialog{border:1px solid var(--line-strong);background:var(--surface);width:min(460px,100% - 32px);max-height:calc(100dvh - 32px);color:var(--text);border-radius:12px;padding:24px;overflow:auto;box-shadow:0 24px 80px #0008}#model-dialog[open]{flex-direction:column;display:flex}.setup-body{min-height:0;overflow:auto}#model-dialog::backdrop{-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#08080dbd}.setup-icon{color:var(--accent);font:36px/1 Georgia,serif}#model-title{margin:16px 0 8px;font-size:21px;font-weight:600;line-height:1.25}#model-description{color:var(--muted);margin:0;line-height:1.6}.setup-facts{border:1px solid var(--line);background:var(--panel);border-radius:6px;flex-direction:column;gap:5px;margin:18px 0;padding:12px;line-height:1.45;display:flex}.setup-facts strong{font-size:12px;font-weight:600}.setup-facts span,.setup-label,.setup-privacy,#model-detail{color:var(--muted);font-size:11px;line-height:1.5}.setup-label{margin:16px 0 6px}#model-preview{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--control);border-radius:6px;max-height:84px;padding:10px 12px;font-size:12px;line-height:1.5;overflow:auto}#model-status{margin:18px 0 0;line-height:1.5}#model-detail{margin:5px 0 0}#model-error{overflow-wrap:anywhere;background:#3c2828;border:1px solid #795454;border-radius:6px;padding:10px 12px;line-height:1.5}#model-open-tab{color:var(--accent);white-space:normal;text-decoration:none}.setup-privacy{margin:18px 0}.setup-actions{flex-wrap:wrap;flex-shrink:0;justify-content:flex-end;gap:8px;padding-top:4px;display:flex}.setup-actions button{min-height:38px;padding:8px 12px}@media (width<=420px){#model-dialog{padding:20px}.setup-actions{flex-direction:column-reverse}.setup-actions button{width:100%}}@media (width<=850px){header{padding:0 12px}main{flex-direction:column;height:auto;display:flex}.conversation{height:calc(100dvh - 46px);min-height:320px;padding:10px}.workspace{border-left:0;border-top:1px solid var(--line);min-height:520px}.file-area{grid-template-columns:115px minmax(0,1fr);min-height:280px}.header-right>span:not(.local-dot){display:none}.header-right button{margin-left:0}h1{font-size:20px}.badge{font-size:9px}.model-panel{padding:8px}footer{font-size:9px}}@media (pointer:coarse){button,.button,#files button{min-height:38px}.editor-heading{min-height:40px}.terminal summary{padding-top:11px;padding-bottom:11px}#command,#pi-terminal .xterm-helper-textarea{font-size:16px}}.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}
assets/index-CTSRWQwn.css DELETED
@@ -1 +0,0 @@
1
- :root{font-synthesis:none;color:#262626;--surface:#fff;--panel:#fafafa;--line:#dedede;--muted:#686868;--accent:#303030;background:#f5f5f5;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px}*{box-sizing:border-box}body{margin:0}button,input,textarea{font:inherit}button,.button{cursor:pointer;border:1px solid var(--line);background:var(--surface);min-height:28px;color:inherit;white-space:nowrap;border-radius:4px;padding:5px 9px;font-size:12px;line-height:1.4}button:hover,.button:hover{background:#ededed}button:disabled{opacity:.45;cursor:default}.primary{background:var(--accent);border-color:var(--accent);color:#fff}.primary:hover{background:#171717}.quiet{background:0 0;border-color:#0000;font-size:11px}textarea,input{outline:none}textarea:focus,input:focus{box-shadow:inset 0 0 0 1px #888}button:focus-visible,.button:focus-visible{outline-offset:2px;outline:2px solid #737373}[hidden]{display:none!important}header{border-bottom:1px solid var(--line);background:#f7f7f7;justify-content:space-between;align-items:center;height:46px;padding:0 16px;display:flex}.brand{color:inherit;align-items:center;gap:8px;font-size:15px;font-weight:600;text-decoration:none;display:flex}.pi{font-family:Georgia,serif;font-size:28px;line-height:1}.brand small{color:var(--muted);margin-left:8px;font-size:11px;font-weight:400;display:inline}.header-right{align-items:center;gap:6px;font-size:11px;display:flex}.header-right button{margin-left:12px}.local-dot{background:#737373;border-radius:50%;width:5px;height:5px}main{grid-template-columns:minmax(420px,1fr) minmax(370px,.83fr);max-width:1800px;height:calc(100dvh - 46px);margin:auto;display:grid}.conversation{flex-direction:column;min-width:0;min-height:0;padding:12px 14px 8px;display:flex}.model-panel{border:1px solid var(--line);background:var(--panel);border-radius:4px;padding:9px 10px}.model-heading{justify-content:space-between;align-items:center;gap:8px;display:flex}.model-heading>div{flex-wrap:wrap;align-items:center;gap:7px;display:flex}.model-heading strong{font-size:12px;font-weight:600}.model-panel p{color:var(--muted);margin:5px 0 0;font-size:11px;line-height:1.45}.badge{color:#595959;background:#f0f0f0;border:1px solid #d9d9d9;border-radius:3px;padding:2px 5px;font-size:10px}progress{width:100%;height:5px;accent-color:var(--accent);margin-top:8px}.muted{color:var(--muted);margin-top:4px;font-size:11px}.muted:empty{display:none}.terminal-panel{color:#d4d4d4;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:#18181e;border:1px solid #393942;border-radius:4px;flex-direction:column;flex:1;min-height:0;margin-top:10px;padding:8px;display:flex}#pi-terminal{background:#18181e;flex:1;min-width:0;min-height:120px;overflow:hidden}#pi-terminal .pi-terminal-mount,#pi-terminal .xterm{height:100%}#pi-terminal .xterm-viewport{background-color:#18181e!important}#pi-terminal textarea:focus{box-shadow:none}.terminal-controls{justify-content:flex-end;align-items:center;gap:6px;padding-top:8px;display:flex}.terminal-panel button{color:#d4d4d4;background:#282832;border-color:#50505c}.terminal-panel button:hover{background:#343541}.terminal-panel button.primary{color:#18181e;background:#d4d4d4;border-color:#d4d4d4}.terminal-panel button.primary:hover{background:#f2f2f4}.terminal-panel footer{color:#9696a4}.terminal-panel #notice{color:#d4d4d4;background:#282832;border-color:#50505c}#notice{background:#f0f0f0;border:1px solid #cfcfcf;border-radius:4px;margin:6px 0;padding:8px 10px;font-size:12px}footer{color:var(--muted);justify-content:space-between;align-items:center;gap:8px;padding-top:7px;font-size:10px;display:flex}.workspace{border-left:1px solid var(--line);background:#f1f1f1;flex-direction:column;min-width:0;min-height:0;display:flex}.workspace-heading{justify-content:space-between;align-items:center;min-height:40px;padding:5px 10px;display:flex}.workspace-heading h2{margin:0;font-size:12px;font-weight:600}.workspace-heading>div{gap:0;display:flex}.button{display:inline-block}.file-area{border-top:1px solid var(--line);border-bottom:1px solid var(--line);flex:1;grid-template-columns:140px minmax(0,1fr);min-height:180px;display:grid}#files{border-right:1px solid var(--line);padding:5px 4px;overflow:auto}#files button{text-align:left;white-space:normal;overflow-wrap:anywhere;background:0 0;border:none;border-radius:3px;width:100%;padding:5px 7px;font-family:ui-monospace,monospace;font-size:11px;line-height:1.45;display:block}#files button:hover{background:#e9e9e9}#files button.selected{color:#202020;background:#dedede}.editor-area{background:var(--surface);flex-direction:column;min-width:0;min-height:0;display:flex}.editor-heading{border-bottom:1px solid var(--line);background:var(--panel);justify-content:space-between;align-items:center;gap:6px;height:32px;padding:0 6px 0 10px;display:flex}#filename{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,monospace;font-size:11px;overflow:hidden}#editor{resize:none;color:#333;tab-size:2;white-space:pre;background:0 0;border:0;flex:1;min-height:140px;padding:10px 12px;font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:auto}.workspace>.terminal{background:#f5f5f5;padding:0 10px}.terminal summary{cursor:pointer;padding:9px 0;font-size:11px;font-weight:500}.terminal summary span{float:right;color:var(--muted);font-size:10px;font-weight:400}#terminal-output{white-space:pre-wrap;overflow-wrap:anywhere;height:112px;margin:0;font:11px/1.5 ui-monospace,monospace;overflow:auto}#shell-form{border-top:1px solid var(--line);align-items:center;gap:6px;padding:4px 0;font-family:ui-monospace,monospace;display:flex}#command{background:0 0;border:0;width:100%;min-width:0;padding:6px 0;font-size:12px}.workspace-note{color:var(--muted);margin:8px 10px;font-size:10px;line-height:1.45}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;padding:0;position:absolute;overflow:hidden}#open-tab{color:var(--accent);white-space:normal;margin-top:8px;font-size:12px;text-decoration:none}@media (width<=850px){header{padding:0 12px}main{flex-direction:column;height:auto;display:flex}.conversation{height:calc(100dvh - 46px);min-height:320px;padding:10px}.workspace{border-left:0;border-top:1px solid var(--line);min-height:520px}.file-area{grid-template-columns:115px minmax(0,1fr);min-height:280px}.header-right>span:not(.local-dot){display:none}.header-right button{margin-left:0}h1{font-size:20px}.badge{font-size:9px}.model-panel{padding:8px}footer{font-size:9px}}@media (pointer:coarse){button,.button,#files button{min-height:38px}.editor-heading{min-height:40px}.terminal summary{padding-top:11px;padding-bottom:11px}#command,#pi-terminal .xterm-helper-textarea{font-size:16px}}.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}
 
 
assets/{index-qmdKynRr.js → index-CsaxMUF5.js} RENAMED
@@ -139,7 +139,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let e=`<p>An error
139
  `);return r.addChild(new fp(Q.fg(`toolOutput`,g?a+`
140
 
141
  `+o:s.slice(0,10).join(`
142
- `)),0,0)),!g&&s.length>10&&r.addChild(new fp(Q.fg(`dim`,`… ${s.length-10} more lines · Ctrl+O to expand`),0,0)),n.addChild(r),n}function w(){l.clear(),l.addChild(b);let e=new Map(h.filter(e=>e.role===`toolResult`).map(e=>[e.toolCallId,e])),t=new Set;for(let n=0;n<=h.length;n++){for(let e of y)e.at===n&&l.addChild(e.component);let r=h[n];if(!r||r.role===`toolResult`)continue;let i=r.role+`:`+r.timestamp+`:`+n;t.add(i);let a=v.get(i);if(a||(a={component:r.role===`user`?new dg(Cg(Sg(r.content))):new pg},v.set(i,a)),r.role===`user`&&S(Sg(r.content)),r.role===`assistant`&&a.message!==r){let e={...r,content:r.content.map(e=>e.type===`text`?{...e,text:Cg(e.text)}:e.type===`thinking`?{...e,thinking:Cg(e.thinking)}:e),errorMessage:r.errorMessage?Cg(r.errorMessage):void 0};a.component.updateContent(e,!r.stopReason)}r.role===`user`&&(n>0||y.length)&&l.addChild(new Rh(1)),a.message=r,l.addChild(a.component);for(let n of Array.isArray(r.content)?r.content:[]){if(n.type!==`toolCall`)continue;let r=e.get(n.id),i=`tool:`+n.id;t.add(i);let a=JSON.stringify([n,r,g]),o=v.get(i);o?.signature!==a&&(o={signature:a,component:C(n,r)},v.set(i,o)),l.addChild(o.component)}}for(let e of v.keys())t.has(e)||v.delete(e);o.requestRender()}function T(){d.text=Q.fg(p.busy?`accent`:`muted`,Cg(p.activity||(p.loaded?`Ready`:`Load the model to chat · /help`))),c.disableSubmit=p.busy||_,c.borderColor=e=>Q.fg(p.busy?`accent`:`borderMuted`,e),t.onInput?.(c.getText()),o.requestRender()}async function E(e){if(!(!e.trim()||p.busy||_)){_=!0,T(),S(e),o.scrollToBottom();try{let n=e.trim();if(n===`/help`)x(`/load Load MiniCPM on this device
143
  /new Clear the conversation; keep workspace files
144
  !cmd Run cmd with just-bash
145
 
@@ -149,7 +149,7 @@ Esc or Ctrl+C: stop · Ctrl+O: expand tools
149
  Ctrl+Shift+F: search transcript
150
 
151
  Pi’s original terminal UI and agent core run here.
152
- Node CLI extensions, npm, Python and native binaries are unavailable.`);else if(n===`/load`)await t.load();else if(n===`/new`||n===`/clear`)await t.newChat(),y.length=0,w();else if(n.startsWith(`/`))throw Error(`Unknown browser command. Type /help for available commands.`);else if(n.startsWith(`!`)){let e=n.slice(1).trim();if(!e)throw Error(`Enter a shell command after !.`);let r=await t.shell(e);x(`$ ${e}\n${r.stdout}${r.stderr}\n[exit ${r.exitCode}]`,r.exitCode!==0)}else await t.prompt(e)}catch(t){x(t.message,!0),c.getText()||c.setText(e)}finally{_=!1,T()}}}c.onSubmit=e=>{E(e)},c.onChange=()=>{t.onInput?.(c.getText()),o.requestRender()},c.onEscape=()=>{p.busy&&t.stop().catch(e=>x(e.message,!0))},c.onCtrlD=()=>x(`This terminal lives in your browser. Close the tab to leave.`),c.onAction(`app.tools.expand`,()=>{g=!g,w()}),c.setAutocompleteProvider({getSuggestions(e,t,n){let r=e[t].slice(0,n),i=r.match(/\S*$/)?.[0]??``;if(!i)return null;let a=(r.startsWith(`/`)?wg:m.map(e=>({value:e,label:e,description:`workspace file`}))).filter(e=>e.value.startsWith(i)&&e.value!==i);return a.length?{items:a,prefix:i}:null},applyCompletion(e,t,n,r,i){let a=[...e];return a[t]=e[t].slice(0,n-i.length)+r.value+` `+e[t].slice(n),{lines:a,cursorLine:t,cursorCol:n-i.length+r.value.length+1}}}),n.attachCustomKeyEventHandler(e=>e.type!==`keydown`||e.isComposing?!0:(e.ctrlKey||e.metaKey)&&e.key.toLowerCase()===`v`?!1:e.ctrlKey&&e.shiftKey&&e.code===`KeyF`?(e.preventDefault(),a.inputData(`\x1B[102;6u`),!1):e.key===`Enter`&&e.shiftKey?(e.preventDefault(),a.inputData(`\x1B\r`),!1):(e.ctrlKey||e.metaKey)&&e.key.toLowerCase()===`c`&&(n.hasSelection()||o.hasActiveSelection())?(o.hasActiveSelection()&&(e.preventDefault(),o.copyActiveSelectionToClipboard()),!1):!0);let D;e.addEventListener(`touchstart`,e=>{e.touches.length===1&&(D=e.touches[0].clientY)},{passive:!0}),e.addEventListener(`touchmove`,e=>{if(D===void 0||e.touches.length!==1)return;let t=e.touches[0].clientY,n=Math.trunc((D-t)/18);n&&(o.scrollBy(n),D=t,e.preventDefault())},{passive:!1}),e.addEventListener(`touchend`,()=>{D=void 0},{passive:!0});function O(){e.clientWidth>0&&e.clientHeight>0&&(i.fit(),o.requestRender())}let te=new ResizeObserver(O);return te.observe(e),O(),o.start(),w(),T(),document.fonts.ready.then(O),n.textarea.setAttribute(`aria-label`,`Message Pi`),n.textarea.setAttribute(`autocapitalize`,`off`),n.textarea.setAttribute(`spellcheck`,`false`),{setMessages(e,{reset:t=!1}={}){h=e,t&&(y.length=0,v.clear()),w()},setFiles(e){m=Object.keys(e).map(e=>e.replace(/^\/workspace\//,``))},setState(e){p={...p,...e},T()},submit(){let e=c.getExpandedText();if(!c.disableSubmit&&e.trim())return c.setText(``),E(e)},focus(){n.focus()},newline(){a.inputData(`\x1B\r`),n.focus()},get text(){return c.getExpandedText()},get cols(){return n.cols},get rows(){return n.rows},get viewportTop(){return o.viewportTop},readScreen(){return Array.from({length:n.buffer.active.length},(e,t)=>n.buffer.active.getLine(t)?.translateToString(!0)??``).join(`
153
- `)},readDocument(){return u.render(n.cols).map(e=>Yu(e).trimEnd())},readCells(){return Array.from({length:n.rows},(e,t)=>Array.from({length:n.cols},(e,r)=>{let i=n.buffer.active.getLine(t)?.getCell(r);return i?{text:i.getChars(),width:i.getWidth(),fg:i.isFgDefault()?ag.foreground:i.getFgColor(),bg:i.isBgDefault()?ag.background:i.getBgColor()}:null}))},dispose(){te.disconnect(),o.stop({preserveScreen:!0}),n.dispose()}}}var $=e=>document.querySelector(e),Eg=new Worker(new URL(new URL(`agent.worker-DqQzo9yL.js`,import.meta.url).href,``+import.meta.url),{type:`module`}),Dg=new Map,Og=1,kg=!1,Ag=!1,jg,Mg=!1,Ng={},Pg=[],Fg,Ig=new Promise(e=>{Fg=e}),Lg={events:[],generations:[],device:null},Rg=Tg($(`#pi-terminal`),{load:()=>Ig.then(qg),prompt:e=>Ig.then(()=>Jg(e)),newChat:()=>Ig.then(Yg),stop:()=>zg(`stop`),shell:e=>{if(Mg)throw Error(`Save your file edits before running commands.`);return zg(`shell`,{command:e})}});function zg(e,t){return new Promise((n,r)=>{let i=Og++;Dg.set(i,{resolve:n,reject:r}),Eg.postMessage({id:i,action:e,args:t})})}function Bg(e){$(`#notice`).textContent=e,$(`#notice`).hidden=!e}function Vg(e){return window.self===window.top||!/denied|storage directory access/i.test(e)?e:($(`#open-tab`).href=location.href,$(`#open-tab`).hidden=!1,`Your browser blocked storage in this embedded app. Open the workspace in a new tab to load the model and save files.`)}function Hg(){$(`#load`).disabled=kg||Ag,$(`#send`).disabled=kg,$(`#newline`).disabled=kg,Rg.setState({busy:kg,loaded:Ag,activity:$(`#activity`).textContent}),$(`#stop`).hidden=!kg;for(let e of[`#new-chat`,`#shell-run`,`#add-file`,`#upload`])$(e).disabled=kg;$(`#save`).disabled=kg||!jg||!Mg,$(`#editor`).disabled=kg||!jg}function Ug(){Rg.setFiles(Ng),$(`#files`).replaceChildren();for(let e of Object.keys(Ng).sort()){let t=document.createElement(`button`);t.textContent=e.replace(/^\/workspace\//,``),t.title=e,t.classList.toggle(`selected`,e===jg),t.onclick=()=>Wg(e),$(`#files`).append(t)}jg&&!Mg&&($(`#editor`).value=Ng[jg]??``)}function Wg(e){if(Mg){Bg(`Save the open file before selecting another file.`);return}jg=e,$(`#filename`).textContent=e,$(`#editor`).value=Ng[e]??``,Ug(),Hg()}function Gg(){Rg.setMessages(Pg)}Eg.onmessage=({data:e})=>{if(e.type===`reply`){let t=Dg.get(e.id);Dg.delete(e.id),e.error?t?.reject(Error(e.error)):t?.resolve(e.value)}else if(e.type===`initialized`)Ng=e.files,Pg=e.messages,Ug(),Gg(),Ng[`/workspace/README.md`]&&Wg(`/workspace/README.md`),Fg();else if(e.type===`busy`)kg=!0,$(`#activity`).textContent=e.action===`prompt`?`Pi is working…`:`Working…`,Hg();else if(e.type===`idle`)kg=!1,$(`#activity`).textContent=Ag?`Ready · running on your device`:`Files and chat are saved on this device.`,Hg();else if(e.type===`workspace`)Ng=e.files,Ug();else if(e.type===`device`)Lg.device=e.device;else if(e.type===`load_progress`)e.phase===`download`?($(`#progress`).hidden=!1,$(`#progress`).value=e.loaded/e.total*100,$(`#load-status`).textContent=e.loaded===e.total?`Download verified. Preparing model…`:`Downloading and verifying model…`,$(`#load-detail`).textContent=`${(e.loaded/1e9).toFixed(2)} / ${(e.total/1e9).toFixed(2)} GB · ${Math.round(e.loaded/e.total*100)}%${e.cached?` · ${(e.cached/1e9).toFixed(2)} GB cached`:``}`):($(`#load-status`).textContent=e.phase===`compile`?`Preparing WebGPU session…`:`Warming up GPU shaders…`,$(`#progress`).removeAttribute(`value`));else if(e.type===`loaded`)Ag=!0,Lg.device=e.device,$(`#progress`).hidden=!0,$(`#load`).textContent=`Loaded ✓`,$(`#load-status`).textContent=`Ready. Model inference and tools run entirely in this browser.`,$(`#load-detail`).textContent=e.cachedBytes?`Verified model restored from browser storage.`:`SHA-256 verified · model cached for next time.`,Hg();else if(e.type===`agent_event`){let t=e.event;if(Lg.events.push(t.type),Lg.events.length>500&&Lg.events.shift(),t.message){let e=Pg.findIndex(e=>e.timestamp===t.message.timestamp&&e.role===t.message.role&&e.toolCallId===t.message.toolCallId);e<0?Pg.push(t.message):Pg[e]=t.message,Gg()}t.type===`tool_execution_start`&&($(`#activity`).textContent=`Running ${t.toolName}…`),t.type===`turn_start`&&($(`#activity`).textContent=`Pi is thinking…`),Rg.setState({activity:$(`#activity`).textContent})}else e.type===`generation`?(Lg.generations.push(e),Lg.generations.length>100&&Lg.generations.shift()):e.type===`context_trim`?Bg(`Older turns were left out of the model context. Your full chat remains saved.`):e.type===`notice`?Bg(e.text):e.type===`persistence_error`?Bg(Vg(e.error)):e.type===`fatal`&&(Ag=!1,Bg(e.error),Hg())},Eg.onerror=e=>{let t=Error(e.message||`The worker stopped. Reload to restore your saved workspace.`);Bg(t.message);for(let e of Dg.values())e.reject(t);Dg.clear(),kg=!1,Ag=!1,Hg()};var Kg=e=>async t=>{t?.preventDefault();try{await Ig,await e(t)}catch(e){Bg(e.message)}};async function qg(){Bg(``);let e=new URLSearchParams(location.search);try{return await zg(`load`,{runtimeURL:new URL(`./runtime/`,location.href).href,local:e.get(`source`)===`local`,origin:location.origin,limit128:e.has(`limit128`)})}catch(e){throw e.message=Vg(e.message),$(`#load-status`).textContent=e.message,$(`#progress`).hidden=!0,e}}async function Jg(e){if(!Ag)throw Error(`Load the model to start chatting.`);if(!e.trim())return;if(Mg)throw Error(`Save your file edits before asking Pi to work on them.`);Bg(``);let t=await zg(`prompt`,{text:e});return Pg=t.messages,Gg(),t}$(`#load`).onclick=Kg(qg),$(`#send`).onclick=()=>{Rg.submit(),Rg.focus()},$(`#newline`).onclick=()=>Rg.newline(),$(`#stop`).onclick=Kg(()=>zg(`stop`));async function Yg(){Pg=(await zg(`new_chat`)).messages,Rg.setMessages(Pg,{reset:!0}),Bg(``)}$(`#new-chat`).onclick=Kg(Yg),$(`#editor`).oninput=()=>{Mg=!0,Hg()},$(`#save`).onclick=Kg(async()=>{await zg(`write`,{path:jg,content:$(`#editor`).value}),Mg=!1,Ug(),Hg(),Bg(``)}),$(`#add-file`).onclick=Kg(async()=>{if(Mg)throw Error(`Save the open file first.`);let e=window.prompt(`File path, relative to /workspace:`);if(!e?.trim())return;let t=e.startsWith(`/`)?e:`/workspace/`+e;if(Object.hasOwn(Ng,t))throw Error(`That file already exists.`);await zg(`write`,{path:t,content:``}),Wg(t)}),$(`#upload`).onchange=Kg(async()=>{if(Mg)throw Error(`Save the open file first.`);for(let e of $(`#upload`).files){if(e.size>1024**2)throw Error(`Import text files of 1 MiB or less.`);let t=await e.text();if(t.includes(`\0`))throw Error(`Import supports text files only.`);let n=`/workspace/`+e.name;if(Object.hasOwn(Ng,n))throw Error(e.name+` already exists. Rename it before importing.`);await zg(`write`,{path:n,content:t}),Wg(n)}$(`#upload`).value=``}),$(`#export`).onclick=Kg(async()=>{let e=await zg(`snapshot`),t=URL.createObjectURL(new Blob([JSON.stringify(e,null,2)],{type:`application/json`})),n=document.createElement(`a`);n.href=t,n.download=`pi-workspace.json`,n.click(),setTimeout(()=>URL.revokeObjectURL(t),1e3)}),$(`#shell-form`).onsubmit=Kg(async()=>{if(Mg)throw Error(`Save your file edits before running commands.`);let e=$(`#command`).value;if(!e.trim())return;$(`#command`).value=``,$(`#terminal-output`).textContent+=`
154
  $ `+e+`
155
- `;let t=await zg(`shell`,{command:e});$(`#terminal-output`).textContent+=t.stdout+t.stderr+`\n[exit ${t.exitCode}]\n`,$(`#terminal-output`).textContent=$(`#terminal-output`).textContent.slice(-24e3),$(`#terminal-output`).scrollTop=$(`#terminal-output`).scrollHeight}),window.browserPi={ready:Ig,load:qg,prompt:Jg,rpc:zg,diagnostics:Lg,terminal:Rg,get loaded(){return Ag},get busy(){return kg}},Hg();
 
139
  `);return r.addChild(new fp(Q.fg(`toolOutput`,g?a+`
140
 
141
  `+o:s.slice(0,10).join(`
142
+ `)),0,0)),!g&&s.length>10&&r.addChild(new fp(Q.fg(`dim`,`… ${s.length-10} more lines · Ctrl+O to expand`),0,0)),n.addChild(r),n}function w(){l.clear(),l.addChild(b);let e=new Map(h.filter(e=>e.role===`toolResult`).map(e=>[e.toolCallId,e])),t=new Set;for(let n=0;n<=h.length;n++){for(let e of y)e.at===n&&l.addChild(e.component);let r=h[n];if(!r||r.role===`toolResult`)continue;let i=r.role+`:`+r.timestamp+`:`+n;t.add(i);let a=v.get(i);if(a||(a={component:r.role===`user`?new dg(Cg(Sg(r.content))):new pg},v.set(i,a)),r.role===`user`&&S(Sg(r.content)),r.role===`assistant`&&a.message!==r){let e={...r,content:r.content.map(e=>e.type===`text`?{...e,text:Cg(e.text)}:e.type===`thinking`?{...e,thinking:Cg(e.thinking)}:e),errorMessage:r.errorMessage?Cg(r.errorMessage):void 0};a.component.updateContent(e,!r.stopReason)}r.role===`user`&&(n>0||y.length)&&l.addChild(new Rh(1)),a.message=r,l.addChild(a.component);for(let n of Array.isArray(r.content)?r.content:[]){if(n.type!==`toolCall`)continue;let r=e.get(n.id),i=`tool:`+n.id;t.add(i);let a=JSON.stringify([n,r,g]),o=v.get(i);o?.signature!==a&&(o={signature:a,component:C(n,r)},v.set(i,o)),l.addChild(o.component)}}for(let e of v.keys())t.has(e)||v.delete(e);o.requestRender()}function T(){d.text=Q.fg(p.busy?`accent`:`muted`,Cg(p.activity||(p.loaded?`Ready`:`Send a message to get started · /help`))),c.disableSubmit=p.busy||_,c.borderColor=e=>Q.fg(p.busy?`accent`:`borderMuted`,e),t.onInput?.(c.getText()),o.requestRender()}async function E(e){if(!(!e.trim()||p.busy||_)){_=!0,T(),S(e),o.scrollToBottom();try{let n=e.trim();if(n===`/help`)x(`/load Load MiniCPM on this device
143
  /new Clear the conversation; keep workspace files
144
  !cmd Run cmd with just-bash
145
 
 
149
  Ctrl+Shift+F: search transcript
150
 
151
  Pi’s original terminal UI and agent core run here.
152
+ Node CLI extensions, npm, Python and native binaries are unavailable.`);else if(n===`/load`)await t.load();else if(n===`/new`||n===`/clear`)await t.newChat(),y.length=0,w();else if(n.startsWith(`/`))throw Error(`Unknown browser command. Type /help for available commands.`);else if(n.startsWith(`!`)){let e=n.slice(1).trim();if(!e)throw Error(`Enter a shell command after !.`);let r=await t.shell(e);x(`$ ${e}\n${r.stdout}${r.stderr}\n[exit ${r.exitCode}]`,r.exitCode!==0)}else await t.prompt(e)}catch(t){t.name!==`AbortError`&&x(t.message,!0),c.getText()||c.setText(e)}finally{_=!1,T()}}}c.onSubmit=e=>{E(e)},c.onChange=()=>{t.onInput?.(c.getText()),o.requestRender()},c.onEscape=()=>{p.busy&&t.stop().catch(e=>x(e.message,!0))},c.onCtrlD=()=>x(`This terminal lives in your browser. Close the tab to leave.`),c.onAction(`app.tools.expand`,()=>{g=!g,w()}),c.setAutocompleteProvider({getSuggestions(e,t,n){let r=e[t].slice(0,n),i=r.match(/\S*$/)?.[0]??``;if(!i)return null;let a=(r.startsWith(`/`)?wg:m.map(e=>({value:e,label:e,description:`workspace file`}))).filter(e=>e.value.startsWith(i)&&e.value!==i);return a.length?{items:a,prefix:i}:null},applyCompletion(e,t,n,r,i){let a=[...e];return a[t]=e[t].slice(0,n-i.length)+r.value+` `+e[t].slice(n),{lines:a,cursorLine:t,cursorCol:n-i.length+r.value.length+1}}}),n.attachCustomKeyEventHandler(e=>e.type!==`keydown`||e.isComposing?!0:(e.ctrlKey||e.metaKey)&&e.key.toLowerCase()===`v`?!1:e.ctrlKey&&e.shiftKey&&e.code===`KeyF`?(e.preventDefault(),a.inputData(`\x1B[102;6u`),!1):e.key===`Enter`&&e.shiftKey?(e.preventDefault(),a.inputData(`\x1B\r`),!1):(e.ctrlKey||e.metaKey)&&e.key.toLowerCase()===`c`&&(n.hasSelection()||o.hasActiveSelection())?(o.hasActiveSelection()&&(e.preventDefault(),o.copyActiveSelectionToClipboard()),!1):!0);let D;e.addEventListener(`touchstart`,e=>{e.touches.length===1&&(D=e.touches[0].clientY)},{passive:!0}),e.addEventListener(`touchmove`,e=>{if(D===void 0||e.touches.length!==1)return;let t=e.touches[0].clientY,n=Math.trunc((D-t)/18);n&&(o.scrollBy(n),D=t,e.preventDefault())},{passive:!1}),e.addEventListener(`touchend`,()=>{D=void 0},{passive:!0});function O(){e.clientWidth>0&&e.clientHeight>0&&(i.fit(),o.requestRender())}let te=new ResizeObserver(O);return te.observe(e),O(),o.start(),w(),T(),document.fonts.ready.then(O),n.textarea.setAttribute(`aria-label`,`Message Pi`),n.textarea.setAttribute(`autocapitalize`,`off`),n.textarea.setAttribute(`spellcheck`,`false`),{setMessages(e,{reset:t=!1}={}){h=e,t&&(y.length=0,v.clear()),w()},setFiles(e){m=Object.keys(e).map(e=>e.replace(/^\/workspace\//,``))},setState(e){p={...p,...e},T()},submit(){let e=c.getExpandedText();if(!c.disableSubmit&&e.trim())return c.setText(``),E(e)},focus(){n.focus()},newline(){a.inputData(`\x1B\r`),n.focus()},get text(){return c.getExpandedText()},get cols(){return n.cols},get rows(){return n.rows},get viewportTop(){return o.viewportTop},readScreen(){return Array.from({length:n.buffer.active.length},(e,t)=>n.buffer.active.getLine(t)?.translateToString(!0)??``).join(`
153
+ `)},readDocument(){return u.render(n.cols).map(e=>Yu(e).trimEnd())},readCells(){return Array.from({length:n.rows},(e,t)=>Array.from({length:n.cols},(e,r)=>{let i=n.buffer.active.getLine(t)?.getCell(r);return i?{text:i.getChars(),width:i.getWidth(),fg:i.isFgDefault()?ag.foreground:i.getFgColor(),bg:i.isBgDefault()?ag.background:i.getBgColor()}:null}))},dispose(){te.disconnect(),o.stop({preserveScreen:!0}),n.dispose()}}}function Eg(e,{load:t,stop:n,onClose:r}){let i=t=>e.querySelector(t),a=i(`#model-confirm`),o=i(`#model-cancel`),s=i(`#model-status`),c=i(`#model-detail`),l=i(`#model-progress`),u=i(`#model-error`),d,f=!1,p=!1;function m(t){let n=d;d=void 0,e.close(),n?.(t),r()}function h(){if(!p){if(!f){m(!1);return}p=!0,o.disabled=!0,o.textContent=`Stopping…`,s.textContent=`Stopping setup. Your message will be kept.`,n().catch(()=>{})}}return o.onclick=h,e.addEventListener(`cancel`,e=>{e.preventDefault(),h()}),a.onclick=async()=>{if(!f&&d){f=!0,p=!1,a.disabled=!0,a.textContent=`Getting ready…`,o.textContent=`Cancel loading`,u.hidden=!0,i(`#model-loading`).hidden=!1,s.textContent=`Checking your browser and saved model…`,c.textContent=``,l.hidden=!1,l.removeAttribute(`value`),i(`.setup-body`).scrollTop=i(`.setup-body`).scrollHeight;try{await t(),m(!p)}catch(e){if(p)m(!1);else{s.textContent=`Pi could not finish loading.`,c.textContent=`Your message is safe. You can try again or return to editing.`,u.textContent=e.message,u.hidden=!1,l.hidden=!0,a.textContent=`Try again`;let t=document.querySelector(`#open-tab`);i(`#model-open-tab`).hidden=t.hidden,i(`#model-open-tab`).href=t.href,i(`.setup-body`).scrollTop=i(`.setup-body`).scrollHeight}}finally{f=!1,a.disabled=!1,o.disabled=!1,o.textContent=`Back to message`}}},{request(t){if(d)throw Error(`Finish the current model setup before sending another message.`);p=!1,f=!1,a.disabled=!1,a.textContent=`Load model & send`,o.disabled=!1,o.textContent=`Back to message`,i(`#model-preview`).textContent=t,i(`#model-loading`).hidden=!0,i(`#model-open-tab`).hidden=!0,u.hidden=!0,l.hidden=!1;let n=new Promise(e=>{d=e});return e.showModal(),i(`.setup-body`).scrollTop=0,a.focus(),n},update(e){if(f&&!p){if(l.hidden=!1,e.phase===`download`){s.textContent=`Downloading Pi to your device…`;let t=Math.round(e.loaded/e.total*100);l.value=t,c.textContent=`${(e.loaded/1e9).toFixed(2)} of ${(e.total/1e9).toFixed(2)} GB · ${t}%`}else l.removeAttribute(`value`),s.textContent=e.phase===`compile`?`Preparing Pi on your device…`:`Almost ready…`,c.textContent=`Your message will be sent automatically when Pi is ready.`}}}}var $=e=>document.querySelector(e),Dg=new Worker(new URL(new URL(`agent.worker-DqQzo9yL.js`,import.meta.url).href,``+import.meta.url),{type:`module`}),Og=new Map,kg=1,Ag=!1,jg=!1,Mg,Ng=!1,Pg={},Fg=[],Ig,Lg=new Promise(e=>{Ig=e}),Rg={events:[],generations:[],device:null},zg=Tg($(`#pi-terminal`),{load:()=>Lg.then(Yg),prompt:e=>Lg.then(()=>Xg(e)),newChat:()=>Lg.then(Zg),stop:()=>Vg(`stop`),shell:e=>{if(Ng)throw Error(`Save your file edits before running commands.`);return Vg(`shell`,{command:e})}}),Bg=Eg($(`#model-dialog`),{load:Yg,stop:()=>Vg(`stop`),onClose:()=>zg.focus()});function Vg(e,t){return new Promise((n,r)=>{let i=kg++;Og.set(i,{resolve:n,reject:r}),Dg.postMessage({id:i,action:e,args:t})})}function Hg(e){$(`#notice`).textContent=e,$(`#notice`).hidden=!e}function Ug(e){return window.self===window.top||!/denied|storage directory access/i.test(e)?e:($(`#open-tab`).href=location.href,$(`#open-tab`).hidden=!1,`Your browser blocked storage in this embedded app. Open the workspace in a new tab to load the model and save files.`)}function Wg(){$(`#load`).disabled=Ag||jg,$(`#send`).disabled=Ag,$(`#newline`).disabled=Ag,zg.setState({busy:Ag,loaded:jg,activity:$(`#activity`).textContent}),$(`#stop`).hidden=!Ag;for(let e of[`#new-chat`,`#shell-run`,`#add-file`,`#upload`])$(e).disabled=Ag;$(`#save`).disabled=Ag||!Mg||!Ng,$(`#editor`).disabled=Ag||!Mg}function Gg(){zg.setFiles(Pg),$(`#files`).replaceChildren();for(let e of Object.keys(Pg).sort()){let t=document.createElement(`button`);t.textContent=e.replace(/^\/workspace\//,``),t.title=e,t.classList.toggle(`selected`,e===Mg),t.onclick=()=>Kg(e),$(`#files`).append(t)}Mg&&!Ng&&($(`#editor`).value=Pg[Mg]??``)}function Kg(e){if(Ng){Hg(`Save the open file before selecting another file.`);return}Mg=e,$(`#filename`).textContent=e,$(`#editor`).value=Pg[e]??``,Gg(),Wg()}function qg(){zg.setMessages(Fg)}Dg.onmessage=({data:e})=>{if(e.type===`reply`){let t=Og.get(e.id);Og.delete(e.id),e.error?t?.reject(Error(e.error)):t?.resolve(e.value)}else if(e.type===`initialized`)Pg=e.files,Fg=e.messages,Gg(),qg(),Pg[`/workspace/README.md`]&&Kg(`/workspace/README.md`),Ig();else if(e.type===`busy`)Ag=!0,$(`#activity`).textContent=e.action===`prompt`?`Pi is working…`:`Working…`,Wg();else if(e.type===`idle`)Ag=!1,$(`#activity`).textContent=jg?`Ready · running on your device`:`Files and chat are saved on this device.`,Wg();else if(e.type===`workspace`)Pg=e.files,Gg();else if(e.type===`device`)Rg.device=e.device;else if(e.type===`load_progress`)Bg.update(e),e.phase===`download`?($(`#progress`).hidden=!1,$(`#progress`).value=e.loaded/e.total*100,$(`#load-status`).textContent=e.loaded===e.total?`Download verified. Preparing model…`:`Downloading and verifying model…`,$(`#load-detail`).textContent=`${(e.loaded/1e9).toFixed(2)} / ${(e.total/1e9).toFixed(2)} GB · ${Math.round(e.loaded/e.total*100)}%${e.cached?` · ${(e.cached/1e9).toFixed(2)} GB cached`:``}`):($(`#load-status`).textContent=e.phase===`compile`?`Preparing WebGPU session…`:`Warming up GPU shaders…`,$(`#progress`).removeAttribute(`value`));else if(e.type===`loaded`)jg=!0,Rg.device=e.device,$(`#progress`).hidden=!0,$(`#load`).textContent=`Loaded ✓`,$(`#load-status`).textContent=`Ready. Model inference and tools run entirely in this browser.`,$(`#load-detail`).textContent=e.cachedBytes?`Verified model restored from browser storage.`:`SHA-256 verified · model cached for next time.`,Wg();else if(e.type===`agent_event`){let t=e.event;if(Rg.events.push(t.type),Rg.events.length>500&&Rg.events.shift(),t.message){let e=Fg.findIndex(e=>e.timestamp===t.message.timestamp&&e.role===t.message.role&&e.toolCallId===t.message.toolCallId);e<0?Fg.push(t.message):Fg[e]=t.message,qg()}t.type===`tool_execution_start`&&($(`#activity`).textContent=`Running ${t.toolName}…`),t.type===`turn_start`&&($(`#activity`).textContent=`Pi is thinking…`),zg.setState({activity:$(`#activity`).textContent})}else e.type===`generation`?(Rg.generations.push(e),Rg.generations.length>100&&Rg.generations.shift()):e.type===`context_trim`?Hg(`Older turns were left out of the model context. Your full chat remains saved.`):e.type===`notice`?Hg(e.text):e.type===`persistence_error`?Hg(Ug(e.error)):e.type===`fatal`&&(jg=!1,Hg(e.error),Wg())},Dg.onerror=e=>{let t=Error(e.message||`The worker stopped. Reload to restore your saved workspace.`);Hg(t.message);for(let e of Og.values())e.reject(t);Og.clear(),Ag=!1,jg=!1,Wg()};var Jg=e=>async t=>{t?.preventDefault();try{await Lg,await e(t)}catch(e){Hg(e.message)}};async function Yg(){Hg(``);let e=new URLSearchParams(location.search);try{return await Vg(`load`,{runtimeURL:new URL(`./runtime/`,location.href).href,local:e.get(`source`)===`local`,origin:location.origin,limit128:e.has(`limit128`)})}catch(e){throw e.message=Ug(e.message),$(`#load-status`).textContent=e.message,$(`#progress`).hidden=!0,e}}async function Xg(e){if(!e.trim())return;if(Ng)throw Error(`Save your file edits before asking Pi to work on them.`);if(!jg&&!await Bg.request(e))throw new DOMException(`Message kept for later.`,`AbortError`);Hg(``);let t=await Vg(`prompt`,{text:e});return Fg=t.messages,qg(),t}$(`#load`).onclick=Jg(Yg),$(`#send`).onclick=()=>{zg.submit(),zg.focus()},$(`#newline`).onclick=()=>zg.newline(),$(`#stop`).onclick=Jg(()=>Vg(`stop`));async function Zg(){Fg=(await Vg(`new_chat`)).messages,zg.setMessages(Fg,{reset:!0}),Hg(``)}$(`#new-chat`).onclick=Jg(Zg),$(`#editor`).oninput=()=>{Ng=!0,Wg()},$(`#save`).onclick=Jg(async()=>{await Vg(`write`,{path:Mg,content:$(`#editor`).value}),Ng=!1,Gg(),Wg(),Hg(``)}),$(`#add-file`).onclick=Jg(async()=>{if(Ng)throw Error(`Save the open file first.`);let e=window.prompt(`File path, relative to /workspace:`);if(!e?.trim())return;let t=e.startsWith(`/`)?e:`/workspace/`+e;if(Object.hasOwn(Pg,t))throw Error(`That file already exists.`);await Vg(`write`,{path:t,content:``}),Kg(t)}),$(`#upload`).onchange=Jg(async()=>{if(Ng)throw Error(`Save the open file first.`);for(let e of $(`#upload`).files){if(e.size>1024**2)throw Error(`Import text files of 1 MiB or less.`);let t=await e.text();if(t.includes(`\0`))throw Error(`Import supports text files only.`);let n=`/workspace/`+e.name;if(Object.hasOwn(Pg,n))throw Error(e.name+` already exists. Rename it before importing.`);await Vg(`write`,{path:n,content:t}),Kg(n)}$(`#upload`).value=``}),$(`#export`).onclick=Jg(async()=>{let e=await Vg(`snapshot`),t=URL.createObjectURL(new Blob([JSON.stringify(e,null,2)],{type:`application/json`})),n=document.createElement(`a`);n.href=t,n.download=`pi-workspace.json`,n.click(),setTimeout(()=>URL.revokeObjectURL(t),1e3)}),$(`#shell-form`).onsubmit=Jg(async()=>{if(Ng)throw Error(`Save your file edits before running commands.`);let e=$(`#command`).value;if(!e.trim())return;$(`#command`).value=``,$(`#terminal-output`).textContent+=`
154
  $ `+e+`
155
+ `;let t=await Vg(`shell`,{command:e});$(`#terminal-output`).textContent+=t.stdout+t.stderr+`\n[exit ${t.exitCode}]\n`,$(`#terminal-output`).textContent=$(`#terminal-output`).textContent.slice(-24e3),$(`#terminal-output`).scrollTop=$(`#terminal-output`).scrollHeight}),window.browserPi={ready:Lg,load:Yg,prompt:Xg,rpc:Vg,diagnostics:Rg,terminal:zg,get loaded(){return jg},get busy(){return Ag}},Wg();
deployment-manifest.json CHANGED
@@ -13,32 +13,32 @@
13
  "sha256": "7b2f4725b2cb4fc1d22031a311857d5ac8754c882bf4e01b5ab1a9778b2417f0"
14
  },
15
  "README.md": {
16
- "bytes": 9804,
17
- "sha256": "4987a8ecbfd1b2a1a3a1d38863203a233677f198b818c5622cfa970bace5c7d7"
18
  },
19
  "SPACE_README.md": {
20
- "bytes": 9804,
21
- "sha256": "4987a8ecbfd1b2a1a3a1d38863203a233677f198b818c5622cfa970bace5c7d7"
22
  },
23
  "app/README.md": {
24
- "bytes": 9065,
25
- "sha256": "a76448dca728560677b11a462d149f17de3cd7090db0b31c49846ef812be623a"
26
  },
27
  "app/index.html": {
28
- "bytes": 3318,
29
- "sha256": "1a4c2b6cf732daa186db47411ecf45907c51c9be746b8e6dbbce7bf958129748"
30
  },
31
  "app/package-lock.json": {
32
  "bytes": 159682,
33
  "sha256": "84f416dfdc71e02e64bef7c3f36004f3a8dbe95e608f1a8710f2e22754b2105b"
34
  },
35
  "app/package.json": {
36
- "bytes": 1003,
37
- "sha256": "1a49f797391d19ab5045ca4c5e978d2bfe1ac26bf80cc6a571303940729f0e67"
38
  },
39
  "app/public/favicon.svg": {
40
  "bytes": 227,
41
- "sha256": "c5cf939c058ae5b63f0cdab8d1becc4544582d6ed74098e9c8460d3acb78309e"
42
  },
43
  "app/public/licenses/onnxruntime-LICENSE": {
44
  "bytes": 1073,
@@ -72,9 +72,13 @@
72
  "bytes": 8633,
73
  "sha256": "04375923fd4d2cfd6a58e17fb2eb966ddf4fb766fb050a88459e2d8e7358926c"
74
  },
 
 
 
 
75
  "app/src/main.mjs": {
76
- "bytes": 10043,
77
- "sha256": "a0a818a35e52bf8edb21d011e7167848531efe01146a4e10dd9d49a4817e93ce"
78
  },
79
  "app/src/markdown.mjs": {
80
  "bytes": 699,
@@ -97,8 +101,8 @@
97
  "sha256": "f7e6ae0ba428b78126c6873f9784dbf7d9220421cd6510ad893819492ed5ff9b"
98
  },
99
  "app/src/pi/terminal-ui.mjs": {
100
- "bytes": 15617,
101
- "sha256": "6ced7c0a45042bcc4f6d7cb1d25b788687a0243c801380cdd685596b57c03d9a"
102
  },
103
  "app/src/pi/theme.mjs": {
104
  "bytes": 2453,
@@ -117,8 +121,8 @@
117
  "sha256": "dec9e2962a0f5c076512368395017fb5af7c9f7bfbe8cfed10105072dbcb76fe"
118
  },
119
  "app/src/style.css": {
120
- "bytes": 7866,
121
- "sha256": "733e3a05abf8495d77a03bef84f4d7f83c48bd229c556da1229f99d7684cae8c"
122
  },
123
  "app/src/workspace.mjs": {
124
  "bytes": 5206,
@@ -136,6 +140,10 @@
136
  "bytes": 1352,
137
  "sha256": "ff314f593b7c8171c63043dd10e4ea1121a83a97b411fccee5a20ede80adc4e1"
138
  },
 
 
 
 
139
  "app/tests/no-gpu.mjs": {
140
  "bytes": 1390,
141
  "sha256": "8331c6664f04d2a5858af36a25d53b33d6283229b7a105696e7040ca1695a881"
@@ -153,8 +161,8 @@
153
  "sha256": "8cc0750f0861f5cf3d63e155c9ea4091bae5432a5a6719a55941d63599456e55"
154
  },
155
  "app/tests/space-embedded.mjs": {
156
- "bytes": 4019,
157
- "sha256": "013523ed4316b1028c9a4daa09313e7dda674dbbb4c671f491be4055900c6348"
158
  },
159
  "app/tests/space-restricted.mjs": {
160
  "bytes": 2580,
@@ -208,13 +216,13 @@
208
  "bytes": 2092911,
209
  "sha256": "1fc0006217cf28bf42951d1738e67d235b9516f9d579fa46064718759fd7ba28"
210
  },
211
- "assets/index-CTSRWQwn.css": {
212
- "bytes": 10716,
213
- "sha256": "d8ad3666932f160ec77397428b275a8cf761265018233b6f4c65ea956f09388f"
214
  },
215
- "assets/index-qmdKynRr.js": {
216
- "bytes": 724782,
217
- "sha256": "63b3e9ed88cef0bd00ac3292e14dec7cdc84299057057ea993b21c2c32066c50"
218
  },
219
  "assets/ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm": {
220
  "bytes": 23567050,
@@ -222,11 +230,11 @@
222
  },
223
  "favicon.svg": {
224
  "bytes": 227,
225
- "sha256": "c5cf939c058ae5b63f0cdab8d1becc4544582d6ed74098e9c8460d3acb78309e"
226
  },
227
  "index.html": {
228
- "bytes": 3415,
229
- "sha256": "7c8ff290b6acc634abcaefc921c2d302e69a5ac1d54119a74fb91841cc81dae9"
230
  },
231
  "licenses/dependencies.txt": {
232
  "bytes": 1385412,
@@ -253,8 +261,8 @@
253
  "sha256": "2a2dca97d6a9480afff1ed0da8f5924610552cfe509d5081788455f9c1d71f8b"
254
  },
255
  "research/browser-agent/REPORT.md": {
256
- "bytes": 18313,
257
- "sha256": "3ae9d5ea4112a01f3719d40f1136a7184b1e9bf13d4858ec4b74cf3c42d1001d"
258
  },
259
  "research/browser-agent/just-bash-README.md": {
260
  "bytes": 29503,
@@ -317,8 +325,8 @@
317
  "sha256": "b9bb39d5bd7bceb260d5eafa633f5af2e47a35d6132c8453d908ea6a4ad8a0f9"
318
  },
319
  "scripts/prepare_space.py": {
320
- "bytes": 4376,
321
- "sha256": "14b73c9cf7523baf4baea09ad453803cdb8d0228e639a9b947d35fa27332cc57"
322
  },
323
  "scripts/reference.py": {
324
  "bytes": 2652,
@@ -336,6 +344,14 @@
336
  "bytes": 1139,
337
  "sha256": "fbefb4b9af0fb703f60d1b47e589104d98510fe4ac77a8e3481c031bc7d22f84"
338
  },
 
 
 
 
 
 
 
 
339
  "validation/agent-browser-dev.json": {
340
  "bytes": 3162,
341
  "sha256": "1044643906485209dfc975edba93596392f8de8e41bf0491fcb9005f08451221"
@@ -356,6 +372,10 @@
356
  "bytes": 2792,
357
  "sha256": "14aa728cd704e53ca1672cdeaaf5f53a3eb898fc3f3acd078321dc2fd8b8b47b"
358
  },
 
 
 
 
359
  "validation/agent-space-checksums.json": {
360
  "bytes": 2915,
361
  "sha256": "4ff4a95ed48aa72b34a8483e5e4f7070a3e61a0afc9b72820923e80de21257f5"
 
13
  "sha256": "7b2f4725b2cb4fc1d22031a311857d5ac8754c882bf4e01b5ab1a9778b2417f0"
14
  },
15
  "README.md": {
16
+ "bytes": 10090,
17
+ "sha256": "52f23506cc034a8f451db3ef8d2ad9dd507e679ef5b6e42048d75e8cc66e1923"
18
  },
19
  "SPACE_README.md": {
20
+ "bytes": 10090,
21
+ "sha256": "52f23506cc034a8f451db3ef8d2ad9dd507e679ef5b6e42048d75e8cc66e1923"
22
  },
23
  "app/README.md": {
24
+ "bytes": 9600,
25
+ "sha256": "246a1d2edab97b31613da69a703c3957cd9b17ba7ad7bf47636ddfc1748245c5"
26
  },
27
  "app/index.html": {
28
+ "bytes": 4647,
29
+ "sha256": "31ec2e45afb26a4945af669a891c2d32562764841fa7383a14dea98e051da959"
30
  },
31
  "app/package-lock.json": {
32
  "bytes": 159682,
33
  "sha256": "84f416dfdc71e02e64bef7c3f36004f3a8dbe95e608f1a8710f2e22754b2105b"
34
  },
35
  "app/package.json": {
36
+ "bytes": 1061,
37
+ "sha256": "f13795fd92e7455056b88dde521b107e4af3080447abc09171718fd8e755d3e5"
38
  },
39
  "app/public/favicon.svg": {
40
  "bytes": 227,
41
+ "sha256": "60c05c29254bfc07d26ca1b57ce71414648f6786105788b39122d1fe90eee698"
42
  },
43
  "app/public/licenses/onnxruntime-LICENSE": {
44
  "bytes": 1073,
 
72
  "bytes": 8633,
73
  "sha256": "04375923fd4d2cfd6a58e17fb2eb966ddf4fb766fb050a88459e2d8e7358926c"
74
  },
75
+ "app/src/load-dialog.mjs": {
76
+ "bytes": 3858,
77
+ "sha256": "68beae07a951bf4e810767c84853c0c6a03d80feaff8a2112fff5e18d9e24dd8"
78
+ },
79
  "app/src/main.mjs": {
80
+ "bytes": 10303,
81
+ "sha256": "67a5b672d607980b13f4403303db718c89020b57d77ce3a61b56ea824c94f456"
82
  },
83
  "app/src/markdown.mjs": {
84
  "bytes": 699,
 
101
  "sha256": "f7e6ae0ba428b78126c6873f9784dbf7d9220421cd6510ad893819492ed5ff9b"
102
  },
103
  "app/src/pi/terminal-ui.mjs": {
104
+ "bytes": 15657,
105
+ "sha256": "70462149eafb94b6f9fec45907bcea4be3d294370409d0da91c144856e55da8e"
106
  },
107
  "app/src/pi/theme.mjs": {
108
  "bytes": 2453,
 
121
  "sha256": "dec9e2962a0f5c076512368395017fb5af7c9f7bfbe8cfed10105072dbcb76fe"
122
  },
123
  "app/src/style.css": {
124
+ "bytes": 9977,
125
+ "sha256": "49d03d12f0c69c98e1695363442c00e3c58c25c1284a0c7056cc47ee14a3d08b"
126
  },
127
  "app/src/workspace.mjs": {
128
  "bytes": 5206,
 
140
  "bytes": 1352,
141
  "sha256": "ff314f593b7c8171c63043dd10e4ea1121a83a97b411fccee5a20ede80adc4e1"
142
  },
143
+ "app/tests/first-message.mjs": {
144
+ "bytes": 7621,
145
+ "sha256": "2f1f3a4652ebd780dcbd1443f5d71db21a255ba99a6cc15deb7d43c2ebb3def4"
146
+ },
147
  "app/tests/no-gpu.mjs": {
148
  "bytes": 1390,
149
  "sha256": "8331c6664f04d2a5858af36a25d53b33d6283229b7a105696e7040ca1695a881"
 
161
  "sha256": "8cc0750f0861f5cf3d63e155c9ea4091bae5432a5a6719a55941d63599456e55"
162
  },
163
  "app/tests/space-embedded.mjs": {
164
+ "bytes": 5104,
165
+ "sha256": "63ad91daa3cfef1abc2cfbdce203fbd170a61c7eb1e5467d151e1ddb30323adc"
166
  },
167
  "app/tests/space-restricted.mjs": {
168
  "bytes": 2580,
 
216
  "bytes": 2092911,
217
  "sha256": "1fc0006217cf28bf42951d1738e67d235b9516f9d579fa46064718759fd7ba28"
218
  },
219
+ "assets/index-9339vMLa.css": {
220
+ "bytes": 12607,
221
+ "sha256": "54581a8cc4bf1844e25e2c3876a15e1e5627b813d2a763d7b1ecbf3d71a06e70"
222
  },
223
+ "assets/index-CsaxMUF5.js": {
224
+ "bytes": 727106,
225
+ "sha256": "9efc9363ab67a78168190f66628a11093bc307cba00f302838017c1c9787ea1f"
226
  },
227
  "assets/ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm": {
228
  "bytes": 23567050,
 
230
  },
231
  "favicon.svg": {
232
  "bytes": 227,
233
+ "sha256": "60c05c29254bfc07d26ca1b57ce71414648f6786105788b39122d1fe90eee698"
234
  },
235
  "index.html": {
236
+ "bytes": 4744,
237
+ "sha256": "a089fb6d9cd6fc432c8741f3d64b1226b83e1c81209a2faf334e435145b8842a"
238
  },
239
  "licenses/dependencies.txt": {
240
  "bytes": 1385412,
 
261
  "sha256": "2a2dca97d6a9480afff1ed0da8f5924610552cfe509d5081788455f9c1d71f8b"
262
  },
263
  "research/browser-agent/REPORT.md": {
264
+ "bytes": 20040,
265
+ "sha256": "35600bccc6500c8ba66deb3666aa35fe347867fead6c515b0bf47eeb4d460775"
266
  },
267
  "research/browser-agent/just-bash-README.md": {
268
  "bytes": 29503,
 
325
  "sha256": "b9bb39d5bd7bceb260d5eafa633f5af2e47a35d6132c8453d908ea6a4ad8a0f9"
326
  },
327
  "scripts/prepare_space.py": {
328
+ "bytes": 4517,
329
+ "sha256": "d6463e9bccd2f60140be603593290158e9243cda7af957f1b850ed7f9e6be64e"
330
  },
331
  "scripts/reference.py": {
332
  "bytes": 2652,
 
344
  "bytes": 1139,
345
  "sha256": "fbefb4b9af0fb703f60d1b47e589104d98510fe4ac77a8e3481c031bc7d22f84"
346
  },
347
+ "validation/agent-app-dark-render.json": {
348
+ "bytes": 7997,
349
+ "sha256": "04e35acd8efb74a87c108c18963c0305e70c96b5094aab6570d32194086c1c25"
350
+ },
351
+ "validation/agent-app-dark-terminal.json": {
352
+ "bytes": 2942,
353
+ "sha256": "5dba1f6d65c38823491c6c004933deec812a3ae9a3d4d4809ed51463dadce117"
354
+ },
355
  "validation/agent-browser-dev.json": {
356
  "bytes": 3162,
357
  "sha256": "1044643906485209dfc975edba93596392f8de8e41bf0491fcb9005f08451221"
 
372
  "bytes": 2792,
373
  "sha256": "14aa728cd704e53ca1672cdeaaf5f53a3eb898fc3f3acd078321dc2fd8b8b47b"
374
  },
375
+ "validation/agent-first-message.json": {
376
+ "bytes": 3717,
377
+ "sha256": "eadd2ee2f6ce5f40ebd864a36408efe0c01bba9d8da859c96c442d2a48103b04"
378
+ },
379
  "validation/agent-space-checksums.json": {
380
  "bytes": 2915,
381
  "sha256": "4ff4a95ed48aa72b34a8483e5e4f7070a3e61a0afc9b72820923e80de21257f5"
favicon.svg CHANGED
index.html CHANGED
@@ -2,12 +2,13 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
5
- <meta name="theme-color" content="#f5f5f5">
 
6
  <link rel="icon" href="./favicon.svg" type="image/svg+xml">
7
  <meta name="description" content="Pi, MiniCPM5 and a bash workspace, running entirely in your browser.">
8
  <title>Pi · Browser workspace</title>
9
- <script type="module" crossorigin src="./assets/index-qmdKynRr.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-CTSRWQwn.css">
11
  </head>
12
  <body>
13
  <header><a class="brand" href="./"><span class="pi">π</span><span>Pi <small>browser workspace</small></span></a>
@@ -38,5 +39,23 @@ Try: ls -la
38
  <p class="workspace-note">Bash, text tools, and a virtual filesystem. Node.js, Python, and network commands are unavailable.</p>
39
  </aside>
40
  </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  </body>
42
  </html>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <meta name="theme-color" content="#18181e">
6
+ <meta name="color-scheme" content="dark">
7
  <link rel="icon" href="./favicon.svg" type="image/svg+xml">
8
  <meta name="description" content="Pi, MiniCPM5 and a bash workspace, running entirely in your browser.">
9
  <title>Pi · Browser workspace</title>
10
+ <script type="module" crossorigin src="./assets/index-CsaxMUF5.js"></script>
11
+ <link rel="stylesheet" crossorigin href="./assets/index-9339vMLa.css">
12
  </head>
13
  <body>
14
  <header><a class="brand" href="./"><span class="pi">π</span><span>Pi <small>browser workspace</small></span></a>
 
39
  <p class="workspace-note">Bash, text tools, and a virtual filesystem. Node.js, Python, and network commands are unavailable.</p>
40
  </aside>
41
  </main>
42
+ <dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
43
+ <div class="setup-body">
44
+ <div class="setup-icon" aria-hidden="true">π</div>
45
+ <h2 id="model-title">Let’s get Pi ready</h2>
46
+ <p id="model-description">Pi runs on your device. Load its model once, and we’ll send your message as soon as it’s ready.</p>
47
+ <div class="setup-facts"><strong>About 1.84 GB on the first download</strong><span>Saved in this browser for faster starts next time.</span></div>
48
+ <p class="setup-label">Your message</p><div id="model-preview"></div>
49
+ <div id="model-loading" hidden>
50
+ <p id="model-status" role="status"></p>
51
+ <progress id="model-progress" max="100" aria-label="Model setup progress"></progress>
52
+ <p id="model-detail"></p>
53
+ </div>
54
+ <p id="model-error" role="alert" hidden></p>
55
+ <a id="model-open-tab" class="button" target="_blank" rel="noopener noreferrer" hidden>Open workspace in a new tab ↗</a>
56
+ <p class="setup-privacy">Your message and the AI stay in your browser.</p>
57
+ </div>
58
+ <div class="setup-actions"><button id="model-cancel" type="button">Back to message</button><button id="model-confirm" type="button" class="primary" autofocus>Load model &amp; send</button></div>
59
+ </dialog>
60
  </body>
61
  </html>
research/browser-agent/REPORT.md CHANGED
@@ -90,3 +90,11 @@ The pinned xterm grapheme addon 0.4.0 activates Unicode 15 grapheme handling to
90
  Five actual-model workflows passed (`agent-terminal-dark-model.json`), with networking disabled during tool execution, zero network requests during those tasks, generation cancellation, persistence, and zero weight downloads on reload. Cached model setup took 2.208 seconds; reload took 2.057 seconds on this run's desktop GPU. `agent-terminal-dark-conversation.json` records ten consecutive user turns without resetting chat, including twelve actual tool results, file creation/read/edit, a forty-line file, Markdown, shell execution, and missing-file recovery. All assertions passed and no page errors occurred. These are integration tests on desktop Chromium, not a physical mobile-device or broad model-quality evaluation.
91
 
92
  The dark application was published in Space commit `4e83552f0452b44d3ddfa8740cc8942ff4f1d54f`; all 117 staged files matched the Hub's Git/LFS hashes and the Space reported RUNNING. The public app passed the full rendering suite and twelve keyboard checks (`agent-terminal-dark-space-render.json`, `agent-terminal-dark-space.json`). The actual Hugging Face iframe restored the model in 3.692 seconds, then used the terminal prompt, Pi, WebGPU and bash/file tools to create and verify a fresh $144 report (`agent-terminal-dark-space-embedded.json`). The embedded screenshot was visually reviewed after deployment.
 
 
 
 
 
 
 
 
 
90
  Five actual-model workflows passed (`agent-terminal-dark-model.json`), with networking disabled during tool execution, zero network requests during those tasks, generation cancellation, persistence, and zero weight downloads on reload. Cached model setup took 2.208 seconds; reload took 2.057 seconds on this run's desktop GPU. `agent-terminal-dark-conversation.json` records ten consecutive user turns without resetting chat, including twelve actual tool results, file creation/read/edit, a forty-line file, Markdown, shell execution, and missing-file recovery. All assertions passed and no page errors occurred. These are integration tests on desktop Chromium, not a physical mobile-device or broad model-quality evaluation.
91
 
92
  The dark application was published in Space commit `4e83552f0452b44d3ddfa8740cc8942ff4f1d54f`; all 117 staged files matched the Hub's Git/LFS hashes and the Space reported RUNNING. The public app passed the full rendering suite and twelve keyboard checks (`agent-terminal-dark-space-render.json`, `agent-terminal-dark-space.json`). The actual Hugging Face iframe restored the model in 3.692 seconds, then used the terminal prompt, Pi, WebGPU and bash/file tools to create and verify a fresh $144 report (`agent-terminal-dark-space-embedded.json`). The embedded screenshot was visually reviewed after deployment.
93
+
94
+ ### Entire app dark theme and first-message setup — 2026-09-13
95
+
96
+ The header, model panel, file list/editor, shell, buttons, notices, native controls and scrollbars now share the terminal's dark color family. The page declares a dark color scheme even when the OS prefers light, and the browser theme color and favicon match. Existing Pi cell padding and transcript rendering are preserved.
97
+
98
+ Sending a message before loading now opens a native modal dialog explaining the approximately 1.84 GB first download and browser cache. The message appears in a preview. **Load model & send** invokes the existing verified loader, shows download/setup progress, and sends the queued message once loading succeeds. Escape or **Back to message** restores the unsent draft without an error in the transcript. Cancellation during setup waits for loader cleanup; errors remain in the modal with retry and an embedded-storage fallback link where applicable. Small viewports scroll the modal body while keeping its action buttons visible.
99
+
100
+ `validation/agent-first-message.json` verifies the production app with actual browser/worker behavior: no model request before confirmation, keyboard focus, five modal sizes down to 320×568 and 390×420, all app surfaces dark under a light OS preference, actual unsupported-WebGPU errors and retries, Escape/draft restoration, cancellation of real setup, and successful actual-model automatic sending. The model read a fresh random value from a workspace file, with exactly one user message in the resulting chat and zero page errors. The terminal's twelve interaction checks, seven-size rendering suite and eight unit tests also passed. Mobile checks remain viewport simulations on desktop Chromium.
scripts/prepare_space.py CHANGED
@@ -46,6 +46,8 @@ for name in [
46
  'agent-terminal-dark-space.json', 'agent-terminal-dark-space-render.json',
47
  'agent-terminal-dark-space-embedded.json',
48
  'agent-terminal-dark-space-checksums.json',
 
 
49
  ]:
50
  source = root / 'validation' / name
51
  if source.is_file():
 
46
  'agent-terminal-dark-space.json', 'agent-terminal-dark-space-render.json',
47
  'agent-terminal-dark-space-embedded.json',
48
  'agent-terminal-dark-space-checksums.json',
49
+ 'agent-app-dark-render.json', 'agent-app-dark-terminal.json',
50
+ 'agent-first-message.json', 'agent-first-message-space-embedded.json',
51
  ]:
52
  source = root / 'validation' / name
53
  if source.is_file():
validation/agent-app-dark-render.json ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "date": "2026-09-12T22:59:14.559Z",
3
+ "url": "http://127.0.0.1:4173/",
4
+ "kind": "synthetic rendering fixtures; no model inference",
5
+ "errors": [],
6
+ "checks": [
7
+ "Upstream Pi user/assistant indentation, inner padding and turn separators",
8
+ "Upstream tool Box(1,1) padding and separator; no trailing empty output rows",
9
+ "CJK, emoji, ZWJ and combining accents agree with Pi cell widths",
10
+ "Long tool results collapse/expand without losing content",
11
+ "Seven viewports: Markdown, tool cards, footer/controls, wrapping and 4.5:1 visible text contrast",
12
+ "60-turn transcript and streamed updates preserve scroll position",
13
+ "Multiline draft remains visible at keyboard-sized height"
14
+ ],
15
+ "sizes": [
16
+ {
17
+ "name": "1440x1000",
18
+ "viewport": {
19
+ "width": 1440,
20
+ "height": 1000
21
+ },
22
+ "cols": 94,
23
+ "rows": 41,
24
+ "panel": {
25
+ "x": 14,
26
+ "y": 137.734375,
27
+ "width": 758.875,
28
+ "height": 854.265625,
29
+ "right": 772.875,
30
+ "bottom": 992
31
+ },
32
+ "terminal": {
33
+ "x": 23,
34
+ "y": 146.734375,
35
+ "width": 740.875,
36
+ "height": 781.46875,
37
+ "right": 763.875,
38
+ "bottom": 928.203125
39
+ },
40
+ "screen": {
41
+ "x": 23,
42
+ "y": 146.734375,
43
+ "width": 736,
44
+ "height": 779,
45
+ "right": 759,
46
+ "bottom": 925.734375
47
+ },
48
+ "controls": {
49
+ "x": 23,
50
+ "y": 928.203125,
51
+ "width": 740.875,
52
+ "height": 36.796875,
53
+ "right": 763.875,
54
+ "bottom": 965
55
+ },
56
+ "overflow": false,
57
+ "background": "rgb(24, 24, 30)"
58
+ },
59
+ {
60
+ "name": "1024x768",
61
+ "viewport": {
62
+ "width": 1024,
63
+ "height": 768
64
+ },
65
+ "cols": 65,
66
+ "rows": 28,
67
+ "panel": {
68
+ "x": 14,
69
+ "y": 137.734375,
70
+ "width": 531.5625,
71
+ "height": 622.265625,
72
+ "right": 545.5625,
73
+ "bottom": 760
74
+ },
75
+ "terminal": {
76
+ "x": 23,
77
+ "y": 146.734375,
78
+ "width": 513.5625,
79
+ "height": 549.46875,
80
+ "right": 536.5625,
81
+ "bottom": 696.203125
82
+ },
83
+ "screen": {
84
+ "x": 23,
85
+ "y": 146.734375,
86
+ "width": 509,
87
+ "height": 532,
88
+ "right": 532,
89
+ "bottom": 678.734375
90
+ },
91
+ "controls": {
92
+ "x": 23,
93
+ "y": 696.203125,
94
+ "width": 513.5625,
95
+ "height": 36.796875,
96
+ "right": 536.5625,
97
+ "bottom": 733
98
+ },
99
+ "overflow": false,
100
+ "background": "rgb(24, 24, 30)"
101
+ },
102
+ {
103
+ "name": "851x640",
104
+ "viewport": {
105
+ "width": 851,
106
+ "height": 640
107
+ },
108
+ "cols": 53,
109
+ "rows": 22,
110
+ "panel": {
111
+ "x": 14,
112
+ "y": 137.734375,
113
+ "width": 437.015625,
114
+ "height": 494.265625,
115
+ "right": 451.015625,
116
+ "bottom": 632
117
+ },
118
+ "terminal": {
119
+ "x": 23,
120
+ "y": 146.734375,
121
+ "width": 419.015625,
122
+ "height": 421.46875,
123
+ "right": 442.015625,
124
+ "bottom": 568.203125
125
+ },
126
+ "screen": {
127
+ "x": 23,
128
+ "y": 146.734375,
129
+ "width": 415,
130
+ "height": 418,
131
+ "right": 438,
132
+ "bottom": 564.734375
133
+ },
134
+ "controls": {
135
+ "x": 23,
136
+ "y": 568.203125,
137
+ "width": 419.015625,
138
+ "height": 36.796875,
139
+ "right": 442.015625,
140
+ "bottom": 605
141
+ },
142
+ "overflow": false,
143
+ "background": "rgb(24, 24, 30)"
144
+ },
145
+ {
146
+ "name": "850x640",
147
+ "viewport": {
148
+ "width": 850,
149
+ "height": 640
150
+ },
151
+ "cols": 103,
152
+ "rows": 22,
153
+ "panel": {
154
+ "x": 10,
155
+ "y": 133.734375,
156
+ "width": 830,
157
+ "height": 496.265625,
158
+ "right": 840,
159
+ "bottom": 630
160
+ },
161
+ "terminal": {
162
+ "x": 19,
163
+ "y": 142.734375,
164
+ "width": 812,
165
+ "height": 424.46875,
166
+ "right": 831,
167
+ "bottom": 567.203125
168
+ },
169
+ "screen": {
170
+ "x": 19,
171
+ "y": 142.734375,
172
+ "width": 806,
173
+ "height": 418,
174
+ "right": 825,
175
+ "bottom": 560.734375
176
+ },
177
+ "controls": {
178
+ "x": 19,
179
+ "y": 567.203125,
180
+ "width": 812,
181
+ "height": 36.796875,
182
+ "right": 831,
183
+ "bottom": 604
184
+ },
185
+ "overflow": false,
186
+ "background": "rgb(24, 24, 30)"
187
+ },
188
+ {
189
+ "name": "390x844",
190
+ "viewport": {
191
+ "width": 390,
192
+ "height": 844
193
+ },
194
+ "cols": 44,
195
+ "rows": 32,
196
+ "panel": {
197
+ "x": 10,
198
+ "y": 149.671875,
199
+ "width": 370,
200
+ "height": 684.328125,
201
+ "right": 380,
202
+ "bottom": 834
203
+ },
204
+ "terminal": {
205
+ "x": 19,
206
+ "y": 158.671875,
207
+ "width": 352,
208
+ "height": 612.53125,
209
+ "right": 371,
210
+ "bottom": 771.203125
211
+ },
212
+ "screen": {
213
+ "x": 19,
214
+ "y": 158.671875,
215
+ "width": 344,
216
+ "height": 608,
217
+ "right": 363,
218
+ "bottom": 766.671875
219
+ },
220
+ "controls": {
221
+ "x": 19,
222
+ "y": 771.203125,
223
+ "width": 352,
224
+ "height": 36.796875,
225
+ "right": 371,
226
+ "bottom": 808
227
+ },
228
+ "overflow": false,
229
+ "background": "rgb(24, 24, 30)"
230
+ },
231
+ {
232
+ "name": "320x568",
233
+ "viewport": {
234
+ "width": 320,
235
+ "height": 568
236
+ },
237
+ "cols": 36,
238
+ "rows": 17,
239
+ "panel": {
240
+ "x": 10,
241
+ "y": 149.671875,
242
+ "width": 300,
243
+ "height": 408.328125,
244
+ "right": 310,
245
+ "bottom": 558
246
+ },
247
+ "terminal": {
248
+ "x": 19,
249
+ "y": 158.671875,
250
+ "width": 282,
251
+ "height": 336.53125,
252
+ "right": 301,
253
+ "bottom": 495.203125
254
+ },
255
+ "screen": {
256
+ "x": 19,
257
+ "y": 158.671875,
258
+ "width": 282,
259
+ "height": 323,
260
+ "right": 301,
261
+ "bottom": 481.671875
262
+ },
263
+ "controls": {
264
+ "x": 19,
265
+ "y": 495.203125,
266
+ "width": 282,
267
+ "height": 36.796875,
268
+ "right": 301,
269
+ "bottom": 532
270
+ },
271
+ "overflow": false,
272
+ "background": "rgb(24, 24, 30)"
273
+ },
274
+ {
275
+ "name": "390x420",
276
+ "viewport": {
277
+ "width": 390,
278
+ "height": 420
279
+ },
280
+ "cols": 44,
281
+ "rows": 9,
282
+ "panel": {
283
+ "x": 10,
284
+ "y": 149.671875,
285
+ "width": 370,
286
+ "height": 260.328125,
287
+ "right": 380,
288
+ "bottom": 410
289
+ },
290
+ "terminal": {
291
+ "x": 19,
292
+ "y": 158.671875,
293
+ "width": 352,
294
+ "height": 188.53125,
295
+ "right": 371,
296
+ "bottom": 347.203125
297
+ },
298
+ "screen": {
299
+ "x": 19,
300
+ "y": 158.671875,
301
+ "width": 344,
302
+ "height": 171,
303
+ "right": 363,
304
+ "bottom": 329.671875
305
+ },
306
+ "controls": {
307
+ "x": 19,
308
+ "y": 347.203125,
309
+ "width": 352,
310
+ "height": 36.796875,
311
+ "right": 371,
312
+ "bottom": 384
313
+ },
314
+ "overflow": false,
315
+ "background": "rgb(24, 24, 30)"
316
+ },
317
+ {
318
+ "name": "multiline-keyboard-height",
319
+ "viewport": {
320
+ "width": 390,
321
+ "height": 420
322
+ },
323
+ "cols": 44,
324
+ "rows": 9,
325
+ "panel": {
326
+ "x": 10,
327
+ "y": 149.671875,
328
+ "width": 370,
329
+ "height": 260.328125,
330
+ "right": 380,
331
+ "bottom": 410
332
+ },
333
+ "terminal": {
334
+ "x": 19,
335
+ "y": 158.671875,
336
+ "width": 352,
337
+ "height": 188.53125,
338
+ "right": 371,
339
+ "bottom": 347.203125
340
+ },
341
+ "screen": {
342
+ "x": 19,
343
+ "y": 158.671875,
344
+ "width": 344,
345
+ "height": 171,
346
+ "right": 363,
347
+ "bottom": 329.671875
348
+ },
349
+ "controls": {
350
+ "x": 19,
351
+ "y": 347.203125,
352
+ "width": 352,
353
+ "height": 36.796875,
354
+ "right": 371,
355
+ "bottom": 384
356
+ },
357
+ "overflow": false,
358
+ "background": "rgb(24, 24, 30)"
359
+ }
360
+ ],
361
+ "mobileLimitation": "Viewport/keyboard-height simulation on desktop Chromium; no physical mobile device.",
362
+ "passed": true
363
+ }
validation/agent-app-dark-terminal.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "date": "2026-09-12T23:02:11.182Z",
3
+ "url": "http://127.0.0.1:4173/",
4
+ "errors": [],
5
+ "requests": [
6
+ "http://127.0.0.1:4173/",
7
+ "http://127.0.0.1:4173/assets/index-DRX26CN4.js",
8
+ "http://127.0.0.1:4173/assets/index-DCbwJNjv.css",
9
+ "http://127.0.0.1:4173/assets/agent.worker-DqQzo9yL.js",
10
+ "http://127.0.0.1:4173/favicon.svg"
11
+ ],
12
+ "checks": [
13
+ "Original Pi TUI rendered through xterm",
14
+ "Enter submits /help without loading a model",
15
+ "Pi fullscreen transcript search preserves the editor draft",
16
+ "Pi multiline editor: Shift+Enter, cursor, deletion, Ctrl+U/K",
17
+ "Pi prompt history",
18
+ "Pi autocomplete: /he → /help",
19
+ "xterm keyboard → Pi editor → real just-bash → workspace file",
20
+ "Shell escape sequences render as inert text",
21
+ "Large Pi paste markers expand before Send",
22
+ "Unicode multiline clipboard paste",
23
+ "Touch-accessible newline button",
24
+ "390px layout and live terminal resize preserve the draft"
25
+ ],
26
+ "initial": " \n π Pi browser workspace \n \n Ask Pi to work with the files on the right. \n \n /load load MiniCPM \n /help show commands \n !ls run a workspace command \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n Files and chat are saved on this device. \n──────────────────────────────────────────────────────────────────────────────────────────────\n \n──────────────────────────────────────────────────────────────────────────────────────────────\n /workspace · MiniCPM5-2B · 4,096 context ",
27
+ "sizes": {
28
+ "desktop": {
29
+ "cols": 94,
30
+ "rows": 41
31
+ },
32
+ "mobile": {
33
+ "cols": 44,
34
+ "rows": 32
35
+ }
36
+ },
37
+ "mobileLimitation": "Desktop Chromium viewport simulation; physical mobile keyboards and GPUs not tested.",
38
+ "passed": true
39
+ }
validation/agent-first-message.json ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "date": "2026-09-12T23:04:01.335Z",
3
+ "url": "http://127.0.0.1:4173/?source=local",
4
+ "checks": [
5
+ "First Send opens an accessible modal; no model download before confirmation",
6
+ "Focus stays in the modal; entire app remains dark under a light OS preference; five viewport sizes fit",
7
+ "Escape and Back to message restore the draft; real unsupported-GPU error supports retry without sending",
8
+ "Cancelling real model setup waits for cleanup and restores the unsent message",
9
+ "Confirm loads the actual model, sends exactly once, executes a tool and answers with an unseen file value"
10
+ ],
11
+ "errors": [],
12
+ "elapsedMs": 4776,
13
+ "messages": [
14
+ {
15
+ "role": "user",
16
+ "content": [
17
+ {
18
+ "type": "text",
19
+ "text": "Read first-message.txt using the read tool and tell me its exact contents."
20
+ }
21
+ ],
22
+ "timestamp": 1789254247498
23
+ },
24
+ {
25
+ "role": "assistant",
26
+ "api": "minicpm-webgpu",
27
+ "provider": "browser",
28
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
29
+ "timestamp": 1789254247499,
30
+ "content": [
31
+ {
32
+ "type": "toolCall",
33
+ "id": "4f33ef83-7e0d-4708-8f28-1e12ec04b6e9",
34
+ "name": "read",
35
+ "arguments": {
36
+ "path": "/workspace/first-message.txt"
37
+ }
38
+ }
39
+ ],
40
+ "stopReason": "toolUse",
41
+ "usage": {
42
+ "input": 719,
43
+ "output": 20,
44
+ "cacheRead": 0,
45
+ "cacheWrite": 0,
46
+ "totalTokens": 739,
47
+ "cost": {
48
+ "input": 0,
49
+ "output": 0,
50
+ "cacheRead": 0,
51
+ "cacheWrite": 0,
52
+ "total": 0
53
+ }
54
+ }
55
+ },
56
+ {
57
+ "role": "toolResult",
58
+ "toolCallId": "4f33ef83-7e0d-4708-8f28-1e12ec04b6e9",
59
+ "toolName": "read",
60
+ "content": [
61
+ {
62
+ "type": "text",
63
+ "text": "FIRST-8b76aa06"
64
+ }
65
+ ],
66
+ "details": {},
67
+ "isError": false,
68
+ "timestamp": 1789254248557
69
+ },
70
+ {
71
+ "role": "assistant",
72
+ "api": "minicpm-webgpu",
73
+ "provider": "browser",
74
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
75
+ "timestamp": 1789254248557,
76
+ "content": [
77
+ {
78
+ "type": "toolCall",
79
+ "id": "16c2b749-7758-4153-b8a2-6665e83dfec0",
80
+ "name": "read",
81
+ "arguments": {
82
+ "path": "/workspace/first-message.txt"
83
+ }
84
+ }
85
+ ],
86
+ "stopReason": "toolUse",
87
+ "usage": {
88
+ "input": 775,
89
+ "output": 20,
90
+ "cacheRead": 0,
91
+ "cacheWrite": 0,
92
+ "totalTokens": 795,
93
+ "cost": {
94
+ "input": 0,
95
+ "output": 0,
96
+ "cacheRead": 0,
97
+ "cacheWrite": 0,
98
+ "total": 0
99
+ }
100
+ }
101
+ },
102
+ {
103
+ "role": "toolResult",
104
+ "toolCallId": "16c2b749-7758-4153-b8a2-6665e83dfec0",
105
+ "toolName": "read",
106
+ "content": [
107
+ {
108
+ "type": "text",
109
+ "text": "FIRST-8b76aa06"
110
+ }
111
+ ],
112
+ "details": {},
113
+ "isError": false,
114
+ "timestamp": 1789254249576
115
+ },
116
+ {
117
+ "role": "assistant",
118
+ "api": "minicpm-webgpu",
119
+ "provider": "browser",
120
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
121
+ "timestamp": 1789254249576,
122
+ "content": [
123
+ {
124
+ "type": "text",
125
+ "text": "The file `/workspace/first-message.txt` contains the text `FIRST-8b76aa06`."
126
+ }
127
+ ],
128
+ "stopReason": "stop",
129
+ "usage": {
130
+ "input": 831,
131
+ "output": 23,
132
+ "cacheRead": 0,
133
+ "cacheWrite": 0,
134
+ "totalTokens": 854,
135
+ "cost": {
136
+ "input": 0,
137
+ "output": 0,
138
+ "cacheRead": 0,
139
+ "cacheWrite": 0,
140
+ "total": 0
141
+ }
142
+ }
143
+ }
144
+ ],
145
+ "passed": true
146
+ }