thebird 1.2.92 → 1.2.94
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 +10 -0
- package/docs/app.js +5 -4
- package/docs/chat-providers.js +10 -2
- package/docs/index.html +21 -2
- package/docs/kilo-http-stream.js +5 -5
- package/docs/terminal.js +11 -1
- package/docs/tui.css +29 -3
- package/package.json +1 -1
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)
|
package/docs/app.js
CHANGED
|
@@ -66,14 +66,15 @@ class BirdChat extends HTMLElement {
|
|
|
66
66
|
|
|
67
67
|
renderBaseUrlInput() {
|
|
68
68
|
const { providerType, baseUrl } = this.state;
|
|
69
|
-
if (providerType !== 'custom' && providerType !== 'kilo' && providerType !== 'opencode') return null;
|
|
70
|
-
const
|
|
69
|
+
if (providerType !== 'custom' && providerType !== 'kilo' && providerType !== 'opencode' && providerType !== 'acp2openai') return null;
|
|
70
|
+
const phMap = { kilo: 'http://localhost:4780', opencode: 'http://localhost:4790', acp2openai: 'http://localhost:4800/v1', custom: 'https://your-endpoint/v1' };
|
|
71
|
+
const ph = phMap[providerType] || phMap.custom;
|
|
71
72
|
return html`<input type="text" class="tui-input" style="flex:1;min-width:140px" placeholder=${ph} value=${baseUrl}
|
|
72
73
|
onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />`;
|
|
73
74
|
}
|
|
74
75
|
renderApiKeyInput() {
|
|
75
76
|
const { providerType, apiKey } = this.state;
|
|
76
|
-
if (providerType === 'kilo' || providerType === 'opencode') return null;
|
|
77
|
+
if (providerType === 'kilo' || providerType === 'opencode' || providerType === 'acp2openai') return null;
|
|
77
78
|
const provDef = PROVIDERS[providerType] || PROVIDERS.custom;
|
|
78
79
|
return html`<input id="api-key-input" type="password" class="tui-input" style="flex:1;min-width:120px" placeholder=${provDef.keyPlaceholder} value=${apiKey}
|
|
79
80
|
onchange=${e => { const v = e.target.value.trim(); localStorage.setItem('provider_api_key', v); this.setState({ apiKey: v }); if (v) this.loadModels(); }} />`;
|
|
@@ -128,7 +129,7 @@ class BirdChat extends HTMLElement {
|
|
|
128
129
|
const text = input?.value.trim();
|
|
129
130
|
if (!text || this.state.streaming) return;
|
|
130
131
|
const { apiKey, model, providerType, baseUrl } = this.state;
|
|
131
|
-
if (!apiKey && providerType !== 'kilo' && providerType !== 'opencode') { this.setState({ status: 'Enter an API key above.' }); return; }
|
|
132
|
+
if (!apiKey && providerType !== 'kilo' && providerType !== 'opencode' && providerType !== 'acp2openai') { this.setState({ status: 'Enter an API key above.' }); return; }
|
|
132
133
|
input.value = '';
|
|
133
134
|
input.style.height = 'auto';
|
|
134
135
|
const normalizedMessages = [...this.state.messages, { role: 'user', content: text }].map(m => ({
|
package/docs/chat-providers.js
CHANGED
|
@@ -8,6 +8,7 @@ export const PROVIDERS = {
|
|
|
8
8
|
cerebras: { label: 'Cerebras', baseUrl: 'https://api.cerebras.ai/v1', keyPlaceholder: 'CEREBRAS_API_KEY', models: ['gpt-oss-120b', 'llama3.1-8b'] },
|
|
9
9
|
kilo: { label: 'Kilo Code', baseUrl: 'http://localhost:4780', keyPlaceholder: '(no key needed)', models: ['x-ai/grok-code-fast-1:optimized:free', 'kilo-auto/free', 'openrouter/free', 'stepfun/step-3.5-flash:free', 'nvidia/nemotron-3-super-120b-a12b:free', 'bytedance-seed/dola-seed-2.0-pro:free'] },
|
|
10
10
|
opencode: { label: 'opencode (zen)', baseUrl: 'http://localhost:4790', keyPlaceholder: '(needs opencode auth login)', models: ['minimax-m2.5-free', 'nemotron-3-super-free'] },
|
|
11
|
+
acp2openai: { label: 'acp2openai (OpenAI-compat)', baseUrl: 'http://localhost:4800/v1', keyPlaceholder: '(no key needed)', models: ['kilo/x-ai/grok-code-fast-1:optimized:free', 'kilo/kilo-auto/free', 'kilo/openrouter/free', 'opencode/minimax-m2.5-free'] },
|
|
11
12
|
custom: { label: 'Custom (OpenAI-compat)', baseUrl: '', keyPlaceholder: 'API_KEY', models: [] },
|
|
12
13
|
};
|
|
13
14
|
|
|
@@ -37,17 +38,24 @@ export async function fetchModels(providerType, baseUrl, apiKey) {
|
|
|
37
38
|
const fmtArgs = a => { try { const s = JSON.stringify(a); return s.length > 140 ? s.slice(0, 137) + '...' : s; } catch { return '?'; } };
|
|
38
39
|
const badge = (label, cls) => `\n\n[${cls}] ${label}\n`;
|
|
39
40
|
|
|
41
|
+
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; };
|
|
42
|
+
|
|
40
43
|
const RENDERERS = {
|
|
41
44
|
status: ev => badge('status: ' + ev.message, 'i'),
|
|
42
45
|
'model-info': ev => badge('model: ' + (ev.providerID || '') + '/' + ev.modelID, 'i'),
|
|
43
|
-
'tool-event': ev =>
|
|
46
|
+
'tool-event': ev => {
|
|
47
|
+
const head = badge('tool ' + (ev.status || '') + ': ' + ev.toolName + ' ' + fmtArgs(ev.input), 't');
|
|
48
|
+
const out = ev.output != null ? '\n → ' + fmtOut(ev.output).replace(/\n/g, '\n ') + '\n' : '';
|
|
49
|
+
const err = ev.error ? '\n ✗ ' + fmtOut(ev.error) + '\n' : '';
|
|
50
|
+
return head + out + err;
|
|
51
|
+
},
|
|
44
52
|
'tool-call': ev => badge('tool: ' + ev.toolName + ' ' + fmtArgs(ev.args), 't'),
|
|
45
53
|
'file-event': ev => badge('file: ' + (ev.filename || ev.url || '?'), 'f'),
|
|
46
54
|
'file-mirrored': ev => badge('wrote: ' + ev.path, 'f'),
|
|
47
55
|
'reasoning-delta': ev => ev.textDelta,
|
|
48
56
|
'step-start': () => badge('step start', 's'),
|
|
49
57
|
'step-finish': ev => badge('step finish' + (ev.tokens ? ' tokens=' + JSON.stringify(ev.tokens) : ''), 's'),
|
|
50
|
-
'unknown-part': ev => badge('?part ' + ev.partType, 'i'),
|
|
58
|
+
'unknown-part': ev => badge('?part ' + ev.partType + (ev.text ? ' text=' + fmtOut(ev.text) : ''), 'i'),
|
|
51
59
|
};
|
|
52
60
|
|
|
53
61
|
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
|
|
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
|
-
|
|
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 => {
|
package/docs/kilo-http-stream.js
CHANGED
|
@@ -18,9 +18,9 @@ const PART_HANDLERS = {
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
tool: (part, st, emit) => {
|
|
21
|
-
const
|
|
22
|
-
if (st.seenTool.has(
|
|
23
|
-
st.seenTool.add(
|
|
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
|
|
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 {
|
|
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(--
|
|
184
|
+
#pane-term { background: var(--paper); }
|
|
159
185
|
#pane-term .xterm { padding: 1ch; }
|
|
160
186
|
.hidden { display: none !important; }
|
package/package.json
CHANGED