thebird 1.2.91 → 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 +10 -0
- package/CLAUDE.md +18 -0
- package/docs/chat-providers.js +9 -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/CLAUDE.md
CHANGED
|
@@ -171,6 +171,24 @@ Run examples against real Gemini API to validate message translation.
|
|
|
171
171
|
- `stripUnsupported(params, caps)` — removes unsupported features, returns warnings
|
|
172
172
|
- Defaults: streaming, toolUse, vision, systemMessage = true; jsonMode = false
|
|
173
173
|
|
|
174
|
+
## Chat Observability (docs/)
|
|
175
|
+
|
|
176
|
+
`docs/kilo-http-stream.js` emits rich events via `PART_HANDLERS` dispatch table (kilo HTTP + opencode SSE unified):
|
|
177
|
+
- `status` — lifecycle (connecting, session id, POST status, mirror step)
|
|
178
|
+
- `model-info` — { providerID, modelID } actually routed to (may differ from requested when gm agent plugin hijacks)
|
|
179
|
+
- `text-delta` / `reasoning-delta` — accumulating growth
|
|
180
|
+
- `tool-event` — { toolName, status, input, output, error, id } from `part.type === 'tool'` state
|
|
181
|
+
- `file-event` / `file-mirrored` — agent file writes / sandbox mirror results
|
|
182
|
+
- `step-start` / `step-finish` — { id, tokens?, cost? } boundaries
|
|
183
|
+
- `unknown-part` — diagnostic for unhandled part.type
|
|
184
|
+
|
|
185
|
+
`window.__debug.agent` permanent registry (populated by `agent-chat.js`):
|
|
186
|
+
`{ provider, model, modelActual, providerActual, active, startedAt, finishedAt, durationMs, textChars, reasoningChars, toolCalls, files, steps, lastTool, lastError, events: [...rolling 300] }`
|
|
187
|
+
|
|
188
|
+
Per-provider rolling logs: `window.__debug.kilo.events`, `window.__debug.opencode.events`.
|
|
189
|
+
|
|
190
|
+
UI consumes via 3 channels: `onChunk(delta)` text streaming | `onEvent(ev)` badge rendering via `renderEvent()` dispatch table in `docs/chat-providers.js` | 4Hz poll on `window.__debug.agent` → `#agent-stats` strip (live counters).
|
|
191
|
+
|
|
174
192
|
## Files
|
|
175
193
|
|
|
176
194
|
- `lib/convert.js`: Message/tool translation logic
|
package/docs/chat-providers.js
CHANGED
|
@@ -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 =>
|
|
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
|
|
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