thebird 1.2.92 → 1.2.93

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
@@ -1,4 +1,14 @@
1
1
 
2
+ ## [unreleased] 2026-04-21 theme system + richer chat output
3
+ - feat: dark/light theme via [data-theme] attribute; pre-paint boot script respects localStorage + prefers-color-scheme
4
+ - feat: theme toggle button (◐) in tabs row; window.setTheme / toggleTheme / __debug.theme
5
+ - feat: xterm terminal re-themes live on tui-theme-change event (reads CSS vars)
6
+ - feat: preview iframe + "no files yet" placeholder inherit current theme
7
+ - fix: select arrow uses CSS gradients instead of hardcoded-fill SVG — theme-reactive
8
+ - feat: tool-event now renders output + error inline (not just input)
9
+ - feat: tool-event sig includes input length + output presence — multiple state snapshots during streaming
10
+ - feat: unknown-part carries raw part.text for diagnostic visibility
11
+
2
12
  ## [unreleased] 2026-04-21 chat observability — rich ACP/kilo/opencode event stream
3
13
  - feat: kilo-http-stream emits status, model-info, reasoning-delta, tool-event, file-event, step-start/finish, file-mirrored, unknown-part
4
14
  - feat: PART_HANDLERS dispatch table replaces part-type branching (kilo + opencode unified)
@@ -37,17 +37,24 @@ export async function fetchModels(providerType, baseUrl, apiKey) {
37
37
  const fmtArgs = a => { try { const s = JSON.stringify(a); return s.length > 140 ? s.slice(0, 137) + '...' : s; } catch { return '?'; } };
38
38
  const badge = (label, cls) => `\n\n[${cls}] ${label}\n`;
39
39
 
40
+ const fmtOut = o => { if (o == null) return ''; const s = typeof o === 'string' ? o : JSON.stringify(o); return s.length > 400 ? s.slice(0, 397) + '...' : s; };
41
+
40
42
  const RENDERERS = {
41
43
  status: ev => badge('status: ' + ev.message, 'i'),
42
44
  'model-info': ev => badge('model: ' + (ev.providerID || '') + '/' + ev.modelID, 'i'),
43
- 'tool-event': ev => badge('tool ' + (ev.status || '') + ': ' + ev.toolName + ' ' + fmtArgs(ev.input), 't'),
45
+ 'tool-event': ev => {
46
+ const head = badge('tool ' + (ev.status || '') + ': ' + ev.toolName + ' ' + fmtArgs(ev.input), 't');
47
+ const out = ev.output != null ? '\n → ' + fmtOut(ev.output).replace(/\n/g, '\n ') + '\n' : '';
48
+ const err = ev.error ? '\n ✗ ' + fmtOut(ev.error) + '\n' : '';
49
+ return head + out + err;
50
+ },
44
51
  'tool-call': ev => badge('tool: ' + ev.toolName + ' ' + fmtArgs(ev.args), 't'),
45
52
  'file-event': ev => badge('file: ' + (ev.filename || ev.url || '?'), 'f'),
46
53
  'file-mirrored': ev => badge('wrote: ' + ev.path, 'f'),
47
54
  'reasoning-delta': ev => ev.textDelta,
48
55
  'step-start': () => badge('step start', 's'),
49
56
  'step-finish': ev => badge('step finish' + (ev.tokens ? ' tokens=' + JSON.stringify(ev.tokens) : ''), 's'),
50
- 'unknown-part': ev => badge('?part ' + ev.partType, 'i'),
57
+ 'unknown-part': ev => badge('?part ' + ev.partType + (ev.text ? ' text=' + fmtOut(ev.text) : ''), 'i'),
51
58
  };
52
59
 
53
60
  export function renderEvent(ev) { const r = RENDERERS[ev.type]; return r ? r(ev) : ''; }
package/docs/index.html CHANGED
@@ -7,6 +7,22 @@
7
7
  <title>thebird — TUI</title>
8
8
  <link rel="stylesheet" href="vendor/xterm.css" />
9
9
  <link rel="stylesheet" href="tui.css" />
10
+ <script>
11
+ (() => {
12
+ const saved = localStorage.getItem('tui_theme');
13
+ const prefDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
14
+ const theme = saved || (prefDark ? 'dark' : 'light');
15
+ document.documentElement.setAttribute('data-theme', theme);
16
+ window.setTheme = t => {
17
+ document.documentElement.setAttribute('data-theme', t);
18
+ localStorage.setItem('tui_theme', t);
19
+ window.dispatchEvent(new CustomEvent('tui-theme-change', { detail: t }));
20
+ };
21
+ window.toggleTheme = () => window.setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
22
+ window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener?.('change', e => { if (!localStorage.getItem('tui_theme')) window.setTheme(e.matches ? 'dark' : 'light'); });
23
+ (window.__debug = window.__debug || {}).theme = { get current() { return document.documentElement.getAttribute('data-theme'); }, set: window.setTheme, toggle: window.toggleTheme };
24
+ })();
25
+ </script>
10
26
  </head>
11
27
  <body>
12
28
  <div style="display:flex;flex-direction:column;height:100%">
@@ -16,6 +32,7 @@
16
32
  <button id="tab-chat" class="tui-tab active" onclick="switchTab('chat')">Chat</button>
17
33
  <button id="tab-term" class="tui-tab" onclick="switchTab('term')">Terminal</button>
18
34
  <button id="tab-preview" class="tui-tab" onclick="switchTab('preview')">Preview</button>
35
+ <button class="tui-tab" style="margin-left:auto" onclick="toggleTheme()" title="Toggle dark/light">◐</button>
19
36
  </div>
20
37
  <div id="pane-chat" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
21
38
  <bird-chat></bird-chat>
@@ -27,7 +44,7 @@
27
44
  <div class="tui-toolbar">
28
45
  <button class="tui-btn" onclick="refreshPreview()">[reload]</button>
29
46
  </div>
30
- <iframe id="preview-frame" style="width:100%;flex:1;border:0;background:#000" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
47
+ <iframe id="preview-frame" style="width:100%;flex:1;border:0;background:var(--paper)" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
31
48
  </div>
32
49
  </div>
33
50
  <script>
@@ -77,7 +94,9 @@ async function refreshPreview() {
77
94
  const htmlFiles = Object.keys(snap).filter(k => k.endsWith('.html'));
78
95
  const pick = htmlFiles.find(k => k === 'index.html') || htmlFiles[0];
79
96
  if (pick) { iframe.srcdoc = snap[pick]; return; }
80
- iframe.srcdoc = '<pre style="color:#0B0B09;background:#EFE9DD;padding:1ch;font-family:monospace">no files yet</pre>';
97
+ const cs = getComputedStyle(document.documentElement);
98
+ const fg = cs.getPropertyValue('--ink').trim(), bg = cs.getPropertyValue('--paper').trim();
99
+ iframe.srcdoc = `<pre style="color:${fg};background:${bg};padding:1ch;font-family:monospace">no files yet</pre>`;
81
100
  }
82
101
  function switchTab(t) {
83
102
  ['chat', 'term', 'preview'].forEach(id => {
@@ -18,9 +18,9 @@ const PART_HANDLERS = {
18
18
  }
19
19
  },
20
20
  tool: (part, st, emit) => {
21
- const key = part.id + ':' + (part.state?.status || '');
22
- if (st.seenTool.has(key)) return;
23
- st.seenTool.add(key);
21
+ const sig = part.id + ':' + (part.state?.status || '') + ':' + JSON.stringify(part.state?.input || '').length + ':' + (part.state?.output ? 1 : 0);
22
+ if (st.seenTool.has(sig)) return;
23
+ st.seenTool.add(sig);
24
24
  emit({ type: 'tool-event', toolName: part.tool || part.state?.tool, status: part.state?.status, input: part.state?.input, output: part.state?.output, error: part.state?.error, id: part.id });
25
25
  },
26
26
  file: (part, st, emit) => {
@@ -80,7 +80,7 @@ export async function* streamKiloHTTP({ url, model, messages, providerType, agen
80
80
  if (part?.sessionID !== sessionId || !assistantMsgs.has(part.messageID)) return;
81
81
  const h = PART_HANDLERS[part.type];
82
82
  if (h) h(part, st, push);
83
- else push({ type: 'unknown-part', partType: part.type, id: part.id });
83
+ else push({ type: 'unknown-part', partType: part.type, id: part.id, text: part.text, raw: part });
84
84
  }
85
85
  } catch (_) {}
86
86
  };
@@ -113,7 +113,7 @@ export async function* streamKiloHTTP({ url, model, messages, providerType, agen
113
113
  const pending = [];
114
114
  const pushLocal = ev => { emit(ev); pending.push(ev); };
115
115
  if (h) h(part, st, pushLocal);
116
- else pushLocal({ type: 'unknown-part', partType: part.type, id: part.id });
116
+ else pushLocal({ type: 'unknown-part', partType: part.type, id: part.id, text: part.text, raw: part });
117
117
  for (const ev of pending) yield ev;
118
118
  }
119
119
  }
package/docs/terminal.js CHANGED
@@ -57,12 +57,22 @@ async function boot() {
57
57
  window.__debug = window.__debug || {};
58
58
  window.__debug.terminal = { get state() { return bootActor.getSnapshot().value; } };
59
59
 
60
- const term = new Terminal({ theme: { background: '#000000', foreground: '#33ff33' }, convertEol: true });
60
+ const readTermTheme = () => {
61
+ const cs = getComputedStyle(document.documentElement);
62
+ return {
63
+ background: cs.getPropertyValue('--paper').trim() || '#000',
64
+ foreground: cs.getPropertyValue('--ink').trim() || '#ccc',
65
+ cursor: cs.getPropertyValue('--green').trim() || '#3FA93A',
66
+ selectionBackground: cs.getPropertyValue('--green').trim() || '#3FA93A',
67
+ };
68
+ };
69
+ const term = new Terminal({ theme: readTermTheme(), convertEol: true });
61
70
  const fit = new FitAddon();
62
71
  term.loadAddon(fit);
63
72
  term.open(el);
64
73
  fit.fit();
65
74
  window.addEventListener('resize', () => fit.fit());
75
+ window.addEventListener('tui-theme-change', () => { term.options.theme = readTermTheme(); });
66
76
 
67
77
  const saved = await idbLoad();
68
78
  let files;
package/docs/tui.css CHANGED
@@ -1,7 +1,9 @@
1
1
  @import url('https://fonts.googleapis.com/css2?family=Archivo+Black&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
2
- :root {
2
+ :root, [data-theme="light"] {
3
3
  --paper: #EFE9DD;
4
4
  --ink: #0B0B09;
5
+ --ink-rgb: 11,11,9;
6
+ --paper-rgb: 239,233,221;
5
7
  --ink-dim: rgba(11,11,9,0.55);
6
8
  --ink-hair: rgba(11,11,9,0.18);
7
9
  --surface: #F6F1E7;
@@ -19,6 +21,22 @@
19
21
  --dur-base: 160ms;
20
22
  --ease: cubic-bezier(0.2,0,0,1);
21
23
  }
24
+ [data-theme="dark"] {
25
+ --paper: #0B0B09;
26
+ --ink: #EFE9DD;
27
+ --ink-rgb: 239,233,221;
28
+ --paper-rgb: 11,11,9;
29
+ --ink-dim: rgba(239,233,221,0.55);
30
+ --ink-hair: rgba(239,233,221,0.18);
31
+ --surface: #17160F;
32
+ --surface-2: rgba(239,233,221,0.04);
33
+ --green: #3FA93A;
34
+ --green-fg: #0B0B09;
35
+ --green-deep: #6CE06A;
36
+ --flame: #FF6B4A;
37
+ --link: #7FA2FF;
38
+ --user: #7FA2FF;
39
+ }
22
40
  *, *::before, *::after { box-sizing: border-box; border: 0; outline: 0; border-radius: 0; font-family: var(--ff-mono); }
23
41
  :focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
24
42
  html, body { background: var(--paper); color: var(--ink); height: 100%; margin: 0; font-size: 13px; line-height: 1.45; }
@@ -71,7 +89,15 @@ html, body { background: var(--paper); color: var(--ink); height: 100%; margin:
71
89
  border-bottom-color: var(--green);
72
90
  background: var(--paper);
73
91
  }
74
- .tui-select { appearance: none; padding-right: 3ch; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%230B0B09'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.75ch center; }
92
+ .tui-select {
93
+ appearance: none;
94
+ padding-right: 3ch;
95
+ background-image: linear-gradient(45deg, transparent 50%, var(--ink) 50%), linear-gradient(-45deg, transparent 50%, var(--ink) 50%);
96
+ background-position: right 1.2ch center, right 0.6ch center;
97
+ background-size: 5px 5px, 5px 5px;
98
+ background-repeat: no-repeat;
99
+ }
100
+ .tui-select::-ms-expand { display: none; }
75
101
  .tui-btn {
76
102
  background: transparent;
77
103
  color: var(--ink);
@@ -155,6 +181,6 @@ html, body { background: var(--paper); color: var(--ink); height: 100%; margin:
155
181
  background: var(--surface);
156
182
  }
157
183
  @keyframes blink { 50% { opacity: 0; } }
158
- #pane-term { background: var(--ink); }
184
+ #pane-term { background: var(--paper); }
159
185
  #pane-term .xterm { padding: 1ch; }
160
186
  .hidden { display: none !important; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.92",
3
+ "version": "1.2.93",
4
4
  "description": "Anthropic SDK to Gemini streaming bridge — drop-in proxy that translates Anthropic message format and tool calls to Google Gemini",
5
5
  "scripts": {
6
6
  "start": "node serve.js"