thebird 1.2.28 → 1.2.29

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 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
 
@@ -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 'https://esm.sh/webjsx@0.0.73';
2
- import htm from 'https://esm.sh/htm@3';
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 contents = [];
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) contents.push({ role, parts: [{ text: m.content }] });
53
- continue;
54
- }
55
- if (Array.isArray(m.content)) {
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 contents;
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 streamGenerate(apiKey, model, convertMessages(messages), chunk => {
171
- full += chunk;
172
- streamEl.textContent = full;
173
- const l = this.querySelector('#msg-list');
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
- <script src="https://cdn.tailwindcss.com"></script>
11
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rippleui@1.12.1/dist/css/styles.css" />
12
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.css" />
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 'https://esm.sh/@webcontainer/api';
2
- import { Terminal } from 'https://esm.sh/@xterm/xterm';
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