thebird 1.2.90 → 1.2.91
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 +8 -0
- package/docs/agent-chat.js +28 -11
- package/docs/app.js +15 -44
- package/docs/chat-providers.js +65 -0
- package/docs/kilo-http-stream.js +63 -13
- package/docs/tui.css +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
|
|
2
|
+
## [unreleased] 2026-04-21 chat observability — rich ACP/kilo/opencode event stream
|
|
3
|
+
- feat: kilo-http-stream emits status, model-info, reasoning-delta, tool-event, file-event, step-start/finish, file-mirrored, unknown-part
|
|
4
|
+
- feat: PART_HANDLERS dispatch table replaces part-type branching (kilo + opencode unified)
|
|
5
|
+
- feat: agent-chat forwards full event stream via onEvent callback; window.__debug.agent.events rolling log (300 cap)
|
|
6
|
+
- feat: agent stats strip in chat UI — provider·model·duration·txt·rsn·tool·file·step counters, live 4Hz
|
|
7
|
+
- feat: inline event badges in stream ([i] status/model/unknown, [t] tool, [f] file, [s] step)
|
|
8
|
+
- refactor: extracted PROVIDERS + fetchModels + renderEvent + formatStats to docs/chat-providers.js (app.js 229→166)
|
|
9
|
+
|
|
2
10
|
## [unreleased] 2026-04-21 node parity pass 12 — internal listen infrastructure
|
|
3
11
|
- feat: busnet — in-browser TCP-like listen/connect via BroadcastChannel cross-tab fabric
|
|
4
12
|
- feat: net.createServer now uses busnet — apps listen on ports other in-browser apps can connect to
|
package/docs/agent-chat.js
CHANGED
|
@@ -98,22 +98,39 @@ function buildStream(provider) {
|
|
|
98
98
|
return streamOpenAI({ url, apiKey: provider.apiKey, messages: provider.messages, model: provider.model, tools: TOOLS, maxOutputTokens: 8192 });
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
const EVENT_LOG_MAX = 300;
|
|
102
|
+
|
|
103
|
+
export async function agentGenerate(provider, messages, onChunk, onTool, onEvent) {
|
|
104
|
+
const agentState = {
|
|
105
|
+
provider: provider.type, model: provider.model, active: true,
|
|
106
|
+
startedAt: Date.now(), lastTool: null, lastError: null,
|
|
107
|
+
events: [], textChars: 0, reasoningChars: 0, toolCalls: 0, files: 0, steps: 0,
|
|
108
|
+
modelActual: null, providerActual: null,
|
|
109
|
+
};
|
|
110
|
+
Object.assign(window.__debug = window.__debug || {}, { agent: agentState });
|
|
105
111
|
try {
|
|
106
112
|
for await (const ev of buildStream({ ...provider, messages })) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const rec = { ...ev, t: Date.now() };
|
|
114
|
+
agentState.events.push(rec);
|
|
115
|
+
if (agentState.events.length > EVENT_LOG_MAX) agentState.events.splice(0, agentState.events.length - EVENT_LOG_MAX);
|
|
116
|
+
onEvent?.(ev);
|
|
117
|
+
if (ev.type === 'text-delta') { agentState.textChars += ev.textDelta.length; onChunk(ev.textDelta); }
|
|
118
|
+
else if (ev.type === 'reasoning-delta') { agentState.reasoningChars += ev.textDelta.length; }
|
|
119
|
+
else if (ev.type === 'tool-call' || ev.type === 'tool-event') {
|
|
120
|
+
agentState.toolCalls++;
|
|
121
|
+
agentState.lastTool = { name: ev.toolName, args: ev.args || ev.input, status: ev.status };
|
|
122
|
+
onTool(ev.toolName, ev.args || ev.input || {});
|
|
123
|
+
} else if (ev.type === 'file-event' || ev.type === 'file-mirrored') { agentState.files++; }
|
|
124
|
+
else if (ev.type === 'start-step' || ev.type === 'step-start') { agentState.steps++; }
|
|
125
|
+
else if (ev.type === 'model-info') { agentState.modelActual = ev.modelID; agentState.providerActual = ev.providerID; }
|
|
126
|
+
else if (ev.type === 'error') throw ev.error;
|
|
112
127
|
}
|
|
113
128
|
} catch (e) {
|
|
114
|
-
|
|
129
|
+
agentState.lastError = { message: e.message, stack: e.stack, timestamp: Date.now() };
|
|
115
130
|
throw e;
|
|
116
131
|
} finally {
|
|
117
|
-
|
|
132
|
+
agentState.active = false;
|
|
133
|
+
agentState.finishedAt = Date.now();
|
|
134
|
+
agentState.durationMs = agentState.finishedAt - agentState.startedAt;
|
|
118
135
|
}
|
|
119
136
|
}
|
package/docs/app.js
CHANGED
|
@@ -1,48 +1,9 @@
|
|
|
1
1
|
import { createElement, applyDiff, htm } from './vendor/ui-libs.js';
|
|
2
2
|
import { agentGenerate } from './agent-chat.js';
|
|
3
|
+
import { PROVIDERS, fetchModels, renderEvent, formatStats } from './chat-providers.js';
|
|
3
4
|
|
|
4
5
|
const html = htm.bind(createElement);
|
|
5
6
|
|
|
6
|
-
const PROVIDERS = {
|
|
7
|
-
gemini: { label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', keyPlaceholder: 'GEMINI_API_KEY', models: [] },
|
|
8
|
-
openai: { label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', keyPlaceholder: 'OPENAI_API_KEY', models: ['gpt-4.1', 'gpt-4o', 'gpt-4o-mini', 'o3', 'o4-mini'] },
|
|
9
|
-
xai: { label: 'xAI Grok', baseUrl: 'https://api.x.ai/v1', keyPlaceholder: 'XAI_API_KEY', models: ['grok-3', 'grok-3-mini', 'grok-3-fast'] },
|
|
10
|
-
groq: { label: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', keyPlaceholder: 'GROQ_API_KEY', models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'] },
|
|
11
|
-
mistral: { label: 'Mistral', baseUrl: 'https://api.mistral.ai/v1', keyPlaceholder: 'MISTRAL_API_KEY', models: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'] },
|
|
12
|
-
deepseek: { label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', keyPlaceholder: 'DEEPSEEK_API_KEY', models: ['deepseek-chat', 'deepseek-reasoner'] },
|
|
13
|
-
cerebras: { label: 'Cerebras', baseUrl: 'https://api.cerebras.ai/v1', keyPlaceholder: 'CEREBRAS_API_KEY', models: ['gpt-oss-120b', 'llama3.1-8b'] },
|
|
14
|
-
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'] },
|
|
15
|
-
opencode: { label: 'opencode (zen)', baseUrl: 'http://localhost:4790', keyPlaceholder: '(needs opencode auth login)', models: ['minimax-m2.5-free', 'nemotron-3-super-free'] },
|
|
16
|
-
custom: { label: 'Custom (OpenAI-compat)', baseUrl: '', keyPlaceholder: 'API_KEY', models: [] },
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
async function fetchGeminiModels(apiKey) {
|
|
20
|
-
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
|
|
21
|
-
if (!res.ok) throw new Error(`Models API ${res.status}`);
|
|
22
|
-
const { models = [] } = await res.json();
|
|
23
|
-
return models
|
|
24
|
-
.filter(m => m.supportedGenerationMethods?.includes('generateContent'))
|
|
25
|
-
.map(m => ({ id: m.name.replace('models/', ''), label: m.displayName || m.name }));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function fetchOpenAIModels(baseUrl, apiKey) {
|
|
29
|
-
const url = baseUrl.replace(/\/$/, '') + '/models';
|
|
30
|
-
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${apiKey}` } });
|
|
31
|
-
if (!res.ok) throw new Error(`Models API ${res.status}`);
|
|
32
|
-
const { data = [] } = await res.json();
|
|
33
|
-
return data.map(m => ({ id: m.id, label: m.id })).sort((a, b) => a.id.localeCompare(b.id));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function fetchModels(providerType, baseUrl, apiKey) {
|
|
37
|
-
if (providerType === 'gemini') return fetchGeminiModels(apiKey);
|
|
38
|
-
const staticModels = PROVIDERS[providerType]?.models || [];
|
|
39
|
-
try {
|
|
40
|
-
return await fetchOpenAIModels(baseUrl, apiKey);
|
|
41
|
-
} catch {
|
|
42
|
-
return staticModels.map(id => ({ id, label: id }));
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
7
|
class BirdChat extends HTMLElement {
|
|
47
8
|
constructor() {
|
|
48
9
|
super();
|
|
@@ -69,6 +30,13 @@ class BirdChat extends HTMLElement {
|
|
|
69
30
|
this.render();
|
|
70
31
|
Object.assign(window.__debug, { acp: { baseUrl: this.state.baseUrl, provider: this.state.providerType } });
|
|
71
32
|
if (this.state.apiKey) this.loadModels();
|
|
33
|
+
this.statsTimer = setInterval(() => this.updateStats(), 250);
|
|
34
|
+
}
|
|
35
|
+
disconnectedCallback() { if (this.statsTimer) clearInterval(this.statsTimer); }
|
|
36
|
+
updateStats() {
|
|
37
|
+
const el = this.querySelector('#agent-stats');
|
|
38
|
+
if (!el) return;
|
|
39
|
+
el.textContent = formatStats(window.__debug?.agent);
|
|
72
40
|
}
|
|
73
41
|
|
|
74
42
|
setState(patch) { Object.assign(this.state, patch); this.render(); }
|
|
@@ -133,6 +101,8 @@ class BirdChat extends HTMLElement {
|
|
|
133
101
|
<button class="tui-btn" onclick=${() => this.setState({ messages: [], status: '' })}>[clear]</button>
|
|
134
102
|
</div>
|
|
135
103
|
|
|
104
|
+
<div id="agent-stats" class="tui-agent-stats"></div>
|
|
105
|
+
|
|
136
106
|
<div id="msg-list" class="tui-msglist">
|
|
137
107
|
${messages.map((m, i) => html`
|
|
138
108
|
<div key=${i} class=${'tui-msg ' + m.role}>${typeof m.content === 'string' ? m.content : (m.content || []).filter(b => b.type === 'text').map(b => b.text).join('')}</div>`)}
|
|
@@ -178,14 +148,15 @@ class BirdChat extends HTMLElement {
|
|
|
178
148
|
wrap.appendChild(cursor);
|
|
179
149
|
const list = this.querySelector('#msg-list');
|
|
180
150
|
if (list) list.appendChild(wrap);
|
|
151
|
+
const scroll = () => { const l = this.querySelector('#msg-list'); if (l) l.scrollTop = l.scrollHeight; };
|
|
181
152
|
await agentGenerate(provider, normalizedMessages,
|
|
182
|
-
chunk => { full += chunk; streamEl.textContent = full;
|
|
183
|
-
(
|
|
153
|
+
chunk => { full += chunk; streamEl.textContent = full; scroll(); },
|
|
154
|
+
() => {},
|
|
155
|
+
ev => { const rendered = renderEvent(ev); if (rendered) { full += rendered; streamEl.textContent = full; scroll(); } }
|
|
184
156
|
);
|
|
185
157
|
wrap.remove();
|
|
186
158
|
this.setState({ messages: [...normalizedMessages, { role: 'assistant', content: [{ type: 'text', text: full || '(empty)' }] }], streaming: false, streamingText: '' });
|
|
187
|
-
|
|
188
|
-
if (l2) l2.scrollTop = l2.scrollHeight;
|
|
159
|
+
scroll();
|
|
189
160
|
} catch (err) {
|
|
190
161
|
this.setState({ streaming: false, streamingText: '', status: 'Error: ' + (err?.message || String(err)) });
|
|
191
162
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const PROVIDERS = {
|
|
2
|
+
gemini: { label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', keyPlaceholder: 'GEMINI_API_KEY', models: [] },
|
|
3
|
+
openai: { label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', keyPlaceholder: 'OPENAI_API_KEY', models: ['gpt-4.1', 'gpt-4o', 'gpt-4o-mini', 'o3', 'o4-mini'] },
|
|
4
|
+
xai: { label: 'xAI Grok', baseUrl: 'https://api.x.ai/v1', keyPlaceholder: 'XAI_API_KEY', models: ['grok-3', 'grok-3-mini', 'grok-3-fast'] },
|
|
5
|
+
groq: { label: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', keyPlaceholder: 'GROQ_API_KEY', models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'] },
|
|
6
|
+
mistral: { label: 'Mistral', baseUrl: 'https://api.mistral.ai/v1', keyPlaceholder: 'MISTRAL_API_KEY', models: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'] },
|
|
7
|
+
deepseek: { label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', keyPlaceholder: 'DEEPSEEK_API_KEY', models: ['deepseek-chat', 'deepseek-reasoner'] },
|
|
8
|
+
cerebras: { label: 'Cerebras', baseUrl: 'https://api.cerebras.ai/v1', keyPlaceholder: 'CEREBRAS_API_KEY', models: ['gpt-oss-120b', 'llama3.1-8b'] },
|
|
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
|
+
opencode: { label: 'opencode (zen)', baseUrl: 'http://localhost:4790', keyPlaceholder: '(needs opencode auth login)', models: ['minimax-m2.5-free', 'nemotron-3-super-free'] },
|
|
11
|
+
custom: { label: 'Custom (OpenAI-compat)', baseUrl: '', keyPlaceholder: 'API_KEY', models: [] },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function fetchGeminiModels(apiKey) {
|
|
15
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
|
|
16
|
+
if (!res.ok) throw new Error(`Models API ${res.status}`);
|
|
17
|
+
const { models = [] } = await res.json();
|
|
18
|
+
return models.filter(m => m.supportedGenerationMethods?.includes('generateContent'))
|
|
19
|
+
.map(m => ({ id: m.name.replace('models/', ''), label: m.displayName || m.name }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fetchOpenAIModels(baseUrl, apiKey) {
|
|
23
|
+
const url = baseUrl.replace(/\/$/, '') + '/models';
|
|
24
|
+
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${apiKey}` } });
|
|
25
|
+
if (!res.ok) throw new Error(`Models API ${res.status}`);
|
|
26
|
+
const { data = [] } = await res.json();
|
|
27
|
+
return data.map(m => ({ id: m.id, label: m.id })).sort((a, b) => a.id.localeCompare(b.id));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchModels(providerType, baseUrl, apiKey) {
|
|
31
|
+
if (providerType === 'gemini') return fetchGeminiModels(apiKey);
|
|
32
|
+
const staticModels = PROVIDERS[providerType]?.models || [];
|
|
33
|
+
try { return await fetchOpenAIModels(baseUrl, apiKey); }
|
|
34
|
+
catch { return staticModels.map(id => ({ id, label: id })); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fmtArgs = a => { try { const s = JSON.stringify(a); return s.length > 140 ? s.slice(0, 137) + '...' : s; } catch { return '?'; } };
|
|
38
|
+
const badge = (label, cls) => `\n\n[${cls}] ${label}\n`;
|
|
39
|
+
|
|
40
|
+
const RENDERERS = {
|
|
41
|
+
status: ev => badge('status: ' + ev.message, 'i'),
|
|
42
|
+
'model-info': ev => badge('model: ' + (ev.providerID || '') + '/' + ev.modelID, 'i'),
|
|
43
|
+
'tool-event': ev => badge('tool ' + (ev.status || '') + ': ' + ev.toolName + ' ' + fmtArgs(ev.input), 't'),
|
|
44
|
+
'tool-call': ev => badge('tool: ' + ev.toolName + ' ' + fmtArgs(ev.args), 't'),
|
|
45
|
+
'file-event': ev => badge('file: ' + (ev.filename || ev.url || '?'), 'f'),
|
|
46
|
+
'file-mirrored': ev => badge('wrote: ' + ev.path, 'f'),
|
|
47
|
+
'reasoning-delta': ev => ev.textDelta,
|
|
48
|
+
'step-start': () => badge('step start', 's'),
|
|
49
|
+
'step-finish': ev => badge('step finish' + (ev.tokens ? ' tokens=' + JSON.stringify(ev.tokens) : ''), 's'),
|
|
50
|
+
'unknown-part': ev => badge('?part ' + ev.partType, 'i'),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function renderEvent(ev) { const r = RENDERERS[ev.type]; return r ? r(ev) : ''; }
|
|
54
|
+
|
|
55
|
+
export function formatStats(a) {
|
|
56
|
+
if (!a) return '';
|
|
57
|
+
const dur = a.active ? ((Date.now() - a.startedAt) / 1000).toFixed(1) : ((a.durationMs || 0) / 1000).toFixed(1);
|
|
58
|
+
const parts = [a.active ? '●' : '○', a.provider, a.modelActual || a.model, dur + 's', 'txt=' + a.textChars];
|
|
59
|
+
if (a.reasoningChars) parts.push('rsn=' + a.reasoningChars);
|
|
60
|
+
if (a.toolCalls) parts.push('tool=' + a.toolCalls);
|
|
61
|
+
if (a.files) parts.push('file=' + a.files);
|
|
62
|
+
if (a.steps) parts.push('step=' + a.steps);
|
|
63
|
+
if (a.lastError) parts.push('ERR:' + a.lastError.message.slice(0, 40));
|
|
64
|
+
return parts.join(' · ');
|
|
65
|
+
}
|
package/docs/kilo-http-stream.js
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
import { mirrorFromSandbox } from './kilo-fs-mirror.js';
|
|
2
2
|
|
|
3
|
+
const PART_HANDLERS = {
|
|
4
|
+
text: (part, st, emit) => {
|
|
5
|
+
const prior = st.textByPart.get(part.id) || '';
|
|
6
|
+
const txt = part.text || '';
|
|
7
|
+
if (txt.length > prior.length) {
|
|
8
|
+
emit({ type: 'text-delta', textDelta: txt.slice(prior.length) });
|
|
9
|
+
st.textByPart.set(part.id, txt);
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
reasoning: (part, st, emit) => {
|
|
13
|
+
const prior = st.reasonByPart.get(part.id) || '';
|
|
14
|
+
const txt = part.text || '';
|
|
15
|
+
if (txt.length > prior.length) {
|
|
16
|
+
emit({ type: 'reasoning-delta', textDelta: txt.slice(prior.length) });
|
|
17
|
+
st.reasonByPart.set(part.id, txt);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
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);
|
|
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
|
+
},
|
|
26
|
+
file: (part, st, emit) => {
|
|
27
|
+
if (st.seenFile.has(part.id)) return;
|
|
28
|
+
st.seenFile.add(part.id);
|
|
29
|
+
emit({ type: 'file-event', filename: part.filename || part.path, mime: part.mime, url: part.url, id: part.id });
|
|
30
|
+
},
|
|
31
|
+
'step-start': (part, st, emit) => { if (!st.seenStep.has(part.id+':start')) { st.seenStep.add(part.id+':start'); emit({ type: 'step-start', id: part.id }); } },
|
|
32
|
+
'step-finish': (part, st, emit) => { if (!st.seenStep.has(part.id+':finish')) { st.seenStep.add(part.id+':finish'); emit({ type: 'step-finish', id: part.id, tokens: part.tokens, cost: part.cost }); st.stepFinished = true; } },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function makeState() { return { textByPart: new Map(), reasonByPart: new Map(), seenTool: new Set(), seenFile: new Set(), seenStep: new Set(), stepFinished: false }; }
|
|
36
|
+
|
|
3
37
|
export async function* streamKiloHTTP({ url, model, messages, providerType, agent }) {
|
|
4
38
|
yield { type: 'start-step' };
|
|
5
39
|
const base = (url || 'http://localhost:4780').replace(/\/$/, '');
|
|
6
40
|
const fsBase = base.replace(/:\d+$/, ':4781');
|
|
7
41
|
const isOpencode = providerType === 'opencode';
|
|
8
42
|
const dbgKey = isOpencode ? 'opencode' : 'kilo';
|
|
43
|
+
yield { type: 'status', message: 'connecting ' + base };
|
|
9
44
|
let sessRes;
|
|
10
45
|
try { sessRes = await fetch(base + '/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); }
|
|
11
46
|
catch (e) { throw new Error(dbgKey + ' serve not reachable at ' + base + ' — start it with: node start-kilo.js --origin ' + location.origin); }
|
|
12
47
|
if (!sessRes.ok) throw new Error('/session ' + sessRes.status + ': ' + await sessRes.text());
|
|
13
48
|
const { id: sessionId } = await sessRes.json();
|
|
14
|
-
Object.assign(window.__debug = window.__debug || {}, { [dbgKey]: { sessionId, url: base, fsBase, lastStatus: null } });
|
|
49
|
+
Object.assign(window.__debug = window.__debug || {}, { [dbgKey]: { sessionId, url: base, fsBase, lastStatus: null, events: [] } });
|
|
50
|
+
yield { type: 'status', message: 'session ' + sessionId.slice(0, 8) };
|
|
15
51
|
|
|
16
52
|
const userText = messages.filter(m => m.role === 'user').map(m =>
|
|
17
53
|
typeof m.content === 'string' ? m.content : (m.content || []).filter(b => b.type === 'text').map(b => b.text).join('')
|
|
@@ -23,55 +59,69 @@ export async function* streamKiloHTTP({ url, model, messages, providerType, agen
|
|
|
23
59
|
if (isOpencode) body.model = { providerID: 'opencode', modelID: modelId };
|
|
24
60
|
else { body.providerID = 'kilo'; body.modelID = modelId; }
|
|
25
61
|
|
|
26
|
-
|
|
62
|
+
const st = makeState();
|
|
63
|
+
const emit = ev => { (window.__debug[dbgKey].events ||= []).push({ ...ev, t: Date.now() }); };
|
|
64
|
+
|
|
27
65
|
if (isOpencode) {
|
|
28
66
|
const es = new EventSource(base + '/event');
|
|
29
|
-
const textByPart = new Map();
|
|
30
67
|
const assistantMsgs = new Set();
|
|
31
|
-
let stepFinished = false;
|
|
32
68
|
const pending = [];
|
|
33
69
|
let resolveNext = null;
|
|
34
70
|
const wake = () => { if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } };
|
|
71
|
+
const push = ev => { emit(ev); pending.push(ev); wake(); };
|
|
35
72
|
es.onmessage = e => {
|
|
36
73
|
try {
|
|
37
74
|
const m = JSON.parse(e.data);
|
|
38
75
|
if (m.type === 'message.updated') {
|
|
39
76
|
const info = m.properties?.info;
|
|
40
|
-
if (info?.sessionID === sessionId && info.role === 'assistant') assistantMsgs.add(info.id);
|
|
77
|
+
if (info?.sessionID === sessionId && info.role === 'assistant') { assistantMsgs.add(info.id); if (info.modelID) push({ type: 'model-info', modelID: info.modelID, providerID: info.providerID }); }
|
|
41
78
|
} else if (m.type === 'message.part.updated') {
|
|
42
79
|
const part = m.properties?.part;
|
|
43
80
|
if (part?.sessionID !== sessionId || !assistantMsgs.has(part.messageID)) return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (txt.length > prior.length) { pending.push({ type:'text-delta', textDelta: txt.slice(prior.length) }); textByPart.set(part.id, txt); wake(); }
|
|
48
|
-
} else if (part.type === 'step-finish') { stepFinished = true; wake(); }
|
|
81
|
+
const h = PART_HANDLERS[part.type];
|
|
82
|
+
if (h) h(part, st, push);
|
|
83
|
+
else push({ type: 'unknown-part', partType: part.type, id: part.id });
|
|
49
84
|
}
|
|
50
85
|
} catch (_) {}
|
|
51
86
|
};
|
|
87
|
+
yield { type: 'status', message: 'POST /message' };
|
|
52
88
|
const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
53
89
|
window.__debug[dbgKey].lastStatus = msgRes.status;
|
|
90
|
+
yield { type: 'status', message: 'msg ' + msgRes.status };
|
|
54
91
|
if (!msgRes.ok) { es.close(); throw new Error('message ' + msgRes.status + ': ' + await msgRes.text()); }
|
|
55
92
|
const deadline = Date.now() + 180000;
|
|
56
93
|
let graceUntil = 0;
|
|
57
94
|
while (true) {
|
|
58
|
-
if (pending.length) {
|
|
59
|
-
if (stepFinished) { if (!graceUntil) graceUntil = Date.now() + 1500; if (Date.now() > graceUntil) break; }
|
|
95
|
+
if (pending.length) { yield pending.shift(); continue; }
|
|
96
|
+
if (st.stepFinished) { if (!graceUntil) graceUntil = Date.now() + 1500; if (Date.now() > graceUntil) break; }
|
|
60
97
|
if (Date.now() > deadline) break;
|
|
61
98
|
await new Promise(r => { resolveNext = r; setTimeout(r, 500); });
|
|
62
99
|
}
|
|
63
100
|
es.close();
|
|
64
101
|
} else {
|
|
102
|
+
yield { type: 'status', message: 'POST /message' };
|
|
65
103
|
const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
66
104
|
window.__debug[dbgKey].lastStatus = msgRes.status;
|
|
105
|
+
yield { type: 'status', message: 'msg ' + msgRes.status };
|
|
67
106
|
if (!msgRes.ok) throw new Error('message ' + msgRes.status + ': ' + await msgRes.text());
|
|
68
107
|
const result = await msgRes.json();
|
|
69
108
|
window.__debug[dbgKey].lastResult = result;
|
|
70
|
-
|
|
109
|
+
const info = result.info || {};
|
|
110
|
+
if (info.modelID) { const ev = { type: 'model-info', modelID: info.modelID, providerID: info.providerID }; emit(ev); yield ev; }
|
|
111
|
+
for (const part of (result.parts || [])) {
|
|
112
|
+
const h = PART_HANDLERS[part.type];
|
|
113
|
+
const pending = [];
|
|
114
|
+
const pushLocal = ev => { emit(ev); pending.push(ev); };
|
|
115
|
+
if (h) h(part, st, pushLocal);
|
|
116
|
+
else pushLocal({ type: 'unknown-part', partType: part.type, id: part.id });
|
|
117
|
+
for (const ev of pending) yield ev;
|
|
118
|
+
}
|
|
71
119
|
}
|
|
120
|
+
yield { type: 'status', message: 'mirror sandbox' };
|
|
72
121
|
const mirrored = await mirrorFromSandbox(fsBase);
|
|
73
122
|
if (mirrored.length) {
|
|
74
123
|
window.__debug[dbgKey].writes = mirrored;
|
|
124
|
+
for (const path of mirrored) yield { type: 'file-mirrored', path };
|
|
75
125
|
window.showPreview?.();
|
|
76
126
|
window.refreshPreview?.();
|
|
77
127
|
}
|
package/docs/tui.css
CHANGED
|
@@ -123,6 +123,21 @@ html, body { background: var(--paper); color: var(--ink); height: 100%; margin:
|
|
|
123
123
|
font-size: 12px;
|
|
124
124
|
}
|
|
125
125
|
.tui-toolbar label { color: var(--ink-dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; }
|
|
126
|
+
.tui-agent-stats {
|
|
127
|
+
padding: 4px 2ch;
|
|
128
|
+
border-bottom: 1px solid var(--ink-hair);
|
|
129
|
+
background: var(--surface);
|
|
130
|
+
font-size: 10px;
|
|
131
|
+
color: var(--ink-dim);
|
|
132
|
+
text-transform: uppercase;
|
|
133
|
+
letter-spacing: 0.05em;
|
|
134
|
+
min-height: 18px;
|
|
135
|
+
font-family: var(--ff-mono);
|
|
136
|
+
white-space: nowrap;
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
text-overflow: ellipsis;
|
|
139
|
+
}
|
|
140
|
+
.tui-agent-stats:empty { display: none; }
|
|
126
141
|
.tui-msglist {
|
|
127
142
|
flex: 1;
|
|
128
143
|
overflow-y: auto;
|
package/package.json
CHANGED