thebird 1.2.28 → 1.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/CLAUDE.md +21 -0
- package/docs/agent-chat.js +55 -0
- package/docs/app.js +14 -51
- package/docs/index.html +3 -3
- package/docs/terminal.js +2 -3
- package/docs/vendor/rippleui.css +1 -0
- package/docs/vendor/tailwind.css +2 -0
- package/docs/vendor/ui-libs.js +2 -0
- package/docs/vendor/webcontainer.js +2 -0
- package/docs/vendor/xterm-bundle.js +53 -0
- package/docs/vendor/xterm.css +285 -0
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
5
|
### Added
|
|
6
|
+
- `docs/agent-chat.js`: Gemini function-calling agentic loop; tools `read_file`, `write_file`, `run_command` dispatch to `window.__debug.container` (WebContainer FS + spawn)
|
|
7
|
+
- `docs/app.js`: imports `agentGenerate` from `agent-chat.js`; chat `send()` now runs agentic tool loop; `window.__debug` constructor uses `Object.assign` merge to not overwrite terminal.js keys; `streamGenerate` removed; `convertMessages` simplified
|
|
8
|
+
|
|
9
|
+
### Added (prev)
|
|
6
10
|
- `docs/index.html`: GEMINI_API_KEY input + Run Agent button in Terminal tab toolbar for in-browser agent validation
|
|
7
11
|
- `docs/terminal.js`: `window.__debug.runAgent(key, task)` spawns `node agent.js` with env, pipes output to terminal, tracks `{ running, output, exitCode }` in `window.__debug.validation`
|
|
8
12
|
|
package/CLAUDE.md
CHANGED
|
@@ -82,6 +82,27 @@ Arguments parsed in index.js lines 88-115.
|
|
|
82
82
|
- Download errors logged with context
|
|
83
83
|
- Progress logged per view: `[1/4] front view...`
|
|
84
84
|
|
|
85
|
+
## docs/ Build System
|
|
86
|
+
|
|
87
|
+
All frontend dependencies are vendored to `docs/vendor/` to eliminate CDN brittleness.
|
|
88
|
+
|
|
89
|
+
**Vendored files:**
|
|
90
|
+
- `ui-libs.js` (7.7KB): webjsx + htm — bundled with esbuild
|
|
91
|
+
- `xterm-bundle.js` (345KB): @xterm/xterm + @xterm/addon-fit — bundled with esbuild
|
|
92
|
+
- `webcontainer.js` (12KB): @webcontainer/api — bundled with esbuild (no runtime CDN deps)
|
|
93
|
+
- `tailwind.css` (13KB): generated by @tailwindcss/cli v4, purged from `docs/**/*.{html,js}`
|
|
94
|
+
- `rippleui.css` (4.7MB): downloaded directly (includes Tailwind v3 preflight, not utilities)
|
|
95
|
+
- `xterm.css` (7KB): downloaded directly
|
|
96
|
+
|
|
97
|
+
**Regenerate:**
|
|
98
|
+
- esbuild bundles: install devDeps then run esbuild with entry files in project root
|
|
99
|
+
- Tailwind: `tailwindcss --input '_tw-input.css' --output docs/vendor/tailwind.css --minify` where input contains `@import "tailwindcss"`
|
|
100
|
+
- CSS files: fetch from jsdelivr directly
|
|
101
|
+
|
|
102
|
+
**Key detail**: esm.sh returns stub files by default — fetch the actual `.mjs` bundle URL paths directly when downloading from esm.sh.
|
|
103
|
+
|
|
104
|
+
**DevDeps**: esbuild, @tailwindcss/cli, @xterm/xterm, @xterm/addon-fit, @webcontainer/api, webjsx, htm.
|
|
105
|
+
|
|
85
106
|
## Development Constraints
|
|
86
107
|
|
|
87
108
|
- Max 200 lines per file (split before hitting limit)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const TOOLS = [
|
|
2
|
+
{ name: 'read_file', description: 'Read a file from the filesystem', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' } }, required: ['path'] } },
|
|
3
|
+
{ name: 'write_file', description: 'Write content to a file', parameters: { type: 'OBJECT', properties: { path: { type: 'STRING' }, content: { type: 'STRING' } }, required: ['path', 'content'] } },
|
|
4
|
+
{ name: 'run_command', description: 'Run a shell command', parameters: { type: 'OBJECT', properties: { command: { type: 'STRING' } }, required: ['command'] } },
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
const toolHandlers = {
|
|
8
|
+
read_file: async ({ path }) => {
|
|
9
|
+
const c = window.__debug.container;
|
|
10
|
+
if (!c) throw new Error('container not ready');
|
|
11
|
+
return await c.fs.readFile(path, 'utf-8');
|
|
12
|
+
},
|
|
13
|
+
write_file: async ({ path, content }) => {
|
|
14
|
+
const c = window.__debug.container;
|
|
15
|
+
if (!c) throw new Error('container not ready');
|
|
16
|
+
await c.fs.writeFile(path, content);
|
|
17
|
+
return 'written: ' + path;
|
|
18
|
+
},
|
|
19
|
+
run_command: async ({ command }) => {
|
|
20
|
+
const c = window.__debug.container;
|
|
21
|
+
if (!c) throw new Error('container not ready');
|
|
22
|
+
const proc = await c.spawn('sh', ['-c', command]);
|
|
23
|
+
let out = '';
|
|
24
|
+
await proc.output.pipeTo(new WritableStream({ write: d => { out += d; } }));
|
|
25
|
+
return out || '(no output)';
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const BASE = 'https://generativelanguage.googleapis.com/v1beta';
|
|
30
|
+
|
|
31
|
+
export async function agentGenerate(apiKey, model, contents, onChunk, onTool) {
|
|
32
|
+
while (true) {
|
|
33
|
+
const res = await fetch(`${BASE}/models/${model}:generateContent?key=${apiKey}`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ contents, tools: [{ functionDeclarations: TOOLS }], generationConfig: { maxOutputTokens: 8192 } }),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) throw new Error('Generate API ' + res.status + ': ' + await res.text());
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const parts = data.candidates?.[0]?.content?.parts || [];
|
|
41
|
+
const finish = data.candidates?.[0]?.finishReason;
|
|
42
|
+
for (const p of parts) if (p.text) onChunk(p.text);
|
|
43
|
+
const calls = parts.filter(p => p.functionCall);
|
|
44
|
+
if (finish === 'STOP' || calls.length === 0) break;
|
|
45
|
+
const toolResults = await Promise.all(calls.map(async p => {
|
|
46
|
+
const { name, args } = p.functionCall;
|
|
47
|
+
onTool(name, args);
|
|
48
|
+
let output;
|
|
49
|
+
try { output = String(await toolHandlers[name](args)); }
|
|
50
|
+
catch (e) { output = 'error: ' + e.message; }
|
|
51
|
+
return { functionResponse: { name, response: { output } } };
|
|
52
|
+
}));
|
|
53
|
+
contents = [...contents, { role: 'model', parts }, { role: 'user', parts: toolResults }];
|
|
54
|
+
}
|
|
55
|
+
}
|
package/docs/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createElement, applyDiff } from '
|
|
2
|
-
import
|
|
1
|
+
import { createElement, applyDiff, htm } from './vendor/ui-libs.js';
|
|
2
|
+
import { agentGenerate } from './agent-chat.js';
|
|
3
3
|
|
|
4
4
|
const html = htm.bind(createElement);
|
|
5
5
|
|
|
@@ -14,53 +14,18 @@ async function fetchModels(apiKey) {
|
|
|
14
14
|
.map(m => ({ id: m.name.replace('models/', ''), label: m.displayName || m.name }));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async function streamGenerate(apiKey, model, contents, onChunk) {
|
|
18
|
-
const res = await fetch(`${BASE}/models/${model}:streamGenerateContent?alt=sse&key=${apiKey}`, {
|
|
19
|
-
method: 'POST',
|
|
20
|
-
headers: { 'Content-Type': 'application/json' },
|
|
21
|
-
body: JSON.stringify({ contents, generationConfig: { maxOutputTokens: 8192, temperature: 0.7 } }),
|
|
22
|
-
});
|
|
23
|
-
if (!res.ok) throw new Error(`Generate API ${res.status}: ${await res.text()}`);
|
|
24
|
-
const reader = res.body.getReader();
|
|
25
|
-
const dec = new TextDecoder();
|
|
26
|
-
let buf = '';
|
|
27
|
-
while (true) {
|
|
28
|
-
const { done, value } = await reader.read();
|
|
29
|
-
if (done) break;
|
|
30
|
-
buf += dec.decode(value, { stream: true });
|
|
31
|
-
const lines = buf.split('\n');
|
|
32
|
-
buf = lines.pop();
|
|
33
|
-
for (const line of lines) {
|
|
34
|
-
if (!line.startsWith('data: ')) continue;
|
|
35
|
-
const json = line.slice(6).trim();
|
|
36
|
-
if (!json || json === '[DONE]') continue;
|
|
37
|
-
try {
|
|
38
|
-
const chunk = JSON.parse(json);
|
|
39
|
-
for (const c of (chunk.candidates || []))
|
|
40
|
-
for (const p of (c.content?.parts || []))
|
|
41
|
-
if (p.text && !p.thought) onChunk(p.text);
|
|
42
|
-
} catch {}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
17
|
function convertMessages(messages) {
|
|
48
|
-
const
|
|
18
|
+
const out = [];
|
|
49
19
|
for (const m of messages) {
|
|
50
20
|
const role = m.role === 'assistant' ? 'model' : 'user';
|
|
51
21
|
if (typeof m.content === 'string') {
|
|
52
|
-
if (m.content)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const parts = m.content.map(b => {
|
|
57
|
-
if (b.type === 'text' && b.text) return { text: b.text };
|
|
58
|
-
return null;
|
|
59
|
-
}).filter(Boolean);
|
|
60
|
-
if (parts.length) contents.push({ role, parts });
|
|
22
|
+
if (m.content) out.push({ role, parts: [{ text: m.content }] });
|
|
23
|
+
} else if (Array.isArray(m.content)) {
|
|
24
|
+
const parts = m.content.flatMap(b => b.type === 'text' && b.text ? [{ text: b.text }] : []);
|
|
25
|
+
if (parts.length) out.push({ role, parts });
|
|
61
26
|
}
|
|
62
27
|
}
|
|
63
|
-
return
|
|
28
|
+
return out;
|
|
64
29
|
}
|
|
65
30
|
|
|
66
31
|
class BirdChat extends HTMLElement {
|
|
@@ -72,11 +37,11 @@ class BirdChat extends HTMLElement {
|
|
|
72
37
|
models: [], modelsLoading: false, status: '', streamingText: '',
|
|
73
38
|
};
|
|
74
39
|
const self = this;
|
|
75
|
-
window.__debug = {
|
|
40
|
+
Object.assign(window.__debug = window.__debug || {}, {
|
|
76
41
|
get state() { return self.state; },
|
|
77
42
|
get messages() { return self.state.messages; },
|
|
78
43
|
get models() { return self.state.models; },
|
|
79
|
-
};
|
|
44
|
+
});
|
|
80
45
|
}
|
|
81
46
|
|
|
82
47
|
connectedCallback() {
|
|
@@ -167,12 +132,10 @@ class BirdChat extends HTMLElement {
|
|
|
167
132
|
wrap.appendChild(cursor);
|
|
168
133
|
const list = this.querySelector('#msg-list');
|
|
169
134
|
if (list) list.appendChild(wrap);
|
|
170
|
-
await
|
|
171
|
-
full += chunk;
|
|
172
|
-
streamEl.textContent = full;
|
|
173
|
-
|
|
174
|
-
if (l) l.scrollTop = l.scrollHeight;
|
|
175
|
-
});
|
|
135
|
+
await agentGenerate(apiKey, model, convertMessages(messages),
|
|
136
|
+
chunk => { full += chunk; streamEl.textContent = full; const l = this.querySelector('#msg-list'); if (l) l.scrollTop = l.scrollHeight; },
|
|
137
|
+
(name, args) => { full += `\n[tool: ${name}(${JSON.stringify(args)})]\n`; streamEl.textContent = full; }
|
|
138
|
+
);
|
|
176
139
|
wrap.remove();
|
|
177
140
|
this.setState({ messages: [...messages, { role: 'assistant', content: full || '(empty)' }], streaming: false, streamingText: '' });
|
|
178
141
|
const l2 = this.querySelector('#msg-list');
|
package/docs/index.html
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
<title>thebird — Gemini chat + terminal</title>
|
|
8
8
|
<script>window.coi = { coepDegrade: () => false };</script>
|
|
9
9
|
<script src="coi-serviceworker.js"></script>
|
|
10
|
-
<
|
|
11
|
-
<link rel="stylesheet" href="
|
|
12
|
-
<link rel="stylesheet" href="
|
|
10
|
+
<link rel="stylesheet" href="vendor/tailwind.css" />
|
|
11
|
+
<link rel="stylesheet" href="vendor/rippleui.css" />
|
|
12
|
+
<link rel="stylesheet" href="vendor/xterm.css" />
|
|
13
13
|
<style>
|
|
14
14
|
html, body { height: 100%; background: #0f1117; }
|
|
15
15
|
.tab-active { border-bottom: 2px solid oklch(var(--p)); }
|
package/docs/terminal.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { WebContainer } from '
|
|
2
|
-
import { Terminal } from '
|
|
3
|
-
import { FitAddon } from 'https://esm.sh/@xterm/addon-fit';
|
|
1
|
+
import { WebContainer } from './vendor/webcontainer.js';
|
|
2
|
+
import { Terminal, FitAddon } from './vendor/xterm-bundle.js';
|
|
4
3
|
|
|
5
4
|
const IDB_KEY = 'thebird_fs_v2';
|
|
6
5
|
|