Mike0021 commited on
Commit
e7c75bb
·
verified ·
1 Parent(s): 473337c

Add optional local-folder workspace with staged review and apply

Browse files
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: MiniCPM5-2B Pi HTTP experiment
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 HTTP requests (experimental)
11
  models:
12
  - openbmb/MiniCPM5-2B
13
  - Mike0021/MiniCPM5-2B-ONNX
@@ -19,10 +19,24 @@ tags:
19
  - just-bash
20
  ---
21
 
22
- # Pi browser HTTP 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
  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 &amp; 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
- async function snapshot() { return { version: 1, demoVersion: 4, files: await workspace.snapshot(), entries: await workspace.serialize(), messages: agent.state.messages }; }
 
 
 
23
  async function persist() {
24
  const state = await snapshot();
25
- try { await storage?.save(state); } catch (e) { emit({ type: 'persistence_error', error: 'Changes are in memory, but browser storage failed: ' + e.message }); }
26
- emit({ type: 'workspace', files: state.files });
 
 
 
 
 
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 && !dirty) editorUI.setFile(selected, files[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 = await rpc('snapshot'), url = URL.createObjectURL(new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }));
250
- const a = document.createElement('a'); a.href = url; a.download = 'pi-workspace.json'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
 
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('current');
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, 'current');
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-B--sYzmA.js"></script>
25
- <link rel="stylesheet" crossorigin href="./assets/index-Bl39c3Js.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,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 &amp; 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

  • SHA256: 14c9e9d7b77b38db406a4622514365c67d749d49182db7a2c42c484649c0dbe1
  • Pointer size: 130 Bytes
  • Size of remote file: 80.2 kB
validation/local-folder-browser-review.png ADDED

Git LFS Details

  • SHA256: 4475037df68600147417b62dc0e94f6aca52871b87f028ca1627a2f7c7ff7e48
  • Pointer size: 131 Bytes
  • Size of remote file: 123 kB
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

  • SHA256: 09a13bc9dea3d6b56435820dbb9cecba462d84d44622ca20c687c1756c287977
  • Pointer size: 130 Bytes
  • Size of remote file: 79.4 kB