Mike0021 commited on
Commit
aba5de3
Β·
verified Β·
1 Parent(s): 779e6b0

Use Pi terminal UI in xterm.js with browser-local model and tools

Browse files
README.md CHANGED
@@ -33,7 +33,7 @@ Requires WebGPU with FP16 (`shader-f16`) in a browser worker, OPFS, and sufficie
33
 
34
  Try: **Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.**
35
 
36
- Pi has `bash`, `read`, `write`, and `edit` tools. just-bash supports shell scripts, pipes, redirects, `awk`, `jq`, `sed`, `grep`, and other text commands. Node.js, Python, npm, native binaries, SQLite, and network commands are unavailable. This embeds Pi's Agent with browser tools; it does not run Pi's Node-based terminal CLI. Files are virtual and remain on your device. Export work you want to keep.
37
 
38
  Source and run instructions: [app/README.md](app/README.md). Architecture and primary-source research: [investigation](research/browser-agent/REPORT.md).
39
 
 
33
 
34
  Try: **Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.**
35
 
36
+ Pi has `bash`, `read`, `write`, and `edit` tools. just-bash supports shell scripts, pipes, redirects, `awk`, `jq`, `sed`, `grep`, and other text commands. Node.js, Python, npm, native binaries, SQLite, and network commands are unavailable. The left column uses xterm.js with Pi's actual terminal renderer, editor, and message components. MiniCPM and the tools run in the browser worker. This reuses Pi's TUI; the full Node CLI and its extensions are not included. Files are virtual and remain on your device. Export work you want to keep.
37
 
38
  Source and run instructions: [app/README.md](app/README.md). Architecture and primary-source research: [investigation](research/browser-agent/REPORT.md).
39
 
SPACE_README.md CHANGED
@@ -33,7 +33,7 @@ Requires WebGPU with FP16 (`shader-f16`) in a browser worker, OPFS, and sufficie
33
 
34
  Try: **Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.**
35
 
36
- Pi has `bash`, `read`, `write`, and `edit` tools. just-bash supports shell scripts, pipes, redirects, `awk`, `jq`, `sed`, `grep`, and other text commands. Node.js, Python, npm, native binaries, SQLite, and network commands are unavailable. This embeds Pi's Agent with browser tools; it does not run Pi's Node-based terminal CLI. Files are virtual and remain on your device. Export work you want to keep.
37
 
38
  Source and run instructions: [app/README.md](app/README.md). Architecture and primary-source research: [investigation](research/browser-agent/REPORT.md).
39
 
 
33
 
34
  Try: **Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.**
35
 
36
+ Pi has `bash`, `read`, `write`, and `edit` tools. just-bash supports shell scripts, pipes, redirects, `awk`, `jq`, `sed`, `grep`, and other text commands. Node.js, Python, npm, native binaries, SQLite, and network commands are unavailable. The left column uses xterm.js with Pi's actual terminal renderer, editor, and message components. MiniCPM and the tools run in the browser worker. This reuses Pi's TUI; the full Node CLI and its extensions are not included. Files are virtual and remain on your device. Export work you want to keep.
37
 
38
  Source and run instructions: [app/README.md](app/README.md). Architecture and primary-source research: [investigation](research/browser-agent/REPORT.md).
39
 
app/README.md CHANGED
@@ -1,6 +1,6 @@
1
  # Pi in the browser
2
 
3
- A static web app running **the real Pi Agent**, **just-bash**, and **our verified MiniCPM5-2B q4f16 conversion** in the browser. A dedicated worker owns the agent loop, WebGPU inference, and virtual shell. No inference API, API key, shell server, or Pi RPC server is involved.
4
 
5
  ```sh
6
  cd app
@@ -15,6 +15,8 @@ For development, use `npm run dev`. To use the already converted weights on this
15
 
16
  ## Features
17
 
 
 
18
  - Streaming conversation with genuine `@earendil-works/pi-agent-core` tool execution and automatic follow-up turns.
19
  - `bash`, `read`, `write`, and exact-match `edit` tools connected to the same just-bash filesystem.
20
  - File editor, text-file import, workspace export, and an interactive command entry.
@@ -27,7 +29,9 @@ For development, use `npm run dev`. To use the already converted weights on this
27
 
28
  ## Runtime boundaries
29
 
30
- This uses Pi's actual agent core with a custom local inference stream and browser tools. Pi's Node-based terminal CLI, host filesystem, OAuth providers, extensions, package installation, and RPC server are not included. The agent runs locally in a browser worker, rather than controlling a remote Pi process.
 
 
31
 
32
  just-bash is a simulated shell, not a Linux VM. Shell scripts, pipes, redirects, `awk`, `jq`, `sed`, `grep`, `find`, and other supported text commands execute in JavaScript. Node.js, Python, npm, native executables, SQLite, and network commands are unavailable. The installed browser bundle also excludes working gzip/zlib commands. Shell cwd and variables reset between commands; filesystem changes persist. Commands have a 10-second deadline, 3,000-command cap, 16 MiB filesystem budget, and 256 KiB output budget. Tool output passed to the model is capped at 6,000 characters.
33
 
@@ -40,11 +44,13 @@ The small 4-bit model can make mistakes. These tests establish working integrati
40
  ```sh
41
  npm test
42
  npm run build
 
 
43
  # Start npm run preview -- --port 4173 and the root static model server first:
44
  npm run test:browser
45
  ```
46
 
47
- The browser suite uses real Chromium, the actual model, and the production bundle. It disables networking while Pi reads a random value from an unseen file, calculates CSV revenue, writes and verifies a report, creates and executes a shell script, edits it, and recovers from a missing-file tool error. It also checks stopping generation, file import/edit/save, manual shell execution, workspace persistence, cached reloads, and a 390 px layout.
48
 
49
  To exercise the public Hugging Face download in a separate browser profile:
50
 
@@ -78,6 +84,8 @@ Browsers that deny storage in third-party embeds get an **Open workspace in a ne
78
  | Component | Version / revision |
79
  |---|---|
80
  | Pi agent core and Pi AI types/event stream | `@earendil-works/*` 0.85.1 |
 
 
81
  | just-bash | 3.4.2, explicit `just-bash/browser` import |
82
  | Transformers.js | 4.2.0 |
83
  | Model | `Mike0021/MiniCPM5-2B-ONNX` at `04a6c49fcba3a65a0351c92644c3a7e9d4343059` |
 
1
  # Pi in the browser
2
 
3
+ A static web app running **Pi’s actual terminal UI in xterm.js**, **the real Pi Agent**, **just-bash**, and **our verified MiniCPM5-2B q4f16 conversion** in the browser. A dedicated worker owns the agent loop, WebGPU inference, and virtual shell. No inference API, API key, shell server, or Pi RPC server is involved.
4
 
5
  ```sh
6
  cd app
 
15
 
16
  ## Features
17
 
18
+ - Pi’s upstream fullscreen renderer, multiline editor, user/assistant message components, Markdown, prompt history, and autocomplete. xterm.js renders their ANSI output.
19
+ - `/load`, `/help`, `/new`, and `!command` browser commands. Enter sends, Shift+Enter inserts a newline, Escape/Ctrl+C stops, Ctrl+O expands tools. Ctrl+P/N recalls prompts; Ctrl+Shift+F searches the transcript. The Send and Newline buttons also support touch input.
20
  - Streaming conversation with genuine `@earendil-works/pi-agent-core` tool execution and automatic follow-up turns.
21
  - `bash`, `read`, `write`, and exact-match `edit` tools connected to the same just-bash filesystem.
22
  - File editor, text-file import, workspace export, and an interactive command entry.
 
29
 
30
  ## Runtime boundaries
31
 
32
+ This uses Pi's actual TUI and agent core with browser host adapters. The npm `pi-tui` package supplies the renderer, editor, input parser, layout, Markdown, search, and keybindings. Unmodified CLI presentation components are in [vendor/pi-coding-agent](vendor/pi-coding-agent/README.md), with provenance checksums and the upstream light palette. [The build adapter](scripts/pi-browser.mjs) replaces terminal host services; [the terminal adapter](src/pi/xterm-terminal.mjs) connects xterm input/output/resize to Pi. Browser command dispatch and just-bash tool cards are app code.
33
+
34
+ The full Node CLI, host filesystem, OAuth providers, extensions, package installation, and RPC server are not included. xterm.js is the terminal emulator; it does not add Node.js or a native shell. Model inference and tool execution remain in the browser worker. Terminal control sequences from model/file output are stripped before rendering, and Markdown images do not trigger downloads.
35
 
36
  just-bash is a simulated shell, not a Linux VM. Shell scripts, pipes, redirects, `awk`, `jq`, `sed`, `grep`, `find`, and other supported text commands execute in JavaScript. Node.js, Python, npm, native executables, SQLite, and network commands are unavailable. The installed browser bundle also excludes working gzip/zlib commands. Shell cwd and variables reset between commands; filesystem changes persist. Commands have a 10-second deadline, 3,000-command cap, 16 MiB filesystem budget, and 256 KiB output budget. Tool output passed to the model is capped at 6,000 characters.
37
 
 
44
  ```sh
45
  npm test
46
  npm run build
47
+ # Start npm run preview, then run the terminal checks (no model needed):
48
+ node tests/terminal.mjs
49
  # Start npm run preview -- --port 4173 and the root static model server first:
50
  npm run test:browser
51
  ```
52
 
53
+ The terminal suite checks real keyboard events, multiline editing, autocomplete, history, Unicode and large clipboard pastes, shell execution, and live resize. The browser suite uses real Chromium, the actual model, and the production bundle; prompts are submitted through xterm and Pi’s editor. It disables networking while Pi reads a random value from an unseen file, calculates CSV revenue, writes and verifies a report, creates and executes a shell script, edits it, and recovers from a missing-file tool error. It also checks stopping generation, file import/edit/save, manual shell execution, workspace persistence, cached reloads, and a 390 px layout.
54
 
55
  To exercise the public Hugging Face download in a separate browser profile:
56
 
 
84
  | Component | Version / revision |
85
  |---|---|
86
  | Pi agent core and Pi AI types/event stream | `@earendil-works/*` 0.85.1 |
87
+ | Pi TUI / CLI presentation components | 0.85.1 |
88
+ | xterm.js / fit addon | 6.0.0 / 0.11.0 |
89
  | just-bash | 3.4.2, explicit `just-bash/browser` import |
90
  | Transformers.js | 4.2.0 |
91
  | Model | `Mike0021/MiniCPM5-2B-ONNX` at `04a6c49fcba3a65a0351c92644c3a7e9d4343059` |
app/index.html CHANGED
@@ -20,17 +20,10 @@
20
  <div id="load-detail" class="muted"></div>
21
  <a id="open-tab" class="button" target="_blank" rel="noopener noreferrer" hidden>Open workspace in a new tab β†—</a>
22
  </div>
23
- <div id="messages" role="log" aria-label="Messages" aria-live="polite">
24
- <div id="welcome"><div class="eyebrow">A small agent. A real workspace.</div><h1>What shall we work on?</h1>
25
- <p>Chat with Pi, work with files, and run shell commands.<br>The model and every tool run here, in your browser.</p>
26
- <div class="suggestions"><button data-prompt="Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.">Analyze the sample data <span>β†—</span></button><button data-prompt="Create a bash script called hello.sh that prints Hello from the browser, run it, and report the output.">Write and run a script <span>β†—</span></button></div>
27
- </div>
28
- </div>
29
  <div id="notice" role="status" hidden></div>
30
- <form id="chat-form"><label class="sr-only" for="prompt">Message Pi</label><textarea id="prompt" rows="3" placeholder="Ask Pi to work on something…"></textarea>
31
- <div class="composer-footer"><span id="activity">Files and chat are saved on this device.</span><button id="stop" type="button" hidden>Stop</button><button id="send" class="primary" disabled>Send ↑</button></div>
32
- </form>
33
- <footer>Pi agent core Β· just-bash Β· Transformers.js <span>4,096 token context</span></footer>
34
  </section>
35
  <aside class="workspace" aria-label="Workspace">
36
  <div class="workspace-heading"><h2>Workspace</h2><div><button id="add-file" class="quiet">+ File</button><label class="button quiet" for="upload">Import</label><input id="upload" type="file" multiple hidden><button id="export" class="quiet">Export</button></div></div>
 
20
  <div id="load-detail" class="muted"></div>
21
  <a id="open-tab" class="button" target="_blank" rel="noopener noreferrer" hidden>Open workspace in a new tab β†—</a>
22
  </div>
23
+ <div id="pi-terminal" aria-label="Pi terminal"></div>
 
 
 
 
 
24
  <div id="notice" role="status" hidden></div>
25
+ <div class="terminal-controls"><span id="activity" role="status">Files and chat are saved on this device.</span><button id="newline" type="button" title="Insert a newline (Shift+Enter)">↡ Newline</button><button id="stop" type="button" hidden>Stop</button><button id="send" class="primary">Send ↑</button></div>
26
+ <footer>Pi TUI Β· xterm.js <span>Model and tools run in your browser</span></footer>
 
 
27
  </section>
28
  <aside class="workspace" aria-label="Workspace">
29
  <div class="workspace-heading"><h2>Workspace</h2><div><button id="add-file" class="quiet">+ File</button><label class="button quiet" for="upload">Import</label><input id="upload" type="file" multiple hidden><button id="export" class="quiet">Export</button></div></div>
app/package-lock.json CHANGED
@@ -10,8 +10,13 @@
10
  "dependencies": {
11
  "@earendil-works/pi-agent-core": "0.85.1",
12
  "@earendil-works/pi-ai": "0.85.1",
 
13
  "@huggingface/transformers": "4.2.0",
14
  "@xmldom/xmldom": "0.9.12",
 
 
 
 
15
  "hash-wasm": "4.12.0",
16
  "just-bash": "3.4.2",
17
  "markdown-it": "15.0.2"
@@ -556,6 +561,19 @@
556
  "node": ">=22.19.0"
557
  }
558
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  "node_modules/@emnapi/runtime": {
560
  "version": "1.11.3",
561
  "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
@@ -2141,6 +2159,21 @@
2141
  "node": ">=14.6"
2142
  }
2143
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2144
  "node_modules/adm-zip": {
2145
  "version": "0.5.18",
2146
  "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz",
@@ -2237,6 +2270,31 @@
2237
  "readable-stream": "^3.4.0"
2238
  }
2239
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2240
  "node_modules/boolean": {
2241
  "version": "3.2.0",
2242
  "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
@@ -2263,9 +2321,9 @@
2263
  }
2264
  },
2265
  "node_modules/buffer": {
2266
- "version": "5.7.1",
2267
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
2268
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
2269
  "funding": [
2270
  {
2271
  "type": "github",
@@ -2281,10 +2339,9 @@
2281
  }
2282
  ],
2283
  "license": "MIT",
2284
- "optional": true,
2285
  "dependencies": {
2286
  "base64-js": "^1.3.1",
2287
- "ieee754": "^1.1.13"
2288
  }
2289
  },
2290
  "node_modules/buffer-equal-constant-time": {
@@ -2527,6 +2584,15 @@
2527
  "url": "https://github.com/sponsors/sindresorhus"
2528
  }
2529
  },
 
 
 
 
 
 
 
 
 
2530
  "node_modules/expand-template": {
2531
  "version": "2.0.3",
2532
  "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -2715,6 +2781,18 @@
2715
  "node": ">=18"
2716
  }
2717
  },
 
 
 
 
 
 
 
 
 
 
 
 
2718
  "node_modules/github-from-package": {
2719
  "version": "0.0.0",
2720
  "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -3309,6 +3387,18 @@
3309
  "markdown-it": "bin/markdown-it.mjs"
3310
  }
3311
  },
 
 
 
 
 
 
 
 
 
 
 
 
3312
  "node_modules/matcher": {
3313
  "version": "3.0.0",
3314
  "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
 
10
  "dependencies": {
11
  "@earendil-works/pi-agent-core": "0.85.1",
12
  "@earendil-works/pi-ai": "0.85.1",
13
+ "@earendil-works/pi-tui": "0.85.1",
14
  "@huggingface/transformers": "4.2.0",
15
  "@xmldom/xmldom": "0.9.12",
16
+ "@xterm/addon-fit": "0.11.0",
17
+ "@xterm/xterm": "6.0.0",
18
+ "buffer": "6.0.3",
19
+ "events": "3.3.0",
20
  "hash-wasm": "4.12.0",
21
  "just-bash": "3.4.2",
22
  "markdown-it": "15.0.2"
 
561
  "node": ">=22.19.0"
562
  }
563
  },
564
+ "node_modules/@earendil-works/pi-tui": {
565
+ "version": "0.85.1",
566
+ "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.85.1.tgz",
567
+ "integrity": "sha512-OIzw9efInmO4WOBnD4TxcTdBjmzvYJpzslkgoUro946nEGoYWg5rwv1p4fDt3/JvMx9QybryUCUwlm7j8Dreig==",
568
+ "license": "MIT",
569
+ "dependencies": {
570
+ "get-east-asian-width": "1.6.0",
571
+ "marked": "18.0.5"
572
+ },
573
+ "engines": {
574
+ "node": ">=22.19.0"
575
+ }
576
+ },
577
  "node_modules/@emnapi/runtime": {
578
  "version": "1.11.3",
579
  "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
 
2159
  "node": ">=14.6"
2160
  }
2161
  },
2162
+ "node_modules/@xterm/addon-fit": {
2163
+ "version": "0.11.0",
2164
+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
2165
+ "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
2166
+ "license": "MIT"
2167
+ },
2168
+ "node_modules/@xterm/xterm": {
2169
+ "version": "6.0.0",
2170
+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
2171
+ "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
2172
+ "license": "MIT",
2173
+ "workspaces": [
2174
+ "addons/*"
2175
+ ]
2176
+ },
2177
  "node_modules/adm-zip": {
2178
  "version": "0.5.18",
2179
  "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz",
 
2270
  "readable-stream": "^3.4.0"
2271
  }
2272
  },
2273
+ "node_modules/bl/node_modules/buffer": {
2274
+ "version": "5.7.1",
2275
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
2276
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
2277
+ "funding": [
2278
+ {
2279
+ "type": "github",
2280
+ "url": "https://github.com/sponsors/feross"
2281
+ },
2282
+ {
2283
+ "type": "patreon",
2284
+ "url": "https://www.patreon.com/feross"
2285
+ },
2286
+ {
2287
+ "type": "consulting",
2288
+ "url": "https://feross.org/support"
2289
+ }
2290
+ ],
2291
+ "license": "MIT",
2292
+ "optional": true,
2293
+ "dependencies": {
2294
+ "base64-js": "^1.3.1",
2295
+ "ieee754": "^1.1.13"
2296
+ }
2297
+ },
2298
  "node_modules/boolean": {
2299
  "version": "3.2.0",
2300
  "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
 
2321
  }
2322
  },
2323
  "node_modules/buffer": {
2324
+ "version": "6.0.3",
2325
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
2326
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
2327
  "funding": [
2328
  {
2329
  "type": "github",
 
2339
  }
2340
  ],
2341
  "license": "MIT",
 
2342
  "dependencies": {
2343
  "base64-js": "^1.3.1",
2344
+ "ieee754": "^1.2.1"
2345
  }
2346
  },
2347
  "node_modules/buffer-equal-constant-time": {
 
2584
  "url": "https://github.com/sponsors/sindresorhus"
2585
  }
2586
  },
2587
+ "node_modules/events": {
2588
+ "version": "3.3.0",
2589
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
2590
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
2591
+ "license": "MIT",
2592
+ "engines": {
2593
+ "node": ">=0.8.x"
2594
+ }
2595
+ },
2596
  "node_modules/expand-template": {
2597
  "version": "2.0.3",
2598
  "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
 
2781
  "node": ">=18"
2782
  }
2783
  },
2784
+ "node_modules/get-east-asian-width": {
2785
+ "version": "1.6.0",
2786
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
2787
+ "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
2788
+ "license": "MIT",
2789
+ "engines": {
2790
+ "node": ">=18"
2791
+ },
2792
+ "funding": {
2793
+ "url": "https://github.com/sponsors/sindresorhus"
2794
+ }
2795
+ },
2796
  "node_modules/github-from-package": {
2797
  "version": "0.0.0",
2798
  "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
 
3387
  "markdown-it": "bin/markdown-it.mjs"
3388
  }
3389
  },
3390
+ "node_modules/marked": {
3391
+ "version": "18.0.5",
3392
+ "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz",
3393
+ "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==",
3394
+ "license": "MIT",
3395
+ "bin": {
3396
+ "marked": "bin/marked.js"
3397
+ },
3398
+ "engines": {
3399
+ "node": ">= 20"
3400
+ }
3401
+ },
3402
  "node_modules/matcher": {
3403
  "version": "3.0.0",
3404
  "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
app/package.json CHANGED
@@ -8,13 +8,19 @@
8
  "build": "vite build",
9
  "preview": "vite preview --host 127.0.0.1",
10
  "test": "node --test tests/*.test.mjs",
11
- "test:browser": "node tests/browser.mjs"
 
12
  },
13
  "dependencies": {
14
  "@earendil-works/pi-agent-core": "0.85.1",
15
  "@earendil-works/pi-ai": "0.85.1",
 
16
  "@huggingface/transformers": "4.2.0",
17
  "@xmldom/xmldom": "0.9.12",
 
 
 
 
18
  "hash-wasm": "4.12.0",
19
  "just-bash": "3.4.2",
20
  "markdown-it": "15.0.2"
 
8
  "build": "vite build",
9
  "preview": "vite preview --host 127.0.0.1",
10
  "test": "node --test tests/*.test.mjs",
11
+ "test:browser": "node tests/browser.mjs",
12
+ "test:terminal": "node tests/terminal.mjs"
13
  },
14
  "dependencies": {
15
  "@earendil-works/pi-agent-core": "0.85.1",
16
  "@earendil-works/pi-ai": "0.85.1",
17
+ "@earendil-works/pi-tui": "0.85.1",
18
  "@huggingface/transformers": "4.2.0",
19
  "@xmldom/xmldom": "0.9.12",
20
+ "@xterm/addon-fit": "0.11.0",
21
+ "@xterm/xterm": "6.0.0",
22
+ "buffer": "6.0.3",
23
+ "events": "3.3.0",
24
  "hash-wasm": "4.12.0",
25
  "just-bash": "3.4.2",
26
  "markdown-it": "15.0.2"
app/scripts/licenses.mjs CHANGED
@@ -23,4 +23,5 @@ export async function collectLicenses() {
23
  await scan('node_modules');
24
  await mkdir('dist/licenses', { recursive: true });
25
  await writeFile('dist/licenses/dependencies.txt', notices.sort().join('\n----------------------------------------\n\n'));
 
26
  }
 
23
  await scan('node_modules');
24
  await mkdir('dist/licenses', { recursive: true });
25
  await writeFile('dist/licenses/dependencies.txt', notices.sort().join('\n----------------------------------------\n\n'));
26
+ await writeFile('dist/licenses/pi-components.json', await readFile('vendor/pi-coding-agent/provenance.json'));
27
  }
app/scripts/pi-browser.mjs ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { resolve } from 'node:path';
2
+ import { readFileSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+
5
+ // Adapt host services at build time; never edit installed or vendored Pi code.
6
+ // Exact package pin + component checksums make this boundary reviewable.
7
+ export function piBrowser() {
8
+ const sourceRoot = resolve('vendor/pi-coding-agent');
9
+ const provenance = JSON.parse(readFileSync(resolve(sourceRoot, 'provenance.json'), 'utf8'));
10
+ for (const [file, expected] of Object.entries(provenance.files)) {
11
+ const actual = createHash('sha256').update(readFileSync(resolve(sourceRoot, file))).digest('hex');
12
+ if (actual !== expected) throw Error(`Vendored Pi component differs from its upstream checksum: ${file}`);
13
+ }
14
+ const host = resolve('src/pi/browser-host.mjs');
15
+ return {
16
+ name: 'pi-browser-host', enforce: 'pre',
17
+ resolveId(source, importer = '') {
18
+ if (source === '@earendil-works/pi-tui') return resolve('src/pi/entry.mjs');
19
+ if (importer.includes('/@earendil-works/pi-tui/')) {
20
+ if (source.endsWith('/terminal-image.js')) return resolve('src/pi/capabilities.mjs');
21
+ if (source === 'node:perf_hooks') return host;
22
+ }
23
+ if (importer.includes('/vendor/pi-coding-agent/') && source === '../theme/theme.js') return resolve('src/pi/theme.mjs');
24
+ },
25
+ transform(code, id) {
26
+ if (!id.includes('/@earendil-works/pi-tui/') || !id.split('?')[0].endsWith('.js')) return;
27
+ if (!/\b(?:process|Buffer)\./.test(code)) return;
28
+ return { code: `import { browserProcess as process, Buffer } from ${JSON.stringify(host)};\n${code}`, map: null };
29
+ },
30
+ };
31
+ }
app/src/main.mjs CHANGED
@@ -1,6 +1,5 @@
1
  import './style.css';
2
- import { textContent } from './protocol.mjs';
3
- import { renderMarkdown } from './markdown.mjs';
4
 
5
  const $ = selector => document.querySelector(selector);
6
  const worker = new Worker(new URL('./agent.worker.mjs', import.meta.url), { type: 'module' });
@@ -8,7 +7,11 @@ const pending = new Map();
8
  let nextId = 1, busy = false, loaded = false, selected, dirty = false, files = {}, messages = [], readyResolve;
9
  const ready = new Promise(resolve => { readyResolve = resolve; });
10
  const diagnostics = { events: [], generations: [], device: null };
11
- const welcome = $('#welcome').cloneNode(true);
 
 
 
 
12
  function rpc(action, args) {
13
  return new Promise((resolve, reject) => {
14
  const id = nextId++; pending.set(id, { resolve, reject }); worker.postMessage({ id, action, args });
@@ -23,13 +26,16 @@ function storageHelp(error) {
23
  }
24
  function controls() {
25
  $('#load').disabled = busy || loaded;
26
- $('#send').disabled = busy || !loaded;
 
 
27
  $('#stop').hidden = !busy;
28
  for (const id of ['#new-chat', '#shell-run', '#add-file', '#upload']) $(id).disabled = busy;
29
  $('#save').disabled = busy || !selected || !dirty;
30
  $('#editor').disabled = busy || !selected;
31
  }
32
  function renderFiles() {
 
33
  $('#files').replaceChildren();
34
  for (const file of Object.keys(files).sort()) {
35
  const button = document.createElement('button'); button.textContent = file.replace(/^\/workspace\//, '');
@@ -43,39 +49,7 @@ function selectFile(file) {
43
  selected = file; $('#filename').textContent = file; $('#editor').value = files[file] ?? '';
44
  renderFiles(); controls();
45
  }
46
- function renderMessages() {
47
- const log = $('#messages');
48
- const nearBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 90;
49
- const opened = new Set([...log.querySelectorAll('details[open]')].map(d => d.dataset.id));
50
- log.replaceChildren();
51
- if (!messages.length) log.append(welcome.cloneNode(true));
52
- const results = new Map(messages.filter(m => m.role === 'toolResult').map(m => [m.toolCallId, m]));
53
- for (const m of messages) {
54
- if (m.role === 'toolResult') continue;
55
- const item = document.createElement('article'); item.className = 'message ' + m.role;
56
- const label = document.createElement('div'); label.className = 'message-label'; label.textContent = m.role === 'user' ? 'You' : 'Pi'; item.append(label);
57
- const content = typeof m.content === 'string' ? [{ type: 'text', text: m.content }] : m.content;
58
- for (const block of content) {
59
- if (block.type === 'text') {
60
- const body = document.createElement('div'); body.className = 'message-body';
61
- if (m.role === 'assistant') { body.classList.add('markdown'); body.innerHTML = renderMarkdown(block.text); }
62
- else body.textContent = block.text;
63
- item.append(body);
64
- } else if (block.type === 'toolCall') {
65
- const result = results.get(block.id), details = document.createElement('details');
66
- details.className = 'tool'; details.dataset.id = block.id; details.open = opened.has(block.id) || !result;
67
- const summary = document.createElement('summary');
68
- summary.textContent = `${result ? result.isError ? 'Γ—' : 'βœ“' : 'Β·'} ${block.name} ${block.arguments.path ?? (block.arguments.command ?? '').slice(0, 65)}`;
69
- const pre = document.createElement('pre'); pre.textContent = JSON.stringify(block.arguments, null, 2) + '\n\n' + (result ? textContent(result.content) : 'Running…');
70
- details.append(summary, pre); item.append(details);
71
- }
72
- }
73
- if (m.errorMessage) { const error = document.createElement('div'); error.className = 'error'; error.textContent = m.errorMessage; item.append(error); }
74
- if (m.stopReason === 'length') { const note = document.createElement('div'); note.className = 'message-stats'; note.textContent = 'Output limit reached. Ask Pi to continue.'; item.append(note); }
75
- log.append(item);
76
- }
77
- if (nearBottom) log.scrollTop = log.scrollHeight;
78
- }
79
  worker.onmessage = ({ data }) => {
80
  if (data.type === 'reply') {
81
  const request = pending.get(data.id); pending.delete(data.id);
@@ -110,6 +84,7 @@ worker.onmessage = ({ data }) => {
110
  }
111
  if (event.type === 'tool_execution_start') $('#activity').textContent = `Running ${event.toolName}…`;
112
  if (event.type === 'turn_start') $('#activity').textContent = 'Pi is thinking…';
 
113
  } else if (data.type === 'generation') { diagnostics.generations.push(data); if (diagnostics.generations.length > 100) diagnostics.generations.shift(); }
114
  else if (data.type === 'context_trim') notice('Older turns were left out of the model context. Your full chat remains saved.');
115
  else if (data.type === 'notice') notice(data.text);
@@ -135,11 +110,14 @@ async function prompt(text) {
135
  notice(''); const result = await rpc('prompt', { text }); messages = result.messages; renderMessages(); return result;
136
  }
137
  $('#load').onclick = guarded(load);
138
- $('#chat-form').onsubmit = guarded(async () => { const text = $('#prompt').value; $('#prompt').value = ''; await prompt(text); });
139
- $('#prompt').onkeydown = event => { if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) $('#chat-form').requestSubmit(); };
140
- $('#messages').onclick = event => { const button = event.target.closest('[data-prompt]'); if (button) { $('#prompt').value = button.dataset.prompt; $('#prompt').focus(); } };
141
  $('#stop').onclick = guarded(() => rpc('stop'));
142
- $('#new-chat').onclick = guarded(async () => { const state = await rpc('new_chat'); messages = state.messages; renderMessages(); notice(''); });
 
 
 
 
143
  $('#editor').oninput = () => { dirty = true; controls(); };
144
  $('#save').onclick = guarded(async () => { await rpc('write', { path: selected, content: $('#editor').value }); dirty = false; renderFiles(); controls(); notice(''); });
145
  $('#add-file').onclick = guarded(async () => {
@@ -175,5 +153,5 @@ $('#shell-form').onsubmit = guarded(async () => {
175
  $('#terminal-output').scrollTop = $('#terminal-output').scrollHeight;
176
  });
177
  // Browser verification exercises the same worker and real Pi / WebGPU paths as the UI.
178
- window.browserPi = { ready, load, prompt, rpc, diagnostics, get loaded() { return loaded; }, get busy() { return busy; } };
179
  controls();
 
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' });
 
7
  let nextId = 1, busy = false, loaded = false, selected, dirty = false, files = {}, messages = [], readyResolve;
8
  const ready = new Promise(resolve => { readyResolve = resolve; });
9
  const diagnostics = { events: [], generations: [], device: null };
10
+ const terminalUI = createTerminalUI($('#pi-terminal'), {
11
+ load: () => ready.then(load), prompt: text => ready.then(() => prompt(text)),
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 });
 
26
  }
27
  function controls() {
28
  $('#load').disabled = busy || loaded;
29
+ $('#send').disabled = busy;
30
+ $('#newline').disabled = busy;
31
+ terminalUI.setState({ busy, loaded, activity: $('#activity').textContent });
32
  $('#stop').hidden = !busy;
33
  for (const id of ['#new-chat', '#shell-run', '#add-file', '#upload']) $(id).disabled = busy;
34
  $('#save').disabled = busy || !selected || !dirty;
35
  $('#editor').disabled = busy || !selected;
36
  }
37
  function renderFiles() {
38
+ terminalUI.setFiles(files);
39
  $('#files').replaceChildren();
40
  for (const file of Object.keys(files).sort()) {
41
  const button = document.createElement('button'); button.textContent = file.replace(/^\/workspace\//, '');
 
49
  selected = file; $('#filename').textContent = file; $('#editor').value = files[file] ?? '';
50
  renderFiles(); controls();
51
  }
52
+ function renderMessages() { terminalUI.setMessages(messages); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  worker.onmessage = ({ data }) => {
54
  if (data.type === 'reply') {
55
  const request = pending.get(data.id); pending.delete(data.id);
 
84
  }
85
  if (event.type === 'tool_execution_start') $('#activity').textContent = `Running ${event.toolName}…`;
86
  if (event.type === 'turn_start') $('#activity').textContent = 'Pi is thinking…';
87
+ terminalUI.setState({ activity: $('#activity').textContent });
88
  } else if (data.type === 'generation') { diagnostics.generations.push(data); if (diagnostics.generations.length > 100) diagnostics.generations.shift(); }
89
  else if (data.type === 'context_trim') notice('Older turns were left out of the model context. Your full chat remains saved.');
90
  else if (data.type === 'notice') notice(data.text);
 
110
  notice(''); const result = await rpc('prompt', { text }); messages = result.messages; renderMessages(); return result;
111
  }
112
  $('#load').onclick = guarded(load);
113
+ $('#send').onclick = () => { terminalUI.submit(); terminalUI.focus(); };
114
+ $('#newline').onclick = () => terminalUI.newline();
 
115
  $('#stop').onclick = guarded(() => rpc('stop'));
116
+ async function newChat() {
117
+ const state = await rpc('new_chat'); messages = state.messages;
118
+ terminalUI.setMessages(messages, { reset: true }); notice('');
119
+ }
120
+ $('#new-chat').onclick = guarded(newChat);
121
  $('#editor').oninput = () => { dirty = true; controls(); };
122
  $('#save').onclick = guarded(async () => { await rpc('write', { path: selected, content: $('#editor').value }); dirty = false; renderFiles(); controls(); notice(''); });
123
  $('#add-file').onclick = guarded(async () => {
 
153
  $('#terminal-output').scrollTop = $('#terminal-output').scrollHeight;
154
  });
155
  // Browser verification exercises the same worker and real Pi / WebGPU paths as the UI.
156
+ window.browserPi = { ready, load, prompt, rpc, diagnostics, terminal: terminalUI, get loaded() { return loaded; }, get busy() { return busy; } };
157
  controls();
app/src/pi/browser-host.mjs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ // Only Pi's terminal modules import this host adapter. No global Node shim.
2
+ export { Buffer } from 'buffer';
3
+ export const performance = globalThis.performance;
4
+ export const browserProcess = {
5
+ env: { TERM: 'xterm-256color', TERM_PROGRAM: 'xtermjs' },
6
+ platform: 'browser',
7
+ nextTick: callback => queueMicrotask(callback),
8
+ };
app/src/pi/capabilities.mjs ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // xterm.js renders text/ANSI; this app does not install Kitty/iTerm image addons.
2
+ // Pi's native image module also probes tmux via child_process. Keep that host
3
+ // dependency out of the browser and advertise only capabilities we implement.
4
+ let capabilities = { images: null, trueColor: true, hyperlinks: true };
5
+ let dimensions = { widthPx: 8, heightPx: 16 };
6
+ export const getCapabilities = () => capabilities;
7
+ export const setCapabilities = value => { capabilities = { ...value, images: null }; };
8
+ export const getCellDimensions = () => dimensions;
9
+ export const setCellDimensions = value => { dimensions = value; };
10
+ export const isImageLine = () => false;
11
+ export const getKittyImagePlacement = () => undefined;
12
+ export const getKittyImageMetadata = () => undefined;
13
+ export const cropKittyImageLine = line => line;
14
+ export const deleteKittyImage = () => '';
15
+ export const deleteAllKittyImages = () => '';
16
+ export const deleteAllKittyPlacements = () => '';
17
+ export function hyperlink(text, url) {
18
+ // Generated text may contain arbitrary URLs. Only explicit user clicks open
19
+ // HTTP(S) links, and no terminal control bytes may enter their OSC envelope.
20
+ try {
21
+ const parsed = new URL(url);
22
+ if (!['https:', 'http:'].includes(parsed.protocol) || /[\x00-\x20\x7f-\x9f]/.test(url)) return text;
23
+ return `\x1b]8;;${parsed.href}\x07${text}\x1b]8;;\x07`;
24
+ } catch { return text; }
25
+ }
app/src/pi/entry.mjs ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Import the real upstream components directly, excluding ProcessTerminal and
2
+ // native filesystem autocomplete from pi-tui's Node-oriented barrel export.
3
+ export { TuiAltScreen } from '@earendil-works/pi-tui/dist/tui-alt-screen.js';
4
+ export { Container } from '@earendil-works/pi-tui/dist/tui.js';
5
+ export { Editor } from '@earendil-works/pi-tui/dist/components/editor.js';
6
+ export { Text } from '@earendil-works/pi-tui/dist/components/text.js';
7
+ export { TruncatedText } from '@earendil-works/pi-tui/dist/components/truncated-text.js';
8
+ export { Markdown } from '@earendil-works/pi-tui/dist/components/markdown.js';
9
+ export { Box } from '@earendil-works/pi-tui/dist/components/box.js';
10
+ export { Spacer } from '@earendil-works/pi-tui/dist/components/spacer.js';
11
+ export { MouseRegion } from '@earendil-works/pi-tui/dist/components/mouse-region.js';
12
+ export { ScrollView } from '@earendil-works/pi-tui/dist/components/scroll-view.js';
13
+ export { VStack } from '@earendil-works/pi-tui/dist/components/v-stack.js';
14
+ export { KeybindingsManager, TUI_KEYBINDINGS, setKeybindings } from '@earendil-works/pi-tui/dist/keybindings.js';
15
+ export { matchesKey } from '@earendil-works/pi-tui/dist/keys.js';
16
+ export { StdinBuffer } from '@earendil-works/pi-tui/dist/stdin-buffer.js';
17
+ export { visibleWidth, stripTerminalSequences } from '@earendil-works/pi-tui/dist/utils.js';
app/src/pi/terminal-ui.mjs ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Terminal } from '@xterm/xterm';
2
+ import { FitAddon } from '@xterm/addon-fit';
3
+ import '@xterm/xterm/css/xterm.css';
4
+ import { TuiAltScreen, Container, Text, TruncatedText, Box, Spacer, ScrollView, VStack,
5
+ KeybindingsManager, TUI_KEYBINDINGS, setKeybindings, stripTerminalSequences } from './entry.mjs';
6
+ import { CustomEditor } from '../../vendor/pi-coding-agent/components/custom-editor.js';
7
+ import { UserMessageComponent } from '../../vendor/pi-coding-agent/components/user-message.js';
8
+ import { AssistantMessageComponent } from '../../vendor/pi-coding-agent/components/assistant-message.js';
9
+ import { getEditorTheme, theme } from './theme.mjs';
10
+ import { XtermTerminal } from './xterm-terminal.mjs';
11
+ import { textContent } from '../protocol.mjs';
12
+
13
+ // Model output and workspace files are text, never terminal instructions.
14
+ export const terminalText = value => stripTerminalSequences(String(value ?? ''))
15
+ .replace(/\r\n?/g, '\n').replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, '');
16
+
17
+ const commands = [
18
+ { value: '/help', label: '/help', description: 'Browser terminal commands' },
19
+ { value: '/load', label: '/load', description: 'Load MiniCPM on this device' },
20
+ { value: '/new', label: '/new', description: 'Start a new conversation; keep files' },
21
+ { value: '/clear', label: '/clear', description: 'Same as /new' },
22
+ ];
23
+
24
+ export function createTerminalUI(element, actions) {
25
+ const xterm = new Terminal({
26
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', fontSize: 13, lineHeight: 1.3,
27
+ theme: { background: '#ffffff', foreground: '#1f2328', cursor: '#5a8080', selectionBackground: '#d0d0e0' },
28
+ cursorBlink: false, scrollback: 2000, screenReaderMode: true, allowProposedApi: false,
29
+ });
30
+ const mount = document.createElement('div'); mount.className = 'pi-terminal-mount'; element.append(mount);
31
+ const fitAddon = new FitAddon(); xterm.loadAddon(fitAddon); xterm.open(mount);
32
+ const terminal = new XtermTerminal(xterm);
33
+ const ui = new TuiAltScreen(terminal, true, undefined, {
34
+ copyOnSelect: false,
35
+ copySelection: async text => { try { await navigator.clipboard.writeText(text); return true; } catch { return false; } },
36
+ openUrl: url => { if (/^https?:\/\//i.test(url)) window.open(url, '_blank', 'noopener,noreferrer'); },
37
+ scrollToEndIndicator: () => theme.fg('accent', ' ↓ Latest '),
38
+ });
39
+ const keybindings = new KeybindingsManager({ ...TUI_KEYBINDINGS,
40
+ 'app.interrupt': { defaultKeys: ['escape', 'ctrl+c'] }, 'app.exit': { defaultKeys: 'ctrl+d' },
41
+ 'app.tools.expand': { defaultKeys: 'ctrl+o' },
42
+ 'tui.editor.historyPrevious': { defaultKeys: 'ctrl+p' }, 'tui.editor.historyNext': { defaultKeys: 'ctrl+n' },
43
+ });
44
+ setKeybindings(keybindings);
45
+ const editor = new CustomEditor(ui, getEditorTheme(), keybindings, { paddingX: 1 });
46
+ const transcript = new Container();
47
+ const scroll = new ScrollView(transcript, { follow: 'end', primary: true, scrollbar: 'auto',
48
+ scrollbarTrackStyle: text => theme.fg('borderMuted', text), scrollbarThumbStyle: text => theme.fg('muted', text) });
49
+ const status = new TruncatedText('', 1, 0);
50
+ const footer = new TruncatedText(theme.fg('dim', '/workspace Β· MiniCPM5-2B Β· 4,096 context'), 1, 0);
51
+ ui.setLayoutRoot(new VStack([
52
+ { component: scroll, basis: 0, grow: 1, minSize: 2 },
53
+ { component: new VStack([status, editor, footer]), basis: 'auto', shrink: 0 },
54
+ ]));
55
+ ui.setFocus(editor);
56
+ let state = { loaded: false, busy: false, activity: '' }, files = [], messages = [], expanded = false;
57
+ let inputPending = false;
58
+ const components = new Map();
59
+ const localEntries = [];
60
+ const history = new Set();
61
+ const welcome = new Container();
62
+ welcome.addChild(new Text(theme.fg('accent', theme.bold('Ο€ Pi')) + theme.fg('muted', ' browser workspace'), 1, 1));
63
+ welcome.addChild(new Text('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', 1, 0));
64
+ welcome.addChild(new Spacer(1));
65
+
66
+ function addLocal(text, error = false) {
67
+ localEntries.push({ at: messages.length, component: new Text(theme.fg(error ? 'error' : 'muted', terminalText(text)), 1, 1) });
68
+ renderMessages(); ui.scrollToBottom();
69
+ }
70
+ function remember(text) {
71
+ if (!text || history.has(text)) return;
72
+ history.add(text); editor.addToHistory(text);
73
+ }
74
+ function toolComponent(call, result) {
75
+ const container = new Container();
76
+ container.addChild(new Spacer(1));
77
+ const box = new Box(1, 0, text => theme.bg(result ? result.isError ? 'toolErrorBg' : 'toolSuccessBg' : 'toolPendingBg', text));
78
+ const title = `${result ? result.isError ? 'Γ—' : 'βœ“' : 'Β·'} ${call.name} ${call.arguments?.path ?? call.arguments?.command ?? ''}`;
79
+ box.addChild(new Text(theme.bold(terminalText(title)), 0, 0));
80
+ const args = terminalText(JSON.stringify(call.arguments, null, 2));
81
+ const output = terminalText(result ? textContent(result.content) : 'Running…');
82
+ const lines = output.split('\n');
83
+ box.addChild(new Text(theme.fg('toolOutput', expanded ? args + '\n\n' + output : lines.slice(0, 8).join('\n')), 0, 0));
84
+ if (!expanded && lines.length > 8) box.addChild(new Text(theme.fg('dim', `… ${lines.length - 8} more lines Β· Ctrl+O to expand`), 0, 0));
85
+ container.addChild(box); return container;
86
+ }
87
+ function renderMessages() {
88
+ transcript.clear(); transcript.addChild(welcome);
89
+ const results = new Map(messages.filter(m => m.role === 'toolResult').map(m => [m.toolCallId, m]));
90
+ const liveKeys = new Set();
91
+ for (let i = 0; i <= messages.length; i++) {
92
+ for (const local of localEntries) if (local.at === i) transcript.addChild(local.component);
93
+ const message = messages[i];
94
+ if (!message || message.role === 'toolResult') continue;
95
+ const key = message.role + ':' + message.timestamp + ':' + i; liveKeys.add(key);
96
+ let cached = components.get(key);
97
+ if (!cached) {
98
+ cached = { component: message.role === 'user' ? new UserMessageComponent(terminalText(textContent(message.content))) : new AssistantMessageComponent() };
99
+ components.set(key, cached);
100
+ }
101
+ if (message.role === 'user') remember(textContent(message.content));
102
+ if (message.role === 'assistant' && cached.message !== message) {
103
+ const clean = { ...message, content: message.content.map(block => block.type === 'text' ? { ...block, text: terminalText(block.text) } :
104
+ block.type === 'thinking' ? { ...block, thinking: terminalText(block.thinking) } : block),
105
+ errorMessage: message.errorMessage ? terminalText(message.errorMessage) : undefined };
106
+ cached.component.updateContent(clean, !message.stopReason);
107
+ }
108
+ cached.message = message; transcript.addChild(cached.component);
109
+ for (const call of Array.isArray(message.content) ? message.content : []) {
110
+ if (call.type !== 'toolCall') continue;
111
+ const result = results.get(call.id), toolKey = 'tool:' + call.id; liveKeys.add(toolKey);
112
+ const signature = JSON.stringify([call, result, expanded]);
113
+ let tool = components.get(toolKey);
114
+ if (tool?.signature !== signature) { tool = { signature, component: toolComponent(call, result) }; components.set(toolKey, tool); }
115
+ transcript.addChild(tool.component);
116
+ }
117
+ }
118
+ for (const key of components.keys()) if (!liveKeys.has(key)) components.delete(key);
119
+ ui.requestRender();
120
+ }
121
+ function refreshStatus() {
122
+ status.text = theme.fg(state.busy ? 'accent' : 'muted', terminalText(state.activity || (state.loaded ? 'Ready' : 'Load the model to chat Β· /help')));
123
+ editor.disableSubmit = state.busy || inputPending;
124
+ editor.borderColor = text => theme.fg(state.busy ? 'accent' : 'borderMuted', text);
125
+ actions.onInput?.(editor.getText()); ui.requestRender();
126
+ }
127
+ async function submit(text) {
128
+ if (!text.trim() || state.busy || inputPending) return;
129
+ inputPending = true; refreshStatus();
130
+ remember(text); ui.scrollToBottom();
131
+ try {
132
+ const trimmed = text.trim();
133
+ if (trimmed === '/help') {
134
+ addLocal('/load Load MiniCPM on this device\n/new Clear the conversation; keep workspace files\n!cmd Run cmd with just-bash\n\nEnter: send Β· Shift+Enter: newline\n↑/↓: edit/history Β· Ctrl+P/N: history\nEsc or Ctrl+C: stop Β· Ctrl+O: expand tools\nCtrl+Shift+F: search transcript\n\nPi’s original terminal UI and agent core run here.\nNode CLI extensions, npm, Python and native binaries are unavailable.');
135
+ } else if (trimmed === '/load') { await actions.load(); }
136
+ else if (trimmed === '/new' || trimmed === '/clear') { await actions.newChat(); localEntries.length = 0; renderMessages(); }
137
+ else if (trimmed.startsWith('/')) { throw Error('Unknown browser command. Type /help for available commands.'); }
138
+ else if (trimmed.startsWith('!')) {
139
+ const command = trimmed.slice(1).trim(); if (!command) throw Error('Enter a shell command after !.');
140
+ const result = await actions.shell(command);
141
+ addLocal(`$ ${command}\n${result.stdout}${result.stderr}\n[exit ${result.exitCode}]`, result.exitCode !== 0);
142
+ } else { await actions.prompt(text); }
143
+ } catch (error) {
144
+ addLocal(error.message, true);
145
+ // Preserve a failed submission without overwriting a draft typed meanwhile.
146
+ if (!editor.getText()) editor.setText(text);
147
+ } finally { inputPending = false; refreshStatus(); }
148
+ }
149
+ editor.onSubmit = text => { void submit(text); };
150
+ editor.onChange = () => { actions.onInput?.(editor.getText()); ui.requestRender(); };
151
+ editor.onEscape = () => { if (state.busy) void actions.stop().catch(error => addLocal(error.message, true)); };
152
+ editor.onCtrlD = () => addLocal('This terminal lives in your browser. Close the tab to leave.');
153
+ editor.onAction('app.tools.expand', () => { expanded = !expanded; renderMessages(); });
154
+ editor.setAutocompleteProvider({
155
+ getSuggestions(lines, line, col) {
156
+ const before = lines[line].slice(0, col), prefix = before.match(/\S*$/)?.[0] ?? '';
157
+ if (!prefix) return null;
158
+ const candidates = before.startsWith('/') ? commands : files.map(path => ({ value: path, label: path, description: 'workspace file' }));
159
+ const items = candidates.filter(item => item.value.startsWith(prefix) && item.value !== prefix);
160
+ return items.length ? { items, prefix } : null;
161
+ },
162
+ applyCompletion(lines, line, col, item, prefix) {
163
+ const updated = [...lines]; updated[line] = lines[line].slice(0, col - prefix.length) + item.value + ' ' + lines[line].slice(col);
164
+ return { lines: updated, cursorLine: line, cursorCol: col - prefix.length + item.value.length + 1 };
165
+ },
166
+ });
167
+ xterm.attachCustomKeyEventHandler(event => {
168
+ if (event.type !== 'keydown' || event.isComposing) return true;
169
+ // Let the browser deliver its native paste event (no clipboard permission
170
+ // request). xterm then applies bracketed paste before Pi sees the text.
171
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v') return false;
172
+ // Legacy xterm input folds Ctrl+Shift+F into Ctrl+F. Preserve the modifier
173
+ // in CSI-u form for Pi's existing fullscreen transcript-search binding.
174
+ if (event.ctrlKey && event.shiftKey && event.code === 'KeyF') {
175
+ event.preventDefault(); terminal.inputData('\x1b[102;6u'); return false;
176
+ }
177
+ if (event.key === 'Enter' && event.shiftKey) { event.preventDefault(); terminal.inputData('\x1b\r'); return false; }
178
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c' && (xterm.hasSelection() || ui.hasActiveSelection())) {
179
+ if (ui.hasActiveSelection()) { event.preventDefault(); void ui.copyActiveSelectionToClipboard(); }
180
+ return false;
181
+ }
182
+ return true;
183
+ });
184
+ // xterm's alternate buffer has no native scrollback. Route touch gestures to
185
+ // Pi's ScrollView while leaving taps/IME handling with xterm itself.
186
+ let touchY;
187
+ element.addEventListener('touchstart', event => { if (event.touches.length === 1) touchY = event.touches[0].clientY; }, { passive: true });
188
+ element.addEventListener('touchmove', event => {
189
+ if (touchY === undefined || event.touches.length !== 1) return;
190
+ const y = event.touches[0].clientY, rows = Math.trunc((touchY - y) / 18);
191
+ if (rows) { ui.scrollBy(rows); touchY = y; event.preventDefault(); }
192
+ }, { passive: false });
193
+ element.addEventListener('touchend', () => { touchY = undefined; }, { passive: true });
194
+ function fit() {
195
+ if (element.clientWidth > 0 && element.clientHeight > 0) { fitAddon.fit(); ui.requestRender(); }
196
+ }
197
+ const observer = new ResizeObserver(fit); observer.observe(element);
198
+ fit(); ui.start(); renderMessages(); refreshStatus();
199
+ document.fonts.ready.then(fit);
200
+ xterm.textarea.setAttribute('aria-label', 'Message Pi');
201
+ xterm.textarea.setAttribute('autocapitalize', 'off');
202
+ xterm.textarea.setAttribute('spellcheck', 'false');
203
+ return {
204
+ setMessages(value, { reset = false } = {}) { messages = value; if (reset) { localEntries.length = 0; components.clear(); } renderMessages(); },
205
+ setFiles(value) { files = Object.keys(value).map(path => path.replace(/^\/workspace\//, '')); },
206
+ setState(value) { state = { ...state, ...value }; refreshStatus(); },
207
+ submit() { const text = editor.getExpandedText(); if (!editor.disableSubmit && text.trim()) { editor.setText(''); return submit(text); } },
208
+ focus() { xterm.focus(); },
209
+ newline() { terminal.inputData('\x1b\r'); xterm.focus(); },
210
+ get text() { return editor.getExpandedText(); },
211
+ get cols() { return xterm.cols; }, get rows() { return xterm.rows; },
212
+ get viewportTop() { return ui.viewportTop; },
213
+ readScreen() { return Array.from({ length: xterm.buffer.active.length }, (_, i) => xterm.buffer.active.getLine(i)?.translateToString(true) ?? '').join('\n'); },
214
+ dispose() { observer.disconnect(); ui.stop({ preserveScreen: true }); xterm.dispose(); },
215
+ };
216
+ }
app/src/pi/theme.mjs ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import palette from '../../vendor/pi-coding-agent/theme/light.json';
2
+
3
+ // Upstream Pi light palette; browser styling replaces the theme file watcher
4
+ // and Node's syntax highlighter. Code blocks use Pi's plain-code fallback.
5
+ function color(name, text, background = false) {
6
+ const value = palette.vars[palette.colors[name]] ?? palette.colors[name] ?? palette.vars.text;
7
+ const rgb = value.slice(1).match(/../g).map(part => parseInt(part, 16)).join(';');
8
+ const open = `\x1b[${background ? 48 : 38};2;${rgb}m`, close = `\x1b[${background ? 49 : 39}m`;
9
+ return open + String(text).replaceAll(close, open) + close;
10
+ }
11
+ export const theme = {
12
+ fg: (name, text) => color(name, text), bg: (name, text) => color(name, text, true),
13
+ bold: text => `\x1b[1m${text}\x1b[22m`, italic: text => `\x1b[3m${text}\x1b[23m`,
14
+ underline: text => `\x1b[4m${text}\x1b[24m`,
15
+ };
16
+ export function getMarkdownTheme() {
17
+ return {
18
+ ...Object.fromEntries(Object.entries({ heading: 'mdHeading', link: 'mdLink', linkUrl: 'mdLinkUrl', code: 'mdCode',
19
+ codeBlock: 'mdCodeBlock', codeBlockBorder: 'mdCodeBlockBorder', quote: 'mdQuote', quoteBorder: 'mdQuoteBorder',
20
+ hr: 'mdHr', listBullet: 'mdListBullet' }).map(([key, value]) => [key, text => theme.fg(value, text)])),
21
+ bold: theme.bold, italic: theme.italic, underline: theme.underline,
22
+ strikethrough: text => `\x1b[9m${text}\x1b[29m`,
23
+ highlightCode: code => code.split('\n').map(line => theme.fg('mdCodeBlock', line)),
24
+ };
25
+ }
26
+ export function getEditorTheme() {
27
+ return { borderColor: text => theme.fg('borderMuted', text), selectList: {
28
+ selectedPrefix: text => theme.fg('accent', text), selectedText: text => theme.fg('accent', text),
29
+ description: text => theme.fg('muted', text), scrollInfo: text => theme.fg('muted', text), noMatch: text => theme.fg('muted', text),
30
+ } };
31
+ }
app/src/pi/xterm-terminal.mjs ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StdinBuffer } from './entry.mjs';
2
+
3
+ // Implements Pi's Terminal interface using xterm.js instead of a Node PTY.
4
+ export class XtermTerminal {
5
+ constructor(xterm) { this.xterm = xterm; this.subscriptions = []; }
6
+ start(onInput, onResize) {
7
+ this.input = new StdinBuffer();
8
+ this.input.on('data', onInput);
9
+ this.input.on('paste', text => onInput(`\x1b[200~${text}\x1b[201~`));
10
+ this.subscriptions.push(this.xterm.onData(data => this.input.process(data)), this.xterm.onResize(onResize));
11
+ this.write('\x1b[?2004h');
12
+ }
13
+ stop() {
14
+ this.subscriptions.forEach(subscription => subscription.dispose()); this.subscriptions = [];
15
+ this.input?.destroy(); this.write('\x1b[?2004l');
16
+ }
17
+ async drainInput() { /* There is no parent process or SSH input to drain. */ }
18
+ write(data) { this.xterm.write(data); }
19
+ get columns() { return this.xterm.cols; }
20
+ get rows() { return this.xterm.rows; }
21
+ get kittyProtocolActive() { return false; }
22
+ moveBy(lines) { if (lines) this.write(`\x1b[${Math.abs(lines)}${lines > 0 ? 'B' : 'A'}`); }
23
+ hideCursor() { this.write('\x1b[?25l'); }
24
+ showCursor() { this.write('\x1b[?25h'); }
25
+ clearLine() { this.write('\x1b[K'); }
26
+ clearFromCursor() { this.write('\x1b[J'); }
27
+ clearScreen() { this.write('\x1b[2J\x1b[H'); }
28
+ setTitle(title) { this.xterm.element?.setAttribute('aria-label', title); }
29
+ setProgress(active) { this.xterm.element?.setAttribute('aria-busy', String(active)); }
30
+ inputData(data) { this.input?.process(data); }
31
+ }
app/src/style.css CHANGED
@@ -64,30 +64,15 @@ 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
- #messages { flex: 1; min-height: 120px; overflow: auto; padding: 14px 2px; scrollbar-width: thin; }
68
- #welcome { max-width: 470px; margin: 22px 8px 0; }
69
- .eyebrow { color: var(--muted); font-size: 11px; }
70
- h1 { margin: 8px 0; font-family: inherit; font-size: 22px; line-height: 1.25; letter-spacing: -.025em; font-weight: 600; }
71
- #welcome p { margin: 8px 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
72
- .suggestions { display: flex; flex-direction: column; gap: 6px; margin-top: 16px; }
73
- .suggestions button { display: flex; justify-content: space-between; padding: 8px 10px; background: transparent; text-align: left; font-size: 12px; }
74
- .suggestions button:hover { background: #ededed; }
75
- .suggestions span { color: var(--muted); }
76
-
77
- .message { margin: 0 0 14px; line-height: 1.55; overflow-wrap: anywhere; }
78
- .message-label { margin-bottom: 4px; color: var(--muted); font-size: 10px; font-weight: 600; }
79
- .message-body { white-space: pre-wrap; }
80
- .message.user { border-left: 2px solid #c9c9c9; padding-left: 10px; }
81
- .tool { margin: 6px 0; border: 1px solid var(--line); border-radius: 4px; background: var(--panel); font-size: 12px; }
82
- .tool summary { cursor: pointer; padding: 7px 9px; font-family: ui-monospace, monospace; }
83
- .tool pre { max-height: 240px; margin: 0; padding: 0 10px 9px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 11px; }
84
- .error { color: #a12c28; }
85
- .message-stats { margin-top: 4px; color: var(--muted); font-size: 10px; }
86
  #notice { margin: 6px 0; padding: 8px 10px; border: 1px solid #cfcfcf; border-radius: 4px; background: #f0f0f0; font-size: 12px; }
87
 
88
- #chat-form { padding: 6px; border: 1px solid #c8c8c8; border-radius: 4px; background: var(--surface); }
89
- #prompt { width: 100%; min-height: 48px; max-height: 160px; padding: 6px; resize: vertical; border: 0; background: transparent; font-size: 13px; line-height: 1.45; }
90
- .composer-footer { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
91
  .composer-footer span { margin-right: auto; padding-left: 6px; color: var(--muted); font-size: 10px; }
92
  footer { display: flex; justify-content: space-between; gap: 8px; padding-top: 7px; color: var(--muted); font-size: 10px; }
93
 
@@ -116,21 +101,10 @@ footer { display: flex; justify-content: space-between; gap: 8px; padding-top: 7
116
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
117
 
118
  #open-tab { margin-top: 8px; font-size: 12px; color: var(--accent); text-decoration: none; white-space: normal; }
119
- .markdown { white-space: normal; }
120
- .markdown p { margin: 0 0 8px; }
121
- .markdown pre { padding: 9px 10px; border: 1px solid var(--line); border-radius: 4px; background: #f0f0f0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
122
- .markdown code { font: 12px/1.5 ui-monospace, monospace; }
123
- .markdown :not(pre) > code { padding: 1px 4px; border-radius: 3px; background: #ededed; }
124
- .markdown a { color: var(--accent); text-underline-offset: 2px; }
125
- .markdown table { border-collapse: collapse; font-size: 12px; }
126
- .markdown td, .markdown th { padding: 4px 7px; border: 1px solid var(--line); }
127
-
128
  @media (max-width: 850px) {
129
  header { padding: 0 12px; }
130
  main { display: flex; flex-direction: column; height: auto; }
131
- .conversation { min-height: calc(100dvh - 46px); padding: 10px; }
132
- #messages { flex: 1; max-height: 65dvh; }
133
- #welcome { margin: 18px 4px; }
134
  .workspace { min-height: 520px; border-left: 0; border-top: 1px solid var(--line); }
135
  .file-area { min-height: 280px; grid-template-columns: 115px minmax(0, 1fr); }
136
  .header-right > span:not(.local-dot) { display: none; }
@@ -144,6 +118,7 @@ footer { display: flex; justify-content: space-between; gap: 8px; padding-top: 7
144
  @media (pointer: coarse) {
145
  button, .button, #files button { min-height: 38px; }
146
  .editor-heading { min-height: 40px; }
147
- .tool summary, .terminal summary { padding-top: 11px; padding-bottom: 11px; }
148
- #prompt, #command { font-size: 16px; }
 
149
  }
 
64
  .muted { margin-top: 4px; color: var(--muted); font-size: 11px; }
65
  .muted:empty { display: none; }
66
 
67
+ #pi-terminal { flex: 1; min-height: 180px; min-width: 0; overflow: hidden; margin-top: 10px; padding: 8px; border: 1px solid var(--line); border-radius: 4px; background: #fff; }
68
+ #pi-terminal .pi-terminal-mount, #pi-terminal .xterm { height: 100%; }
69
+ #pi-terminal .xterm-viewport { background-color: #fff !important; }
70
+ #pi-terminal textarea:focus { box-shadow: none; }
71
+ .terminal-controls { display: flex; align-items: center; gap: 6px; padding-top: 7px; }
72
+ .terminal-controls > span { flex: 1; min-width: 0; color: var(--muted); font-size: 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  #notice { margin: 6px 0; padding: 8px 10px; border: 1px solid #cfcfcf; border-radius: 4px; background: #f0f0f0; font-size: 12px; }
74
 
75
+ footer { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
 
 
76
  .composer-footer span { margin-right: auto; padding-left: 6px; color: var(--muted); font-size: 10px; }
77
  footer { display: flex; justify-content: space-between; gap: 8px; padding-top: 7px; color: var(--muted); font-size: 10px; }
78
 
 
101
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
102
 
103
  #open-tab { margin-top: 8px; font-size: 12px; color: var(--accent); text-decoration: none; white-space: normal; }
 
 
 
 
 
 
 
 
 
104
  @media (max-width: 850px) {
105
  header { padding: 0 12px; }
106
  main { display: flex; flex-direction: column; height: auto; }
107
+ .conversation { height: calc(100dvh - 46px); min-height: 500px; padding: 10px; }
 
 
108
  .workspace { min-height: 520px; border-left: 0; border-top: 1px solid var(--line); }
109
  .file-area { min-height: 280px; grid-template-columns: 115px minmax(0, 1fr); }
110
  .header-right > span:not(.local-dot) { display: none; }
 
118
  @media (pointer: coarse) {
119
  button, .button, #files button { min-height: 38px; }
120
  .editor-heading { min-height: 40px; }
121
+ .terminal summary { padding-top: 11px; padding-bottom: 11px; }
122
+ #command { font-size: 16px; }
123
+ #pi-terminal .xterm-helper-textarea { font-size: 16px; }
124
  }
app/tests/browser.mjs CHANGED
@@ -2,8 +2,8 @@ import { chromium } from 'playwright';
2
  import fs from 'node:fs/promises';
3
  import assert from 'node:assert/strict';
4
 
5
- const base = process.env.APP_URL ?? 'http://127.0.0.1:4173/?source=local';
6
- const label = process.env.TEST_LABEL ?? 'agent-browser';
7
  const profile = process.env.BROWSER_PROFILE ?? new URL('../../validation/agent-browser-profile', import.meta.url).pathname;
8
  const output = new URL(`../../validation/${label}.json`, import.meta.url);
9
  const result = { date: new Date().toISOString(), url: base, cases: [], errors: [], requests: [] };
@@ -73,9 +73,18 @@ try {
73
  } },
74
  ];
75
  for (const task of tasks) {
76
- await page.evaluate(() => window.browserPi.rpc('new_chat'));
 
 
77
  const start = Date.now();
78
- const state = await page.evaluate(prompt => window.browserPi.prompt(prompt), task.prompt);
 
 
 
 
 
 
 
79
  const entry = { name: task.name, elapsedMs: Date.now() - start, ...state };
80
  result.cases.push(entry); console.log('CASE', JSON.stringify(entry));
81
  task.check(state);
@@ -84,7 +93,8 @@ try {
84
  const running = page.evaluate(() => window.browserPi.prompt('Write a very long detailed tutorial about shell scripting. Do not use tools.'));
85
  await page.waitForFunction(() => window.browserPi.busy);
86
  await page.waitForTimeout(900);
87
- await page.locator('#stop').click();
 
88
  const stopped = await running;
89
  assert.equal(stopped.messages.filter(m => m.role === 'assistant').at(-1).stopReason, 'aborted');
90
  result.cancellation = { passed: true, completedGenerations: await page.evaluate(() => window.browserPi.diagnostics.generations.length) - beforeStop };
 
2
  import fs from 'node:fs/promises';
3
  import assert from 'node:assert/strict';
4
 
5
+ const base = process.argv[2] ?? process.env.APP_URL ?? 'http://127.0.0.1:4173/?source=local';
6
+ const label = process.argv[3] ?? process.env.TEST_LABEL ?? 'agent-browser';
7
  const profile = process.env.BROWSER_PROFILE ?? new URL('../../validation/agent-browser-profile', import.meta.url).pathname;
8
  const output = new URL(`../../validation/${label}.json`, import.meta.url);
9
  const result = { date: new Date().toISOString(), url: base, cases: [], errors: [], requests: [] };
 
73
  } },
74
  ];
75
  for (const task of tasks) {
76
+ console.log('START', task.name);
77
+ await page.locator('#new-chat').click();
78
+ await page.waitForFunction(() => !window.browserPi.busy);
79
  const start = Date.now();
80
+ await page.locator('.xterm-helper-textarea').focus();
81
+ await page.keyboard.type(task.prompt);
82
+ console.log('TYPED', await page.evaluate(() => window.browserPi.terminal.text));
83
+ await page.waitForFunction(prompt => window.browserPi.terminal.text === prompt, task.prompt, { timeout: 15_000 });
84
+ await page.locator('#send').click();
85
+ await page.waitForFunction(() => window.browserPi.busy);
86
+ await page.waitForFunction(() => !window.browserPi.busy);
87
+ const state = await page.evaluate(() => window.browserPi.rpc('snapshot'));
88
  const entry = { name: task.name, elapsedMs: Date.now() - start, ...state };
89
  result.cases.push(entry); console.log('CASE', JSON.stringify(entry));
90
  task.check(state);
 
93
  const running = page.evaluate(() => window.browserPi.prompt('Write a very long detailed tutorial about shell scripting. Do not use tools.'));
94
  await page.waitForFunction(() => window.browserPi.busy);
95
  await page.waitForTimeout(900);
96
+ await page.locator('.xterm-helper-textarea').focus();
97
+ await page.keyboard.press('Escape');
98
  const stopped = await running;
99
  assert.equal(stopped.messages.filter(m => m.role === 'assistant').at(-1).stopReason, 'aborted');
100
  result.cancellation = { passed: true, completedGenerations: await page.evaluate(() => window.browserPi.diagnostics.generations.length) - beforeStop };
app/tests/space-embedded.mjs CHANGED
@@ -36,7 +36,8 @@ try {
36
  result.loadMs = Date.now() - start;
37
  result.device = await app.evaluate(() => window.browserPi.diagnostics.device);
38
  await app.locator('#new-chat').click(); await app.waitForFunction(() => !window.browserPi.busy);
39
- await app.locator('#prompt').fill('Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.');
 
40
  await app.locator('#send').click();
41
  await app.waitForFunction(() => window.browserPi.busy);
42
  await app.waitForFunction(() => !window.browserPi.busy);
 
36
  result.loadMs = Date.now() - start;
37
  result.device = await app.evaluate(() => window.browserPi.diagnostics.device);
38
  await app.locator('#new-chat').click(); await app.waitForFunction(() => !window.browserPi.busy);
39
+ await app.locator('.xterm-helper-textarea').focus();
40
+ await page.keyboard.type('Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.');
41
  await app.locator('#send').click();
42
  await app.waitForFunction(() => window.browserPi.busy);
43
  await app.waitForFunction(() => !window.browserPi.busy);
app/tests/terminal.mjs ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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] ?? process.env.APP_URL ?? 'http://127.0.0.1:4173/';
6
+ const label = process.argv[3] ?? process.env.TEST_LABEL ?? 'agent-terminal';
7
+ const result = { date: new Date().toISOString(), url, errors: [], requests: [], checks: [] };
8
+ const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true });
9
+ const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
10
+ context.setDefaultTimeout(15_000);
11
+ const page = await context.newPage();
12
+ page.on('pageerror', error => { result.errors.push(error.stack); console.log('PAGEERROR', error.stack); });
13
+ page.on('console', message => { if (message.type() === 'error') console.log('CONSOLE', message.text().slice(0, 500)); });
14
+ context.on('request', request => result.requests.push(request.url()));
15
+ const screen = () => page.evaluate(() => window.browserPi.terminal.readScreen());
16
+ const text = () => page.evaluate(() => window.browserPi.terminal.text);
17
+ async function type(value) { await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.type(value); }
18
+ async function waitScreen(value) { await page.waitForFunction(value => window.browserPi.terminal.readScreen().includes(value), value); }
19
+ try {
20
+ await page.goto(url);
21
+ await page.waitForFunction(() => window.browserPi);
22
+ await page.evaluate(() => window.browserPi.ready);
23
+ await waitScreen('browser workspace');
24
+ result.initial = await screen();
25
+ assert(result.initial.includes('/load'));
26
+ result.checks.push('Original Pi TUI rendered through xterm');
27
+
28
+ await type('/help'); await page.keyboard.press('Enter'); await waitScreen('Ctrl+O');
29
+ result.checks.push('Enter submits /help without loading a model');
30
+ await page.keyboard.press('Control+Shift+f'); await type('browser');
31
+ await page.waitForFunction(() => window.browserPi.terminal.text === '' && /\d+\/\d+/.test(window.browserPi.terminal.readScreen()));
32
+ await page.keyboard.press('Escape'); await page.waitForTimeout(30);
33
+ result.checks.push('Pi fullscreen transcript search preserves the editor draft');
34
+ await type('one'); await page.keyboard.press('Shift+Enter'); await type('two');
35
+ assert.equal(await text(), 'one\ntwo');
36
+ await page.keyboard.press('Backspace'); await page.keyboard.press('Control+a'); await type('X');
37
+ assert.equal(await text(), 'one\nXtw');
38
+ await page.keyboard.press('Control+u'); await page.keyboard.press('Control+k');
39
+ await page.keyboard.press('Backspace'); await page.keyboard.press('Control+u');
40
+ assert.equal(await text(), '');
41
+ result.checks.push('Pi multiline editor: Shift+Enter, cursor, deletion, Ctrl+U/K');
42
+ await page.keyboard.press('Control+p'); assert.equal(await text(), '/help');
43
+ await page.keyboard.press('Control+n'); assert.equal(await text(), '');
44
+ result.checks.push('Pi prompt history');
45
+ await type('/he'); await page.keyboard.press('Tab');
46
+ await page.waitForFunction(() => window.browserPi.terminal.text === '/help ');
47
+ await page.keyboard.press('Enter'); await waitScreen('Ctrl+O');
48
+ result.checks.push('Pi autocomplete: /he β†’ /help');
49
+
50
+ await type("!printf 'terminal-works\\n' > terminal.txt; cat terminal.txt");
51
+ await page.keyboard.press('Escape'); // Dismiss optional workspace completion.
52
+ await page.waitForTimeout(30); // A lone ESC is delimited by Pi's 10ms input timer.
53
+ await page.keyboard.press('Enter'); await waitScreen('[exit 0]');
54
+ await page.waitForFunction(() => !window.browserPi.busy);
55
+ const state = await page.evaluate(() => window.browserPi.rpc('snapshot'));
56
+ assert.equal(state.files['/workspace/terminal.txt'], 'terminal-works\n');
57
+ assert((await screen()).includes('[exit 0]'));
58
+ result.checks.push('xterm keyboard β†’ Pi editor β†’ real just-bash β†’ workspace file');
59
+
60
+ // Model/file text must not be able to clear the terminal or inject OSC URLs.
61
+ await type("!printf 'before\\033[2Jafter\\n'");
62
+ await page.locator('#send').click(); await waitScreen('beforeafter');
63
+ result.checks.push('Shell escape sequences render as inert text');
64
+
65
+ // Paste uses xterm's actual bracketed-paste path through Pi's StdinBuffer.
66
+ await context.grantPermissions(['clipboard-read', 'clipboard-write']);
67
+ const largePaste = '!cat > pasted.txt <<\'EOF\'\n' + Array.from({ length: 15 }, (_, i) => 'row-' + i).join('\n') + '\nEOF';
68
+ await page.evaluate(text => navigator.clipboard.writeText(text), largePaste);
69
+ await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.press('Control+v');
70
+ await page.waitForFunction(text => window.browserPi.terminal.text === text, largePaste);
71
+ await page.locator('#send').click();
72
+ await page.waitForFunction(() => document.querySelector('#files').textContent.includes('pasted.txt'));
73
+ const pastedState = await page.evaluate(() => window.browserPi.rpc('snapshot'));
74
+ assert.equal(pastedState.files['/workspace/pasted.txt'], Array.from({ length: 15 }, (_, i) => 'row-' + i).join('\n') + '\n');
75
+ result.checks.push('Large Pi paste markers expand before Send');
76
+ const pasted = 'hello δΈ–η•Œ πŸ¦™\nline two';
77
+ await page.evaluate(text => navigator.clipboard.writeText(text), pasted);
78
+ await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.press('Control+v');
79
+ await page.waitForFunction(text => window.browserPi.terminal.text === text, pasted);
80
+ result.checks.push('Unicode multiline clipboard paste');
81
+ await page.locator('#newline').click(); assert.equal(await text(), pasted + '\n');
82
+ result.checks.push('Touch-accessible newline button');
83
+
84
+ // Resize while preserving a multiline draft and Pi's own viewport renderer.
85
+ const desktop = await page.evaluate(() => ({ cols: window.browserPi.terminal.cols, rows: window.browserPi.terminal.rows }));
86
+ await page.screenshot({ path: `../validation/${label}-desktop.png`, fullPage: true });
87
+ await page.setViewportSize({ width: 390, height: 844 });
88
+ await page.waitForFunction(cols => window.browserPi.terminal.cols < cols, desktop.cols);
89
+ assert.equal(await text(), pasted + '\n');
90
+ assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
91
+ result.sizes = { desktop, mobile: await page.evaluate(() => ({ cols: window.browserPi.terminal.cols, rows: window.browserPi.terminal.rows })) };
92
+ assert(result.sizes.mobile.cols >= 30); assert(result.sizes.mobile.rows >= 15);
93
+ await page.screenshot({ path: `../validation/${label}-mobile.png`, fullPage: true });
94
+ result.checks.push('390px layout and live terminal resize preserve the draft');
95
+ result.mobileLimitation = 'Desktop Chromium viewport simulation; physical mobile keyboards and GPUs not tested.';
96
+ assert.equal(result.errors.length, 0);
97
+ assert.equal(result.requests.filter(url => /^https?:/.test(url) && new URL(url).origin !== new URL(result.url).origin).length, 0);
98
+ result.passed = true; console.log(JSON.stringify({ passed: true, checks: result.checks, sizes: result.sizes }));
99
+ } catch (error) {
100
+ result.failure = error.stack;
101
+ result.screen = await screen().catch(() => 'App failed before initialization');
102
+ await page.screenshot({ path: `../validation/${label}-failure.png`, fullPage: true });
103
+ throw error;
104
+ } finally { await fs.writeFile(`../validation/${label}.json`, JSON.stringify(result, null, 2)); await browser.close(); }
app/vendor/pi-coding-agent/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mario Zechner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
app/vendor/pi-coding-agent/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pi CLI presentation components
2
+
3
+ These files are **unmodified copies** from the published
4
+ `@earendil-works/pi-coding-agent@0.85.1` npm tarball, under `dist/modes/interactive/`.
5
+ `provenance.json` records their SHA-256 digests. Pi's MIT license is included.
6
+
7
+ - `CustomEditor` is the CLI's actual editor subclass, extending `pi-tui`'s editor.
8
+ - `UserMessageComponent` and `AssistantMessageComponent` are its actual message renderers.
9
+ - `markdown-transform.js` is their upstream helper.
10
+ - `light.json` is the upstream light theme palette.
11
+
12
+ The build redirects their `@earendil-works/pi-tui` import to a selective export
13
+ of the installed **real pi-tui** package. It redirects their Node theme service
14
+ to `src/pi/theme.mjs`, which uses the same palette without filesystem watchers or
15
+ Node syntax highlighting. See `scripts/pi-browser.mjs` for all host adaptations.
16
+
17
+ The rest of the Node CLI is not vendored. Browser shell tool cards, slash-command
18
+ dispatch, worker integration, and workspace controls are app code. They do not
19
+ claim to implement the full CLI's sessions, extensions, or provider management.
20
+
21
+ Upstream: https://github.com/earendil-works/pi/tree/v0.85.1/packages/coding-agent
app/vendor/pi-coding-agent/components/assistant-message.js ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Container, Markdown, MouseRegion, Spacer, Text } from "@earendil-works/pi-tui";
2
+ import { getMarkdownTheme, theme } from "../theme/theme.js";
3
+ import { createMarkdownTransform } from "./markdown-transform.js";
4
+ const OSC133_ZONE_START = "\x1b]133;A\x07";
5
+ const OSC133_ZONE_END = "\x1b]133;B\x07";
6
+ const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
7
+ /**
8
+ * Component that renders a complete assistant message
9
+ */
10
+ export class AssistantMessageComponent extends Container {
11
+ contentContainer;
12
+ hideThinkingBlock;
13
+ markdownTheme;
14
+ hiddenThinkingLabel;
15
+ outputPad;
16
+ markdownTransformers;
17
+ lastMessage;
18
+ hasToolCalls = false;
19
+ isStreaming = false;
20
+ thinkingVisibilityOverrides = new Map();
21
+ constructor(message, hideThinkingBlock = false, markdownTheme = getMarkdownTheme(), hiddenThinkingLabel = "Thinking...", outputPad = 1, markdownTransformers = []) {
22
+ super();
23
+ this.hideThinkingBlock = hideThinkingBlock;
24
+ this.markdownTheme = markdownTheme;
25
+ this.hiddenThinkingLabel = hiddenThinkingLabel;
26
+ this.outputPad = outputPad;
27
+ this.markdownTransformers = markdownTransformers;
28
+ // Container for text/thinking content
29
+ this.contentContainer = new Container();
30
+ this.addChild(this.contentContainer);
31
+ if (message) {
32
+ this.updateContent(message);
33
+ }
34
+ }
35
+ invalidate() {
36
+ super.invalidate();
37
+ if (this.lastMessage) {
38
+ this.updateContent(this.lastMessage);
39
+ }
40
+ }
41
+ setHideThinkingBlock(hide) {
42
+ this.hideThinkingBlock = hide;
43
+ this.thinkingVisibilityOverrides.clear();
44
+ if (this.lastMessage) {
45
+ this.updateContent(this.lastMessage);
46
+ }
47
+ }
48
+ setHiddenThinkingLabel(label) {
49
+ this.hiddenThinkingLabel = label;
50
+ if (this.lastMessage) {
51
+ this.updateContent(this.lastMessage);
52
+ }
53
+ }
54
+ setOutputPad(padding) {
55
+ this.outputPad = padding;
56
+ if (this.lastMessage) {
57
+ this.updateContent(this.lastMessage);
58
+ }
59
+ }
60
+ render(width) {
61
+ const lines = super.render(width);
62
+ if (this.hasToolCalls || lines.length === 0) {
63
+ return lines;
64
+ }
65
+ lines[0] = OSC133_ZONE_START + lines[0];
66
+ lines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];
67
+ return lines;
68
+ }
69
+ updateContent(message, isStreaming = this.isStreaming) {
70
+ this.lastMessage = message;
71
+ this.isStreaming = isStreaming;
72
+ // Clear content container
73
+ this.contentContainer.clear();
74
+ const hasVisibleContent = message.content.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
75
+ if (hasVisibleContent) {
76
+ this.contentContainer.addChild(new Spacer(1));
77
+ }
78
+ // Render content in order
79
+ let thinkingRunIndex = 0;
80
+ for (let i = 0; i < message.content.length; i++) {
81
+ const content = message.content[i];
82
+ if (content.type === "text" && content.text.trim()) {
83
+ // Assistant text messages with no background - trim the text
84
+ // Set paddingY=0 to avoid extra spacing before tool executions
85
+ this.contentContainer.addChild(new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme, undefined, {
86
+ transform: createMarkdownTransform("assistant", this.isStreaming, this.markdownTransformers),
87
+ }));
88
+ }
89
+ else if (content.type === "thinking") {
90
+ const thinkingBlocks = [];
91
+ for (; i < message.content.length; i++) {
92
+ const thinkingContent = message.content[i];
93
+ if (thinkingContent.type !== "thinking") {
94
+ break;
95
+ }
96
+ const thinking = thinkingContent.thinking.trim();
97
+ if (thinking) {
98
+ thinkingBlocks.push(thinking);
99
+ }
100
+ }
101
+ i--;
102
+ if (thinkingBlocks.length === 0) {
103
+ continue;
104
+ }
105
+ // Add spacing only when another visible assistant content block follows.
106
+ // This avoids a superfluous blank line before separately-rendered tool execution blocks.
107
+ const hasVisibleContentAfter = message.content
108
+ .slice(i + 1)
109
+ .some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
110
+ const runIndex = thinkingRunIndex++;
111
+ const hidden = this.thinkingVisibilityOverrides.get(runIndex) ?? this.hideThinkingBlock;
112
+ const thinkingComponent = hidden
113
+ ? new Text(theme.italic(theme.fg("thinkingText", this.hiddenThinkingLabel)), this.outputPad, 0)
114
+ : new Markdown(thinkingBlocks.join("\n\n"), this.outputPad, 0, this.markdownTheme, {
115
+ color: (text) => theme.fg("thinkingText", text),
116
+ italic: true,
117
+ }, {
118
+ transform: createMarkdownTransform("assistant-thinking", this.isStreaming, this.markdownTransformers),
119
+ });
120
+ this.contentContainer.addChild(new MouseRegion(thinkingComponent, (event) => {
121
+ if (event.type !== "click" || event.button !== "left")
122
+ return undefined;
123
+ this.thinkingVisibilityOverrides.set(runIndex, !hidden);
124
+ if (this.lastMessage)
125
+ this.updateContent(this.lastMessage);
126
+ return { handled: true };
127
+ }));
128
+ if (hasVisibleContentAfter) {
129
+ this.contentContainer.addChild(new Spacer(1));
130
+ }
131
+ }
132
+ }
133
+ // Check if incomplete/failed - show after partial content.
134
+ // For aborted/error tool calls, tool execution components show the error.
135
+ // Length stops can happen before a tool call is complete, so surface them here too.
136
+ const hasToolCalls = message.content.some((c) => c.type === "toolCall");
137
+ this.hasToolCalls = hasToolCalls;
138
+ if (message.stopReason === "length") {
139
+ this.contentContainer.addChild(new Spacer(1));
140
+ this.contentContainer.addChild(new Text(theme.fg("error", "Response was truncated before completion."), this.outputPad, 0));
141
+ }
142
+ else if (!hasToolCalls) {
143
+ if (message.stopReason === "aborted") {
144
+ const abortMessage = message.errorMessage && message.errorMessage !== "Request was aborted"
145
+ ? message.errorMessage
146
+ : "Operation aborted";
147
+ this.contentContainer.addChild(new Spacer(1));
148
+ this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), this.outputPad, 0));
149
+ }
150
+ else if (message.stopReason === "error") {
151
+ const errorMsg = message.errorMessage || "Unknown error";
152
+ this.contentContainer.addChild(new Spacer(1));
153
+ this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), this.outputPad, 0));
154
+ }
155
+ }
156
+ }
157
+ }
158
+ //# sourceMappingURL=assistant-message.js.map
app/vendor/pi-coding-agent/components/custom-editor.js ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Editor, visibleWidth } from "@earendil-works/pi-tui";
2
+ /**
3
+ * Custom editor that handles app-level keybindings for coding-agent.
4
+ */
5
+ export class CustomEditor extends Editor {
6
+ keybindings;
7
+ workingStatusIndicator;
8
+ embedWorkingStatus;
9
+ actionHandlers = new Map();
10
+ // Special handlers that can be dynamically replaced
11
+ onEscape;
12
+ onCtrlD;
13
+ onPasteImage;
14
+ /** Handler for extension-registered shortcuts. Returns true if handled. */
15
+ onExtensionShortcut;
16
+ constructor(tui, theme, keybindings, options) {
17
+ super(tui, theme, options);
18
+ this.keybindings = keybindings;
19
+ this.embedWorkingStatus = options?.embedWorkingStatus ?? false;
20
+ }
21
+ setWorkingStatusIndicator(indicator) {
22
+ this.workingStatusIndicator = indicator;
23
+ }
24
+ renderTopBorder(width, hiddenLineCount) {
25
+ if (!this.embedWorkingStatus || !this.workingStatusIndicator || width <= 0) {
26
+ return super.renderTopBorder(width, hiddenLineCount);
27
+ }
28
+ let status = this.workingStatusIndicator.renderInBorder(Math.max(1, width - 5));
29
+ let statusWidth = visibleWidth(status);
30
+ if (statusWidth === 0)
31
+ return super.renderTopBorder(width, hiddenLineCount);
32
+ const overflowLabel = hiddenLineCount > 0 ? ` ↑ ${hiddenLineCount} more ` : undefined;
33
+ const overflowLabelWidth = overflowLabel ? visibleWidth(overflowLabel) : 0;
34
+ const overflowStart = Math.floor((width - overflowLabelWidth) / 2);
35
+ const canFitOverflow = () => overflowLabel !== undefined && overflowLabelWidth + 2 <= width && overflowStart - (3 + statusWidth + 1) >= 1;
36
+ if (overflowLabel && !canFitOverflow()) {
37
+ status = this.workingStatusIndicator.renderSpinnerInBorder(width);
38
+ statusWidth = visibleWidth(status);
39
+ }
40
+ if (canFitOverflow()) {
41
+ const leftBlockWidth = 3 + statusWidth + 1;
42
+ return (this.borderColor("── ") +
43
+ status +
44
+ this.borderColor(` ${"─".repeat(overflowStart - leftBlockWidth)}${overflowLabel}${"─".repeat(width - overflowStart - overflowLabelWidth)}`));
45
+ }
46
+ if (width >= statusWidth + 5) {
47
+ return this.borderColor("── ") + status + this.borderColor(` ${"─".repeat(width - statusWidth - 4)}`);
48
+ }
49
+ status = this.workingStatusIndicator.renderSpinnerInBorder(width);
50
+ statusWidth = visibleWidth(status);
51
+ const prefixWidth = Math.min(3, Math.max(0, width - statusWidth));
52
+ return (this.borderColor("─".repeat(prefixWidth)) +
53
+ status +
54
+ this.borderColor("─".repeat(Math.max(0, width - prefixWidth - statusWidth))));
55
+ }
56
+ /**
57
+ * Register a handler for an app action.
58
+ */
59
+ onAction(action, handler) {
60
+ this.actionHandlers.set(action, handler);
61
+ }
62
+ handleInput(data) {
63
+ // Check extension-registered shortcuts first
64
+ if (this.onExtensionShortcut?.(data)) {
65
+ return;
66
+ }
67
+ // Check for clipboard paste keybinding
68
+ if (this.keybindings.matches(data, "app.clipboard.pasteImage")) {
69
+ this.onPasteImage?.();
70
+ return;
71
+ }
72
+ // Check app keybindings first
73
+ // Escape/interrupt - only if autocomplete is NOT active
74
+ if (this.keybindings.matches(data, "app.interrupt")) {
75
+ if (!this.isShowingAutocomplete()) {
76
+ // Use dynamic onEscape if set, otherwise registered handler
77
+ const handler = this.onEscape ?? this.actionHandlers.get("app.interrupt");
78
+ if (handler) {
79
+ handler();
80
+ return;
81
+ }
82
+ }
83
+ // Let parent handle escape for autocomplete cancellation
84
+ super.handleInput(data);
85
+ return;
86
+ }
87
+ // Exit (Ctrl+D) - only when editor is empty
88
+ if (this.keybindings.matches(data, "app.exit")) {
89
+ if (this.getText().length === 0) {
90
+ const handler = this.onCtrlD ?? this.actionHandlers.get("app.exit");
91
+ if (handler)
92
+ handler();
93
+ return;
94
+ }
95
+ // Fall through to editor handling for delete-char-forward when not empty
96
+ }
97
+ // Explicit history bindings take precedence over app actions while the editor is focused.
98
+ // This lets users bind Ctrl+P even though it cycles models by default.
99
+ if (this.keybindings.matches(data, "tui.editor.historyPrevious") ||
100
+ this.keybindings.matches(data, "tui.editor.historyNext")) {
101
+ super.handleInput(data);
102
+ return;
103
+ }
104
+ // Check all other app actions
105
+ for (const [action, handler] of this.actionHandlers) {
106
+ if (action !== "app.interrupt" && action !== "app.exit" && this.keybindings.matches(data, action)) {
107
+ handler();
108
+ return;
109
+ }
110
+ }
111
+ // Pass to parent for editor handling
112
+ super.handleInput(data);
113
+ }
114
+ }
115
+ //# sourceMappingURL=custom-editor.js.map
app/vendor/pi-coding-agent/components/markdown-transform.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function createMarkdownTransform(messageType, isStreaming, transformers) {
2
+ return (markdown, availableWidth) => applyMarkdownTransformers(markdown, { messageType, isStreaming, availableWidth }, transformers);
3
+ }
4
+ function applyMarkdownTransformers(markdown, context, transformers) {
5
+ let transformedMarkdown = markdown;
6
+ for (const transformer of transformers) {
7
+ try {
8
+ const transformed = transformer(transformedMarkdown, context);
9
+ if (typeof transformed === "string") {
10
+ transformedMarkdown = transformed;
11
+ }
12
+ }
13
+ catch {
14
+ // Keep the current Markdown and continue with the next transformer.
15
+ }
16
+ }
17
+ return transformedMarkdown;
18
+ }
19
+ //# sourceMappingURL=markdown-transform.js.map
app/vendor/pi-coding-agent/components/user-message.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Box, Container, Markdown } from "@earendil-works/pi-tui";
2
+ import { getMarkdownTheme, theme } from "../theme/theme.js";
3
+ import { createMarkdownTransform } from "./markdown-transform.js";
4
+ const OSC133_ZONE_START = "\x1b]133;A\x07";
5
+ const OSC133_ZONE_END = "\x1b]133;B\x07";
6
+ const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
7
+ /**
8
+ * Component that renders a user message
9
+ */
10
+ export class UserMessageComponent extends Container {
11
+ text;
12
+ markdownTheme;
13
+ outputPad;
14
+ markdownTransformers;
15
+ constructor(text, markdownTheme = getMarkdownTheme(), outputPad = 1, markdownTransformers = []) {
16
+ super();
17
+ this.text = text;
18
+ this.markdownTheme = markdownTheme;
19
+ this.outputPad = outputPad;
20
+ this.markdownTransformers = markdownTransformers;
21
+ this.rebuild();
22
+ }
23
+ setOutputPad(padding) {
24
+ this.outputPad = padding;
25
+ this.rebuild();
26
+ }
27
+ rebuild() {
28
+ this.clear();
29
+ const contentBox = new Box(this.outputPad, 1, (content) => theme.bg("userMessageBg", content));
30
+ contentBox.addChild(new Markdown(this.text, 0, 0, this.markdownTheme, {
31
+ color: (content) => theme.fg("userMessageText", content),
32
+ }, {
33
+ preserveOrderedListMarkers: true,
34
+ preserveBackslashEscapes: true,
35
+ transform: createMarkdownTransform("user", false, this.markdownTransformers),
36
+ }));
37
+ this.addChild(contentBox);
38
+ }
39
+ render(width) {
40
+ const lines = super.render(width);
41
+ if (lines.length === 0) {
42
+ return lines;
43
+ }
44
+ lines[0] = OSC133_ZONE_START + lines[0];
45
+ lines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];
46
+ return lines;
47
+ }
48
+ }
49
+ //# sourceMappingURL=user-message.js.map
app/vendor/pi-coding-agent/provenance.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "package": "@earendil-works/pi-coding-agent",
3
+ "version": "0.85.1",
4
+ "source": "https://github.com/earendil-works/pi/tree/v0.85.1/packages/coding-agent",
5
+ "files": {
6
+ "components/custom-editor.js": "000b0056f2bf645537537468bdd6f6ee0c82e4110f5417c1c70c2e8fefc04f29",
7
+ "components/user-message.js": "398a34b8a979daea5614be4cbbe6ff66e7eca97e9fd855508791dcddce994bc8",
8
+ "components/assistant-message.js": "edbd1e712234609d2ad1078edcf960e3b6f549669075f1c5d1a7a4481e2b3d19",
9
+ "components/markdown-transform.js": "31ad2245189328104a31bb9b12f58b0a12e4af9c7924775a242c9dcd526d2c1a",
10
+ "theme/light.json": "14c7172ba7e75eab6f509504de806af0f2d9ff817bfb45d9533bff15c7e6e657"
11
+ }
12
+ }
app/vendor/pi-coding-agent/theme/light.json ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "light",
4
+ "vars": {
5
+ "teal": "#5a8080",
6
+ "blue": "#547da7",
7
+ "green": "#588458",
8
+ "red": "#aa5555",
9
+ "yellow": "#9a7326",
10
+ "text": "#1f2328",
11
+ "mediumGray": "#6c6c6c",
12
+ "dimGray": "#767676",
13
+ "lightGray": "#b0b0b0",
14
+ "selectedBg": "#d0d0e0",
15
+ "userMsgBg": "#e8e8e8",
16
+ "toolPendingBg": "#e8e8f0",
17
+ "toolSuccessBg": "#e8f0e8",
18
+ "toolErrorBg": "#f0e8e8",
19
+ "customMsgBg": "#ede7f6"
20
+ },
21
+ "colors": {
22
+ "accent": "teal",
23
+ "border": "blue",
24
+ "borderAccent": "teal",
25
+ "borderMuted": "lightGray",
26
+ "success": "green",
27
+ "error": "red",
28
+ "warning": "yellow",
29
+ "muted": "mediumGray",
30
+ "dim": "dimGray",
31
+ "text": "text",
32
+ "thinkingText": "mediumGray",
33
+
34
+ "selectedBg": "selectedBg",
35
+ "scrollbarTrack": "lightGray",
36
+ "scrollbarThumb": "text",
37
+ "searchMatchBg": "selectedBg",
38
+ "searchMatchText": "text",
39
+ "userMessageBg": "userMsgBg",
40
+ "userMessageText": "text",
41
+ "customMessageBg": "customMsgBg",
42
+ "customMessageText": "text",
43
+ "customMessageLabel": "#7e57c2",
44
+ "toolPendingBg": "toolPendingBg",
45
+ "toolSuccessBg": "toolSuccessBg",
46
+ "toolErrorBg": "toolErrorBg",
47
+ "toolTitle": "text",
48
+ "toolOutput": "mediumGray",
49
+
50
+ "mdHeading": "yellow",
51
+ "mdLink": "blue",
52
+ "mdLinkUrl": "dimGray",
53
+ "mdCode": "teal",
54
+ "mdCodeBlock": "green",
55
+ "mdCodeBlockBorder": "mediumGray",
56
+ "mdQuote": "mediumGray",
57
+ "mdQuoteBorder": "mediumGray",
58
+ "mdHr": "mediumGray",
59
+ "mdListBullet": "green",
60
+
61
+ "toolDiffAdded": "green",
62
+ "toolDiffRemoved": "red",
63
+ "toolDiffContext": "mediumGray",
64
+
65
+ "syntaxComment": "#008000",
66
+ "syntaxKeyword": "#0000FF",
67
+ "syntaxFunction": "#795E26",
68
+ "syntaxVariable": "#001080",
69
+ "syntaxString": "#A31515",
70
+ "syntaxNumber": "#098658",
71
+ "syntaxType": "#267F99",
72
+ "syntaxOperator": "#000000",
73
+ "syntaxPunctuation": "#000000",
74
+
75
+ "thinkingOff": "lightGray",
76
+ "thinkingMinimal": "#767676",
77
+ "thinkingLow": "blue",
78
+ "thinkingMedium": "teal",
79
+ "thinkingHigh": "#875f87",
80
+ "thinkingXhigh": "#8b008b",
81
+ "thinkingMax": "#af005f",
82
+
83
+ "bashMode": "green"
84
+ },
85
+ "export": {
86
+ "pageBg": "#f8f8f8",
87
+ "cardBg": "#ffffff",
88
+ "infoBg": "#fffae6"
89
+ }
90
+ }
app/vite.config.mjs CHANGED
@@ -2,14 +2,16 @@ import { defineConfig } from 'vite';
2
  import { cpSync, mkdirSync, readdirSync, createReadStream, statSync } from 'node:fs';
3
  import { resolve } from 'node:path';
4
  import { collectLicenses } from './scripts/licenses.mjs';
 
5
 
6
  export default defineConfig({
7
  base: './',
8
  worker: { format: 'es' },
 
9
  resolve: { alias: { '@huggingface/transformers': resolve('node_modules/@huggingface/transformers/dist/transformers.web.js') } },
10
  server: { proxy: { '/models': 'http://127.0.0.1:8765' } },
11
  preview: { proxy: { '/models': 'http://127.0.0.1:8765' } },
12
- plugins: [{
13
  name: 'local-ort-runtime',
14
  configureServer(server) {
15
  server.middlewares.use('/runtime', (req, res, next) => {
 
2
  import { cpSync, mkdirSync, readdirSync, createReadStream, statSync } from 'node:fs';
3
  import { resolve } from 'node:path';
4
  import { collectLicenses } from './scripts/licenses.mjs';
5
+ import { piBrowser } from './scripts/pi-browser.mjs';
6
 
7
  export default defineConfig({
8
  base: './',
9
  worker: { format: 'es' },
10
+ optimizeDeps: { exclude: ['@earendil-works/pi-tui'], include: ['events', 'buffer'] },
11
  resolve: { alias: { '@huggingface/transformers': resolve('node_modules/@huggingface/transformers/dist/transformers.web.js') } },
12
  server: { proxy: { '/models': 'http://127.0.0.1:8765' } },
13
  preview: { proxy: { '/models': 'http://127.0.0.1:8765' } },
14
+ plugins: [piBrowser(), {
15
  name: 'local-ort-runtime',
16
  configureServer(server) {
17
  server.middlewares.use('/runtime', (req, res, next) => {
assets/index-BRKUEVCq.js ADDED
The diff for this file is too large to render. See raw diff
 
assets/index-Cfkr7NLu.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}#messages{scrollbar-width:thin;flex:1;min-height:120px;padding:14px 2px;overflow:auto}#welcome{max-width:470px;margin:22px 8px 0}.eyebrow{color:var(--muted);font-size:11px}h1{letter-spacing:-.025em;margin:8px 0;font-family:inherit;font-size:22px;font-weight:600;line-height:1.25}#welcome p{color:var(--muted);margin:8px 0;font-size:12px;line-height:1.6}.suggestions{flex-direction:column;gap:6px;margin-top:16px;display:flex}.suggestions button{text-align:left;background:0 0;justify-content:space-between;padding:8px 10px;font-size:12px;display:flex}.suggestions button:hover{background:#ededed}.suggestions span{color:var(--muted)}.message{overflow-wrap:anywhere;margin:0 0 14px;line-height:1.55}.message-label{color:var(--muted);margin-bottom:4px;font-size:10px;font-weight:600}.message-body{white-space:pre-wrap}.message.user{border-left:2px solid #c9c9c9;padding-left:10px}.tool{border:1px solid var(--line);background:var(--panel);border-radius:4px;margin:6px 0;font-size:12px}.tool summary{cursor:pointer;padding:7px 9px;font-family:ui-monospace,monospace}.tool pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:240px;margin:0;padding:0 10px 9px;font-size:11px;overflow:auto}.error{color:#a12c28}.message-stats{color:var(--muted);margin-top:4px;font-size:10px}#notice{background:#f0f0f0;border:1px solid #cfcfcf;border-radius:4px;margin:6px 0;padding:8px 10px;font-size:12px}#chat-form{background:var(--surface);border:1px solid #c8c8c8;border-radius:4px;padding:6px}#prompt{resize:vertical;background:0 0;border:0;width:100%;min-height:48px;max-height:160px;padding:6px;font-size:13px;line-height:1.45}.composer-footer{justify-content:flex-end;align-items:center;gap:6px;display:flex}.composer-footer span{color:var(--muted);margin-right:auto;padding-left:6px;font-size:10px}footer{color:var(--muted);justify-content:space-between;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}.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}.markdown{white-space:normal}.markdown p{margin:0 0 8px}.markdown pre{border:1px solid var(--line);white-space:pre-wrap;overflow-wrap:anywhere;background:#f0f0f0;border-radius:4px;padding:9px 10px;overflow:auto}.markdown code{font:12px/1.5 ui-monospace,monospace}.markdown :not(pre)>code{background:#ededed;border-radius:3px;padding:1px 4px}.markdown a{color:var(--accent);text-underline-offset:2px}.markdown table{border-collapse:collapse;font-size:12px}.markdown td,.markdown th{border:1px solid var(--line);padding:4px 7px}@media (width<=850px){header{padding:0 12px}main{flex-direction:column;height:auto;display:flex}.conversation{min-height:calc(100dvh - 46px);padding:10px}#messages{flex:1;max-height:65dvh}#welcome{margin:18px 4px}.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}.tool summary,.terminal summary{padding-top:11px;padding-bottom:11px}#prompt,#command{font-size:16px}}
 
 
assets/index-DL-Ot0hN.css ADDED
@@ -0,0 +1 @@
 
 
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}#pi-terminal{border:1px solid var(--line);background:#fff;border-radius:4px;flex:1;min-width:0;min-height:180px;margin-top:10px;padding:8px;overflow:hidden}#pi-terminal .pi-terminal-mount,#pi-terminal .xterm{height:100%}#pi-terminal .xterm-viewport{background-color:#fff!important}#pi-terminal textarea:focus{box-shadow:none}.terminal-controls{align-items:center;gap:6px;padding-top:7px;display:flex}.terminal-controls>span{min-width:0;color:var(--muted);flex:1;font-size:10px}#notice{background:#f0f0f0;border:1px solid #cfcfcf;border-radius:4px;margin:6px 0;padding:8px 10px;font-size:12px}footer{justify-content:flex-end;align-items:center;gap:6px;display:flex}.composer-footer span{color:var(--muted);margin-right:auto;padding-left:6px;font-size:10px}footer{color:var(--muted);justify-content:space-between;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}.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:500px;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-LA8xezhP.js DELETED
The diff for this file is too large to render. See raw diff
 
deployment-manifest.json CHANGED
@@ -13,28 +13,28 @@
13
  "sha256": "7b2f4725b2cb4fc1d22031a311857d5ac8754c882bf4e01b5ab1a9778b2417f0"
14
  },
15
  "README.md": {
16
- "bytes": 8633,
17
- "sha256": "979de7c20b8dc21256c1de2706f2b406a4b3c5a086e3ecb8283dcf6484d0ff7a"
18
  },
19
  "SPACE_README.md": {
20
- "bytes": 8633,
21
- "sha256": "979de7c20b8dc21256c1de2706f2b406a4b3c5a086e3ecb8283dcf6484d0ff7a"
22
  },
23
  "app/README.md": {
24
- "bytes": 6739,
25
- "sha256": "db7a2c96658c57ba566f3857809a8f0f39efee60fa65d93c8b017909979dd0dc"
26
  },
27
  "app/index.html": {
28
- "bytes": 4033,
29
- "sha256": "88ad3a62bf464e306d1fd798b2d65ea89a85d22a677c2e3eb6a7033086ae3b4e"
30
  },
31
  "app/package-lock.json": {
32
- "bytes": 156122,
33
- "sha256": "d26257b0e926fb4a5481207f47a143c861c66eca652582cf07cb26b9648ff086"
34
  },
35
  "app/package.json": {
36
- "bytes": 659,
37
- "sha256": "f353b4fa416e51c2a8695d972bfcfe73e39e69eb5ec4235ac52fd2428893a177"
38
  },
39
  "app/public/favicon.svg": {
40
  "bytes": 227,
@@ -53,8 +53,12 @@
53
  "sha256": "0457f5bcec3b3b211605dfb5d1a49042fd638f3686a410fe099c24a25af13c48"
54
  },
55
  "app/scripts/licenses.mjs": {
56
- "bytes": 1423,
57
- "sha256": "979835b01259283c99d367f5a724331197a68df9fcfbe8681ed6c53cfdc6ac69"
 
 
 
 
58
  },
59
  "app/src/agent.worker.mjs": {
60
  "bytes": 4265,
@@ -69,8 +73,8 @@
69
  "sha256": "04375923fd4d2cfd6a58e17fb2eb966ddf4fb766fb050a88459e2d8e7358926c"
70
  },
71
  "app/src/main.mjs": {
72
- "bytes": 12114,
73
- "sha256": "41b46870cb5a06b12af6bd9a9d49bf432025c71402253e94fb4517cfb9ed7745"
74
  },
75
  "app/src/markdown.mjs": {
76
  "bytes": 699,
@@ -80,6 +84,30 @@
80
  "bytes": 3656,
81
  "sha256": "67bf7df79a547f8183a6321269e9c250fd2d2aabeebbecd13949f75c4cce8bb9"
82
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  "app/src/protocol.mjs": {
84
  "bytes": 4981,
85
  "sha256": "1d4416d6a77b60aeb847d88e6d98ff9a580e8dcb163add5f42923f2d145bf52e"
@@ -89,16 +117,16 @@
89
  "sha256": "dec9e2962a0f5c076512368395017fb5af7c9f7bfbe8cfed10105072dbcb76fe"
90
  },
91
  "app/src/style.css": {
92
- "bytes": 9353,
93
- "sha256": "0f69e55070dd85f7b053067b2600b4b91aa0910c3e33e972438d4594cb6fae6c"
94
  },
95
  "app/src/workspace.mjs": {
96
  "bytes": 5206,
97
  "sha256": "d3bf4e77e6ab855cd4d10f11330f52531068a7ca13456ddf4c357d03bfee40e0"
98
  },
99
  "app/tests/browser.mjs": {
100
- "bytes": 8972,
101
- "sha256": "04372cea62da264d5ce6d5f30277b14604bb12fda43bdc6aef08681fe6a6bfe6"
102
  },
103
  "app/tests/dev-smoke.mjs": {
104
  "bytes": 1352,
@@ -117,28 +145,64 @@
117
  "sha256": "8cc0750f0861f5cf3d63e155c9ea4091bae5432a5a6719a55941d63599456e55"
118
  },
119
  "app/tests/space-embedded.mjs": {
120
- "bytes": 3553,
121
- "sha256": "2296ad1867723ccd28553213c6a7d789d4d53c80fd355ea8fa1598c41b0090df"
122
  },
123
  "app/tests/space-restricted.mjs": {
124
  "bytes": 2580,
125
  "sha256": "f71fb23f85cbf021cbf3f368e27687e64fb25cddf232dc1224202a920de78458"
126
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  "app/vite.config.mjs": {
128
- "bytes": 1524,
129
- "sha256": "43ddd0d504729d9eba88e05f530c3d324e3a2705a15fd17971886668a787f6df"
130
  },
131
  "assets/agent.worker-DqQzo9yL.js": {
132
  "bytes": 2092911,
133
  "sha256": "1fc0006217cf28bf42951d1738e67d235b9516f9d579fa46064718759fd7ba28"
134
  },
135
- "assets/index-Cfkr7NLu.css": {
136
- "bytes": 8002,
137
- "sha256": "cbd285909fa229f37aa9147314d6c8f21937550a12f7609fcfd13cd33b020bc6"
138
  },
139
- "assets/index-LA8xezhP.js": {
140
- "bytes": 192028,
141
- "sha256": "1bc7b0bc05fad0e289c4ac9ed7000e6063f1690ee339e62771079a3be967e0ab"
142
  },
143
  "assets/ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm": {
144
  "bytes": 23567050,
@@ -149,12 +213,12 @@
149
  "sha256": "c5cf939c058ae5b63f0cdab8d1becc4544582d6ed74098e9c8460d3acb78309e"
150
  },
151
  "index.html": {
152
- "bytes": 4130,
153
- "sha256": "2d287cf57ed9665b4f87a837f93c8ce6db95f60c1c422a2974d9b7ed8b9652c1"
154
  },
155
  "licenses/dependencies.txt": {
156
- "bytes": 1367435,
157
- "sha256": "1f7eac34d3c2ac44325f7ef6d38852620ea672c124ee2f1713a0796587ffe59c"
158
  },
159
  "licenses/onnxruntime-LICENSE": {
160
  "bytes": 1073,
@@ -168,13 +232,17 @@
168
  "bytes": 1069,
169
  "sha256": "0457f5bcec3b3b211605dfb5d1a49042fd638f3686a410fe099c24a25af13c48"
170
  },
 
 
 
 
171
  "requirements-convert.txt": {
172
  "bytes": 254,
173
  "sha256": "2a2dca97d6a9480afff1ed0da8f5924610552cfe509d5081788455f9c1d71f8b"
174
  },
175
  "research/browser-agent/REPORT.md": {
176
- "bytes": 9988,
177
- "sha256": "4c204b435a7ff62cb492dff883a284b95c60854306be31575bb4dc15da19a899"
178
  },
179
  "research/browser-agent/just-bash-README.md": {
180
  "bytes": 29503,
@@ -237,8 +305,8 @@
237
  "sha256": "b9bb39d5bd7bceb260d5eafa633f5af2e47a35d6132c8453d908ea6a4ad8a0f9"
238
  },
239
  "scripts/prepare_space.py": {
240
- "bytes": 3774,
241
- "sha256": "1a281fd8f61dc9caf206e11c645dffd39c19ee5689e4680e2e6c32211d176234"
242
  },
243
  "scripts/reference.py": {
244
  "bytes": 2652,
@@ -292,6 +360,18 @@
292
  "bytes": 2244,
293
  "sha256": "64f5f41f482beb1886a922d4f5872a8f82ad61b701ff45c9e21fb7980acd4ff7"
294
  },
 
 
 
 
 
 
 
 
 
 
 
 
295
  "validation/benchmark-summary.json": {
296
  "bytes": 1500,
297
  "sha256": "5bc51fbdbc43b58caa2036b621ded2bd3b4c1c95ba487b07bb5b84f19e97ebc0"
 
13
  "sha256": "7b2f4725b2cb4fc1d22031a311857d5ac8754c882bf4e01b5ab1a9778b2417f0"
14
  },
15
  "README.md": {
16
+ "bytes": 8768,
17
+ "sha256": "3c928e6cc25c94390c8564c09bf207d6ef3520ec69b8eae78bfea2f9272942b9"
18
  },
19
  "SPACE_README.md": {
20
+ "bytes": 8768,
21
+ "sha256": "3c928e6cc25c94390c8564c09bf207d6ef3520ec69b8eae78bfea2f9272942b9"
22
  },
23
  "app/README.md": {
24
+ "bytes": 8286,
25
+ "sha256": "d7f0c4e8efd9b7237de4a938dc6f8dd3937c58139e09f87ca22041e83a44ae97"
26
  },
27
  "app/index.html": {
28
+ "bytes": 3254,
29
+ "sha256": "fddf34b58f9bc96fd7812fbfc8946562d75d34d4d08bf4283e5ab2c27b48f1e8"
30
  },
31
  "app/package-lock.json": {
32
+ "bytes": 159289,
33
+ "sha256": "abf0ce0c8b9240388940b2c1d666fbdcaa4f453eebdde9d2f80744f5cc461ac7"
34
  },
35
  "app/package.json": {
36
+ "bytes": 856,
37
+ "sha256": "83ec2cd1c54a65c91b6b8d3162fc2a4f9543aeec7e520fe6958aba3a1c7fe06a"
38
  },
39
  "app/public/favicon.svg": {
40
  "bytes": 227,
 
53
  "sha256": "0457f5bcec3b3b211605dfb5d1a49042fd638f3686a410fe099c24a25af13c48"
54
  },
55
  "app/scripts/licenses.mjs": {
56
+ "bytes": 1536,
57
+ "sha256": "8f04bf1c94122d5ad9fe8cb6964b69f8fdf71a5c1005921be960d5c8c6d14739"
58
+ },
59
+ "app/scripts/pi-browser.mjs": {
60
+ "bytes": 1635,
61
+ "sha256": "bd061a3825e46db09624261b39150f2dfab1c838bc252d54599d0f950f246d3f"
62
  },
63
  "app/src/agent.worker.mjs": {
64
  "bytes": 4265,
 
73
  "sha256": "04375923fd4d2cfd6a58e17fb2eb966ddf4fb766fb050a88459e2d8e7358926c"
74
  },
75
  "app/src/main.mjs": {
76
+ "bytes": 10043,
77
+ "sha256": "a0a818a35e52bf8edb21d011e7167848531efe01146a4e10dd9d49a4817e93ce"
78
  },
79
  "app/src/markdown.mjs": {
80
  "bytes": 699,
 
84
  "bytes": 3656,
85
  "sha256": "67bf7df79a547f8183a6321269e9c250fd2d2aabeebbecd13949f75c4cce8bb9"
86
  },
87
+ "app/src/pi/browser-host.mjs": {
88
+ "bytes": 329,
89
+ "sha256": "27db1727103727a5a01e455daefa45634fb3be62af76cc883b21abb7069a1d87"
90
+ },
91
+ "app/src/pi/capabilities.mjs": {
92
+ "bytes": 1385,
93
+ "sha256": "62f09aa13c695980717c7bc7e25ad6aba66254faa994bf81f39d2a912a89ff83"
94
+ },
95
+ "app/src/pi/entry.mjs": {
96
+ "bytes": 1351,
97
+ "sha256": "f7e6ae0ba428b78126c6873f9784dbf7d9220421cd6510ad893819492ed5ff9b"
98
+ },
99
+ "app/src/pi/terminal-ui.mjs": {
100
+ "bytes": 13938,
101
+ "sha256": "0c8bbef004dd83f2f979fc8963d8ebba376f33e35973abe20f134a081f38c6ed"
102
+ },
103
+ "app/src/pi/theme.mjs": {
104
+ "bytes": 1835,
105
+ "sha256": "2fb5e6d44ff8f68a369c8a4cc203bd3f1bb6cdf8dd64a50674ea83118961d9e8"
106
+ },
107
+ "app/src/pi/xterm-terminal.mjs": {
108
+ "bytes": 1496,
109
+ "sha256": "152d6d8cf26932e4a01efcb9613513bb30bd892aa4f3096800b98884346cba48"
110
+ },
111
  "app/src/protocol.mjs": {
112
  "bytes": 4981,
113
  "sha256": "1d4416d6a77b60aeb847d88e6d98ff9a580e8dcb163add5f42923f2d145bf52e"
 
117
  "sha256": "dec9e2962a0f5c076512368395017fb5af7c9f7bfbe8cfed10105072dbcb76fe"
118
  },
119
  "app/src/style.css": {
120
+ "bytes": 7512,
121
+ "sha256": "b6423d7323eb469f40c322ccb81e0b9f724a01e3c8c9794365222ccd63252dcb"
122
  },
123
  "app/src/workspace.mjs": {
124
  "bytes": 5206,
125
  "sha256": "d3bf4e77e6ab855cd4d10f11330f52531068a7ca13456ddf4c357d03bfee40e0"
126
  },
127
  "app/tests/browser.mjs": {
128
+ "bytes": 9598,
129
+ "sha256": "01f506319c6654b0deaa853bc8e79c3a034fb47b1f7a4e16fbbe7ca4ba257b8f"
130
  },
131
  "app/tests/dev-smoke.mjs": {
132
  "bytes": 1352,
 
145
  "sha256": "8cc0750f0861f5cf3d63e155c9ea4091bae5432a5a6719a55941d63599456e55"
146
  },
147
  "app/tests/space-embedded.mjs": {
148
+ "bytes": 3599,
149
+ "sha256": "c5475b6eda91e9dbbacc8e81dbaaf6f89ec41be390182d37739076c81157bf7f"
150
  },
151
  "app/tests/space-restricted.mjs": {
152
  "bytes": 2580,
153
  "sha256": "f71fb23f85cbf021cbf3f368e27687e64fb25cddf232dc1224202a920de78458"
154
  },
155
+ "app/tests/terminal.mjs": {
156
+ "bytes": 7329,
157
+ "sha256": "d19a8df4e4559f75a6c8f5397dde82b42e522da49e637fb5a7ed5496603b8013"
158
+ },
159
+ "app/vendor/pi-coding-agent/LICENSE": {
160
+ "bytes": 1069,
161
+ "sha256": "0457f5bcec3b3b211605dfb5d1a49042fd638f3686a410fe099c24a25af13c48"
162
+ },
163
+ "app/vendor/pi-coding-agent/README.md": {
164
+ "bytes": 1185,
165
+ "sha256": "b7d5406662cbece954f1bcd53d99a15b41a867adbf225380ac40d08f3487da4b"
166
+ },
167
+ "app/vendor/pi-coding-agent/components/assistant-message.js": {
168
+ "bytes": 7410,
169
+ "sha256": "edbd1e712234609d2ad1078edcf960e3b6f549669075f1c5d1a7a4481e2b3d19"
170
+ },
171
+ "app/vendor/pi-coding-agent/components/custom-editor.js": {
172
+ "bytes": 5095,
173
+ "sha256": "000b0056f2bf645537537468bdd6f6ee0c82e4110f5417c1c70c2e8fefc04f29"
174
+ },
175
+ "app/vendor/pi-coding-agent/components/markdown-transform.js": {
176
+ "bytes": 786,
177
+ "sha256": "31ad2245189328104a31bb9b12f58b0a12e4af9c7924775a242c9dcd526d2c1a"
178
+ },
179
+ "app/vendor/pi-coding-agent/components/user-message.js": {
180
+ "bytes": 1791,
181
+ "sha256": "398a34b8a979daea5614be4cbbe6ff66e7eca97e9fd855508791dcddce994bc8"
182
+ },
183
+ "app/vendor/pi-coding-agent/provenance.json": {
184
+ "bytes": 690,
185
+ "sha256": "9f8797b8d55c60820d422d88f31cb5eec6d5e9283cd4d6f5a374649f7d535b0b"
186
+ },
187
+ "app/vendor/pi-coding-agent/theme/light.json": {
188
+ "bytes": 2236,
189
+ "sha256": "14c7172ba7e75eab6f509504de806af0f2d9ff817bfb45d9533bff15c7e6e657"
190
+ },
191
  "app/vite.config.mjs": {
192
+ "bytes": 1679,
193
+ "sha256": "eab9d66533efd95e346d22f3f455c4c77a85c1386e82e14f3e72c4fdbb8d9c16"
194
  },
195
  "assets/agent.worker-DqQzo9yL.js": {
196
  "bytes": 2092911,
197
  "sha256": "1fc0006217cf28bf42951d1738e67d235b9516f9d579fa46064718759fd7ba28"
198
  },
199
+ "assets/index-BRKUEVCq.js": {
200
+ "bytes": 696106,
201
+ "sha256": "d5eb8ca63b1d540c55787b03658c4c74dcb541f5f6fd18165bae838c42a21077"
202
  },
203
+ "assets/index-DL-Ot0hN.css": {
204
+ "bytes": 10345,
205
+ "sha256": "8e50e0599e5a5e56558fbcf529b549cf55c4b609eb598772a19b8fb3db7437b1"
206
  },
207
  "assets/ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm": {
208
  "bytes": 23567050,
 
213
  "sha256": "c5cf939c058ae5b63f0cdab8d1becc4544582d6ed74098e9c8460d3acb78309e"
214
  },
215
  "index.html": {
216
+ "bytes": 3351,
217
+ "sha256": "a32849e547e267c36f069003b8816134513c476ecade94bd82eb33ade4f641de"
218
  },
219
  "licenses/dependencies.txt": {
220
+ "bytes": 1384222,
221
+ "sha256": "6e11a0fa7b1140bc41a9c2f50d06bfa2f12906a45437f26e1f8048f30210d4ff"
222
  },
223
  "licenses/onnxruntime-LICENSE": {
224
  "bytes": 1073,
 
232
  "bytes": 1069,
233
  "sha256": "0457f5bcec3b3b211605dfb5d1a49042fd638f3686a410fe099c24a25af13c48"
234
  },
235
+ "licenses/pi-components.json": {
236
+ "bytes": 690,
237
+ "sha256": "9f8797b8d55c60820d422d88f31cb5eec6d5e9283cd4d6f5a374649f7d535b0b"
238
+ },
239
  "requirements-convert.txt": {
240
  "bytes": 254,
241
  "sha256": "2a2dca97d6a9480afff1ed0da8f5924610552cfe509d5081788455f9c1d71f8b"
242
  },
243
  "research/browser-agent/REPORT.md": {
244
+ "bytes": 14214,
245
+ "sha256": "29fd4bdec42d8eb0e6c13698badd6182efc24697cc44585a9783c7ecc52eff00"
246
  },
247
  "research/browser-agent/just-bash-README.md": {
248
  "bytes": 29503,
 
305
  "sha256": "b9bb39d5bd7bceb260d5eafa633f5af2e47a35d6132c8453d908ea6a4ad8a0f9"
306
  },
307
  "scripts/prepare_space.py": {
308
+ "bytes": 3867,
309
+ "sha256": "7f39c89f4df965e29ba05b7a5ee4fe4ec71e225f8e04ed0f6fb933c471938502"
310
  },
311
  "scripts/reference.py": {
312
  "bytes": 2652,
 
360
  "bytes": 2244,
361
  "sha256": "64f5f41f482beb1886a922d4f5872a8f82ad61b701ff45c9e21fb7980acd4ff7"
362
  },
363
+ "validation/agent-terminal-dev.json": {
364
+ "bytes": 12592,
365
+ "sha256": "3c0dc1cad52da619a31abc0b86c892b37c32ac0fd44b62afb43f326bf5ac8d66"
366
+ },
367
+ "validation/agent-terminal-model.json": {
368
+ "bytes": 55713,
369
+ "sha256": "1cf1b205095b6cb15f62a3e799e3398ebefa0d1c265fedc18a83b0e24f6ae077"
370
+ },
371
+ "validation/agent-terminal.json": {
372
+ "bytes": 2874,
373
+ "sha256": "1f0bf618a027c1a5252c247f8a82da1d9ca1b10cb8e50575994effe4c0245e37"
374
+ },
375
  "validation/benchmark-summary.json": {
376
  "bytes": 1500,
377
  "sha256": "5bc51fbdbc43b58caa2036b621ded2bd3b4c1c95ba487b07bb5b84f19e97ebc0"
index.html CHANGED
@@ -6,8 +6,8 @@
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-LA8xezhP.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-Cfkr7NLu.css">
11
  </head>
12
  <body>
13
  <header><a class="brand" href="./"><span class="pi">Ο€</span><span>Pi <small>browser workspace</small></span></a>
@@ -22,17 +22,10 @@
22
  <div id="load-detail" class="muted"></div>
23
  <a id="open-tab" class="button" target="_blank" rel="noopener noreferrer" hidden>Open workspace in a new tab β†—</a>
24
  </div>
25
- <div id="messages" role="log" aria-label="Messages" aria-live="polite">
26
- <div id="welcome"><div class="eyebrow">A small agent. A real workspace.</div><h1>What shall we work on?</h1>
27
- <p>Chat with Pi, work with files, and run shell commands.<br>The model and every tool run here, in your browser.</p>
28
- <div class="suggestions"><button data-prompt="Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file.">Analyze the sample data <span>β†—</span></button><button data-prompt="Create a bash script called hello.sh that prints Hello from the browser, run it, and report the output.">Write and run a script <span>β†—</span></button></div>
29
- </div>
30
- </div>
31
  <div id="notice" role="status" hidden></div>
32
- <form id="chat-form"><label class="sr-only" for="prompt">Message Pi</label><textarea id="prompt" rows="3" placeholder="Ask Pi to work on something…"></textarea>
33
- <div class="composer-footer"><span id="activity">Files and chat are saved on this device.</span><button id="stop" type="button" hidden>Stop</button><button id="send" class="primary" disabled>Send ↑</button></div>
34
- </form>
35
- <footer>Pi agent core Β· just-bash Β· Transformers.js <span>4,096 token context</span></footer>
36
  </section>
37
  <aside class="workspace" aria-label="Workspace">
38
  <div class="workspace-heading"><h2>Workspace</h2><div><button id="add-file" class="quiet">+ File</button><label class="button quiet" for="upload">Import</label><input id="upload" type="file" multiple hidden><button id="export" class="quiet">Export</button></div></div>
 
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-BRKUEVCq.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-DL-Ot0hN.css">
11
  </head>
12
  <body>
13
  <header><a class="brand" href="./"><span class="pi">Ο€</span><span>Pi <small>browser workspace</small></span></a>
 
22
  <div id="load-detail" class="muted"></div>
23
  <a id="open-tab" class="button" target="_blank" rel="noopener noreferrer" hidden>Open workspace in a new tab β†—</a>
24
  </div>
25
+ <div id="pi-terminal" aria-label="Pi terminal"></div>
 
 
 
 
 
26
  <div id="notice" role="status" hidden></div>
27
+ <div class="terminal-controls"><span id="activity" role="status">Files and chat are saved on this device.</span><button id="newline" type="button" title="Insert a newline (Shift+Enter)">↡ Newline</button><button id="stop" type="button" hidden>Stop</button><button id="send" class="primary">Send ↑</button></div>
28
+ <footer>Pi TUI Β· xterm.js <span>Model and tools run in your browser</span></footer>
 
 
29
  </section>
30
  <aside class="workspace" aria-label="Workspace">
31
  <div class="workspace-heading"><h2>Workspace</h2><div><button id="add-file" class="quiet">+ File</button><label class="button quiet" for="upload">Import</label><input id="upload" type="file" multiple hidden><button id="export" class="quiet">Export</button></div></div>
licenses/dependencies.txt CHANGED
@@ -4375,6 +4375,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
4375
  @earendil-works/pi-telemetry@0.85.1 (MIT)
4376
 
4377
 
 
 
 
 
 
4378
  ----------------------------------------
4379
 
4380
  @esbuild/linux-x64@0.28.1 (MIT)
@@ -8088,6 +8093,56 @@ The above copyright notice and this permission notice shall be included in all c
8088
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8089
 
8090
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8091
  ----------------------------------------
8092
 
8093
  adm-zip@0.5.18 (MIT)
@@ -8441,7 +8496,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
8441
 
8442
  ----------------------------------------
8443
 
8444
- buffer@5.7.1 (MIT)
8445
  The MIT License (MIT)
8446
 
8447
  Copyright (c) Feross Aboukhadijeh, and other contributors.
@@ -9263,6 +9318,33 @@ The above copyright notice and this permission notice shall be included in all c
9263
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9264
 
9265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9266
  ----------------------------------------
9267
 
9268
  expand-template@2.0.3 ((MIT OR WTFPL))
@@ -10122,6 +10204,20 @@ gcp-metadata@8.1.2 (Apache-2.0)
10122
  limitations under the License.
10123
 
10124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10125
  ----------------------------------------
10126
 
10127
  github-from-package@0.0.0 (MIT)
@@ -12213,6 +12309,55 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
12213
  OTHER DEALINGS IN THE SOFTWARE.
12214
 
12215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12216
  ----------------------------------------
12217
 
12218
  matcher@3.0.0 (MIT)
@@ -12596,6 +12741,202 @@ SOFTWARE.
12596
 
12597
 
12598
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12599
  ----------------------------------------
12600
 
12601
  object-keys@1.1.1 (MIT)
 
4375
  @earendil-works/pi-telemetry@0.85.1 (MIT)
4376
 
4377
 
4378
+ ----------------------------------------
4379
+
4380
+ @earendil-works/pi-tui@0.85.1 (MIT)
4381
+
4382
+
4383
  ----------------------------------------
4384
 
4385
  @esbuild/linux-x64@0.28.1 (MIT)
 
8093
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8094
 
8095
 
8096
+ ----------------------------------------
8097
+
8098
+ @xterm/addon-fit@0.11.0 (MIT)
8099
+ Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
8100
+
8101
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8102
+ of this software and associated documentation files (the "Software"), to deal
8103
+ in the Software without restriction, including without limitation the rights
8104
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8105
+ copies of the Software, and to permit persons to whom the Software is
8106
+ furnished to do so, subject to the following conditions:
8107
+
8108
+ The above copyright notice and this permission notice shall be included in
8109
+ all copies or substantial portions of the Software.
8110
+
8111
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
8112
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
8113
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
8114
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
8115
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
8116
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
8117
+ THE SOFTWARE.
8118
+
8119
+
8120
+ ----------------------------------------
8121
+
8122
+ @xterm/xterm@6.0.0 (MIT)
8123
+ Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
8124
+ Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
8125
+ Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
8126
+
8127
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8128
+ of this software and associated documentation files (the "Software"), to deal
8129
+ in the Software without restriction, including without limitation the rights
8130
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8131
+ copies of the Software, and to permit persons to whom the Software is
8132
+ furnished to do so, subject to the following conditions:
8133
+
8134
+ The above copyright notice and this permission notice shall be included in
8135
+ all copies or substantial portions of the Software.
8136
+
8137
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
8138
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
8139
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
8140
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
8141
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
8142
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
8143
+ THE SOFTWARE.
8144
+
8145
+
8146
  ----------------------------------------
8147
 
8148
  adm-zip@0.5.18 (MIT)
 
8496
 
8497
  ----------------------------------------
8498
 
8499
+ buffer@6.0.3 (MIT)
8500
  The MIT License (MIT)
8501
 
8502
  Copyright (c) Feross Aboukhadijeh, and other contributors.
 
9318
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9319
 
9320
 
9321
+ ----------------------------------------
9322
+
9323
+ events@3.3.0 (MIT)
9324
+ MIT
9325
+
9326
+ Copyright Joyent, Inc. and other Node contributors.
9327
+
9328
+ Permission is hereby granted, free of charge, to any person obtaining a
9329
+ copy of this software and associated documentation files (the
9330
+ "Software"), to deal in the Software without restriction, including
9331
+ without limitation the rights to use, copy, modify, merge, publish,
9332
+ distribute, sublicense, and/or sell copies of the Software, and to permit
9333
+ persons to whom the Software is furnished to do so, subject to the
9334
+ following conditions:
9335
+
9336
+ The above copyright notice and this permission notice shall be included
9337
+ in all copies or substantial portions of the Software.
9338
+
9339
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
9340
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
9341
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
9342
+ NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
9343
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
9344
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
9345
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
9346
+
9347
+
9348
  ----------------------------------------
9349
 
9350
  expand-template@2.0.3 ((MIT OR WTFPL))
 
10204
  limitations under the License.
10205
 
10206
 
10207
+ ----------------------------------------
10208
+
10209
+ get-east-asian-width@1.6.0 (MIT)
10210
+ MIT License
10211
+
10212
+ Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
10213
+
10214
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10215
+
10216
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10217
+
10218
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10219
+
10220
+
10221
  ----------------------------------------
10222
 
10223
  github-from-package@0.0.0 (MIT)
 
12309
  OTHER DEALINGS IN THE SOFTWARE.
12310
 
12311
 
12312
+ ----------------------------------------
12313
+
12314
+ marked@18.0.5 (MIT)
12315
+ # License information
12316
+
12317
+ ## Contribution License Agreement
12318
+
12319
+ If you contribute code to this project, you are implicitly allowing your code
12320
+ to be distributed under the MIT license. You are also implicitly verifying that
12321
+ all code is your original work. `</legalese>`
12322
+
12323
+ ## Marked
12324
+
12325
+ Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/)
12326
+ Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
12327
+
12328
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12329
+ of this software and associated documentation files (the "Software"), to deal
12330
+ in the Software without restriction, including without limitation the rights
12331
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12332
+ copies of the Software, and to permit persons to whom the Software is
12333
+ furnished to do so, subject to the following conditions:
12334
+
12335
+ The above copyright notice and this permission notice shall be included in
12336
+ all copies or substantial portions of the Software.
12337
+
12338
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12339
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
12340
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
12341
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
12342
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
12343
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
12344
+ THE SOFTWARE.
12345
+
12346
+ ## Markdown
12347
+
12348
+ Copyright Β© 2004, John Gruber
12349
+ http://daringfireball.net/
12350
+ All rights reserved.
12351
+
12352
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
12353
+
12354
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
12355
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
12356
+ * Neither the name β€œMarkdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
12357
+
12358
+ This software is provided by the copyright holders and contributors β€œas is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
12359
+
12360
+
12361
  ----------------------------------------
12362
 
12363
  matcher@3.0.0 (MIT)
 
12741
 
12742
 
12743
 
12744
+ ----------------------------------------
12745
+
12746
+ node-gyp-build@4.8.4 (MIT)
12747
+ The MIT License (MIT)
12748
+
12749
+ Copyright (c) 2017 Mathias Buus
12750
+
12751
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12752
+ of this software and associated documentation files (the "Software"), to deal
12753
+ in the Software without restriction, including without limitation the rights
12754
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12755
+ copies of the Software, and to permit persons to whom the Software is
12756
+ furnished to do so, subject to the following conditions:
12757
+
12758
+ The above copyright notice and this permission notice shall be included in
12759
+ all copies or substantial portions of the Software.
12760
+
12761
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12762
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
12763
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
12764
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
12765
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
12766
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
12767
+ THE SOFTWARE.
12768
+
12769
+
12770
+ ----------------------------------------
12771
+
12772
+ node-liblzma@2.2.0 (LGPL-3.0)
12773
+ GNU LESSER GENERAL PUBLIC LICENSE
12774
+ Version 3, 29 June 2007
12775
+
12776
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
12777
+ Everyone is permitted to copy and distribute verbatim copies
12778
+ of this license document, but changing it is not allowed.
12779
+
12780
+
12781
+ This version of the GNU Lesser General Public License incorporates
12782
+ the terms and conditions of version 3 of the GNU General Public
12783
+ License, supplemented by the additional permissions listed below.
12784
+
12785
+ 0. Additional Definitions.
12786
+
12787
+ As used herein, "this License" refers to version 3 of the GNU Lesser
12788
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
12789
+ General Public License.
12790
+
12791
+ "The Library" refers to a covered work governed by this License,
12792
+ other than an Application or a Combined Work as defined below.
12793
+
12794
+ An "Application" is any work that makes use of an interface provided
12795
+ by the Library, but which is not otherwise based on the Library.
12796
+ Defining a subclass of a class defined by the Library is deemed a mode
12797
+ of using an interface provided by the Library.
12798
+
12799
+ A "Combined Work" is a work produced by combining or linking an
12800
+ Application with the Library. The particular version of the Library
12801
+ with which the Combined Work was made is also called the "Linked
12802
+ Version".
12803
+
12804
+ The "Minimal Corresponding Source" for a Combined Work means the
12805
+ Corresponding Source for the Combined Work, excluding any source code
12806
+ for portions of the Combined Work that, considered in isolation, are
12807
+ based on the Application, and not on the Linked Version.
12808
+
12809
+ The "Corresponding Application Code" for a Combined Work means the
12810
+ object code and/or source code for the Application, including any data
12811
+ and utility programs needed for reproducing the Combined Work from the
12812
+ Application, but excluding the System Libraries of the Combined Work.
12813
+
12814
+ 1. Exception to Section 3 of the GNU GPL.
12815
+
12816
+ You may convey a covered work under sections 3 and 4 of this License
12817
+ without being bound by section 3 of the GNU GPL.
12818
+
12819
+ 2. Conveying Modified Versions.
12820
+
12821
+ If you modify a copy of the Library, and, in your modifications, a
12822
+ facility refers to a function or data to be supplied by an Application
12823
+ that uses the facility (other than as an argument passed when the
12824
+ facility is invoked), then you may convey a copy of the modified
12825
+ version:
12826
+
12827
+ a) under this License, provided that you make a good faith effort to
12828
+ ensure that, in the event an Application does not supply the
12829
+ function or data, the facility still operates, and performs
12830
+ whatever part of its purpose remains meaningful, or
12831
+
12832
+ b) under the GNU GPL, with none of the additional permissions of
12833
+ this License applicable to that copy.
12834
+
12835
+ 3. Object Code Incorporating Material from Library Header Files.
12836
+
12837
+ The object code form of an Application may incorporate material from
12838
+ a header file that is part of the Library. You may convey such object
12839
+ code under terms of your choice, provided that, if the incorporated
12840
+ material is not limited to numerical parameters, data structure
12841
+ layouts and accessors, or small macros, inline functions and templates
12842
+ (ten or fewer lines in length), you do both of the following:
12843
+
12844
+ a) Give prominent notice with each copy of the object code that the
12845
+ Library is used in it and that the Library and its use are
12846
+ covered by this License.
12847
+
12848
+ b) Accompany the object code with a copy of the GNU GPL and this license
12849
+ document.
12850
+
12851
+ 4. Combined Works.
12852
+
12853
+ You may convey a Combined Work under terms of your choice that,
12854
+ taken together, effectively do not restrict modification of the
12855
+ portions of the Library contained in the Combined Work and reverse
12856
+ engineering for debugging such modifications, if you also do each of
12857
+ the following:
12858
+
12859
+ a) Give prominent notice with each copy of the Combined Work that
12860
+ the Library is used in it and that the Library and its use are
12861
+ covered by this License.
12862
+
12863
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
12864
+ document.
12865
+
12866
+ c) For a Combined Work that displays copyright notices during
12867
+ execution, include the copyright notice for the Library among
12868
+ these notices, as well as a reference directing the user to the
12869
+ copies of the GNU GPL and this license document.
12870
+
12871
+ d) Do one of the following:
12872
+
12873
+ 0) Convey the Minimal Corresponding Source under the terms of this
12874
+ License, and the Corresponding Application Code in a form
12875
+ suitable for, and under terms that permit, the user to
12876
+ recombine or relink the Application with a modified version of
12877
+ the Linked Version to produce a modified Combined Work, in the
12878
+ manner specified by section 6 of the GNU GPL for conveying
12879
+ Corresponding Source.
12880
+
12881
+ 1) Use a suitable shared library mechanism for linking with the
12882
+ Library. A suitable mechanism is one that (a) uses at run time
12883
+ a copy of the Library already present on the user's computer
12884
+ system, and (b) will operate properly with a modified version
12885
+ of the Library that is interface-compatible with the Linked
12886
+ Version.
12887
+
12888
+ e) Provide Installation Information, but only if you would otherwise
12889
+ be required to provide such information under section 6 of the
12890
+ GNU GPL, and only to the extent that such information is
12891
+ necessary to install and execute a modified version of the
12892
+ Combined Work produced by recombining or relinking the
12893
+ Application with a modified version of the Linked Version. (If
12894
+ you use option 4d0, the Installation Information must accompany
12895
+ the Minimal Corresponding Source and Corresponding Application
12896
+ Code. If you use option 4d1, you must provide the Installation
12897
+ Information in the manner specified by section 6 of the GNU GPL
12898
+ for conveying Corresponding Source.)
12899
+
12900
+ 5. Combined Libraries.
12901
+
12902
+ You may place library facilities that are a work based on the
12903
+ Library side by side in a single library together with other library
12904
+ facilities that are not Applications and are not covered by this
12905
+ License, and convey such a combined library under terms of your
12906
+ choice, if you do both of the following:
12907
+
12908
+ a) Accompany the combined library with a copy of the same work based
12909
+ on the Library, uncombined with any other library facilities,
12910
+ conveyed under the terms of this License.
12911
+
12912
+ b) Give prominent notice with the combined library that part of it
12913
+ is a work based on the Library, and explaining where to find the
12914
+ accompanying uncombined form of the same work.
12915
+
12916
+ 6. Revised Versions of the GNU Lesser General Public License.
12917
+
12918
+ The Free Software Foundation may publish revised and/or new versions
12919
+ of the GNU Lesser General Public License from time to time. Such new
12920
+ versions will be similar in spirit to the present version, but may
12921
+ differ in detail to address new problems or concerns.
12922
+
12923
+ Each version is given a distinguishing version number. If the
12924
+ Library as you received it specifies that a certain numbered version
12925
+ of the GNU Lesser General Public License "or any later version"
12926
+ applies to it, you have the option of following the terms and
12927
+ conditions either of that published version or of any later version
12928
+ published by the Free Software Foundation. If the Library as you
12929
+ received it does not specify a version number of the GNU Lesser
12930
+ General Public License, you may choose any version of the GNU Lesser
12931
+ General Public License ever published by the Free Software Foundation.
12932
+
12933
+ If the Library as you received it specifies that a proxy can decide
12934
+ whether future versions of the GNU Lesser General Public License shall
12935
+ apply, that proxy's public statement of acceptance of any version is
12936
+ permanent authorization for you to choose that version for the
12937
+ Library.
12938
+
12939
+
12940
  ----------------------------------------
12941
 
12942
  object-keys@1.1.1 (MIT)
licenses/pi-components.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "package": "@earendil-works/pi-coding-agent",
3
+ "version": "0.85.1",
4
+ "source": "https://github.com/earendil-works/pi/tree/v0.85.1/packages/coding-agent",
5
+ "files": {
6
+ "components/custom-editor.js": "000b0056f2bf645537537468bdd6f6ee0c82e4110f5417c1c70c2e8fefc04f29",
7
+ "components/user-message.js": "398a34b8a979daea5614be4cbbe6ff66e7eca97e9fd855508791dcddce994bc8",
8
+ "components/assistant-message.js": "edbd1e712234609d2ad1078edcf960e3b6f549669075f1c5d1a7a4481e2b3d19",
9
+ "components/markdown-transform.js": "31ad2245189328104a31bb9b12f58b0a12e4af9c7924775a242c9dcd526d2c1a",
10
+ "theme/light.json": "14c7172ba7e75eab6f509504de806af0f2d9ff817bfb45d9533bff15c7e6e657"
11
+ }
12
+ }
research/browser-agent/REPORT.md CHANGED
@@ -28,7 +28,7 @@ The three README snapshots in this directory are upstream documentation fetched
28
 
29
  ## Current validation artifacts
30
 
31
- - `validation/agent-browser.json`: initial actual-model integration and reload evidence.
32
  - `validation/agent-browser-hub.json`: public model download, interrupted-load retry, actual offline agent workflows, editing, tool-error handling, and persistence.
33
  - `validation/agent-browser-limit128.json`: the same runtime under smaller GPU buffer limits.
34
  - `app/tests/protocol.test.mjs`: XML/CDATAs, malformed call rejection, history/context conversion, real just-bash commands, lossless workspace restoration, and inert Markdown rendering.
@@ -52,3 +52,22 @@ The browser workspace is published to [Mike0021/MiniCPM5-2B-WebGPU](https://hugg
52
  The embedded page initially exposed denied OPFS, IndexedDB, and Web Locks access. Inspection identified Playwright 1.58's default `--disable-features` list as explicitly disabling `ThirdPartyStoragePartitioning`. Restoring only normal Chromium storage partitioning allowed the real Hugging Face iframe to load the model and complete the CSV/report task through its actual chat controls. The app also provides a direct new-tab link when a browser does deny embedded storage. This does not change browser privacy settings or grant storage permission automatically. Hugging Face documents the cross-domain iframe structure and related restrictions in its [cookie limitations guide](https://huggingface.co/docs/hub/spaces-cookie-limitations).
53
 
54
  `validation/agent-space-deployment.json`, `agent-space-embedded.json`, and `agent-space-checksums.json` contain the deployed results. The restricted-storage/new-tab path has a separate browser test in `app/tests/space-restricted.mjs`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  ## Current validation artifacts
30
 
31
+ - `validation/agent-browser.json`: latest actual-model integration and reload evidence (also saved as `agent-terminal-model.json` for the terminal integration).
32
  - `validation/agent-browser-hub.json`: public model download, interrupted-load retry, actual offline agent workflows, editing, tool-error handling, and persistence.
33
  - `validation/agent-browser-limit128.json`: the same runtime under smaller GPU buffer limits.
34
  - `app/tests/protocol.test.mjs`: XML/CDATAs, malformed call rejection, history/context conversion, real just-bash commands, lossless workspace restoration, and inert Markdown rendering.
 
52
  The embedded page initially exposed denied OPFS, IndexedDB, and Web Locks access. Inspection identified Playwright 1.58's default `--disable-features` list as explicitly disabling `ThirdPartyStoragePartitioning`. Restoring only normal Chromium storage partitioning allowed the real Hugging Face iframe to load the model and complete the CSV/report task through its actual chat controls. The app also provides a direct new-tab link when a browser does deny embedded storage. This does not change browser privacy settings or grant storage permission automatically. Hugging Face documents the cross-domain iframe structure and related restrictions in its [cookie limitations guide](https://huggingface.co/docs/hub/spaces-cookie-limitations).
53
 
54
  `validation/agent-space-deployment.json`, `agent-space-embedded.json`, and `agent-space-checksums.json` contain the deployed results. The restricted-storage/new-tab path has a separate browser test in `app/tests/space-restricted.mjs`.
55
+
56
+
57
+ ## Pi terminal UI in xterm.js β€” 2026-09-12
58
+
59
+ The left column now uses Pi's original terminal UI through xterm.js. The user chose to keep all execution in the browser. This is a browser host for Pi's presentation components and agent core; it does not launch the full Node CLI.
60
+
61
+ Pi's published `Terminal` interface accepts input, output, resize, and lifecycle operations, making xterm.js a suitable replacement for `ProcessTerminal`. The browser imports `TuiAltScreen`, `Editor`, Markdown, containers, scroll/search handling, keybindings, and `StdinBuffer` from `@earendil-works/pi-tui@0.85.1`. It also uses the unmodified CLI `CustomEditor`, `UserMessageComponent`, and `AssistantMessageComponent` copied from the matching npm tarball, with SHA-256 provenance and MIT licensing. [Pi TUI documentation](https://github.com/earendil-works/pi/tree/v0.85.1/packages/tui), [CLI presentation source](https://github.com/earendil-works/pi/tree/v0.85.1/packages/coding-agent/src/modes/interactive/components), [xterm fit addon documentation](https://xtermjs.org/docs/guides/using-addons/).
62
+
63
+ The original CLI entry point depends on Node filesystem/process services and native terminal discovery. Browser-specific host adapters instead supply the terminal, scheduling, Buffer/EventEmitter, clipboard, text-only terminal capabilities, and the upstream light theme without its file watcher. The build selects only the required TUI exports, leaving Node startup and provider/extension management out. Every vendored component is verified against its recorded upstream checksum when Vite loads its config. There are no edits to installed or vendored Pi source files.
64
+
65
+ App code connects Pi's editor to the existing worker and implements `/help`, `/load`, `/new`, `/clear`, `!command`, virtual-file completion, and just-bash tool cards. The worker bundle remains `agent.worker-DqQzo9yL.js`: model inference, quantization, downloads, and agent tool execution are unchanged. The terminal uses xterm's default renderer, without a second WebGPU/WebGL rendering context.
66
+
67
+ The browser adapter preserves native clipboard paste and sends Shift+Enter as Alt+Enter. It translates Ctrl+Shift+F into CSI-u because legacy terminal input otherwise loses that modifier distinction. Large Pi paste markers are expanded before the Send button submits text. Text from tools, files, and model messages is stripped of terminal control sequences before presentation; only user-clicked HTTP(S) Markdown links may open a tab. Images do not download.
68
+
69
+ `validation/agent-terminal.json` and `agent-terminal-dev.json` record the production/development keyboard checks: multiline editing, cursor/deletion operations, prompt history, slash completion, shell file writes, escape-sequence stripping, Unicode clipboard paste, large-paste expansion, and live resizing from 90 to 40 columns while preserving a draft. A dedicated inner mount keeps xterm's fit calculation inside the panel's padding. Development mode explicitly bundles the CommonJS EventEmitter/Buffer dependencies and adapts versioned module URLs.
70
+
71
+ `validation/agent-terminal-model.json` records five real MiniCPM workflows submitted through xterm keyboard input and Pi's editor: unseen-file read, CSV calculation/report, script creation/execution, edit/verification, and recovery from a missing-file error. All passed. Escape stopped generation; editor/import/manual shell, persistence, and cache reload passed. Verified cached model setup took 2.281 seconds and the subsequent reload 2.122 seconds in this run. The five workflows made zero network requests with networking disabled. Reload fetched zero weight shards. These are integration observations on the desktop AMD GPU, not a new quality or throughput benchmark.
72
+
73
+ An early automation attempt used CDP bulk `insertText`, which did not traverse xterm's normal keyboard input path and left the test waiting for a draft. The final model suite uses actual keyboard events. Physical Android/iOS input methods, mobile GPU execution, and embedded hosting of this new terminal build have not been verified. The previous deployed Space evidence remains evidence for the earlier chat UI.
scripts/prepare_space.py CHANGED
@@ -23,7 +23,7 @@ app_out = out / 'app'
23
  app_out.mkdir()
24
  for name in ['package.json', 'package-lock.json', 'index.html', 'vite.config.mjs', 'README.md']:
25
  shutil.copyfile(root / 'app' / name, app_out / name)
26
- for name in ['src', 'scripts', 'public', 'tests']:
27
  shutil.copytree(root / 'app' / name, app_out / name)
28
 
29
  validation = out / 'validation'
@@ -37,6 +37,7 @@ for name in [
37
  'agent-browser-no-gpu.json', 'agent-completion.json',
38
  'agent-space-deployment.json', 'agent-space-embedded.json', 'agent-space-restricted.json',
39
  'agent-space-checksums.json',
 
40
  ]:
41
  source = root / 'validation' / name
42
  if source.is_file():
 
23
  app_out.mkdir()
24
  for name in ['package.json', 'package-lock.json', 'index.html', 'vite.config.mjs', 'README.md']:
25
  shutil.copyfile(root / 'app' / name, app_out / name)
26
+ for name in ['src', 'scripts', 'public', 'tests', 'vendor']:
27
  shutil.copytree(root / 'app' / name, app_out / name)
28
 
29
  validation = out / 'validation'
 
37
  'agent-browser-no-gpu.json', 'agent-completion.json',
38
  'agent-space-deployment.json', 'agent-space-embedded.json', 'agent-space-restricted.json',
39
  'agent-space-checksums.json',
40
+ 'agent-terminal.json', 'agent-terminal-model.json', 'agent-terminal-dev.json',
41
  ]:
42
  source = root / 'validation' / name
43
  if source.is_file():
validation/agent-terminal-dev.json ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "date": "2026-09-12T08:25:07.051Z",
3
+ "url": "http://127.0.0.1:5173/",
4
+ "errors": [],
5
+ "requests": [
6
+ "http://127.0.0.1:5173/",
7
+ "http://127.0.0.1:5173/@vite/client",
8
+ "http://127.0.0.1:5173/src/main.mjs?t=1789201506558",
9
+ "http://127.0.0.1:5173/node_modules/vite/dist/client/env.mjs",
10
+ "http://127.0.0.1:5173/src/style.css",
11
+ "http://127.0.0.1:5173/src/pi/terminal-ui.mjs?t=1789201506558",
12
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@xterm_xterm.js?v=55d56926",
13
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@xterm_addon-fit.js?v=55d56926",
14
+ "http://127.0.0.1:5173/node_modules/@xterm/xterm/css/xterm.css",
15
+ "http://127.0.0.1:5173/src/pi/entry.mjs",
16
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/components/custom-editor.js",
17
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/components/user-message.js",
18
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/components/assistant-message.js",
19
+ "http://127.0.0.1:5173/src/pi/theme.mjs",
20
+ "http://127.0.0.1:5173/src/pi/xterm-terminal.mjs",
21
+ "http://127.0.0.1:5173/src/protocol.mjs",
22
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/components/markdown-transform.js",
23
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js?v=55d56926",
24
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/tui.js?v=55d56926",
25
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/editor.js?v=55d56926",
26
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/text.js?v=55d56926",
27
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.js?v=55d56926",
28
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/markdown.js?v=55d56926",
29
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/box.js?v=55d56926",
30
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/spacer.js?v=55d56926",
31
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/mouse-region.js?v=55d56926",
32
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/scroll-view.js?v=55d56926",
33
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/v-stack.js?v=55d56926",
34
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/keybindings.js?v=55d56926",
35
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/keys.js?v=55d56926",
36
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js?v=55d56926",
37
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/utils.js?v=55d56926",
38
+ "http://127.0.0.1:5173/",
39
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/theme/light.json?import",
40
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@xmldom_xmldom.js?v=55d56926",
41
+ "http://127.0.0.1:5173/src/pi/browser-host.mjs",
42
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/terminal-colors.js?v=55d56926",
43
+ "http://127.0.0.1:5173/src/pi/capabilities.mjs",
44
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/alt-screen-search.js?v=55d56926",
45
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/alt-screen-flash.js?v=55d56926",
46
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/layout.js?v=55d56926",
47
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/layout-node.js?v=55d56926",
48
+ "http://127.0.0.1:5173/node_modules/marked/lib/marked.esm.js?v=55d56926",
49
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/latex.js?v=55d56926",
50
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/kill-ring.js?v=55d56926",
51
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/undo-stack.js?v=55d56926",
52
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/word-navigation.js?v=55d56926",
53
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/select-list.js?v=55d56926",
54
+ "http://127.0.0.1:5173/@vite/client",
55
+ "http://127.0.0.1:5173/src/main.mjs?t=1789201506558",
56
+ "http://127.0.0.1:5173/node_modules/vite/dist/client/env.mjs",
57
+ "http://127.0.0.1:5173/src/style.css",
58
+ "http://127.0.0.1:5173/src/pi/terminal-ui.mjs?t=1789201506558",
59
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@xterm_xterm.js?v=55d56926",
60
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@xterm_addon-fit.js?v=55d56926",
61
+ "http://127.0.0.1:5173/node_modules/@xterm/xterm/css/xterm.css",
62
+ "http://127.0.0.1:5173/src/pi/entry.mjs",
63
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/components/custom-editor.js",
64
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/components/user-message.js",
65
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/components/assistant-message.js",
66
+ "http://127.0.0.1:5173/src/pi/theme.mjs",
67
+ "http://127.0.0.1:5173/src/pi/xterm-terminal.mjs",
68
+ "http://127.0.0.1:5173/src/protocol.mjs",
69
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js?v=55d56926",
70
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/tui.js?v=55d56926",
71
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/editor.js?v=55d56926",
72
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/text.js?v=55d56926",
73
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.js?v=55d56926",
74
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/markdown.js?v=55d56926",
75
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/box.js?v=55d56926",
76
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/spacer.js?v=55d56926",
77
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/mouse-region.js?v=55d56926",
78
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/scroll-view.js?v=55d56926",
79
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/v-stack.js?v=55d56926",
80
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/keybindings.js?v=55d56926",
81
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/keys.js?v=55d56926",
82
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js?v=55d56926",
83
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/utils.js?v=55d56926",
84
+ "http://127.0.0.1:5173/src/pi/browser-host.mjs",
85
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/alt-screen-search.js?v=55d56926",
86
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/alt-screen-flash.js?v=55d56926",
87
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/layout.js?v=55d56926",
88
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/layout-node.js?v=55d56926",
89
+ "http://127.0.0.1:5173/src/pi/capabilities.mjs",
90
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/terminal-colors.js?v=55d56926",
91
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/kill-ring.js?v=55d56926",
92
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/undo-stack.js?v=55d56926",
93
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/word-navigation.js?v=55d56926",
94
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/select-list.js?v=55d56926",
95
+ "http://127.0.0.1:5173/node_modules/marked/lib/marked.esm.js?v=55d56926",
96
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/latex.js?v=55d56926",
97
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/components/markdown-transform.js",
98
+ "http://127.0.0.1:5173/vendor/pi-coding-agent/theme/light.json?import",
99
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@xmldom_xmldom.js?v=55d56926",
100
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/stack.js?v=55d56926",
101
+ "http://127.0.0.1:5173/node_modules/.vite/deps/events.js?v=55d56926",
102
+ "http://127.0.0.1:5173/node_modules/.vite/deps/buffer.js?v=55d56926",
103
+ "http://127.0.0.1:5173/node_modules/@earendil-works/pi-tui/dist/components/input.js?v=55d56926",
104
+ "http://127.0.0.1:5173/node_modules/get-east-asian-width/index.js?v=55d56926",
105
+ "http://127.0.0.1:5173/node_modules/.vite/deps/rolldown-runtime-B-lAHAz2.js?v=55d56926",
106
+ "http://127.0.0.1:5173/node_modules/get-east-asian-width/lookup.js?v=55d56926",
107
+ "http://127.0.0.1:5173/node_modules/get-east-asian-width/lookup-data.js?v=55d56926",
108
+ "http://127.0.0.1:5173/node_modules/get-east-asian-width/utilities.js?v=55d56926",
109
+ "http://127.0.0.1:5173/src/agent.worker.mjs?worker_file&type=module",
110
+ "http://127.0.0.1:5173/node_modules/vite/dist/client/env.mjs",
111
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@earendil-works_pi-agent-core.js?v=55d56926",
112
+ "http://127.0.0.1:5173/src/inference.mjs",
113
+ "http://127.0.0.1:5173/src/workspace.mjs",
114
+ "http://127.0.0.1:5173/src/storage.mjs",
115
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@huggingface_transformers.js?v=55d56926",
116
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@earendil-works_pi-ai_utils_event-stream.js?v=55d56926",
117
+ "http://127.0.0.1:5173/src/download.mjs",
118
+ "http://127.0.0.1:5173/src/protocol.mjs",
119
+ "http://127.0.0.1:5173/node_modules/.vite/deps/just-bash_browser.js?v=55d56926",
120
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@earendil-works_pi-ai.js?v=55d56926",
121
+ "http://127.0.0.1:5173/favicon.svg",
122
+ "http://127.0.0.1:5173/node_modules/.vite/deps/rolldown-runtime-B-lAHAz2.js?v=55d56926",
123
+ "http://127.0.0.1:5173/node_modules/.vite/deps/dist-HiCO7I8Y.js?v=55d56926",
124
+ "http://127.0.0.1:5173/node_modules/.vite/deps/libesm-Ds0pOwVw.js?v=55d56926",
125
+ "http://127.0.0.1:5173/node_modules/.vite/deps/hash-wasm.js?v=55d56926",
126
+ "http://127.0.0.1:5173/src/model-manifest.json?import",
127
+ "http://127.0.0.1:5173/node_modules/.vite/deps/@xmldom_xmldom.js?v=55d56926",
128
+ "http://127.0.0.1:5173/@vite/client"
129
+ ],
130
+ "checks": [
131
+ "Original Pi TUI rendered through xterm",
132
+ "Enter submits /help without loading a model",
133
+ "Pi fullscreen transcript search preserves the editor draft",
134
+ "Pi multiline editor: Shift+Enter, cursor, deletion, Ctrl+U/K",
135
+ "Pi prompt history",
136
+ "Pi autocomplete: /he β†’ /help",
137
+ "xterm keyboard β†’ Pi editor β†’ real just-bash β†’ workspace file",
138
+ "Shell escape sequences render as inert text",
139
+ "Large Pi paste markers expand before Send",
140
+ "Unicode multiline clipboard paste",
141
+ "Touch-accessible newline button",
142
+ "390px layout and live terminal resize preserve the draft"
143
+ ],
144
+ "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 ",
145
+ "sizes": {
146
+ "desktop": {
147
+ "cols": 90,
148
+ "rows": 41
149
+ },
150
+ "mobile": {
151
+ "cols": 40,
152
+ "rows": 32
153
+ }
154
+ },
155
+ "mobileLimitation": "Desktop Chromium viewport simulation; physical mobile keyboards and GPUs not tested.",
156
+ "passed": true
157
+ }
validation/agent-terminal-model.json ADDED
@@ -0,0 +1,1885 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "date": "2026-09-12T08:21:57.234Z",
3
+ "url": "http://127.0.0.1:4173/?source=local",
4
+ "cases": [
5
+ {
6
+ "name": "read-unseen-file",
7
+ "elapsedMs": 2233,
8
+ "version": 1,
9
+ "files": {
10
+ "/tmp/revenue.txt": "Notebook 12 5 60\nPencil 30 2 60\nFolder 8 3 24\nTOTAL: 144\n",
11
+ "/workspace/README.md": "# Browser workspace\n\nPi can read, write, edit, and run shell commands here.\nEverything stays in this browser. Try analyzing sales.csv.\n",
12
+ "/workspace/revenue.txt": "Notebook 12 5 -> 12 x 5 = 60\nPencil 30 2 -> 30 x 2 = 60\nFolder 8 3 -> 8 x 3 = 24\nTOTAL REVENUE: 144\n",
13
+ "/workspace/sales.csv": "product,units,price\nNotebook,12,5\nPencil,30,2\nFolder,8,3\n",
14
+ "/workspace/secret.txt": "LOCAL-fd008be1"
15
+ },
16
+ "entries": [
17
+ [
18
+ "/",
19
+ {
20
+ "type": "directory",
21
+ "mode": 493
22
+ }
23
+ ],
24
+ [
25
+ "/tmp",
26
+ {
27
+ "type": "directory",
28
+ "mode": 493
29
+ }
30
+ ],
31
+ [
32
+ "/tmp/revenue.txt",
33
+ {
34
+ "type": "file",
35
+ "base64": "Tm90ZWJvb2sgMTIgNSA2MApQZW5jaWwgMzAgMiA2MApGb2xkZXIgOCAzIDI0ClRPVEFMOiAxNDQK",
36
+ "mode": 420
37
+ }
38
+ ],
39
+ [
40
+ "/usr",
41
+ {
42
+ "type": "directory",
43
+ "mode": 493
44
+ }
45
+ ],
46
+ [
47
+ "/workspace",
48
+ {
49
+ "type": "directory",
50
+ "mode": 493
51
+ }
52
+ ],
53
+ [
54
+ "/workspace/README.md",
55
+ {
56
+ "type": "file",
57
+ "base64": "IyBCcm93c2VyIHdvcmtzcGFjZQoKUGkgY2FuIHJlYWQsIHdyaXRlLCBlZGl0LCBhbmQgcnVuIHNoZWxsIGNvbW1hbmRzIGhlcmUuCkV2ZXJ5dGhpbmcgc3RheXMgaW4gdGhpcyBicm93c2VyLiBUcnkgYW5hbHl6aW5nIHNhbGVzLmNzdi4K",
58
+ "mode": 420
59
+ }
60
+ ],
61
+ [
62
+ "/workspace/revenue.txt",
63
+ {
64
+ "type": "file",
65
+ "base64": "Tm90ZWJvb2sgMTIgNSAtPiAxMiB4IDUgPSA2MApQZW5jaWwgMzAgMiAtPiAzMCB4IDIgPSA2MApGb2xkZXIgOCAzIC0+IDggeCAzID0gMjQKVE9UQUwgUkVWRU5VRTogMTQ0Cg==",
66
+ "mode": 420
67
+ }
68
+ ],
69
+ [
70
+ "/workspace/sales.csv",
71
+ {
72
+ "type": "file",
73
+ "base64": "cHJvZHVjdCx1bml0cyxwcmljZQpOb3RlYm9vaywxMiw1ClBlbmNpbCwzMCwyCkZvbGRlciw4LDMK",
74
+ "mode": 420
75
+ }
76
+ ],
77
+ [
78
+ "/workspace/secret.txt",
79
+ {
80
+ "type": "file",
81
+ "base64": "TE9DQUwtZmQwMDhiZTE=",
82
+ "mode": 420
83
+ }
84
+ ]
85
+ ],
86
+ "messages": [
87
+ {
88
+ "role": "user",
89
+ "content": [
90
+ {
91
+ "type": "text",
92
+ "text": "Read secret.txt with the read tool and tell me its exact contents."
93
+ }
94
+ ],
95
+ "timestamp": 1789201320594
96
+ },
97
+ {
98
+ "role": "assistant",
99
+ "api": "minicpm-webgpu",
100
+ "provider": "browser",
101
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
102
+ "timestamp": 1789201320595,
103
+ "content": [
104
+ {
105
+ "type": "toolCall",
106
+ "id": "4e94e30c-5284-416d-aacb-c66b4d8bc051",
107
+ "name": "read",
108
+ "arguments": {
109
+ "path": "/workspace/secret.txt"
110
+ }
111
+ }
112
+ ],
113
+ "stopReason": "toolUse",
114
+ "usage": {
115
+ "input": 717,
116
+ "output": 18,
117
+ "cacheRead": 0,
118
+ "cacheWrite": 0,
119
+ "totalTokens": 735,
120
+ "cost": {
121
+ "input": 0,
122
+ "output": 0,
123
+ "cacheRead": 0,
124
+ "cacheWrite": 0,
125
+ "total": 0
126
+ }
127
+ }
128
+ },
129
+ {
130
+ "role": "toolResult",
131
+ "toolCallId": "4e94e30c-5284-416d-aacb-c66b4d8bc051",
132
+ "toolName": "read",
133
+ "content": [
134
+ {
135
+ "type": "text",
136
+ "text": "LOCAL-fd008be1"
137
+ }
138
+ ],
139
+ "details": {},
140
+ "isError": false,
141
+ "timestamp": 1789201321616
142
+ },
143
+ {
144
+ "role": "assistant",
145
+ "api": "minicpm-webgpu",
146
+ "provider": "browser",
147
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
148
+ "timestamp": 1789201321616,
149
+ "content": [
150
+ {
151
+ "type": "text",
152
+ "text": "The read tool returned the contents of secret.txt as:\n\nLOCAL-fd008be1"
153
+ }
154
+ ],
155
+ "stopReason": "stop",
156
+ "usage": {
157
+ "input": 771,
158
+ "output": 20,
159
+ "cacheRead": 0,
160
+ "cacheWrite": 0,
161
+ "totalTokens": 791,
162
+ "cost": {
163
+ "input": 0,
164
+ "output": 0,
165
+ "cacheRead": 0,
166
+ "cacheWrite": 0,
167
+ "total": 0
168
+ }
169
+ }
170
+ }
171
+ ]
172
+ },
173
+ {
174
+ "name": "analyze-and-write",
175
+ "elapsedMs": 8251,
176
+ "version": 1,
177
+ "files": {
178
+ "/tmp/revenue.txt": "Notebook 12 5 60\nPencil 30 2 60\nFolder 8 3 24\nTOTAL: 144\n",
179
+ "/workspace/README.md": "# Browser workspace\n\nPi can read, write, edit, and run shell commands here.\nEverything stays in this browser. Try analyzing sales.csv.\n",
180
+ "/workspace/report.md": "# Sales Report\n\nTotal revenue from sales.csv is **$144**.\n\n- Notebook: 12 Γ— $5 = $60\n- Pencil: 30 Γ— $2 = $60\n- Folder: 8 Γ— $3 = $24\n",
181
+ "/workspace/revenue.txt": "Notebook 12 5 -> 12 x 5 = 60\nPencil 30 2 -> 30 x 2 = 60\nFolder 8 3 -> 8 x 3 = 24\nTOTAL REVENUE: 144\n",
182
+ "/workspace/sales.csv": "product,units,price\nNotebook,12,5\nPencil,30,2\nFolder,8,3\n",
183
+ "/workspace/secret.txt": "LOCAL-fd008be1"
184
+ },
185
+ "entries": [
186
+ [
187
+ "/",
188
+ {
189
+ "type": "directory",
190
+ "mode": 493
191
+ }
192
+ ],
193
+ [
194
+ "/tmp",
195
+ {
196
+ "type": "directory",
197
+ "mode": 493
198
+ }
199
+ ],
200
+ [
201
+ "/tmp/revenue.txt",
202
+ {
203
+ "type": "file",
204
+ "base64": "Tm90ZWJvb2sgMTIgNSA2MApQZW5jaWwgMzAgMiA2MApGb2xkZXIgOCAzIDI0ClRPVEFMOiAxNDQK",
205
+ "mode": 420
206
+ }
207
+ ],
208
+ [
209
+ "/usr",
210
+ {
211
+ "type": "directory",
212
+ "mode": 493
213
+ }
214
+ ],
215
+ [
216
+ "/workspace",
217
+ {
218
+ "type": "directory",
219
+ "mode": 493
220
+ }
221
+ ],
222
+ [
223
+ "/workspace/README.md",
224
+ {
225
+ "type": "file",
226
+ "base64": "IyBCcm93c2VyIHdvcmtzcGFjZQoKUGkgY2FuIHJlYWQsIHdyaXRlLCBlZGl0LCBhbmQgcnVuIHNoZWxsIGNvbW1hbmRzIGhlcmUuCkV2ZXJ5dGhpbmcgc3RheXMgaW4gdGhpcyBicm93c2VyLiBUcnkgYW5hbHl6aW5nIHNhbGVzLmNzdi4K",
227
+ "mode": 420
228
+ }
229
+ ],
230
+ [
231
+ "/workspace/report.md",
232
+ {
233
+ "type": "file",
234
+ "base64": "IyBTYWxlcyBSZXBvcnQKClRvdGFsIHJldmVudWUgZnJvbSBzYWxlcy5jc3YgaXMgKiokMTQ0KiouCgotIE5vdGVib29rOiAxMiDDlyAkNSA9ICQ2MAotIFBlbmNpbDogMzAgw5cgJDIgPSAkNjAKLSBGb2xkZXI6IDggw5cgJDMgPSAkMjQK",
235
+ "mode": 420
236
+ }
237
+ ],
238
+ [
239
+ "/workspace/revenue.txt",
240
+ {
241
+ "type": "file",
242
+ "base64": "Tm90ZWJvb2sgMTIgNSAtPiAxMiB4IDUgPSA2MApQZW5jaWwgMzAgMiAtPiAzMCB4IDIgPSA2MApGb2xkZXIgOCAzIC0+IDggeCAzID0gMjQKVE9UQUwgUkVWRU5VRTogMTQ0Cg==",
243
+ "mode": 420
244
+ }
245
+ ],
246
+ [
247
+ "/workspace/sales.csv",
248
+ {
249
+ "type": "file",
250
+ "base64": "cHJvZHVjdCx1bml0cyxwcmljZQpOb3RlYm9vaywxMiw1ClBlbmNpbCwzMCwyCkZvbGRlciw4LDMK",
251
+ "mode": 420
252
+ }
253
+ ],
254
+ [
255
+ "/workspace/secret.txt",
256
+ {
257
+ "type": "file",
258
+ "base64": "TE9DQUwtZmQwMDhiZTE=",
259
+ "mode": 420
260
+ }
261
+ ]
262
+ ],
263
+ "messages": [
264
+ {
265
+ "role": "user",
266
+ "content": [
267
+ {
268
+ "type": "text",
269
+ "text": "Read sales.csv, calculate total revenue using bash, and save a short report to report.md. Verify the file."
270
+ }
271
+ ],
272
+ "timestamp": 1789201322878
273
+ },
274
+ {
275
+ "role": "assistant",
276
+ "api": "minicpm-webgpu",
277
+ "provider": "browser",
278
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
279
+ "timestamp": 1789201322878,
280
+ "content": [
281
+ {
282
+ "type": "toolCall",
283
+ "id": "d609cb7d-ae48-45c5-8042-95e3aac1e0ca",
284
+ "name": "read",
285
+ "arguments": {
286
+ "path": "/workspace/sales.csv"
287
+ }
288
+ }
289
+ ],
290
+ "stopReason": "toolUse",
291
+ "usage": {
292
+ "input": 726,
293
+ "output": 18,
294
+ "cacheRead": 0,
295
+ "cacheWrite": 0,
296
+ "totalTokens": 744,
297
+ "cost": {
298
+ "input": 0,
299
+ "output": 0,
300
+ "cacheRead": 0,
301
+ "cacheWrite": 0,
302
+ "total": 0
303
+ }
304
+ }
305
+ },
306
+ {
307
+ "role": "toolResult",
308
+ "toolCallId": "d609cb7d-ae48-45c5-8042-95e3aac1e0ca",
309
+ "toolName": "read",
310
+ "content": [
311
+ {
312
+ "type": "text",
313
+ "text": "product,units,price\nNotebook,12,5\nPencil,30,2\nFolder,8,3\n"
314
+ }
315
+ ],
316
+ "details": {},
317
+ "isError": false,
318
+ "timestamp": 1789201323848
319
+ },
320
+ {
321
+ "role": "assistant",
322
+ "api": "minicpm-webgpu",
323
+ "provider": "browser",
324
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
325
+ "timestamp": 1789201323848,
326
+ "content": [
327
+ {
328
+ "type": "toolCall",
329
+ "id": "24c2dfa0-6414-418a-8c15-c1f246eabd3e",
330
+ "name": "bash",
331
+ "arguments": {
332
+ "command": "awk -F',' 'NR>1 {rev=$2*$3; total+=rev; print $1,$2,$3,\"->\",$2,\"x\",$3,\"=\",rev} END {print \"TOTAL REVENUE:\", total}' sales.csv > revenue.txt && cat revenue.txt"
333
+ }
334
+ }
335
+ ],
336
+ "stopReason": "toolUse",
337
+ "usage": {
338
+ "input": 798,
339
+ "output": 78,
340
+ "cacheRead": 0,
341
+ "cacheWrite": 0,
342
+ "totalTokens": 876,
343
+ "cost": {
344
+ "input": 0,
345
+ "output": 0,
346
+ "cacheRead": 0,
347
+ "cacheWrite": 0,
348
+ "total": 0
349
+ }
350
+ }
351
+ },
352
+ {
353
+ "role": "toolResult",
354
+ "toolCallId": "24c2dfa0-6414-418a-8c15-c1f246eabd3e",
355
+ "toolName": "bash",
356
+ "content": [
357
+ {
358
+ "type": "text",
359
+ "text": "Notebook 12 5 -> 12 x 5 = 60\nPencil 30 2 -> 30 x 2 = 60\nFolder 8 3 -> 8 x 3 = 24\nTOTAL REVENUE: 144\n\nExit code: 0"
360
+ }
361
+ ],
362
+ "details": {
363
+ "exitCode": 0
364
+ },
365
+ "isError": false,
366
+ "timestamp": 1789201325902
367
+ },
368
+ {
369
+ "role": "assistant",
370
+ "api": "minicpm-webgpu",
371
+ "provider": "browser",
372
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
373
+ "timestamp": 1789201325902,
374
+ "content": [
375
+ {
376
+ "type": "toolCall",
377
+ "id": "28f41bf2-1b3f-4d1d-85cc-d3e26f230be8",
378
+ "name": "write",
379
+ "arguments": {
380
+ "path": "report.md",
381
+ "content": "# Sales Report\n\nTotal revenue from sales.csv is **$144**.\n\n- Notebook: 12 Γ— $5 = $60\n- Pencil: 30 Γ— $2 = $60\n- Folder: 8 Γ— $3 = $24\n"
382
+ }
383
+ }
384
+ ],
385
+ "stopReason": "toolUse",
386
+ "usage": {
387
+ "input": 964,
388
+ "output": 79,
389
+ "cacheRead": 0,
390
+ "cacheWrite": 0,
391
+ "totalTokens": 1043,
392
+ "cost": {
393
+ "input": 0,
394
+ "output": 0,
395
+ "cacheRead": 0,
396
+ "cacheWrite": 0,
397
+ "total": 0
398
+ }
399
+ }
400
+ },
401
+ {
402
+ "role": "toolResult",
403
+ "toolCallId": "28f41bf2-1b3f-4d1d-85cc-d3e26f230be8",
404
+ "toolName": "write",
405
+ "content": [
406
+ {
407
+ "type": "text",
408
+ "text": "Wrote /workspace/report.md"
409
+ }
410
+ ],
411
+ "details": {},
412
+ "isError": false,
413
+ "timestamp": 1789201328112
414
+ },
415
+ {
416
+ "role": "assistant",
417
+ "api": "minicpm-webgpu",
418
+ "provider": "browser",
419
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
420
+ "timestamp": 1789201328112,
421
+ "content": [
422
+ {
423
+ "type": "toolCall",
424
+ "id": "92952bd2-6deb-45ef-b1d1-ee04a554d595",
425
+ "name": "read",
426
+ "arguments": {
427
+ "path": "/workspace/report.md"
428
+ }
429
+ }
430
+ ],
431
+ "stopReason": "toolUse",
432
+ "usage": {
433
+ "input": 1079,
434
+ "output": 18,
435
+ "cacheRead": 0,
436
+ "cacheWrite": 0,
437
+ "totalTokens": 1097,
438
+ "cost": {
439
+ "input": 0,
440
+ "output": 0,
441
+ "cacheRead": 0,
442
+ "cacheWrite": 0,
443
+ "total": 0
444
+ }
445
+ }
446
+ },
447
+ {
448
+ "role": "toolResult",
449
+ "toolCallId": "92952bd2-6deb-45ef-b1d1-ee04a554d595",
450
+ "toolName": "read",
451
+ "content": [
452
+ {
453
+ "type": "text",
454
+ "text": "# Sales Report\n\nTotal revenue from sales.csv is **$144**.\n\n- Notebook: 12 Γ— $5 = $60\n- Pencil: 30 Γ— $2 = $60\n- Folder: 8 Γ— $3 = $24\n"
455
+ }
456
+ ],
457
+ "details": {},
458
+ "isError": false,
459
+ "timestamp": 1789201329332
460
+ },
461
+ {
462
+ "role": "assistant",
463
+ "api": "minicpm-webgpu",
464
+ "provider": "browser",
465
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
466
+ "timestamp": 1789201329332,
467
+ "content": [
468
+ {
469
+ "type": "text",
470
+ "text": "Done. The report is saved at `/workspace/report.md` with total revenue of $144 (Notebook $60, Pencil $60, Folder $24)."
471
+ }
472
+ ],
473
+ "stopReason": "stop",
474
+ "usage": {
475
+ "input": 1180,
476
+ "output": 35,
477
+ "cacheRead": 0,
478
+ "cacheWrite": 0,
479
+ "totalTokens": 1215,
480
+ "cost": {
481
+ "input": 0,
482
+ "output": 0,
483
+ "cacheRead": 0,
484
+ "cacheWrite": 0,
485
+ "total": 0
486
+ }
487
+ }
488
+ }
489
+ ]
490
+ },
491
+ {
492
+ "name": "write-and-run-script",
493
+ "elapsedMs": 4368,
494
+ "version": 1,
495
+ "files": {
496
+ "/tmp/revenue.txt": "Notebook 12 5 60\nPencil 30 2 60\nFolder 8 3 24\nTOTAL: 144\n",
497
+ "/workspace/README.md": "# Browser workspace\n\nPi can read, write, edit, and run shell commands here.\nEverything stays in this browser. Try analyzing sales.csv.\n",
498
+ "/workspace/hello.sh": "#!/bin/bash\necho \"Hello from the browser\"\n",
499
+ "/workspace/report.md": "# Sales Report\n\nTotal revenue from sales.csv is **$144**.\n\n- Notebook: 12 Γ— $5 = $60\n- Pencil: 30 Γ— $2 = $60\n- Folder: 8 Γ— $3 = $24\n",
500
+ "/workspace/revenue.txt": "Notebook 12 5 -> 12 x 5 = 60\nPencil 30 2 -> 30 x 2 = 60\nFolder 8 3 -> 8 x 3 = 24\nTOTAL REVENUE: 144\n",
501
+ "/workspace/sales.csv": "product,units,price\nNotebook,12,5\nPencil,30,2\nFolder,8,3\n",
502
+ "/workspace/secret.txt": "LOCAL-fd008be1"
503
+ },
504
+ "entries": [
505
+ [
506
+ "/",
507
+ {
508
+ "type": "directory",
509
+ "mode": 493
510
+ }
511
+ ],
512
+ [
513
+ "/tmp",
514
+ {
515
+ "type": "directory",
516
+ "mode": 493
517
+ }
518
+ ],
519
+ [
520
+ "/tmp/revenue.txt",
521
+ {
522
+ "type": "file",
523
+ "base64": "Tm90ZWJvb2sgMTIgNSA2MApQZW5jaWwgMzAgMiA2MApGb2xkZXIgOCAzIDI0ClRPVEFMOiAxNDQK",
524
+ "mode": 420
525
+ }
526
+ ],
527
+ [
528
+ "/usr",
529
+ {
530
+ "type": "directory",
531
+ "mode": 493
532
+ }
533
+ ],
534
+ [
535
+ "/workspace",
536
+ {
537
+ "type": "directory",
538
+ "mode": 493
539
+ }
540
+ ],
541
+ [
542
+ "/workspace/README.md",
543
+ {
544
+ "type": "file",
545
+ "base64": "IyBCcm93c2VyIHdvcmtzcGFjZQoKUGkgY2FuIHJlYWQsIHdyaXRlLCBlZGl0LCBhbmQgcnVuIHNoZWxsIGNvbW1hbmRzIGhlcmUuCkV2ZXJ5dGhpbmcgc3RheXMgaW4gdGhpcyBicm93c2VyLiBUcnkgYW5hbHl6aW5nIHNhbGVzLmNzdi4K",
546
+ "mode": 420
547
+ }
548
+ ],
549
+ [
550
+ "/workspace/hello.sh",
551
+ {
552
+ "type": "file",
553
+ "base64": "IyEvYmluL2Jhc2gKZWNobyAiSGVsbG8gZnJvbSB0aGUgYnJvd3NlciIK",
554
+ "mode": 493
555
+ }
556
+ ],
557
+ [
558
+ "/workspace/report.md",
559
+ {
560
+ "type": "file",
561
+ "base64": "IyBTYWxlcyBSZXBvcnQKClRvdGFsIHJldmVudWUgZnJvbSBzYWxlcy5jc3YgaXMgKiokMTQ0KiouCgotIE5vdGVib29rOiAxMiDDlyAkNSA9ICQ2MAotIFBlbmNpbDogMzAgw5cgJDIgPSAkNjAKLSBGb2xkZXI6IDggw5cgJDMgPSAkMjQK",
562
+ "mode": 420
563
+ }
564
+ ],
565
+ [
566
+ "/workspace/revenue.txt",
567
+ {
568
+ "type": "file",
569
+ "base64": "Tm90ZWJvb2sgMTIgNSAtPiAxMiB4IDUgPSA2MApQZW5jaWwgMzAgMiAtPiAzMCB4IDIgPSA2MApGb2xkZXIgOCAzIC0+IDggeCAzID0gMjQKVE9UQUwgUkVWRU5VRTogMTQ0Cg==",
570
+ "mode": 420
571
+ }
572
+ ],
573
+ [
574
+ "/workspace/sales.csv",
575
+ {
576
+ "type": "file",
577
+ "base64": "cHJvZHVjdCx1bml0cyxwcmljZQpOb3RlYm9vaywxMiw1ClBlbmNpbCwzMCwyCkZvbGRlciw4LDMK",
578
+ "mode": 420
579
+ }
580
+ ],
581
+ [
582
+ "/workspace/secret.txt",
583
+ {
584
+ "type": "file",
585
+ "base64": "TE9DQUwtZmQwMDhiZTE=",
586
+ "mode": 420
587
+ }
588
+ ]
589
+ ],
590
+ "messages": [
591
+ {
592
+ "role": "user",
593
+ "content": [
594
+ {
595
+ "type": "text",
596
+ "text": "Create a bash script called hello.sh that prints Hello from the browser, run it, and report the output."
597
+ }
598
+ ],
599
+ "timestamp": 1789201331178
600
+ },
601
+ {
602
+ "role": "assistant",
603
+ "api": "minicpm-webgpu",
604
+ "provider": "browser",
605
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
606
+ "timestamp": 1789201331178,
607
+ "content": [
608
+ {
609
+ "type": "toolCall",
610
+ "id": "b5138935-5cd3-40e5-8021-af9f2b79ca14",
611
+ "name": "write",
612
+ "arguments": {
613
+ "path": "/workspace/hello.sh",
614
+ "content": "#!/bin/bash\necho \"Hello from the browser\"\n"
615
+ }
616
+ }
617
+ ],
618
+ "stopReason": "toolUse",
619
+ "usage": {
620
+ "input": 725,
621
+ "output": 43,
622
+ "cacheRead": 0,
623
+ "cacheWrite": 0,
624
+ "totalTokens": 768,
625
+ "cost": {
626
+ "input": 0,
627
+ "output": 0,
628
+ "cacheRead": 0,
629
+ "cacheWrite": 0,
630
+ "total": 0
631
+ }
632
+ }
633
+ },
634
+ {
635
+ "role": "toolResult",
636
+ "toolCallId": "b5138935-5cd3-40e5-8021-af9f2b79ca14",
637
+ "toolName": "write",
638
+ "content": [
639
+ {
640
+ "type": "text",
641
+ "text": "Wrote /workspace/hello.sh"
642
+ }
643
+ ],
644
+ "details": {},
645
+ "isError": false,
646
+ "timestamp": 1789201332541
647
+ },
648
+ {
649
+ "role": "assistant",
650
+ "api": "minicpm-webgpu",
651
+ "provider": "browser",
652
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
653
+ "timestamp": 1789201332541,
654
+ "content": [
655
+ {
656
+ "type": "toolCall",
657
+ "id": "26cda3ca-c25b-436b-bf85-33d3dd3eac83",
658
+ "name": "bash",
659
+ "arguments": {
660
+ "command": "chmod +x /workspace/hello.sh && /workspace/hello.sh"
661
+ }
662
+ }
663
+ ],
664
+ "stopReason": "toolUse",
665
+ "usage": {
666
+ "input": 804,
667
+ "output": 34,
668
+ "cacheRead": 0,
669
+ "cacheWrite": 0,
670
+ "totalTokens": 838,
671
+ "cost": {
672
+ "input": 0,
673
+ "output": 0,
674
+ "cacheRead": 0,
675
+ "cacheWrite": 0,
676
+ "total": 0
677
+ }
678
+ }
679
+ },
680
+ {
681
+ "role": "toolResult",
682
+ "toolCallId": "26cda3ca-c25b-436b-bf85-33d3dd3eac83",
683
+ "toolName": "bash",
684
+ "content": [
685
+ {
686
+ "type": "text",
687
+ "text": "Hello from the browser\n\nExit code: 0"
688
+ }
689
+ ],
690
+ "details": {
691
+ "exitCode": 0
692
+ },
693
+ "isError": false,
694
+ "timestamp": 1789201333991
695
+ },
696
+ {
697
+ "role": "assistant",
698
+ "api": "minicpm-webgpu",
699
+ "provider": "browser",
700
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
701
+ "timestamp": 1789201333991,
702
+ "content": [
703
+ {
704
+ "type": "text",
705
+ "text": "The script `/workspace/hello.sh` prints \"Hello from the browser\". Running it outputs:\n\n```\nHello from the browser\n```\n\nwith exit code 0."
706
+ }
707
+ ],
708
+ "stopReason": "stop",
709
+ "usage": {
710
+ "input": 878,
711
+ "output": 35,
712
+ "cacheRead": 0,
713
+ "cacheWrite": 0,
714
+ "totalTokens": 913,
715
+ "cost": {
716
+ "input": 0,
717
+ "output": 0,
718
+ "cacheRead": 0,
719
+ "cacheWrite": 0,
720
+ "total": 0
721
+ }
722
+ }
723
+ }
724
+ ]
725
+ },
726
+ {
727
+ "name": "edit-and-verify",
728
+ "elapsedMs": 4833,
729
+ "version": 1,
730
+ "files": {
731
+ "/tmp/revenue.txt": "Notebook 12 5 60\nPencil 30 2 60\nFolder 8 3 24\nTOTAL: 144\n",
732
+ "/workspace/README.md": "# Browser workspace\n\nPi can read, write, edit, and run shell commands here.\nEverything stays in this browser. Try analyzing sales.csv.\n",
733
+ "/workspace/hello.sh": "#!/bin/bash\necho \"Hello from Pi\"\n",
734
+ "/workspace/report.md": "# Sales Report\n\nTotal revenue from sales.csv is **$144**.\n\n- Notebook: 12 Γ— $5 = $60\n- Pencil: 30 Γ— $2 = $60\n- Folder: 8 Γ— $3 = $24\n",
735
+ "/workspace/revenue.txt": "Notebook 12 5 -> 12 x 5 = 60\nPencil 30 2 -> 30 x 2 = 60\nFolder 8 3 -> 8 x 3 = 24\nTOTAL REVENUE: 144\n",
736
+ "/workspace/sales.csv": "product,units,price\nNotebook,12,5\nPencil,30,2\nFolder,8,3\n",
737
+ "/workspace/secret.txt": "LOCAL-fd008be1"
738
+ },
739
+ "entries": [
740
+ [
741
+ "/",
742
+ {
743
+ "type": "directory",
744
+ "mode": 493
745
+ }
746
+ ],
747
+ [
748
+ "/tmp",
749
+ {
750
+ "type": "directory",
751
+ "mode": 493
752
+ }
753
+ ],
754
+ [
755
+ "/tmp/revenue.txt",
756
+ {
757
+ "type": "file",
758
+ "base64": "Tm90ZWJvb2sgMTIgNSA2MApQZW5jaWwgMzAgMiA2MApGb2xkZXIgOCAzIDI0ClRPVEFMOiAxNDQK",
759
+ "mode": 420
760
+ }
761
+ ],
762
+ [
763
+ "/usr",
764
+ {
765
+ "type": "directory",
766
+ "mode": 493
767
+ }
768
+ ],
769
+ [
770
+ "/workspace",
771
+ {
772
+ "type": "directory",
773
+ "mode": 493
774
+ }
775
+ ],
776
+ [
777
+ "/workspace/README.md",
778
+ {
779
+ "type": "file",
780
+ "base64": "IyBCcm93c2VyIHdvcmtzcGFjZQoKUGkgY2FuIHJlYWQsIHdyaXRlLCBlZGl0LCBhbmQgcnVuIHNoZWxsIGNvbW1hbmRzIGhlcmUuCkV2ZXJ5dGhpbmcgc3RheXMgaW4gdGhpcyBicm93c2VyLiBUcnkgYW5hbHl6aW5nIHNhbGVzLmNzdi4K",
781
+ "mode": 420
782
+ }
783
+ ],
784
+ [
785
+ "/workspace/hello.sh",
786
+ {
787
+ "type": "file",
788
+ "base64": "IyEvYmluL2Jhc2gKZWNobyAiSGVsbG8gZnJvbSBQaSIK",
789
+ "mode": 493
790
+ }
791
+ ],
792
+ [
793
+ "/workspace/report.md",
794
+ {
795
+ "type": "file",
796
+ "base64": "IyBTYWxlcyBSZXBvcnQKClRvdGFsIHJldmVudWUgZnJvbSBzYWxlcy5jc3YgaXMgKiokMTQ0KiouCgotIE5vdGVib29rOiAxMiDDlyAkNSA9ICQ2MAotIFBlbmNpbDogMzAgw5cgJDIgPSAkNjAKLSBGb2xkZXI6IDggw5cgJDMgPSAkMjQK",
797
+ "mode": 420
798
+ }
799
+ ],
800
+ [
801
+ "/workspace/revenue.txt",
802
+ {
803
+ "type": "file",
804
+ "base64": "Tm90ZWJvb2sgMTIgNSAtPiAxMiB4IDUgPSA2MApQZW5jaWwgMzAgMiAtPiAzMCB4IDIgPSA2MApGb2xkZXIgOCAzIC0+IDggeCAzID0gMjQKVE9UQUwgUkVWRU5VRTogMTQ0Cg==",
805
+ "mode": 420
806
+ }
807
+ ],
808
+ [
809
+ "/workspace/sales.csv",
810
+ {
811
+ "type": "file",
812
+ "base64": "cHJvZHVjdCx1bml0cyxwcmljZQpOb3RlYm9vaywxMiw1ClBlbmNpbCwzMCwyCkZvbGRlciw4LDMK",
813
+ "mode": 420
814
+ }
815
+ ],
816
+ [
817
+ "/workspace/secret.txt",
818
+ {
819
+ "type": "file",
820
+ "base64": "TE9DQUwtZmQwMDhiZTE=",
821
+ "mode": 420
822
+ }
823
+ ]
824
+ ],
825
+ "messages": [
826
+ {
827
+ "role": "user",
828
+ "content": [
829
+ {
830
+ "type": "text",
831
+ "text": "Read hello.sh, use the edit tool to replace Hello from the browser with Hello from Pi, then run the script and report the output."
832
+ }
833
+ ],
834
+ "timestamp": 1789201335595
835
+ },
836
+ {
837
+ "role": "assistant",
838
+ "api": "minicpm-webgpu",
839
+ "provider": "browser",
840
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
841
+ "timestamp": 1789201335596,
842
+ "content": [
843
+ {
844
+ "type": "toolCall",
845
+ "id": "4cf27702-1db6-4297-8dcd-1a6b28cba1cb",
846
+ "name": "read",
847
+ "arguments": {
848
+ "path": "hello.sh"
849
+ }
850
+ }
851
+ ],
852
+ "stopReason": "toolUse",
853
+ "usage": {
854
+ "input": 731,
855
+ "output": 15,
856
+ "cacheRead": 0,
857
+ "cacheWrite": 0,
858
+ "totalTokens": 746,
859
+ "cost": {
860
+ "input": 0,
861
+ "output": 0,
862
+ "cacheRead": 0,
863
+ "cacheWrite": 0,
864
+ "total": 0
865
+ }
866
+ }
867
+ },
868
+ {
869
+ "role": "toolResult",
870
+ "toolCallId": "4cf27702-1db6-4297-8dcd-1a6b28cba1cb",
871
+ "toolName": "read",
872
+ "content": [
873
+ {
874
+ "type": "text",
875
+ "text": "#!/bin/bash\necho \"Hello from the browser\"\n"
876
+ }
877
+ ],
878
+ "details": {},
879
+ "isError": false,
880
+ "timestamp": 1789201336493
881
+ },
882
+ {
883
+ "role": "assistant",
884
+ "api": "minicpm-webgpu",
885
+ "provider": "browser",
886
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
887
+ "timestamp": 1789201336493,
888
+ "content": [
889
+ {
890
+ "type": "toolCall",
891
+ "id": "57259aeb-a574-4271-bebc-0693a9eac75f",
892
+ "name": "edit",
893
+ "arguments": {
894
+ "path": "hello.sh",
895
+ "oldText": "echo \"Hello from the browser\"",
896
+ "newText": "echo \"Hello from Pi\""
897
+ }
898
+ }
899
+ ],
900
+ "stopReason": "toolUse",
901
+ "usage": {
902
+ "input": 790,
903
+ "output": 42,
904
+ "cacheRead": 0,
905
+ "cacheWrite": 0,
906
+ "totalTokens": 832,
907
+ "cost": {
908
+ "input": 0,
909
+ "output": 0,
910
+ "cacheRead": 0,
911
+ "cacheWrite": 0,
912
+ "total": 0
913
+ }
914
+ }
915
+ },
916
+ {
917
+ "role": "toolResult",
918
+ "toolCallId": "57259aeb-a574-4271-bebc-0693a9eac75f",
919
+ "toolName": "edit",
920
+ "content": [
921
+ {
922
+ "type": "text",
923
+ "text": "Edited /workspace/hello.sh"
924
+ }
925
+ ],
926
+ "details": {},
927
+ "isError": false,
928
+ "timestamp": 1789201337880
929
+ },
930
+ {
931
+ "role": "assistant",
932
+ "api": "minicpm-webgpu",
933
+ "provider": "browser",
934
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
935
+ "timestamp": 1789201337881,
936
+ "content": [
937
+ {
938
+ "type": "toolCall",
939
+ "id": "5ec18439-e1b8-4db2-bd9e-cde21bb97b91",
940
+ "name": "bash",
941
+ "arguments": {
942
+ "command": "chmod +x hello.sh && ./hello.sh"
943
+ }
944
+ }
945
+ ],
946
+ "stopReason": "toolUse",
947
+ "usage": {
948
+ "input": 867,
949
+ "output": 29,
950
+ "cacheRead": 0,
951
+ "cacheWrite": 0,
952
+ "totalTokens": 896,
953
+ "cost": {
954
+ "input": 0,
955
+ "output": 0,
956
+ "cacheRead": 0,
957
+ "cacheWrite": 0,
958
+ "total": 0
959
+ }
960
+ }
961
+ },
962
+ {
963
+ "role": "toolResult",
964
+ "toolCallId": "5ec18439-e1b8-4db2-bd9e-cde21bb97b91",
965
+ "toolName": "bash",
966
+ "content": [
967
+ {
968
+ "type": "text",
969
+ "text": "Hello from Pi\n\nExit code: 0"
970
+ }
971
+ ],
972
+ "details": {
973
+ "exitCode": 0
974
+ },
975
+ "isError": false,
976
+ "timestamp": 1789201339204
977
+ },
978
+ {
979
+ "role": "assistant",
980
+ "api": "minicpm-webgpu",
981
+ "provider": "browser",
982
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
983
+ "timestamp": 1789201339204,
984
+ "content": [
985
+ {
986
+ "type": "text",
987
+ "text": "The script now outputs \"Hello from Pi\" when run."
988
+ }
989
+ ],
990
+ "stopReason": "stop",
991
+ "usage": {
992
+ "input": 935,
993
+ "output": 13,
994
+ "cacheRead": 0,
995
+ "cacheWrite": 0,
996
+ "totalTokens": 948,
997
+ "cost": {
998
+ "input": 0,
999
+ "output": 0,
1000
+ "cacheRead": 0,
1001
+ "cacheWrite": 0,
1002
+ "total": 0
1003
+ }
1004
+ }
1005
+ }
1006
+ ]
1007
+ },
1008
+ {
1009
+ "name": "recover-tool-error",
1010
+ "elapsedMs": 5084,
1011
+ "version": 1,
1012
+ "files": {
1013
+ "/tmp/revenue.txt": "Notebook 12 5 60\nPencil 30 2 60\nFolder 8 3 24\nTOTAL: 144\n",
1014
+ "/workspace/README.md": "# Browser workspace\n\nPi can read, write, edit, and run shell commands here.\nEverything stays in this browser. Try analyzing sales.csv.\n",
1015
+ "/workspace/hello.sh": "#!/bin/bash\necho \"Hello from Pi\"\n",
1016
+ "/workspace/missing.txt": "Recovered",
1017
+ "/workspace/report.md": "# Sales Report\n\nTotal revenue from sales.csv is **$144**.\n\n- Notebook: 12 Γ— $5 = $60\n- Pencil: 30 Γ— $2 = $60\n- Folder: 8 Γ— $3 = $24\n",
1018
+ "/workspace/revenue.txt": "Notebook 12 5 -> 12 x 5 = 60\nPencil 30 2 -> 30 x 2 = 60\nFolder 8 3 -> 8 x 3 = 24\nTOTAL REVENUE: 144\n",
1019
+ "/workspace/sales.csv": "product,units,price\nNotebook,12,5\nPencil,30,2\nFolder,8,3\n",
1020
+ "/workspace/secret.txt": "LOCAL-fd008be1"
1021
+ },
1022
+ "entries": [
1023
+ [
1024
+ "/",
1025
+ {
1026
+ "type": "directory",
1027
+ "mode": 493
1028
+ }
1029
+ ],
1030
+ [
1031
+ "/tmp",
1032
+ {
1033
+ "type": "directory",
1034
+ "mode": 493
1035
+ }
1036
+ ],
1037
+ [
1038
+ "/tmp/revenue.txt",
1039
+ {
1040
+ "type": "file",
1041
+ "base64": "Tm90ZWJvb2sgMTIgNSA2MApQZW5jaWwgMzAgMiA2MApGb2xkZXIgOCAzIDI0ClRPVEFMOiAxNDQK",
1042
+ "mode": 420
1043
+ }
1044
+ ],
1045
+ [
1046
+ "/usr",
1047
+ {
1048
+ "type": "directory",
1049
+ "mode": 493
1050
+ }
1051
+ ],
1052
+ [
1053
+ "/workspace",
1054
+ {
1055
+ "type": "directory",
1056
+ "mode": 493
1057
+ }
1058
+ ],
1059
+ [
1060
+ "/workspace/README.md",
1061
+ {
1062
+ "type": "file",
1063
+ "base64": "IyBCcm93c2VyIHdvcmtzcGFjZQoKUGkgY2FuIHJlYWQsIHdyaXRlLCBlZGl0LCBhbmQgcnVuIHNoZWxsIGNvbW1hbmRzIGhlcmUuCkV2ZXJ5dGhpbmcgc3RheXMgaW4gdGhpcyBicm93c2VyLiBUcnkgYW5hbHl6aW5nIHNhbGVzLmNzdi4K",
1064
+ "mode": 420
1065
+ }
1066
+ ],
1067
+ [
1068
+ "/workspace/hello.sh",
1069
+ {
1070
+ "type": "file",
1071
+ "base64": "IyEvYmluL2Jhc2gKZWNobyAiSGVsbG8gZnJvbSBQaSIK",
1072
+ "mode": 493
1073
+ }
1074
+ ],
1075
+ [
1076
+ "/workspace/missing.txt",
1077
+ {
1078
+ "type": "file",
1079
+ "base64": "UmVjb3ZlcmVk",
1080
+ "mode": 420
1081
+ }
1082
+ ],
1083
+ [
1084
+ "/workspace/report.md",
1085
+ {
1086
+ "type": "file",
1087
+ "base64": "IyBTYWxlcyBSZXBvcnQKClRvdGFsIHJldmVudWUgZnJvbSBzYWxlcy5jc3YgaXMgKiokMTQ0KiouCgotIE5vdGVib29rOiAxMiDDlyAkNSA9ICQ2MAotIFBlbmNpbDogMzAgw5cgJDIgPSAkNjAKLSBGb2xkZXI6IDggw5cgJDMgPSAkMjQK",
1088
+ "mode": 420
1089
+ }
1090
+ ],
1091
+ [
1092
+ "/workspace/revenue.txt",
1093
+ {
1094
+ "type": "file",
1095
+ "base64": "Tm90ZWJvb2sgMTIgNSAtPiAxMiB4IDUgPSA2MApQZW5jaWwgMzAgMiAtPiAzMCB4IDIgPSA2MApGb2xkZXIgOCAzIC0+IDggeCAzID0gMjQKVE9UQUwgUkVWRU5VRTogMTQ0Cg==",
1096
+ "mode": 420
1097
+ }
1098
+ ],
1099
+ [
1100
+ "/workspace/sales.csv",
1101
+ {
1102
+ "type": "file",
1103
+ "base64": "cHJvZHVjdCx1bml0cyxwcmljZQpOb3RlYm9vaywxMiw1ClBlbmNpbCwzMCwyCkZvbGRlciw4LDMK",
1104
+ "mode": 420
1105
+ }
1106
+ ],
1107
+ [
1108
+ "/workspace/secret.txt",
1109
+ {
1110
+ "type": "file",
1111
+ "base64": "TE9DQUwtZmQwMDhiZTE=",
1112
+ "mode": 420
1113
+ }
1114
+ ]
1115
+ ],
1116
+ "messages": [
1117
+ {
1118
+ "role": "user",
1119
+ "content": [
1120
+ {
1121
+ "type": "text",
1122
+ "text": "Read missing.txt using the read tool. If it does not exist, create it with the exact text Recovered and verify it by reading the file."
1123
+ }
1124
+ ],
1125
+ "timestamp": 1789201340477
1126
+ },
1127
+ {
1128
+ "role": "assistant",
1129
+ "api": "minicpm-webgpu",
1130
+ "provider": "browser",
1131
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
1132
+ "timestamp": 1789201340478,
1133
+ "content": [
1134
+ {
1135
+ "type": "toolCall",
1136
+ "id": "493508b4-a058-4d93-ae05-15a1329a5da9",
1137
+ "name": "read",
1138
+ "arguments": {
1139
+ "path": "/workspace/missing.txt"
1140
+ }
1141
+ }
1142
+ ],
1143
+ "stopReason": "toolUse",
1144
+ "usage": {
1145
+ "input": 733,
1146
+ "output": 18,
1147
+ "cacheRead": 0,
1148
+ "cacheWrite": 0,
1149
+ "totalTokens": 751,
1150
+ "cost": {
1151
+ "input": 0,
1152
+ "output": 0,
1153
+ "cacheRead": 0,
1154
+ "cacheWrite": 0,
1155
+ "total": 0
1156
+ }
1157
+ }
1158
+ },
1159
+ {
1160
+ "role": "toolResult",
1161
+ "toolCallId": "493508b4-a058-4d93-ae05-15a1329a5da9",
1162
+ "toolName": "read",
1163
+ "content": [
1164
+ {
1165
+ "type": "text",
1166
+ "text": "ENOENT: no such file or directory, open '/workspace/missing.txt'"
1167
+ }
1168
+ ],
1169
+ "details": {},
1170
+ "isError": true,
1171
+ "timestamp": 1789201341439
1172
+ },
1173
+ {
1174
+ "role": "assistant",
1175
+ "api": "minicpm-webgpu",
1176
+ "provider": "browser",
1177
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
1178
+ "timestamp": 1789201341439,
1179
+ "content": [
1180
+ {
1181
+ "type": "toolCall",
1182
+ "id": "7d5678e4-fba5-43f1-bd2b-d770667f6bb1",
1183
+ "name": "write",
1184
+ "arguments": {
1185
+ "path": "/workspace/missing.txt",
1186
+ "content": "Recovered"
1187
+ }
1188
+ }
1189
+ ],
1190
+ "stopReason": "toolUse",
1191
+ "usage": {
1192
+ "input": 796,
1193
+ "output": 32,
1194
+ "cacheRead": 0,
1195
+ "cacheWrite": 0,
1196
+ "totalTokens": 828,
1197
+ "cost": {
1198
+ "input": 0,
1199
+ "output": 0,
1200
+ "cacheRead": 0,
1201
+ "cacheWrite": 0,
1202
+ "total": 0
1203
+ }
1204
+ }
1205
+ },
1206
+ {
1207
+ "role": "toolResult",
1208
+ "toolCallId": "7d5678e4-fba5-43f1-bd2b-d770667f6bb1",
1209
+ "toolName": "write",
1210
+ "content": [
1211
+ {
1212
+ "type": "text",
1213
+ "text": "Wrote /workspace/missing.txt"
1214
+ }
1215
+ ],
1216
+ "details": {},
1217
+ "isError": false,
1218
+ "timestamp": 1789201342688
1219
+ },
1220
+ {
1221
+ "role": "assistant",
1222
+ "api": "minicpm-webgpu",
1223
+ "provider": "browser",
1224
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
1225
+ "timestamp": 1789201342688,
1226
+ "content": [
1227
+ {
1228
+ "type": "toolCall",
1229
+ "id": "3f6e9269-4018-408d-9055-7e1a8ed81ab7",
1230
+ "name": "read",
1231
+ "arguments": {
1232
+ "path": "/workspace/missing.txt"
1233
+ }
1234
+ }
1235
+ ],
1236
+ "stopReason": "toolUse",
1237
+ "usage": {
1238
+ "input": 858,
1239
+ "output": 18,
1240
+ "cacheRead": 0,
1241
+ "cacheWrite": 0,
1242
+ "totalTokens": 876,
1243
+ "cost": {
1244
+ "input": 0,
1245
+ "output": 0,
1246
+ "cacheRead": 0,
1247
+ "cacheWrite": 0,
1248
+ "total": 0
1249
+ }
1250
+ }
1251
+ },
1252
+ {
1253
+ "role": "toolResult",
1254
+ "toolCallId": "3f6e9269-4018-408d-9055-7e1a8ed81ab7",
1255
+ "toolName": "read",
1256
+ "content": [
1257
+ {
1258
+ "type": "text",
1259
+ "text": "Recovered"
1260
+ }
1261
+ ],
1262
+ "details": {},
1263
+ "isError": false,
1264
+ "timestamp": 1789201343745
1265
+ },
1266
+ {
1267
+ "role": "assistant",
1268
+ "api": "minicpm-webgpu",
1269
+ "provider": "browser",
1270
+ "model": "Mike0021/MiniCPM5-2B-ONNX",
1271
+ "timestamp": 1789201343745,
1272
+ "content": [
1273
+ {
1274
+ "type": "text",
1275
+ "text": "The file `/workspace/missing.txt` did not exist, so I created it with the exact text \"Recovered\" and verified it by reading the file back, which confirms the content is \"Recovered\"."
1276
+ }
1277
+ ],
1278
+ "stopReason": "stop",
1279
+ "usage": {
1280
+ "input": 907,
1281
+ "output": 43,
1282
+ "cacheRead": 0,
1283
+ "cacheWrite": 0,
1284
+ "totalTokens": 950,
1285
+ "cost": {
1286
+ "input": 0,
1287
+ "output": 0,
1288
+ "cacheRead": 0,
1289
+ "cacheWrite": 0,
1290
+ "total": 0
1291
+ }
1292
+ }
1293
+ }
1294
+ ]
1295
+ }
1296
+ ],
1297
+ "errors": [],
1298
+ "requests": [
1299
+ {
1300
+ "method": "GET",
1301
+ "url": "http://127.0.0.1:4173/"
1302
+ },
1303
+ {
1304
+ "method": "GET",
1305
+ "url": "http://127.0.0.1:4173/assets/index-DOivj3sw.js"
1306
+ },
1307
+ {
1308
+ "method": "GET",
1309
+ "url": "http://127.0.0.1:4173/assets/index-DL-Ot0hN.css"
1310
+ },
1311
+ {
1312
+ "method": "GET",
1313
+ "url": "http://127.0.0.1:4173/assets/agent.worker-DqQzo9yL.js"
1314
+ },
1315
+ {
1316
+ "method": "GET",
1317
+ "url": "http://127.0.0.1:4173/runtime/ort-wasm-simd-threaded.asyncify.mjs"
1318
+ },
1319
+ {
1320
+ "method": "GET",
1321
+ "url": "http://127.0.0.1:4173/runtime/ort-wasm-simd-threaded.asyncify.wasm"
1322
+ },
1323
+ {
1324
+ "method": "GET",
1325
+ "url": "http://127.0.0.1:4173/"
1326
+ },
1327
+ {
1328
+ "method": "GET",
1329
+ "url": "http://127.0.0.1:4173/assets/index-DOivj3sw.js"
1330
+ },
1331
+ {
1332
+ "method": "GET",
1333
+ "url": "http://127.0.0.1:4173/assets/index-DL-Ot0hN.css"
1334
+ },
1335
+ {
1336
+ "method": "GET",
1337
+ "url": "http://127.0.0.1:4173/assets/agent.worker-DqQzo9yL.js"
1338
+ },
1339
+ {
1340
+ "method": "GET",
1341
+ "url": "http://127.0.0.1:4173/favicon.svg"
1342
+ },
1343
+ {
1344
+ "method": "GET",
1345
+ "url": "http://127.0.0.1:4173/runtime/ort-wasm-simd-threaded.asyncify.mjs"
1346
+ },
1347
+ {
1348
+ "method": "GET",
1349
+ "url": "http://127.0.0.1:4173/runtime/ort-wasm-simd-threaded.asyncify.wasm"
1350
+ }
1351
+ ],
1352
+ "coldLoadMs": 2281,
1353
+ "device": {
1354
+ "vendor": "amd",
1355
+ "architecture": "rdna-3",
1356
+ "maxStorageBufferBindingSize": 4294967292,
1357
+ "features": [
1358
+ "depth32float-stencil8",
1359
+ "chromium-experimental-subgroup-matrix",
1360
+ "rg11b10ufloat-renderable",
1361
+ "bgra8unorm-storage",
1362
+ "texture-formats-tier1",
1363
+ "chromium-experimental-multi-draw-indirect",
1364
+ "texture-compression-bc",
1365
+ "dual-source-blending",
1366
+ "core-features-and-limits",
1367
+ "chromium-experimental-sampling-resource-table",
1368
+ "chromium-experimental-timestamp-query-inside-passes",
1369
+ "float32-filterable",
1370
+ "indirect-first-instance",
1371
+ "float32-blendable",
1372
+ "subgroup-size-control",
1373
+ "texture-compression-unaligned",
1374
+ "depth-clip-control",
1375
+ "texture-compression-bc-sliced-3d",
1376
+ "clip-distances",
1377
+ "shader-f16",
1378
+ "texture-formats-tier2",
1379
+ "timestamp-query",
1380
+ "primitive-index",
1381
+ "texture-component-swizzle",
1382
+ "subgroups"
1383
+ ],
1384
+ "actualLimits": {
1385
+ "maxStorageBufferBindingSize": 4294967292,
1386
+ "maxBufferSize": 4294967292
1387
+ }
1388
+ },
1389
+ "cancellation": {
1390
+ "passed": true,
1391
+ "completedGenerations": 0
1392
+ },
1393
+ "diagnostics": {
1394
+ "events": [
1395
+ "agent_start",
1396
+ "turn_start",
1397
+ "message_start",
1398
+ "message_end",
1399
+ "message_start",
1400
+ "message_update",
1401
+ "message_update",
1402
+ "message_end",
1403
+ "tool_execution_start",
1404
+ "tool_execution_end",
1405
+ "message_start",
1406
+ "message_end",
1407
+ "turn_end",
1408
+ "turn_start",
1409
+ "message_start",
1410
+ "message_update",
1411
+ "message_update",
1412
+ "message_update",
1413
+ "message_update",
1414
+ "message_update",
1415
+ "message_update",
1416
+ "message_update",
1417
+ "message_update",
1418
+ "message_update",
1419
+ "message_update",
1420
+ "message_update",
1421
+ "message_update",
1422
+ "message_update",
1423
+ "message_end",
1424
+ "turn_end",
1425
+ "agent_end",
1426
+ "agent_start",
1427
+ "turn_start",
1428
+ "message_start",
1429
+ "message_end",
1430
+ "message_start",
1431
+ "message_update",
1432
+ "message_update",
1433
+ "message_end",
1434
+ "tool_execution_start",
1435
+ "tool_execution_end",
1436
+ "message_start",
1437
+ "message_end",
1438
+ "turn_end",
1439
+ "turn_start",
1440
+ "message_start",
1441
+ "message_update",
1442
+ "message_update",
1443
+ "message_end",
1444
+ "tool_execution_start",
1445
+ "tool_execution_end",
1446
+ "message_start",
1447
+ "message_end",
1448
+ "turn_end",
1449
+ "turn_start",
1450
+ "message_start",
1451
+ "message_update",
1452
+ "message_update",
1453
+ "message_end",
1454
+ "tool_execution_start",
1455
+ "tool_execution_end",
1456
+ "message_start",
1457
+ "message_end",
1458
+ "turn_end",
1459
+ "turn_start",
1460
+ "message_start",
1461
+ "message_update",
1462
+ "message_update",
1463
+ "message_end",
1464
+ "tool_execution_start",
1465
+ "tool_execution_end",
1466
+ "message_start",
1467
+ "message_end",
1468
+ "turn_end",
1469
+ "turn_start",
1470
+ "message_start",
1471
+ "message_update",
1472
+ "message_update",
1473
+ "message_update",
1474
+ "message_update",
1475
+ "message_update",
1476
+ "message_update",
1477
+ "message_update",
1478
+ "message_update",
1479
+ "message_update",
1480
+ "message_update",
1481
+ "message_update",
1482
+ "message_update",
1483
+ "message_update",
1484
+ "message_update",
1485
+ "message_update",
1486
+ "message_update",
1487
+ "message_update",
1488
+ "message_update",
1489
+ "message_update",
1490
+ "message_update",
1491
+ "message_end",
1492
+ "turn_end",
1493
+ "agent_end",
1494
+ "agent_start",
1495
+ "turn_start",
1496
+ "message_start",
1497
+ "message_end",
1498
+ "message_start",
1499
+ "message_update",
1500
+ "message_update",
1501
+ "message_end",
1502
+ "tool_execution_start",
1503
+ "tool_execution_end",
1504
+ "message_start",
1505
+ "message_end",
1506
+ "turn_end",
1507
+ "turn_start",
1508
+ "message_start",
1509
+ "message_update",
1510
+ "message_update",
1511
+ "message_end",
1512
+ "tool_execution_start",
1513
+ "tool_execution_end",
1514
+ "message_start",
1515
+ "message_end",
1516
+ "turn_end",
1517
+ "turn_start",
1518
+ "message_start",
1519
+ "message_update",
1520
+ "message_update",
1521
+ "message_update",
1522
+ "message_update",
1523
+ "message_update",
1524
+ "message_update",
1525
+ "message_update",
1526
+ "message_update",
1527
+ "message_update",
1528
+ "message_update",
1529
+ "message_update",
1530
+ "message_update",
1531
+ "message_update",
1532
+ "message_update",
1533
+ "message_update",
1534
+ "message_update",
1535
+ "message_update",
1536
+ "message_update",
1537
+ "message_update",
1538
+ "message_update",
1539
+ "message_update",
1540
+ "message_update",
1541
+ "message_update",
1542
+ "message_update",
1543
+ "message_update",
1544
+ "message_end",
1545
+ "turn_end",
1546
+ "agent_end",
1547
+ "agent_start",
1548
+ "turn_start",
1549
+ "message_start",
1550
+ "message_end",
1551
+ "message_start",
1552
+ "message_update",
1553
+ "message_update",
1554
+ "message_end",
1555
+ "tool_execution_start",
1556
+ "tool_execution_end",
1557
+ "message_start",
1558
+ "message_end",
1559
+ "turn_end",
1560
+ "turn_start",
1561
+ "message_start",
1562
+ "message_update",
1563
+ "message_update",
1564
+ "message_end",
1565
+ "tool_execution_start",
1566
+ "tool_execution_end",
1567
+ "message_start",
1568
+ "message_end",
1569
+ "turn_end",
1570
+ "turn_start",
1571
+ "message_start",
1572
+ "message_update",
1573
+ "message_update",
1574
+ "message_end",
1575
+ "tool_execution_start",
1576
+ "tool_execution_end",
1577
+ "message_start",
1578
+ "message_end",
1579
+ "turn_end",
1580
+ "turn_start",
1581
+ "message_start",
1582
+ "message_update",
1583
+ "message_update",
1584
+ "message_update",
1585
+ "message_update",
1586
+ "message_update",
1587
+ "message_update",
1588
+ "message_update",
1589
+ "message_update",
1590
+ "message_update",
1591
+ "message_update",
1592
+ "message_update",
1593
+ "message_end",
1594
+ "turn_end",
1595
+ "agent_end",
1596
+ "agent_start",
1597
+ "turn_start",
1598
+ "message_start",
1599
+ "message_end",
1600
+ "message_start",
1601
+ "message_update",
1602
+ "message_update",
1603
+ "message_end",
1604
+ "tool_execution_start",
1605
+ "tool_execution_end",
1606
+ "message_start",
1607
+ "message_end",
1608
+ "turn_end",
1609
+ "turn_start",
1610
+ "message_start",
1611
+ "message_update",
1612
+ "message_update",
1613
+ "message_end",
1614
+ "tool_execution_start",
1615
+ "tool_execution_end",
1616
+ "message_start",
1617
+ "message_end",
1618
+ "turn_end",
1619
+ "turn_start",
1620
+ "message_start",
1621
+ "message_update",
1622
+ "message_update",
1623
+ "message_end",
1624
+ "tool_execution_start",
1625
+ "tool_execution_end",
1626
+ "message_start",
1627
+ "message_end",
1628
+ "turn_end",
1629
+ "turn_start",
1630
+ "message_start",
1631
+ "message_update",
1632
+ "message_update",
1633
+ "message_update",
1634
+ "message_update",
1635
+ "message_update",
1636
+ "message_update",
1637
+ "message_update",
1638
+ "message_update",
1639
+ "message_update",
1640
+ "message_update",
1641
+ "message_update",
1642
+ "message_update",
1643
+ "message_update",
1644
+ "message_update",
1645
+ "message_update",
1646
+ "message_update",
1647
+ "message_update",
1648
+ "message_update",
1649
+ "message_update",
1650
+ "message_update",
1651
+ "message_update",
1652
+ "message_update",
1653
+ "message_update",
1654
+ "message_update",
1655
+ "message_update",
1656
+ "message_update",
1657
+ "message_update",
1658
+ "message_update",
1659
+ "message_update",
1660
+ "message_update",
1661
+ "message_update",
1662
+ "message_end",
1663
+ "turn_end",
1664
+ "agent_end",
1665
+ "agent_start",
1666
+ "turn_start",
1667
+ "message_start",
1668
+ "message_end",
1669
+ "message_start",
1670
+ "message_update",
1671
+ "message_update",
1672
+ "message_update",
1673
+ "message_update",
1674
+ "message_end",
1675
+ "turn_end",
1676
+ "agent_end"
1677
+ ],
1678
+ "generations": [
1679
+ {
1680
+ "type": "generation",
1681
+ "raw": "<function name=\"read\"><param name=\"path\">/workspace/secret.txt</param></function><|im_end|>",
1682
+ "inputTokens": 717,
1683
+ "outputTokens": 18,
1684
+ "elapsedMs": 1002.8000000044703,
1685
+ "firstTokenMs": 666.3999999985099,
1686
+ "stopReason": "toolUse"
1687
+ },
1688
+ {
1689
+ "type": "generation",
1690
+ "raw": "The read tool returned the contents of secret.txt as:\n\nLOCAL-fd008be1<|im_end|>",
1691
+ "inputTokens": 771,
1692
+ "outputTokens": 20,
1693
+ "elapsedMs": 1041.2000000029802,
1694
+ "firstTokenMs": 677.9000000059605,
1695
+ "stopReason": "stop"
1696
+ },
1697
+ {
1698
+ "type": "generation",
1699
+ "raw": "<function name=\"read\"><param name=\"path\">/workspace/sales.csv</param></function><|im_end|>",
1700
+ "inputTokens": 726,
1701
+ "outputTokens": 18,
1702
+ "elapsedMs": 965.1000000014901,
1703
+ "firstTokenMs": 641.2000000029802,
1704
+ "stopReason": "toolUse"
1705
+ },
1706
+ {
1707
+ "type": "generation",
1708
+ "raw": "<function name=\"bash\"><param name=\"command\"><![CDATA[awk -F',' 'NR>1 {rev=$2*$3; total+=rev; print $1,$2,$3,\"->\",$2,\"x\",$3,\"=\",rev} END {print \"TOTAL REVENUE:\", total}' sales.csv > revenue.txt && cat revenue.txt]]></param></function><|im_end|>",
1709
+ "inputTokens": 798,
1710
+ "outputTokens": 78,
1711
+ "elapsedMs": 2038.1000000014901,
1712
+ "firstTokenMs": 690.6000000014901,
1713
+ "stopReason": "toolUse"
1714
+ },
1715
+ {
1716
+ "type": "generation",
1717
+ "raw": "<function name=\"write\"><param name=\"path\">report.md</param><param name=\"content\"><![CDATA[# Sales Report\n\nTotal revenue from sales.csv is **$144**.\n\n- Notebook: 12 Γ— $5 = $60\n- Pencil: 30 Γ— $2 = $60\n- Folder: 8 Γ— $3 = $24\n]]></param></function><|im_end|>",
1718
+ "inputTokens": 964,
1719
+ "outputTokens": 79,
1720
+ "elapsedMs": 2203.0999999940395,
1721
+ "firstTokenMs": 839.0999999940395,
1722
+ "stopReason": "toolUse"
1723
+ },
1724
+ {
1725
+ "type": "generation",
1726
+ "raw": "<function name=\"read\"><param name=\"path\">/workspace/report.md</param></function><|im_end|>",
1727
+ "inputTokens": 1079,
1728
+ "outputTokens": 18,
1729
+ "elapsedMs": 1214,
1730
+ "firstTokenMs": 906.7000000029802,
1731
+ "stopReason": "toolUse"
1732
+ },
1733
+ {
1734
+ "type": "generation",
1735
+ "raw": "Done. The report is saved at `/workspace/report.md` with total revenue of $144 (Notebook $60, Pencil $60, Folder $24).<|im_end|>",
1736
+ "inputTokens": 1180,
1737
+ "outputTokens": 35,
1738
+ "elapsedMs": 1609.3999999985099,
1739
+ "firstTokenMs": 984.7999999970198,
1740
+ "stopReason": "stop"
1741
+ },
1742
+ {
1743
+ "type": "generation",
1744
+ "raw": "<function name=\"write\"><param name=\"path\">/workspace/hello.sh</param><param name=\"content\"><![CDATA[#!/bin/bash\necho \"Hello from the browser\"\n]]></param></function><|im_end|>",
1745
+ "inputTokens": 725,
1746
+ "outputTokens": 43,
1747
+ "elapsedMs": 1358.5,
1748
+ "firstTokenMs": 638.8999999985099,
1749
+ "stopReason": "toolUse"
1750
+ },
1751
+ {
1752
+ "type": "generation",
1753
+ "raw": "<function name=\"bash\"><param name=\"command\"><![CDATA[chmod +x /workspace/hello.sh && /workspace/hello.sh]]></param></function><|im_end|>",
1754
+ "inputTokens": 804,
1755
+ "outputTokens": 34,
1756
+ "elapsedMs": 1441.5,
1757
+ "firstTokenMs": 686.5,
1758
+ "stopReason": "toolUse"
1759
+ },
1760
+ {
1761
+ "type": "generation",
1762
+ "raw": "The script `/workspace/hello.sh` prints \"Hello from the browser\". Running it outputs:\n\n```\nHello from the browser\n```\n\nwith exit code 0.<|im_end|>",
1763
+ "inputTokens": 878,
1764
+ "outputTokens": 35,
1765
+ "elapsedMs": 1343.4000000059605,
1766
+ "firstTokenMs": 747.6000000014901,
1767
+ "stopReason": "stop"
1768
+ },
1769
+ {
1770
+ "type": "generation",
1771
+ "raw": "<function name=\"read\"><param name=\"path\">hello.sh</param></function><|im_end|>",
1772
+ "inputTokens": 731,
1773
+ "outputTokens": 15,
1774
+ "elapsedMs": 893.0999999940395,
1775
+ "firstTokenMs": 638.7999999970198,
1776
+ "stopReason": "toolUse"
1777
+ },
1778
+ {
1779
+ "type": "generation",
1780
+ "raw": "<function name=\"edit\"><param name=\"path\">hello.sh</param><param name=\"oldText\">echo \"Hello from the browser\"</param><param name=\"newText\">echo \"Hello from Pi\"</param></function><|im_end|>",
1781
+ "inputTokens": 790,
1782
+ "outputTokens": 42,
1783
+ "elapsedMs": 1381.2000000029802,
1784
+ "firstTokenMs": 679.3999999985099,
1785
+ "stopReason": "toolUse"
1786
+ },
1787
+ {
1788
+ "type": "generation",
1789
+ "raw": "<function name=\"bash\"><param name=\"command\"><![CDATA[chmod +x hello.sh && ./hello.sh]]></param></function><|im_end|>",
1790
+ "inputTokens": 867,
1791
+ "outputTokens": 29,
1792
+ "elapsedMs": 1316.6999999955297,
1793
+ "firstTokenMs": 738.7999999970198,
1794
+ "stopReason": "toolUse"
1795
+ },
1796
+ {
1797
+ "type": "generation",
1798
+ "raw": "The script now outputs \"Hello from Pi\" when run.<|im_end|>",
1799
+ "inputTokens": 935,
1800
+ "outputTokens": 13,
1801
+ "elapsedMs": 1007.1999999955297,
1802
+ "firstTokenMs": 796.8999999985099,
1803
+ "stopReason": "stop"
1804
+ },
1805
+ {
1806
+ "type": "generation",
1807
+ "raw": "<function name=\"read\"><param name=\"path\">/workspace/missing.txt</param></function><|im_end|>",
1808
+ "inputTokens": 733,
1809
+ "outputTokens": 18,
1810
+ "elapsedMs": 952.5,
1811
+ "firstTokenMs": 647.2999999970198,
1812
+ "stopReason": "toolUse"
1813
+ },
1814
+ {
1815
+ "type": "generation",
1816
+ "raw": "<function name=\"write\"><param name=\"path\">/workspace/missing.txt</param><param name=\"content\"><![CDATA[Recovered]]></param></function><|im_end|>",
1817
+ "inputTokens": 796,
1818
+ "outputTokens": 32,
1819
+ "elapsedMs": 1242.6000000014901,
1820
+ "firstTokenMs": 685.9000000059605,
1821
+ "stopReason": "toolUse"
1822
+ },
1823
+ {
1824
+ "type": "generation",
1825
+ "raw": "<function name=\"read\"><param name=\"path\">/workspace/missing.txt</param></function><|im_end|>",
1826
+ "inputTokens": 858,
1827
+ "outputTokens": 18,
1828
+ "elapsedMs": 1051.5,
1829
+ "firstTokenMs": 751.1000000014901,
1830
+ "stopReason": "toolUse"
1831
+ },
1832
+ {
1833
+ "type": "generation",
1834
+ "raw": "The file `/workspace/missing.txt` did not exist, so I created it with the exact text \"Recovered\" and verified it by reading the file back, which confirms the content is \"Recovered\".<|im_end|>",
1835
+ "inputTokens": 907,
1836
+ "outputTokens": 43,
1837
+ "elapsedMs": 1566.5,
1838
+ "firstTokenMs": 792.9000000059605,
1839
+ "stopReason": "stop"
1840
+ }
1841
+ ],
1842
+ "device": {
1843
+ "vendor": "amd",
1844
+ "architecture": "rdna-3",
1845
+ "maxStorageBufferBindingSize": 4294967292,
1846
+ "features": [
1847
+ "depth32float-stencil8",
1848
+ "chromium-experimental-subgroup-matrix",
1849
+ "rg11b10ufloat-renderable",
1850
+ "bgra8unorm-storage",
1851
+ "texture-formats-tier1",
1852
+ "chromium-experimental-multi-draw-indirect",
1853
+ "texture-compression-bc",
1854
+ "dual-source-blending",
1855
+ "core-features-and-limits",
1856
+ "chromium-experimental-sampling-resource-table",
1857
+ "chromium-experimental-timestamp-query-inside-passes",
1858
+ "float32-filterable",
1859
+ "indirect-first-instance",
1860
+ "float32-blendable",
1861
+ "subgroup-size-control",
1862
+ "texture-compression-unaligned",
1863
+ "depth-clip-control",
1864
+ "texture-compression-bc-sliced-3d",
1865
+ "clip-distances",
1866
+ "shader-f16",
1867
+ "texture-formats-tier2",
1868
+ "timestamp-query",
1869
+ "primitive-index",
1870
+ "texture-component-swizzle",
1871
+ "subgroups"
1872
+ ],
1873
+ "actualLimits": {
1874
+ "maxStorageBufferBindingSize": 4294967292,
1875
+ "maxBufferSize": 4294967292
1876
+ }
1877
+ }
1878
+ },
1879
+ "editorAndShell": true,
1880
+ "requestsDuringOfflineTasks": [],
1881
+ "warmLoadMs": 2122,
1882
+ "reloadWeightRequests": [],
1883
+ "mobileLayout": "390 Γ— 844; desktop GPU, not a mobile hardware test",
1884
+ "passed": true
1885
+ }
validation/agent-terminal.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "date": "2026-09-12T08:26:57.014Z",
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-BRKUEVCq.js",
8
+ "http://127.0.0.1:4173/assets/index-DL-Ot0hN.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": 90,
30
+ "rows": 41
31
+ },
32
+ "mobile": {
33
+ "cols": 40,
34
+ "rows": 32
35
+ }
36
+ },
37
+ "mobileLimitation": "Desktop Chromium viewport simulation; physical mobile keyboards and GPUs not tested.",
38
+ "passed": true
39
+ }