anentrypoint-design 0.0.87 → 0.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.87",
3
+ "version": "0.0.89",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -1,5 +1,5 @@
1
1
  import * as webjsx from '../../../vendor/webjsx/index.js';
2
- import { Panel, Row, Hero, Receipt, Kpi, Table, Form } from '../content.js';
2
+ import { Panel, Hero, Receipt, Kpi, Table, Form } from '../content.js';
3
3
  import { Chip } from '../shell.js';
4
4
  import { EmptyState } from '../files.js';
5
5
  import { skillLabel } from './helpers.js';
@@ -24,7 +24,10 @@ export async function models(h0) {
24
24
  const ms = Array.isArray(probeState[p.name]) ? probeState[p.name] : p.models;
25
25
  const loading = probeState[p.name] === 'loading';
26
26
  if (loading) probedPanels.push(Panel({ title: p.name + ' ⏳', children: h('span', { class: 'fd-muted' }, 'probing…') }));
27
- else if (ms && ms.length > 0) probedPanels.push(Panel({ title: p.name + (p.available ? ' ●' : ' ○'), count: ms.length, children: Table({ headers: ['model id'], rows: ms.map(m => [m]) }) }));
27
+ else if (ms && ms.length > 0) probedPanels.push(Panel({ title: p.name + (p.available ? ' ●' : ' ○'), count: ms.length, children: h('div', { class: 'fd-list fd-list-compact' }, ...ms.map((m, i) => h('div', { key: m, class: 'fd-list-row', 'data-cat': 'preview' },
28
+ h('span', { class: 'fd-list-code' }, String(i+1).padStart(2,'0')),
29
+ h('div', { class: 'fd-list-main' }, h('div', { class: 'fd-list-title fd-mono' }, m))
30
+ ))) }));
28
31
  else unprobedRows.push([p.name, p.available ? 'available' : 'unavailable', p.modelsError ? 'error: '+p.modelsError : '—']);
29
32
  }
30
33
  const modelPanels = [
@@ -64,7 +67,16 @@ export async function cron(h0) {
64
67
  ['weekdays 18:00', '0 18 * * 1-5'],
65
68
  ['every 15 min', '*/15 * * * *']
66
69
  ] }) }),
67
- Panel({ title: 'jobs', count: list.length, children: list.length === 0 ? EmptyState({ text: 'no cron jobs — add one with the form above', glyph: '◷' }) : Table({ headers: ['id','cron','prompt','enabled'], rows: list.map(j => [j.id, j.cron, (j.prompt||'').slice(0,40), j.enabled ? 'yes' : 'no']) }) })
70
+ Panel({ title: 'jobs', count: list.length, children: list.length === 0
71
+ ? EmptyState({ text: 'no cron jobs — add one with the form above', glyph: '◷' })
72
+ : h('div', { class: 'fd-list' }, ...list.map(j => h('div', { key: j.id, class: 'fd-list-row', 'data-cat': j.enabled ? 'kit' : 'external' },
73
+ h('span', { class: 'fd-list-code' }, j.enabled ? '●' : '○'),
74
+ h('div', { class: 'fd-list-main' },
75
+ h('div', { class: 'fd-list-title' }, (j.prompt||'(no prompt)').slice(0,80)),
76
+ h('div', { class: 'fd-list-sub' }, j.cron + ' · ' + j.id)
77
+ ),
78
+ h('div', { class: 'fd-list-meta' }, h('span', { class: 'fd-list-meta-mono' }, j.enabled ? 'enabled' : 'disabled'))
79
+ ))) })
68
80
  ];
69
81
  }
70
82
 
@@ -75,7 +87,13 @@ export async function skills(h0) {
75
87
  Hero({ title: 'skills', body: 'SKILL.md bundles loaded from ~/.freddie/skills + plugins.', accent: list.length+' loaded' }),
76
88
  Kpi({ items: [[list.length,'skills'],[Object.keys(byCat).length,'categories']] }),
77
89
  list.length === 0 ? EmptyState({ text: 'no skills — add SKILL.md files to ~/.freddie/skills/', glyph: '◈' }) : null,
78
- ...Object.entries(byCat).map(([cat, ss]) => Panel({ title: cat, count: ss.length, children: Table({ headers: ['name','description'], rows: ss.map(s => [skillLabel(s), (s.description||'').slice(0,120)]) }) }))
90
+ ...Object.entries(byCat).map(([cat, ss]) => Panel({ title: cat, count: ss.length, children: h('div', { class: 'fd-list' }, ...ss.map((s, i) => h('div', { key: s.name, class: 'fd-list-row', 'data-cat': cat === 'creative' ? 'preview' : cat === 'software-development' ? 'kit' : cat === 'planning' ? 'doc' : cat === 'ops' ? 'external' : 'doc' },
91
+ h('span', { class: 'fd-list-code' }, String(i+1).padStart(2,'0')),
92
+ h('div', { class: 'fd-list-main' },
93
+ h('div', { class: 'fd-list-title' }, skillLabel(s)),
94
+ s.description ? h('div', { class: 'fd-list-sub' }, (s.description||'').slice(0,140)) : null
95
+ )
96
+ ))) }))
79
97
  ].filter(Boolean);
80
98
  }
81
99
 
@@ -114,62 +132,20 @@ export async function tools(h0) {
114
132
  Hero({ title: 'tools', body: 'every tool the agent can call. param count + required env per row.', accent: list.length+' tools' }),
115
133
  Kpi({ items: [[list.length,'tools'],[filtered.length,'shown'],[Object.keys(bySet).length,'toolsets']] }),
116
134
  Panel({ title: 'filter', right: search, children: filtered.length === 0 ? EmptyState({ text: 'no matches for "'+f.q+'"', glyph: '⌕' }) : h('span', { class: 'fd-muted' }, filtered.length+' / '+list.length+' tools shown') }),
117
- ...Object.entries(bySet).map(([ts, items]) => Panel({ title: 'toolset · '+ts, count: items.length, children: items.map(t => {
135
+ ...Object.entries(bySet).map(([ts, items]) => Panel({ title: 'toolset · '+ts, count: items.length, children: h('div', { class: 'fd-list' }, ...items.map(t => {
118
136
  const params = t.schema?.parameters?.properties ? Object.keys(t.schema.parameters.properties).length : 0;
119
137
  const reqEnv = Array.isArray(t.requiresEnv) ? t.requiresEnv : [];
120
- const meta = h('span', { class: 'fd-tool-meta' },
121
- params > 0 ? Chip({ tone: 'neutral', children: params+' params' }) : null,
122
- ...reqEnv.map(k => Chip({ tone: envIsSet(k) ? 'ok' : 'miss', children: k }))
123
- );
124
- return Row({ key: t.name, code: '', title: t.name, sub: (t.description || (t.schema && t.schema.description) || '').slice(0,80), meta });
125
- }) }))
138
+ return h('div', { key: t.name, class: 'fd-list-row', 'data-cat': 'kit' },
139
+ h('span', { class: 'fd-list-code' }, ''),
140
+ h('div', { class: 'fd-list-main' },
141
+ h('div', { class: 'fd-list-title fd-mono' }, t.name),
142
+ h('div', { class: 'fd-list-sub' }, (t.description || (t.schema && t.schema.description) || '').slice(0,120))
143
+ ),
144
+ h('div', { class: 'fd-list-meta' },
145
+ params > 0 ? Chip({ tone: 'neutral', children: params+' params' }) : null,
146
+ ...reqEnv.map(k => Chip({ tone: envIsSet(k) ? 'ok' : 'miss', children: k }))
147
+ ));
148
+ })) }))
126
149
  ];
127
150
  }
128
151
 
129
- export async function batch(h0) {
130
- const results = window.__fd_batchResults = window.__fd_batchResults || [];
131
- const status = window.__fd_batchStatus = window.__fd_batchStatus || { running: false, error: null };
132
- const onSubmit = async ev => {
133
- const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
134
- if (!prompts.length) return;
135
- status.running = true; status.error = null; window.__fd_batchResults = [];
136
- if (typeof window.__fd_nav === 'function') window.__fd_nav('batch');
137
- try {
138
- const r = await h0.pi.batch.run(prompts, Number(ev.target.elements.concurrency.value) || 4);
139
- const arr = Array.isArray(r) ? r : (r && typeof r === 'object' ? Object.entries(r).map(([k, v]) => ({ prompt: k, result: v })) : []);
140
- window.__fd_batchResults = arr;
141
- } catch (e) { status.error = e.message || String(e); }
142
- status.running = false;
143
- if (typeof window.__fd_nav === 'function') window.__fd_nav('batch');
144
- };
145
- const resPanel = status.running ? Panel({ title: 'results', children: h('span', {}, 'running…') })
146
- : status.error ? Panel({ title: 'results', children: h('span', { class: 'fd-muted' }, 'error: '+status.error) })
147
- : results.length === 0 ? Panel({ title: 'results', children: EmptyState({ text: 'no results yet', glyph: '⊞' }) })
148
- : Panel({ title: 'results', count: results.length, children: Table({ headers: ['#','prompt','result','status'], rows: results.map((it, i) => [String(i+1), (it.prompt||'').slice(0,60), (it.result||it.error||'').slice(0,120), it.error ? 'error' : 'ok']) }) });
149
- return [
150
- Hero({ title: 'batch', body: 'run many prompts in parallel. one per line.', accent: results.length ? results.length+' results' : 'idle' }),
151
- Panel({ title: 'run batch', children: Form({ fields: [
152
- { name: 'prompts', kind: 'textarea', placeholder: 'one prompt per line', rows: 6 },
153
- { name: 'concurrency', type: 'number', value: '4' }
154
- ], submit: 'run', onSubmit }) }),
155
- resPanel
156
- ];
157
- }
158
-
159
- export async function gateway(h0) {
160
- const platforms = typeof h0.pi.gateway?.platforms === 'function' ? h0.pi.gateway.platforms() : [];
161
- const active = platforms.filter(p => p.enabled);
162
- const envIsSet = k => typeof h0.pi.env?.isSet === 'function' ? h0.pi.env.isSet(k) : false;
163
- return [
164
- Hero({ title: 'gateway', body: 'webhook + bot platforms. each adapter declares required env keys.', accent: active.length+' / '+platforms.length+' active' }),
165
- Kpi({ items: [[platforms.length,'platforms'],[active.length,'active']] }),
166
- Panel({ title: 'platforms', count: platforms.length, right: active.length > 0 ? Chip({ tone: 'ok', children: active.length+' active' }) : Chip({ tone: 'miss', children: 'none active' }),
167
- children: platforms.length === 0 ? EmptyState({ text: 'no platforms registered', glyph: '⇌' }) : platforms.map(p => {
168
- const reqEnv = Array.isArray(p.requiresEnv) ? p.requiresEnv : [];
169
- const setN = reqEnv.filter(envIsSet).length;
170
- const envSummary = reqEnv.length === 0 ? '' : setN === reqEnv.length ? '✓ env ready' : 'missing '+(reqEnv.length-setN)+' / '+reqEnv.length;
171
- return Row({ key: p.name, code: p.enabled ? '●' : '○', title: p.name, sub: p.note || '', meta: [p.enabled ? 'enabled' : '', envSummary].filter(Boolean).join(' · ') });
172
- })
173
- })
174
- ];
175
- }
@@ -1,5 +1,6 @@
1
1
  import * as webjsx from '../../../vendor/webjsx/index.js';
2
- import { Panel, Row, Hero, Receipt, Kpi, Table } from '../content.js';
2
+ import { Panel, Hero, Receipt, Kpi } from '../content.js';
3
+ import { Chip } from '../shell.js';
3
4
  import { EmptyState } from '../files.js';
4
5
  import { skillLabel } from './helpers.js';
5
6
  const h = webjsx.createElement;
@@ -25,40 +26,77 @@ export async function home(h0) {
25
26
  ];
26
27
  }
27
28
 
29
+ function relTime(ts) {
30
+ if (!ts) return '';
31
+ const t = typeof ts === 'string' ? Date.parse(ts) : Number(ts);
32
+ if (!Number.isFinite(t)) return '';
33
+ const diff = (Date.now() - t) / 1000;
34
+ if (diff < 60) return Math.floor(diff)+'s ago';
35
+ if (diff < 3600) return Math.floor(diff/60)+'m ago';
36
+ if (diff < 86400) return Math.floor(diff/3600)+'h ago';
37
+ if (diff < 86400*30) return Math.floor(diff/86400)+'d ago';
38
+ return new Date(t).toISOString().slice(0,10);
39
+ }
40
+
41
+ const PLATFORM_CAT = { web: 'kit', cli: 'doc', telegram: 'preview', slack: 'preview', discord: 'preview', api_server: 'external', webhook: 'external' };
42
+
28
43
  export async function sessions(h0) {
29
44
  const list = await h0.pi.sessions.list();
30
45
  const f = window.__fd_sessFilter = window.__fd_sessFilter || { q: '' };
31
46
  const q = (f.q || '').toLowerCase();
32
47
  const filtered = q ? list.filter(s => (s.title||'').toLowerCase().includes(q) || (s.id||'').includes(q) || (s.platform||'').toLowerCase().includes(q) || (s.cwd||'').toLowerCase().includes(q)) : list;
33
- const rows = filtered.map(s => {
34
- const cont = h('button', { class: 'btn-primary', onclick: async () => {
35
- const msgs = await h0.pi.sessions.getMessages(s.id);
36
- const cs = window.__fd_chatState = window.__fd_chatState || { messages: [], busy: false, sessionId: null, cwd: '', skill: '', provider: '', model: '' };
37
- cs.sessionId = s.id;
38
- cs.messages = msgs.map(m => ({ role: m.role, content: String(m.content || '') }));
39
- if (s.cwd) cs.cwd = s.cwd;
40
- if (s.skill) cs.skill = s.skill;
41
- if (typeof window.__fd_nav === 'function') window.__fd_nav('chat');
42
- } }, 'continue');
43
- return [(s.id||'').slice(0,8), s.title||'—', s.platform||'—', s.model||'—', s.cwd?s.cwd.slice(-30):'', s.skill?skillLabel({name:s.skill}):'—', cont];
48
+ const openSession = async s => {
49
+ const msgs = await h0.pi.sessions.getMessages(s.id);
50
+ const cs = window.__fd_chatState = window.__fd_chatState || { messages: [], busy: false, sessionId: null, cwd: '', skill: '', provider: '', model: '' };
51
+ cs.sessionId = s.id;
52
+ cs.messages = msgs.map(m => ({ role: m.role, content: String(m.content || '') }));
53
+ if (s.cwd) cs.cwd = s.cwd;
54
+ if (s.skill) cs.skill = s.skill;
55
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('chat');
56
+ };
57
+ const items = filtered.map(s => {
58
+ const cat = PLATFORM_CAT[s.platform] || 'doc';
59
+ const subParts = [s.platform || '—', s.model || '—', s.skill ? skillLabel({name:s.skill}) : null].filter(Boolean);
60
+ const cwdMeta = s.cwd ? '…'+s.cwd.slice(-28) : '';
61
+ const tsMeta = relTime(s.updated_at || s.created_at || s.ts);
62
+ return h('div', { key: s.id, class: 'fd-list-row', 'data-cat': cat, role: 'button', tabindex: '0', onclick: () => openSession(s), onkeydown: ev => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openSession(s); } } },
63
+ h('span', { class: 'fd-list-code' }, (s.id||'').slice(0,8)),
64
+ h('div', { class: 'fd-list-main' },
65
+ h('div', { class: 'fd-list-title' }, s.title || '(untitled)'),
66
+ h('div', { class: 'fd-list-sub' }, subParts.join(' · '))
67
+ ),
68
+ h('div', { class: 'fd-list-meta' },
69
+ cwdMeta ? h('span', { class: 'fd-list-meta-mono' }, cwdMeta) : null,
70
+ tsMeta ? h('span', { class: 'fd-list-meta-rel' }, tsMeta) : null
71
+ )
72
+ );
44
73
  });
45
- const search = h('input', { type: 'search', placeholder: 'filter by title / id / platform / cwd…', value: f.q, oninput: ev => { f.q = ev.target.value; if (typeof window.__fd_nav === 'function') window.__fd_nav('sessions'); }, class: 'fd-search' });
74
+ const search = h('input', { type: 'search', 'aria-label': 'filter sessions', placeholder: 'filter by title / id / platform / cwd…', value: f.q, oninput: ev => { f.q = ev.target.value; if (typeof window.__fd_nav === 'function') window.__fd_nav('sessions'); }, class: 'fd-search' });
75
+ const body = list.length === 0
76
+ ? EmptyState({ text: 'no sessions yet — start one in /chat', glyph: '✉' })
77
+ : filtered.length === 0
78
+ ? EmptyState({ text: 'no matches for "'+f.q+'"', glyph: '⌕' })
79
+ : h('div', { class: 'fd-list' }, ...items);
46
80
  return [
47
- Hero({ title: 'sessions', body: 'every chat turn lives here.', accent: list.length+' total' }),
81
+ Hero({ title: 'sessions', body: 'every chat turn lives here. click a row to continue.', accent: list.length+' total' }),
48
82
  Kpi({ items: [[list.length,'sessions'],[filtered.length,'shown']] }),
49
- Panel({ title: 'sessions', count: filtered.length, right: search, children: list.length === 0 ? EmptyState({ text: 'no sessions yet — start one in /chat', glyph: '✉' }) : filtered.length === 0 ? EmptyState({ text: 'no matches for "'+f.q+'"', glyph: '⌕' }) : Table({ headers: ['id','title','platform','model','cwd','skill',''], rows }) })
83
+ Panel({ title: 'sessions', count: filtered.length, right: search, children: body })
50
84
  ];
51
85
  }
52
86
 
53
87
  export async function projects(h0) {
54
88
  const list = h0.pi.projects.list();
55
89
  const active = h0.pi.projects.active();
56
- const rows = list.map(p => Row({
57
- key: p.name,
58
- code: p.name === active?.name ? '' : '',
59
- title: p.name + (p.name === active?.name ? ' (active)' : ''),
60
- meta: p.path,
61
- onClick: () => { if (p.name !== active?.name) h0.pi.projects.setActive(p.name); }
90
+ const rows = h('div', { class: 'fd-list' }, ...list.map(p => {
91
+ const isActive = p.name === active?.name;
92
+ return h('div', { key: p.name, class: 'fd-list-row', 'data-cat': isActive ? 'kit' : 'doc', role: 'button', tabindex: '0',
93
+ onclick: () => { if (!isActive) h0.pi.projects.setActive(p.name); },
94
+ onkeydown: ev => { if ((ev.key === 'Enter' || ev.key === ' ') && !isActive) { ev.preventDefault(); h0.pi.projects.setActive(p.name); } } },
95
+ h('span', { class: 'fd-list-code' }, isActive ? '●' : '○'),
96
+ h('div', { class: 'fd-list-main' },
97
+ h('div', { class: 'fd-list-title' }, p.name + (isActive ? ' (active)' : '')),
98
+ h('div', { class: 'fd-list-sub' }, p.path)
99
+ ));
62
100
  }));
63
101
  return [
64
102
  Hero({ title: 'projects', body: 'each project is its own ~/.freddie home.', accent: active ? 'active · '+active.name : 'no active project' }),
@@ -68,7 +106,7 @@ export async function projects(h0) {
68
106
  h('input', { name: 'path', placeholder: '/abs/path' }),
69
107
  h('button', { type: 'submit', class: 'btn-primary' }, 'add')
70
108
  ) }),
71
- Panel({ title: 'all projects', count: list.length, children: rows.length ? rows : EmptyState({ text: 'no projects', glyph: '◆' }) })
109
+ Panel({ title: 'all projects', count: list.length, children: list.length ? rows : EmptyState({ text: 'no projects', glyph: '◆' }) })
72
110
  ];
73
111
  }
74
112
 
@@ -89,7 +127,16 @@ export async function agents(h0) {
89
127
  ] }) }),
90
128
  Panel({ title: 'recent sessions', count: recent.length, children: recent.length === 0
91
129
  ? EmptyState({ text: 'no recent sessions', glyph: '✉' })
92
- : Table({ headers: ['id','title','platform','turns'], rows: recent.map(s => [(s.id||'').slice(0,8), s.title||'', s.platform||'', String(s.turns ?? s.message_count ?? '')]) })
130
+ : h('div', { class: 'fd-list' }, ...recent.map(s => h('div', { key: s.id, class: 'fd-list-row', 'data-cat': PLATFORM_CAT[s.platform] || 'doc' },
131
+ h('span', { class: 'fd-list-code' }, (s.id||'').slice(0,8)),
132
+ h('div', { class: 'fd-list-main' },
133
+ h('div', { class: 'fd-list-title' }, s.title || '(untitled)'),
134
+ h('div', { class: 'fd-list-sub' }, [s.platform||'—', s.model||'—'].join(' · '))
135
+ ),
136
+ h('div', { class: 'fd-list-meta' },
137
+ h('span', { class: 'fd-list-meta-mono' }, String(s.turns ?? s.message_count ?? 0)+' turns')
138
+ )
139
+ )))
93
140
  })
94
141
  ];
95
142
  }
@@ -102,10 +149,15 @@ export async function analytics(h0) {
102
149
  const byModel = list.reduce((a,s) => { const k = s.model||'(unset)'; a[k] = (a[k]||0)+1; return a; }, {});
103
150
  const byToolset = tools.reduce((a,t) => { const k = t.toolset||'core'; a[k] = (a[k]||0)+1; return a; }, {});
104
151
  const bySkillCat = skills.reduce((a,s) => { const k = s.category||'other'; a[k] = (a[k]||0)+1; return a; }, {});
105
- const sortDesc = obj => Object.entries(obj).sort((a,b) => b[1]-a[1]).map(([k,v]) => [k, String(v)]);
152
+ const sortDesc = obj => Object.entries(obj).sort((a,b) => b[1]-a[1]);
153
+ const CAT_BY_TITLE = { 'by platform': 'kit', 'by model': 'preview', 'by toolset': 'doc', 'by skill category': 'external' };
106
154
  const mkPanel = (title, obj, glyph) => Panel({ title, count: Object.keys(obj).length, children: Object.keys(obj).length === 0
107
155
  ? EmptyState({ text: 'no data', glyph })
108
- : Table({ headers: ['name','count'], rows: sortDesc(obj) })
156
+ : h('div', { class: 'fd-list fd-list-compact' }, ...sortDesc(obj).map(([k, n], i) => h('div', { key: k, class: 'fd-list-row', 'data-cat': CAT_BY_TITLE[title] || 'doc' },
157
+ h('span', { class: 'fd-list-code' }, String(i+1).padStart(2,'0')),
158
+ h('div', { class: 'fd-list-main' }, h('div', { class: 'fd-list-title' }, k)),
159
+ h('div', { class: 'fd-list-meta' }, h('span', { class: 'fd-list-meta-mono' }, String(n)))
160
+ )))
109
161
  });
110
162
  return [
111
163
  Hero({ title: 'analytics', body: 'shape of work — what platforms, what models, what tools.' }),
@@ -0,0 +1,69 @@
1
+ import * as webjsx from '../../../vendor/webjsx/index.js';
2
+ import { Panel, Hero, Kpi, Form } from '../content.js';
3
+ import { Chip } from '../shell.js';
4
+ import { EmptyState } from '../files.js';
5
+ const h = webjsx.createElement;
6
+
7
+ export async function batch(h0) {
8
+ const results = window.__fd_batchResults = window.__fd_batchResults || [];
9
+ const status = window.__fd_batchStatus = window.__fd_batchStatus || { running: false, error: null };
10
+ const onSubmit = async ev => {
11
+ const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
12
+ if (!prompts.length) return;
13
+ status.running = true; status.error = null; window.__fd_batchResults = [];
14
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('batch');
15
+ try {
16
+ const r = await h0.pi.batch.run(prompts, Number(ev.target.elements.concurrency.value) || 4);
17
+ const arr = Array.isArray(r) ? r : (r && typeof r === 'object' ? Object.entries(r).map(([k, v]) => ({ prompt: k, result: v })) : []);
18
+ window.__fd_batchResults = arr;
19
+ } catch (e) { status.error = e.message || String(e); }
20
+ status.running = false;
21
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('batch');
22
+ };
23
+ const resPanel = status.running ? Panel({ title: 'results', children: h('span', {}, 'running…') })
24
+ : status.error ? Panel({ title: 'results', children: h('span', { class: 'fd-muted' }, 'error: '+status.error) })
25
+ : results.length === 0 ? Panel({ title: 'results', children: EmptyState({ text: 'no results yet', glyph: '⊞' }) })
26
+ : Panel({ title: 'results', count: results.length, children: h('div', { class: 'fd-list' }, ...results.map((it, i) => h('div', { key: i, class: 'fd-list-row', 'data-cat': it.error ? 'external' : 'kit' },
27
+ h('span', { class: 'fd-list-code' }, String(i+1).padStart(2,'0')),
28
+ h('div', { class: 'fd-list-main' },
29
+ h('div', { class: 'fd-list-title' }, (it.prompt||'').slice(0,80)),
30
+ h('div', { class: 'fd-list-sub' }, (it.result||it.error||'').slice(0,160))
31
+ ),
32
+ h('div', { class: 'fd-list-meta' }, h('span', { class: 'fd-list-meta-mono' }, it.error ? 'error' : 'ok'))
33
+ ))) });
34
+ return [
35
+ Hero({ title: 'batch', body: 'run many prompts in parallel. one per line.', accent: results.length ? results.length+' results' : 'idle' }),
36
+ Panel({ title: 'run batch', children: Form({ fields: [
37
+ { name: 'prompts', kind: 'textarea', placeholder: 'one prompt per line', rows: 6 },
38
+ { name: 'concurrency', type: 'number', value: '4' }
39
+ ], submit: 'run', onSubmit }) }),
40
+ resPanel
41
+ ];
42
+ }
43
+
44
+ export async function gateway(h0) {
45
+ const platforms = typeof h0.pi.gateway?.platforms === 'function' ? h0.pi.gateway.platforms() : [];
46
+ const active = platforms.filter(p => p.enabled);
47
+ const envIsSet = k => typeof h0.pi.env?.isSet === 'function' ? h0.pi.env.isSet(k) : false;
48
+ return [
49
+ Hero({ title: 'gateway', body: 'webhook + bot platforms. each adapter declares required env keys.', accent: active.length+' / '+platforms.length+' active' }),
50
+ Kpi({ items: [[platforms.length,'platforms'],[active.length,'active']] }),
51
+ Panel({ title: 'platforms', count: platforms.length, right: active.length > 0 ? Chip({ tone: 'ok', children: active.length+' active' }) : Chip({ tone: 'miss', children: 'none active' }),
52
+ children: platforms.length === 0 ? EmptyState({ text: 'no platforms registered', glyph: '⇌' }) : h('div', { class: 'fd-list' }, ...platforms.map(p => {
53
+ const reqEnv = Array.isArray(p.requiresEnv) ? p.requiresEnv : [];
54
+ const setN = reqEnv.filter(envIsSet).length;
55
+ const envSummary = reqEnv.length === 0 ? '' : setN === reqEnv.length ? '✓ env ready' : 'missing '+(reqEnv.length-setN)+' / '+reqEnv.length;
56
+ return h('div', { key: p.name, class: 'fd-list-row', 'data-cat': p.enabled ? 'kit' : 'external' },
57
+ h('span', { class: 'fd-list-code' }, p.enabled ? '●' : '○'),
58
+ h('div', { class: 'fd-list-main' },
59
+ h('div', { class: 'fd-list-title' }, p.name),
60
+ h('div', { class: 'fd-list-sub' }, p.note || '—')
61
+ ),
62
+ h('div', { class: 'fd-list-meta' },
63
+ h('span', { class: 'fd-list-meta-mono' }, p.enabled ? 'enabled' : 'disabled'),
64
+ envSummary ? h('span', { class: 'fd-list-meta-rel' }, envSummary) : null
65
+ ));
66
+ }))
67
+ })
68
+ ];
69
+ }
@@ -76,10 +76,11 @@ export async function voice(h0) {
76
76
  ),
77
77
  lines.length === 0 && !s.partial
78
78
  ? EmptyState({ text: s.listening ? 'listening — speak into your mic' : 'press start to capture speech', glyph: '◌' })
79
- : h('div', { class: 'fd-voice-log', role: 'log', 'aria-live': 'polite' },
80
- ...lines.map((it, i) => h('div', { key: i, class: 'fd-voice-line' },
81
- h('span', { class: 'fd-voice-ts' }, new Date(it.ts).toLocaleTimeString()),
82
- h('span', { class: 'fd-voice-text' }, it.text))))
79
+ : h('div', { class: 'fd-list', role: 'log', 'aria-live': 'polite' },
80
+ ...lines.map((it, i) => h('div', { key: i, class: 'fd-list-row', 'data-cat': 'doc' },
81
+ h('span', { class: 'fd-list-code' }, new Date(it.ts).toLocaleTimeString().slice(0,8)),
82
+ h('div', { class: 'fd-list-main' }, h('div', { class: 'fd-list-title' }, it.text))
83
+ )))
83
84
  ] });
84
85
 
85
86
  const ttsPanel = !ttsSupported
@@ -1,13 +1,15 @@
1
1
  export { skillLabel, getRecentPaths, saveRecentPath, renderChatMessages } from './freddie/helpers.js';
2
2
  export { home, sessions, projects, agents, analytics } from './freddie/pages-core.js';
3
3
  export { chat } from './freddie/pages-chat.js';
4
- export { models, cron, skills, env, tools, batch, gateway } from './freddie/pages-config.js';
4
+ export { models, cron, skills, env, tools } from './freddie/pages-config.js';
5
+ export { batch, gateway } from './freddie/pages-runtime.js';
5
6
  export { config } from './freddie/pages-config-edit.js';
6
7
  export { voice } from './freddie/pages-voice.js';
7
8
 
8
9
  import { home, sessions, projects, agents, analytics } from './freddie/pages-core.js';
9
10
  import { chat } from './freddie/pages-chat.js';
10
- import { models, cron, skills, env, tools, batch, gateway } from './freddie/pages-config.js';
11
+ import { models, cron, skills, env, tools } from './freddie/pages-config.js';
12
+ import { batch, gateway } from './freddie/pages-runtime.js';
11
13
  import { config } from './freddie/pages-config-edit.js';
12
14
  import { voice } from './freddie/pages-voice.js';
13
15
  export const FREDDIE_PAGES = { home, chat, voice, sessions, projects, agents, analytics, models, cron, skills, config, env, tools, batch, gateway };
@@ -41,4 +41,21 @@
41
41
  }
42
42
  .launcher-add { font-size: 20px; }
43
43
 
44
+ .launcher-instances {
45
+ display: flex;
46
+ flex-direction: column;
47
+ align-items: center;
48
+ gap: 8px;
49
+ }
50
+
51
+ .launcher-row {
52
+ display: flex;
53
+ flex-direction: column;
54
+ align-items: center;
55
+ gap: 2px;
56
+ }
57
+
58
+ .launcher-close { font-size: 11px; opacity: 0.7; width: 20px; height: 20px; }
59
+ .launcher-close:hover { opacity: 1; }
60
+
44
61
  .launcher-dock + .wm-root { left: 56px !important; }
@@ -19,10 +19,6 @@ export function renderDock(opts = {}) {
19
19
 
20
20
  const instancesHost = document.createElement('div');
21
21
  instancesHost.className = 'launcher-instances';
22
- instancesHost.style.display = 'flex';
23
- instancesHost.style.flexDirection = 'column';
24
- instancesHost.style.gap = '8px';
25
- instancesHost.style.alignItems = 'center';
26
22
  el.appendChild(instancesHost);
27
23
 
28
24
  root.appendChild(el);
@@ -41,10 +37,6 @@ export function renderDock(opts = {}) {
41
37
  const row = document.createElement('div');
42
38
  row.className = 'launcher-row';
43
39
  row.dataset.instanceId = inst.id;
44
- row.style.display = 'flex';
45
- row.style.flexDirection = 'column';
46
- row.style.gap = '2px';
47
- row.style.alignItems = 'center';
48
40
 
49
41
  const selBtn = document.createElement('button');
50
42
  selBtn.className = 'launcher-btn';