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