thebird 1.2.76 → 1.2.77

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
@@ -8,6 +8,11 @@
8
8
  - `lib/circuit-breaker.js`: `createCircuitBreaker()` — per-provider failure tracking with auto-recovery after cooldown
9
9
  - `lib/capabilities.js`: `getCapabilities()` / `stripUnsupported()` — provider capability metadata with automatic feature stripping and warnings
10
10
  - `lib/router-stream.js`: Router logic extracted from index.js — circuit breaker and capability checks integrated
11
+ - `docs/tui.css`: TUI (text user interface) theme — monospace, green-on-black, box-drawing borders, scanline overlay, ASCII spinner
12
+
13
+ ### Changed
14
+ - `docs/index.html`: Restyled to TUI aesthetic — ASCII art header, bracket-style tabs, removed Tailwind/RippleUI dependencies
15
+ - `docs/app.js`: Chat UI uses TUI-styled classes — monospace messages with `> ` / `< ` prefixes, bracket buttons
11
16
 
12
17
  ### Changed
13
18
  - `index.js`: Trimmed from 177 to 104 lines by extracting router logic to lib/router-stream.js
package/docs/app.js CHANGED
@@ -103,51 +103,44 @@ class BirdChat extends HTMLElement {
103
103
  html`<option value=${id} selected=${id === providerType}>${p.label}</option>`);
104
104
 
105
105
  applyDiff(this, html`
106
- <div class="flex flex-col h-full">
107
- <header class="navbar bg-base-200 border-b border-base-300 gap-2 flex-wrap px-4 py-2">
108
- <span class="text-primary font-bold text-lg mr-2">🐦 thebird</span>
109
- <div class="flex gap-2 flex-1 min-w-0 items-center flex-wrap">
110
- <select class="select select-sm select-bordered"
111
- onchange=${e => this.setProvider(e.target.value)}>${provOpts}</select>
112
- ${(providerType === 'custom' || providerType === 'acp') ? html`
113
- <input type="text" class="input input-sm input-bordered flex-1 min-w-[160px]"
114
- placeholder=${providerType === 'acp' ? 'ws://localhost:3000' : 'https://your-endpoint/v1'} value=${baseUrl}
115
- onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />` : ''}
116
- ${providerType !== 'acp' ? html`<input id="api-key-input" type="password" class="input input-sm input-bordered flex-1 min-w-[140px]"
117
- placeholder=${provDef.keyPlaceholder} value=${apiKey}
118
- onchange=${e => {
119
- const v = e.target.value.trim();
120
- localStorage.setItem('provider_api_key', v);
121
- this.setState({ apiKey: v });
122
- if (v) this.loadModels();
123
- }} />` : ''}
124
- <div class="relative">
125
- ${modelsLoading
126
- ? html`<span class="loading loading-spinner loading-sm text-primary"></span>`
127
- : html`<select class="select select-sm select-bordered" value=${model}
128
- onchange=${e => { localStorage.setItem('provider_model', e.target.value); this.setState({ model: e.target.value }); }}>${opts}</select>`}
129
- </div>
130
- <button class="btn btn-sm btn-ghost" onclick=${() => this.setState({ messages: [], status: '' })}>Clear</button>
131
- </div>
132
- </header>
133
-
134
- <div id="msg-list" class="flex-1 overflow-y-auto flex flex-col gap-3 p-4">
106
+ <div style="display:flex;flex-direction:column;height:100%">
107
+ <div class="tui-toolbar">
108
+ <label>provider:</label>
109
+ <select class="tui-select" onchange=${e => this.setProvider(e.target.value)}>${provOpts}</select>
110
+ ${(providerType === 'custom' || providerType === 'acp') ? html`
111
+ <input type="text" class="tui-input" style="flex:1;min-width:140px"
112
+ placeholder=${providerType === 'acp' ? 'ws://localhost:3000' : 'https://your-endpoint/v1'} value=${baseUrl}
113
+ onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />` : ''}
114
+ ${providerType !== 'acp' ? html`<input id="api-key-input" type="password" class="tui-input" style="flex:1;min-width:120px"
115
+ placeholder=${provDef.keyPlaceholder} value=${apiKey}
116
+ onchange=${e => {
117
+ const v = e.target.value.trim();
118
+ localStorage.setItem('provider_api_key', v);
119
+ this.setState({ apiKey: v });
120
+ if (v) this.loadModels();
121
+ }} />` : ''}
122
+ ${modelsLoading
123
+ ? html`<span class="tui-spinner"></span>`
124
+ : html`<select class="tui-select" value=${model}
125
+ onchange=${e => { localStorage.setItem('provider_model', e.target.value); this.setState({ model: e.target.value }); }}>${opts}</select>`}
126
+ <button class="tui-btn" onclick=${() => this.setState({ messages: [], status: '' })}>[clear]</button>
127
+ </div>
128
+
129
+ <div id="msg-list" class="tui-msglist">
135
130
  ${messages.map((m, i) => html`
136
- <div key=${i} class=${'flex ' + (m.role === 'user' ? 'justify-end' : 'justify-start')}>
137
- <div class=${'msg-bubble card px-4 py-3 text-sm leading-relaxed ' + (m.role === 'user' ? 'bg-primary text-primary-content' : 'bg-base-200 text-base-content')}>${m.content}</div>
138
- </div>`)}
139
- ${streaming && !streamingText && html`<div class="flex justify-start"><div class="card bg-base-200 px-4 py-3"><span class="loading loading-dots loading-sm"></span></div></div>`}
131
+ <div key=${i} class=${'tui-msg ' + m.role}>${m.content}</div>`)}
132
+ ${streaming && !streamingText && html`<div class="tui-msg assistant"><span class="tui-spinner"></span> thinking...</div>`}
140
133
  </div>
141
134
 
142
- ${status && html`<div class="text-xs text-error px-4 pb-1">${status}</div>`}
135
+ ${status && html`<div class="tui-error-text" style="padding:0 1ch">${status}</div>`}
143
136
 
144
- <form class="flex gap-2 p-3 border-t border-base-300 bg-base-200" onsubmit=${e => { e.preventDefault(); this.send(); }}>
145
- <textarea id="chat-input" class="textarea textarea-bordered flex-1 resize-none min-h-[42px] max-h-[120px] text-sm"
146
- placeholder="Message… (Shift+Enter for newline)" rows="1" disabled=${streaming}
137
+ <form class="tui-compose" onsubmit=${e => { e.preventDefault(); this.send(); }}>
138
+ <textarea id="chat-input" class="tui-textarea" style="flex:1;resize:none;min-height:24px;max-height:120px"
139
+ placeholder="type message... (shift+enter for newline)" rows="1" disabled=${streaming}
147
140
  onkeydown=${e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.send(); } }}
148
141
  oninput=${e => { e.target.style.height = 'auto'; e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; }}></textarea>
149
- <button type="submit" class="btn btn-primary self-end" disabled=${streaming}>
150
- ${streaming ? html`<span class="loading loading-spinner loading-sm"></span>` : 'Send'}
142
+ <button type="submit" class="tui-btn primary" disabled=${streaming}>
143
+ ${streaming ? html`<span class="tui-spinner"></span>` : '[send]'}
151
144
  </button>
152
145
  </form>
153
146
  </div>`);
@@ -167,12 +160,11 @@ class BirdChat extends HTMLElement {
167
160
  try {
168
161
  let full = '';
169
162
  const streamEl = document.createElement('div');
170
- streamEl.className = 'msg-bubble card bg-base-200 text-base-content px-4 py-3 text-sm leading-relaxed';
163
+ streamEl.className = 'tui-msg assistant';
171
164
  const cursor = document.createElement('span');
172
- cursor.className = 'animate-pulse ml-1';
173
- cursor.textContent = '';
165
+ cursor.style.cssText = 'animation:blink 0.5s step-end infinite';
166
+ cursor.textContent = '';
174
167
  const wrap = document.createElement('div');
175
- wrap.className = 'flex justify-start';
176
168
  wrap.appendChild(streamEl);
177
169
  wrap.appendChild(cursor);
178
170
  const list = this.querySelector('#msg-list');
package/docs/index.html CHANGED
@@ -1,43 +1,38 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" data-theme="dark">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <meta name="version" content="1.2.23">
7
- <title>thebird — Gemini chat + terminal</title>
7
+ <title>thebird — TUI</title>
8
8
  <script>window.coi = { coepDegrade: () => false };</script>
9
9
  <script src="coi-serviceworker.js"></script>
10
- <link rel="stylesheet" href="vendor/tailwind.css" />
11
- <link rel="stylesheet" href="vendor/rippleui.css" />
12
10
  <link rel="stylesheet" href="vendor/xterm.css" />
13
- <style>
14
- html, body { height: 100%; background: #0f1117; }
15
- .tab-active { border-bottom: 2px solid oklch(var(--p)); }
16
- bird-chat { display: flex; flex-direction: column; height: 100%; }
17
- .msg-bubble { max-width: 680px; white-space: pre-wrap; word-break: break-word; }
18
- #msg-list { scroll-behavior: smooth; }
19
- </style>
11
+ <link rel="stylesheet" href="tui.css" />
20
12
  </head>
21
- <body class="bg-base-100 text-base-content h-full">
22
- <div class="flex flex-col h-full">
23
- <div class="flex border-b border-base-300 bg-base-200 gap-2 px-4 pt-2 shrink-0">
24
- <button id="tab-chat" class="px-3 py-1 text-sm tab-active" onclick="switchTab('chat')">Chat</button>
25
- <button id="tab-term" class="px-3 py-1 text-sm" onclick="switchTab('term')">Terminal</button>
26
- <button id="tab-preview" class="px-3 py-1 text-sm" onclick="switchTab('preview')">Preview</button>
13
+ <body>
14
+ <div style="display:flex;flex-direction:column;height:100%">
15
+ <div class="tui-header"><pre class="logo" style="margin:0;padding:2px 1ch"> ▀█▀ █░█ █▀▀ █▄▄ █ █▀█ █▀▄ <span class="ver">v1.2</span>
16
+ █ █▀█ ██▄ █▄█ █▀▄ █▄▀ <span style="color:#1a9a1a">anthropic → gemini/openai bridge</span></pre></div>
17
+ <div class="tui-tabs">
18
+ <button id="tab-chat" class="tui-tab active" onclick="switchTab('chat')">Chat</button>
19
+ <button id="tab-term" class="tui-tab" onclick="switchTab('term')">Terminal</button>
20
+ <button id="tab-preview" class="tui-tab" onclick="switchTab('preview')">Preview</button>
27
21
  </div>
28
- <div id="pane-chat" class="flex-1 overflow-hidden">
22
+ <div id="pane-chat" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
29
23
  <bird-chat></bird-chat>
30
24
  </div>
31
- <div id="pane-term" class="flex flex-col flex-1 overflow-hidden hidden bg-black">
32
- <div id="term-container" class="flex-1"></div>
25
+ <div id="pane-term" class="hidden" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
26
+ <div id="term-container" style="flex:1"></div>
33
27
  </div>
34
- <div id="pane-preview" class="flex flex-col flex-1 overflow-hidden hidden">
35
- <div class="flex gap-2 px-3 py-1 bg-base-200 border-b border-base-300 shrink-0">
36
- <button class="btn btn-xs btn-ghost" onclick="refreshPreview()">↺ Reload</button>
28
+ <div id="pane-preview" class="hidden" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
29
+ <div class="tui-toolbar">
30
+ <button class="tui-btn" onclick="refreshPreview()">[reload]</button>
37
31
  </div>
38
- <iframe id="preview-frame" class="w-full flex-1 border-0" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
32
+ <iframe id="preview-frame" style="width:100%;flex:1;border:0;background:#000" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
39
33
  </div>
40
34
  </div>
35
+ <div class="tui-scanline"></div>
41
36
  <script>
42
37
  function callExpressRoute(path, method) {
43
38
  const handlers = window.__debug?.shell?.httpHandlers || {};
@@ -64,12 +59,13 @@ async function refreshPreview() {
64
59
  if (result) { iframe.srcdoc = result.body; return; }
65
60
  const snap = window.__debug?.idbSnapshot || {};
66
61
  if (snap['index.html']) { iframe.srcdoc = snap['index.html']; return; }
67
- iframe.srcdoc = '<p style="font-family:sans-serif;padding:2rem;color:#888">No express app running and no index.html in filesystem.<br>Run <code>node app.js</code> in the terminal.</p>';
62
+ iframe.srcdoc = '<pre style="color:#33ff33;padding:1ch;font-family:monospace">No express app running.\nRun node app.js in terminal.</pre>';
68
63
  }
69
64
  function switchTab(t) {
70
65
  ['chat', 'term', 'preview'].forEach(id => {
71
66
  document.getElementById('pane-' + id).classList.toggle('hidden', id !== t);
72
- document.getElementById('tab-' + id).classList.toggle('tab-active', id === t);
67
+ const tab = document.getElementById('tab-' + id);
68
+ tab.classList.toggle('active', id === t);
73
69
  });
74
70
  if (t === 'preview') refreshPreview();
75
71
  }
package/docs/tui.css ADDED
@@ -0,0 +1,131 @@
1
+ :root {
2
+ --tui-fg: #33ff33;
3
+ --tui-fg-dim: #1a9a1a;
4
+ --tui-fg-bright: #66ff66;
5
+ --tui-bg: #0a0a0a;
6
+ --tui-bg-alt: #111311;
7
+ --tui-border: #33ff33;
8
+ --tui-border-dim: #1a5a1a;
9
+ --tui-accent: #ffcc00;
10
+ --tui-error: #ff3333;
11
+ --tui-user: #00ccff;
12
+ }
13
+ *, *::before, *::after { font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'SF Mono', 'Consolas', monospace !important; }
14
+ html, body { background: var(--tui-bg); color: var(--tui-fg); height: 100%; margin: 0; font-size: 14px; line-height: 1.4; }
15
+ ::selection { background: var(--tui-fg); color: var(--tui-bg); }
16
+ .tui-header {
17
+ border-bottom: 1px solid var(--tui-border-dim);
18
+ background: var(--tui-bg);
19
+ padding: 0;
20
+ white-space: pre;
21
+ font-size: 11px;
22
+ line-height: 1.2;
23
+ color: var(--tui-fg-dim);
24
+ overflow: hidden;
25
+ }
26
+ .tui-header .logo { color: var(--tui-fg-bright); }
27
+ .tui-header .ver { color: var(--tui-accent); }
28
+ .tui-tabs {
29
+ display: flex;
30
+ gap: 0;
31
+ background: var(--tui-bg);
32
+ border-bottom: 1px solid var(--tui-border-dim);
33
+ padding: 0 1ch;
34
+ }
35
+ .tui-tab {
36
+ background: none;
37
+ border: 1px solid var(--tui-border-dim);
38
+ border-bottom: none;
39
+ color: var(--tui-fg-dim);
40
+ padding: 2px 2ch;
41
+ cursor: pointer;
42
+ font-size: 13px;
43
+ margin-bottom: -1px;
44
+ position: relative;
45
+ }
46
+ .tui-tab:hover { color: var(--tui-fg); }
47
+ .tui-tab.active {
48
+ color: var(--tui-accent);
49
+ border-color: var(--tui-border);
50
+ background: var(--tui-bg-alt);
51
+ border-bottom: 1px solid var(--tui-bg-alt);
52
+ }
53
+ .tui-input, .tui-select, .tui-textarea {
54
+ background: var(--tui-bg);
55
+ border: 1px solid var(--tui-border-dim);
56
+ color: var(--tui-fg);
57
+ padding: 2px 1ch;
58
+ font-size: 13px;
59
+ outline: none;
60
+ }
61
+ .tui-input:focus, .tui-select:focus, .tui-textarea:focus {
62
+ border-color: var(--tui-border);
63
+ box-shadow: 0 0 4px rgba(51, 255, 51, 0.15);
64
+ }
65
+ .tui-select { appearance: none; padding-right: 2ch; 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='%2333ff33'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.5ch center; }
66
+ .tui-btn {
67
+ background: var(--tui-bg);
68
+ border: 1px solid var(--tui-border-dim);
69
+ color: var(--tui-fg);
70
+ padding: 2px 2ch;
71
+ cursor: pointer;
72
+ font-size: 13px;
73
+ }
74
+ .tui-btn:hover { border-color: var(--tui-border); color: var(--tui-fg-bright); }
75
+ .tui-btn.primary { border-color: var(--tui-accent); color: var(--tui-accent); }
76
+ .tui-btn.primary:hover { background: var(--tui-accent); color: var(--tui-bg); }
77
+ .tui-msg {
78
+ max-width: 72ch;
79
+ padding: 4px 1ch;
80
+ white-space: pre-wrap;
81
+ word-break: break-word;
82
+ font-size: 13px;
83
+ line-height: 1.4;
84
+ }
85
+ .tui-msg.user { color: var(--tui-user); }
86
+ .tui-msg.user::before { content: '> '; color: var(--tui-user); }
87
+ .tui-msg.assistant { color: var(--tui-fg); }
88
+ .tui-msg.assistant::before { content: '< '; color: var(--tui-fg-dim); }
89
+ .tui-status { color: var(--tui-accent); font-size: 12px; }
90
+ .tui-error-text { color: var(--tui-error); font-size: 12px; }
91
+ .tui-spinner::after { content: '⠋'; animation: tui-spin 0.6s steps(6) infinite; }
92
+ @keyframes tui-spin {
93
+ 0% { content: '⠋'; } 16% { content: '⠙'; } 33% { content: '⠹'; }
94
+ 50% { content: '⠸'; } 66% { content: '⠼'; } 83% { content: '⠴'; }
95
+ }
96
+ .tui-toolbar {
97
+ display: flex;
98
+ gap: 1ch;
99
+ padding: 2px 1ch;
100
+ border-bottom: 1px solid var(--tui-border-dim);
101
+ background: var(--tui-bg);
102
+ align-items: center;
103
+ flex-wrap: wrap;
104
+ font-size: 13px;
105
+ }
106
+ .tui-toolbar label { color: var(--tui-fg-dim); font-size: 12px; }
107
+ .tui-msglist {
108
+ flex: 1;
109
+ overflow-y: auto;
110
+ padding: 1ch;
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: 4px;
114
+ }
115
+ .tui-compose {
116
+ display: flex;
117
+ gap: 1ch;
118
+ padding: 4px 1ch;
119
+ border-top: 1px solid var(--tui-border-dim);
120
+ background: var(--tui-bg);
121
+ }
122
+ .tui-scanline {
123
+ pointer-events: none;
124
+ position: fixed;
125
+ top: 0; left: 0; right: 0; bottom: 0;
126
+ background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px);
127
+ z-index: 9999;
128
+ }
129
+ @keyframes blink { 50% { opacity: 0; } }
130
+ #pane-term { background: #000; }
131
+ .hidden { display: none !important; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.76",
3
+ "version": "1.2.77",
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"