Add optional local-folder workspace with staged review and apply
Browse files- README.md +17 -3
- app/index.html +10 -0
- app/package-lock.json +1 -0
- app/package.json +1 -0
- app/src/agent.worker.mjs +51 -5
- app/src/local-folder-ui.mjs +141 -0
- app/src/local-folder.mjs +265 -0
- app/src/main.mjs +44 -6
- app/src/storage.mjs +4 -4
- app/src/style.css +23 -0
- app/src/workspace.mjs +5 -3
- app/tests/local-folder-browser.mjs +88 -0
- app/tests/local-folder.test.mjs +125 -0
- assets/agent.worker-B7d5sboN.js +0 -0
- assets/agent.worker-DAy1yjYb.js +0 -0
- assets/agent.worker-DONV7yi_.js +0 -0
- assets/{agent.worker-CS8jL-AS.js → agent.worker-Dng_UV-X.js} +0 -0
- assets/agent.worker-euGpP_Md.js +0 -0
- assets/index-B--sYzmA.js +0 -0
- assets/index-BzGjruQ9.js +0 -0
- assets/index-CLisF1Ml.css +0 -1
- assets/index-D2Hvfy3l.js +0 -0
- assets/{index-A7Nq9zGD.js → index-DUQ_UspG.js} +0 -0
- assets/index-DnXFGQ8z.js +0 -0
- assets/{index-Bl39c3Js.css → index-Uy7sEXBC.css} +1 -1
- assets/index-wZi62oe2.js +0 -0
- index.html +12 -2
- licenses/dependencies.txt +196 -0
- scripts/stage.py +1 -1
- validation/LOCAL-FOLDER.md +12 -0
- validation/local-folder-browser-390.png +3 -0
- validation/local-folder-browser-review.png +3 -0
- validation/local-folder-browser.json +16 -0
- validation/local-folder-native.png +3 -0
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: MiniCPM5-2B Pi
|
| 3 |
emoji: 🥧
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
|
@@ -7,7 +7,7 @@ sdk: static
|
|
| 7 |
app_file: index.html
|
| 8 |
pinned: false
|
| 9 |
license: apache-2.0
|
| 10 |
-
short_description: Browser coding agent with
|
| 11 |
models:
|
| 12 |
- openbmb/MiniCPM5-2B
|
| 13 |
- Mike0021/MiniCPM5-2B-ONNX
|
|
@@ -19,10 +19,24 @@ tags:
|
|
| 19 |
- just-bash
|
| 20 |
---
|
| 21 |
|
| 22 |
-
# Pi browser
|
| 23 |
|
| 24 |
An independent duplicate of [victor/MiniCPM5-2B-WebGPU-Pi](https://huggingface.co/spaces/victor/MiniCPM5-2B-WebGPU-Pi) at revision `959e272adeb7931e32d3eb2177dc1d5e96c3e967`. The original Space is unchanged.
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
MiniCPM5-2B inference and just-bash run in a browser worker. This copy adds real `curl` requests through browser `fetch`, with no backend or proxy. The first model download is about 1.84 GB, verified and cached locally. WebGPU with shader-f16 is required for inference; the Shell can run HTTP commands without loading the model.
|
| 27 |
|
| 28 |
Try in Shell:
|
|
|
|
| 1 |
---
|
| 2 |
+
title: MiniCPM5-2B Pi workspace experiment
|
| 3 |
emoji: 🥧
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
|
|
|
| 7 |
app_file: index.html
|
| 8 |
pinned: false
|
| 9 |
license: apache-2.0
|
| 10 |
+
short_description: Browser coding agent with local folders and HTTP
|
| 11 |
models:
|
| 12 |
- openbmb/MiniCPM5-2B
|
| 13 |
- Mike0021/MiniCPM5-2B-ONNX
|
|
|
|
| 19 |
- just-bash
|
| 20 |
---
|
| 21 |
|
| 22 |
+
# Pi browser workspace experiment
|
| 23 |
|
| 24 |
An independent duplicate of [victor/MiniCPM5-2B-WebGPU-Pi](https://huggingface.co/spaces/victor/MiniCPM5-2B-WebGPU-Pi) at revision `959e272adeb7931e32d3eb2177dc1d5e96c3e967`. The original Space is unchanged.
|
| 25 |
|
| 26 |
+
## Local folder experiment
|
| 27 |
+
|
| 28 |
+
The demo remains the default. In the [standalone app](https://mike0021-minicpm5-2b-webgpu-pi-http.static.hf.space/), choose **Open local folder**. The selected folder replaces `/workspace` in the existing file panel and agent tools. Edits from Pi, the Shell, and the editor are staged. **Review & apply → Apply to folder** shows a diff and requests write permission before saving to disk. **Back to demo** restores the demo files and conversation; **Reconnect folder** resumes the local session, including staged edits saved separately in browser storage.
|
| 29 |
+
|
| 30 |
+
This uses the File System Access API, available in supported Chromium browsers. The Hugging Face iframe offers a link to the standalone app because cross-origin frames cannot request folder permissions. Safari and Firefox retain the existing Import/Export workflow for copies of individual files; they cannot save directly to a selected folder.
|
| 31 |
+
|
| 32 |
+
Files load on demand. **Refresh** reloads names and content from disk and starts a fresh chat; apply or discard staged edits first. Application compares each changed file with the version originally read and stops on conflicts. Multi-file application is not atomic: on a write failure, applied files are reported and the remaining changes stay staged. Avoid editing the same files in another program during Apply.
|
| 33 |
+
|
| 34 |
+
Current bounds: 1 MiB per file, 16 MiB staged content and 16 MiB read baselines, 5,000 indexed entries, depth 40. `.git`, `node_modules`, `.venv`, and `__pycache__` are excluded. Create/edit/delete/rename files and create directories are supported. Removing/renaming existing directories, symlinks, hard links, Unix permissions and timestamp changes are unsupported. Paths outside `/workspace` remain temporary virtual files. Local folder access does not provide Node, Python, npm or native programs. HTTP commands still send explicitly requested URLs/data to external servers.
|
| 35 |
+
|
| 36 |
+
Implementation: `app/src/local-folder.mjs`, `local-folder-ui.mjs`, and the existing worker/workspace modules. Tests: `cd app && npm test`; with the preview server on port 4193, run `node tests/local-folder-browser.mjs`. See [local-folder validation](validation/LOCAL-FOLDER.md).
|
| 37 |
+
|
| 38 |
+
## HTTP experiment
|
| 39 |
+
|
| 40 |
MiniCPM5-2B inference and just-bash run in a browser worker. This copy adds real `curl` requests through browser `fetch`, with no backend or proxy. The first model download is about 1.84 GB, verified and cached locally. WebGPU with shader-f16 is required for inference; the Shell can run HTTP commands without loading the model.
|
| 41 |
|
| 42 |
Try in Shell:
|
app/index.html
CHANGED
|
@@ -53,6 +53,11 @@
|
|
| 53 |
<div id="column-resizer" class="column-resizer" role="separator" tabindex="0" aria-label="Resize chat and workspace" aria-orientation="vertical" aria-controls="conversation-panel workspace-panel" aria-valuemin="0" aria-valuemax="100" aria-valuenow="55" title="Drag to resize · Arrow keys to adjust · Double-click or Enter to reset"></div>
|
| 54 |
<aside id="workspace-panel" class="workspace" aria-label="Workspace">
|
| 55 |
<div class="workspace-heading"><h2>Workspace</h2><div><button id="add-file" class="quiet">+ File</button><label class="button quiet" for="upload">Import</label><input id="upload" type="file" multiple hidden><button id="export" class="quiet">Export</button></div></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
<div class="file-area"><nav id="files" aria-label="Files"></nav><div class="editor-area"><div class="editor-heading"><span class="editor-file"><span id="filetype"></span><span id="filename">Select a file</span></span><button id="save" class="quiet" disabled>Save</button></div><label for="editor" class="sr-only">File contents</label><div class="editor-input"><pre id="editor-highlight" aria-hidden="true"></pre><textarea id="editor" wrap="off" autocomplete="off" autocapitalize="off" spellcheck="false" disabled placeholder="Select a file to inspect or edit it."></textarea></div></div></div>
|
| 57 |
<details class="terminal" open>
|
| 58 |
<summary>Shell <span>just-bash</span></summary>
|
|
@@ -68,6 +73,11 @@
|
|
| 68 |
<p class="workspace-note">HTTP: curl can request any HTTPS URL. Browser CORS applies. <a href="./http-help.html" target="_blank" rel="noopener">HTTP help ↗</a></p>
|
| 69 |
</aside>
|
| 70 |
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
<dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
|
| 72 |
<div class="setup-paws" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
| 73 |
<div class="setup-cat" aria-hidden="true"><span class="setup-cat-tail"></span><img src="./modal-cat-v1.png" width="1254" height="1254" alt="" draggable="false" loading="eager" fetchpriority="high" decoding="async"><span class="setup-cat-eyelids"></span></div>
|
|
|
|
| 53 |
<div id="column-resizer" class="column-resizer" role="separator" tabindex="0" aria-label="Resize chat and workspace" aria-orientation="vertical" aria-controls="conversation-panel workspace-panel" aria-valuemin="0" aria-valuemax="100" aria-valuenow="55" title="Drag to resize · Arrow keys to adjust · Double-click or Enter to reset"></div>
|
| 54 |
<aside id="workspace-panel" class="workspace" aria-label="Workspace">
|
| 55 |
<div class="workspace-heading"><h2>Workspace</h2><div><button id="add-file" class="quiet">+ File</button><label class="button quiet" for="upload">Import</label><input id="upload" type="file" multiple hidden><button id="export" class="quiet">Export</button></div></div>
|
| 56 |
+
<div class="folder-controls"><button id="open-folder" type="button" class="quiet">Open local folder</button><button id="reconnect-folder" type="button" class="quiet" hidden>Reconnect folder</button></div>
|
| 57 |
+
<div id="local-folder-bar" hidden>
|
| 58 |
+
<strong id="local-folder-name"></strong><span id="local-folder-hint" role="status"></span>
|
| 59 |
+
<div class="folder-actions"><button id="review-changes" type="button">Review & apply</button><button id="refresh-folder" type="button" class="quiet" title="Reload files from disk and start a fresh chat">Refresh</button><button id="disconnect-folder" type="button" class="quiet">Back to demo</button></div>
|
| 60 |
+
</div>
|
| 61 |
<div class="file-area"><nav id="files" aria-label="Files"></nav><div class="editor-area"><div class="editor-heading"><span class="editor-file"><span id="filetype"></span><span id="filename">Select a file</span></span><button id="save" class="quiet" disabled>Save</button></div><label for="editor" class="sr-only">File contents</label><div class="editor-input"><pre id="editor-highlight" aria-hidden="true"></pre><textarea id="editor" wrap="off" autocomplete="off" autocapitalize="off" spellcheck="false" disabled placeholder="Select a file to inspect or edit it."></textarea></div></div></div>
|
| 62 |
<details class="terminal" open>
|
| 63 |
<summary>Shell <span>just-bash</span></summary>
|
|
|
|
| 73 |
<p class="workspace-note">HTTP: curl can request any HTTPS URL. Browser CORS applies. <a href="./http-help.html" target="_blank" rel="noopener">HTTP help ↗</a></p>
|
| 74 |
</aside>
|
| 75 |
</main>
|
| 76 |
+
<dialog id="local-review" aria-labelledby="review-title" aria-describedby="review-description">
|
| 77 |
+
<div class="review-heading"><h2 id="review-title">Review local changes</h2><button id="review-close" type="button" aria-label="Close change review">×</button></div>
|
| 78 |
+
<p id="review-description"></p><div id="review-files"></div><p id="review-error" role="alert"></p>
|
| 79 |
+
<div class="review-actions"><button id="review-discard" type="button">Discard changes</button><button id="review-apply" type="button" class="primary">Apply to folder</button></div>
|
| 80 |
+
</dialog>
|
| 81 |
<dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
|
| 82 |
<div class="setup-paws" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
| 83 |
<div class="setup-cat" aria-hidden="true"><span class="setup-cat-tail"></span><img src="./modal-cat-v1.png" width="1254" height="1254" alt="" draggable="false" loading="eager" fetchpriority="high" decoding="async"><span class="setup-cat-eyelids"></span></div>
|
app/package-lock.json
CHANGED
|
@@ -17,6 +17,7 @@
|
|
| 17 |
"@xterm/addon-unicode-graphemes": "0.4.0",
|
| 18 |
"@xterm/xterm": "6.0.0",
|
| 19 |
"buffer": "6.0.3",
|
|
|
|
| 20 |
"events": "3.3.0",
|
| 21 |
"hash-wasm": "4.12.0",
|
| 22 |
"just-bash": "3.4.2",
|
|
|
|
| 17 |
"@xterm/addon-unicode-graphemes": "0.4.0",
|
| 18 |
"@xterm/xterm": "6.0.0",
|
| 19 |
"buffer": "6.0.3",
|
| 20 |
+
"diff": "8.0.4",
|
| 21 |
"events": "3.3.0",
|
| 22 |
"hash-wasm": "4.12.0",
|
| 23 |
"just-bash": "3.4.2",
|
app/package.json
CHANGED
|
@@ -36,6 +36,7 @@
|
|
| 36 |
"@xterm/addon-unicode-graphemes": "0.4.0",
|
| 37 |
"@xterm/xterm": "6.0.0",
|
| 38 |
"buffer": "6.0.3",
|
|
|
|
| 39 |
"events": "3.3.0",
|
| 40 |
"hash-wasm": "4.12.0",
|
| 41 |
"just-bash": "3.4.2",
|
|
|
|
| 36 |
"@xterm/addon-unicode-graphemes": "0.4.0",
|
| 37 |
"@xterm/xterm": "6.0.0",
|
| 38 |
"buffer": "6.0.3",
|
| 39 |
+
"diff": "8.0.4",
|
| 40 |
"events": "3.3.0",
|
| 41 |
"hash-wasm": "4.12.0",
|
| 42 |
"just-bash": "3.4.2",
|
app/src/agent.worker.mjs
CHANGED
|
@@ -3,10 +3,12 @@ import { createInference, localModel } from './inference.mjs';
|
|
| 3 |
import { createWorkspace, initialFiles, legacyProductsCsv, legacyProductsJson, legacyCostProductsJson, legacySalesCsv, legacyWorkspaceReadme, legacyProfitReadme } from './workspace.mjs';
|
| 4 |
import { openStorage } from './storage.mjs';
|
| 5 |
import { isModelCached } from './download.mjs';
|
|
|
|
| 6 |
|
| 7 |
const emit = data => postMessage(data);
|
| 8 |
const inference = createInference(emit);
|
| 9 |
let workspace, agent, storage, busy = false, operation, turnCount = 0;
|
|
|
|
| 10 |
const systemPrompt = `You are Pi, a coding assistant powered by MiniCPM5-2B, a language model from OpenBMB.
|
| 11 |
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.
|
| 12 |
Use your tools to inspect and change actual files. Never claim a command ran or a file changed without a successful tool result.
|
|
@@ -19,16 +21,29 @@ You can create and edit text files, including HTML, CSS, JavaScript, and shell s
|
|
| 19 |
Interpret short follow-ups using the conversation and inspect the relevant files when needed.
|
| 20 |
Inspect files before editing them. After changing files, verify the result with a tool. Finish with a short factual answer.`;
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
async function persist() {
|
| 24 |
const state = await snapshot();
|
| 25 |
-
try {
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
return state;
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
const initialized = (async () => {
|
| 30 |
let saved;
|
| 31 |
-
try { storage = await openStorage(); saved = await storage.load(); }
|
| 32 |
catch (e) { emit({ type: 'persistence_error', error: 'Workspace saving is unavailable: ' + e.message }); }
|
| 33 |
workspace = createWorkspace(saved?.files, saved?.entries);
|
| 34 |
await workspace.ready;
|
|
@@ -72,7 +87,7 @@ const initialized = (async () => {
|
|
| 72 |
try { await storage?.save(state); }
|
| 73 |
catch (e) { emit({ type: 'persistence_error', error: 'Changes are in memory, but browser storage failed: ' + e.message }); }
|
| 74 |
}
|
| 75 |
-
emit({ type: 'initialized', ...state });
|
| 76 |
})();
|
| 77 |
|
| 78 |
onmessage = async ({ data }) => {
|
|
@@ -83,6 +98,8 @@ onmessage = async ({ data }) => {
|
|
| 83 |
if (action === 'stop') { operation?.abort(); agent.abort(); inference.stop(); return reply(true); }
|
| 84 |
if (action === 'snapshot') return reply(await snapshot());
|
| 85 |
if (action === 'cache_status') return reply(await isModelCached());
|
|
|
|
|
|
|
| 86 |
if (busy) throw Error('Wait for the current operation or stop it first.');
|
| 87 |
busy = true; operation = new AbortController(); emit({ type: 'busy', action, cachedOnly: args.cachedOnly === true });
|
| 88 |
try {
|
|
@@ -95,6 +112,35 @@ onmessage = async ({ data }) => {
|
|
| 95 |
} else if (action === 'write') { await workspace.write(args.path, args.content); value = await persist(); }
|
| 96 |
else if (action === 'new_chat') { agent.reset(); value = await persist(); }
|
| 97 |
else if (action === 'read') value = await workspace.read(args.path);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
else throw Error('Unknown action: ' + action);
|
| 99 |
reply(value);
|
| 100 |
} finally { busy = false; operation = undefined; emit({ type: 'idle' }); }
|
|
|
|
| 3 |
import { createWorkspace, initialFiles, legacyProductsCsv, legacyProductsJson, legacyCostProductsJson, legacySalesCsv, legacyWorkspaceReadme, legacyProfitReadme } from './workspace.mjs';
|
| 4 |
import { openStorage } from './storage.mjs';
|
| 5 |
import { isModelCached } from './download.mjs';
|
| 6 |
+
import { LocalFolderFs, displayText } from './local-folder.mjs';
|
| 7 |
|
| 8 |
const emit = data => postMessage(data);
|
| 9 |
const inference = createInference(emit);
|
| 10 |
let workspace, agent, storage, busy = false, operation, turnCount = 0;
|
| 11 |
+
let demoWorkspace, demoMessages, savedLocal;
|
| 12 |
const systemPrompt = `You are Pi, a coding assistant powered by MiniCPM5-2B, a language model from OpenBMB.
|
| 13 |
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.
|
| 14 |
Use your tools to inspect and change actual files. Never claim a command ran or a file changed without a successful tool result.
|
|
|
|
| 21 |
Interpret short follow-ups using the conversation and inspect the relevant files when needed.
|
| 22 |
Inspect files before editing them. After changing files, verify the result with a tool. Finish with a short factual answer.`;
|
| 23 |
|
| 24 |
+
const localPrompt = systemPrompt.replace("Workspace files are stored in this browser, not on the user's operating-system filesystem.",
|
| 25 |
+
'The user selected a real local folder mounted at /workspace. Reads use that folder; all writes, edits and deletions are staged in this browser. Only the user can apply staged changes to disk with Review & apply. Say edits are staged, never claim they are saved to disk. Files outside /workspace are temporary virtual files. Files are limited to 1 MiB, 16 MiB total loaded; .git, node_modules, .venv and __pycache__ are excluded. Unix permissions, links and renaming/removing existing directories are unsupported.');
|
| 26 |
+
async function snapshot() { return { version: 1, demoVersion: 4, files: await workspace.snapshot(), entries: await workspace.serialize(), messages: agent.state.messages,
|
| 27 |
+
localFolder: workspace.localFolder ? await workspace.localFolder.status() : null }; }
|
| 28 |
async function persist() {
|
| 29 |
const state = await snapshot();
|
| 30 |
+
try {
|
| 31 |
+
if (workspace.localFolder) {
|
| 32 |
+
savedLocal = { handle: workspace.localFolder.handle, patches: await workspace.localFolder.patches(), messages: state.messages };
|
| 33 |
+
await storage?.save(savedLocal, 'local-folder');
|
| 34 |
+
} else await storage?.save(state);
|
| 35 |
+
} catch (e) { emit({ type: 'persistence_error', error: 'Changes are in memory, but browser storage failed: ' + e.message }); }
|
| 36 |
+
emit({ type: 'workspace', files: state.files, localFolder: state.localFolder });
|
| 37 |
return state;
|
| 38 |
}
|
| 39 |
+
function switchWorkspace(next, messages = []) {
|
| 40 |
+
workspace = next;
|
| 41 |
+
agent.reset(); agent.state.messages = messages; agent.state.tools = next.tools;
|
| 42 |
+
agent.state.systemPrompt = next.localFolder ? localPrompt : systemPrompt;
|
| 43 |
+
}
|
| 44 |
const initialized = (async () => {
|
| 45 |
let saved;
|
| 46 |
+
try { storage = await openStorage(); saved = await storage.load(); savedLocal = await storage.load('local-folder'); }
|
| 47 |
catch (e) { emit({ type: 'persistence_error', error: 'Workspace saving is unavailable: ' + e.message }); }
|
| 48 |
workspace = createWorkspace(saved?.files, saved?.entries);
|
| 49 |
await workspace.ready;
|
|
|
|
| 87 |
try { await storage?.save(state); }
|
| 88 |
catch (e) { emit({ type: 'persistence_error', error: 'Changes are in memory, but browser storage failed: ' + e.message }); }
|
| 89 |
}
|
| 90 |
+
emit({ type: 'initialized', ...state, savedLocal: savedLocal ? { handle: savedLocal.handle, count: savedLocal.patches.length } : null });
|
| 91 |
})();
|
| 92 |
|
| 93 |
onmessage = async ({ data }) => {
|
|
|
|
| 98 |
if (action === 'stop') { operation?.abort(); agent.abort(); inference.stop(); return reply(true); }
|
| 99 |
if (action === 'snapshot') return reply(await snapshot());
|
| 100 |
if (action === 'cache_status') return reply(await isModelCached());
|
| 101 |
+
if (action === 'preview') return reply(workspace.localFolder
|
| 102 |
+
? displayText(await workspace.bash.fs.readFileBuffer(args.path)) : await workspace.read(args.path));
|
| 103 |
if (busy) throw Error('Wait for the current operation or stop it first.');
|
| 104 |
busy = true; operation = new AbortController(); emit({ type: 'busy', action, cachedOnly: args.cachedOnly === true });
|
| 105 |
try {
|
|
|
|
| 112 |
} else if (action === 'write') { await workspace.write(args.path, args.content); value = await persist(); }
|
| 113 |
else if (action === 'new_chat') { agent.reset(); value = await persist(); }
|
| 114 |
else if (action === 'read') value = await workspace.read(args.path);
|
| 115 |
+
else if (action === 'local_open') {
|
| 116 |
+
if (workspace.localFolder) throw Error('Return to the demo before opening another local folder.');
|
| 117 |
+
const restore = args.restore && savedLocal && await args.handle.isSameEntry(savedLocal.handle);
|
| 118 |
+
const folder = await LocalFolderFs.open(args.handle, { signal: operation.signal, patches: restore ? savedLocal.patches : [] });
|
| 119 |
+
const next = createWorkspace({}, undefined, folder); await next.ready;
|
| 120 |
+
await persist(); demoWorkspace = workspace; demoMessages = agent.state.messages;
|
| 121 |
+
switchWorkspace(next, restore ? savedLocal.messages : []);
|
| 122 |
+
value = await persist();
|
| 123 |
+
} else if (action === 'local_close') {
|
| 124 |
+
if (!workspace.localFolder) throw Error('No local folder is open.');
|
| 125 |
+
await persist(); switchWorkspace(demoWorkspace, demoMessages);
|
| 126 |
+
demoWorkspace = demoMessages = undefined;
|
| 127 |
+
value = await persist();
|
| 128 |
+
} else if (action === 'local_changes') {
|
| 129 |
+
if (!workspace.localFolder) throw Error('No local folder is open.');
|
| 130 |
+
value = await workspace.localFolder.patches();
|
| 131 |
+
} else if (action === 'local_apply') {
|
| 132 |
+
if (!workspace.localFolder) throw Error('No local folder is open.');
|
| 133 |
+
try { value = { applied: await workspace.localFolder.apply(operation.signal) }; }
|
| 134 |
+
finally { await persist(); }
|
| 135 |
+
} else if (action === 'local_refresh' || action === 'local_discard') {
|
| 136 |
+
const folder = workspace.localFolder;
|
| 137 |
+
if (!folder) throw Error('No local folder is open.');
|
| 138 |
+
if (action === 'local_refresh' && (await folder.patches()).length) throw Error('Apply or discard staged edits before refreshing.');
|
| 139 |
+
const fresh = await LocalFolderFs.open(folder.handle, { signal: operation.signal });
|
| 140 |
+
const next = createWorkspace({}, undefined, fresh); await next.ready;
|
| 141 |
+
// A fresh conversation avoids stale tool results after external changes.
|
| 142 |
+
switchWorkspace(next); value = await persist();
|
| 143 |
+
}
|
| 144 |
else throw Error('Unknown action: ' + action);
|
| 145 |
reply(value);
|
| 146 |
} finally { busy = false; operation = undefined; emit({ type: 'idle' }); }
|
app/src/local-folder-ui.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createTwoFilesPatch } from 'diff';
|
| 2 |
+
|
| 3 |
+
const $ = selector => document.querySelector(selector);
|
| 4 |
+
const text = value => {
|
| 5 |
+
if (value === null) return '';
|
| 6 |
+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(atob(value), c => c.charCodeAt(0)));
|
| 7 |
+
if (decoded.includes('\0')) throw Error('Binary file');
|
| 8 |
+
return decoded;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
export function createLocalFolderUI({ rpc, notice, canSwitch, onSwitch }) {
|
| 12 |
+
let handle, saved, current, busy = false, opening = false, reviewPatches = [];
|
| 13 |
+
const dialog = $('#local-review');
|
| 14 |
+
const embedded = window.self !== window.top;
|
| 15 |
+
const supported = typeof window.showDirectoryPicker === 'function';
|
| 16 |
+
function controls() {
|
| 17 |
+
$('#open-folder').hidden = Boolean(current);
|
| 18 |
+
$('#reconnect-folder').hidden = Boolean(current) || !saved;
|
| 19 |
+
$('#local-folder-bar').hidden = !current;
|
| 20 |
+
for (const id of ['open-folder', 'reconnect-folder', 'review-changes', 'refresh-folder', 'disconnect-folder']) $('#' + id).disabled = busy || opening;
|
| 21 |
+
$('#review-changes').disabled ||= !current?.count;
|
| 22 |
+
$('#refresh-folder').disabled ||= Boolean(current?.count);
|
| 23 |
+
$('#review-apply').disabled = busy || !reviewPatches.length;
|
| 24 |
+
$('#review-discard').disabled = busy || !reviewPatches.length;
|
| 25 |
+
$('#review-close').disabled = busy;
|
| 26 |
+
if (current) {
|
| 27 |
+
$('#local-folder-name').textContent = current.name;
|
| 28 |
+
$('#local-folder-hint').textContent = current.count ? `${current.count} staged change${current.count === 1 ? '' : 's'} · original files unchanged until Apply` : 'Local folder · edits are staged until you apply them';
|
| 29 |
+
$('#review-changes').textContent = current.count ? `Review & apply (${current.count})` : 'Review & apply';
|
| 30 |
+
}
|
| 31 |
+
$('#export').textContent = current ? 'Export changes' : 'Export';
|
| 32 |
+
$('#save').textContent = current ? 'Stage edit' : 'Save';
|
| 33 |
+
}
|
| 34 |
+
function available() {
|
| 35 |
+
if (embedded) {
|
| 36 |
+
$('#open-tab').href = new URL('?local-folder', location.href).href; $('#open-tab').hidden = false;
|
| 37 |
+
notice('Open the workspace in a new tab, then choose Open local folder. Browsers cannot grant folder access inside this embedded Space.');
|
| 38 |
+
return false;
|
| 39 |
+
}
|
| 40 |
+
if (!supported) {
|
| 41 |
+
notice('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.');
|
| 42 |
+
return false;
|
| 43 |
+
}
|
| 44 |
+
return true;
|
| 45 |
+
}
|
| 46 |
+
async function connect(restore) {
|
| 47 |
+
if (!available() || busy || opening) return;
|
| 48 |
+
try {
|
| 49 |
+
canSwitch(); opening = true; controls();
|
| 50 |
+
// Invoke the picker/permission request directly from the click, before
|
| 51 |
+
// waiting for the worker: these APIs require transient user activation.
|
| 52 |
+
let next;
|
| 53 |
+
if (restore) {
|
| 54 |
+
next = saved.handle;
|
| 55 |
+
if (await next.requestPermission({ mode: 'read' }) !== 'granted') throw Error('Folder access was not granted.');
|
| 56 |
+
} else {
|
| 57 |
+
if (saved?.count && !window.confirm('Opening another folder replaces the saved staged edits for the previous folder. Reconnect to review them first, or continue?')) return;
|
| 58 |
+
next = await window.showDirectoryPicker({ id: 'pi-workspace', mode: 'read' });
|
| 59 |
+
}
|
| 60 |
+
const state = await rpc('local_open', { handle: next, restore });
|
| 61 |
+
handle = next; saved = { handle: next, count: state.localFolder.count };
|
| 62 |
+
onSwitch(state); notice(`Opened ${next.name}. Review & apply writes staged changes to disk. Files load on demand; Refresh picks up external edits.`);
|
| 63 |
+
} catch (error) { if (error.name !== 'AbortError') notice(error.message); }
|
| 64 |
+
finally { opening = false; controls(); }
|
| 65 |
+
}
|
| 66 |
+
$('#open-folder').onclick = () => connect(false);
|
| 67 |
+
$('#reconnect-folder').onclick = () => connect(true);
|
| 68 |
+
$('#disconnect-folder').onclick = async () => {
|
| 69 |
+
try {
|
| 70 |
+
canSwitch(); saved = { handle, count: current.count };
|
| 71 |
+
const state = await rpc('local_close'); onSwitch(state);
|
| 72 |
+
notice('Demo workspace restored. Reconnect to continue with your local folder and staged edits.');
|
| 73 |
+
} catch (error) { notice(error.message); }
|
| 74 |
+
};
|
| 75 |
+
$('#refresh-folder').onclick = async () => {
|
| 76 |
+
try { canSwitch(); onSwitch(await rpc('local_refresh')); notice('Folder refreshed from disk. Started a new chat with the current files.'); }
|
| 77 |
+
catch (error) { notice(error.message); }
|
| 78 |
+
};
|
| 79 |
+
function renderReview(patches) {
|
| 80 |
+
reviewPatches = patches;
|
| 81 |
+
$('#review-files').replaceChildren(); $('#review-error').textContent = '';
|
| 82 |
+
$('#review-description').textContent = `Review changes to ${current.name}. Applying writes them to your local folder.`;
|
| 83 |
+
for (const patch of patches) {
|
| 84 |
+
const details = document.createElement('details'), title = document.createElement('summary'), pre = document.createElement('pre');
|
| 85 |
+
const kind = patch.kind === 'directory' ? 'New folder' : patch.after === null ? 'Delete' : patch.before === null ? 'Create' : 'Edit';
|
| 86 |
+
title.textContent = `${kind} · ${patch.path.slice(1)}`;
|
| 87 |
+
details.append(title); details.open = patches.length < 5;
|
| 88 |
+
if (patch.kind !== 'directory') {
|
| 89 |
+
try {
|
| 90 |
+
const before = text(patch.before), after = text(patch.after);
|
| 91 |
+
if (before.length + after.length > 160_000) pre.textContent = 'Large text change. Inspect the file in the editor before applying.';
|
| 92 |
+
else {
|
| 93 |
+
const diff = createTwoFilesPatch(patch.path, patch.path, before, after, 'on disk when read', 'staged', { context: 3 });
|
| 94 |
+
for (const line of diff.split('\n')) {
|
| 95 |
+
const span = document.createElement('span'); span.textContent = line + '\n';
|
| 96 |
+
if (line.startsWith('+')) span.className = 'diff-add';
|
| 97 |
+
if (line.startsWith('-')) span.className = 'diff-remove';
|
| 98 |
+
pre.append(span);
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
} catch { pre.textContent = `Binary file · ${patch.before === null ? 0 : atob(patch.before).length} → ${patch.after === null ? 0 : atob(patch.after).length} bytes`; }
|
| 102 |
+
details.append(pre);
|
| 103 |
+
}
|
| 104 |
+
$('#review-files').append(details);
|
| 105 |
+
}
|
| 106 |
+
controls();
|
| 107 |
+
}
|
| 108 |
+
$('#review-changes').onclick = async () => {
|
| 109 |
+
try { canSwitch(); renderReview(await rpc('local_changes')); dialog.showModal(); }
|
| 110 |
+
catch (error) { notice(error.message); }
|
| 111 |
+
};
|
| 112 |
+
$('#review-close').onclick = () => dialog.close();
|
| 113 |
+
dialog.addEventListener('cancel', event => { if (busy) event.preventDefault(); });
|
| 114 |
+
$('#review-apply').onclick = async () => {
|
| 115 |
+
try {
|
| 116 |
+
// Only this user gesture requests write access. The worker never prompts.
|
| 117 |
+
if (await handle.requestPermission({ mode: 'readwrite' }) !== 'granted') throw Error('Write access was not granted. Changes are still staged.');
|
| 118 |
+
const result = await rpc('local_apply');
|
| 119 |
+
dialog.close(); notice(`Applied ${result.applied} change${result.applied === 1 ? '' : 's'} to ${handle.name}.`);
|
| 120 |
+
} catch (error) {
|
| 121 |
+
// Application can partially succeed; refresh the review to show only the remaining edits.
|
| 122 |
+
try { renderReview(await rpc('local_changes')); } catch {}
|
| 123 |
+
$('#review-error').textContent = error.message;
|
| 124 |
+
}
|
| 125 |
+
};
|
| 126 |
+
$('#review-discard').onclick = async () => {
|
| 127 |
+
if (!window.confirm('Discard all staged changes and reload files from disk? Your local files will not be changed.')) return;
|
| 128 |
+
try { onSwitch(await rpc('local_discard')); dialog.close(); notice('Staged edits discarded. Folder refreshed and chat reset.'); }
|
| 129 |
+
catch (error) { $('#review-error').textContent = error.message; }
|
| 130 |
+
};
|
| 131 |
+
window.addEventListener('beforeunload', event => {
|
| 132 |
+
if (current?.count) { event.preventDefault(); event.returnValue = ''; }
|
| 133 |
+
});
|
| 134 |
+
if (new URLSearchParams(location.search).has('local-folder')) notice('Choose Open local folder to select the project you want Pi to work on.');
|
| 135 |
+
return {
|
| 136 |
+
setBusy(value) { busy = value; controls(); },
|
| 137 |
+
setState(value) { current = value; if (saved && value) saved.count = value.count; controls(); },
|
| 138 |
+
restore(value) { saved = value; controls(); },
|
| 139 |
+
get active() { return Boolean(current); },
|
| 140 |
+
};
|
| 141 |
+
}
|
app/src/local-folder.mjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { InMemoryFs, MountableFs } from 'just-bash/browser';
|
| 2 |
+
|
| 3 |
+
// Only enumerate names at connection time. File bytes are loaded on demand,
|
| 4 |
+
// into a bounded staging filesystem; only Apply can write to the real handle.
|
| 5 |
+
export const LOCAL_FILE_LIMIT = 1024 ** 2;
|
| 6 |
+
const TOTAL_LIMIT = 16 * LOCAL_FILE_LIMIT;
|
| 7 |
+
const SKIP = new Set(['.git', 'node_modules', '.venv', '__pycache__']);
|
| 8 |
+
const same = (a, b) => a?.length === b?.length && (a == null || a.every((v, i) => v === b[i]));
|
| 9 |
+
export function encodeBytes(bytes) {
|
| 10 |
+
if (bytes == null) return null;
|
| 11 |
+
let text = '';
|
| 12 |
+
for (let i = 0; i < bytes.length; i += 32768) text += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
| 13 |
+
return btoa(text);
|
| 14 |
+
}
|
| 15 |
+
export const decodeBytes = text => text == null ? null : Uint8Array.from(atob(text), c => c.charCodeAt(0));
|
| 16 |
+
export function displayText(bytes) {
|
| 17 |
+
if (bytes == null) return '';
|
| 18 |
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
| 19 |
+
if (text.includes('\0')) throw Error('This is a binary file. The editor supports UTF-8 text.');
|
| 20 |
+
return text;
|
| 21 |
+
}
|
| 22 |
+
const unsupported = () => { throw Error('ENOTSUP: Local folders do not support Unix permissions, timestamps, or links.'); };
|
| 23 |
+
|
| 24 |
+
export class LocalFolderFs {
|
| 25 |
+
constructor(handle) {
|
| 26 |
+
this.handle = handle;
|
| 27 |
+
this.memory = new InMemoryFs({}, { maxTotalBytes: TOTAL_LIMIT });
|
| 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 |
+
}
|
| 34 |
+
static async open(handle, { signal, patches = [] } = {}) {
|
| 35 |
+
if (handle?.kind !== 'directory') throw Error('Select a folder.');
|
| 36 |
+
const fs = new LocalFolderFs(handle);
|
| 37 |
+
let count = 0;
|
| 38 |
+
async function walk(dir, prefix, depth) {
|
| 39 |
+
if (depth > 40) throw Error('This folder is too deeply nested. Choose a smaller project folder.');
|
| 40 |
+
for await (const [name, child] of dir.entries()) {
|
| 41 |
+
signal?.throwIfAborted();
|
| 42 |
+
if (SKIP.has(name)) { fs.skipped++; continue; }
|
| 43 |
+
if (++count > 5000) throw Error('This folder contains more than 5,000 entries. Choose a smaller project folder.');
|
| 44 |
+
const path = prefix + '/' + name;
|
| 45 |
+
fs.original.set(path, { kind: child.kind, handle: child });
|
| 46 |
+
if (child.kind === 'directory') {
|
| 47 |
+
await fs.memory.mkdir(path, { recursive: true });
|
| 48 |
+
await walk(child, path, depth + 1);
|
| 49 |
+
} else fs.memory.writeFileLazy(path, () => fs.loadOriginal(path));
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
await walk(handle, '', 0);
|
| 53 |
+
for (const patch of patches) {
|
| 54 |
+
const path = fs.path(patch.path);
|
| 55 |
+
if (path === '/') throw Error('Invalid saved local-folder change.');
|
| 56 |
+
const before = decodeBytes(patch.before);
|
| 57 |
+
fs.baselineBytes += before?.length ?? 0;
|
| 58 |
+
if (fs.baselineBytes > TOTAL_LIMIT) throw Error('Saved changes exceed the 16 MiB local-folder limit.');
|
| 59 |
+
fs.baselines.set(path, before);
|
| 60 |
+
fs.dirty.add(path);
|
| 61 |
+
if (patch.kind === 'directory') await fs.memory.mkdir(path, { recursive: true });
|
| 62 |
+
else if (patch.after === null) await fs.memory.rm(path, { force: true });
|
| 63 |
+
else {
|
| 64 |
+
await fs.memory.mkdir(fs.parent(path), { recursive: true });
|
| 65 |
+
await fs.memory.writeFile(path, decodeBytes(patch.after));
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
return fs;
|
| 69 |
+
}
|
| 70 |
+
path(value) {
|
| 71 |
+
if (typeof value !== 'string' || value.includes('\0')) throw Error('Invalid file path.');
|
| 72 |
+
const path = this.memory.resolvePath('/', value);
|
| 73 |
+
if (path.split('/').some(part => SKIP.has(part))) throw Error('This folder is excluded: .git, node_modules, .venv, and __pycache__.');
|
| 74 |
+
return path;
|
| 75 |
+
}
|
| 76 |
+
parent(path) { return path.slice(0, path.lastIndexOf('/')) || '/'; }
|
| 77 |
+
async diskBytes(path) {
|
| 78 |
+
const handle = await this.diskEntry(path);
|
| 79 |
+
if (!handle) return null;
|
| 80 |
+
if (handle.kind !== 'file') throw Error('A folder now occupies ' + path + '. No file was overwritten.');
|
| 81 |
+
const file = await handle.getFile();
|
| 82 |
+
if (file.size > LOCAL_FILE_LIMIT) throw Error(path + ' is larger than the 1 MiB per-file limit.');
|
| 83 |
+
return new Uint8Array(await file.arrayBuffer());
|
| 84 |
+
}
|
| 85 |
+
async diskEntry(path) {
|
| 86 |
+
let dir = this.handle;
|
| 87 |
+
const parts = this.path(path).split('/').filter(Boolean);
|
| 88 |
+
if (!parts.length) return dir;
|
| 89 |
+
try {
|
| 90 |
+
for (const part of parts.slice(0, -1)) dir = await dir.getDirectoryHandle(part);
|
| 91 |
+
try { return await dir.getFileHandle(parts.at(-1)); }
|
| 92 |
+
catch (e) { if (e.name !== 'TypeMismatchError') throw e; return await dir.getDirectoryHandle(parts.at(-1)); }
|
| 93 |
+
} catch (e) { if (e.name === 'NotFoundError') return null; throw e; }
|
| 94 |
+
}
|
| 95 |
+
async loadOriginal(path) {
|
| 96 |
+
const bytes = await this.diskBytes(path);
|
| 97 |
+
if (!bytes) throw Error('ENOENT: File was removed from disk: ' + path + '. Refresh the folder.');
|
| 98 |
+
// Preserve the version actually read, even if the host file later changes.
|
| 99 |
+
if (!this.baselines.has(path)) {
|
| 100 |
+
if (this.baselineBytes + bytes.length > TOTAL_LIMIT) throw Error('Local file reads reached the 16 MiB session limit. Apply or discard changes, then Refresh.');
|
| 101 |
+
this.baselines.set(path, bytes); this.baselineBytes += bytes.length;
|
| 102 |
+
}
|
| 103 |
+
return bytes;
|
| 104 |
+
}
|
| 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 readFile(path, options) { return this.memory.readFile(this.path(path), options); }
|
| 109 |
+
async readFileBuffer(path) { return this.memory.readFileBuffer(this.path(path)); }
|
| 110 |
+
async readFileBytes(path) { return this.memory.readFileBytes(this.path(path)); }
|
| 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) && !this.baselines.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 |
+
}
|
| 121 |
+
async lstat(path) { return this.stat(path); }
|
| 122 |
+
async realpath(path) { return this.memory.realpath(this.path(path)); }
|
| 123 |
+
resolvePath(base, path) { return this.memory.resolvePath(base, path); }
|
| 124 |
+
getAllPaths() { return this.memory.getAllPaths(); }
|
| 125 |
+
async readdir(path) { return this.memory.readdir(this.path(path)); }
|
| 126 |
+
async readdirWithFileTypes(path) { return this.memory.readdirWithFileTypes(this.path(path)); }
|
| 127 |
+
async mkdir(value, options) {
|
| 128 |
+
const path = this.path(value), previous = new Set(this.getAllPaths());
|
| 129 |
+
await this.memory.mkdir(path, options);
|
| 130 |
+
for (const created of this.getAllPaths()) if (!previous.has(created)) { this.dirty.add(created); this.baselines.set(created, null); }
|
| 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 });
|
| 137 |
+
await temporary.writeFile('/data', content, options);
|
| 138 |
+
const bytes = await temporary.readFileBuffer('/data');
|
| 139 |
+
if (bytes.length > LOCAL_FILE_LIMIT) throw Error('Local files are limited to 1 MiB.');
|
| 140 |
+
if (!await this.memory.exists(this.parent(path))) throw Error('ENOENT: Parent folder does not exist: ' + this.parent(path));
|
| 141 |
+
await this.baseline(path);
|
| 142 |
+
await this.memory.writeFile(path, bytes);
|
| 143 |
+
this.dirty.add(path);
|
| 144 |
+
}
|
| 145 |
+
async appendFile(path, content, options) {
|
| 146 |
+
const old = await this.exists(path) ? await this.readFileBuffer(path) : new Uint8Array();
|
| 147 |
+
const temporary = new InMemoryFs({ '/data': old }, { maxTotalBytes: LOCAL_FILE_LIMIT });
|
| 148 |
+
await temporary.appendFile('/data', content, options);
|
| 149 |
+
await this.writeFile(path, await temporary.readFileBuffer('/data'));
|
| 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) {
|
| 156 |
+
if (this.original.has(path)) throw Error('Removing or renaming existing local folders is not supported. Remove individual files instead.');
|
| 157 |
+
for (const p of this.getAllPaths().filter(p => p.startsWith(path + '/'))) {
|
| 158 |
+
if (this.original.has(p)) throw Error('This folder contains existing local files.');
|
| 159 |
+
}
|
| 160 |
+
const removed = this.getAllPaths().filter(p => p === path || p.startsWith(path + '/'));
|
| 161 |
+
await this.memory.rm(path, options);
|
| 162 |
+
for (const p of removed) { this.dirty.delete(p); this.baselines.delete(p); }
|
| 163 |
+
} else {
|
| 164 |
+
await this.baseline(path); await this.memory.rm(path, options); this.dirty.add(path);
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
async cp(source, destination, options) {
|
| 168 |
+
const src = this.path(source); let dest = this.path(destination);
|
| 169 |
+
if (await this.exists(dest) && (await this.stat(dest)).isDirectory) dest += '/' + src.split('/').at(-1);
|
| 170 |
+
if (src === dest || dest.startsWith(src + '/')) throw Error('Cannot copy a path into itself.');
|
| 171 |
+
if ((await this.stat(src)).isDirectory) {
|
| 172 |
+
if (!options?.recursive) throw Error('EISDIR: Use cp -r to copy a directory.');
|
| 173 |
+
await this.mkdir(dest, { recursive: true });
|
| 174 |
+
for (const name of await this.readdir(src)) await this.cp(src + '/' + name, dest + '/' + name, options);
|
| 175 |
+
} else await this.writeFile(dest, await this.readFileBuffer(src));
|
| 176 |
+
}
|
| 177 |
+
async mv(src, dest) {
|
| 178 |
+
if ((await this.stat(src)).isDirectory) throw Error('Renaming local folders is not supported.');
|
| 179 |
+
await this.cp(src, dest); await this.rm(src);
|
| 180 |
+
}
|
| 181 |
+
chmod = unsupported;
|
| 182 |
+
symlink = unsupported;
|
| 183 |
+
link = unsupported;
|
| 184 |
+
readlink = unsupported;
|
| 185 |
+
utimes = unsupported;
|
| 186 |
+
|
| 187 |
+
async patches() {
|
| 188 |
+
const patches = [];
|
| 189 |
+
for (const path of [...this.dirty].sort()) {
|
| 190 |
+
const before = this.baselines.get(path) ?? null;
|
| 191 |
+
const exists = await this.exists(path);
|
| 192 |
+
const kind = exists && (await this.memory.stat(path)).isDirectory ? 'directory' : 'file';
|
| 193 |
+
const after = exists && kind === 'file' ? await this.readFileBuffer(path) : null;
|
| 194 |
+
if (kind === 'file' && same(before, after)) continue;
|
| 195 |
+
patches.push({ path, kind, before: encodeBytes(before), after: encodeBytes(after) });
|
| 196 |
+
}
|
| 197 |
+
return patches;
|
| 198 |
+
}
|
| 199 |
+
async listing() {
|
| 200 |
+
const files = Object.create(null);
|
| 201 |
+
for (const path of this.getAllPaths()) {
|
| 202 |
+
const entry = this.original.get(path);
|
| 203 |
+
if (entry?.kind === 'file' || (!entry && (await this.memory.stat(path)).isFile)) files['/workspace' + path] = null;
|
| 204 |
+
}
|
| 205 |
+
return files;
|
| 206 |
+
}
|
| 207 |
+
async status() {
|
| 208 |
+
return { name: this.handle.name, count: (await this.patches()).length, skipped: this.skipped };
|
| 209 |
+
}
|
| 210 |
+
async apply(signal) {
|
| 211 |
+
const patches = await this.patches();
|
| 212 |
+
// Preflight the entire set before touching disk. Check again per file to
|
| 213 |
+
// catch edits made during application. File System Access has no multi-file transaction.
|
| 214 |
+
const check = async patch => {
|
| 215 |
+
const actual = await this.diskEntry(patch.path);
|
| 216 |
+
if (patch.kind === 'directory') {
|
| 217 |
+
if (actual) throw Error('Conflict: ' + patch.path + ' was created on disk.');
|
| 218 |
+
} else if (!same(await this.diskBytes(patch.path), decodeBytes(patch.before))) {
|
| 219 |
+
throw Error('Conflict: ' + patch.path + ' changed on disk. Nothing will overwrite it. Discard staged edits and refresh to read the new version.');
|
| 220 |
+
}
|
| 221 |
+
};
|
| 222 |
+
for (const patch of patches) { signal?.throwIfAborted(); await check(patch); }
|
| 223 |
+
let applied = 0;
|
| 224 |
+
try {
|
| 225 |
+
for (const patch of patches) {
|
| 226 |
+
signal?.throwIfAborted(); await check(patch);
|
| 227 |
+
const parts = patch.path.split('/').filter(Boolean); let dir = this.handle;
|
| 228 |
+
for (const part of parts.slice(0, -1)) dir = await dir.getDirectoryHandle(part);
|
| 229 |
+
const name = parts.at(-1);
|
| 230 |
+
if (patch.kind === 'directory') {
|
| 231 |
+
const handle = await dir.getDirectoryHandle(name, { create: true });
|
| 232 |
+
this.original.set(patch.path, { kind: 'directory', handle });
|
| 233 |
+
} else if (patch.after === null) {
|
| 234 |
+
await dir.removeEntry(name); this.original.delete(patch.path);
|
| 235 |
+
this.baselineBytes -= this.baselines.get(patch.path)?.length ?? 0; this.baselines.delete(patch.path);
|
| 236 |
+
} else {
|
| 237 |
+
const handle = await dir.getFileHandle(name, { create: true });
|
| 238 |
+
let stream;
|
| 239 |
+
try {
|
| 240 |
+
stream = await handle.createWritable(); await stream.write(decodeBytes(patch.after));
|
| 241 |
+
signal?.throwIfAborted(); await stream.close();
|
| 242 |
+
} catch (e) {
|
| 243 |
+
await stream?.abort().catch(() => {});
|
| 244 |
+
// create:true creates an empty entry before createWritable runs.
|
| 245 |
+
// If writing failed, remove only our still-empty new entry.
|
| 246 |
+
if (patch.before === null) {
|
| 247 |
+
try { if ((await handle.getFile()).size === 0 && await handle.isSameEntry(await dir.getFileHandle(name))) await dir.removeEntry(name); } catch {}
|
| 248 |
+
}
|
| 249 |
+
throw e;
|
| 250 |
+
}
|
| 251 |
+
this.original.set(patch.path, { kind: 'file', handle });
|
| 252 |
+
this.baselineBytes += decodeBytes(patch.after).length - (this.baselines.get(patch.path)?.length ?? 0);
|
| 253 |
+
this.baselines.set(patch.path, decodeBytes(patch.after));
|
| 254 |
+
}
|
| 255 |
+
this.dirty.delete(patch.path); applied++;
|
| 256 |
+
}
|
| 257 |
+
} catch (error) {
|
| 258 |
+
throw Error(`${applied} of ${patches.length} changes applied. Remaining changes are still staged. ${error.message}`);
|
| 259 |
+
}
|
| 260 |
+
return applied;
|
| 261 |
+
}
|
| 262 |
+
mount() {
|
| 263 |
+
return new MountableFs({ mounts: [{ mountPoint: '/workspace', filesystem: this }] });
|
| 264 |
+
}
|
| 265 |
+
}
|
app/src/main.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { createRuntimeMeter } from './runtime-meter.mjs';
|
|
| 7 |
import { createFileIcon } from './file-icons.mjs';
|
| 8 |
import { createEditorUI } from './editor-ui.mjs';
|
| 9 |
import { createColumnResizer } from './column-resizer.mjs';
|
|
|
|
| 10 |
|
| 11 |
const $ = selector => document.querySelector(selector);
|
| 12 |
createColumnResizer($('main'), $('#column-resizer'));
|
|
@@ -14,6 +15,7 @@ const worker = new Worker(new URL('./agent.worker.mjs', import.meta.url), { type
|
|
| 14 |
const pending = new Map();
|
| 15 |
let nextId = 1, busy = false, loaded = false, selected, dirty = false, files = {}, messages = [], readyResolve;
|
| 16 |
let pendingReadPath;
|
|
|
|
| 17 |
const ready = new Promise(resolve => { readyResolve = resolve; });
|
| 18 |
const diagnostics = { events: [], generations: [], runtimeSamples: [], device: null, contextUsage: restoredContextUsage([]) };
|
| 19 |
const runtimeMeter = createRuntimeMeter($('#runtime-meter'));
|
|
@@ -31,6 +33,22 @@ const shellUI = createShellUI($('.workspace > .terminal'), { run: async command
|
|
| 31 |
if (busy) throw Error('Wait for Pi to finish or stop the current task first.');
|
| 32 |
return rpc('shell', { command });
|
| 33 |
} });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
function rpc(action, args) {
|
| 35 |
return new Promise((resolve, reject) => {
|
| 36 |
const id = nextId++; pending.set(id, { resolve, reject }); worker.postMessage({ id, action, args });
|
|
@@ -55,8 +73,9 @@ function controls() {
|
|
| 55 |
$('#stop').hidden = !busy;
|
| 56 |
for (const id of ['#new-chat', '#add-file', '#upload']) $(id).disabled = busy;
|
| 57 |
shellUI.setBusy(busy);
|
|
|
|
| 58 |
$('#save').disabled = busy || !selected || !dirty;
|
| 59 |
-
$('#editor').disabled = busy || !selected;
|
| 60 |
}
|
| 61 |
function renderFiles() {
|
| 62 |
terminalUI.setFiles(files);
|
|
@@ -68,7 +87,24 @@ function renderFiles() {
|
|
| 68 |
button.title = file; button.classList.toggle('selected', file === selected);
|
| 69 |
button.onclick = () => selectFile(file); $('#files').append(button);
|
| 70 |
}
|
| 71 |
-
if (selected && !
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
}
|
| 73 |
function selectFile(file) {
|
| 74 |
pendingReadPath = undefined;
|
|
@@ -107,6 +143,7 @@ worker.onmessage = ({ data }) => {
|
|
| 107 |
const request = pending.get(data.id); pending.delete(data.id);
|
| 108 |
if (data.error) request?.reject(Object.assign(Error(data.error), { name: data.errorName || 'Error', code: data.errorCode })); else request?.resolve(data.value);
|
| 109 |
} else if (data.type === 'initialized') {
|
|
|
|
| 110 |
files = data.files; messages = data.messages; renderFiles(); renderMessages();
|
| 111 |
updateContextUsage(restoredContextUsage(messages));
|
| 112 |
if (files['/workspace/README.md']) selectFile('/workspace/README.md');
|
|
@@ -117,8 +154,8 @@ worker.onmessage = ({ data }) => {
|
|
| 117 |
if (data.action === 'load') $('#load').textContent = 'Loading…';
|
| 118 |
busy = true; $('#activity').textContent = data.action === 'prompt' ? 'Pi is working…' : data.action === 'load' ? (data.cachedOnly ? 'Loading local model…' : 'Loading model…') : 'Working…'; controls();
|
| 119 |
} else if (data.type === 'idle') {
|
| 120 |
-
busy = false; $('#progress').hidden = true; $('#activity').textContent = loaded ? 'Ready · running on your device' : 'Files and chat are saved on this device.'; controls();
|
| 121 |
-
} else if (data.type === 'workspace') { files = data.files; renderFiles(); followReadFile(); }
|
| 122 |
else if (data.type === 'device') { diagnostics.device = data.device; }
|
| 123 |
else if (data.type === 'load_progress') {
|
| 124 |
$('#progress').hidden = false;
|
|
@@ -246,8 +283,9 @@ $('#upload').onchange = guarded(async () => {
|
|
| 246 |
$('#upload').value = '';
|
| 247 |
});
|
| 248 |
$('#export').onclick = guarded(async () => {
|
| 249 |
-
const state =
|
| 250 |
-
const
|
|
|
|
| 251 |
});
|
| 252 |
// Browser verification exercises the same worker and real Pi / WebGPU paths as the UI.
|
| 253 |
window.browserPi = { ready, load, prompt, rpc, diagnostics, terminal: terminalUI, runtimeMeter, get loaded() { return loaded; }, get busy() { return busy; } };
|
|
|
|
| 7 |
import { createFileIcon } from './file-icons.mjs';
|
| 8 |
import { createEditorUI } from './editor-ui.mjs';
|
| 9 |
import { createColumnResizer } from './column-resizer.mjs';
|
| 10 |
+
import { createLocalFolderUI } from './local-folder-ui.mjs';
|
| 11 |
|
| 12 |
const $ = selector => document.querySelector(selector);
|
| 13 |
createColumnResizer($('main'), $('#column-resizer'));
|
|
|
|
| 15 |
const pending = new Map();
|
| 16 |
let nextId = 1, busy = false, loaded = false, selected, dirty = false, files = {}, messages = [], readyResolve;
|
| 17 |
let pendingReadPath;
|
| 18 |
+
let previewVersion = 0, previewReady = true;
|
| 19 |
const ready = new Promise(resolve => { readyResolve = resolve; });
|
| 20 |
const diagnostics = { events: [], generations: [], runtimeSamples: [], device: null, contextUsage: restoredContextUsage([]) };
|
| 21 |
const runtimeMeter = createRuntimeMeter($('#runtime-meter'));
|
|
|
|
| 33 |
if (busy) throw Error('Wait for Pi to finish or stop the current task first.');
|
| 34 |
return rpc('shell', { command });
|
| 35 |
} });
|
| 36 |
+
const localUI = createLocalFolderUI({ rpc, notice,
|
| 37 |
+
canSwitch() {
|
| 38 |
+
if (busy) throw Error('Wait for Pi to finish or stop the current task first.');
|
| 39 |
+
if (dirty) throw Error('Save or stage the open file first.');
|
| 40 |
+
},
|
| 41 |
+
onSwitch(state) {
|
| 42 |
+
pendingReadPath = undefined; selected = undefined; dirty = false; previewVersion++; previewReady = false;
|
| 43 |
+
files = state.files; messages = state.messages; localUI.setState(state.localFolder);
|
| 44 |
+
$('#filename').textContent = 'Select a file'; $('#filetype').replaceChildren(); editorUI.setFile('', '');
|
| 45 |
+
terminalUI.setMessages(messages, { reset: true }); updateContextUsage(restoredContextUsage(messages));
|
| 46 |
+
$('#shell-clear').click(); renderFiles();
|
| 47 |
+
const file = Object.hasOwn(files, '/workspace/README.md') ? '/workspace/README.md' : Object.keys(files).sort()[0];
|
| 48 |
+
if (file) selectFile(file);
|
| 49 |
+
controls();
|
| 50 |
+
},
|
| 51 |
+
});
|
| 52 |
function rpc(action, args) {
|
| 53 |
return new Promise((resolve, reject) => {
|
| 54 |
const id = nextId++; pending.set(id, { resolve, reject }); worker.postMessage({ id, action, args });
|
|
|
|
| 73 |
$('#stop').hidden = !busy;
|
| 74 |
for (const id of ['#new-chat', '#add-file', '#upload']) $(id).disabled = busy;
|
| 75 |
shellUI.setBusy(busy);
|
| 76 |
+
localUI.setBusy(busy);
|
| 77 |
$('#save').disabled = busy || !selected || !dirty;
|
| 78 |
+
$('#editor').disabled = busy || !selected || !previewReady;
|
| 79 |
}
|
| 80 |
function renderFiles() {
|
| 81 |
terminalUI.setFiles(files);
|
|
|
|
| 87 |
button.title = file; button.classList.toggle('selected', file === selected);
|
| 88 |
button.onclick = () => selectFile(file); $('#files').append(button);
|
| 89 |
}
|
| 90 |
+
if (selected && !Object.hasOwn(files, selected) && !dirty) {
|
| 91 |
+
selected = undefined; previewVersion++; previewReady = false;
|
| 92 |
+
$('#filename').textContent = 'Select a file'; $('#filetype').replaceChildren(); editorUI.setFile('', ''); controls();
|
| 93 |
+
}
|
| 94 |
+
if (selected && !dirty) {
|
| 95 |
+
if (files[selected] === null) loadPreview(selected);
|
| 96 |
+
else { previewReady = true; editorUI.setFile(selected, files[selected] ?? ''); }
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
async function loadPreview(file) {
|
| 100 |
+
const version = ++previewVersion;
|
| 101 |
+
previewReady = false; editorUI.setFile(file, ''); controls();
|
| 102 |
+
try {
|
| 103 |
+
const content = await rpc('preview', { path: file });
|
| 104 |
+
if (version !== previewVersion || file !== selected || dirty) return;
|
| 105 |
+
editorUI.setFile(file, content); previewReady = true;
|
| 106 |
+
} catch (error) { if (version === previewVersion) notice(error.message); }
|
| 107 |
+
finally { if (version === previewVersion) controls(); }
|
| 108 |
}
|
| 109 |
function selectFile(file) {
|
| 110 |
pendingReadPath = undefined;
|
|
|
|
| 143 |
const request = pending.get(data.id); pending.delete(data.id);
|
| 144 |
if (data.error) request?.reject(Object.assign(Error(data.error), { name: data.errorName || 'Error', code: data.errorCode })); else request?.resolve(data.value);
|
| 145 |
} else if (data.type === 'initialized') {
|
| 146 |
+
localUI.restore(data.savedLocal); localUI.setState(data.localFolder);
|
| 147 |
files = data.files; messages = data.messages; renderFiles(); renderMessages();
|
| 148 |
updateContextUsage(restoredContextUsage(messages));
|
| 149 |
if (files['/workspace/README.md']) selectFile('/workspace/README.md');
|
|
|
|
| 154 |
if (data.action === 'load') $('#load').textContent = 'Loading…';
|
| 155 |
busy = true; $('#activity').textContent = data.action === 'prompt' ? 'Pi is working…' : data.action === 'load' ? (data.cachedOnly ? 'Loading local model…' : 'Loading model…') : 'Working…'; controls();
|
| 156 |
} else if (data.type === 'idle') {
|
| 157 |
+
busy = false; $('#progress').hidden = true; $('#activity').textContent = localUI.active ? 'Local folder · review & apply to save changes to disk' : loaded ? 'Ready · running on your device' : 'Files and chat are saved on this device.'; controls();
|
| 158 |
+
} else if (data.type === 'workspace') { files = data.files; localUI.setState(data.localFolder); renderFiles(); followReadFile(); }
|
| 159 |
else if (data.type === 'device') { diagnostics.device = data.device; }
|
| 160 |
else if (data.type === 'load_progress') {
|
| 161 |
$('#progress').hidden = false;
|
|
|
|
| 283 |
$('#upload').value = '';
|
| 284 |
});
|
| 285 |
$('#export').onclick = guarded(async () => {
|
| 286 |
+
const state = localUI.active ? { version: 1, patches: await rpc('local_changes') } : await rpc('snapshot');
|
| 287 |
+
const url = URL.createObjectURL(new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }));
|
| 288 |
+
const a = document.createElement('a'); a.href = url; a.download = localUI.active ? 'pi-staged-changes.json' : 'pi-workspace.json'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
|
| 289 |
});
|
| 290 |
// Browser verification exercises the same worker and real Pi / WebGPU paths as the UI.
|
| 291 |
window.browserPi = { ready, load, prompt, rpc, diagnostics, terminal: terminalUI, runtimeMeter, get loaded() { return loaded; }, get busy() { return busy; } };
|
app/src/storage.mjs
CHANGED
|
@@ -5,15 +5,15 @@ export async function openStorage() {
|
|
| 5 |
request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
|
| 6 |
});
|
| 7 |
return {
|
| 8 |
-
async load() {
|
| 9 |
return new Promise((resolve, reject) => {
|
| 10 |
-
const request = db.transaction('state').objectStore('state').get(
|
| 11 |
request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
|
| 12 |
});
|
| 13 |
},
|
| 14 |
-
async save(value) {
|
| 15 |
return new Promise((resolve, reject) => {
|
| 16 |
-
const tx = db.transaction('state', 'readwrite'); tx.objectStore('state').put(value,
|
| 17 |
tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error);
|
| 18 |
});
|
| 19 |
},
|
|
|
|
| 5 |
request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
|
| 6 |
});
|
| 7 |
return {
|
| 8 |
+
async load(key = 'current') {
|
| 9 |
return new Promise((resolve, reject) => {
|
| 10 |
+
const request = db.transaction('state').objectStore('state').get(key);
|
| 11 |
request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
|
| 12 |
});
|
| 13 |
},
|
| 14 |
+
async save(value, key = 'current') {
|
| 15 |
return new Promise((resolve, reject) => {
|
| 16 |
+
const tx = db.transaction('state', 'readwrite'); tx.objectStore('state').put(value, key);
|
| 17 |
tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error);
|
| 18 |
});
|
| 19 |
},
|
app/src/style.css
CHANGED
|
@@ -176,6 +176,29 @@ footer { display: flex; align-items: center; justify-content: space-between; gap
|
|
| 176 |
.workspace-heading { display: flex; align-items: center; justify-content: space-between; min-height: 40px; padding: 5px 10px; }
|
| 177 |
.workspace-heading h2 { margin: 0; font-size: 12px; font-weight: 600; }
|
| 178 |
.workspace-heading > div { display: flex; gap: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
.button { display: inline-block; }
|
| 180 |
.file-area { display: grid; grid-template-columns: 140px minmax(0, 1fr); flex: 1; min-height: 180px; }
|
| 181 |
#files { overflow: auto; padding: 5px 4px; }
|
|
|
|
| 176 |
.workspace-heading { display: flex; align-items: center; justify-content: space-between; min-height: 40px; padding: 5px 10px; }
|
| 177 |
.workspace-heading h2 { margin: 0; font-size: 12px; font-weight: 600; }
|
| 178 |
.workspace-heading > div { display: flex; gap: 0; }
|
| 179 |
+
.workspace-heading { flex-wrap: wrap; }
|
| 180 |
+
.folder-controls { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 10px 7px; }
|
| 181 |
+
.folder-controls:has(> button:not([hidden])) { min-height: 30px; }
|
| 182 |
+
.folder-controls:not(:has(> button:not([hidden]))) { display: none; }
|
| 183 |
+
#open-folder { color: var(--accent, #8abeb7); background: #8abeb710; }
|
| 184 |
+
#local-folder-bar { margin: 0 8px 8px; padding: 10px; border-radius: 10px; background: #8abeb710; font-size: 11px; }
|
| 185 |
+
#local-folder-name { display: block; color: var(--accent, #8abeb7); overflow-wrap: anywhere; }
|
| 186 |
+
#local-folder-hint { display: block; margin: 4px 0 8px; color: var(--muted); line-height: 1.5; }
|
| 187 |
+
.folder-actions { display: flex; gap: 5px; flex-wrap: wrap; }
|
| 188 |
+
.folder-actions button { font-size: 11px; padding: 6px 9px; }
|
| 189 |
+
#local-review { box-sizing: border-box; width: min(820px, calc(100vw - 24px)); max-height: calc(100dvh - 32px); padding: 20px; border: 1px solid #8abeb740; border-radius: 18px; background: var(--bg, #18181e); color: var(--text); }
|
| 190 |
+
#local-review::backdrop { background: #0009; backdrop-filter: blur(4px); }
|
| 191 |
+
.review-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
| 192 |
+
.review-heading h2 { margin: 0; font-size: 20px; }
|
| 193 |
+
#review-close { font-size: 24px; padding: 2px 10px; }
|
| 194 |
+
#review-description { color: var(--muted); font-size: 13px; line-height: 1.5; }
|
| 195 |
+
#review-files { max-height: 55dvh; overflow: auto; }
|
| 196 |
+
#review-files details { border: 1px solid #ffffff18; border-radius: 8px; margin: 8px 0; overflow: hidden; }
|
| 197 |
+
#review-files summary { padding: 10px; cursor: pointer; font: 12px ui-monospace, monospace; overflow-wrap: anywhere; }
|
| 198 |
+
#review-files pre { overflow: auto; margin: 0; padding: 10px; background: #0003; font: 12px/1.55 ui-monospace, monospace; tab-size: 2; }
|
| 199 |
+
.diff-add { color: #98dfb4; } .diff-remove { color: #f2a6a6; }
|
| 200 |
+
#review-error { color: #f2a6a6; font-size: 13px; line-height: 1.5; }
|
| 201 |
+
.review-actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
|
| 202 |
.button { display: inline-block; }
|
| 203 |
.file-area { display: grid; grid-template-columns: 140px minmax(0, 1fr); flex: 1; min-height: 180px; }
|
| 204 |
#files { overflow: auto; padding: 5px 4px; }
|
app/src/workspace.mjs
CHANGED
|
@@ -31,10 +31,10 @@ export const initialFiles = {
|
|
| 31 |
const limitText = text => text.length > 6000 ? text.slice(0, 6000) + '\n[Output truncated at 6,000 characters; use head, grep, or a narrower query.]' : text;
|
| 32 |
const result = (text, details = {}) => ({ content: [{ type: 'text', text: limitText(text) }], details });
|
| 33 |
|
| 34 |
-
export function createWorkspace(files = initialFiles, entries) {
|
| 35 |
if (entries) files = Object.fromEntries(entries.filter(([, entry]) => entry.type === 'file').map(([path, entry]) => [path,
|
| 36 |
{ mode: entry.mode, content: Uint8Array.from(atob(entry.base64), c => c.charCodeAt(0)) }]));
|
| 37 |
-
const bash = new Bash({ files, cwd: '/workspace', executionLimitProfile: 'hardened', fetch: createBrowserFetch(),
|
| 38 |
executionLimits: { maxExecutionTimeMs: 10_000, maxCommandCount: 3000,
|
| 39 |
maxFileSystemBytes: 16 * 1024 ** 2, maxOutputSize: 256 * 1024, maxSourceBytes: 128 * 1024 },
|
| 40 |
});
|
|
@@ -80,6 +80,7 @@ export function createWorkspace(files = initialFiles, entries) {
|
|
| 80 |
},
|
| 81 |
}];
|
| 82 |
async function snapshot() {
|
|
|
|
| 83 |
const files = {};
|
| 84 |
for (const file of bash.fs.getAllPaths().sort()) {
|
| 85 |
// Persist real files across the virtual FS, excluding built-in executable stubs.
|
|
@@ -90,6 +91,7 @@ export function createWorkspace(files = initialFiles, entries) {
|
|
| 90 |
return files;
|
| 91 |
}
|
| 92 |
async function serialize() {
|
|
|
|
| 93 |
const entries = [];
|
| 94 |
for (const file of bash.fs.getAllPaths().sort()) {
|
| 95 |
if (['/bin', '/usr/bin', '/dev', '/proc'].some(root => file === root || file.startsWith(root + '/'))) continue;
|
|
@@ -104,5 +106,5 @@ export function createWorkspace(files = initialFiles, entries) {
|
|
| 104 |
}
|
| 105 |
return entries;
|
| 106 |
}
|
| 107 |
-
return { bash, tools, ready, write, snapshot, serialize, read: file => bash.fs.readFile(path(file)) };
|
| 108 |
}
|
|
|
|
| 31 |
const limitText = text => text.length > 6000 ? text.slice(0, 6000) + '\n[Output truncated at 6,000 characters; use head, grep, or a narrower query.]' : text;
|
| 32 |
const result = (text, details = {}) => ({ content: [{ type: 'text', text: limitText(text) }], details });
|
| 33 |
|
| 34 |
+
export function createWorkspace(files = initialFiles, entries, localFolder) {
|
| 35 |
if (entries) files = Object.fromEntries(entries.filter(([, entry]) => entry.type === 'file').map(([path, entry]) => [path,
|
| 36 |
{ mode: entry.mode, content: Uint8Array.from(atob(entry.base64), c => c.charCodeAt(0)) }]));
|
| 37 |
+
const bash = new Bash({ ...(localFolder ? { fs: localFolder.mount() } : { files }), cwd: '/workspace', executionLimitProfile: 'hardened', fetch: createBrowserFetch(),
|
| 38 |
executionLimits: { maxExecutionTimeMs: 10_000, maxCommandCount: 3000,
|
| 39 |
maxFileSystemBytes: 16 * 1024 ** 2, maxOutputSize: 256 * 1024, maxSourceBytes: 128 * 1024 },
|
| 40 |
});
|
|
|
|
| 80 |
},
|
| 81 |
}];
|
| 82 |
async function snapshot() {
|
| 83 |
+
if (localFolder) return localFolder.listing();
|
| 84 |
const files = {};
|
| 85 |
for (const file of bash.fs.getAllPaths().sort()) {
|
| 86 |
// Persist real files across the virtual FS, excluding built-in executable stubs.
|
|
|
|
| 91 |
return files;
|
| 92 |
}
|
| 93 |
async function serialize() {
|
| 94 |
+
if (localFolder) return null; // Local file bytes are never included in demo snapshots.
|
| 95 |
const entries = [];
|
| 96 |
for (const file of bash.fs.getAllPaths().sort()) {
|
| 97 |
if (['/bin', '/usr/bin', '/dev', '/proc'].some(root => file === root || file.startsWith(root + '/'))) continue;
|
|
|
|
| 106 |
}
|
| 107 |
return entries;
|
| 108 |
}
|
| 109 |
+
return { bash, tools, ready, write, snapshot, serialize, localFolder, read: file => bash.fs.readFile(path(file)) };
|
| 110 |
}
|
app/tests/local-folder-browser.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { chromium, webkit } from 'playwright';
|
| 2 |
+
import assert from 'node:assert/strict';
|
| 3 |
+
import fs from 'node:fs/promises';
|
| 4 |
+
const url = process.argv[2] || 'http://127.0.0.1:4193/';
|
| 5 |
+
const label = process.argv[3] || 'local-folder-browser';
|
| 6 |
+
const result = { url, date: new Date().toISOString(), checks: [], errors: [], picker: 'Test supplies a real OPFS directory handle; native OS picker covered separately.' };
|
| 7 |
+
const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true });
|
| 8 |
+
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
| 9 |
+
const page = await context.newPage();
|
| 10 |
+
page.on('pageerror', e => result.errors.push(e.message));
|
| 11 |
+
const idle = () => page.waitForFunction(() => window.browserPi && !window.browserPi.busy);
|
| 12 |
+
const rpc = (action, args) => page.evaluate(({ action, args }) => window.browserPi.rpc(action, args), { action, args });
|
| 13 |
+
const disk = name => page.evaluate(async name => (await (await window.testFolder.getFileHandle(name)).getFile()).text(), name);
|
| 14 |
+
async function open() { await page.goto(url); await page.waitForFunction(() => window.browserPi); await page.evaluate(() => window.browserPi.ready); await idle(); }
|
| 15 |
+
try {
|
| 16 |
+
await open();
|
| 17 |
+
const demo = await rpc('snapshot');
|
| 18 |
+
await page.evaluate(async () => {
|
| 19 |
+
const root = await navigator.storage.getDirectory();
|
| 20 |
+
window.testFolder = await root.getDirectoryHandle('project-fixture', { create: true });
|
| 21 |
+
const write = async (name, text) => { const s = await (await window.testFolder.getFileHandle(name, { create: true })).createWritable(); await s.write(text); await s.close(); };
|
| 22 |
+
await write('README.md', '# Local fixture\n'); await write('count.txt', '1\n');
|
| 23 |
+
const modules = await window.testFolder.getDirectoryHandle('node_modules', { create: true });
|
| 24 |
+
const ignored = await (await modules.getFileHandle('ignored.js', { create: true })).createWritable(); await ignored.write('ignore'); await ignored.close();
|
| 25 |
+
window.pickerCalls = 0;
|
| 26 |
+
window.showDirectoryPicker = async () => { window.pickerCalls++; window.pickerGesture = navigator.userActivation.isActive; return window.testFolder; };
|
| 27 |
+
});
|
| 28 |
+
await page.locator('#open-folder').click();
|
| 29 |
+
await page.waitForFunction(() => document.querySelector('#editor').value === '# Local fixture\n'); await idle();
|
| 30 |
+
assert.equal(await page.evaluate(() => window.pickerGesture), true);
|
| 31 |
+
assert.equal((await rpc('snapshot')).entries, null);
|
| 32 |
+
assert.equal(await page.locator('#files').innerText(), 'README.md\ncount.txt');
|
| 33 |
+
result.checks.push('Real directory handle crosses worker boundary; picker called with user activation; existing editor/tree reused; excluded directories omitted');
|
| 34 |
+
await page.locator('#editor').fill('# Edited local fixture\n'); await page.locator('#save').click(); await idle();
|
| 35 |
+
assert.equal(await disk('README.md'), '# Local fixture\n');
|
| 36 |
+
await page.locator('#review-changes').click();
|
| 37 |
+
await page.locator('#local-review').waitFor({ state: 'visible' });
|
| 38 |
+
assert.match(await page.locator('#review-files').innerText(), /\+# Edited local fixture/);
|
| 39 |
+
await page.screenshot({ path: `../validation/${label}-review.png` });
|
| 40 |
+
await page.locator('#review-apply').click(); await page.locator('#local-review').waitFor({ state: 'hidden' }); await idle();
|
| 41 |
+
assert.equal(await disk('README.md'), '# Edited local fixture\n');
|
| 42 |
+
result.checks.push('Editor changes are staged; readable diff reviewed; Apply writes through the browser API');
|
| 43 |
+
const shell = await rpc('shell', { command: "printf 'new file\\n' > added.txt; printf '2\\n' > count.txt" });
|
| 44 |
+
assert.equal(shell.exitCode, 0, shell.stderr); await idle();
|
| 45 |
+
assert.equal(await disk('count.txt'), '1\n');
|
| 46 |
+
await page.locator('#disconnect-folder').click(); await page.locator('#open-folder').waitFor({ state: 'visible' }); await idle();
|
| 47 |
+
assert.deepEqual((await rpc('snapshot')).files, demo.files);
|
| 48 |
+
assert.deepEqual((await rpc('snapshot')).messages, demo.messages);
|
| 49 |
+
await page.reload(); await page.waitForFunction(() => window.browserPi); await page.evaluate(() => window.browserPi.ready);
|
| 50 |
+
await page.locator('#reconnect-folder').click(); await page.locator('#local-folder-bar').waitFor({ state: 'visible' }); await idle();
|
| 51 |
+
const staged = await rpc('local_changes'); assert.equal(staged.length, 2);
|
| 52 |
+
await page.evaluate(async () => { window.testFolder = await (await navigator.storage.getDirectory()).getDirectoryHandle('project-fixture'); });
|
| 53 |
+
assert.equal(await rpc('read', { path: '/workspace/added.txt' }), 'new file\n'); await idle();
|
| 54 |
+
result.checks.push('Back to demo restores original files/chat; reload + reconnect restores handle, staged edits and local chat from separate IndexedDB record');
|
| 55 |
+
await page.evaluate(async () => { const s = await (await window.testFolder.getFileHandle('count.txt')).createWritable(); await s.write('9\n'); await s.close(); });
|
| 56 |
+
await page.locator('#review-changes').click(); await page.locator('#review-apply').click();
|
| 57 |
+
await page.waitForFunction(() => document.querySelector('#review-error').textContent.includes('Conflict'));
|
| 58 |
+
assert.equal(await disk('count.txt'), '9\n');
|
| 59 |
+
assert.equal(await page.evaluate(async () => { try { await window.testFolder.getFileHandle('added.txt'); return true; } catch { return false; } }), false);
|
| 60 |
+
page.once('dialog', d => d.accept()); await page.locator('#review-discard').click();
|
| 61 |
+
await page.locator('#local-review').waitFor({ state: 'hidden' }); await idle();
|
| 62 |
+
assert.equal((await rpc('local_changes')).length, 0);
|
| 63 |
+
result.checks.push('External edit blocks the whole apply before any disk mutation; Discard leaves external contents intact');
|
| 64 |
+
for (const width of [1440, 390, 320]) {
|
| 65 |
+
await page.setViewportSize({ width, height: width === 1440 ? 1000 : 844 });
|
| 66 |
+
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
| 67 |
+
await page.locator('#local-folder-bar').scrollIntoViewIfNeeded();
|
| 68 |
+
await page.screenshot({ path: `../validation/${label}-${width}.png`, fullPage: true });
|
| 69 |
+
}
|
| 70 |
+
result.checks.push('Folder controls and editor fit 1440, 390 and 320px layouts');
|
| 71 |
+
await page.locator('#disconnect-folder').click(); await idle();
|
| 72 |
+
await page.evaluate(() => { window.showDirectoryPicker = async () => { throw new DOMException('Cancelled', 'AbortError'); }; });
|
| 73 |
+
await page.locator('#open-folder').click(); await idle();
|
| 74 |
+
assert.deepEqual((await rpc('snapshot')).files, demo.files);
|
| 75 |
+
result.checks.push('Cancelling picker preserves the demo workspace');
|
| 76 |
+
// True cross-origin embedding, not a replaced window.top property.
|
| 77 |
+
const embed = await context.newPage();
|
| 78 |
+
await context.grantPermissions(['local-network-access']);
|
| 79 |
+
const parent = url.startsWith('http:') ? 'http://localhost:4193/embed-fixture' : 'https://embed.example/';
|
| 80 |
+
await embed.route(parent, route => route.fulfill({ contentType: 'text/html', body: `<iframe src="${url}" style="width:100%;height:900px"></iframe>` }));
|
| 81 |
+
await embed.goto(parent);
|
| 82 |
+
const frame = embed.frameLocator('iframe'); await frame.locator('#open-folder').click();
|
| 83 |
+
assert.match(await frame.locator('#notice').innerText(), /new tab/); await frame.locator('#open-tab').waitFor({ state: 'visible' });
|
| 84 |
+
await embed.close(); result.checks.push('Cross-origin iframe offers direct app tab instead of a blocked native picker');
|
| 85 |
+
assert.deepEqual(result.errors, []);
|
| 86 |
+
result.passed = true;
|
| 87 |
+
} catch (e) { result.failure = e.stack; await page.screenshot({ path: `../validation/${label}-failure.png`, fullPage: true }); throw e; }
|
| 88 |
+
finally { await fs.writeFile(`../validation/${label}.json`, JSON.stringify(result, null, 2)); await browser.close(); console.log(JSON.stringify(result, null, 2)); }
|
app/tests/local-folder.test.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import test from 'node:test';
|
| 2 |
+
import assert from 'node:assert/strict';
|
| 3 |
+
import fs from 'node:fs/promises';
|
| 4 |
+
import os from 'node:os';
|
| 5 |
+
import path from 'node:path';
|
| 6 |
+
import { LocalFolderFs, displayText } from '../src/local-folder.mjs';
|
| 7 |
+
import { createWorkspace } from '../src/workspace.mjs';
|
| 8 |
+
|
| 9 |
+
// OS-backed handle fixture for adapter semantics. Actual browser handle cloning,
|
| 10 |
+
// permissions, picker UX, and IndexedDB are covered by the browser suite.
|
| 11 |
+
async function fixture(t, initial = { 'README.md': '# Local project\n', 'src/app.js': 'export const answer = 1;\n' }) {
|
| 12 |
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pi-folder-test-'));
|
| 13 |
+
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
| 14 |
+
for (const [name, data] of Object.entries(initial)) { await fs.mkdir(path.dirname(path.join(root, name)), { recursive: true }); await fs.writeFile(path.join(root, name), data); }
|
| 15 |
+
let reads = 0, failWrite = false;
|
| 16 |
+
const handle = (p, kind) => ({ kind, name: path.basename(p),
|
| 17 |
+
async *entries() { for (const item of await fs.readdir(p, { withFileTypes: true })) yield [item.name, handle(path.join(p, item.name), item.isDirectory() ? 'directory' : 'file')]; },
|
| 18 |
+
async getFile() { const stat = await fs.stat(p); return { size: stat.size, lastModified: stat.mtimeMs, async arrayBuffer() { reads++; return new Uint8Array(await fs.readFile(p)).buffer; } }; },
|
| 19 |
+
fixturePath: p,
|
| 20 |
+
async isSameEntry(other) { return other.fixturePath === p; },
|
| 21 |
+
async createWritable() {
|
| 22 |
+
if (failWrite) throw new DOMException('Write denied', 'NotAllowedError');
|
| 23 |
+
let bytes;
|
| 24 |
+
return { async write(data) { bytes = data; }, async close() { await fs.writeFile(p, bytes); }, async abort() {} };
|
| 25 |
+
},
|
| 26 |
+
async removeEntry(name) { await fs.rm(path.join(p, name)); },
|
| 27 |
+
});
|
| 28 |
+
// Install traversal separately so each handle closes over its own path.
|
| 29 |
+
function make(p, kind) {
|
| 30 |
+
const h = handle(p, kind);
|
| 31 |
+
const child = async (name, wanted, create) => {
|
| 32 |
+
assert(!name.includes('/') && name !== '..'); const target = path.join(p, name);
|
| 33 |
+
let stat;
|
| 34 |
+
try { stat = await fs.stat(target); }
|
| 35 |
+
catch (e) { if (e.code !== 'ENOENT') throw e; if (!create) throw new DOMException('Missing', 'NotFoundError'); if (wanted === 'directory') await fs.mkdir(target); else await fs.writeFile(target, ''); stat = await fs.stat(target); }
|
| 36 |
+
if (stat.isDirectory() !== (wanted === 'directory')) throw new DOMException('Wrong kind', 'TypeMismatchError');
|
| 37 |
+
return make(target, wanted);
|
| 38 |
+
};
|
| 39 |
+
h.getDirectoryHandle = (name, { create = false } = {}) => child(name, 'directory', create);
|
| 40 |
+
h.getFileHandle = (name, { create = false } = {}) => child(name, 'file', create);
|
| 41 |
+
h.entries = async function* () { for (const item of await fs.readdir(p, { withFileTypes: true })) yield [item.name, make(path.join(p, item.name), item.isDirectory() ? 'directory' : 'file')]; };
|
| 42 |
+
return h;
|
| 43 |
+
}
|
| 44 |
+
const directory = make(root, 'directory');
|
| 45 |
+
return { root, directory, get reads() { return reads; }, denyWrites() { failWrite = true; }, read: name => fs.readFile(path.join(root, name), 'utf8'), write: (name, text) => fs.writeFile(path.join(root, name), text) };
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
test('Local folder: indexing/listing/stat do not read file bytes; workspace snapshots stay metadata-only', async t => {
|
| 49 |
+
const disk = await fixture(t, { 'a.txt': 'hello', 'large.bin': Buffer.alloc(2 * 1024 ** 2), 'node_modules/ignore.js': 'ignored' });
|
| 50 |
+
const local = await LocalFolderFs.open(disk.directory), workspace = createWorkspace({}, undefined, local);
|
| 51 |
+
await workspace.ready;
|
| 52 |
+
assert.equal(disk.reads, 0);
|
| 53 |
+
assert.deepEqual(Object.keys(await workspace.snapshot()), ['/workspace/a.txt', '/workspace/large.bin']);
|
| 54 |
+
assert.equal(await workspace.serialize(), null);
|
| 55 |
+
const listing = await workspace.bash.exec('ls -l /workspace'); assert.equal(listing.exitCode, 0, listing.stderr);
|
| 56 |
+
assert.equal(disk.reads, 0); assert.equal(local.skipped, 1);
|
| 57 |
+
assert.equal(await workspace.read('a.txt'), 'hello'); assert.equal(disk.reads, 1);
|
| 58 |
+
await assert.rejects(workspace.read('large.bin'), /1 MiB/);
|
| 59 |
+
});
|
| 60 |
+
test('Local folder: real Bash and agent tools stage edits, mkdir, copy, rename, delete; Apply writes only staged paths', async t => {
|
| 61 |
+
const disk = await fixture(t);
|
| 62 |
+
const local = await LocalFolderFs.open(disk.directory), workspace = createWorkspace({}, undefined, local);
|
| 63 |
+
await workspace.ready;
|
| 64 |
+
await workspace.tools.find(t => t.name === 'edit').execute('', { path: 'src/app.js', oldText: '= 1', newText: '= 2' });
|
| 65 |
+
const result = await workspace.bash.exec("mkdir -p reports/nested; printf 'ok\\n' > reports/nested/result.txt; cp README.md copy.md; mv copy.md moved.md; rm README.md");
|
| 66 |
+
assert.equal(result.exitCode, 0, result.stderr);
|
| 67 |
+
assert.equal(await disk.read('src/app.js'), 'export const answer = 1;\n');
|
| 68 |
+
assert.equal(await disk.read('README.md'), '# Local project\n');
|
| 69 |
+
const patches = await local.patches(); assert.equal(patches.length, 6);
|
| 70 |
+
assert.equal(await local.apply(), 6);
|
| 71 |
+
assert.equal(await disk.read('src/app.js'), 'export const answer = 2;\n');
|
| 72 |
+
assert.equal(await disk.read('moved.md'), '# Local project\n');
|
| 73 |
+
assert.equal(await disk.read('reports/nested/result.txt'), 'ok\n');
|
| 74 |
+
await assert.rejects(disk.read('README.md'), { code: 'ENOENT' });
|
| 75 |
+
assert.equal((await local.patches()).length, 0);
|
| 76 |
+
});
|
| 77 |
+
test('Local folder: conflicts preflight all writes, including same-size edits, creations and deletions', async t => {
|
| 78 |
+
const disk = await fixture(t);
|
| 79 |
+
const local = await LocalFolderFs.open(disk.directory);
|
| 80 |
+
await local.writeFile('/README.md', 'changed'); await local.writeFile('/src/app.js', 'staged');
|
| 81 |
+
await disk.write('src/app.js', 'export const answer = 9;\n');
|
| 82 |
+
await assert.rejects(local.apply(), /Conflict/);
|
| 83 |
+
assert.equal(await disk.read('README.md'), '# Local project\n');
|
| 84 |
+
assert.equal(await disk.read('src/app.js'), 'export const answer = 9;\n');
|
| 85 |
+
const fresh = await LocalFolderFs.open(disk.directory);
|
| 86 |
+
await fresh.writeFile('/new.txt', 'staged'); await disk.write('new.txt', 'external');
|
| 87 |
+
await assert.rejects(fresh.apply(), /Conflict/); assert.equal(await disk.read('new.txt'), 'external');
|
| 88 |
+
const deleted = await LocalFolderFs.open(disk.directory);
|
| 89 |
+
await deleted.rm('/README.md'); await disk.write('README.md', 'other editor');
|
| 90 |
+
await assert.rejects(deleted.apply(), /Conflict/); assert.equal(await disk.read('README.md'), 'other editor');
|
| 91 |
+
});
|
| 92 |
+
test('Local folder: staged patches restore with the original baseline, not a newly changed disk file', async t => {
|
| 93 |
+
const disk = await fixture(t), local = await LocalFolderFs.open(disk.directory);
|
| 94 |
+
await local.writeFile('/src/app.js', 'staged'); await local.mkdir('/new'); await local.writeFile('/new/file', 'new');
|
| 95 |
+
const patches = await local.patches();
|
| 96 |
+
await disk.write('src/app.js', 'external');
|
| 97 |
+
const restored = await LocalFolderFs.open(disk.directory, { patches });
|
| 98 |
+
assert.equal(await restored.readFile('/src/app.js'), 'staged');
|
| 99 |
+
assert.equal(await restored.readFile('/new/file'), 'new');
|
| 100 |
+
await assert.rejects(restored.apply(), /Conflict/);
|
| 101 |
+
});
|
| 102 |
+
test('Local folder: failed or cancelled application retains pending edits', async t => {
|
| 103 |
+
const disk = await fixture(t), local = await LocalFolderFs.open(disk.directory);
|
| 104 |
+
await local.writeFile('/README.md', 'staged');
|
| 105 |
+
await assert.rejects(local.apply(AbortSignal.abort()), { name: 'AbortError' });
|
| 106 |
+
disk.denyWrites(); await assert.rejects(local.apply(), /0 of 1 changes applied/);
|
| 107 |
+
assert.equal((await local.patches()).length, 1);
|
| 108 |
+
assert.equal(await disk.read('README.md'), '# Local project\n');
|
| 109 |
+
const newFile = await LocalFolderFs.open(disk.directory);
|
| 110 |
+
await newFile.writeFile('/new.txt', 'staged');
|
| 111 |
+
await assert.rejects(newFile.apply(), /0 of 1/);
|
| 112 |
+
await assert.rejects(disk.read('new.txt'), { code: 'ENOENT' });
|
| 113 |
+
assert.equal((await newFile.patches()).length, 1);
|
| 114 |
+
});
|
| 115 |
+
test('Local folder: unsupported operations, excludes, binary editor and traversal do not touch disk', async t => {
|
| 116 |
+
const disk = await fixture(t), local = await LocalFolderFs.open(disk.directory), workspace = createWorkspace({}, undefined, local);
|
| 117 |
+
await workspace.ready;
|
| 118 |
+
for (const method of ['chmod', 'symlink', 'link', 'utimes']) assert.throws(() => local[method]('/README.md', 0o777), /ENOTSUP/);
|
| 119 |
+
await assert.rejects(local.rm('/src', { recursive: true }), /not supported/);
|
| 120 |
+
await assert.rejects(local.writeFile('/.git/config', 'oops'), /excluded/);
|
| 121 |
+
assert.throws(() => displayText(new Uint8Array([0, 1])), /binary/);
|
| 122 |
+
await workspace.write('../outside.txt', 'virtual only');
|
| 123 |
+
assert.deepEqual(await local.patches(), []);
|
| 124 |
+
await assert.rejects(disk.read('outside.txt'), { code: 'ENOENT' });
|
| 125 |
+
});
|
assets/agent.worker-B7d5sboN.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/agent.worker-DAy1yjYb.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/agent.worker-DONV7yi_.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/{agent.worker-CS8jL-AS.js → agent.worker-Dng_UV-X.js}
RENAMED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/agent.worker-euGpP_Md.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/index-B--sYzmA.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/index-BzGjruQ9.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/index-CLisF1Ml.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 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;--surface-inset:8px;--radius-small:8px;--radius:calc(var(--radius-small) + var(--surface-inset));--radius-large:calc(var(--radius) + var(--surface-inset));--content-width:1800px;--content-gutter:14px;--header-inset:12px;--header-control-height:32px;--header-height:calc(var(--header-control-height) + 2 * var(--surface-inset));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-radius:var(--radius-small);background:var(--control);min-height:28px;color:inherit;white-space:nowrap;border:1px solid #0000;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);color:var(--text)}.primary:hover{background:var(--selected)}#send.has-input:not(:disabled){color:#18181e;background:#fff}#send.has-input:not(:disabled):hover{background:#e8e8ed}.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{width:calc(100% - 2 * var(--content-gutter));max-width:calc(var(--content-width) - 2 * var(--content-gutter));height:var(--header-height);margin:var(--header-inset) auto 0;padding:var(--surface-inset);justify-content:space-between;align-items:center;gap:var(--surface-inset) 16px;background:var(--surface);border-radius:var(--radius);grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);display:grid;position:relative;overflow:hidden}.brand{color:inherit;align-items:center;gap:8px;font-size:15px;font-weight:600;text-decoration:none;display:flex}.pi-logo{flex:none;width:18px;height:18px;display:block}.brand{justify-self:start;padding:0 4px}.runtime-meter{--runtime-padding:10px;width:max-content;max-width:100%;height:var(--header-control-height);padding:5px var(--runtime-padding);border-radius:var(--radius-small);background:var(--control);white-space:nowrap;justify-self:center;align-items:center;font-size:11px;transition:width .22s cubic-bezier(.2,.8,.2,1);display:flex;overflow:clip}.runtime-content{flex:none;align-items:center;gap:8px;width:max-content;height:100%;display:flex}.runtime-location{flex:none;align-items:center;gap:6px;display:flex}.runtime-dot{background:var(--muted);border-radius:50%;width:5px;height:5px}.runtime-meter[data-state=generating] .runtime-dot,.runtime-meter[data-state=tool] .runtime-dot{background:var(--accent)}.runtime-meter[data-state=error] .runtime-dot{background:#d98c8c}.runtime-divider{background:var(--line);flex:0 0 1px;align-self:stretch;margin:2px 0}.runtime-sparkline{color:var(--accent);flex:none}.runtime-value{color:var(--muted);font-variant-numeric:tabular-nums;flex:none}.runtime-meter[data-state=generating] .runtime-value{color:var(--accent)}@media (prefers-reduced-motion:reduce){.runtime-meter{transition:none}}main{--conversation-min:420px;--workspace-min:370px;grid-template-columns:minmax(var(--conversation-min), var(--conversation-width,1.20482fr)) 0 minmax(var(--workspace-min), 1fr);height:calc(100dvh - var(--header-height) - var(--header-inset));max-width:var(--content-width);margin:auto;display:grid}.column-resizer{left:calc(-1 * var(--content-gutter));z-index:2;width:var(--content-gutter);cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;outline:none;justify-content:center;align-items:center;margin:12px 0 8px;display:flex;position:relative}.column-resizer:after{content:"";background:var(--line-strong);border-radius:999px;flex:none;width:3px;height:44px;transition:background-color .12s}.column-resizer:hover:after{background:var(--text)}.column-resizer:focus-visible:after{background:var(--accent);outline:2px solid var(--accent);outline-offset:4px}.is-resizing-columns,.is-resizing-columns *{cursor:col-resize!important;-webkit-user-select:none!important;user-select:none!important}.is-resizing-columns .column-resizer:after{background:var(--accent)}@media (prefers-reduced-motion:reduce){.column-resizer:after{transition:none}}@media (forced-colors:active){.column-resizer:after{background:buttontext}}.conversation{min-width:0;min-height:0;padding:12px var(--content-gutter) 8px;flex-direction:column;display:flex}.model-panel{background:var(--surface);margin-left:auto}.model-heading{justify-content:space-between;align-items:center;gap:8px;display:flex}.model-heading>div{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.model-heading strong{font-size:12px;font-weight:600}.header-model-source{color:inherit;white-space:nowrap;border-radius:3px;flex:none;align-items:center;gap:6px;text-decoration:none;display:inline-flex}.header-model-source img{border-radius:50%;flex:none;width:16px;height:16px;display:block}.header-model-source:hover{color:var(--accent)}.header-model-source:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.badge{border-radius:var(--radius-small);background:var(--control);color:var(--muted);padding:3px 7px;font-size:10px}#load{min-width:90px;height:var(--header-control-height);flex-shrink:0}#load.is-loaded{opacity:1;min-width:0;height:28px;min-height:28px;color:var(--accent);background:#25312f;border-radius:6px;justify-content:center;align-items:center;gap:6px;padding:4px 8px;display:inline-flex}#load.is-loaded:before{content:"";background:currentColor;border-radius:50%;flex:none;width:5px;height:5px}progress{width:100%;height:5px;accent-color:var(--accent);margin-top:8px}header #progress{bottom:3px;left:var(--radius);width:calc(100% - 2 * var(--radius));appearance:none;background:var(--line);--progress-fade:clamp(24px, 5vw, 72px);--progress-mask:linear-gradient(90deg, transparent 2px, #000 var(--progress-fade), #000 calc(100% - var(--progress-fade)), transparent calc(100% - 2px));height:2px;-webkit-mask-image:var(--progress-mask);mask-image:var(--progress-mask);-webkit-mask-image:var(--progress-mask);border:0;border-radius:999px;margin:0;position:absolute;overflow:hidden}header #progress::-webkit-progress-bar{background:var(--line);border-radius:999px}header #progress::-webkit-progress-value{background:var(--accent);border-radius:999px}header #progress::-moz-progress-bar{background:var(--accent);border-radius:999px}header #progress:indeterminate{background:linear-gradient(90deg, transparent, var(--accent), transparent) 0 0 / 35% 100% no-repeat var(--line);animation:1.4s ease-in-out infinite alternate header-progress}header #progress:indeterminate::-webkit-progress-bar{background:0 0}header #progress:indeterminate::-webkit-progress-value{background:0 0}header #progress:indeterminate::-moz-progress-bar{background:0 0}@keyframes header-progress{0%{background-position:0 0}to{background-position:100% 0}}@media (prefers-reduced-motion:reduce){header #progress:indeterminate{background-position:50% 0;animation:none}}.muted{color:var(--muted);margin-top:4px;font-size:11px}.muted:empty{display:none}.terminal-panel{background:var(--background);border-radius:var(--radius-large);border:1px solid #0000;flex-direction:column;flex:1;min-height:0;padding:8px;display:flex}@media (width>=851px){.terminal-panel{padding-right:0}}#pi-terminal{isolation:isolate;background:var(--background);flex:1;min-width:0;min-height:120px;position:relative;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-paws{pointer-events:none;-webkit-user-select:none;user-select:none;position:absolute;overflow:hidden}.terminal-paws i{background:var(--muted);opacity:0;width:24px;height:24px;position:absolute;transform:rotate(28deg);-webkit-mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat}.terminal-paws:not([hidden]) i{animation:3.2s ease-in-out both terminal-paw;animation-delay:var(--delay)}.terminal-paws i:first-child{left:calc(18% + var(--trail-shift));--delay:0s;top:85%}.terminal-paws i:nth-child(2){left:calc(26% + var(--trail-shift));--delay:.65s;top:78%}.terminal-paws i:nth-child(3){left:calc(27% + var(--trail-shift));--delay:1.3s;top:68%}.terminal-paws i:nth-child(4){left:calc(35% + var(--trail-shift));--delay:1.95s;top:61%}.terminal-paws i:nth-child(5){left:calc(36% + var(--trail-shift));--delay:2.6s;top:51%}.terminal-paws i:nth-child(6){left:calc(44% + var(--trail-shift));--delay:3.25s;top:44%}@keyframes terminal-paw{0%,to{opacity:0}15%,40%{opacity:.14}}@media (width<=600px){.terminal-paws i{width:18px;height:18px}}@media (prefers-reduced-motion:reduce){.terminal-paws{display:none}}.empty-mascot{pointer-events:none;flex-direction:column;align-items:flex-end;gap:10px;width:196px;max-width:calc(100% - 24px);display:flex;position:absolute;right:12px}.mascot-cat{pointer-events:auto;cursor:pointer;mix-blend-mode:lighten;background:0 0;border:0;border-radius:0;flex:none;min-height:0;padding:0;line-height:0;display:block;position:relative;transform:translate(0,0)}.mascot-cat:hover{background:0 0}.mascot-cat,.setup-cat{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;outline:none}.mascot-cat,.mascot-cat *,.setup-cat,.setup-cat *{-webkit-user-select:none;user-select:none;-webkit-user-drag:none}.mascot-cat>*,.setup-cat>*{pointer-events:none}.empty-mascot img{object-fit:contain;width:100%;height:100%;image-rendering:pixelated;-webkit-user-select:none;user-select:none;display:block}.mascot-eyelids{opacity:0;position:absolute;inset:0}.mascot-eyelids:before,.mascot-eyelids:after{content:"";background:linear-gradient(to bottom, #f6f0e1 70%, var(--background) 70%, var(--background) 86%, #f6f0e1 86%);width:7%;height:7.4%;position:absolute;top:39.3%}.mascot-eyelids:before{left:28.5%}.mascot-eyelids:after{left:54%}.mascot-suggestion{pointer-events:auto;color:#24242c;text-align:left;white-space:normal;background:#f5efdf;border:0;border-radius:8px;padding:10px 12px;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;position:relative;box-shadow:0 3px #0c0c10}.mascot-suggestion:after{content:"";background:inherit;clip-path:polygon(0 0,100% 0,100% 50%,50% 50%,50% 100%,0 100%);width:12px;height:8px;position:absolute;bottom:-8px;right:52px}.mascot-suggestion:hover{background:#fffaf0}.terminal-controls{justify-content:flex-end;align-items:center;gap:8px;padding-top:4px;display:flex}.terminal-controls,.terminal-panel>footer{cursor:text}#new-chat{margin-right:auto}#newline:not(:hover){background:0 0}#notice{border-radius:var(--radius-small);background:var(--control);margin:6px 0;padding:8px 10px;font-size:12px}footer{color:var(--muted);justify-content:space-between;align-items:center;gap:8px;padding-top:8px;font-size:10px;display:flex}.workspace{min-width:0;min-height:0;margin:12px var(--content-gutter) 8px 0;background:var(--panel);border-radius:20px;flex-direction:column;display:flex;overflow:hidden}.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{flex:1;grid-template-columns:140px minmax(0,1fr);min-height:180px;display:grid}#files{padding:5px 4px;overflow:auto}#files button{border-radius:var(--radius-small);text-align:left;white-space:normal;overflow-wrap:anywhere;background:0 0;border:none;align-items:center;gap:6px;width:100%;padding:5px 7px;font-family:ui-monospace,monospace;font-size:11px;line-height:1.45;display:flex}.file-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.file-icon{background-color:var(--file-color);width:16px;height:16px;-webkit-mask:var(--file-icon) center / 90% 90% no-repeat;mask:var(--file-icon) center / 90% 90% no-repeat;-webkit-mask:var(--file-icon) center / 90% 90% no-repeat;flex:0 0 16px;display:inline-block}.editor-file{align-items:center;gap:6px;min-width:0;display:flex}#filetype{flex:none;display:flex}#filetype:empty{display:none}@media (forced-colors:active){.file-icon{background-color:canvastext}}#files button:hover{background:var(--hover)}#files button.selected{background:var(--selected);color:var(--text)}.editor-area{border-radius:var(--radius);background:var(--surface);flex-direction:column;min-width:0;min-height:0;margin:0 8px 8px 0;display:flex;overflow:hidden}.editor-heading{background:#282832;justify-content:space-between;align-items:center;gap:6px;height:32px;padding:0 6px 0 10px;transition:background-color .14s,box-shadow .14s;display:flex}.editor-area:focus-within .editor-heading{background:#30323a;box-shadow:inset 0 -1px #8abeb74d}@media (prefers-reduced-motion:reduce){.editor-heading{transition:none}}@media (forced-colors:active){.editor-area:focus-within .editor-heading{outline-offset:-1px;outline:1px solid highlight}}#filename{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,monospace;font-size:11px;overflow:hidden}.editor-heading{flex-shrink:0}.editor-input{flex:1;min-width:0;min-height:140px;position:relative;overflow:hidden}#editor,#editor-highlight{width:100%;height:100%;color:var(--text);font-variant-ligatures:none;letter-spacing:normal;tab-size:2;white-space:pre;overflow-wrap:normal;border:0;border-radius:0;margin:0;padding:10px 12px;font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace;position:absolute;top:0;left:0}#editor{resize:none;caret-color:var(--text);background:0 0;overflow:auto}#editor-highlight{visibility:hidden;pointer-events:none;overflow:hidden}.is-highlighted #editor-highlight{visibility:visible}.is-highlighted #editor{color:#0000}.is-highlighted #editor::selection{color:#0000}#editor::selection{background:#8abeb740}#editor:focus{box-shadow:none}#editor-highlight .token{font-style:normal;font-weight:400}#editor-highlight .token.comment,#editor-highlight .token.prolog,#editor-highlight .token.doctype{color:#9292a3}#editor-highlight .token.punctuation{color:#b0b0bd}#editor-highlight .token.tag,#editor-highlight .token.deleted{color:#e29a9a}#editor-highlight .token.keyword,#editor-highlight .token.selector,#editor-highlight .token.important{color:#c4a7d5}#editor-highlight .token.string,#editor-highlight .token.attr-value,#editor-highlight .token.inserted{color:#b5bd68}#editor-highlight .token.function,#editor-highlight .token.builtin,#editor-highlight .token.class-name{color:#8abeb7}#editor-highlight .token.attr-name,#editor-highlight .token.property,#editor-highlight .token.variable{color:#9db9d4}#editor-highlight .token.number,#editor-highlight .token.boolean,#editor-highlight .token.constant,#editor-highlight .token.operator,#editor-highlight .token.entity,#editor-highlight .token.url{color:#f0c674}@media (forced-colors:active){.is-highlighted #editor-highlight{visibility:hidden}.is-highlighted #editor{color:canvastext}.is-highlighted #editor::selection{color:canvastext}}.workspace>.terminal{border-radius:var(--radius);background:var(--background);flex-shrink:0;margin:0 8px;padding:0 8px 8px}.terminal summary{cursor:pointer;padding:6px 0;font-size:11px;font-weight:500}.terminal summary span{float:right;color:var(--muted);font-size:10px;font-weight:400}.shell-toolbar{color:var(--accent);justify-content:space-between;align-items:center;font:11px/1.5 ui-monospace,monospace;display:flex}.shell-toolbar button{color:var(--muted)}#terminal-output{resize:vertical;scrollbar-width:thin;overflow-wrap:anywhere;height:132px;min-height:84px;max-height:min(480px,45dvh);margin:0 0 6px;padding:2px 0;font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:auto}#terminal-output:focus-visible{outline:1px solid var(--accent);outline-offset:2px;border-radius:var(--radius-small)}.shell-empty{color:var(--muted);flex-direction:column;gap:4px;padding:8px 2px;display:flex}.shell-empty strong{color:var(--text);font-weight:500}.shell-empty code{color:var(--accent)}.shell-entry{border-radius:var(--radius-small);background:var(--surface);margin-bottom:6px;padding:8px}.shell-entry:last-child{margin-bottom:0}.shell-entry-heading{align-items:baseline;gap:8px;display:flex}.shell-entry pre{font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;margin:0}.shell-code{flex:1;min-width:0}.shell-status{color:var(--muted);flex-shrink:0;font-size:10px}.shell-status.success{color:#b5bd68}.shell-status.error,.shell-stderr{color:#e29a9a}.shell-entry .shell-stdout,.shell-entry .shell-stderr{margin:6px 0 0 16px}#shell-form{border-radius:var(--radius-small);background:var(--control);align-items:flex-start;gap:8px;padding:4px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;display:flex}#shell-form:focus-within{box-shadow:0 0 0 1px var(--accent)}.shell-prompt{color:var(--accent);flex-shrink:0}#shell-form>.shell-prompt{padding-top:6px;line-height:20px}.shell-input{flex:1;min-width:0;position:relative}#command,#command-highlight{font-variant-ligatures:none;letter-spacing:normal;tab-size:2;white-space:pre-wrap;overflow-wrap:anywhere;border:0;border-radius:0;width:100%;margin:0;padding:6px 0;font:12px/20px ui-monospace,SFMono-Regular,Consolas,monospace}#command-highlight{pointer-events:none;position:absolute;inset:0;overflow:hidden}#command{resize:none;color:#0000;min-height:32px;max-height:104px;caret-color:var(--text);scrollbar-width:thin;background:0 0;display:block;position:relative}#command:focus{box-shadow:none}#command::selection{color:#0000;background:#8abeb740}#command::placeholder{color:var(--muted)}#shell-run{border-radius:4px;min-height:32px}#shell-hint{color:var(--muted);flex-wrap:wrap;justify-content:space-between;gap:2px 12px;padding-top:5px;font-size:10px;line-height:1.5;display:flex}.shell-code .token,#command-highlight .token{font-style:normal;font-weight:400}.shell-code .token.comment,#command-highlight .token.comment{color:#aaaab7}.shell-code .token.string,#command-highlight .token.string{color:#b5bd68}.shell-code .token.function,.shell-code .token.builtin,#command-highlight .token.function,#command-highlight .token.builtin{color:#8abeb7}.shell-code .token.variable,.shell-code .token.assign-left,#command-highlight .token.variable,#command-highlight .token.assign-left{color:#c4a7d5}.shell-code .token.keyword,.shell-code .token.operator,.shell-code .token.number,#command-highlight .token.keyword,#command-highlight .token.operator,#command-highlight .token.number{color:#f0c674}.shell-code .token.parameter,#command-highlight .token.parameter{color:#9db9d4}@media (forced-colors:active){#command-highlight{display:none}#command{color:canvastext}#command::selection{color:canvastext}}.workspace-note{color:var(--muted);margin:6px 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{--dialog-inset:24px;--cat-size:152px;--cat-rise:calc(var(--cat-size) * .58);width:min(460px,100% - 32px);max-height:calc(100dvh - 32px - var(--cat-rise));padding:var(--dialog-inset);border-radius:calc(var(--radius-small) + var(--dialog-inset));background:var(--surface);color:var(--text);transform:translateY(calc(var(--cat-rise) / 2));border:0;overflow:visible;box-shadow:0 24px 80px #0008}#model-dialog[open]{flex-direction:column;display:flex}.setup-paws{--modal-half:min(230px, calc(50vw - 16px));left:50%;top:calc(50% - var(--cat-rise) / 2);pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;width:100vw;height:100dvh;position:absolute;overflow:hidden;transform:translate(-50%,-50%)}.setup-paws i{background:var(--muted);opacity:0;width:28px;height:28px;transform:rotate(var(--angle));position:absolute;-webkit-mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat}#model-dialog[open] .setup-paws i{animation:10s ease-in-out infinite setup-paw;animation-delay:var(--delay);animation-play-state:var(--cat-play-state,running)}.setup-paws i:first-child{left:calc(50% - var(--modal-half) - clamp(48px, 9vw, 136px));--angle:-34deg;--delay:-1s;top:68%}.setup-paws i:nth-child(2){left:calc(50% - var(--modal-half) - clamp(30px, 6vw, 88px));--angle:-24deg;--delay:-.3s;top:60%}.setup-paws i:nth-child(3){left:calc(50% - var(--modal-half) - clamp(45px, 8vw, 120px));--angle:-12deg;--delay:.4s;top:52%}.setup-paws i:nth-child(4){left:calc(50% + var(--modal-half) + clamp(25px, 4vw, 64px));--angle:28deg;--delay:4s;top:46%}.setup-paws i:nth-child(5){left:calc(50% + var(--modal-half) + clamp(50px, 8vw, 120px));--angle:35deg;--delay:4.7s;top:38%}.setup-paws i:nth-child(6){left:calc(50% + var(--modal-half) + clamp(35px, 7vw, 100px));--angle:25deg;--delay:5.4s;top:30%}@keyframes setup-paw{0%,5%,38%,to{opacity:0}14%,24%{opacity:.16}}@media (width<=600px){.setup-paws i{width:18px;height:18px}.setup-paws i:first-child{--angle:65deg;top:22px;left:8%}.setup-paws i:nth-child(2){--angle:75deg;top:14px;left:19%}.setup-paws i:nth-child(3){--angle:65deg;top:22px;left:30%}.setup-paws i:nth-child(n+4){display:none}}@media (prefers-reduced-motion:reduce){#model-dialog[open] .setup-paws i{animation:none}.setup-paws i:first-child,.setup-paws i:nth-child(2){opacity:.1}}.setup-cat{top:calc(-1 * var(--cat-rise));right:var(--dialog-inset);width:var(--cat-size);height:calc(var(--cat-size) * .66);pointer-events:auto;cursor:pointer;touch-action:manipulation;-webkit-user-select:none;user-select:none;position:absolute;overflow:hidden}.setup-cat img,.setup-cat-tail,.setup-cat-eyelids{top:calc(var(--cat-size) * -.23);width:100%;height:auto;image-rendering:pixelated;opacity:0;display:block;position:absolute;left:0}.setup-cat img{clip-path:inset(0 22% 0 0)}.setup-cat-tail{aspect-ratio:1;background:var(--cat-sprite) 0 0 / 100% 100% no-repeat;clip-path:polygon(78% 40%,100% 40%,100% 82%,77% 82%,77% 77%,78% 77%);transform-origin:78% 80%}.setup-cat.is-ready img,.setup-cat.is-ready .setup-cat-tail{opacity:1}.setup-cat-eyelids{aspect-ratio:1}.setup-cat-eyelids:before,.setup-cat-eyelids:after{content:"";background:linear-gradient(#080709,#080709) 50% 80%/78% 14% no-repeat,#f6efdf;width:8.4%;height:8.7%;position:absolute;top:55.6%}.setup-cat-eyelids:before{left:28%}.setup-cat-eyelids:after{left:54.3%}#model-dialog[open] .setup-cat.is-ready .setup-cat-tail{animation:9s ease-in-out infinite setup-tail-swish;animation-play-state:var(--cat-play-state,running)}#model-dialog[open] .setup-cat.is-ready .setup-cat-eyelids{animation:7.4s step-end infinite setup-cat-blink;animation-play-state:var(--cat-play-state,running)}@keyframes setup-cat-blink{0%,45%,to{opacity:0}43%{opacity:1}}@keyframes setup-tail-swish{0%,24%,50%,to{transform:rotate(0)}33%{transform:rotate(-4deg)}41%{transform:rotate(2.5deg)}46%{transform:rotate(-.6deg)}}@media (prefers-reduced-motion:reduce){#model-dialog[open] .setup-cat.is-ready .setup-cat-tail,#model-dialog[open] .setup-cat.is-ready .setup-cat-eyelids{animation:none}}.setup-heading{flex-shrink:0;align-items:flex-start;gap:8px;display:flex}#model-cancel{width:32px;height:32px;min-height:32px;color:var(--muted);background:0 0;flex:none;place-items:center;padding:0;display:grid}#model-cancel:hover:not(:disabled){background:var(--control);color:var(--text)}.setup-body{min-height:0;overflow:auto}#model-dialog::backdrop{-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#08080dbd}#model-title{letter-spacing:-.5px;flex:1;min-width:0;margin:0 0 8px;font-size:27px;font-weight:650;line-height:1.2}#model-description{color:var(--muted);margin:0;line-height:1.6}.setup-runtime{grid-template-columns:minmax(max-content,1fr) 1fr 1.2fr;gap:8px;margin:18px 0;display:grid}.setup-runtime>div{border-radius:var(--radius-small);background:var(--control);padding:10px}.setup-runtime dt{color:var(--accent);font-size:12px;font-weight:600}.setup-runtime dd{margin:5px 0 0;font-size:11px;line-height:1.5}.setup-model{align-items:center;gap:6px;display:flex}.model-org{border-radius:50%;flex:none;display:inline-flex}.model-org img{border-radius:inherit;width:18px;height:18px;display:block}.model-source{color:inherit;-webkit-text-decoration-color:var(--muted);text-decoration-color:var(--muted);text-underline-offset:3px;white-space:nowrap}.model-source:hover{color:var(--accent)}.setup-model a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}.setup-facts{border-radius:var(--radius-small);background:var(--panel);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{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;border-radius:var(--radius-small);background:var(--control);max-height:84px;padding:10px 12px;font-size:12px;line-height:1.5;overflow:auto}#model-error{border-radius:var(--radius-small);overflow-wrap:anywhere;background:#3c2828;padding:10px 12px;line-height:1.5}#model-open-tab{color:var(--accent);white-space:normal;text-decoration:none}.setup-credits{color:var(--muted);flex-wrap:wrap;align-items:center;gap:6px 10px;margin:18px 0;font-size:11px;line-height:1.6;display:flex}.setup-credits a{color:inherit;white-space:nowrap;align-items:center;gap:4px;text-decoration:none;display:inline-flex}.setup-credits a:hover{color:var(--text);text-underline-offset:3px;text-decoration:underline}.setup-credits a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}.setup-credits img,.setup-credits svg{object-fit:contain;flex:none;width:14px;height:14px}.setup-credits .credit-pi{padding:2px}.setup-actions{flex-shrink:0;padding-top:4px;display:flex}.setup-actions button{width:100%;min-height:44px;padding:10px 16px}#model-confirm{color:#18181e;background:#fff;font-size:14px;font-weight:600}#model-confirm:hover{background:#e8e8ed}@media (width<=420px){#model-dialog{--dialog-inset:16px;--cat-size:116px}#model-title{font-size:24px}.setup-runtime{grid-template-columns:1fr;gap:5px;margin:14px 0}.setup-runtime>div{align-items:baseline;gap:12px;padding:8px 10px;display:flex}.setup-runtime dt{flex:0 0 64px}.setup-runtime dd{margin:0}.setup-facts{margin:14px 0}.setup-credits{margin:12px 0}}@media (width>=851px) and (width<=1000px){header{grid-template-columns:auto minmax(0,1fr) auto}}@media (height<=620px){#model-dialog{--cat-size:72px}.setup-runtime,.setup-facts{margin:12px 0}}@media (width<=420px) and (height<=620px){.setup-runtime{grid-template-columns:minmax(0,1fr) max-content}.setup-runtime>div:first-child{grid-column:1/-1}.setup-runtime>div:not(:first-child){padding:6px 10px;display:block}.setup-runtime>div:not(:first-child) dd{margin-top:2px}}@media (width<=850px){:root{--header-height:calc(2 * var(--header-control-height) + 3 * var(--surface-inset));--header-inset:10px;--content-gutter:10px}header{grid-template-columns:auto minmax(0,1fr);grid-template-rows:repeat(2, var(--header-control-height));gap:var(--surface-inset);grid-template-areas:"brand runtime""model model"}.brand{grid-area:brand;justify-self:start}.runtime-meter{grid-area:runtime;justify-self:end}.model-panel{grid-area:model;margin:0}main{flex-direction:column;height:auto;display:flex}.column-resizer{display:none}.conversation{height:calc(100dvh - var(--header-height) - var(--header-inset));min-height:320px;padding:10px var(--content-gutter)}.workspace{min-height:520px;margin:0 var(--content-gutter) 10px}.file-area{grid-template-columns:115px minmax(0,1fr);min-height:280px}h1{font-size:20px}.badge,footer{font-size:9px}}@media (pointer:coarse){:root{--header-control-height:38px}button,.button,#files button{min-height:38px}.editor-heading{min-height:40px}.terminal summary{padding-top:11px;padding-bottom:11px}#command,#command-highlight,#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-D2Hvfy3l.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/{index-A7Nq9zGD.js → index-DUQ_UspG.js}
RENAMED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/index-DnXFGQ8z.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/{index-Bl39c3Js.css → index-Uy7sEXBC.css}
RENAMED
|
@@ -1 +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;--surface-inset:8px;--radius-small:8px;--radius:calc(var(--radius-small) + var(--surface-inset));--radius-large:calc(var(--radius) + var(--surface-inset));--content-width:1800px;--content-gutter:14px;--header-inset:12px;--header-control-height:32px;--header-height:calc(var(--header-control-height) + 2 * var(--surface-inset));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-radius:var(--radius-small);background:var(--control);min-height:28px;color:inherit;white-space:nowrap;border:1px solid #0000;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);color:var(--text)}.primary:hover{background:var(--selected)}#send.has-input:not(:disabled){color:#18181e;background:#fff}#send.has-input:not(:disabled):hover{background:#e8e8ed}.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{width:calc(100% - 2 * var(--content-gutter));max-width:calc(var(--content-width) - 2 * var(--content-gutter));height:var(--header-height);margin:var(--header-inset) auto 0;padding:var(--surface-inset);justify-content:space-between;align-items:center;gap:var(--surface-inset) 16px;background:var(--surface);border-radius:var(--radius);grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);display:grid;position:relative;overflow:hidden}.brand{color:inherit;align-items:center;gap:8px;font-size:15px;font-weight:600;text-decoration:none;display:flex}.pi-logo{flex:none;width:18px;height:18px;display:block}.brand{justify-self:start;padding:0 4px}.runtime-meter{--runtime-padding:10px;width:max-content;max-width:100%;height:var(--header-control-height);padding:5px var(--runtime-padding);border-radius:var(--radius-small);background:var(--control);white-space:nowrap;justify-self:center;align-items:center;font-size:11px;transition:width .22s cubic-bezier(.2,.8,.2,1);display:flex;overflow:clip}.runtime-content{flex:none;align-items:center;gap:8px;width:max-content;height:100%;display:flex}.runtime-location{flex:none;align-items:center;gap:6px;display:flex}.runtime-dot{background:var(--muted);border-radius:50%;width:5px;height:5px}.runtime-meter[data-state=generating] .runtime-dot,.runtime-meter[data-state=tool] .runtime-dot{background:var(--accent)}.runtime-meter[data-state=error] .runtime-dot{background:#d98c8c}.runtime-divider{background:var(--line);flex:0 0 1px;align-self:stretch;margin:2px 0}.runtime-sparkline{color:var(--accent);flex:none}.runtime-value{color:var(--muted);font-variant-numeric:tabular-nums;flex:none}.runtime-meter[data-state=generating] .runtime-value{color:var(--accent)}@media (prefers-reduced-motion:reduce){.runtime-meter{transition:none}}main{--conversation-min:420px;--workspace-min:370px;grid-template-columns:minmax(var(--conversation-min), var(--conversation-width,1.20482fr)) 0 minmax(var(--workspace-min), 1fr);height:calc(100dvh - var(--header-height) - var(--header-inset));max-width:var(--content-width);margin:auto;display:grid}.column-resizer{left:calc(-1 * var(--content-gutter));z-index:2;width:var(--content-gutter);cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;outline:none;justify-content:center;align-items:center;margin:12px 0 8px;display:flex;position:relative}.column-resizer:after{content:"";background:var(--line-strong);border-radius:999px;flex:none;width:3px;height:44px;transition:background-color .12s}.column-resizer:hover:after{background:var(--text)}.column-resizer:focus-visible:after{background:var(--accent);outline:2px solid var(--accent);outline-offset:4px}.is-resizing-columns,.is-resizing-columns *{cursor:col-resize!important;-webkit-user-select:none!important;user-select:none!important}.is-resizing-columns .column-resizer:after{background:var(--accent)}@media (prefers-reduced-motion:reduce){.column-resizer:after{transition:none}}@media (forced-colors:active){.column-resizer:after{background:buttontext}}.conversation{min-width:0;min-height:0;padding:12px var(--content-gutter) 8px;flex-direction:column;display:flex}.model-panel{background:var(--surface);margin-left:auto}.model-heading{justify-content:space-between;align-items:center;gap:8px;display:flex}.model-heading>div{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.model-heading strong{font-size:12px;font-weight:600}.header-model-source{color:inherit;white-space:nowrap;border-radius:3px;flex:none;align-items:center;gap:6px;text-decoration:none;display:inline-flex}.header-model-source img{border-radius:50%;flex:none;width:16px;height:16px;display:block}.header-model-source:hover{color:var(--accent)}.header-model-source:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.badge{border-radius:var(--radius-small);background:var(--control);color:var(--muted);padding:3px 7px;font-size:10px}#load{min-width:90px;height:var(--header-control-height);flex-shrink:0}#load.is-loaded{opacity:1;min-width:0;height:28px;min-height:28px;color:var(--accent);background:#25312f;border-radius:6px;justify-content:center;align-items:center;gap:6px;padding:4px 8px;display:inline-flex}#load.is-loaded:before{content:"";background:currentColor;border-radius:50%;flex:none;width:5px;height:5px}progress{width:100%;height:5px;accent-color:var(--accent);margin-top:8px}header #progress{bottom:3px;left:var(--radius);width:calc(100% - 2 * var(--radius));appearance:none;background:var(--line);--progress-fade:clamp(24px, 5vw, 72px);--progress-mask:linear-gradient(90deg, transparent 2px, #000 var(--progress-fade), #000 calc(100% - var(--progress-fade)), transparent calc(100% - 2px));height:2px;-webkit-mask-image:var(--progress-mask);mask-image:var(--progress-mask);-webkit-mask-image:var(--progress-mask);border:0;border-radius:999px;margin:0;position:absolute;overflow:hidden}header #progress::-webkit-progress-bar{background:var(--line);border-radius:999px}header #progress::-webkit-progress-value{background:var(--accent);border-radius:999px}header #progress::-moz-progress-bar{background:var(--accent);border-radius:999px}header #progress:indeterminate{background:linear-gradient(90deg, transparent, var(--accent), transparent) 0 0 / 35% 100% no-repeat var(--line);animation:1.4s ease-in-out infinite alternate header-progress}header #progress:indeterminate::-webkit-progress-bar{background:0 0}header #progress:indeterminate::-webkit-progress-value{background:0 0}header #progress:indeterminate::-moz-progress-bar{background:0 0}@keyframes header-progress{0%{background-position:0 0}to{background-position:100% 0}}@media (prefers-reduced-motion:reduce){header #progress:indeterminate{background-position:50% 0;animation:none}}.muted{color:var(--muted);margin-top:4px;font-size:11px}.muted:empty{display:none}.terminal-panel{background:var(--background);border-radius:var(--radius-large);border:1px solid #0000;flex-direction:column;flex:1;min-height:0;padding:8px;display:flex}@media (width>=851px){.terminal-panel{padding-right:0}}#pi-terminal{isolation:isolate;background:var(--background);flex:1;min-width:0;min-height:120px;position:relative;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-paws{pointer-events:none;-webkit-user-select:none;user-select:none;position:absolute;overflow:hidden}.terminal-paws i{background:var(--muted);opacity:0;width:24px;height:24px;position:absolute;transform:rotate(28deg);-webkit-mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat}.terminal-paws:not([hidden]) i{animation:3.2s ease-in-out both terminal-paw;animation-delay:var(--delay)}.terminal-paws i:first-child{left:calc(18% + var(--trail-shift));--delay:0s;top:85%}.terminal-paws i:nth-child(2){left:calc(26% + var(--trail-shift));--delay:.65s;top:78%}.terminal-paws i:nth-child(3){left:calc(27% + var(--trail-shift));--delay:1.3s;top:68%}.terminal-paws i:nth-child(4){left:calc(35% + var(--trail-shift));--delay:1.95s;top:61%}.terminal-paws i:nth-child(5){left:calc(36% + var(--trail-shift));--delay:2.6s;top:51%}.terminal-paws i:nth-child(6){left:calc(44% + var(--trail-shift));--delay:3.25s;top:44%}@keyframes terminal-paw{0%,to{opacity:0}15%,40%{opacity:.14}}@media (width<=600px){.terminal-paws i{width:18px;height:18px}}@media (prefers-reduced-motion:reduce){.terminal-paws{display:none}}.empty-mascot{pointer-events:none;flex-direction:column;align-items:flex-end;gap:10px;width:196px;max-width:calc(100% - 24px);display:flex;position:absolute;right:12px}.mascot-cat{pointer-events:auto;cursor:pointer;background:0 0;border:0;border-radius:0;flex:none;min-height:0;padding:0;line-height:0;display:block;position:relative}.mascot-cat:hover{background:0 0}.mascot-cat,.setup-cat{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;outline:none}.mascot-cat,.mascot-cat *,.setup-cat,.setup-cat *{-webkit-user-select:none;user-select:none;-webkit-user-drag:none}.mascot-cat>*,.setup-cat>*{pointer-events:none}.empty-mascot img{object-fit:contain;width:100%;height:100%;image-rendering:pixelated;-webkit-user-select:none;user-select:none;clip-path:polygon(16.906% 6.22%,26.077% 6.22%,26.077% 9.729%,30.861% 9.729%,30.861% 14.274%,35.407% 14.274%,35.407% 20.175%,56.539% 20.175%,56.539% 14.274%,61.404% 14.274%,61.404% 9.729%,65.71% 9.729%,65.71% 6.22%,75.04% 6.22%,75.04% 44.498%,68.82% 44.498%,68.82% 48.166%,75.04% 48.166%,75.04% 52.073%,68.82% 52.073%,68.82% 57.576%,65.152% 57.576%,65.152% 61.324%,68.979% 61.324%,68.979% 67.225%,73.285% 67.225%,73.285% 77.033%,77.671% 77.033%,77.671% 55.742%,81.818% 55.742%,81.818% 51.595%,91.228% 51.595%,91.228% 63.397%,86.124% 63.397%,86.124% 85.566%,81.579% 85.566%,81.579% 92.265%,25.04% 92.265%,25.04% 85.566%,28.07% 85.566%,28.07% 57.576%,19.856% 57.576%,19.856% 52.073%,12.52% 52.073%,12.52% 48.166%,19.856% 48.166%,19.856% 44.498%,12.52% 44.498%,12.52% 41.467%,16.906% 41.467%);display:block}.mascot-eyelids{opacity:0;position:absolute;inset:0}.mascot-eyelids:before,.mascot-eyelids:after{content:"";background:linear-gradient(to bottom, #f6f0e1 70%, var(--background) 70%, var(--background) 86%, #f6f0e1 86%);width:7%;height:7.4%;position:absolute;top:39.3%}.mascot-eyelids:before{left:28.5%}.mascot-eyelids:after{left:54%}.mascot-suggestion{pointer-events:auto;color:#24242c;text-align:left;white-space:normal;background:#f5efdf;border:0;border-radius:8px;padding:10px 12px;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;position:relative;box-shadow:0 3px #0c0c10}.mascot-suggestion:after{content:"";background:inherit;clip-path:polygon(0 0,100% 0,100% 50%,50% 50%,50% 100%,0 100%);width:12px;height:8px;position:absolute;bottom:-8px;right:52px}.mascot-suggestion:hover{background:#fffaf0}.terminal-controls{justify-content:flex-end;align-items:center;gap:8px;padding-top:4px;display:flex}.terminal-controls,.terminal-panel>footer{cursor:text}#new-chat{margin-right:auto}#newline:not(:hover){background:0 0}#notice{border-radius:var(--radius-small);background:var(--control);margin:6px 0;padding:8px 10px;font-size:12px}footer{color:var(--muted);justify-content:space-between;align-items:center;gap:8px;padding-top:8px;font-size:10px;display:flex}.workspace{min-width:0;min-height:0;margin:12px var(--content-gutter) 8px 0;background:var(--panel);border-radius:20px;flex-direction:column;display:flex;overflow:hidden}.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{flex:1;grid-template-columns:140px minmax(0,1fr);min-height:180px;display:grid}#files{padding:5px 4px;overflow:auto}#files button{border-radius:var(--radius-small);text-align:left;white-space:normal;overflow-wrap:anywhere;background:0 0;border:none;align-items:center;gap:6px;width:100%;padding:5px 7px;font-family:ui-monospace,monospace;font-size:11px;line-height:1.45;display:flex}.file-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.file-icon{background-color:var(--file-color);width:16px;height:16px;-webkit-mask:var(--file-icon) center / 90% 90% no-repeat;mask:var(--file-icon) center / 90% 90% no-repeat;-webkit-mask:var(--file-icon) center / 90% 90% no-repeat;flex:0 0 16px;display:inline-block}.editor-file{align-items:center;gap:6px;min-width:0;display:flex}#filetype{flex:none;display:flex}#filetype:empty{display:none}@media (forced-colors:active){.file-icon{background-color:canvastext}}#files button:hover{background:var(--hover)}#files button.selected{background:var(--selected);color:var(--text)}.editor-area{border-radius:var(--radius);background:var(--surface);flex-direction:column;min-width:0;min-height:0;margin:0 8px 8px 0;display:flex;overflow:hidden}.editor-heading{background:#282832;justify-content:space-between;align-items:center;gap:6px;height:32px;padding:0 6px 0 10px;transition:background-color .14s,box-shadow .14s;display:flex}.editor-area:focus-within .editor-heading{background:#30323a;box-shadow:inset 0 -1px #8abeb74d}@media (prefers-reduced-motion:reduce){.editor-heading{transition:none}}@media (forced-colors:active){.editor-area:focus-within .editor-heading{outline-offset:-1px;outline:1px solid highlight}}#filename{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,monospace;font-size:11px;overflow:hidden}.editor-heading{flex-shrink:0}.editor-input{flex:1;min-width:0;min-height:140px;position:relative;overflow:hidden}#editor,#editor-highlight{width:100%;height:100%;color:var(--text);font-variant-ligatures:none;letter-spacing:normal;tab-size:2;white-space:pre;overflow-wrap:normal;border:0;border-radius:0;margin:0;padding:10px 12px;font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace;position:absolute;top:0;left:0}#editor{resize:none;caret-color:var(--text);background:0 0;overflow:auto}#editor-highlight{visibility:hidden;pointer-events:none;overflow:hidden}.is-highlighted #editor-highlight{visibility:visible}.is-highlighted #editor{color:#0000}.is-highlighted #editor::selection{color:#0000}#editor::selection{background:#8abeb740}#editor:focus{box-shadow:none}#editor-highlight .token{font-style:normal;font-weight:400}#editor-highlight .token.comment,#editor-highlight .token.prolog,#editor-highlight .token.doctype{color:#9292a3}#editor-highlight .token.punctuation{color:#b0b0bd}#editor-highlight .token.tag,#editor-highlight .token.deleted{color:#e29a9a}#editor-highlight .token.keyword,#editor-highlight .token.selector,#editor-highlight .token.important{color:#c4a7d5}#editor-highlight .token.string,#editor-highlight .token.attr-value,#editor-highlight .token.inserted{color:#b5bd68}#editor-highlight .token.function,#editor-highlight .token.builtin,#editor-highlight .token.class-name{color:#8abeb7}#editor-highlight .token.attr-name,#editor-highlight .token.property,#editor-highlight .token.variable{color:#9db9d4}#editor-highlight .token.number,#editor-highlight .token.boolean,#editor-highlight .token.constant,#editor-highlight .token.operator,#editor-highlight .token.entity,#editor-highlight .token.url{color:#f0c674}@media (forced-colors:active){.is-highlighted #editor-highlight{visibility:hidden}.is-highlighted #editor{color:canvastext}.is-highlighted #editor::selection{color:canvastext}}.workspace>.terminal{border-radius:var(--radius);background:var(--background);flex-shrink:0;margin:0 8px;padding:0 8px 8px}.terminal summary{cursor:pointer;padding:6px 0;font-size:11px;font-weight:500}.terminal summary span{float:right;color:var(--muted);font-size:10px;font-weight:400}.shell-toolbar{color:var(--accent);justify-content:space-between;align-items:center;font:11px/1.5 ui-monospace,monospace;display:flex}.shell-toolbar button{color:var(--muted)}#terminal-output{resize:vertical;scrollbar-width:thin;overflow-wrap:anywhere;height:132px;min-height:84px;max-height:min(480px,45dvh);margin:0 0 6px;padding:2px 0;font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:auto}#terminal-output:focus-visible{outline:1px solid var(--accent);outline-offset:2px;border-radius:var(--radius-small)}.shell-empty{color:var(--muted);flex-direction:column;gap:4px;padding:8px 2px;display:flex}.shell-empty strong{color:var(--text);font-weight:500}.shell-empty code{color:var(--accent)}.shell-entry{border-radius:var(--radius-small);background:var(--surface);margin-bottom:6px;padding:8px}.shell-entry:last-child{margin-bottom:0}.shell-entry-heading{align-items:baseline;gap:8px;display:flex}.shell-entry pre{font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;margin:0}.shell-code{flex:1;min-width:0}.shell-status{color:var(--muted);flex-shrink:0;font-size:10px}.shell-status.success{color:#b5bd68}.shell-status.error,.shell-stderr{color:#e29a9a}.shell-entry .shell-stdout,.shell-entry .shell-stderr{margin:6px 0 0 16px}#shell-form{border-radius:var(--radius-small);background:var(--control);align-items:flex-start;gap:8px;padding:4px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;display:flex}#shell-form:focus-within{box-shadow:0 0 0 1px var(--accent)}.shell-prompt{color:var(--accent);flex-shrink:0}#shell-form>.shell-prompt{padding-top:6px;line-height:20px}.shell-input{flex:1;min-width:0;position:relative}#command,#command-highlight{font-variant-ligatures:none;letter-spacing:normal;tab-size:2;white-space:pre-wrap;overflow-wrap:anywhere;border:0;border-radius:0;width:100%;margin:0;padding:6px 0;font:12px/20px ui-monospace,SFMono-Regular,Consolas,monospace}#command-highlight{pointer-events:none;position:absolute;inset:0;overflow:hidden}#command{resize:none;color:#0000;min-height:32px;max-height:104px;caret-color:var(--text);scrollbar-width:thin;background:0 0;display:block;position:relative}#command:focus{box-shadow:none}#command::selection{color:#0000;background:#8abeb740}#command::placeholder{color:var(--muted)}#shell-run{border-radius:4px;min-height:32px}#shell-hint{color:var(--muted);flex-wrap:wrap;justify-content:space-between;gap:2px 12px;padding-top:5px;font-size:10px;line-height:1.5;display:flex}.shell-code .token,#command-highlight .token{font-style:normal;font-weight:400}.shell-code .token.comment,#command-highlight .token.comment{color:#aaaab7}.shell-code .token.string,#command-highlight .token.string{color:#b5bd68}.shell-code .token.function,.shell-code .token.builtin,#command-highlight .token.function,#command-highlight .token.builtin{color:#8abeb7}.shell-code .token.variable,.shell-code .token.assign-left,#command-highlight .token.variable,#command-highlight .token.assign-left{color:#c4a7d5}.shell-code .token.keyword,.shell-code .token.operator,.shell-code .token.number,#command-highlight .token.keyword,#command-highlight .token.operator,#command-highlight .token.number{color:#f0c674}.shell-code .token.parameter,#command-highlight .token.parameter{color:#9db9d4}@media (forced-colors:active){#command-highlight{display:none}#command{color:canvastext}#command::selection{color:canvastext}}.workspace-note{color:var(--muted);margin:6px 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{--dialog-inset:24px;--cat-size:152px;--cat-rise:calc(var(--cat-size) * .58);width:min(460px,100% - 32px);max-height:calc(100dvh - 32px - var(--cat-rise));padding:var(--dialog-inset);border-radius:calc(var(--radius-small) + var(--dialog-inset));background:var(--surface);color:var(--text);transform:translateY(calc(var(--cat-rise) / 2));border:0;overflow:visible;box-shadow:0 24px 80px #0008}#model-dialog[open]{flex-direction:column;display:flex}.setup-paws{--modal-half:min(230px, calc(50vw - 16px));left:50%;top:calc(50% - var(--cat-rise) / 2);pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;width:100vw;height:100dvh;position:absolute;overflow:hidden;transform:translate(-50%,-50%)}.setup-paws i{background:var(--muted);opacity:0;width:28px;height:28px;transform:rotate(var(--angle));position:absolute;-webkit-mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat}#model-dialog[open] .setup-paws i{animation:10s ease-in-out infinite setup-paw;animation-delay:var(--delay);animation-play-state:var(--cat-play-state,running)}.setup-paws i:first-child{left:calc(50% - var(--modal-half) - clamp(48px, 9vw, 136px));--angle:-34deg;--delay:-1s;top:68%}.setup-paws i:nth-child(2){left:calc(50% - var(--modal-half) - clamp(30px, 6vw, 88px));--angle:-24deg;--delay:-.3s;top:60%}.setup-paws i:nth-child(3){left:calc(50% - var(--modal-half) - clamp(45px, 8vw, 120px));--angle:-12deg;--delay:.4s;top:52%}.setup-paws i:nth-child(4){left:calc(50% + var(--modal-half) + clamp(25px, 4vw, 64px));--angle:28deg;--delay:4s;top:46%}.setup-paws i:nth-child(5){left:calc(50% + var(--modal-half) + clamp(50px, 8vw, 120px));--angle:35deg;--delay:4.7s;top:38%}.setup-paws i:nth-child(6){left:calc(50% + var(--modal-half) + clamp(35px, 7vw, 100px));--angle:25deg;--delay:5.4s;top:30%}@keyframes setup-paw{0%,5%,38%,to{opacity:0}14%,24%{opacity:.16}}@media (width<=600px){.setup-paws i{width:18px;height:18px}.setup-paws i:first-child{--angle:65deg;top:22px;left:8%}.setup-paws i:nth-child(2){--angle:75deg;top:14px;left:19%}.setup-paws i:nth-child(3){--angle:65deg;top:22px;left:30%}.setup-paws i:nth-child(n+4){display:none}}@media (prefers-reduced-motion:reduce){#model-dialog[open] .setup-paws i{animation:none}.setup-paws i:first-child,.setup-paws i:nth-child(2){opacity:.1}}.setup-cat{top:calc(-1 * var(--cat-rise));right:var(--dialog-inset);width:var(--cat-size);height:calc(var(--cat-size) * .66);pointer-events:auto;cursor:pointer;touch-action:manipulation;-webkit-user-select:none;user-select:none;position:absolute;overflow:hidden}.setup-cat img,.setup-cat-tail,.setup-cat-eyelids{top:calc(var(--cat-size) * -.23);width:100%;height:auto;image-rendering:pixelated;opacity:0;display:block;position:absolute;left:0}.setup-cat img{clip-path:inset(0 22% 0 0)}.setup-cat-tail{aspect-ratio:1;background:var(--cat-sprite) 0 0 / 100% 100% no-repeat;clip-path:polygon(78% 40%,100% 40%,100% 82%,77% 82%,77% 77%,78% 77%);transform-origin:78% 80%}.setup-cat.is-ready img,.setup-cat.is-ready .setup-cat-tail{opacity:1}.setup-cat-eyelids{aspect-ratio:1}.setup-cat-eyelids:before,.setup-cat-eyelids:after{content:"";background:linear-gradient(#080709,#080709) 50% 80%/78% 14% no-repeat,#f6efdf;width:8.4%;height:8.7%;position:absolute;top:55.6%}.setup-cat-eyelids:before{left:28%}.setup-cat-eyelids:after{left:54.3%}#model-dialog[open] .setup-cat.is-ready .setup-cat-tail{animation:9s ease-in-out infinite setup-tail-swish;animation-play-state:var(--cat-play-state,running)}#model-dialog[open] .setup-cat.is-ready .setup-cat-eyelids{animation:7.4s step-end infinite setup-cat-blink;animation-play-state:var(--cat-play-state,running)}@keyframes setup-cat-blink{0%,45%,to{opacity:0}43%{opacity:1}}@keyframes setup-tail-swish{0%,24%,50%,to{transform:rotate(0)}33%{transform:rotate(-4deg)}41%{transform:rotate(2.5deg)}46%{transform:rotate(-.6deg)}}@media (prefers-reduced-motion:reduce){#model-dialog[open] .setup-cat.is-ready .setup-cat-tail,#model-dialog[open] .setup-cat.is-ready .setup-cat-eyelids{animation:none}}.setup-heading{flex-shrink:0;align-items:flex-start;gap:8px;display:flex}#model-cancel{width:32px;height:32px;min-height:32px;color:var(--muted);background:0 0;flex:none;place-items:center;padding:0;display:grid}#model-cancel:hover:not(:disabled){background:var(--control);color:var(--text)}.setup-body{min-height:0;overflow:auto}#model-dialog::backdrop{-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#08080dbd}#model-title{letter-spacing:-.5px;flex:1;min-width:0;margin:0 0 8px;font-size:27px;font-weight:650;line-height:1.2}#model-description{color:var(--muted);margin:0;line-height:1.6}.setup-runtime{grid-template-columns:minmax(max-content,1fr) 1fr 1.2fr;gap:8px;margin:18px 0;display:grid}.setup-runtime>div{border-radius:var(--radius-small);background:var(--control);padding:10px}.setup-runtime dt{color:var(--accent);font-size:12px;font-weight:600}.setup-runtime dd{margin:5px 0 0;font-size:11px;line-height:1.5}.setup-model{align-items:center;gap:6px;display:flex}.model-org{border-radius:50%;flex:none;display:inline-flex}.model-org img{border-radius:inherit;width:18px;height:18px;display:block}.model-source{color:inherit;-webkit-text-decoration-color:var(--muted);text-decoration-color:var(--muted);text-underline-offset:3px;white-space:nowrap}.model-source:hover{color:var(--accent)}.setup-model a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}.setup-facts{border-radius:var(--radius-small);background:var(--panel);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{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;border-radius:var(--radius-small);background:var(--control);max-height:84px;padding:10px 12px;font-size:12px;line-height:1.5;overflow:auto}#model-error{border-radius:var(--radius-small);overflow-wrap:anywhere;background:#3c2828;padding:10px 12px;line-height:1.5}#model-open-tab{color:var(--accent);white-space:normal;text-decoration:none}.setup-credits{color:var(--muted);flex-wrap:wrap;align-items:center;gap:6px 10px;margin:18px 0;font-size:11px;line-height:1.6;display:flex}.setup-credits a{color:inherit;white-space:nowrap;align-items:center;gap:4px;text-decoration:none;display:inline-flex}.setup-credits a:hover{color:var(--text);text-underline-offset:3px;text-decoration:underline}.setup-credits a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}.setup-credits img,.setup-credits svg{object-fit:contain;flex:none;width:14px;height:14px}.setup-credits .credit-pi{padding:2px}.setup-actions{flex-shrink:0;padding-top:4px;display:flex}.setup-actions button{width:100%;min-height:44px;padding:10px 16px}#model-confirm{color:#18181e;background:#fff;font-size:14px;font-weight:600}#model-confirm:hover{background:#e8e8ed}@media (width<=420px){#model-dialog{--dialog-inset:16px;--cat-size:116px}#model-title{font-size:24px}.setup-runtime{grid-template-columns:1fr;gap:5px;margin:14px 0}.setup-runtime>div{align-items:baseline;gap:12px;padding:8px 10px;display:flex}.setup-runtime dt{flex:0 0 64px}.setup-runtime dd{margin:0}.setup-facts{margin:14px 0}.setup-credits{margin:12px 0}}@media (width>=851px) and (width<=1000px){header{grid-template-columns:auto minmax(0,1fr) auto}}@media (height<=620px){#model-dialog{--cat-size:72px}.setup-runtime,.setup-facts{margin:12px 0}}@media (width<=420px) and (height<=620px){.setup-runtime{grid-template-columns:minmax(0,1fr) max-content}.setup-runtime>div:first-child{grid-column:1/-1}.setup-runtime>div:not(:first-child){padding:6px 10px;display:block}.setup-runtime>div:not(:first-child) dd{margin-top:2px}}@media (width<=850px){:root{--header-height:calc(2 * var(--header-control-height) + 3 * var(--surface-inset));--header-inset:10px;--content-gutter:10px}header{grid-template-columns:auto minmax(0,1fr);grid-template-rows:repeat(2, var(--header-control-height));gap:var(--surface-inset);grid-template-areas:"brand runtime""model model"}.brand{grid-area:brand;justify-self:start}.runtime-meter{grid-area:runtime;justify-self:end}.model-panel{grid-area:model;margin:0}main{flex-direction:column;height:auto;display:flex}.column-resizer{display:none}.conversation{height:calc(100dvh - var(--header-height) - var(--header-inset));min-height:320px;padding:10px var(--content-gutter)}.workspace{min-height:520px;margin:0 var(--content-gutter) 10px}.file-area{grid-template-columns:115px minmax(0,1fr);min-height:280px}h1{font-size:20px}.badge,footer{font-size:9px}}@media (pointer:coarse){:root{--header-control-height:38px}button,.button,#files button{min-height:38px}.editor-heading{min-height:40px}.terminal summary{padding-top:11px;padding-bottom:11px}#command,#command-highlight,#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}
|
|
|
|
| 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;--surface-inset:8px;--radius-small:8px;--radius:calc(var(--radius-small) + var(--surface-inset));--radius-large:calc(var(--radius) + var(--surface-inset));--content-width:1800px;--content-gutter:14px;--header-inset:12px;--header-control-height:32px;--header-height:calc(var(--header-control-height) + 2 * var(--surface-inset));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-radius:var(--radius-small);background:var(--control);min-height:28px;color:inherit;white-space:nowrap;border:1px solid #0000;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);color:var(--text)}.primary:hover{background:var(--selected)}#send.has-input:not(:disabled){color:#18181e;background:#fff}#send.has-input:not(:disabled):hover{background:#e8e8ed}.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{width:calc(100% - 2 * var(--content-gutter));max-width:calc(var(--content-width) - 2 * var(--content-gutter));height:var(--header-height);margin:var(--header-inset) auto 0;padding:var(--surface-inset);justify-content:space-between;align-items:center;gap:var(--surface-inset) 16px;background:var(--surface);border-radius:var(--radius);grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);display:grid;position:relative;overflow:hidden}.brand{color:inherit;align-items:center;gap:8px;font-size:15px;font-weight:600;text-decoration:none;display:flex}.pi-logo{flex:none;width:18px;height:18px;display:block}.brand{justify-self:start;padding:0 4px}.runtime-meter{--runtime-padding:10px;width:max-content;max-width:100%;height:var(--header-control-height);padding:5px var(--runtime-padding);border-radius:var(--radius-small);background:var(--control);white-space:nowrap;justify-self:center;align-items:center;font-size:11px;transition:width .22s cubic-bezier(.2,.8,.2,1);display:flex;overflow:clip}.runtime-content{flex:none;align-items:center;gap:8px;width:max-content;height:100%;display:flex}.runtime-location{flex:none;align-items:center;gap:6px;display:flex}.runtime-dot{background:var(--muted);border-radius:50%;width:5px;height:5px}.runtime-meter[data-state=generating] .runtime-dot,.runtime-meter[data-state=tool] .runtime-dot{background:var(--accent)}.runtime-meter[data-state=error] .runtime-dot{background:#d98c8c}.runtime-divider{background:var(--line);flex:0 0 1px;align-self:stretch;margin:2px 0}.runtime-sparkline{color:var(--accent);flex:none}.runtime-value{color:var(--muted);font-variant-numeric:tabular-nums;flex:none}.runtime-meter[data-state=generating] .runtime-value{color:var(--accent)}@media (prefers-reduced-motion:reduce){.runtime-meter{transition:none}}main{--conversation-min:420px;--workspace-min:370px;grid-template-columns:minmax(var(--conversation-min), var(--conversation-width,1.20482fr)) 0 minmax(var(--workspace-min), 1fr);height:calc(100dvh - var(--header-height) - var(--header-inset));max-width:var(--content-width);margin:auto;display:grid}.column-resizer{left:calc(-1 * var(--content-gutter));z-index:2;width:var(--content-gutter);cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;outline:none;justify-content:center;align-items:center;margin:12px 0 8px;display:flex;position:relative}.column-resizer:after{content:"";background:var(--line-strong);border-radius:999px;flex:none;width:3px;height:44px;transition:background-color .12s}.column-resizer:hover:after{background:var(--text)}.column-resizer:focus-visible:after{background:var(--accent);outline:2px solid var(--accent);outline-offset:4px}.is-resizing-columns,.is-resizing-columns *{cursor:col-resize!important;-webkit-user-select:none!important;user-select:none!important}.is-resizing-columns .column-resizer:after{background:var(--accent)}@media (prefers-reduced-motion:reduce){.column-resizer:after{transition:none}}@media (forced-colors:active){.column-resizer:after{background:buttontext}}.conversation{min-width:0;min-height:0;padding:12px var(--content-gutter) 8px;flex-direction:column;display:flex}.model-panel{background:var(--surface);margin-left:auto}.model-heading{justify-content:space-between;align-items:center;gap:8px;display:flex}.model-heading>div{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.model-heading strong{font-size:12px;font-weight:600}.header-model-source{color:inherit;white-space:nowrap;border-radius:3px;flex:none;align-items:center;gap:6px;text-decoration:none;display:inline-flex}.header-model-source img{border-radius:50%;flex:none;width:16px;height:16px;display:block}.header-model-source:hover{color:var(--accent)}.header-model-source:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.badge{border-radius:var(--radius-small);background:var(--control);color:var(--muted);padding:3px 7px;font-size:10px}#load{min-width:90px;height:var(--header-control-height);flex-shrink:0}#load.is-loaded{opacity:1;min-width:0;height:28px;min-height:28px;color:var(--accent);background:#25312f;border-radius:6px;justify-content:center;align-items:center;gap:6px;padding:4px 8px;display:inline-flex}#load.is-loaded:before{content:"";background:currentColor;border-radius:50%;flex:none;width:5px;height:5px}progress{width:100%;height:5px;accent-color:var(--accent);margin-top:8px}header #progress{bottom:3px;left:var(--radius);width:calc(100% - 2 * var(--radius));appearance:none;background:var(--line);--progress-fade:clamp(24px, 5vw, 72px);--progress-mask:linear-gradient(90deg, transparent 2px, #000 var(--progress-fade), #000 calc(100% - var(--progress-fade)), transparent calc(100% - 2px));height:2px;-webkit-mask-image:var(--progress-mask);mask-image:var(--progress-mask);-webkit-mask-image:var(--progress-mask);border:0;border-radius:999px;margin:0;position:absolute;overflow:hidden}header #progress::-webkit-progress-bar{background:var(--line);border-radius:999px}header #progress::-webkit-progress-value{background:var(--accent);border-radius:999px}header #progress::-moz-progress-bar{background:var(--accent);border-radius:999px}header #progress:indeterminate{background:linear-gradient(90deg, transparent, var(--accent), transparent) 0 0 / 35% 100% no-repeat var(--line);animation:1.4s ease-in-out infinite alternate header-progress}header #progress:indeterminate::-webkit-progress-bar{background:0 0}header #progress:indeterminate::-webkit-progress-value{background:0 0}header #progress:indeterminate::-moz-progress-bar{background:0 0}@keyframes header-progress{0%{background-position:0 0}to{background-position:100% 0}}@media (prefers-reduced-motion:reduce){header #progress:indeterminate{background-position:50% 0;animation:none}}.muted{color:var(--muted);margin-top:4px;font-size:11px}.muted:empty{display:none}.terminal-panel{background:var(--background);border-radius:var(--radius-large);border:1px solid #0000;flex-direction:column;flex:1;min-height:0;padding:8px;display:flex}@media (width>=851px){.terminal-panel{padding-right:0}}#pi-terminal{isolation:isolate;background:var(--background);flex:1;min-width:0;min-height:120px;position:relative;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-paws{pointer-events:none;-webkit-user-select:none;user-select:none;position:absolute;overflow:hidden}.terminal-paws i{background:var(--muted);opacity:0;width:24px;height:24px;position:absolute;transform:rotate(28deg);-webkit-mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat}.terminal-paws:not([hidden]) i{animation:3.2s ease-in-out both terminal-paw;animation-delay:var(--delay)}.terminal-paws i:first-child{left:calc(18% + var(--trail-shift));--delay:0s;top:85%}.terminal-paws i:nth-child(2){left:calc(26% + var(--trail-shift));--delay:.65s;top:78%}.terminal-paws i:nth-child(3){left:calc(27% + var(--trail-shift));--delay:1.3s;top:68%}.terminal-paws i:nth-child(4){left:calc(35% + var(--trail-shift));--delay:1.95s;top:61%}.terminal-paws i:nth-child(5){left:calc(36% + var(--trail-shift));--delay:2.6s;top:51%}.terminal-paws i:nth-child(6){left:calc(44% + var(--trail-shift));--delay:3.25s;top:44%}@keyframes terminal-paw{0%,to{opacity:0}15%,40%{opacity:.14}}@media (width<=600px){.terminal-paws i{width:18px;height:18px}}@media (prefers-reduced-motion:reduce){.terminal-paws{display:none}}.empty-mascot{pointer-events:none;flex-direction:column;align-items:flex-end;gap:10px;width:196px;max-width:calc(100% - 24px);display:flex;position:absolute;right:12px}.mascot-cat{pointer-events:auto;cursor:pointer;background:0 0;border:0;border-radius:0;flex:none;min-height:0;padding:0;line-height:0;display:block;position:relative}.mascot-cat:hover{background:0 0}.mascot-cat,.setup-cat{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;outline:none}.mascot-cat,.mascot-cat *,.setup-cat,.setup-cat *{-webkit-user-select:none;user-select:none;-webkit-user-drag:none}.mascot-cat>*,.setup-cat>*{pointer-events:none}.empty-mascot img{object-fit:contain;width:100%;height:100%;image-rendering:pixelated;-webkit-user-select:none;user-select:none;clip-path:polygon(16.906% 6.22%,26.077% 6.22%,26.077% 9.729%,30.861% 9.729%,30.861% 14.274%,35.407% 14.274%,35.407% 20.175%,56.539% 20.175%,56.539% 14.274%,61.404% 14.274%,61.404% 9.729%,65.71% 9.729%,65.71% 6.22%,75.04% 6.22%,75.04% 44.498%,68.82% 44.498%,68.82% 48.166%,75.04% 48.166%,75.04% 52.073%,68.82% 52.073%,68.82% 57.576%,65.152% 57.576%,65.152% 61.324%,68.979% 61.324%,68.979% 67.225%,73.285% 67.225%,73.285% 77.033%,77.671% 77.033%,77.671% 55.742%,81.818% 55.742%,81.818% 51.595%,91.228% 51.595%,91.228% 63.397%,86.124% 63.397%,86.124% 85.566%,81.579% 85.566%,81.579% 92.265%,25.04% 92.265%,25.04% 85.566%,28.07% 85.566%,28.07% 57.576%,19.856% 57.576%,19.856% 52.073%,12.52% 52.073%,12.52% 48.166%,19.856% 48.166%,19.856% 44.498%,12.52% 44.498%,12.52% 41.467%,16.906% 41.467%);display:block}.mascot-eyelids{opacity:0;position:absolute;inset:0}.mascot-eyelids:before,.mascot-eyelids:after{content:"";background:linear-gradient(to bottom, #f6f0e1 70%, var(--background) 70%, var(--background) 86%, #f6f0e1 86%);width:7%;height:7.4%;position:absolute;top:39.3%}.mascot-eyelids:before{left:28.5%}.mascot-eyelids:after{left:54%}.mascot-suggestion{pointer-events:auto;color:#24242c;text-align:left;white-space:normal;background:#f5efdf;border:0;border-radius:8px;padding:10px 12px;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;position:relative;box-shadow:0 3px #0c0c10}.mascot-suggestion:after{content:"";background:inherit;clip-path:polygon(0 0,100% 0,100% 50%,50% 50%,50% 100%,0 100%);width:12px;height:8px;position:absolute;bottom:-8px;right:52px}.mascot-suggestion:hover{background:#fffaf0}.terminal-controls{justify-content:flex-end;align-items:center;gap:8px;padding-top:4px;display:flex}.terminal-controls,.terminal-panel>footer{cursor:text}#new-chat{margin-right:auto}#newline:not(:hover){background:0 0}#notice{border-radius:var(--radius-small);background:var(--control);margin:6px 0;padding:8px 10px;font-size:12px}footer{color:var(--muted);justify-content:space-between;align-items:center;gap:8px;padding-top:8px;font-size:10px;display:flex}.workspace{min-width:0;min-height:0;margin:12px var(--content-gutter) 8px 0;background:var(--panel);border-radius:20px;flex-direction:column;display:flex;overflow:hidden}.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}.workspace-heading{flex-wrap:wrap}.folder-controls{flex-wrap:wrap;gap:4px;padding:0 10px 7px;display:flex}.folder-controls:has(>button:not([hidden])){min-height:30px}.folder-controls:not(:has(>button:not([hidden]))){display:none}#open-folder{color:var(--accent,#8abeb7);background:#8abeb710}#local-folder-bar{background:#8abeb710;border-radius:10px;margin:0 8px 8px;padding:10px;font-size:11px}#local-folder-name{color:var(--accent,#8abeb7);overflow-wrap:anywhere;display:block}#local-folder-hint{color:var(--muted);margin:4px 0 8px;line-height:1.5;display:block}.folder-actions{flex-wrap:wrap;gap:5px;display:flex}.folder-actions button{padding:6px 9px;font-size:11px}#local-review{box-sizing:border-box;background:var(--bg,#18181e);width:min(820px,100vw - 24px);max-height:calc(100dvh - 32px);color:var(--text);border:1px solid #8abeb740;border-radius:18px;padding:20px}#local-review::backdrop{-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#0009}.review-heading{justify-content:space-between;align-items:center;gap:12px;display:flex}.review-heading h2{margin:0;font-size:20px}#review-close{padding:2px 10px;font-size:24px}#review-description{color:var(--muted);font-size:13px;line-height:1.5}#review-files{max-height:55dvh;overflow:auto}#review-files details{border:1px solid #ffffff18;border-radius:8px;margin:8px 0;overflow:hidden}#review-files summary{cursor:pointer;overflow-wrap:anywhere;padding:10px;font:12px ui-monospace,monospace}#review-files pre{tab-size:2;background:#0003;margin:0;padding:10px;font:12px/1.55 ui-monospace,monospace;overflow:auto}.diff-add{color:#98dfb4}.diff-remove{color:#f2a6a6}#review-error{color:#f2a6a6;font-size:13px;line-height:1.5}.review-actions{flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:16px;display:flex}.button{display:inline-block}.file-area{flex:1;grid-template-columns:140px minmax(0,1fr);min-height:180px;display:grid}#files{padding:5px 4px;overflow:auto}#files button{border-radius:var(--radius-small);text-align:left;white-space:normal;overflow-wrap:anywhere;background:0 0;border:none;align-items:center;gap:6px;width:100%;padding:5px 7px;font-family:ui-monospace,monospace;font-size:11px;line-height:1.45;display:flex}.file-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.file-icon{background-color:var(--file-color);width:16px;height:16px;-webkit-mask:var(--file-icon) center / 90% 90% no-repeat;mask:var(--file-icon) center / 90% 90% no-repeat;-webkit-mask:var(--file-icon) center / 90% 90% no-repeat;flex:0 0 16px;display:inline-block}.editor-file{align-items:center;gap:6px;min-width:0;display:flex}#filetype{flex:none;display:flex}#filetype:empty{display:none}@media (forced-colors:active){.file-icon{background-color:canvastext}}#files button:hover{background:var(--hover)}#files button.selected{background:var(--selected);color:var(--text)}.editor-area{border-radius:var(--radius);background:var(--surface);flex-direction:column;min-width:0;min-height:0;margin:0 8px 8px 0;display:flex;overflow:hidden}.editor-heading{background:#282832;justify-content:space-between;align-items:center;gap:6px;height:32px;padding:0 6px 0 10px;transition:background-color .14s,box-shadow .14s;display:flex}.editor-area:focus-within .editor-heading{background:#30323a;box-shadow:inset 0 -1px #8abeb74d}@media (prefers-reduced-motion:reduce){.editor-heading{transition:none}}@media (forced-colors:active){.editor-area:focus-within .editor-heading{outline-offset:-1px;outline:1px solid highlight}}#filename{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,monospace;font-size:11px;overflow:hidden}.editor-heading{flex-shrink:0}.editor-input{flex:1;min-width:0;min-height:140px;position:relative;overflow:hidden}#editor,#editor-highlight{width:100%;height:100%;color:var(--text);font-variant-ligatures:none;letter-spacing:normal;tab-size:2;white-space:pre;overflow-wrap:normal;border:0;border-radius:0;margin:0;padding:10px 12px;font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace;position:absolute;top:0;left:0}#editor{resize:none;caret-color:var(--text);background:0 0;overflow:auto}#editor-highlight{visibility:hidden;pointer-events:none;overflow:hidden}.is-highlighted #editor-highlight{visibility:visible}.is-highlighted #editor{color:#0000}.is-highlighted #editor::selection{color:#0000}#editor::selection{background:#8abeb740}#editor:focus{box-shadow:none}#editor-highlight .token{font-style:normal;font-weight:400}#editor-highlight .token.comment,#editor-highlight .token.prolog,#editor-highlight .token.doctype{color:#9292a3}#editor-highlight .token.punctuation{color:#b0b0bd}#editor-highlight .token.tag,#editor-highlight .token.deleted{color:#e29a9a}#editor-highlight .token.keyword,#editor-highlight .token.selector,#editor-highlight .token.important{color:#c4a7d5}#editor-highlight .token.string,#editor-highlight .token.attr-value,#editor-highlight .token.inserted{color:#b5bd68}#editor-highlight .token.function,#editor-highlight .token.builtin,#editor-highlight .token.class-name{color:#8abeb7}#editor-highlight .token.attr-name,#editor-highlight .token.property,#editor-highlight .token.variable{color:#9db9d4}#editor-highlight .token.number,#editor-highlight .token.boolean,#editor-highlight .token.constant,#editor-highlight .token.operator,#editor-highlight .token.entity,#editor-highlight .token.url{color:#f0c674}@media (forced-colors:active){.is-highlighted #editor-highlight{visibility:hidden}.is-highlighted #editor{color:canvastext}.is-highlighted #editor::selection{color:canvastext}}.workspace>.terminal{border-radius:var(--radius);background:var(--background);flex-shrink:0;margin:0 8px;padding:0 8px 8px}.terminal summary{cursor:pointer;padding:6px 0;font-size:11px;font-weight:500}.terminal summary span{float:right;color:var(--muted);font-size:10px;font-weight:400}.shell-toolbar{color:var(--accent);justify-content:space-between;align-items:center;font:11px/1.5 ui-monospace,monospace;display:flex}.shell-toolbar button{color:var(--muted)}#terminal-output{resize:vertical;scrollbar-width:thin;overflow-wrap:anywhere;height:132px;min-height:84px;max-height:min(480px,45dvh);margin:0 0 6px;padding:2px 0;font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:auto}#terminal-output:focus-visible{outline:1px solid var(--accent);outline-offset:2px;border-radius:var(--radius-small)}.shell-empty{color:var(--muted);flex-direction:column;gap:4px;padding:8px 2px;display:flex}.shell-empty strong{color:var(--text);font-weight:500}.shell-empty code{color:var(--accent)}.shell-entry{border-radius:var(--radius-small);background:var(--surface);margin-bottom:6px;padding:8px}.shell-entry:last-child{margin-bottom:0}.shell-entry-heading{align-items:baseline;gap:8px;display:flex}.shell-entry pre{font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;margin:0}.shell-code{flex:1;min-width:0}.shell-status{color:var(--muted);flex-shrink:0;font-size:10px}.shell-status.success{color:#b5bd68}.shell-status.error,.shell-stderr{color:#e29a9a}.shell-entry .shell-stdout,.shell-entry .shell-stderr{margin:6px 0 0 16px}#shell-form{border-radius:var(--radius-small);background:var(--control);align-items:flex-start;gap:8px;padding:4px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;display:flex}#shell-form:focus-within{box-shadow:0 0 0 1px var(--accent)}.shell-prompt{color:var(--accent);flex-shrink:0}#shell-form>.shell-prompt{padding-top:6px;line-height:20px}.shell-input{flex:1;min-width:0;position:relative}#command,#command-highlight{font-variant-ligatures:none;letter-spacing:normal;tab-size:2;white-space:pre-wrap;overflow-wrap:anywhere;border:0;border-radius:0;width:100%;margin:0;padding:6px 0;font:12px/20px ui-monospace,SFMono-Regular,Consolas,monospace}#command-highlight{pointer-events:none;position:absolute;inset:0;overflow:hidden}#command{resize:none;color:#0000;min-height:32px;max-height:104px;caret-color:var(--text);scrollbar-width:thin;background:0 0;display:block;position:relative}#command:focus{box-shadow:none}#command::selection{color:#0000;background:#8abeb740}#command::placeholder{color:var(--muted)}#shell-run{border-radius:4px;min-height:32px}#shell-hint{color:var(--muted);flex-wrap:wrap;justify-content:space-between;gap:2px 12px;padding-top:5px;font-size:10px;line-height:1.5;display:flex}.shell-code .token,#command-highlight .token{font-style:normal;font-weight:400}.shell-code .token.comment,#command-highlight .token.comment{color:#aaaab7}.shell-code .token.string,#command-highlight .token.string{color:#b5bd68}.shell-code .token.function,.shell-code .token.builtin,#command-highlight .token.function,#command-highlight .token.builtin{color:#8abeb7}.shell-code .token.variable,.shell-code .token.assign-left,#command-highlight .token.variable,#command-highlight .token.assign-left{color:#c4a7d5}.shell-code .token.keyword,.shell-code .token.operator,.shell-code .token.number,#command-highlight .token.keyword,#command-highlight .token.operator,#command-highlight .token.number{color:#f0c674}.shell-code .token.parameter,#command-highlight .token.parameter{color:#9db9d4}@media (forced-colors:active){#command-highlight{display:none}#command{color:canvastext}#command::selection{color:canvastext}}.workspace-note{color:var(--muted);margin:6px 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{--dialog-inset:24px;--cat-size:152px;--cat-rise:calc(var(--cat-size) * .58);width:min(460px,100% - 32px);max-height:calc(100dvh - 32px - var(--cat-rise));padding:var(--dialog-inset);border-radius:calc(var(--radius-small) + var(--dialog-inset));background:var(--surface);color:var(--text);transform:translateY(calc(var(--cat-rise) / 2));border:0;overflow:visible;box-shadow:0 24px 80px #0008}#model-dialog[open]{flex-direction:column;display:flex}.setup-paws{--modal-half:min(230px, calc(50vw - 16px));left:50%;top:calc(50% - var(--cat-rise) / 2);pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;width:100vw;height:100dvh;position:absolute;overflow:hidden;transform:translate(-50%,-50%)}.setup-paws i{background:var(--muted);opacity:0;width:28px;height:28px;transform:rotate(var(--angle));position:absolute;-webkit-mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cg%20fill='black'%3e%3cpath%20d='M2%207h4v5H2zM7%202h4v6H7zM13%202h4v6h-4zM18%207h4v5h-4zM8%2012h8v2h3v6h-4v-1H9v1H5v-6h3z'/%3e%3c/g%3e%3c/svg%3e") 50%/contain no-repeat}#model-dialog[open] .setup-paws i{animation:10s ease-in-out infinite setup-paw;animation-delay:var(--delay);animation-play-state:var(--cat-play-state,running)}.setup-paws i:first-child{left:calc(50% - var(--modal-half) - clamp(48px, 9vw, 136px));--angle:-34deg;--delay:-1s;top:68%}.setup-paws i:nth-child(2){left:calc(50% - var(--modal-half) - clamp(30px, 6vw, 88px));--angle:-24deg;--delay:-.3s;top:60%}.setup-paws i:nth-child(3){left:calc(50% - var(--modal-half) - clamp(45px, 8vw, 120px));--angle:-12deg;--delay:.4s;top:52%}.setup-paws i:nth-child(4){left:calc(50% + var(--modal-half) + clamp(25px, 4vw, 64px));--angle:28deg;--delay:4s;top:46%}.setup-paws i:nth-child(5){left:calc(50% + var(--modal-half) + clamp(50px, 8vw, 120px));--angle:35deg;--delay:4.7s;top:38%}.setup-paws i:nth-child(6){left:calc(50% + var(--modal-half) + clamp(35px, 7vw, 100px));--angle:25deg;--delay:5.4s;top:30%}@keyframes setup-paw{0%,5%,38%,to{opacity:0}14%,24%{opacity:.16}}@media (width<=600px){.setup-paws i{width:18px;height:18px}.setup-paws i:first-child{--angle:65deg;top:22px;left:8%}.setup-paws i:nth-child(2){--angle:75deg;top:14px;left:19%}.setup-paws i:nth-child(3){--angle:65deg;top:22px;left:30%}.setup-paws i:nth-child(n+4){display:none}}@media (prefers-reduced-motion:reduce){#model-dialog[open] .setup-paws i{animation:none}.setup-paws i:first-child,.setup-paws i:nth-child(2){opacity:.1}}.setup-cat{top:calc(-1 * var(--cat-rise));right:var(--dialog-inset);width:var(--cat-size);height:calc(var(--cat-size) * .66);pointer-events:auto;cursor:pointer;touch-action:manipulation;-webkit-user-select:none;user-select:none;position:absolute;overflow:hidden}.setup-cat img,.setup-cat-tail,.setup-cat-eyelids{top:calc(var(--cat-size) * -.23);width:100%;height:auto;image-rendering:pixelated;opacity:0;display:block;position:absolute;left:0}.setup-cat img{clip-path:inset(0 22% 0 0)}.setup-cat-tail{aspect-ratio:1;background:var(--cat-sprite) 0 0 / 100% 100% no-repeat;clip-path:polygon(78% 40%,100% 40%,100% 82%,77% 82%,77% 77%,78% 77%);transform-origin:78% 80%}.setup-cat.is-ready img,.setup-cat.is-ready .setup-cat-tail{opacity:1}.setup-cat-eyelids{aspect-ratio:1}.setup-cat-eyelids:before,.setup-cat-eyelids:after{content:"";background:linear-gradient(#080709,#080709) 50% 80%/78% 14% no-repeat,#f6efdf;width:8.4%;height:8.7%;position:absolute;top:55.6%}.setup-cat-eyelids:before{left:28%}.setup-cat-eyelids:after{left:54.3%}#model-dialog[open] .setup-cat.is-ready .setup-cat-tail{animation:9s ease-in-out infinite setup-tail-swish;animation-play-state:var(--cat-play-state,running)}#model-dialog[open] .setup-cat.is-ready .setup-cat-eyelids{animation:7.4s step-end infinite setup-cat-blink;animation-play-state:var(--cat-play-state,running)}@keyframes setup-cat-blink{0%,45%,to{opacity:0}43%{opacity:1}}@keyframes setup-tail-swish{0%,24%,50%,to{transform:rotate(0)}33%{transform:rotate(-4deg)}41%{transform:rotate(2.5deg)}46%{transform:rotate(-.6deg)}}@media (prefers-reduced-motion:reduce){#model-dialog[open] .setup-cat.is-ready .setup-cat-tail,#model-dialog[open] .setup-cat.is-ready .setup-cat-eyelids{animation:none}}.setup-heading{flex-shrink:0;align-items:flex-start;gap:8px;display:flex}#model-cancel{width:32px;height:32px;min-height:32px;color:var(--muted);background:0 0;flex:none;place-items:center;padding:0;display:grid}#model-cancel:hover:not(:disabled){background:var(--control);color:var(--text)}.setup-body{min-height:0;overflow:auto}#model-dialog::backdrop{-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#08080dbd}#model-title{letter-spacing:-.5px;flex:1;min-width:0;margin:0 0 8px;font-size:27px;font-weight:650;line-height:1.2}#model-description{color:var(--muted);margin:0;line-height:1.6}.setup-runtime{grid-template-columns:minmax(max-content,1fr) 1fr 1.2fr;gap:8px;margin:18px 0;display:grid}.setup-runtime>div{border-radius:var(--radius-small);background:var(--control);padding:10px}.setup-runtime dt{color:var(--accent);font-size:12px;font-weight:600}.setup-runtime dd{margin:5px 0 0;font-size:11px;line-height:1.5}.setup-model{align-items:center;gap:6px;display:flex}.model-org{border-radius:50%;flex:none;display:inline-flex}.model-org img{border-radius:inherit;width:18px;height:18px;display:block}.model-source{color:inherit;-webkit-text-decoration-color:var(--muted);text-decoration-color:var(--muted);text-underline-offset:3px;white-space:nowrap}.model-source:hover{color:var(--accent)}.setup-model a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}.setup-facts{border-radius:var(--radius-small);background:var(--panel);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{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;border-radius:var(--radius-small);background:var(--control);max-height:84px;padding:10px 12px;font-size:12px;line-height:1.5;overflow:auto}#model-error{border-radius:var(--radius-small);overflow-wrap:anywhere;background:#3c2828;padding:10px 12px;line-height:1.5}#model-open-tab{color:var(--accent);white-space:normal;text-decoration:none}.setup-credits{color:var(--muted);flex-wrap:wrap;align-items:center;gap:6px 10px;margin:18px 0;font-size:11px;line-height:1.6;display:flex}.setup-credits a{color:inherit;white-space:nowrap;align-items:center;gap:4px;text-decoration:none;display:inline-flex}.setup-credits a:hover{color:var(--text);text-underline-offset:3px;text-decoration:underline}.setup-credits a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}.setup-credits img,.setup-credits svg{object-fit:contain;flex:none;width:14px;height:14px}.setup-credits .credit-pi{padding:2px}.setup-actions{flex-shrink:0;padding-top:4px;display:flex}.setup-actions button{width:100%;min-height:44px;padding:10px 16px}#model-confirm{color:#18181e;background:#fff;font-size:14px;font-weight:600}#model-confirm:hover{background:#e8e8ed}@media (width<=420px){#model-dialog{--dialog-inset:16px;--cat-size:116px}#model-title{font-size:24px}.setup-runtime{grid-template-columns:1fr;gap:5px;margin:14px 0}.setup-runtime>div{align-items:baseline;gap:12px;padding:8px 10px;display:flex}.setup-runtime dt{flex:0 0 64px}.setup-runtime dd{margin:0}.setup-facts{margin:14px 0}.setup-credits{margin:12px 0}}@media (width>=851px) and (width<=1000px){header{grid-template-columns:auto minmax(0,1fr) auto}}@media (height<=620px){#model-dialog{--cat-size:72px}.setup-runtime,.setup-facts{margin:12px 0}}@media (width<=420px) and (height<=620px){.setup-runtime{grid-template-columns:minmax(0,1fr) max-content}.setup-runtime>div:first-child{grid-column:1/-1}.setup-runtime>div:not(:first-child){padding:6px 10px;display:block}.setup-runtime>div:not(:first-child) dd{margin-top:2px}}@media (width<=850px){:root{--header-height:calc(2 * var(--header-control-height) + 3 * var(--surface-inset));--header-inset:10px;--content-gutter:10px}header{grid-template-columns:auto minmax(0,1fr);grid-template-rows:repeat(2, var(--header-control-height));gap:var(--surface-inset);grid-template-areas:"brand runtime""model model"}.brand{grid-area:brand;justify-self:start}.runtime-meter{grid-area:runtime;justify-self:end}.model-panel{grid-area:model;margin:0}main{flex-direction:column;height:auto;display:flex}.column-resizer{display:none}.conversation{height:calc(100dvh - var(--header-height) - var(--header-inset));min-height:320px;padding:10px var(--content-gutter)}.workspace{min-height:520px;margin:0 var(--content-gutter) 10px}.file-area{grid-template-columns:115px minmax(0,1fr);min-height:280px}h1{font-size:20px}.badge,footer{font-size:9px}}@media (pointer:coarse){:root{--header-control-height:38px}button,.button,#files button{min-height:38px}.editor-heading{min-height:40px}.terminal summary{padding-top:11px;padding-bottom:11px}#command,#command-highlight,#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-wZi62oe2.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
index.html
CHANGED
|
@@ -21,8 +21,8 @@
|
|
| 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-
|
| 26 |
</head>
|
| 27 |
<body>
|
| 28 |
<header><a class="brand" href="./" aria-label="Pi"><svg class="pi-logo" viewBox="165.29 165.29 469.43 469.43" width="18" height="18" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/><path fill="currentColor" d="M517.36 400H634.72V634.72H517.36Z"/></svg></a>
|
|
@@ -55,6 +55,11 @@
|
|
| 55 |
<div id="column-resizer" class="column-resizer" role="separator" tabindex="0" aria-label="Resize chat and workspace" aria-orientation="vertical" aria-controls="conversation-panel workspace-panel" aria-valuemin="0" aria-valuemax="100" aria-valuenow="55" title="Drag to resize · Arrow keys to adjust · Double-click or Enter to reset"></div>
|
| 56 |
<aside id="workspace-panel" class="workspace" aria-label="Workspace">
|
| 57 |
<div class="workspace-heading"><h2>Workspace</h2><div><button id="add-file" class="quiet">+ File</button><label class="button quiet" for="upload">Import</label><input id="upload" type="file" multiple hidden><button id="export" class="quiet">Export</button></div></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
<div class="file-area"><nav id="files" aria-label="Files"></nav><div class="editor-area"><div class="editor-heading"><span class="editor-file"><span id="filetype"></span><span id="filename">Select a file</span></span><button id="save" class="quiet" disabled>Save</button></div><label for="editor" class="sr-only">File contents</label><div class="editor-input"><pre id="editor-highlight" aria-hidden="true"></pre><textarea id="editor" wrap="off" autocomplete="off" autocapitalize="off" spellcheck="false" disabled placeholder="Select a file to inspect or edit it."></textarea></div></div></div>
|
| 59 |
<details class="terminal" open>
|
| 60 |
<summary>Shell <span>just-bash</span></summary>
|
|
@@ -70,6 +75,11 @@
|
|
| 70 |
<p class="workspace-note">HTTP: curl can request any HTTPS URL. Browser CORS applies. <a href="./http-help.html" target="_blank" rel="noopener">HTTP help ↗</a></p>
|
| 71 |
</aside>
|
| 72 |
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
<dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
|
| 74 |
<div class="setup-paws" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
| 75 |
<div class="setup-cat" aria-hidden="true"><span class="setup-cat-tail"></span><img src="./modal-cat-v1.png" width="1254" height="1254" alt="" draggable="false" loading="eager" fetchpriority="high" decoding="async"><span class="setup-cat-eyelids"></span></div>
|
|
|
|
| 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-DUQ_UspG.js"></script>
|
| 25 |
+
<link rel="stylesheet" crossorigin href="./assets/index-Uy7sEXBC.css">
|
| 26 |
</head>
|
| 27 |
<body>
|
| 28 |
<header><a class="brand" href="./" aria-label="Pi"><svg class="pi-logo" viewBox="165.29 165.29 469.43 469.43" width="18" height="18" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/><path fill="currentColor" d="M517.36 400H634.72V634.72H517.36Z"/></svg></a>
|
|
|
|
| 55 |
<div id="column-resizer" class="column-resizer" role="separator" tabindex="0" aria-label="Resize chat and workspace" aria-orientation="vertical" aria-controls="conversation-panel workspace-panel" aria-valuemin="0" aria-valuemax="100" aria-valuenow="55" title="Drag to resize · Arrow keys to adjust · Double-click or Enter to reset"></div>
|
| 56 |
<aside id="workspace-panel" class="workspace" aria-label="Workspace">
|
| 57 |
<div class="workspace-heading"><h2>Workspace</h2><div><button id="add-file" class="quiet">+ File</button><label class="button quiet" for="upload">Import</label><input id="upload" type="file" multiple hidden><button id="export" class="quiet">Export</button></div></div>
|
| 58 |
+
<div class="folder-controls"><button id="open-folder" type="button" class="quiet">Open local folder</button><button id="reconnect-folder" type="button" class="quiet" hidden>Reconnect folder</button></div>
|
| 59 |
+
<div id="local-folder-bar" hidden>
|
| 60 |
+
<strong id="local-folder-name"></strong><span id="local-folder-hint" role="status"></span>
|
| 61 |
+
<div class="folder-actions"><button id="review-changes" type="button">Review & apply</button><button id="refresh-folder" type="button" class="quiet" title="Reload files from disk and start a fresh chat">Refresh</button><button id="disconnect-folder" type="button" class="quiet">Back to demo</button></div>
|
| 62 |
+
</div>
|
| 63 |
<div class="file-area"><nav id="files" aria-label="Files"></nav><div class="editor-area"><div class="editor-heading"><span class="editor-file"><span id="filetype"></span><span id="filename">Select a file</span></span><button id="save" class="quiet" disabled>Save</button></div><label for="editor" class="sr-only">File contents</label><div class="editor-input"><pre id="editor-highlight" aria-hidden="true"></pre><textarea id="editor" wrap="off" autocomplete="off" autocapitalize="off" spellcheck="false" disabled placeholder="Select a file to inspect or edit it."></textarea></div></div></div>
|
| 64 |
<details class="terminal" open>
|
| 65 |
<summary>Shell <span>just-bash</span></summary>
|
|
|
|
| 75 |
<p class="workspace-note">HTTP: curl can request any HTTPS URL. Browser CORS applies. <a href="./http-help.html" target="_blank" rel="noopener">HTTP help ↗</a></p>
|
| 76 |
</aside>
|
| 77 |
</main>
|
| 78 |
+
<dialog id="local-review" aria-labelledby="review-title" aria-describedby="review-description">
|
| 79 |
+
<div class="review-heading"><h2 id="review-title">Review local changes</h2><button id="review-close" type="button" aria-label="Close change review">×</button></div>
|
| 80 |
+
<p id="review-description"></p><div id="review-files"></div><p id="review-error" role="alert"></p>
|
| 81 |
+
<div class="review-actions"><button id="review-discard" type="button">Discard changes</button><button id="review-apply" type="button" class="primary">Apply to folder</button></div>
|
| 82 |
+
</dialog>
|
| 83 |
<dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
|
| 84 |
<div class="setup-paws" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
| 85 |
<div class="setup-cat" aria-hidden="true"><span class="setup-cat-tail"></span><img src="./modal-cat-v1.png" width="1254" height="1254" alt="" draggable="false" loading="eager" fetchpriority="high" decoding="async"><span class="setup-cat-eyelids"></span></div>
|
licenses/dependencies.txt
CHANGED
|
@@ -12765,6 +12765,202 @@ SOFTWARE.
|
|
| 12765 |
|
| 12766 |
|
| 12767 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12768 |
----------------------------------------
|
| 12769 |
|
| 12770 |
object-keys@1.1.1 (MIT)
|
|
|
|
| 12765 |
|
| 12766 |
|
| 12767 |
|
| 12768 |
+
----------------------------------------
|
| 12769 |
+
|
| 12770 |
+
node-gyp-build@4.8.4 (MIT)
|
| 12771 |
+
The MIT License (MIT)
|
| 12772 |
+
|
| 12773 |
+
Copyright (c) 2017 Mathias Buus
|
| 12774 |
+
|
| 12775 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 12776 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 12777 |
+
in the Software without restriction, including without limitation the rights
|
| 12778 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 12779 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 12780 |
+
furnished to do so, subject to the following conditions:
|
| 12781 |
+
|
| 12782 |
+
The above copyright notice and this permission notice shall be included in
|
| 12783 |
+
all copies or substantial portions of the Software.
|
| 12784 |
+
|
| 12785 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 12786 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 12787 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 12788 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 12789 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 12790 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 12791 |
+
THE SOFTWARE.
|
| 12792 |
+
|
| 12793 |
+
|
| 12794 |
+
----------------------------------------
|
| 12795 |
+
|
| 12796 |
+
node-liblzma@2.2.0 (LGPL-3.0)
|
| 12797 |
+
GNU LESSER GENERAL PUBLIC LICENSE
|
| 12798 |
+
Version 3, 29 June 2007
|
| 12799 |
+
|
| 12800 |
+
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
| 12801 |
+
Everyone is permitted to copy and distribute verbatim copies
|
| 12802 |
+
of this license document, but changing it is not allowed.
|
| 12803 |
+
|
| 12804 |
+
|
| 12805 |
+
This version of the GNU Lesser General Public License incorporates
|
| 12806 |
+
the terms and conditions of version 3 of the GNU General Public
|
| 12807 |
+
License, supplemented by the additional permissions listed below.
|
| 12808 |
+
|
| 12809 |
+
0. Additional Definitions.
|
| 12810 |
+
|
| 12811 |
+
As used herein, "this License" refers to version 3 of the GNU Lesser
|
| 12812 |
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
| 12813 |
+
General Public License.
|
| 12814 |
+
|
| 12815 |
+
"The Library" refers to a covered work governed by this License,
|
| 12816 |
+
other than an Application or a Combined Work as defined below.
|
| 12817 |
+
|
| 12818 |
+
An "Application" is any work that makes use of an interface provided
|
| 12819 |
+
by the Library, but which is not otherwise based on the Library.
|
| 12820 |
+
Defining a subclass of a class defined by the Library is deemed a mode
|
| 12821 |
+
of using an interface provided by the Library.
|
| 12822 |
+
|
| 12823 |
+
A "Combined Work" is a work produced by combining or linking an
|
| 12824 |
+
Application with the Library. The particular version of the Library
|
| 12825 |
+
with which the Combined Work was made is also called the "Linked
|
| 12826 |
+
Version".
|
| 12827 |
+
|
| 12828 |
+
The "Minimal Corresponding Source" for a Combined Work means the
|
| 12829 |
+
Corresponding Source for the Combined Work, excluding any source code
|
| 12830 |
+
for portions of the Combined Work that, considered in isolation, are
|
| 12831 |
+
based on the Application, and not on the Linked Version.
|
| 12832 |
+
|
| 12833 |
+
The "Corresponding Application Code" for a Combined Work means the
|
| 12834 |
+
object code and/or source code for the Application, including any data
|
| 12835 |
+
and utility programs needed for reproducing the Combined Work from the
|
| 12836 |
+
Application, but excluding the System Libraries of the Combined Work.
|
| 12837 |
+
|
| 12838 |
+
1. Exception to Section 3 of the GNU GPL.
|
| 12839 |
+
|
| 12840 |
+
You may convey a covered work under sections 3 and 4 of this License
|
| 12841 |
+
without being bound by section 3 of the GNU GPL.
|
| 12842 |
+
|
| 12843 |
+
2. Conveying Modified Versions.
|
| 12844 |
+
|
| 12845 |
+
If you modify a copy of the Library, and, in your modifications, a
|
| 12846 |
+
facility refers to a function or data to be supplied by an Application
|
| 12847 |
+
that uses the facility (other than as an argument passed when the
|
| 12848 |
+
facility is invoked), then you may convey a copy of the modified
|
| 12849 |
+
version:
|
| 12850 |
+
|
| 12851 |
+
a) under this License, provided that you make a good faith effort to
|
| 12852 |
+
ensure that, in the event an Application does not supply the
|
| 12853 |
+
function or data, the facility still operates, and performs
|
| 12854 |
+
whatever part of its purpose remains meaningful, or
|
| 12855 |
+
|
| 12856 |
+
b) under the GNU GPL, with none of the additional permissions of
|
| 12857 |
+
this License applicable to that copy.
|
| 12858 |
+
|
| 12859 |
+
3. Object Code Incorporating Material from Library Header Files.
|
| 12860 |
+
|
| 12861 |
+
The object code form of an Application may incorporate material from
|
| 12862 |
+
a header file that is part of the Library. You may convey such object
|
| 12863 |
+
code under terms of your choice, provided that, if the incorporated
|
| 12864 |
+
material is not limited to numerical parameters, data structure
|
| 12865 |
+
layouts and accessors, or small macros, inline functions and templates
|
| 12866 |
+
(ten or fewer lines in length), you do both of the following:
|
| 12867 |
+
|
| 12868 |
+
a) Give prominent notice with each copy of the object code that the
|
| 12869 |
+
Library is used in it and that the Library and its use are
|
| 12870 |
+
covered by this License.
|
| 12871 |
+
|
| 12872 |
+
b) Accompany the object code with a copy of the GNU GPL and this license
|
| 12873 |
+
document.
|
| 12874 |
+
|
| 12875 |
+
4. Combined Works.
|
| 12876 |
+
|
| 12877 |
+
You may convey a Combined Work under terms of your choice that,
|
| 12878 |
+
taken together, effectively do not restrict modification of the
|
| 12879 |
+
portions of the Library contained in the Combined Work and reverse
|
| 12880 |
+
engineering for debugging such modifications, if you also do each of
|
| 12881 |
+
the following:
|
| 12882 |
+
|
| 12883 |
+
a) Give prominent notice with each copy of the Combined Work that
|
| 12884 |
+
the Library is used in it and that the Library and its use are
|
| 12885 |
+
covered by this License.
|
| 12886 |
+
|
| 12887 |
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
| 12888 |
+
document.
|
| 12889 |
+
|
| 12890 |
+
c) For a Combined Work that displays copyright notices during
|
| 12891 |
+
execution, include the copyright notice for the Library among
|
| 12892 |
+
these notices, as well as a reference directing the user to the
|
| 12893 |
+
copies of the GNU GPL and this license document.
|
| 12894 |
+
|
| 12895 |
+
d) Do one of the following:
|
| 12896 |
+
|
| 12897 |
+
0) Convey the Minimal Corresponding Source under the terms of this
|
| 12898 |
+
License, and the Corresponding Application Code in a form
|
| 12899 |
+
suitable for, and under terms that permit, the user to
|
| 12900 |
+
recombine or relink the Application with a modified version of
|
| 12901 |
+
the Linked Version to produce a modified Combined Work, in the
|
| 12902 |
+
manner specified by section 6 of the GNU GPL for conveying
|
| 12903 |
+
Corresponding Source.
|
| 12904 |
+
|
| 12905 |
+
1) Use a suitable shared library mechanism for linking with the
|
| 12906 |
+
Library. A suitable mechanism is one that (a) uses at run time
|
| 12907 |
+
a copy of the Library already present on the user's computer
|
| 12908 |
+
system, and (b) will operate properly with a modified version
|
| 12909 |
+
of the Library that is interface-compatible with the Linked
|
| 12910 |
+
Version.
|
| 12911 |
+
|
| 12912 |
+
e) Provide Installation Information, but only if you would otherwise
|
| 12913 |
+
be required to provide such information under section 6 of the
|
| 12914 |
+
GNU GPL, and only to the extent that such information is
|
| 12915 |
+
necessary to install and execute a modified version of the
|
| 12916 |
+
Combined Work produced by recombining or relinking the
|
| 12917 |
+
Application with a modified version of the Linked Version. (If
|
| 12918 |
+
you use option 4d0, the Installation Information must accompany
|
| 12919 |
+
the Minimal Corresponding Source and Corresponding Application
|
| 12920 |
+
Code. If you use option 4d1, you must provide the Installation
|
| 12921 |
+
Information in the manner specified by section 6 of the GNU GPL
|
| 12922 |
+
for conveying Corresponding Source.)
|
| 12923 |
+
|
| 12924 |
+
5. Combined Libraries.
|
| 12925 |
+
|
| 12926 |
+
You may place library facilities that are a work based on the
|
| 12927 |
+
Library side by side in a single library together with other library
|
| 12928 |
+
facilities that are not Applications and are not covered by this
|
| 12929 |
+
License, and convey such a combined library under terms of your
|
| 12930 |
+
choice, if you do both of the following:
|
| 12931 |
+
|
| 12932 |
+
a) Accompany the combined library with a copy of the same work based
|
| 12933 |
+
on the Library, uncombined with any other library facilities,
|
| 12934 |
+
conveyed under the terms of this License.
|
| 12935 |
+
|
| 12936 |
+
b) Give prominent notice with the combined library that part of it
|
| 12937 |
+
is a work based on the Library, and explaining where to find the
|
| 12938 |
+
accompanying uncombined form of the same work.
|
| 12939 |
+
|
| 12940 |
+
6. Revised Versions of the GNU Lesser General Public License.
|
| 12941 |
+
|
| 12942 |
+
The Free Software Foundation may publish revised and/or new versions
|
| 12943 |
+
of the GNU Lesser General Public License from time to time. Such new
|
| 12944 |
+
versions will be similar in spirit to the present version, but may
|
| 12945 |
+
differ in detail to address new problems or concerns.
|
| 12946 |
+
|
| 12947 |
+
Each version is given a distinguishing version number. If the
|
| 12948 |
+
Library as you received it specifies that a certain numbered version
|
| 12949 |
+
of the GNU Lesser General Public License "or any later version"
|
| 12950 |
+
applies to it, you have the option of following the terms and
|
| 12951 |
+
conditions either of that published version or of any later version
|
| 12952 |
+
published by the Free Software Foundation. If the Library as you
|
| 12953 |
+
received it does not specify a version number of the GNU Lesser
|
| 12954 |
+
General Public License, you may choose any version of the GNU Lesser
|
| 12955 |
+
General Public License ever published by the Free Software Foundation.
|
| 12956 |
+
|
| 12957 |
+
If the Library as you received it specifies that a proxy can decide
|
| 12958 |
+
whether future versions of the GNU Lesser General Public License shall
|
| 12959 |
+
apply, that proxy's public statement of acceptance of any version is
|
| 12960 |
+
permanent authorization for you to choose that version for the
|
| 12961 |
+
Library.
|
| 12962 |
+
|
| 12963 |
+
|
| 12964 |
----------------------------------------
|
| 12965 |
|
| 12966 |
object-keys@1.1.1 (MIT)
|
scripts/stage.py
CHANGED
|
@@ -12,7 +12,7 @@ shutil.copytree(root / 'scripts', out / 'scripts')
|
|
| 12 |
(out / 'validation').mkdir()
|
| 13 |
for path in (root / 'validation').glob('*'):
|
| 14 |
# Keep temporary failures and screenshots containing HTTP echo metadata local.
|
| 15 |
-
if path.is_file() and path.name in ['source.json', 'deployment.json', 'http-shell-live.json', 'http-agent-live.json', 'http-final-smoke.json', 'http-embedded.json', 'http-any-https.json', 'REPORT.md', 'http-desktop.png', 'http-mobile.png', 'http-agent-fetch-count-write.png']:
|
| 16 |
destination = out / 'validation' / path.name
|
| 17 |
if path.suffix == '.json':
|
| 18 |
data = path.read_text()
|
|
|
|
| 12 |
(out / 'validation').mkdir()
|
| 13 |
for path in (root / 'validation').glob('*'):
|
| 14 |
# Keep temporary failures and screenshots containing HTTP echo metadata local.
|
| 15 |
+
if path.is_file() and path.name in ['source.json', 'deployment.json', 'http-shell-live.json', 'http-agent-live.json', 'http-final-smoke.json', 'http-embedded.json', 'http-any-https.json', 'REPORT.md', 'http-desktop.png', 'http-mobile.png', 'http-agent-fetch-count-write.png', 'LOCAL-FOLDER.md', 'local-folder-browser.json', 'local-folder-live.json', 'local-folder-agent-live.json', 'local-folder-native.png', 'local-folder-browser-review.png', 'local-folder-browser-390.png']:
|
| 16 |
destination = out / 'validation' / path.name
|
| 17 |
if path.suffix == '.json':
|
| 18 |
data = path.read_text()
|
validation/LOCAL-FOLDER.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Local folder validation
|
| 2 |
+
|
| 3 |
+
September 16, 2026. Duplicate only: `Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP`.
|
| 4 |
+
|
| 5 |
+
- Production build and 28 unit tests passed. Six 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 and virtual paths outside the mount. 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 |
+
|
| 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 |
+
|
| 12 |
+
These checks validate the adapter and UI. They are not a general model reliability benchmark. Multi-file application has no OS transaction and can partially succeed; errors retain pending changes and report the count applied.
|
validation/local-folder-browser-390.png
ADDED
|
Git LFS Details
|
validation/local-folder-browser-review.png
ADDED
|
Git LFS Details
|
validation/local-folder-browser.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"url": "http://<redacted IP>:4193/",
|
| 3 |
+
"date": "2026-09-16T14:16:47.286Z",
|
| 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 |
+
}
|
validation/local-folder-native.png
ADDED
|
Git LFS Details
|