thebird 1.2.88 → 1.2.89

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.
@@ -0,0 +1,30 @@
1
+ export function extractCodeBlocks(text) {
2
+ const out = [];
3
+ const re = /```(\w+)?(?:\s+([^\n`]+))?\n([\s\S]*?)```/g;
4
+ let m, idx = 0;
5
+ while ((m = re.exec(text))) {
6
+ const lang = (m[1] || '').toLowerCase();
7
+ const hint = (m[2] || '').trim();
8
+ const code = m[3];
9
+ const name = pickName(lang, hint, code, idx++);
10
+ out.push({ name, lang, code });
11
+ }
12
+ return out;
13
+ }
14
+ function pickName(lang, hint, code, idx) {
15
+ if (hint && /\.\w+$/.test(hint)) return hint.replace(/^[./]+/, '');
16
+ if (lang === 'html' || /<!DOCTYPE|<html/i.test(code)) return idx === 0 ? 'index.html' : `file-${idx}.html`;
17
+ if (lang === 'css') return idx === 0 ? 'styles.css' : `file-${idx}.css`;
18
+ if (lang === 'js' || lang === 'javascript') return idx === 0 ? 'app.js' : `file-${idx}.js`;
19
+ if (lang === 'json') return idx === 0 ? 'data.json' : `file-${idx}.json`;
20
+ return `snippet-${idx}.txt`;
21
+ }
22
+ export function applyExtracted(blocks) {
23
+ const snap = window.__debug.idbSnapshot || (window.__debug.idbSnapshot = {});
24
+ const written = [];
25
+ for (const b of blocks) {
26
+ if (snap[b.name] !== b.code) { snap[b.name] = b.code; written.push(b.name); }
27
+ }
28
+ if (written.length) { window.__debug.idbPersist?.(); window.__debug.shell?.onPreviewWrite?.(); }
29
+ return written;
30
+ }
package/docs/index.html CHANGED
@@ -10,8 +10,8 @@
10
10
  </head>
11
11
  <body>
12
12
  <div style="display:flex;flex-direction:column;height:100%">
13
- <div class="tui-header"><pre class="logo" style="margin:0;padding:2px 1ch"> ▀█▀ █░█ █▀▀ █▄▄ █ █▀█ █▀▄ <span class="ver">v1.2</span>
14
- █ █▀█ ██▄ █▄█ █ █▀▄ █▄▀ <span style="color:#1a9a1a">instant posix js</span></pre></div>
13
+ <div class="tui-header"><pre class="logo" style="margin:0"> ▀█▀ █░█ █▀▀ █▄▄ █ █▀█ █▀▄ <span class="ver">v1.2</span>
14
+ █ █▀█ ██▄ █▄█ █ █▀▄ █▄▀ <span style="color:#247420">instant posix js</span></pre></div>
15
15
  <div class="tui-tabs">
16
16
  <button id="tab-chat" class="tui-tab active" onclick="switchTab('chat')">Chat</button>
17
17
  <button id="tab-term" class="tui-tab" onclick="switchTab('term')">Terminal</button>
@@ -30,7 +30,6 @@
30
30
  <iframe id="preview-frame" style="width:100%;flex:1;border:0;background:#000" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
31
31
  </div>
32
32
  </div>
33
- <div class="tui-scanline"></div>
34
33
  <script>
35
34
  function callExpressRoute(path, method) {
36
35
  const handlers = window.__debug?.shell?.httpHandlers || {};
@@ -86,6 +85,7 @@ function switchTab(t) {
86
85
  });
87
86
  if (t === 'preview') refreshPreview();
88
87
  }
88
+ window.showPreview = () => switchTab('preview');
89
89
  </script>
90
90
  <script type="module" src="app.js"></script>
91
91
  <script type="module" src="terminal.js"></script>
@@ -11,7 +11,12 @@ export async function mirrorFromSandbox(fsBase) {
11
11
  const content = await r.text();
12
12
  if (snap[rel] !== content) { snap[rel] = content; mirrored.push(rel); }
13
13
  }
14
- if (mirrored.length) { window.__debug.idbPersist?.(); window.__debug.shell?.onPreviewWrite?.(); }
14
+ if (mirrored.length) {
15
+ window.__debug.idbPersist?.();
16
+ window.__debug.shell?.onPreviewWrite?.();
17
+ window.showPreview?.();
18
+ window.refreshPreview?.();
19
+ }
15
20
  return mirrored;
16
21
  } catch (e) { return []; }
17
22
  }
@@ -1,4 +1,5 @@
1
1
  import { mirrorFromSandbox } from './kilo-fs-mirror.js';
2
+ import { extractCodeBlocks, applyExtracted } from './extract-code-blocks.js';
2
3
 
3
4
  export async function* streamKiloHTTP({ url, model, messages, providerType, agent }) {
4
5
  yield { type: 'start-step' };
@@ -19,8 +20,11 @@ export async function* streamKiloHTTP({ url, model, messages, providerType, agen
19
20
 
20
21
  const modelId = model || (isOpencode ? 'minimax-m2.5-free' : 'x-ai/grok-code-fast-1:optimized:free');
21
22
  const codingIntent = /\b(write|create|make|build|generate|save|file|html|css|script|app|page|code)\b/i.test(userText);
22
- const agentName = agent || (isOpencode ? (codingIntent ? 'build' : 'general') : (codingIntent ? 'code' : 'ask'));
23
- const body = { parts: [{ type: 'text', text: userText }], agent: agentName };
23
+ const prompt = codingIntent
24
+ ? userText + '\n\nRespond ONLY with one or more markdown code blocks. Each block MUST have the filename after the language tag, e.g. ```html index.html\\n...```. No prose outside code blocks.'
25
+ : userText;
26
+ const agentName = agent || (isOpencode ? 'general' : 'ask');
27
+ const body = { parts: [{ type: 'text', text: prompt }], agent: agentName };
24
28
  if (isOpencode) body.model = { providerID: 'opencode', modelID: modelId };
25
29
  else { body.providerID = 'kilo'; body.modelID = modelId; }
26
30
 
@@ -29,26 +33,24 @@ export async function* streamKiloHTTP({ url, model, messages, providerType, agen
29
33
  const es = new EventSource(base + '/event');
30
34
  const textByPart = new Map();
31
35
  const assistantMsgs = new Set();
32
- let done = false;
36
+ let stepFinished = false;
33
37
  const pending = [];
34
38
  let resolveNext = null;
35
- const push = ev => { pending.push(ev); if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } };
39
+ const wake = () => { if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } };
36
40
  es.onmessage = e => {
37
41
  try {
38
42
  const m = JSON.parse(e.data);
39
43
  if (m.type === 'message.updated') {
40
44
  const info = m.properties?.info;
41
- if (info?.sessionID === sessionId && info.role === 'assistant') {
42
- assistantMsgs.add(info.id);
43
- if (info.time?.completed) { done = true; if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } }
44
- }
45
+ if (info?.sessionID === sessionId && info.role === 'assistant') assistantMsgs.add(info.id);
45
46
  } else if (m.type === 'message.part.updated') {
46
47
  const part = m.properties?.part;
47
- if (part?.sessionID === sessionId && part.type === 'text' && assistantMsgs.has(part.messageID)) {
48
+ if (part?.sessionID !== sessionId || !assistantMsgs.has(part.messageID)) return;
49
+ if (part.type === 'text') {
48
50
  const prior = textByPart.get(part.id) || '';
49
51
  const txt = part.text || '';
50
- if (txt.length > prior.length) { push({ type:'text-delta', textDelta: txt.slice(prior.length) }); textByPart.set(part.id, txt); }
51
- }
52
+ if (txt.length > prior.length) { pending.push({ type:'text-delta', textDelta: txt.slice(prior.length) }); textByPart.set(part.id, txt); wake(); }
53
+ } else if (part.type === 'step-finish') { stepFinished = true; wake(); }
52
54
  }
53
55
  } catch (_) {}
54
56
  };
@@ -56,10 +58,12 @@ export async function* streamKiloHTTP({ url, model, messages, providerType, agen
56
58
  window.__debug[dbgKey].lastStatus = msgRes.status;
57
59
  if (!msgRes.ok) { es.close(); throw new Error('message ' + msgRes.status + ': ' + await msgRes.text()); }
58
60
  const deadline = Date.now() + 180000;
59
- while (!done || pending.length) {
60
- if (pending.length) { const ev = pending.shift(); if (ev.type === 'text-delta') text += ev.textDelta; yield ev; continue; }
61
+ let graceUntil = 0;
62
+ while (true) {
63
+ if (pending.length) { const ev = pending.shift(); text += ev.textDelta; yield ev; continue; }
64
+ if (stepFinished) { if (!graceUntil) graceUntil = Date.now() + 1500; if (Date.now() > graceUntil) break; }
61
65
  if (Date.now() > deadline) break;
62
- await new Promise(r => { resolveNext = r; setTimeout(r, 5000); });
66
+ await new Promise(r => { resolveNext = r; setTimeout(r, 500); });
63
67
  }
64
68
  es.close();
65
69
  } else {
@@ -71,6 +75,13 @@ export async function* streamKiloHTTP({ url, model, messages, providerType, agen
71
75
  for (const tp of (result.parts || []).filter(p => p.type === 'text')) { text += tp.text; yield { type: 'text-delta', textDelta: tp.text }; }
72
76
  }
73
77
  const mirrored = await mirrorFromSandbox(fsBase);
74
- if (mirrored.length) { window.__debug[dbgKey].writes = mirrored; window.refreshPreview?.(); }
78
+ const blocks = extractCodeBlocks(text);
79
+ const extracted = blocks.length ? applyExtracted(blocks) : [];
80
+ const all = [...new Set([...mirrored, ...extracted])];
81
+ if (all.length) {
82
+ window.__debug[dbgKey].writes = all;
83
+ window.showPreview?.();
84
+ window.refreshPreview?.();
85
+ }
75
86
  yield { type: 'finish-step', finishReason: 'stop' };
76
87
  }
package/docs/tui.css CHANGED
@@ -1,94 +1,113 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Archivo+Black&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
1
2
  :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;
3
+ --paper: #EFE9DD;
4
+ --ink: #0B0B09;
5
+ --ink-dim: rgba(11,11,9,0.55);
6
+ --ink-hair: rgba(11,11,9,0.18);
7
+ --surface: #F6F1E7;
8
+ --surface-2: rgba(11,11,9,0.04);
9
+ --green: #247420;
10
+ --green-fg: #FFFFFF;
11
+ --green-deep: #18794E;
12
+ --sun: #FFD100;
13
+ --flame: #FF3B1F;
14
+ --link: #1F4DFF;
15
+ --user: #1F4DFF;
16
+ --ff-display: 'Archivo Black', system-ui, sans-serif;
17
+ --ff-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
18
+ --dur-snap: 80ms;
19
+ --dur-base: 160ms;
20
+ --ease: cubic-bezier(0.2,0,0,1);
12
21
  }
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); }
22
+ *, *::before, *::after { box-sizing: border-box; border: 0; outline: 0; border-radius: 0; font-family: var(--ff-mono); }
23
+ :focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
24
+ html, body { background: var(--paper); color: var(--ink); height: 100%; margin: 0; font-size: 13px; line-height: 1.45; }
25
+ ::selection { background: var(--green); color: var(--green-fg); }
16
26
  .tui-header {
17
- border-bottom: 1px solid var(--tui-border-dim);
18
- background: var(--tui-bg);
19
- padding: 0;
27
+ border-bottom: 1px solid var(--ink-hair);
28
+ background: var(--paper);
29
+ padding: 6px 2ch;
20
30
  white-space: pre;
21
- font-size: 11px;
22
- line-height: 1.2;
23
- color: var(--tui-fg-dim);
31
+ font-size: 10px;
32
+ line-height: 1.1;
33
+ color: var(--ink);
24
34
  overflow: hidden;
25
35
  }
26
- .tui-header .logo { color: var(--tui-fg-bright); }
27
- .tui-header .ver { color: var(--tui-accent); }
36
+ .tui-header .logo { color: var(--ink); font-family: var(--ff-mono); font-weight: 700; }
37
+ .tui-header .ver { color: var(--green); font-weight: 600; }
28
38
  .tui-tabs {
29
39
  display: flex;
30
40
  gap: 0;
31
- background: var(--tui-bg);
32
- border-bottom: 1px solid var(--tui-border-dim);
33
- padding: 0 1ch;
41
+ background: var(--paper);
42
+ border-bottom: 1px solid var(--ink);
43
+ padding: 0 2ch;
34
44
  }
35
45
  .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;
46
+ background: transparent;
47
+ border-bottom: 2px solid transparent;
48
+ color: var(--ink-dim);
49
+ padding: 8px 2ch;
41
50
  cursor: pointer;
42
- font-size: 13px;
43
- margin-bottom: -1px;
44
- position: relative;
51
+ font-size: 11px;
52
+ font-weight: 600;
53
+ text-transform: uppercase;
54
+ letter-spacing: 0.08em;
55
+ transition: color var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease);
45
56
  }
46
- .tui-tab:hover { color: var(--tui-fg); }
57
+ .tui-tab:hover { color: var(--ink); }
47
58
  .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);
59
+ color: var(--ink);
60
+ border-bottom-color: var(--green);
52
61
  }
53
62
  .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;
63
+ background: var(--surface);
64
+ border-bottom: 1px solid var(--ink-hair);
65
+ color: var(--ink);
66
+ padding: 4px 1ch;
67
+ font-size: 12px;
68
+ font-family: var(--ff-mono);
60
69
  }
61
70
  .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);
71
+ border-bottom-color: var(--green);
72
+ background: var(--paper);
64
73
  }
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; }
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; }
66
75
  .tui-btn {
67
- background: var(--tui-bg);
68
- border: 1px solid var(--tui-border-dim);
69
- color: var(--tui-fg);
70
- padding: 2px 2ch;
76
+ background: transparent;
77
+ color: var(--ink);
78
+ padding: 4px 1.5ch;
71
79
  cursor: pointer;
72
- font-size: 13px;
80
+ font-size: 11px;
81
+ font-weight: 600;
82
+ text-transform: uppercase;
83
+ letter-spacing: 0.06em;
84
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
85
+ }
86
+ .tui-btn:hover { background: var(--ink); color: var(--paper); }
87
+ .tui-btn.primary {
88
+ background: var(--ink);
89
+ color: var(--paper);
90
+ box-shadow: 3px 3px 0 var(--green);
91
+ transition: transform var(--dur-snap) var(--ease), box-shadow var(--dur-snap) var(--ease);
73
92
  }
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); }
93
+ .tui-btn.primary:hover { transform: translate(1px,1px); box-shadow: 2px 2px 0 var(--green); background: var(--green); color: var(--green-fg); }
94
+ .tui-btn.primary:active { transform: translate(3px,3px); box-shadow: 0 0 0 var(--green); }
95
+ .tui-btn:disabled { opacity: 0.4; cursor: not-allowed; }
77
96
  .tui-msg {
78
- max-width: 72ch;
79
- padding: 4px 1ch;
97
+ max-width: 78ch;
98
+ padding: 6px 1ch;
80
99
  white-space: pre-wrap;
81
100
  word-break: break-word;
82
101
  font-size: 13px;
83
- line-height: 1.4;
102
+ line-height: 1.5;
84
103
  }
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; }
104
+ .tui-msg.user { color: var(--user); border-left: 2px solid var(--user); padding-left: 1.5ch; }
105
+ .tui-msg.user::before { content: 'you / '; color: var(--ink-dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; display: block; margin-bottom: 2px; font-weight: 600; }
106
+ .tui-msg.assistant { color: var(--ink); border-left: 2px solid var(--green); padding-left: 1.5ch; }
107
+ .tui-msg.assistant::before { content: 'agent / '; color: var(--green); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; display: block; margin-bottom: 2px; font-weight: 600; }
108
+ .tui-status { color: var(--green-deep); font-size: 11px; }
109
+ .tui-error-text { color: var(--flame); font-size: 11px; font-weight: 600; }
110
+ .tui-spinner::after { content: '⠋'; animation: tui-spin 0.6s steps(6) infinite; color: var(--green); }
92
111
  @keyframes tui-spin {
93
112
  0% { content: '⠋'; } 16% { content: '⠙'; } 33% { content: '⠹'; }
94
113
  50% { content: '⠸'; } 66% { content: '⠼'; } 83% { content: '⠴'; }
@@ -96,36 +115,31 @@ html, body { background: var(--tui-bg); color: var(--tui-fg); height: 100%; marg
96
115
  .tui-toolbar {
97
116
  display: flex;
98
117
  gap: 1ch;
99
- padding: 2px 1ch;
100
- border-bottom: 1px solid var(--tui-border-dim);
101
- background: var(--tui-bg);
118
+ padding: 6px 2ch;
119
+ border-bottom: 1px solid var(--ink-hair);
120
+ background: var(--paper);
102
121
  align-items: center;
103
122
  flex-wrap: wrap;
104
- font-size: 13px;
123
+ font-size: 12px;
105
124
  }
106
- .tui-toolbar label { color: var(--tui-fg-dim); font-size: 12px; }
125
+ .tui-toolbar label { color: var(--ink-dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; }
107
126
  .tui-msglist {
108
127
  flex: 1;
109
128
  overflow-y: auto;
110
- padding: 1ch;
129
+ padding: 2ch;
111
130
  display: flex;
112
131
  flex-direction: column;
113
- gap: 4px;
132
+ gap: 8px;
133
+ background: var(--paper);
114
134
  }
115
135
  .tui-compose {
116
136
  display: flex;
117
137
  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;
138
+ padding: 8px 2ch;
139
+ border-top: 1px solid var(--ink);
140
+ background: var(--surface);
128
141
  }
129
142
  @keyframes blink { 50% { opacity: 0; } }
130
- #pane-term { background: #000; }
143
+ #pane-term { background: var(--ink); }
144
+ #pane-term .xterm { padding: 1ch; }
131
145
  .hidden { display: none !important; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.88",
3
+ "version": "1.2.89",
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"