Guard concurrent local previews and publish folder validation
Browse files- app/src/local-folder.mjs +21 -5
- app/tests/local-folder-agent-live.mjs +50 -0
- app/tests/local-folder.test.mjs +14 -0
- assets/{agent.worker-Dng_UV-X.js → agent.worker-BLr4cL8b.js} +1 -1
- assets/{index-DUQ_UspG.js → index-CV4PDQqc.js} +1 -1
- index.html +1 -1
- validation/LOCAL-FOLDER.md +3 -1
- validation/local-folder-agent-live.json +221 -0
- validation/local-folder-browser.json +1 -1
- validation/local-folder-live.json +16 -0
app/src/local-folder.mjs
CHANGED
|
@@ -28,6 +28,7 @@ export class LocalFolderFs {
|
|
| 28 |
this.original = new Map([['/', { kind: 'directory', handle }]]);
|
| 29 |
this.baselines = new Map();
|
| 30 |
this.dirty = new Set();
|
|
|
|
| 31 |
this.skipped = 0;
|
| 32 |
this.baselineBytes = 0;
|
| 33 |
}
|
|
@@ -105,16 +106,25 @@ export class LocalFolderFs {
|
|
| 105 |
async baseline(path) {
|
| 106 |
if (!this.baselines.has(path)) this.baselines.set(path, this.original.get(path)?.kind === 'file' ? await this.loadOriginal(path) : null);
|
| 107 |
}
|
| 108 |
-
async
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
async exists(path) { return this.memory.exists(this.path(path)); }
|
| 112 |
async stat(value) {
|
| 113 |
const path = this.path(value), entry = this.original.get(path);
|
| 114 |
// InMemoryFs.stat materializes lazy files. Avoid that for ls/find/indexing.
|
| 115 |
-
if (entry?.kind === 'file' && !this.dirty.has(path)
|
| 116 |
const file = await entry.handle.getFile();
|
| 117 |
-
return { isFile: true, isDirectory: false, isSymbolicLink: false, mode: 0o644, size: file.size, mtime: new Date(file.lastModified) };
|
| 118 |
}
|
| 119 |
return this.memory.stat(path);
|
| 120 |
}
|
|
@@ -131,6 +141,9 @@ export class LocalFolderFs {
|
|
| 131 |
}
|
| 132 |
async writeFile(value, content, options) {
|
| 133 |
const path = this.path(value);
|
|
|
|
|
|
|
|
|
|
| 134 |
if (path === '/') throw Error('EISDIR: Cannot replace the selected folder.');
|
| 135 |
// Decode using just-bash's encoding semantics before enforcing byte limits.
|
| 136 |
const temporary = new InMemoryFs({}, { maxTotalBytes: LOCAL_FILE_LIMIT });
|
|
@@ -150,6 +163,9 @@ export class LocalFolderFs {
|
|
| 150 |
}
|
| 151 |
async rm(value, options) {
|
| 152 |
const path = this.path(value);
|
|
|
|
|
|
|
|
|
|
| 153 |
if (path === '/') throw Error('Cannot remove the selected local folder.');
|
| 154 |
if (!await this.exists(path)) return this.memory.rm(path, options);
|
| 155 |
if ((await this.stat(path)).isDirectory) {
|
|
|
|
| 28 |
this.original = new Map([['/', { kind: 'directory', handle }]]);
|
| 29 |
this.baselines = new Map();
|
| 30 |
this.dirty = new Set();
|
| 31 |
+
this.operations = new Map();
|
| 32 |
this.skipped = 0;
|
| 33 |
this.baselineBytes = 0;
|
| 34 |
}
|
|
|
|
| 106 |
async baseline(path) {
|
| 107 |
if (!this.baselines.has(path)) this.baselines.set(path, this.original.get(path)?.kind === 'file' ? await this.loadOriginal(path) : null);
|
| 108 |
}
|
| 109 |
+
async atPath(path, operation) {
|
| 110 |
+
// A preview can arrive while an agent tool is working. Do not let a slow
|
| 111 |
+
// lazy read materialize its old contents over a more recent staged write.
|
| 112 |
+
const previous = this.operations.get(path) ?? Promise.resolve();
|
| 113 |
+
const pending = previous.catch(() => {}).then(operation);
|
| 114 |
+
this.operations.set(path, pending);
|
| 115 |
+
try { return await pending; }
|
| 116 |
+
finally { if (this.operations.get(path) === pending) this.operations.delete(path); }
|
| 117 |
+
}
|
| 118 |
+
async readFile(value, options) { const path = this.path(value); return this.atPath(path, () => this.memory.readFile(path, options)); }
|
| 119 |
+
async readFileBuffer(value) { const path = this.path(value); return this.atPath(path, () => this.memory.readFileBuffer(path)); }
|
| 120 |
+
async readFileBytes(value) { const path = this.path(value); return this.atPath(path, () => this.memory.readFileBytes(path)); }
|
| 121 |
async exists(path) { return this.memory.exists(this.path(path)); }
|
| 122 |
async stat(value) {
|
| 123 |
const path = this.path(value), entry = this.original.get(path);
|
| 124 |
// InMemoryFs.stat materializes lazy files. Avoid that for ls/find/indexing.
|
| 125 |
+
if (entry?.kind === 'file' && !this.dirty.has(path)) {
|
| 126 |
const file = await entry.handle.getFile();
|
| 127 |
+
return { isFile: true, isDirectory: false, isSymbolicLink: false, mode: 0o644, size: this.baselines.get(path)?.length ?? file.size, mtime: new Date(file.lastModified) };
|
| 128 |
}
|
| 129 |
return this.memory.stat(path);
|
| 130 |
}
|
|
|
|
| 141 |
}
|
| 142 |
async writeFile(value, content, options) {
|
| 143 |
const path = this.path(value);
|
| 144 |
+
return this.atPath(path, () => this.writeAtPath(path, content, options));
|
| 145 |
+
}
|
| 146 |
+
async writeAtPath(path, content, options) {
|
| 147 |
if (path === '/') throw Error('EISDIR: Cannot replace the selected folder.');
|
| 148 |
// Decode using just-bash's encoding semantics before enforcing byte limits.
|
| 149 |
const temporary = new InMemoryFs({}, { maxTotalBytes: LOCAL_FILE_LIMIT });
|
|
|
|
| 163 |
}
|
| 164 |
async rm(value, options) {
|
| 165 |
const path = this.path(value);
|
| 166 |
+
return this.atPath(path, () => this.removeAtPath(path, options));
|
| 167 |
+
}
|
| 168 |
+
async removeAtPath(path, options) {
|
| 169 |
if (path === '/') throw Error('Cannot remove the selected local folder.');
|
| 170 |
if (!await this.exists(path)) return this.memory.rm(path, options);
|
| 171 |
if ((await this.stat(path)).isDirectory) {
|
app/tests/local-folder-agent-live.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { chromium } from 'playwright';
|
| 2 |
+
import assert from 'node:assert/strict';
|
| 3 |
+
import fs from 'node:fs/promises';
|
| 4 |
+
const root = new URL('../..', import.meta.url).pathname;
|
| 5 |
+
const url = process.argv[2] || 'https://mike0021-minicpm5-2b-webgpu-pi-http.static.hf.space/';
|
| 6 |
+
const result = { date: new Date().toISOString(), url, errors: [], prompt: 'Read app.js and change the exported greeting from Hello to Bonjour. Leave other files unchanged and verify the change.', handle: 'Real OPFS directory handle passed through the native worker protocol; model and tools are not mocked.' };
|
| 7 |
+
const context = await chromium.launchPersistentContext(root + '/validation/http-browser-profile', {
|
| 8 |
+
executablePath: '/usr/bin/chromium', headless: true, viewport: { width: 1440, height: 1000 },
|
| 9 |
+
args: ['--enable-unsafe-webgpu', '--use-angle=vulkan', '--enable-features=Vulkan', '--disable-vulkan-surface'],
|
| 10 |
+
});
|
| 11 |
+
const page = await context.newPage(); page.setDefaultTimeout(120_000);
|
| 12 |
+
page.on('pageerror', e => result.errors.push(e.message));
|
| 13 |
+
const idle = () => page.waitForFunction(() => window.browserPi && !window.browserPi.busy);
|
| 14 |
+
let timer;
|
| 15 |
+
try {
|
| 16 |
+
await page.goto(url); await page.waitForFunction(() => window.browserPi); await page.evaluate(() => window.browserPi.ready); await idle();
|
| 17 |
+
await page.evaluate(async () => {
|
| 18 |
+
const root = await navigator.storage.getDirectory();
|
| 19 |
+
try { await root.removeEntry('local-agent-check', { recursive: true }); } catch (e) { if (e.name !== 'NotFoundError') throw e; }
|
| 20 |
+
window.testFolder = await root.getDirectoryHandle('local-agent-check', { create: true });
|
| 21 |
+
for (const [name, content] of [['README.md', '# Example JavaScript project\n'], ['app.js', 'export const greeting = "Hello";\n']]) {
|
| 22 |
+
const stream = await (await window.testFolder.getFileHandle(name, { create: true })).createWritable(); await stream.write(content); await stream.close();
|
| 23 |
+
}
|
| 24 |
+
window.showDirectoryPicker = async () => window.testFolder;
|
| 25 |
+
});
|
| 26 |
+
await page.locator('#open-folder').click(); await page.locator('#local-folder-bar').waitFor({ state: 'visible' }); await idle(); await page.waitForTimeout(300);
|
| 27 |
+
const startLoad = Date.now(); await page.evaluate(() => window.browserPi.load()); result.loadMs = Date.now() - startLoad; await idle(); await page.waitForTimeout(300);
|
| 28 |
+
const started = Date.now();
|
| 29 |
+
const pending = page.evaluate(prompt => window.browserPi.prompt(prompt), result.prompt);
|
| 30 |
+
try { await Promise.race([pending, new Promise((_, reject) => { timer = setTimeout(() => reject(Error('Model trial exceeded 120 seconds')), 120000); })]); }
|
| 31 |
+
catch (error) { await page.evaluate(() => window.browserPi.rpc('stop')); await pending.catch(() => {}); throw error; }
|
| 32 |
+
finally { clearTimeout(timer); }
|
| 33 |
+
result.elapsedMs = Date.now() - started; await idle();
|
| 34 |
+
const state = await page.evaluate(() => window.browserPi.rpc('snapshot')); result.messages = state.messages;
|
| 35 |
+
result.patches = await page.evaluate(() => window.browserPi.rpc('local_changes'));
|
| 36 |
+
assert.equal(result.patches.length, 1); assert.equal(result.patches[0].path, '/app.js');
|
| 37 |
+
assert.match(Buffer.from(result.patches[0].after, 'base64').toString(), /greeting\s*=\s*["']Bonjour["']/);
|
| 38 |
+
result.diskBeforeApply = await page.evaluate(async () => (await (await window.testFolder.getFileHandle('app.js')).getFile()).text());
|
| 39 |
+
assert.equal(result.diskBeforeApply, 'export const greeting = "Hello";\n');
|
| 40 |
+
await page.locator('#review-changes').click(); await page.locator('#local-review').waitFor({ state: 'visible' });
|
| 41 |
+
await page.screenshot({ path: root + '/validation/local-folder-agent-review.png' });
|
| 42 |
+
await page.locator('#review-apply').click(); await page.locator('#local-review').waitFor({ state: 'hidden' }); await idle();
|
| 43 |
+
result.diskAfterApply = await page.evaluate(async () => (await (await window.testFolder.getFileHandle('app.js')).getFile()).text());
|
| 44 |
+
assert.equal(result.diskAfterApply, Buffer.from(result.patches[0].after, 'base64').toString());
|
| 45 |
+
result.final = state.messages.filter(m => m.role === 'assistant').at(-1)?.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
| 46 |
+
assert.deepEqual(result.errors, []); result.passed = true;
|
| 47 |
+
await page.locator('#disconnect-folder').click(); await idle();
|
| 48 |
+
console.log(JSON.stringify({ passed: true, loadMs: result.loadMs, elapsedMs: result.elapsedMs, final: result.final, changes: result.patches.length }));
|
| 49 |
+
} catch (error) { result.failure = error.stack; console.error(error); process.exitCode = 1; }
|
| 50 |
+
finally { clearTimeout(timer); await fs.writeFile(root + '/validation/local-folder-agent-live.json', JSON.stringify(result, null, 2)); await context.close(); }
|
app/tests/local-folder.test.mjs
CHANGED
|
@@ -123,3 +123,17 @@ test('Local folder: unsupported operations, excludes, binary editor and traversa
|
|
| 123 |
assert.deepEqual(await local.patches(), []);
|
| 124 |
await assert.rejects(disk.read('outside.txt'), { code: 'ENOENT' });
|
| 125 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
assert.deepEqual(await local.patches(), []);
|
| 124 |
await assert.rejects(disk.read('outside.txt'), { code: 'ENOENT' });
|
| 125 |
});
|
| 126 |
+
test('Local folder: an in-flight editor preview cannot overwrite a staged tool edit', async t => {
|
| 127 |
+
const disk = await fixture(t), local = await LocalFolderFs.open(disk.directory);
|
| 128 |
+
let unblock, started;
|
| 129 |
+
const gate = new Promise(resolve => { unblock = resolve; });
|
| 130 |
+
const loading = new Promise(resolve => { started = resolve; });
|
| 131 |
+
const load = local.loadOriginal.bind(local);
|
| 132 |
+
local.loadOriginal = async path => { started(); await gate; return load(path); };
|
| 133 |
+
const preview = local.readFile('/README.md'); await loading;
|
| 134 |
+
const edit = local.writeFile('/README.md', 'new staged version');
|
| 135 |
+
unblock(); await Promise.all([preview, edit]);
|
| 136 |
+
assert.equal(await local.readFile('/README.md'), 'new staged version');
|
| 137 |
+
assert.equal(await disk.read('README.md'), '# Local project\n');
|
| 138 |
+
assert.equal((await local.patches()).length, 1);
|
| 139 |
+
});
|
assets/{agent.worker-Dng_UV-X.js → agent.worker-BLr4cL8b.js}
RENAMED
|
@@ -1766,7 +1766,7 @@ Folder,8
|
|
| 1766 |
`},cFe=e=>e.length>6e3?e.slice(0,6e3)+`
|
| 1767 |
[Output truncated at 6,000 characters; use head, grep, or a narrower query.]`:e,k9=(e,t={})=>({content:[{type:`text`,text:cFe(e)}],details:t});function A9(e=O9,t,n){t&&(e=Object.fromEntries(t.filter(([,e])=>e.type===`file`).map(([e,t])=>[e,{mode:t.mode,content:Uint8Array.from(atob(t.base64),e=>e.charCodeAt(0))}])));let r=new tFe({...n?{fs:n.mount()}:{files:e},cwd:`/workspace`,executionLimitProfile:`hardened`,fetch:oFe(),executionLimits:{maxExecutionTimeMs:1e4,maxCommandCount:3e3,maxFileSystemBytes:16*1024**2,maxOutputSize:262144,maxSourceBytes:131072}}),i=e=>r.fs.resolvePath(`/workspace`,e),a=(async()=>{for(let[e,n]of t??[])n.type===`directory`?(await r.fs.mkdir(e,{recursive:!0}),await r.fs.chmod(e,n.mode)):n.type===`symlink`&&await r.fs.symlink(n.target,e)})();async function o(e,t){let n=i(e);if(new TextEncoder().encode(t).length>1024**2)throw Error(`The editor accepts files up to 1 MiB.`);await r.fs.mkdir(n.slice(0,n.lastIndexOf(`/`))||`/`,{recursive:!0}),await r.fs.writeFile(n,t)}let s=[{name:`bash`,label:`Bash`,description:`Run a bash command in /workspace. Supports cat, ls, grep, sed, awk, jq, sort, pipes, redirects, shell scripts, and curl. HTTP GET/HEAD/POST is enabled for any HTTPS URL; there is no host allowlist. Use curl -fsS URL; use set -o pipefail before piping HTTP output to jq. Browser CORS applies; redirects require the final URL. HTTP limits: 8 seconds, 1 MiB. No Node.js, Python, npm, or native binaries. Files persist across commands; cd and variables reset each call.`,parameters:Sn({command:Yr({description:`The shell command to execute.`})}),execute:async(e,{command:t},n)=>{let i=await r.exec(t,{signal:n});return k9(`${i.stdout}${i.stderr?`
|
| 1768 |
STDERR:
|
| 1769 |
-
`+i.stderr:``}\nExit code: ${i.exitCode}`,{exitCode:i.exitCode})}},{name:`read`,label:`Read file`,description:`Read a UTF-8 text file. Use bash head/grep for large files.`,parameters:Sn({path:Yr()}),execute:async(e,t)=>{let n=await r.fs.realpath(i(t.path));return k9(await r.fs.readFile(n),{path:n})}},{name:`write`,label:`Write file`,description:`Create or replace a UTF-8 text file. Parent directories are created.`,parameters:Sn({path:Yr(),content:Yr()}),execute:async(e,t)=>(await o(t.path,t.content),k9(`Wrote `+i(t.path)))},{name:`edit`,label:`Edit file`,description:`Replace one exact occurrence of oldText with newText in a UTF-8 file. oldText must match exactly once.`,parameters:Sn({path:Yr(),oldText:Yr(),newText:Yr()}),execute:async(e,t)=>{let n=await r.fs.readFile(i(t.path));if(!t.oldText||n.split(t.oldText).length!==2)throw Error(`oldText must match exactly once. Read the file first.`);return await o(t.path,n.replace(t.oldText,()=>t.newText)),k9(`Edited `+i(t.path))}}];async function c(){if(n)return n.listing();let e={};for(let t of r.fs.getAllPaths().sort())t.startsWith(`/bin/`)||t.startsWith(`/usr/bin/`)||t.startsWith(`/dev/`)||t.startsWith(`/proc/`)||(await r.fs.lstat(t)).isFile&&(e[t]=await r.fs.readFile(t));return e}async function l(){if(n)return null;let e=[];for(let t of r.fs.getAllPaths().sort()){if([`/bin`,`/usr/bin`,`/dev`,`/proc`].some(e=>t===e||t.startsWith(e+`/`)))continue;let n=await r.fs.lstat(t);if(n.isSymbolicLink)e.push([t,{type:`symlink`,target:await r.fs.readlink(t)}]);else if(n.isDirectory)e.push([t,{type:`directory`,mode:n.mode}]);else if(n.isFile){let i=await r.fs.readFileBuffer(t),a=``;for(let e=0;e<i.length;e+=32768)a+=String.fromCharCode(...i.subarray(e,e+32768));e.push([t,{type:`file`,base64:btoa(a),mode:n.mode}])}}return e}return{bash:r,tools:s,ready:a,write:o,snapshot:c,serialize:l,localFolder:n,read:e=>r.fs.readFile(i(e))}}async function lFe(){let e=await new Promise((e,t)=>{let n=indexedDB.open(`minicpm5-pi-workspace`,1);n.onupgradeneeded=()=>n.result.createObjectStore(`state`),n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)});return{async load(t=`current`){return new Promise((n,r)=>{let i=e.transaction(`state`).objectStore(`state`).get(t);i.onsuccess=()=>n(i.result),i.onerror=()=>r(i.error)})},async save(t,n=`current`){return new Promise((r,i)=>{let a=e.transaction(`state`,`readwrite`);a.objectStore(`state`).put(t,n),a.oncomplete=r,a.onerror=()=>i(a.error),a.onabort=()=>i(a.error)})}}}const j9=1024**2,M9=16*j9,N9=new Set([`.git`,`node_modules`,`.venv`,`__pycache__`]),P9=(e,t)=>e?.length===t?.length&&(e==null||e.every((e,n)=>e===t[n]));function F9(e){if(e==null)return null;let t=``;for(let n=0;n<e.length;n+=32768)t+=String.fromCharCode(...e.subarray(n,n+32768));return btoa(t)}const I9=e=>e==null?null:Uint8Array.from(atob(e),e=>e.charCodeAt(0));function uFe(e){if(e==null)return``;let t=new TextDecoder(`utf-8`,{fatal:!0}).decode(e);if(t.includes(`\0`))throw Error(`This is a binary file. The editor supports UTF-8 text.`);return t}const L9=()=>{throw Error(`ENOTSUP: Local folders do not support Unix permissions, timestamps, or links.`)};var R9=class e{constructor(e){this.handle=e,this.memory=new N2({},{maxTotalBytes:M9}),this.original=new Map([[`/`,{kind:`directory`,handle:e}]]),this.baselines=new Map,this.dirty=new Set,this.skipped=0,this.baselineBytes=0}static async open(t,{signal:n,patches:r=[]}={}){if(t?.kind!==`directory`)throw Error(`Select a folder.`);let i=new e(t),a=0;async function o(e,t,r){if(r>40)throw Error(`This folder is too deeply nested. Choose a smaller project folder.`);for await(let[s,c]of e.entries()){if(n?.throwIfAborted(),N9.has(s)){i.skipped++;continue}if(++a>5e3)throw Error(`This folder contains more than 5,000 entries. Choose a smaller project folder.`);let e=t+`/`+s;i.original.set(e,{kind:c.kind,handle:c}),c.kind===`directory`?(await i.memory.mkdir(e,{recursive:!0}),await o(c,e,r+1)):i.memory.writeFileLazy(e,()=>i.loadOriginal(e))}}await o(t,``,0);for(let e of r){let t=i.path(e.path);if(t===`/`)throw Error(`Invalid saved local-folder change.`);let n=I9(e.before);if(i.baselineBytes+=n?.length??0,i.baselineBytes>M9)throw Error(`Saved changes exceed the 16 MiB local-folder limit.`);i.baselines.set(t,n),i.dirty.add(t),e.kind===`directory`?await i.memory.mkdir(t,{recursive:!0}):e.after===null?await i.memory.rm(t,{force:!0}):(await i.memory.mkdir(i.parent(t),{recursive:!0}),await i.memory.writeFile(t,I9(e.after)))}return i}path(e){if(typeof e!=`string`||e.includes(`\0`))throw Error(`Invalid file path.`);let t=this.memory.resolvePath(`/`,e);if(t.split(`/`).some(e=>N9.has(e)))throw Error(`This folder is excluded: .git, node_modules, .venv, and __pycache__.`);return t}parent(e){return e.slice(0,e.lastIndexOf(`/`))||`/`}async diskBytes(e){let t=await this.diskEntry(e);if(!t)return null;if(t.kind!==`file`)throw Error(`A folder now occupies `+e+`. No file was overwritten.`);let n=await t.getFile();if(n.size>j9)throw Error(e+` is larger than the 1 MiB per-file limit.`);return new Uint8Array(await n.arrayBuffer())}async diskEntry(e){let t=this.handle,n=this.path(e).split(`/`).filter(Boolean);if(!n.length)return t;try{for(let e of n.slice(0,-1))t=await t.getDirectoryHandle(e);try{return await t.getFileHandle(n.at(-1))}catch(e){if(e.name!==`TypeMismatchError`)throw e;return await t.getDirectoryHandle(n.at(-1))}}catch(e){if(e.name===`NotFoundError`)return null;throw e}}async loadOriginal(e){let t=await this.diskBytes(e);if(!t)throw Error(`ENOENT: File was removed from disk: `+e+`. Refresh the folder.`);if(!this.baselines.has(e)){if(this.baselineBytes+t.length>M9)throw Error(`Local file reads reached the 16 MiB session limit. Apply or discard changes, then Refresh.`);this.baselines.set(e,t),this.baselineBytes+=t.length}return t}async baseline(e){this.baselines.has(e)||this.baselines.set(e,this.original.get(e)?.kind===`file`?await this.loadOriginal(e):null)}async readFile(e,t){return this.memory.readFile(this.path(e),t)}async readFileBuffer(e){return this.memory.readFileBuffer(this.path(e))}async readFileBytes(e){return this.memory.readFileBytes(this.path(e))}async exists(e){return this.memory.exists(this.path(e))}async stat(e){let t=this.path(e),n=this.original.get(t);if(n?.kind===`file`&&!this.dirty.has(t)&&!this.baselines.has(t)){let e=await n.handle.getFile();return{isFile:!0,isDirectory:!1,isSymbolicLink:!1,mode:420,size:e.size,mtime:new Date(e.lastModified)}}return this.memory.stat(t)}async lstat(e){return this.stat(e)}async realpath(e){return this.memory.realpath(this.path(e))}resolvePath(e,t){return this.memory.resolvePath(e,t)}getAllPaths(){return this.memory.getAllPaths()}async readdir(e){return this.memory.readdir(this.path(e))}async readdirWithFileTypes(e){return this.memory.readdirWithFileTypes(this.path(e))}async mkdir(e,t){let n=this.path(e),r=new Set(this.getAllPaths());await this.memory.mkdir(n,t);for(let e of this.getAllPaths())r.has(e)||(this.dirty.add(e),this.baselines.set(e,null))}async writeFile(e,t,n){let r=this.path(e);if(r===`/`)throw Error(`EISDIR: Cannot replace the selected folder.`);let i=new N2({},{maxTotalBytes:j9});await i.writeFile(`/data`,t,n);let a=await i.readFileBuffer(`/data`);if(a.length>j9)throw Error(`Local files are limited to 1 MiB.`);if(!await this.memory.exists(this.parent(r)))throw Error(`ENOENT: Parent folder does not exist: `+this.parent(r));await this.baseline(r),await this.memory.writeFile(r,a),this.dirty.add(r)}async appendFile(e,t,n){let r=new N2({"/data":await this.exists(e)?await this.readFileBuffer(e):new Uint8Array},{maxTotalBytes:j9});await r.appendFile(`/data`,t,n),await this.writeFile(e,await r.readFileBuffer(`/data`))}async rm(e,t){let n=this.path(e);if(n===`/`)throw Error(`Cannot remove the selected local folder.`);if(!await this.exists(n))return this.memory.rm(n,t);if((await this.stat(n)).isDirectory){if(this.original.has(n))throw Error(`Removing or renaming existing local folders is not supported. Remove individual files instead.`);for(let e of this.getAllPaths().filter(e=>e.startsWith(n+`/`)))if(this.original.has(e))throw Error(`This folder contains existing local files.`);let e=this.getAllPaths().filter(e=>e===n||e.startsWith(n+`/`));await this.memory.rm(n,t);for(let t of e)this.dirty.delete(t),this.baselines.delete(t)}else await this.baseline(n),await this.memory.rm(n,t),this.dirty.add(n)}async cp(e,t,n){let r=this.path(e),i=this.path(t);if(await this.exists(i)&&(await this.stat(i)).isDirectory&&(i+=`/`+r.split(`/`).at(-1)),r===i||i.startsWith(r+`/`))throw Error(`Cannot copy a path into itself.`);if((await this.stat(r)).isDirectory){if(!n?.recursive)throw Error(`EISDIR: Use cp -r to copy a directory.`);await this.mkdir(i,{recursive:!0});for(let e of await this.readdir(r))await this.cp(r+`/`+e,i+`/`+e,n)}else await this.writeFile(i,await this.readFileBuffer(r))}async mv(e,t){if((await this.stat(e)).isDirectory)throw Error(`Renaming local folders is not supported.`);await this.cp(e,t),await this.rm(e)}chmod=L9;symlink=L9;link=L9;readlink=L9;utimes=L9;async patches(){let e=[];for(let t of[...this.dirty].sort()){let n=this.baselines.get(t)??null,r=await this.exists(t),i=r&&(await this.memory.stat(t)).isDirectory?`directory`:`file`,a=r&&i===`file`?await this.readFileBuffer(t):null;i===`file`&&P9(n,a)||e.push({path:t,kind:i,before:F9(n),after:F9(a)})}return e}async listing(){let e=Object.create(null);for(let t of this.getAllPaths()){let n=this.original.get(t);(n?.kind===`file`||!n&&(await this.memory.stat(t)).isFile)&&(e[`/workspace`+t]=null)}return e}async status(){return{name:this.handle.name,count:(await this.patches()).length,skipped:this.skipped}}async apply(e){let t=await this.patches(),n=async e=>{let t=await this.diskEntry(e.path);if(e.kind===`directory`){if(t)throw Error(`Conflict: `+e.path+` was created on disk.`)}else if(!P9(await this.diskBytes(e.path),I9(e.before)))throw Error(`Conflict: `+e.path+` changed on disk. Nothing will overwrite it. Discard staged edits and refresh to read the new version.`)};for(let r of t)e?.throwIfAborted(),await n(r);let r=0;try{for(let i of t){e?.throwIfAborted(),await n(i);let t=i.path.split(`/`).filter(Boolean),a=this.handle;for(let e of t.slice(0,-1))a=await a.getDirectoryHandle(e);let o=t.at(-1);if(i.kind===`directory`){let e=await a.getDirectoryHandle(o,{create:!0});this.original.set(i.path,{kind:`directory`,handle:e})}else if(i.after===null)await a.removeEntry(o),this.original.delete(i.path),this.baselineBytes-=this.baselines.get(i.path)?.length??0,this.baselines.delete(i.path);else{let t=await a.getFileHandle(o,{create:!0}),n;try{n=await t.createWritable(),await n.write(I9(i.after)),e?.throwIfAborted(),await n.close()}catch(e){if(await n?.abort().catch(()=>{}),i.before===null)try{(await t.getFile()).size===0&&await t.isSameEntry(await a.getFileHandle(o))&&await a.removeEntry(o)}catch{}throw e}this.original.set(i.path,{kind:`file`,handle:t}),this.baselineBytes+=I9(i.after).length-(this.baselines.get(i.path)?.length??0),this.baselines.set(i.path,I9(i.after))}this.dirty.delete(i.path),r++}}catch(e){throw Error(`${r} of ${t.length} changes applied. Remaining changes are still staged. ${e.message}`)}return r}mount(){return new J6({mounts:[{mountPoint:`/workspace`,filesystem:this}]})}};const z9=e=>postMessage(e),B9=MI(z9);let V9,H9,U9,W9=!1,G9,K9=0,q9,J9,Y9;const X9=`You are Pi, a coding assistant powered by MiniCPM5-2B, a language model from OpenBMB.
|
| 1770 |
The model, inference, tools, and workspace run entirely in the user's browser. Workspace files are stored in this browser, not on the user's operating-system filesystem.
|
| 1771 |
Use your tools to inspect and change actual files. Never claim a command ran or a file changed without a successful tool result.
|
| 1772 |
When the user explicitly asks you to use a tool, call that tool even if you already know the answer.
|
|
|
|
| 1766 |
`},cFe=e=>e.length>6e3?e.slice(0,6e3)+`
|
| 1767 |
[Output truncated at 6,000 characters; use head, grep, or a narrower query.]`:e,k9=(e,t={})=>({content:[{type:`text`,text:cFe(e)}],details:t});function A9(e=O9,t,n){t&&(e=Object.fromEntries(t.filter(([,e])=>e.type===`file`).map(([e,t])=>[e,{mode:t.mode,content:Uint8Array.from(atob(t.base64),e=>e.charCodeAt(0))}])));let r=new tFe({...n?{fs:n.mount()}:{files:e},cwd:`/workspace`,executionLimitProfile:`hardened`,fetch:oFe(),executionLimits:{maxExecutionTimeMs:1e4,maxCommandCount:3e3,maxFileSystemBytes:16*1024**2,maxOutputSize:262144,maxSourceBytes:131072}}),i=e=>r.fs.resolvePath(`/workspace`,e),a=(async()=>{for(let[e,n]of t??[])n.type===`directory`?(await r.fs.mkdir(e,{recursive:!0}),await r.fs.chmod(e,n.mode)):n.type===`symlink`&&await r.fs.symlink(n.target,e)})();async function o(e,t){let n=i(e);if(new TextEncoder().encode(t).length>1024**2)throw Error(`The editor accepts files up to 1 MiB.`);await r.fs.mkdir(n.slice(0,n.lastIndexOf(`/`))||`/`,{recursive:!0}),await r.fs.writeFile(n,t)}let s=[{name:`bash`,label:`Bash`,description:`Run a bash command in /workspace. Supports cat, ls, grep, sed, awk, jq, sort, pipes, redirects, shell scripts, and curl. HTTP GET/HEAD/POST is enabled for any HTTPS URL; there is no host allowlist. Use curl -fsS URL; use set -o pipefail before piping HTTP output to jq. Browser CORS applies; redirects require the final URL. HTTP limits: 8 seconds, 1 MiB. No Node.js, Python, npm, or native binaries. Files persist across commands; cd and variables reset each call.`,parameters:Sn({command:Yr({description:`The shell command to execute.`})}),execute:async(e,{command:t},n)=>{let i=await r.exec(t,{signal:n});return k9(`${i.stdout}${i.stderr?`
|
| 1768 |
STDERR:
|
| 1769 |
+
`+i.stderr:``}\nExit code: ${i.exitCode}`,{exitCode:i.exitCode})}},{name:`read`,label:`Read file`,description:`Read a UTF-8 text file. Use bash head/grep for large files.`,parameters:Sn({path:Yr()}),execute:async(e,t)=>{let n=await r.fs.realpath(i(t.path));return k9(await r.fs.readFile(n),{path:n})}},{name:`write`,label:`Write file`,description:`Create or replace a UTF-8 text file. Parent directories are created.`,parameters:Sn({path:Yr(),content:Yr()}),execute:async(e,t)=>(await o(t.path,t.content),k9(`Wrote `+i(t.path)))},{name:`edit`,label:`Edit file`,description:`Replace one exact occurrence of oldText with newText in a UTF-8 file. oldText must match exactly once.`,parameters:Sn({path:Yr(),oldText:Yr(),newText:Yr()}),execute:async(e,t)=>{let n=await r.fs.readFile(i(t.path));if(!t.oldText||n.split(t.oldText).length!==2)throw Error(`oldText must match exactly once. Read the file first.`);return await o(t.path,n.replace(t.oldText,()=>t.newText)),k9(`Edited `+i(t.path))}}];async function c(){if(n)return n.listing();let e={};for(let t of r.fs.getAllPaths().sort())t.startsWith(`/bin/`)||t.startsWith(`/usr/bin/`)||t.startsWith(`/dev/`)||t.startsWith(`/proc/`)||(await r.fs.lstat(t)).isFile&&(e[t]=await r.fs.readFile(t));return e}async function l(){if(n)return null;let e=[];for(let t of r.fs.getAllPaths().sort()){if([`/bin`,`/usr/bin`,`/dev`,`/proc`].some(e=>t===e||t.startsWith(e+`/`)))continue;let n=await r.fs.lstat(t);if(n.isSymbolicLink)e.push([t,{type:`symlink`,target:await r.fs.readlink(t)}]);else if(n.isDirectory)e.push([t,{type:`directory`,mode:n.mode}]);else if(n.isFile){let i=await r.fs.readFileBuffer(t),a=``;for(let e=0;e<i.length;e+=32768)a+=String.fromCharCode(...i.subarray(e,e+32768));e.push([t,{type:`file`,base64:btoa(a),mode:n.mode}])}}return e}return{bash:r,tools:s,ready:a,write:o,snapshot:c,serialize:l,localFolder:n,read:e=>r.fs.readFile(i(e))}}async function lFe(){let e=await new Promise((e,t)=>{let n=indexedDB.open(`minicpm5-pi-workspace`,1);n.onupgradeneeded=()=>n.result.createObjectStore(`state`),n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)});return{async load(t=`current`){return new Promise((n,r)=>{let i=e.transaction(`state`).objectStore(`state`).get(t);i.onsuccess=()=>n(i.result),i.onerror=()=>r(i.error)})},async save(t,n=`current`){return new Promise((r,i)=>{let a=e.transaction(`state`,`readwrite`);a.objectStore(`state`).put(t,n),a.oncomplete=r,a.onerror=()=>i(a.error),a.onabort=()=>i(a.error)})}}}const j9=1024**2,M9=16*j9,N9=new Set([`.git`,`node_modules`,`.venv`,`__pycache__`]),P9=(e,t)=>e?.length===t?.length&&(e==null||e.every((e,n)=>e===t[n]));function F9(e){if(e==null)return null;let t=``;for(let n=0;n<e.length;n+=32768)t+=String.fromCharCode(...e.subarray(n,n+32768));return btoa(t)}const I9=e=>e==null?null:Uint8Array.from(atob(e),e=>e.charCodeAt(0));function uFe(e){if(e==null)return``;let t=new TextDecoder(`utf-8`,{fatal:!0}).decode(e);if(t.includes(`\0`))throw Error(`This is a binary file. The editor supports UTF-8 text.`);return t}const L9=()=>{throw Error(`ENOTSUP: Local folders do not support Unix permissions, timestamps, or links.`)};var R9=class e{constructor(e){this.handle=e,this.memory=new N2({},{maxTotalBytes:M9}),this.original=new Map([[`/`,{kind:`directory`,handle:e}]]),this.baselines=new Map,this.dirty=new Set,this.operations=new Map,this.skipped=0,this.baselineBytes=0}static async open(t,{signal:n,patches:r=[]}={}){if(t?.kind!==`directory`)throw Error(`Select a folder.`);let i=new e(t),a=0;async function o(e,t,r){if(r>40)throw Error(`This folder is too deeply nested. Choose a smaller project folder.`);for await(let[s,c]of e.entries()){if(n?.throwIfAborted(),N9.has(s)){i.skipped++;continue}if(++a>5e3)throw Error(`This folder contains more than 5,000 entries. Choose a smaller project folder.`);let e=t+`/`+s;i.original.set(e,{kind:c.kind,handle:c}),c.kind===`directory`?(await i.memory.mkdir(e,{recursive:!0}),await o(c,e,r+1)):i.memory.writeFileLazy(e,()=>i.loadOriginal(e))}}await o(t,``,0);for(let e of r){let t=i.path(e.path);if(t===`/`)throw Error(`Invalid saved local-folder change.`);let n=I9(e.before);if(i.baselineBytes+=n?.length??0,i.baselineBytes>M9)throw Error(`Saved changes exceed the 16 MiB local-folder limit.`);i.baselines.set(t,n),i.dirty.add(t),e.kind===`directory`?await i.memory.mkdir(t,{recursive:!0}):e.after===null?await i.memory.rm(t,{force:!0}):(await i.memory.mkdir(i.parent(t),{recursive:!0}),await i.memory.writeFile(t,I9(e.after)))}return i}path(e){if(typeof e!=`string`||e.includes(`\0`))throw Error(`Invalid file path.`);let t=this.memory.resolvePath(`/`,e);if(t.split(`/`).some(e=>N9.has(e)))throw Error(`This folder is excluded: .git, node_modules, .venv, and __pycache__.`);return t}parent(e){return e.slice(0,e.lastIndexOf(`/`))||`/`}async diskBytes(e){let t=await this.diskEntry(e);if(!t)return null;if(t.kind!==`file`)throw Error(`A folder now occupies `+e+`. No file was overwritten.`);let n=await t.getFile();if(n.size>j9)throw Error(e+` is larger than the 1 MiB per-file limit.`);return new Uint8Array(await n.arrayBuffer())}async diskEntry(e){let t=this.handle,n=this.path(e).split(`/`).filter(Boolean);if(!n.length)return t;try{for(let e of n.slice(0,-1))t=await t.getDirectoryHandle(e);try{return await t.getFileHandle(n.at(-1))}catch(e){if(e.name!==`TypeMismatchError`)throw e;return await t.getDirectoryHandle(n.at(-1))}}catch(e){if(e.name===`NotFoundError`)return null;throw e}}async loadOriginal(e){let t=await this.diskBytes(e);if(!t)throw Error(`ENOENT: File was removed from disk: `+e+`. Refresh the folder.`);if(!this.baselines.has(e)){if(this.baselineBytes+t.length>M9)throw Error(`Local file reads reached the 16 MiB session limit. Apply or discard changes, then Refresh.`);this.baselines.set(e,t),this.baselineBytes+=t.length}return t}async baseline(e){this.baselines.has(e)||this.baselines.set(e,this.original.get(e)?.kind===`file`?await this.loadOriginal(e):null)}async atPath(e,t){let n=(this.operations.get(e)??Promise.resolve()).catch(()=>{}).then(t);this.operations.set(e,n);try{return await n}finally{this.operations.get(e)===n&&this.operations.delete(e)}}async readFile(e,t){let n=this.path(e);return this.atPath(n,()=>this.memory.readFile(n,t))}async readFileBuffer(e){let t=this.path(e);return this.atPath(t,()=>this.memory.readFileBuffer(t))}async readFileBytes(e){let t=this.path(e);return this.atPath(t,()=>this.memory.readFileBytes(t))}async exists(e){return this.memory.exists(this.path(e))}async stat(e){let t=this.path(e),n=this.original.get(t);if(n?.kind===`file`&&!this.dirty.has(t)){let e=await n.handle.getFile();return{isFile:!0,isDirectory:!1,isSymbolicLink:!1,mode:420,size:this.baselines.get(t)?.length??e.size,mtime:new Date(e.lastModified)}}return this.memory.stat(t)}async lstat(e){return this.stat(e)}async realpath(e){return this.memory.realpath(this.path(e))}resolvePath(e,t){return this.memory.resolvePath(e,t)}getAllPaths(){return this.memory.getAllPaths()}async readdir(e){return this.memory.readdir(this.path(e))}async readdirWithFileTypes(e){return this.memory.readdirWithFileTypes(this.path(e))}async mkdir(e,t){let n=this.path(e),r=new Set(this.getAllPaths());await this.memory.mkdir(n,t);for(let e of this.getAllPaths())r.has(e)||(this.dirty.add(e),this.baselines.set(e,null))}async writeFile(e,t,n){let r=this.path(e);return this.atPath(r,()=>this.writeAtPath(r,t,n))}async writeAtPath(e,t,n){if(e===`/`)throw Error(`EISDIR: Cannot replace the selected folder.`);let r=new N2({},{maxTotalBytes:j9});await r.writeFile(`/data`,t,n);let i=await r.readFileBuffer(`/data`);if(i.length>j9)throw Error(`Local files are limited to 1 MiB.`);if(!await this.memory.exists(this.parent(e)))throw Error(`ENOENT: Parent folder does not exist: `+this.parent(e));await this.baseline(e),await this.memory.writeFile(e,i),this.dirty.add(e)}async appendFile(e,t,n){let r=new N2({"/data":await this.exists(e)?await this.readFileBuffer(e):new Uint8Array},{maxTotalBytes:j9});await r.appendFile(`/data`,t,n),await this.writeFile(e,await r.readFileBuffer(`/data`))}async rm(e,t){let n=this.path(e);return this.atPath(n,()=>this.removeAtPath(n,t))}async removeAtPath(e,t){if(e===`/`)throw Error(`Cannot remove the selected local folder.`);if(!await this.exists(e))return this.memory.rm(e,t);if((await this.stat(e)).isDirectory){if(this.original.has(e))throw Error(`Removing or renaming existing local folders is not supported. Remove individual files instead.`);for(let t of this.getAllPaths().filter(t=>t.startsWith(e+`/`)))if(this.original.has(t))throw Error(`This folder contains existing local files.`);let n=this.getAllPaths().filter(t=>t===e||t.startsWith(e+`/`));await this.memory.rm(e,t);for(let e of n)this.dirty.delete(e),this.baselines.delete(e)}else await this.baseline(e),await this.memory.rm(e,t),this.dirty.add(e)}async cp(e,t,n){let r=this.path(e),i=this.path(t);if(await this.exists(i)&&(await this.stat(i)).isDirectory&&(i+=`/`+r.split(`/`).at(-1)),r===i||i.startsWith(r+`/`))throw Error(`Cannot copy a path into itself.`);if((await this.stat(r)).isDirectory){if(!n?.recursive)throw Error(`EISDIR: Use cp -r to copy a directory.`);await this.mkdir(i,{recursive:!0});for(let e of await this.readdir(r))await this.cp(r+`/`+e,i+`/`+e,n)}else await this.writeFile(i,await this.readFileBuffer(r))}async mv(e,t){if((await this.stat(e)).isDirectory)throw Error(`Renaming local folders is not supported.`);await this.cp(e,t),await this.rm(e)}chmod=L9;symlink=L9;link=L9;readlink=L9;utimes=L9;async patches(){let e=[];for(let t of[...this.dirty].sort()){let n=this.baselines.get(t)??null,r=await this.exists(t),i=r&&(await this.memory.stat(t)).isDirectory?`directory`:`file`,a=r&&i===`file`?await this.readFileBuffer(t):null;i===`file`&&P9(n,a)||e.push({path:t,kind:i,before:F9(n),after:F9(a)})}return e}async listing(){let e=Object.create(null);for(let t of this.getAllPaths()){let n=this.original.get(t);(n?.kind===`file`||!n&&(await this.memory.stat(t)).isFile)&&(e[`/workspace`+t]=null)}return e}async status(){return{name:this.handle.name,count:(await this.patches()).length,skipped:this.skipped}}async apply(e){let t=await this.patches(),n=async e=>{let t=await this.diskEntry(e.path);if(e.kind===`directory`){if(t)throw Error(`Conflict: `+e.path+` was created on disk.`)}else if(!P9(await this.diskBytes(e.path),I9(e.before)))throw Error(`Conflict: `+e.path+` changed on disk. Nothing will overwrite it. Discard staged edits and refresh to read the new version.`)};for(let r of t)e?.throwIfAborted(),await n(r);let r=0;try{for(let i of t){e?.throwIfAborted(),await n(i);let t=i.path.split(`/`).filter(Boolean),a=this.handle;for(let e of t.slice(0,-1))a=await a.getDirectoryHandle(e);let o=t.at(-1);if(i.kind===`directory`){let e=await a.getDirectoryHandle(o,{create:!0});this.original.set(i.path,{kind:`directory`,handle:e})}else if(i.after===null)await a.removeEntry(o),this.original.delete(i.path),this.baselineBytes-=this.baselines.get(i.path)?.length??0,this.baselines.delete(i.path);else{let t=await a.getFileHandle(o,{create:!0}),n;try{n=await t.createWritable(),await n.write(I9(i.after)),e?.throwIfAborted(),await n.close()}catch(e){if(await n?.abort().catch(()=>{}),i.before===null)try{(await t.getFile()).size===0&&await t.isSameEntry(await a.getFileHandle(o))&&await a.removeEntry(o)}catch{}throw e}this.original.set(i.path,{kind:`file`,handle:t}),this.baselineBytes+=I9(i.after).length-(this.baselines.get(i.path)?.length??0),this.baselines.set(i.path,I9(i.after))}this.dirty.delete(i.path),r++}}catch(e){throw Error(`${r} of ${t.length} changes applied. Remaining changes are still staged. ${e.message}`)}return r}mount(){return new J6({mounts:[{mountPoint:`/workspace`,filesystem:this}]})}};const z9=e=>postMessage(e),B9=MI(z9);let V9,H9,U9,W9=!1,G9,K9=0,q9,J9,Y9;const X9=`You are Pi, a coding assistant powered by MiniCPM5-2B, a language model from OpenBMB.
|
| 1770 |
The model, inference, tools, and workspace run entirely in the user's browser. Workspace files are stored in this browser, not on the user's operating-system filesystem.
|
| 1771 |
Use your tools to inspect and change actual files. Never claim a command ran or a file changed without a successful tool result.
|
| 1772 |
When the user explicitly asks you to use a tool, call that tool even if you already know the answer.
|
assets/{index-DUQ_UspG.js → index-CV4PDQqc.js}
RENAMED
|
@@ -171,4 +171,4 @@ Node CLI extensions, npm, Python and native binaries are unavailable.`);else if(
|
|
| 171 |
`).map(e=>e+`
|
| 172 |
`);return t?n.pop():n.push(n.pop().slice(0,-1)),n}var Q=e=>document.querySelector(e),F_=e=>{if(e===null)return``;let t=new TextDecoder(`utf-8`,{fatal:!0}).decode(Uint8Array.from(atob(e),e=>e.charCodeAt(0)));if(t.includes(`\0`))throw Error(`Binary file`);return t};function I_({rpc:e,notice:t,canSwitch:n,onSwitch:r}){let i,a,o,s=!1,c=!1,l=[],u=Q(`#local-review`),d=window.self!==window.top,f=typeof window.showDirectoryPicker==`function`;function p(){Q(`#open-folder`).hidden=!!o,Q(`#reconnect-folder`).hidden=!!o||!a,Q(`#local-folder-bar`).hidden=!o;for(let e of[`open-folder`,`reconnect-folder`,`review-changes`,`refresh-folder`,`disconnect-folder`])Q(`#`+e).disabled=s||c;Q(`#review-changes`).disabled||=!o?.count,Q(`#refresh-folder`).disabled||=!!o?.count,Q(`#review-apply`).disabled=s||!l.length,Q(`#review-discard`).disabled=s||!l.length,Q(`#review-close`).disabled=s,o&&(Q(`#local-folder-name`).textContent=o.name,Q(`#local-folder-hint`).textContent=o.count?`${o.count} staged change${o.count===1?``:`s`} · original files unchanged until Apply`:`Local folder · edits are staged until you apply them`,Q(`#review-changes`).textContent=o.count?`Review & apply (${o.count})`:`Review & apply`),Q(`#export`).textContent=o?`Export changes`:`Export`,Q(`#save`).textContent=o?`Stage edit`:`Save`}function m(){return d?(Q(`#open-tab`).href=new URL(`?local-folder`,location.href).href,Q(`#open-tab`).hidden=!1,t(`Open the workspace in a new tab, then choose Open local folder. Browsers cannot grant folder access inside this embedded Space.`),!1):f?!0:(t(`Direct local-folder access needs a supported browser, such as Chrome or Edge. In this browser, use Import and Export to work with copies of your files.`),!1)}async function h(o){if(!(!m()||s||c))try{n(),c=!0,p();let s;if(o){if(s=a.handle,await s.requestPermission({mode:`read`})!==`granted`)throw Error(`Folder access was not granted.`)}else{if(a?.count&&!window.confirm(`Opening another folder replaces the saved staged edits for the previous folder. Reconnect to review them first, or continue?`))return;s=await window.showDirectoryPicker({id:`pi-workspace`,mode:`read`})}let l=await e(`local_open`,{handle:s,restore:o});i=s,a={handle:s,count:l.localFolder.count},r(l),t(`Opened ${s.name}. Review & apply writes staged changes to disk. Files load on demand; Refresh picks up external edits.`)}catch(e){e.name!==`AbortError`&&t(e.message)}finally{c=!1,p()}}Q(`#open-folder`).onclick=()=>h(!1),Q(`#reconnect-folder`).onclick=()=>h(!0),Q(`#disconnect-folder`).onclick=async()=>{try{n(),a={handle:i,count:o.count},r(await e(`local_close`)),t(`Demo workspace restored. Reconnect to continue with your local folder and staged edits.`)}catch(e){t(e.message)}},Q(`#refresh-folder`).onclick=async()=>{try{n(),r(await e(`local_refresh`)),t(`Folder refreshed from disk. Started a new chat with the current files.`)}catch(e){t(e.message)}};function g(e){l=e,Q(`#review-files`).replaceChildren(),Q(`#review-error`).textContent=``,Q(`#review-description`).textContent=`Review changes to ${o.name}. Applying writes them to your local folder.`;for(let t of e){let n=document.createElement(`details`),r=document.createElement(`summary`),i=document.createElement(`pre`);if(r.textContent=`${t.kind===`directory`?`New folder`:t.after===null?`Delete`:t.before===null?`Create`:`Edit`} · ${t.path.slice(1)}`,n.append(r),n.open=e.length<5,t.kind!==`directory`){try{let e=F_(t.before),n=F_(t.after);if(e.length+n.length>16e4)i.textContent=`Large text change. Inspect the file in the editor before applying.`;else{let r=N_(t.path,t.path,e,n,`on disk when read`,`staged`,{context:3});for(let e of r.split(`
|
| 173 |
`)){let t=document.createElement(`span`);t.textContent=e+`
|
| 174 |
-
`,e.startsWith(`+`)&&(t.className=`diff-add`),e.startsWith(`-`)&&(t.className=`diff-remove`),i.append(t)}}}catch{i.textContent=`Binary file · ${t.before===null?0:atob(t.before).length} → ${t.after===null?0:atob(t.after).length} bytes`}n.append(i)}Q(`#review-files`).append(n)}p()}return Q(`#review-changes`).onclick=async()=>{try{n(),g(await e(`local_changes`)),u.showModal()}catch(e){t(e.message)}},Q(`#review-close`).onclick=()=>u.close(),u.addEventListener(`cancel`,e=>{s&&e.preventDefault()}),Q(`#review-apply`).onclick=async()=>{try{if(await i.requestPermission({mode:`readwrite`})!==`granted`)throw Error(`Write access was not granted. Changes are still staged.`);let n=await e(`local_apply`);u.close(),t(`Applied ${n.applied} change${n.applied===1?``:`s`} to ${i.name}.`)}catch(t){try{g(await e(`local_changes`))}catch{}Q(`#review-error`).textContent=t.message}},Q(`#review-discard`).onclick=async()=>{if(window.confirm(`Discard all staged changes and reload files from disk? Your local files will not be changed.`))try{r(await e(`local_discard`)),u.close(),t(`Staged edits discarded. Folder refreshed and chat reset.`)}catch(e){Q(`#review-error`).textContent=e.message}},window.addEventListener(`beforeunload`,e=>{o?.count&&(e.preventDefault(),e.returnValue=``)}),new URLSearchParams(location.search).has(`local-folder`)&&t(`Choose Open local folder to select the project you want Pi to work on.`),{setBusy(e){s=e,p()},setState(e){o=e,a&&e&&(a.count=e.count),p()},restore(e){a=e,p()},get active(){return!!o}}}var $=e=>document.querySelector(e);c_($(`main`),$(`#column-resizer`));var L_=new Worker(new URL(new URL(`agent.worker-Dng_UV-X.js`,import.meta.url).href,``+import.meta.url),{type:`module`}),R_=new Map,z_=1,B_=!1,V_=!1,H_,U_=!1,W_={},G_=[],K_,q_,J_=0,Y_=!0,X_=new Promise(e=>{K_=e}),Z_={events:[],generations:[],runtimeSamples:[],device:null,contextUsage:Ng([])},Q_=Zg($(`#runtime-meter`)),$_=a_($(`.editor-input`)),ev=Ug($(`#pi-terminal`),{onInput:e=>$(`#send`).classList.toggle(`has-input`,!!e.trim()),load:()=>X_.then(hv),prompt:e=>X_.then(()=>gv(e)),newChat:()=>X_.then(_v),stop:()=>iv(`stop`),shell:e=>{if(U_)throw Error(`Save your file edits before running commands.`);return iv(`shell`,{command:e})}}),tv=Wg($(`#model-dialog`),{onClose:()=>ev.focus()}),nv=Xg($(`.workspace > .terminal`),{run:async e=>{if(await X_,U_)throw Error(`Save your file edits before running commands.`);if(B_)throw Error(`Wait for Pi to finish or stop the current task first.`);return iv(`shell`,{command:e})}}),rv=I_({rpc:iv,notice:av,canSwitch(){if(B_)throw Error(`Wait for Pi to finish or stop the current task first.`);if(U_)throw Error(`Save or stage the open file first.`)},onSwitch(e){q_=void 0,H_=void 0,U_=!1,J_++,Y_=!1,W_=e.files,G_=e.messages,rv.setState(e.localFolder),$(`#filename`).textContent=`Select a file`,$(`#filetype`).replaceChildren(),$_.setFile(``,``),ev.setMessages(G_,{reset:!0}),pv(Ng(G_)),$(`#shell-clear`).click(),cv();let t=Object.hasOwn(W_,`/workspace/README.md`)?`/workspace/README.md`:Object.keys(W_).sort()[0];t&&uv(t),sv()}});function iv(e,t){return new Promise((n,r)=>{let i=z_++;R_.set(i,{resolve:n,reject:r}),L_.postMessage({id:i,action:e,args:t})})}function av(e){$(`#notice`).textContent=e,$(`#notice`).hidden=!e}function ov(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 sv(){$(`#load`).disabled=B_||V_,$(`#load`).classList.toggle(`is-loaded`,V_),V_?$(`#load`).textContent=`Loaded`:B_||($(`#load`).textContent=`Load model`),$(`#load`).title=$(`#load-status`).textContent+` `+$(`#load-detail`).textContent,$(`#send`).disabled=B_,$(`#newline`).disabled=B_,ev.setState({busy:B_,loaded:V_,activity:$(`#activity`).textContent}),$(`#stop`).hidden=!B_;for(let e of[`#new-chat`,`#add-file`,`#upload`])$(e).disabled=B_;nv.setBusy(B_),rv.setBusy(B_),$(`#save`).disabled=B_||!H_||!U_,$(`#editor`).disabled=B_||!H_||!Y_}function cv(){ev.setFiles(W_),$(`#files`).replaceChildren();for(let e of Object.keys(W_).sort()){let t=document.createElement(`button`),n=document.createElement(`span`);n.className=`file-name`,n.textContent=e.replace(/^\/workspace\//,``),t.append(t_(e),n),t.title=e,t.classList.toggle(`selected`,e===H_),t.onclick=()=>uv(e),$(`#files`).append(t)}H_&&!Object.hasOwn(W_,H_)&&!U_&&(H_=void 0,J_++,Y_=!1,$(`#filename`).textContent=`Select a file`,$(`#filetype`).replaceChildren(),$_.setFile(``,``),sv()),H_&&!U_&&(W_[H_]===null?lv(H_):(Y_=!0,$_.setFile(H_,W_[H_]??``)))}async function lv(e){let t=++J_;Y_=!1,$_.setFile(e,``),sv();try{let n=await iv(`preview`,{path:e});if(t!==J_||e!==H_||U_)return;$_.setFile(e,n),Y_=!0}catch(e){t===J_&&av(e.message)}finally{t===J_&&sv()}}function uv(e){if(q_=void 0,U_){av(`Save the open file before selecting another file.`);return}H_=e,$(`#filename`).textContent=e,$(`#filetype`).replaceChildren(t_(e)),cv(),sv()}function dv(){if(U_){q_=void 0;return}if(!q_||!Object.hasOwn(W_,q_))return;let e=q_,t=e!==H_;uv(e),t&&($(`#editor`).scrollTop=0,$(`#editor`).scrollLeft=0);let n=[...$(`#files`).children].find(t=>t.title===e);if(n){let e=n.getBoundingClientRect(),t=$(`#files`).getBoundingClientRect();e.top<t.top?$(`#files`).scrollTop+=e.top-t.top:e.bottom>t.bottom&&($(`#files`).scrollTop+=e.bottom-t.bottom)}}function fv(){ev.setMessages(G_)}function pv(e){Z_.contextUsage=e,ev.setState({contextUsage:e})}L_.onmessage=({data:e})=>{if(Q_.update(e),e.type===`inference_activity`&&(Z_.runtimeSamples.push(e),Z_.runtimeSamples.length>160&&Z_.runtimeSamples.shift()),e.type===`reply`){let t=R_.get(e.id);R_.delete(e.id),e.error?t?.reject(Object.assign(Error(e.error),{name:e.errorName||`Error`,code:e.errorCode})):t?.resolve(e.value)}else if(e.type===`initialized`)rv.restore(e.savedLocal),rv.setState(e.localFolder),W_=e.files,G_=e.messages,cv(),fv(),pv(Ng(G_)),W_[`/workspace/README.md`]&&uv(`/workspace/README.md`),document.activeElement===document.body&&ev.focus(),K_();else if(e.type===`busy`)e.action===`load`&&($(`#load`).textContent=`Loading…`),B_=!0,$(`#activity`).textContent=e.action===`prompt`?`Pi is working…`:e.action===`load`?e.cachedOnly?`Loading local model…`:`Loading model…`:`Working…`,sv();else if(e.type===`idle`)B_=!1,$(`#progress`).hidden=!0,$(`#activity`).textContent=rv.active?`Local folder · review & apply to save changes to disk`:V_?`Ready · running on your device`:`Files and chat are saved on this device.`,sv();else if(e.type===`workspace`)W_=e.files,rv.setState(e.localFolder),cv(),dv();else if(e.type===`device`)Z_.device=e.device;else if(e.type===`load_progress`)$(`#progress`).hidden=!1,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 model weights from Hugging Face…`,$(`#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`).textContent=`Loading ${Math.round(e.loaded/e.total*100)}%`):($(`#load-status`).textContent=e.phase===`compile`?`Preparing WebGPU session…`:`Warming up GPU shaders…`,$(`#load-detail`).textContent=``,$(`#progress`).removeAttribute(`value`),$(`#load`).textContent=e.phase===`compile`?`Preparing…`:`Almost ready…`),$(`#load`).title=$(`#load-status`).textContent+` `+$(`#load-detail`).textContent;else if(e.type===`loaded`)V_=!0,Z_.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.`,sv();else if(e.type===`agent_event`){let t=e.event;if(Z_.events.push(t.type),Z_.events.length>500&&Z_.events.shift(),t.message){let e=G_.findIndex(e=>e.timestamp===t.message.timestamp&&e.role===t.message.role&&e.toolCallId===t.message.toolCallId);e<0?G_.push(t.message):G_[e]=t.message,fv()}t.type===`tool_execution_start`&&($(`#activity`).textContent=`Running ${t.toolName}…`),t.type===`tool_execution_end`&&t.toolName===`read`&&!t.isError&&typeof t.result?.details?.path==`string`&&(q_=t.result.details.path,dv()),t.type===`turn_start`&&($(`#activity`).textContent=`Pi is thinking…`),ev.setState({activity:$(`#activity`).textContent})}else e.type===`context_usage`?pv({inputTokens:e.inputTokens,outputTokens:e.outputTokens}):e.type===`generation`?(Z_.generations.push(e),Z_.generations.length>100&&Z_.generations.shift()):e.type===`context_trim`?av(`Older turns were left out of the model context. Your full chat remains saved.`):e.type===`notice`?av(e.text):e.type===`persistence_error`?av(ov(e.error)):e.type===`fatal`&&(V_=!1,av(e.error),sv())},L_.onerror=e=>{Q_.update({type:`fatal`});let t=Error(e.message||`The worker stopped. Reload to restore your saved workspace.`);av(t.message);for(let e of R_.values())e.reject(t);R_.clear(),B_=!1,V_=!1,$(`#progress`).hidden=!0,sv()};var mv=e=>async t=>{t?.preventDefault();try{await X_,await e(t)}catch(e){av(e.message)}};async function hv({cachedOnly:e=!1}={}){if(B_)throw Error(`Wait for the current operation or stop it first.`);av(``);let t=new URLSearchParams(location.search);B_=!0,$(`#load`).textContent=`Loading…`,$(`#activity`).textContent=e?`Loading local model…`:`Loading model…`,$(`#load-status`).textContent=e?`Loading local model…`:`Checking your browser and saved model…`,$(`#load-detail`).textContent=``,$(`#progress`).hidden=!1,$(`#progress`).removeAttribute(`value`),Q_.update({type:`busy`,action:`load`,cachedOnly:e}),sv();try{return await iv(`load`,{runtimeURL:new URL(`./runtime/`,location.href).href,local:t.get(`source`)===`local`,origin:location.origin,limit128:t.has(`limit128`),cachedOnly:e})}catch(e){throw e.message=ov(e.message),$(`#load-status`).textContent=e.message,$(`#progress`).hidden=!0,e}}async function gv(e){if(e.trim()){if(U_)throw Error(`Save your file edits before asking Pi to work on them.`);try{if(!V_){let t;if(await iv(`cache_status`))try{ev.setQueuedPrompt(e),await hv({cachedOnly:!0})}catch(e){if(e.name===`AbortError`)throw e;e.code!==`MODEL_CACHE_MISS`&&(t=e.message)}for(;!V_;){if(ev.setQueuedPrompt(``),!await tv.request(e,{errorMessage:t}))throw new DOMException(`Message kept for later.`,`AbortError`);ev.setQueuedPrompt(e);try{await hv()}catch(e){if(e.name===`AbortError`)throw e;t=e.message}}}ev.setQueuedPrompt(``),av(``);let t=await iv(`prompt`,{text:e});return G_=t.messages,fv(),t}finally{ev.setQueuedPrompt(``)}}}$(`#load`).onclick=mv(hv),$(`#send`).onclick=()=>{ev.submit(),ev.focus()},$(`#newline`).onclick=()=>ev.newline(),$(`#stop`).onclick=mv(()=>iv(`stop`)),$(`.terminal-panel`).addEventListener(`click`,e=>{!e.defaultPrevented&&e.button===0&&e.target instanceof Element&&(e.target.closest(`#pi-terminal, button, a, input, textarea, select, label, summary, [role="button"], [contenteditable]`)||window.getSelection()?.isCollapsed!==!1&&ev.focus())});async function _v(){q_=void 0,G_=(await iv(`new_chat`)).messages,pv(Ng(G_)),ev.setMessages(G_,{reset:!0}),av(``),ev.focus()}$(`#new-chat`).onclick=mv(_v),$(`#editor`).oninput=()=>{U_=!0,sv()},$(`#save`).onclick=mv(async()=>{await iv(`write`,{path:H_,content:$(`#editor`).value}),U_=!1,cv(),sv(),av(``)}),$(`#add-file`).onclick=mv(async()=>{if(U_)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(W_,t))throw Error(`That file already exists.`);await iv(`write`,{path:t,content:``}),uv(t)}),$(`#upload`).onchange=mv(async()=>{if(U_)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(W_,n))throw Error(e.name+` already exists. Rename it before importing.`);await iv(`write`,{path:n,content:t}),uv(n)}$(`#upload`).value=``}),$(`#export`).onclick=mv(async()=>{let e=rv.active?{version:1,patches:await iv(`local_changes`)}:await iv(`snapshot`),t=URL.createObjectURL(new Blob([JSON.stringify(e,null,2)],{type:`application/json`})),n=document.createElement(`a`);n.href=t,n.download=rv.active?`pi-staged-changes.json`:`pi-workspace.json`,n.click(),setTimeout(()=>URL.revokeObjectURL(t),1e3)}),window.browserPi={ready:X_,load:hv,prompt:gv,rpc:iv,diagnostics:Z_,terminal:ev,runtimeMeter:Q_,get loaded(){return V_},get busy(){return B_}},sv();
|
|
|
|
| 171 |
`).map(e=>e+`
|
| 172 |
`);return t?n.pop():n.push(n.pop().slice(0,-1)),n}var Q=e=>document.querySelector(e),F_=e=>{if(e===null)return``;let t=new TextDecoder(`utf-8`,{fatal:!0}).decode(Uint8Array.from(atob(e),e=>e.charCodeAt(0)));if(t.includes(`\0`))throw Error(`Binary file`);return t};function I_({rpc:e,notice:t,canSwitch:n,onSwitch:r}){let i,a,o,s=!1,c=!1,l=[],u=Q(`#local-review`),d=window.self!==window.top,f=typeof window.showDirectoryPicker==`function`;function p(){Q(`#open-folder`).hidden=!!o,Q(`#reconnect-folder`).hidden=!!o||!a,Q(`#local-folder-bar`).hidden=!o;for(let e of[`open-folder`,`reconnect-folder`,`review-changes`,`refresh-folder`,`disconnect-folder`])Q(`#`+e).disabled=s||c;Q(`#review-changes`).disabled||=!o?.count,Q(`#refresh-folder`).disabled||=!!o?.count,Q(`#review-apply`).disabled=s||!l.length,Q(`#review-discard`).disabled=s||!l.length,Q(`#review-close`).disabled=s,o&&(Q(`#local-folder-name`).textContent=o.name,Q(`#local-folder-hint`).textContent=o.count?`${o.count} staged change${o.count===1?``:`s`} · original files unchanged until Apply`:`Local folder · edits are staged until you apply them`,Q(`#review-changes`).textContent=o.count?`Review & apply (${o.count})`:`Review & apply`),Q(`#export`).textContent=o?`Export changes`:`Export`,Q(`#save`).textContent=o?`Stage edit`:`Save`}function m(){return d?(Q(`#open-tab`).href=new URL(`?local-folder`,location.href).href,Q(`#open-tab`).hidden=!1,t(`Open the workspace in a new tab, then choose Open local folder. Browsers cannot grant folder access inside this embedded Space.`),!1):f?!0:(t(`Direct local-folder access needs a supported browser, such as Chrome or Edge. In this browser, use Import and Export to work with copies of your files.`),!1)}async function h(o){if(!(!m()||s||c))try{n(),c=!0,p();let s;if(o){if(s=a.handle,await s.requestPermission({mode:`read`})!==`granted`)throw Error(`Folder access was not granted.`)}else{if(a?.count&&!window.confirm(`Opening another folder replaces the saved staged edits for the previous folder. Reconnect to review them first, or continue?`))return;s=await window.showDirectoryPicker({id:`pi-workspace`,mode:`read`})}let l=await e(`local_open`,{handle:s,restore:o});i=s,a={handle:s,count:l.localFolder.count},r(l),t(`Opened ${s.name}. Review & apply writes staged changes to disk. Files load on demand; Refresh picks up external edits.`)}catch(e){e.name!==`AbortError`&&t(e.message)}finally{c=!1,p()}}Q(`#open-folder`).onclick=()=>h(!1),Q(`#reconnect-folder`).onclick=()=>h(!0),Q(`#disconnect-folder`).onclick=async()=>{try{n(),a={handle:i,count:o.count},r(await e(`local_close`)),t(`Demo workspace restored. Reconnect to continue with your local folder and staged edits.`)}catch(e){t(e.message)}},Q(`#refresh-folder`).onclick=async()=>{try{n(),r(await e(`local_refresh`)),t(`Folder refreshed from disk. Started a new chat with the current files.`)}catch(e){t(e.message)}};function g(e){l=e,Q(`#review-files`).replaceChildren(),Q(`#review-error`).textContent=``,Q(`#review-description`).textContent=`Review changes to ${o.name}. Applying writes them to your local folder.`;for(let t of e){let n=document.createElement(`details`),r=document.createElement(`summary`),i=document.createElement(`pre`);if(r.textContent=`${t.kind===`directory`?`New folder`:t.after===null?`Delete`:t.before===null?`Create`:`Edit`} · ${t.path.slice(1)}`,n.append(r),n.open=e.length<5,t.kind!==`directory`){try{let e=F_(t.before),n=F_(t.after);if(e.length+n.length>16e4)i.textContent=`Large text change. Inspect the file in the editor before applying.`;else{let r=N_(t.path,t.path,e,n,`on disk when read`,`staged`,{context:3});for(let e of r.split(`
|
| 173 |
`)){let t=document.createElement(`span`);t.textContent=e+`
|
| 174 |
+
`,e.startsWith(`+`)&&(t.className=`diff-add`),e.startsWith(`-`)&&(t.className=`diff-remove`),i.append(t)}}}catch{i.textContent=`Binary file · ${t.before===null?0:atob(t.before).length} → ${t.after===null?0:atob(t.after).length} bytes`}n.append(i)}Q(`#review-files`).append(n)}p()}return Q(`#review-changes`).onclick=async()=>{try{n(),g(await e(`local_changes`)),u.showModal()}catch(e){t(e.message)}},Q(`#review-close`).onclick=()=>u.close(),u.addEventListener(`cancel`,e=>{s&&e.preventDefault()}),Q(`#review-apply`).onclick=async()=>{try{if(await i.requestPermission({mode:`readwrite`})!==`granted`)throw Error(`Write access was not granted. Changes are still staged.`);let n=await e(`local_apply`);u.close(),t(`Applied ${n.applied} change${n.applied===1?``:`s`} to ${i.name}.`)}catch(t){try{g(await e(`local_changes`))}catch{}Q(`#review-error`).textContent=t.message}},Q(`#review-discard`).onclick=async()=>{if(window.confirm(`Discard all staged changes and reload files from disk? Your local files will not be changed.`))try{r(await e(`local_discard`)),u.close(),t(`Staged edits discarded. Folder refreshed and chat reset.`)}catch(e){Q(`#review-error`).textContent=e.message}},window.addEventListener(`beforeunload`,e=>{o?.count&&(e.preventDefault(),e.returnValue=``)}),new URLSearchParams(location.search).has(`local-folder`)&&t(`Choose Open local folder to select the project you want Pi to work on.`),{setBusy(e){s=e,p()},setState(e){o=e,a&&e&&(a.count=e.count),p()},restore(e){a=e,p()},get active(){return!!o}}}var $=e=>document.querySelector(e);c_($(`main`),$(`#column-resizer`));var L_=new Worker(new URL(new URL(`agent.worker-BLr4cL8b.js`,import.meta.url).href,``+import.meta.url),{type:`module`}),R_=new Map,z_=1,B_=!1,V_=!1,H_,U_=!1,W_={},G_=[],K_,q_,J_=0,Y_=!0,X_=new Promise(e=>{K_=e}),Z_={events:[],generations:[],runtimeSamples:[],device:null,contextUsage:Ng([])},Q_=Zg($(`#runtime-meter`)),$_=a_($(`.editor-input`)),ev=Ug($(`#pi-terminal`),{onInput:e=>$(`#send`).classList.toggle(`has-input`,!!e.trim()),load:()=>X_.then(hv),prompt:e=>X_.then(()=>gv(e)),newChat:()=>X_.then(_v),stop:()=>iv(`stop`),shell:e=>{if(U_)throw Error(`Save your file edits before running commands.`);return iv(`shell`,{command:e})}}),tv=Wg($(`#model-dialog`),{onClose:()=>ev.focus()}),nv=Xg($(`.workspace > .terminal`),{run:async e=>{if(await X_,U_)throw Error(`Save your file edits before running commands.`);if(B_)throw Error(`Wait for Pi to finish or stop the current task first.`);return iv(`shell`,{command:e})}}),rv=I_({rpc:iv,notice:av,canSwitch(){if(B_)throw Error(`Wait for Pi to finish or stop the current task first.`);if(U_)throw Error(`Save or stage the open file first.`)},onSwitch(e){q_=void 0,H_=void 0,U_=!1,J_++,Y_=!1,W_=e.files,G_=e.messages,rv.setState(e.localFolder),$(`#filename`).textContent=`Select a file`,$(`#filetype`).replaceChildren(),$_.setFile(``,``),ev.setMessages(G_,{reset:!0}),pv(Ng(G_)),$(`#shell-clear`).click(),cv();let t=Object.hasOwn(W_,`/workspace/README.md`)?`/workspace/README.md`:Object.keys(W_).sort()[0];t&&uv(t),sv()}});function iv(e,t){return new Promise((n,r)=>{let i=z_++;R_.set(i,{resolve:n,reject:r}),L_.postMessage({id:i,action:e,args:t})})}function av(e){$(`#notice`).textContent=e,$(`#notice`).hidden=!e}function ov(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 sv(){$(`#load`).disabled=B_||V_,$(`#load`).classList.toggle(`is-loaded`,V_),V_?$(`#load`).textContent=`Loaded`:B_||($(`#load`).textContent=`Load model`),$(`#load`).title=$(`#load-status`).textContent+` `+$(`#load-detail`).textContent,$(`#send`).disabled=B_,$(`#newline`).disabled=B_,ev.setState({busy:B_,loaded:V_,activity:$(`#activity`).textContent}),$(`#stop`).hidden=!B_;for(let e of[`#new-chat`,`#add-file`,`#upload`])$(e).disabled=B_;nv.setBusy(B_),rv.setBusy(B_),$(`#save`).disabled=B_||!H_||!U_,$(`#editor`).disabled=B_||!H_||!Y_}function cv(){ev.setFiles(W_),$(`#files`).replaceChildren();for(let e of Object.keys(W_).sort()){let t=document.createElement(`button`),n=document.createElement(`span`);n.className=`file-name`,n.textContent=e.replace(/^\/workspace\//,``),t.append(t_(e),n),t.title=e,t.classList.toggle(`selected`,e===H_),t.onclick=()=>uv(e),$(`#files`).append(t)}H_&&!Object.hasOwn(W_,H_)&&!U_&&(H_=void 0,J_++,Y_=!1,$(`#filename`).textContent=`Select a file`,$(`#filetype`).replaceChildren(),$_.setFile(``,``),sv()),H_&&!U_&&(W_[H_]===null?lv(H_):(Y_=!0,$_.setFile(H_,W_[H_]??``)))}async function lv(e){let t=++J_;Y_=!1,$_.setFile(e,``),sv();try{let n=await iv(`preview`,{path:e});if(t!==J_||e!==H_||U_)return;$_.setFile(e,n),Y_=!0}catch(e){t===J_&&av(e.message)}finally{t===J_&&sv()}}function uv(e){if(q_=void 0,U_){av(`Save the open file before selecting another file.`);return}H_=e,$(`#filename`).textContent=e,$(`#filetype`).replaceChildren(t_(e)),cv(),sv()}function dv(){if(U_){q_=void 0;return}if(!q_||!Object.hasOwn(W_,q_))return;let e=q_,t=e!==H_;uv(e),t&&($(`#editor`).scrollTop=0,$(`#editor`).scrollLeft=0);let n=[...$(`#files`).children].find(t=>t.title===e);if(n){let e=n.getBoundingClientRect(),t=$(`#files`).getBoundingClientRect();e.top<t.top?$(`#files`).scrollTop+=e.top-t.top:e.bottom>t.bottom&&($(`#files`).scrollTop+=e.bottom-t.bottom)}}function fv(){ev.setMessages(G_)}function pv(e){Z_.contextUsage=e,ev.setState({contextUsage:e})}L_.onmessage=({data:e})=>{if(Q_.update(e),e.type===`inference_activity`&&(Z_.runtimeSamples.push(e),Z_.runtimeSamples.length>160&&Z_.runtimeSamples.shift()),e.type===`reply`){let t=R_.get(e.id);R_.delete(e.id),e.error?t?.reject(Object.assign(Error(e.error),{name:e.errorName||`Error`,code:e.errorCode})):t?.resolve(e.value)}else if(e.type===`initialized`)rv.restore(e.savedLocal),rv.setState(e.localFolder),W_=e.files,G_=e.messages,cv(),fv(),pv(Ng(G_)),W_[`/workspace/README.md`]&&uv(`/workspace/README.md`),document.activeElement===document.body&&ev.focus(),K_();else if(e.type===`busy`)e.action===`load`&&($(`#load`).textContent=`Loading…`),B_=!0,$(`#activity`).textContent=e.action===`prompt`?`Pi is working…`:e.action===`load`?e.cachedOnly?`Loading local model…`:`Loading model…`:`Working…`,sv();else if(e.type===`idle`)B_=!1,$(`#progress`).hidden=!0,$(`#activity`).textContent=rv.active?`Local folder · review & apply to save changes to disk`:V_?`Ready · running on your device`:`Files and chat are saved on this device.`,sv();else if(e.type===`workspace`)W_=e.files,rv.setState(e.localFolder),cv(),dv();else if(e.type===`device`)Z_.device=e.device;else if(e.type===`load_progress`)$(`#progress`).hidden=!1,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 model weights from Hugging Face…`,$(`#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`).textContent=`Loading ${Math.round(e.loaded/e.total*100)}%`):($(`#load-status`).textContent=e.phase===`compile`?`Preparing WebGPU session…`:`Warming up GPU shaders…`,$(`#load-detail`).textContent=``,$(`#progress`).removeAttribute(`value`),$(`#load`).textContent=e.phase===`compile`?`Preparing…`:`Almost ready…`),$(`#load`).title=$(`#load-status`).textContent+` `+$(`#load-detail`).textContent;else if(e.type===`loaded`)V_=!0,Z_.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.`,sv();else if(e.type===`agent_event`){let t=e.event;if(Z_.events.push(t.type),Z_.events.length>500&&Z_.events.shift(),t.message){let e=G_.findIndex(e=>e.timestamp===t.message.timestamp&&e.role===t.message.role&&e.toolCallId===t.message.toolCallId);e<0?G_.push(t.message):G_[e]=t.message,fv()}t.type===`tool_execution_start`&&($(`#activity`).textContent=`Running ${t.toolName}…`),t.type===`tool_execution_end`&&t.toolName===`read`&&!t.isError&&typeof t.result?.details?.path==`string`&&(q_=t.result.details.path,dv()),t.type===`turn_start`&&($(`#activity`).textContent=`Pi is thinking…`),ev.setState({activity:$(`#activity`).textContent})}else e.type===`context_usage`?pv({inputTokens:e.inputTokens,outputTokens:e.outputTokens}):e.type===`generation`?(Z_.generations.push(e),Z_.generations.length>100&&Z_.generations.shift()):e.type===`context_trim`?av(`Older turns were left out of the model context. Your full chat remains saved.`):e.type===`notice`?av(e.text):e.type===`persistence_error`?av(ov(e.error)):e.type===`fatal`&&(V_=!1,av(e.error),sv())},L_.onerror=e=>{Q_.update({type:`fatal`});let t=Error(e.message||`The worker stopped. Reload to restore your saved workspace.`);av(t.message);for(let e of R_.values())e.reject(t);R_.clear(),B_=!1,V_=!1,$(`#progress`).hidden=!0,sv()};var mv=e=>async t=>{t?.preventDefault();try{await X_,await e(t)}catch(e){av(e.message)}};async function hv({cachedOnly:e=!1}={}){if(B_)throw Error(`Wait for the current operation or stop it first.`);av(``);let t=new URLSearchParams(location.search);B_=!0,$(`#load`).textContent=`Loading…`,$(`#activity`).textContent=e?`Loading local model…`:`Loading model…`,$(`#load-status`).textContent=e?`Loading local model…`:`Checking your browser and saved model…`,$(`#load-detail`).textContent=``,$(`#progress`).hidden=!1,$(`#progress`).removeAttribute(`value`),Q_.update({type:`busy`,action:`load`,cachedOnly:e}),sv();try{return await iv(`load`,{runtimeURL:new URL(`./runtime/`,location.href).href,local:t.get(`source`)===`local`,origin:location.origin,limit128:t.has(`limit128`),cachedOnly:e})}catch(e){throw e.message=ov(e.message),$(`#load-status`).textContent=e.message,$(`#progress`).hidden=!0,e}}async function gv(e){if(e.trim()){if(U_)throw Error(`Save your file edits before asking Pi to work on them.`);try{if(!V_){let t;if(await iv(`cache_status`))try{ev.setQueuedPrompt(e),await hv({cachedOnly:!0})}catch(e){if(e.name===`AbortError`)throw e;e.code!==`MODEL_CACHE_MISS`&&(t=e.message)}for(;!V_;){if(ev.setQueuedPrompt(``),!await tv.request(e,{errorMessage:t}))throw new DOMException(`Message kept for later.`,`AbortError`);ev.setQueuedPrompt(e);try{await hv()}catch(e){if(e.name===`AbortError`)throw e;t=e.message}}}ev.setQueuedPrompt(``),av(``);let t=await iv(`prompt`,{text:e});return G_=t.messages,fv(),t}finally{ev.setQueuedPrompt(``)}}}$(`#load`).onclick=mv(hv),$(`#send`).onclick=()=>{ev.submit(),ev.focus()},$(`#newline`).onclick=()=>ev.newline(),$(`#stop`).onclick=mv(()=>iv(`stop`)),$(`.terminal-panel`).addEventListener(`click`,e=>{!e.defaultPrevented&&e.button===0&&e.target instanceof Element&&(e.target.closest(`#pi-terminal, button, a, input, textarea, select, label, summary, [role="button"], [contenteditable]`)||window.getSelection()?.isCollapsed!==!1&&ev.focus())});async function _v(){q_=void 0,G_=(await iv(`new_chat`)).messages,pv(Ng(G_)),ev.setMessages(G_,{reset:!0}),av(``),ev.focus()}$(`#new-chat`).onclick=mv(_v),$(`#editor`).oninput=()=>{U_=!0,sv()},$(`#save`).onclick=mv(async()=>{await iv(`write`,{path:H_,content:$(`#editor`).value}),U_=!1,cv(),sv(),av(``)}),$(`#add-file`).onclick=mv(async()=>{if(U_)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(W_,t))throw Error(`That file already exists.`);await iv(`write`,{path:t,content:``}),uv(t)}),$(`#upload`).onchange=mv(async()=>{if(U_)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(W_,n))throw Error(e.name+` already exists. Rename it before importing.`);await iv(`write`,{path:n,content:t}),uv(n)}$(`#upload`).value=``}),$(`#export`).onclick=mv(async()=>{let e=rv.active?{version:1,patches:await iv(`local_changes`)}:await iv(`snapshot`),t=URL.createObjectURL(new Blob([JSON.stringify(e,null,2)],{type:`application/json`})),n=document.createElement(`a`);n.href=t,n.download=rv.active?`pi-staged-changes.json`:`pi-workspace.json`,n.click(),setTimeout(()=>URL.revokeObjectURL(t),1e3)}),window.browserPi={ready:X_,load:hv,prompt:gv,rpc:iv,diagnostics:Z_,terminal:ev,runtimeMeter:Q_,get loaded(){return V_},get busy(){return B_}},sv();
|
index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
| 21 |
<meta name="twitter:image" content="https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/social-thumbnail-v3.png">
|
| 22 |
<meta name="twitter:image:alt" content="A coding agent running entirely in your browser, above the Pi terminal and its pixel cat mascot.">
|
| 23 |
<title>Pi · Coding agent in your browser</title>
|
| 24 |
-
<script type="module" crossorigin src="./assets/index-
|
| 25 |
<link rel="stylesheet" crossorigin href="./assets/index-Uy7sEXBC.css">
|
| 26 |
</head>
|
| 27 |
<body>
|
|
|
|
| 21 |
<meta name="twitter:image" content="https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/social-thumbnail-v3.png">
|
| 22 |
<meta name="twitter:image:alt" content="A coding agent running entirely in your browser, above the Pi terminal and its pixel cat mascot.">
|
| 23 |
<title>Pi · Coding agent in your browser</title>
|
| 24 |
+
<script type="module" crossorigin src="./assets/index-CV4PDQqc.js"></script>
|
| 25 |
<link rel="stylesheet" crossorigin href="./assets/index-Uy7sEXBC.css">
|
| 26 |
</head>
|
| 27 |
<body>
|
validation/LOCAL-FOLDER.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
| 2 |
|
| 3 |
September 16, 2026. Duplicate only: `Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP`.
|
| 4 |
|
| 5 |
-
- Production build and
|
| 6 |
- Browser integration passed in Chromium 152 using real OPFS directory handles through the real worker and IndexedDB. Only the picker is replaced in this suite. Checks cover user activation, staging/diff/application, demo restoration, reload/reconnect, external conflicts, discard, picker cancellation, iframe fallback, and 1440/390/320px layouts. `local-folder-browser.json` records results.
|
| 7 |
- Separately, a headed Chromium 152 test on an isolated X display used the **native OS directory picker**, selected a real test directory, accepted Chromium's read permission, edited README.md, reviewed its diff, then accepted write permission. Node read the host file and confirmed `# Native edited\n` on disk. This was not OPFS or a mock picker. Earlier test attempts accidentally denied the read permission with a forwarded Enter key; clicking the chooser's Open button and then Allow completed the native flow.
|
| 8 |
- WebKit 26 at 390px correctly reports that direct folder access is unsupported and keeps all three demo files available. This is a WebKit engine test, not a physical Safari/iPhone test.
|
|
|
|
|
|
|
| 9 |
|
| 10 |
Native reproduction: open the standalone app in Chrome, select a small disposable project with Open local folder, allow read access, edit a file and Stage edit. Verify the original file is unchanged. Open Review & apply, inspect the diff, click Apply to folder, allow write access, and verify the file on disk. Back to demo restores the original demo; reload then Reconnect folder restores the local session. Change a previously read file externally before applying a staged edit to check conflict handling.
|
| 11 |
|
|
|
|
| 2 |
|
| 3 |
September 16, 2026. Duplicate only: `Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP`.
|
| 4 |
|
| 5 |
+
- Production build and 29 unit tests passed. Seven new OS-backed adapter tests cover lazy reads, shell/tool writes, file creation/copy/rename/delete, directory creation, restored baselines, conflicts, cancellation, write failure, unsupported operations, virtual paths outside the mount, and concurrent preview/tool writes. Failed creation also removes its still-empty placeholder.
|
| 6 |
- Browser integration passed in Chromium 152 using real OPFS directory handles through the real worker and IndexedDB. Only the picker is replaced in this suite. Checks cover user activation, staging/diff/application, demo restoration, reload/reconnect, external conflicts, discard, picker cancellation, iframe fallback, and 1440/390/320px layouts. `local-folder-browser.json` records results.
|
| 7 |
- Separately, a headed Chromium 152 test on an isolated X display used the **native OS directory picker**, selected a real test directory, accepted Chromium's read permission, edited README.md, reviewed its diff, then accepted write permission. Node read the host file and confirmed `# Native edited\n` on disk. This was not OPFS or a mock picker. Earlier test attempts accidentally denied the read permission with a forwarded Enter key; clicking the chooser's Open button and then Allow completed the native flow.
|
| 8 |
- WebKit 26 at 390px correctly reports that direct folder access is unsupported and keeps all three demo files available. This is a WebKit engine test, not a physical Safari/iPhone test.
|
| 9 |
+
- The browser integration suite also passed against the deployed duplicate (`local-folder-live.json`). Existing editor and read-following regression suites passed on the production build.
|
| 10 |
+
- Actual MiniCPM5-2B WebGPU trial on the deployed duplicate: read app.js, change its exported greeting from Hello to Bonjour, leave other files unchanged, and verify. The model completed in **9.935 seconds** after a **5.499-second** cached model load. Exactly one file was staged, the real OPFS file remained unchanged before Apply, and the reviewed Apply wrote the expected contents. The actual model/tool messages are in `local-folder-agent-live.json`. This trial used an actual browser directory handle with a supplied picker; OS folder permissions and host-disk writes were tested separately above.
|
| 11 |
|
| 12 |
Native reproduction: open the standalone app in Chrome, select a small disposable project with Open local folder, allow read access, edit a file and Stage edit. Verify the original file is unchanged. Open Review & apply, inspect the diff, click Apply to folder, allow write access, and verify the file on disk. Back to demo restores the original demo; reload then Reconnect folder restores the local session. Change a previously read file externally before applying a staged edit to check conflict handling.
|
| 13 |
|
validation/local-folder-agent-live.json
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"date": "2026-09-16T14:27:12.285Z",
|
| 3 |
+
"url": "https://mike0021-minicpm5-2b-webgpu-pi-http.static.hf.space/",
|
| 4 |
+
"errors": [],
|
| 5 |
+
"prompt": "Read app.js and change the exported greeting from Hello to Bonjour. Leave other files unchanged and verify the change.",
|
| 6 |
+
"handle": "Real OPFS directory handle passed through the native worker protocol; model and tools are not mocked.",
|
| 7 |
+
"loadMs": 5499,
|
| 8 |
+
"elapsedMs": 9935,
|
| 9 |
+
"messages": [
|
| 10 |
+
{
|
| 11 |
+
"role": "user",
|
| 12 |
+
"content": [
|
| 13 |
+
{
|
| 14 |
+
"type": "text",
|
| 15 |
+
"text": "Read app.js and change the exported greeting from Hello to Bonjour. Leave other files unchanged and verify the change."
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"timestamp": 1789568840826
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"role": "assistant",
|
| 22 |
+
"api": "minicpm-webgpu",
|
| 23 |
+
"provider": "browser",
|
| 24 |
+
"model": "Mike0021/MiniCPM5-2B-ONNX",
|
| 25 |
+
"timestamp": 1789568840827,
|
| 26 |
+
"content": [
|
| 27 |
+
{
|
| 28 |
+
"type": "thinking",
|
| 29 |
+
"thinking": "The user wants me to:\n1. Read app.js\n2. Change the exported greeting from \"Hello\" to \"Bonjour\"\n3. Verify the change\n\nLet me start by reading app.js."
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"type": "toolCall",
|
| 33 |
+
"id": "ef005c6b-726e-4507-a76e-a3852c464fc3",
|
| 34 |
+
"name": "read",
|
| 35 |
+
"arguments": {
|
| 36 |
+
"path": "/workspace/app.js"
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
],
|
| 40 |
+
"usage": {
|
| 41 |
+
"input": 1079,
|
| 42 |
+
"output": 60,
|
| 43 |
+
"cacheRead": 0,
|
| 44 |
+
"cacheWrite": 0,
|
| 45 |
+
"totalTokens": 1139,
|
| 46 |
+
"cost": {
|
| 47 |
+
"input": 0,
|
| 48 |
+
"output": 0,
|
| 49 |
+
"cacheRead": 0,
|
| 50 |
+
"cacheWrite": 0,
|
| 51 |
+
"total": 0
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"stopReason": "toolUse"
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"role": "toolResult",
|
| 58 |
+
"toolCallId": "ef005c6b-726e-4507-a76e-a3852c464fc3",
|
| 59 |
+
"toolName": "read",
|
| 60 |
+
"content": [
|
| 61 |
+
{
|
| 62 |
+
"type": "text",
|
| 63 |
+
"text": "export const greeting = \"Hello\";\n"
|
| 64 |
+
}
|
| 65 |
+
],
|
| 66 |
+
"details": {
|
| 67 |
+
"path": "/workspace/app.js"
|
| 68 |
+
},
|
| 69 |
+
"isError": false,
|
| 70 |
+
"timestamp": 1789568843419
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"role": "assistant",
|
| 74 |
+
"api": "minicpm-webgpu",
|
| 75 |
+
"provider": "browser",
|
| 76 |
+
"model": "Mike0021/MiniCPM5-2B-ONNX",
|
| 77 |
+
"timestamp": 1789568843419,
|
| 78 |
+
"content": [
|
| 79 |
+
{
|
| 80 |
+
"type": "thinking",
|
| 81 |
+
"thinking": "The file has `export const greeting = \"Hello\";`. I need to change it to \"Bonjour\"."
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"type": "toolCall",
|
| 85 |
+
"id": "9d600265-bd2d-409f-8a6e-76126edfd2cf",
|
| 86 |
+
"name": "edit",
|
| 87 |
+
"arguments": {
|
| 88 |
+
"path": "/workspace/app.js",
|
| 89 |
+
"oldText": "export const greeting = \"Hello\";",
|
| 90 |
+
"newText": "export const greeting = \"Bonjour\";"
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
],
|
| 94 |
+
"usage": {
|
| 95 |
+
"input": 1176,
|
| 96 |
+
"output": 70,
|
| 97 |
+
"cacheRead": 0,
|
| 98 |
+
"cacheWrite": 0,
|
| 99 |
+
"totalTokens": 1246,
|
| 100 |
+
"cost": {
|
| 101 |
+
"input": 0,
|
| 102 |
+
"output": 0,
|
| 103 |
+
"cacheRead": 0,
|
| 104 |
+
"cacheWrite": 0,
|
| 105 |
+
"total": 0
|
| 106 |
+
}
|
| 107 |
+
},
|
| 108 |
+
"stopReason": "toolUse"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"role": "toolResult",
|
| 112 |
+
"toolCallId": "9d600265-bd2d-409f-8a6e-76126edfd2cf",
|
| 113 |
+
"toolName": "edit",
|
| 114 |
+
"content": [
|
| 115 |
+
{
|
| 116 |
+
"type": "text",
|
| 117 |
+
"text": "Edited /workspace/app.js"
|
| 118 |
+
}
|
| 119 |
+
],
|
| 120 |
+
"details": {},
|
| 121 |
+
"isError": false,
|
| 122 |
+
"timestamp": 1789568846220
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"role": "assistant",
|
| 126 |
+
"api": "minicpm-webgpu",
|
| 127 |
+
"provider": "browser",
|
| 128 |
+
"model": "Mike0021/MiniCPM5-2B-ONNX",
|
| 129 |
+
"timestamp": 1789568846220,
|
| 130 |
+
"content": [
|
| 131 |
+
{
|
| 132 |
+
"type": "thinking",
|
| 133 |
+
"thinking": "Now let me verify the change by reading the file again."
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"type": "toolCall",
|
| 137 |
+
"id": "077ebf29-ec52-44fc-b346-172a1aea1262",
|
| 138 |
+
"name": "read",
|
| 139 |
+
"arguments": {
|
| 140 |
+
"path": "/workspace/app.js"
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
],
|
| 144 |
+
"usage": {
|
| 145 |
+
"input": 1278,
|
| 146 |
+
"output": 31,
|
| 147 |
+
"cacheRead": 0,
|
| 148 |
+
"cacheWrite": 0,
|
| 149 |
+
"totalTokens": 1309,
|
| 150 |
+
"cost": {
|
| 151 |
+
"input": 0,
|
| 152 |
+
"output": 0,
|
| 153 |
+
"cacheRead": 0,
|
| 154 |
+
"cacheWrite": 0,
|
| 155 |
+
"total": 0
|
| 156 |
+
}
|
| 157 |
+
},
|
| 158 |
+
"stopReason": "toolUse"
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"role": "toolResult",
|
| 162 |
+
"toolCallId": "077ebf29-ec52-44fc-b346-172a1aea1262",
|
| 163 |
+
"toolName": "read",
|
| 164 |
+
"content": [
|
| 165 |
+
{
|
| 166 |
+
"type": "text",
|
| 167 |
+
"text": "export const greeting = \"Bonjour\";\n"
|
| 168 |
+
}
|
| 169 |
+
],
|
| 170 |
+
"details": {
|
| 171 |
+
"path": "/workspace/app.js"
|
| 172 |
+
},
|
| 173 |
+
"isError": false,
|
| 174 |
+
"timestamp": 1789568848116
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"role": "assistant",
|
| 178 |
+
"api": "minicpm-webgpu",
|
| 179 |
+
"provider": "browser",
|
| 180 |
+
"model": "Mike0021/MiniCPM5-2B-ONNX",
|
| 181 |
+
"timestamp": 1789568848116,
|
| 182 |
+
"content": [
|
| 183 |
+
{
|
| 184 |
+
"type": "thinking",
|
| 185 |
+
"thinking": "The change has been verified. The file now contains \"Bonjour\" instead of \"Hello\"."
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"type": "text",
|
| 189 |
+
"text": "Done. The greeting in `/workspace/app.js` was changed from `\"Hello\"` to `\"Bonjour\"`, and verified by re-reading the file."
|
| 190 |
+
}
|
| 191 |
+
],
|
| 192 |
+
"usage": {
|
| 193 |
+
"input": 1347,
|
| 194 |
+
"output": 55,
|
| 195 |
+
"cacheRead": 0,
|
| 196 |
+
"cacheWrite": 0,
|
| 197 |
+
"totalTokens": 1402,
|
| 198 |
+
"cost": {
|
| 199 |
+
"input": 0,
|
| 200 |
+
"output": 0,
|
| 201 |
+
"cacheRead": 0,
|
| 202 |
+
"cacheWrite": 0,
|
| 203 |
+
"total": 0
|
| 204 |
+
}
|
| 205 |
+
},
|
| 206 |
+
"stopReason": "stop"
|
| 207 |
+
}
|
| 208 |
+
],
|
| 209 |
+
"patches": [
|
| 210 |
+
{
|
| 211 |
+
"path": "/app.js",
|
| 212 |
+
"kind": "file",
|
| 213 |
+
"before": "ZXhwb3J0IGNvbnN0IGdyZWV0aW5nID0gIkhlbGxvIjsK",
|
| 214 |
+
"after": "ZXhwb3J0IGNvbnN0IGdyZWV0aW5nID0gIkJvbmpvdXIiOwo="
|
| 215 |
+
}
|
| 216 |
+
],
|
| 217 |
+
"diskBeforeApply": "export const greeting = \"Hello\";\n",
|
| 218 |
+
"diskAfterApply": "export const greeting = \"Bonjour\";\n",
|
| 219 |
+
"final": "Done. The greeting in `/workspace/app.js` was changed from `\"Hello\"` to `\"Bonjour\"`, and verified by re-reading the file.",
|
| 220 |
+
"passed": true
|
| 221 |
+
}
|
validation/local-folder-browser.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"url": "http://<redacted IP>:4193/",
|
| 3 |
-
"date": "2026-09-16T14:
|
| 4 |
"checks": [
|
| 5 |
"Real directory handle crosses worker boundary; picker called with user activation; existing editor/tree reused; excluded directories omitted",
|
| 6 |
"Editor changes are staged; readable diff reviewed; Apply writes through the browser API",
|
|
|
|
| 1 |
{
|
| 2 |
"url": "http://<redacted IP>:4193/",
|
| 3 |
+
"date": "2026-09-16T14:29:20.033Z",
|
| 4 |
"checks": [
|
| 5 |
"Real directory handle crosses worker boundary; picker called with user activation; existing editor/tree reused; excluded directories omitted",
|
| 6 |
"Editor changes are staged; readable diff reviewed; Apply writes through the browser API",
|
validation/local-folder-live.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"url": "https://mike0021-minicpm5-2b-webgpu-pi-http.static.hf.space/",
|
| 3 |
+
"date": "2026-09-16T14:27:12.282Z",
|
| 4 |
+
"checks": [
|
| 5 |
+
"Real directory handle crosses worker boundary; picker called with user activation; existing editor/tree reused; excluded directories omitted",
|
| 6 |
+
"Editor changes are staged; readable diff reviewed; Apply writes through the browser API",
|
| 7 |
+
"Back to demo restores original files/chat; reload + reconnect restores handle, staged edits and local chat from separate IndexedDB record",
|
| 8 |
+
"External edit blocks the whole apply before any disk mutation; Discard leaves external contents intact",
|
| 9 |
+
"Folder controls and editor fit 1440, 390 and 320px layouts",
|
| 10 |
+
"Cancelling picker preserves the demo workspace",
|
| 11 |
+
"Cross-origin iframe offers direct app tab instead of a blocked native picker"
|
| 12 |
+
],
|
| 13 |
+
"errors": [],
|
| 14 |
+
"picker": "Test supplies a real OPFS directory handle; native OS picker covered separately.",
|
| 15 |
+
"passed": true
|
| 16 |
+
}
|