anentrypoint-design 0.0.73 → 0.0.75

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.73",
3
+ "version": "0.0.75",
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",
@@ -19,6 +19,7 @@
19
19
  "./desktop/theme.css": "./src/desktop/theme.css",
20
20
  "./desktop/wm.css": "./src/desktop/wm.css",
21
21
  "./desktop/wm.js": "./src/desktop/wm.js",
22
+ "./desktop/launcher.js": "./src/desktop/launcher.js",
22
23
  "./desktop/launcher.css": "./src/desktop/launcher.css",
23
24
  "./desktop/validate.css": "./src/desktop/validate.css",
24
25
  "./desktop/icons.js": "./src/desktop/icons.js",
@@ -0,0 +1,89 @@
1
+ import * as webjsx from '../../../vendor/webjsx/index.js';
2
+ import { Receipt, Panel } from '../content.js';
3
+ import { Chip } from '../shell.js';
4
+ const h = webjsx.createElement;
5
+
6
+ export const skillLabel = s => (s.name||'').replace(/^gm:/,'').replace(/-/g,' ');
7
+ export const getRecentPaths = () => { try { return JSON.parse(localStorage.getItem('fd_recent_cwds')||'[]'); } catch { return []; } };
8
+ export const saveRecentPath = p => { if (!p) return; const a = getRecentPaths().filter(x=>x!==p); a.unshift(p); localStorage.setItem('fd_recent_cwds', JSON.stringify(a.slice(0,5))); };
9
+
10
+ export function tryParseJson(s) { if (typeof s !== 'string') return null; const t = s.trim(); if (!t || (t[0] !== '{' && t[0] !== '[')) return null; try { return JSON.parse(t); } catch { return null; } }
11
+
12
+ export function flattenKv(obj, prefix='') {
13
+ const rows = [];
14
+ for (const [k, v] of Object.entries(obj || {})) {
15
+ const key = prefix ? prefix+'.'+k : k;
16
+ if (v === null || v === undefined) rows.push([key, '—']);
17
+ else if (typeof v === 'object' && !Array.isArray(v)) rows.push(...flattenKv(v, key));
18
+ else if (Array.isArray(v)) rows.push([key, v.length === 0 ? '[]' : v.map(x => typeof x === 'object' ? '{…}' : String(x)).join(', ')]);
19
+ else rows.push([key, String(v)]);
20
+ }
21
+ return rows;
22
+ }
23
+
24
+ export function renderConfigSections(cfg) {
25
+ const sections = [];
26
+ for (const [k, v] of Object.entries(cfg || {})) {
27
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
28
+ sections.push(Panel({ title: k, children: Receipt({ rows: flattenKv(v) }) }));
29
+ }
30
+ }
31
+ const scalars = Object.entries(cfg || {}).filter(([_, v]) => !(v && typeof v === 'object' && !Array.isArray(v))).map(([k, v]) => [k, Array.isArray(v) ? '['+v.length+']' : String(v ?? '—')]);
32
+ if (scalars.length) sections.unshift(Panel({ title: 'top-level', children: Receipt({ rows: scalars }) }));
33
+ return sections;
34
+ }
35
+
36
+ export function renderToolArgs(args) {
37
+ if (!args || typeof args !== 'object') return h('span', { class: 'fd-muted' }, '(no args)');
38
+ const rows = flattenKv(args);
39
+ if (!rows.length) return h('span', { class: 'fd-muted' }, '(empty)');
40
+ return h('table', { class: 'kv fd-tool-kv' }, h('tbody', {},
41
+ ...rows.map(([k, v], i) => h('tr', { key: i },
42
+ h('td', {}, k),
43
+ h('td', {}, String(v).length > 200 ? String(v).slice(0,200)+'…' : String(v))
44
+ ))
45
+ ));
46
+ }
47
+
48
+ export function renderChatMessages(el, msgs) {
49
+ if (!el) return; el.innerHTML = '';
50
+ for (const m of msgs) {
51
+ const div = document.createElement('div');
52
+ div.className = 'fd-msg' + (m.role === 'assistant' ? ' fd-msg-assistant' : m.role === 'tool' ? ' fd-msg-tool' : '');
53
+ if (m.role === 'tool') {
54
+ const det = document.createElement('details');
55
+ det.className = 'fd-tool-call';
56
+ const sum = document.createElement('summary');
57
+ sum.textContent = (m.name || 'tool') + (m.argsSummary ? ' · '+m.argsSummary : '');
58
+ det.appendChild(sum);
59
+ const body = document.createElement('div');
60
+ body.className = 'fd-tool-body';
61
+ const parsedArgs = tryParseJson(m.content);
62
+ if (parsedArgs && typeof parsedArgs === 'object') {
63
+ const tbl = document.createElement('table');
64
+ tbl.className = 'kv fd-tool-kv';
65
+ const tb = document.createElement('tbody');
66
+ for (const [k, v] of flattenKv(parsedArgs)) {
67
+ const tr = document.createElement('tr');
68
+ const tdK = document.createElement('td'); tdK.textContent = k;
69
+ const tdV = document.createElement('td'); tdV.textContent = String(v).length > 240 ? String(v).slice(0,240)+'…' : String(v);
70
+ tr.appendChild(tdK); tr.appendChild(tdV); tb.appendChild(tr);
71
+ }
72
+ tbl.appendChild(tb); body.appendChild(tbl);
73
+ } else {
74
+ const pre = document.createElement('pre');
75
+ pre.className = 'fd-pre';
76
+ pre.textContent = m.content || '';
77
+ body.appendChild(pre);
78
+ }
79
+ det.appendChild(body); div.appendChild(det);
80
+ } else {
81
+ const pre = document.createElement('pre');
82
+ pre.className = 'fd-pre';
83
+ pre.textContent = m.content || '';
84
+ div.appendChild(pre);
85
+ }
86
+ el.appendChild(div);
87
+ }
88
+ el.scrollTop = el.scrollHeight;
89
+ }
@@ -0,0 +1,109 @@
1
+ import * as webjsx from '../../../vendor/webjsx/index.js';
2
+ import { Panel, Receipt } from '../content.js';
3
+ import { Chip } from '../shell.js';
4
+ import { skillLabel, getRecentPaths, saveRecentPath, renderChatMessages } from './helpers.js';
5
+ const h = webjsx.createElement;
6
+
7
+ function parseSse(text) {
8
+ const evs = []; let ev = null, data = '';
9
+ for (const line of text.split('\n')) {
10
+ if (line.startsWith('event: ')) ev = line.slice(7).trim();
11
+ else if (line.startsWith('data: ')) data = line.slice(6).trim();
12
+ else if (line === '' && ev) { try { evs.push({ event: ev, data: JSON.parse(data) }); } catch {} ev = null; data = ''; }
13
+ }
14
+ return evs;
15
+ }
16
+
17
+ export async function chat(h0) {
18
+ const skills = [...h0.pi.skills.values()];
19
+ const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
20
+ const configured = providers.filter(p => p.configured);
21
+ const cs = window.__fd_chatState = window.__fd_chatState || { cwd: '', skill: '', provider: '', model: '', messages: [], busy: false, sessionId: null };
22
+ if (!cs.cwd) cs.cwd = (getRecentPaths()[0] || '');
23
+ const root = document.getElementById('app');
24
+ const getMsgs = () => root.querySelector('#fd-chat-msgs');
25
+ const newSession = () => { if (cs.busy) return; cs.messages = []; cs.sessionId = null; renderChatMessages(getMsgs(), cs.messages); };
26
+ const sendChat = async ev => {
27
+ ev.preventDefault();
28
+ if (cs.busy) return;
29
+ const promptEl = ev.target.elements.prompt;
30
+ const prompt = promptEl.value.trim();
31
+ if (!prompt) return;
32
+ cs.messages.push({ role: 'user', content: prompt });
33
+ promptEl.value = ''; promptEl.style.height = 'auto';
34
+ cs.busy = true;
35
+ saveRecentPath(cs.cwd);
36
+ renderChatMessages(getMsgs(), cs.messages);
37
+ try {
38
+ const body = { prompt, cwd: cs.cwd || undefined, skill: cs.skill || undefined, provider: cs.provider || undefined, model: cs.model || undefined, sessionId: cs.sessionId || undefined };
39
+ const resp = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
40
+ const text = await resp.text();
41
+ const events = parseSse(text);
42
+ let ac = '';
43
+ for (const { event, data } of events) {
44
+ if (event === 'start' && data.sessionId) cs.sessionId = data.sessionId;
45
+ if (event === 'done' && data.sessionId) cs.sessionId = data.sessionId;
46
+ if (event === 'message') {
47
+ if (data.role === 'assistant') {
48
+ const content = Array.isArray(data.content) ? data.content : [{ type: 'text', text: String(data.content || '') }];
49
+ for (const block of content) {
50
+ if (block.type === 'text') ac += block.text;
51
+ if (block.type === 'tool_use') {
52
+ if (ac) { cs.messages.push({ role: 'assistant', content: ac }); ac = ''; }
53
+ const argSummary = Object.keys(block.input || {}).slice(0, 3).map(k => k+'='+String(block.input[k]).slice(0,20)).join(' ');
54
+ cs.messages.push({ role: 'tool', name: block.name, argsSummary: argSummary, content: JSON.stringify(block.input || {}, null, 2) });
55
+ }
56
+ }
57
+ } else if (data.role === 'tool') {
58
+ const tc = Array.isArray(data.content) ? data.content[0] : data;
59
+ cs.messages.push({ role: 'tool', name: 'result', argsSummary: '', content: String(tc?.content || tc?.text || JSON.stringify(tc)) });
60
+ }
61
+ }
62
+ if (event === 'done' && data.result && !ac) ac = data.result;
63
+ if (event === 'error') ac = 'error: ' + (data.error || 'unknown');
64
+ }
65
+ if (ac) cs.messages.push({ role: 'assistant', content: ac });
66
+ if (!events.length) cs.messages.push({ role: 'assistant', content: '(no response)' });
67
+ } catch (e) { cs.messages.push({ role: 'assistant', content: 'error: '+e.message }); }
68
+ cs.busy = false;
69
+ renderChatMessages(getMsgs(), cs.messages);
70
+ };
71
+ const byCat = skills.reduce((a, s) => { const c = s.category || 'other'; (a[c] = a[c] || []).push(s); return a; }, {});
72
+ setTimeout(() => renderChatMessages(getMsgs(), cs.messages), 50);
73
+ return [
74
+ Panel({ title: 'chat', right: h('button', { class: 'btn-primary', onclick: ev => { ev.preventDefault(); newSession(); } }, '+ new'), children: [
75
+ h('form', { class: 'fd-chat-form', onsubmit: sendChat },
76
+ h('label', { class: 'fd-label' }, 'WORKING DIRECTORY'),
77
+ h('input', { name: 'cwd', type: 'text', placeholder: 'e.g. C:/dev/myproject', value: cs.cwd, oninput: ev => { cs.cwd = ev.target.value; } }),
78
+ h('div', { class: 'fd-row' },
79
+ h('div', { class: 'fd-col' },
80
+ h('label', { class: 'fd-label' }, 'SKILL'),
81
+ h('select', { name: 'skill', onchange: ev => { cs.skill = ev.target.value; } },
82
+ h('option', { value: '' }, '— no skill —'),
83
+ ...Object.entries(byCat).map(([cat, ss]) => h('optgroup', { label: cat }, ...ss.map(s => h('option', { value: s.name, selected: cs.skill === s.name ? 'true' : null }, skillLabel(s)))))
84
+ )
85
+ ),
86
+ h('div', { class: 'fd-col' },
87
+ h('label', { class: 'fd-label' }, 'PROVIDER'),
88
+ h('select', { name: 'provider', onchange: ev => { cs.provider = ev.target.value; } },
89
+ h('option', { value: '' }, configured.length ? '— auto —' : '— none configured —'),
90
+ ...configured.map(p => h('option', { value: p.name, selected: cs.provider === p.name ? 'true' : null }, (p.available ? '● ' : '○ ') + p.name))
91
+ )
92
+ ),
93
+ h('div', { class: 'fd-col' },
94
+ h('label', { class: 'fd-label' }, 'MODEL'),
95
+ h('input', { name: 'model', type: 'text', placeholder: 'default', value: cs.model, oninput: ev => { cs.model = ev.target.value; } })
96
+ )
97
+ ),
98
+ h('div', { class: 'fd-chat-send-row' },
99
+ h('textarea', { name: 'prompt', placeholder: 'describe what you want…', rows: 4, oninput: ev => { ev.target.style.height = 'auto'; ev.target.style.height = Math.min(ev.target.scrollHeight, 240)+'px'; } }),
100
+ h('button', { type: 'submit', class: 'btn-primary', disabled: cs.busy ? 'true' : null }, cs.busy ? '…' : 'send')
101
+ )
102
+ ),
103
+ h('div', { id: 'fd-chat-msgs', class: 'fd-chat-thread' })
104
+ ] }),
105
+ configured.length === 0
106
+ ? Panel({ title: 'no providers configured', children: Receipt({ rows: [['set API key', 'keys tab → click chip'], ['or use acptoapi', 'run acptoapi server on localhost:4800']] }) })
107
+ : Panel({ title: 'providers', children: h('div', { class: 'fd-chips' }, ...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.name + (p.configured ? (p.available ? ' ●' : ' ○') : '') }))) })
108
+ ];
109
+ }
@@ -0,0 +1,169 @@
1
+ import * as webjsx from '../../../vendor/webjsx/index.js';
2
+ import { Panel, Row, Receipt, Kpi, Table, Form } from '../content.js';
3
+ import { Chip } from '../shell.js';
4
+ import { EmptyState } from '../files.js';
5
+ import { skillLabel, renderConfigSections } from './helpers.js';
6
+ const h = webjsx.createElement;
7
+
8
+ export async function models(h0) {
9
+ const cfg = typeof h0.pi.config?.load === 'function' ? await h0.pi.config.load() : {};
10
+ const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
11
+ const configured = providers.filter(p => p.configured);
12
+ const probeState = window.__fd_probeState = window.__fd_probeState || {};
13
+ async function probeAll() {
14
+ await Promise.allSettled(configured.map(async p => {
15
+ probeState[p.name] = 'loading';
16
+ try { const r = await fetch('/api/providers/'+p.name+'/probe', { method: 'POST' }).then(x => x.json()); probeState[p.name] = r.models || r.error || '?'; }
17
+ catch (e) { probeState[p.name] = 'error: '+e.message; }
18
+ }));
19
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('models');
20
+ }
21
+ const modelPanels = configured.map(p => {
22
+ const models = Array.isArray(probeState[p.name]) ? probeState[p.name] : p.models;
23
+ const loading = probeState[p.name] === 'loading';
24
+ const children = loading ? h('span', {}, 'probing…')
25
+ : models && models.length > 0 ? Table({ headers: ['model id'], rows: models.map(m => [m]) })
26
+ : h('span', { class: 'fd-muted' }, p.modelsError ? 'error: '+p.modelsError : 'not probed — click "probe all"');
27
+ return Panel({ title: p.name + (p.available ? ' ●' : ' ○'), children });
28
+ });
29
+ return [
30
+ Kpi({ items: [[configured.length,'configured'],[providers.filter(p => p.available).length,'available']] }),
31
+ Panel({ title: 'change active model', children: Form({ fields: [
32
+ { name: 'provider', placeholder: 'provider', value: cfg.agent?.provider || '' },
33
+ { name: 'model', placeholder: 'model id', value: cfg.agent?.model || '' }
34
+ ], submit: 'update', onSubmit: async ev => {
35
+ await h0.pi.config.saveValue('agent.provider', ev.target.elements.provider.value);
36
+ await h0.pi.config.saveValue('agent.model', ev.target.elements.model.value);
37
+ } }) }),
38
+ Panel({ title: 'providers', right: h('button', { class: 'btn-primary', onclick: ev => { ev.preventDefault(); probeAll(); } }, 'probe all'),
39
+ children: h('div', { class: 'fd-chips' }, ...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.name + (p.configured ? (p.available ? ' ●' : ' ○') : ' ·') })))
40
+ }),
41
+ ...modelPanels
42
+ ];
43
+ }
44
+
45
+ export async function cron(h0) {
46
+ const list = await h0.pi.cron.list();
47
+ return [
48
+ Kpi({ items: [[list.length,'jobs']] }),
49
+ Panel({ title: 'add job', children: Form({ fields: [
50
+ { name: 'cron', placeholder: '* * * * *', required: true },
51
+ { name: 'prompt', placeholder: 'prompt', required: true }
52
+ ], submit: 'create', onSubmit: async ev => { await h0.pi.cron.create({ cron: ev.target.elements.cron.value, prompt: ev.target.elements.prompt.value }); } }) }),
53
+ Panel({ title: 'jobs', count: list.length, children: list.length === 0 ? EmptyState({ text: 'no cron jobs', glyph: '◷' }) : Table({ headers: ['id','cron','prompt','enabled'], rows: list.map(j => [j.id, j.cron, (j.prompt||'').slice(0,40), j.enabled ? 'yes' : 'no']) }) })
54
+ ];
55
+ }
56
+
57
+ export async function skills(h0) {
58
+ const list = [...h0.pi.skills.values()];
59
+ const byCat = list.reduce((a, s) => { (a[s.category||'other'] = a[s.category||'other'] || []).push(s); return a; }, {});
60
+ return [
61
+ Kpi({ items: [[list.length,'skills'],[Object.keys(byCat).length,'categories']] }),
62
+ list.length === 0 ? EmptyState({ text: 'no skills — add SKILL.md files to ~/.freddie/skills/', glyph: '◈' }) : null,
63
+ ...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)]) }) }))
64
+ ].filter(Boolean);
65
+ }
66
+
67
+ export async function config(h0) {
68
+ const cfg = typeof h0.pi.config?.load === 'function' ? await h0.pi.config.load() : {};
69
+ const commands = typeof h0.pi.cli?.values === 'function' ? [...h0.pi.cli.values()] : [];
70
+ return [
71
+ Kpi({ items: [[commands.length,'commands'],[cfg._config_version||0,'config version']] }),
72
+ Panel({ title: 'set config value', children: Form({ fields: [
73
+ { name: 'key', placeholder: 'dotted.key', required: true },
74
+ { name: 'value', placeholder: 'value (json or string)', required: true }
75
+ ], submit: 'save', onSubmit: async ev => {
76
+ let v = ev.target.elements.value.value;
77
+ try { v = JSON.parse(v); } catch {}
78
+ await h0.pi.config.saveValue(ev.target.elements.key.value, v);
79
+ } }) }),
80
+ Panel({ title: 'commands', count: commands.length, children: Table({ headers: ['name','description'], rows: commands.map(c => [c.name, c.description||'']) }) }),
81
+ ...renderConfigSections(cfg)
82
+ ];
83
+ }
84
+
85
+ export async function env(h0) {
86
+ const list = typeof h0.pi.env?.list === 'function' ? h0.pi.env.list() : [];
87
+ const setCount = list.filter(k => k.set).length;
88
+ const groups = {};
89
+ for (const k of list) {
90
+ const idx = k.key.indexOf('_');
91
+ const g = idx > 0 ? k.key.slice(0, idx) : 'OTHER';
92
+ (groups[g] = groups[g] || []).push(k);
93
+ }
94
+ const sortedGroups = Object.entries(groups).sort((a,b) => b[1].length - a[1].length);
95
+ return [
96
+ Kpi({ items: [[setCount,'set'],[list.length-setCount,'missing'],[list.length,'total'],[sortedGroups.length,'groups']] }),
97
+ list.length === 0 ? EmptyState({ text: 'no env keys registered', glyph: '⚿' }) : null,
98
+ ...sortedGroups.map(([g, keys]) => {
99
+ const setN = keys.filter(k => k.set).length;
100
+ return Panel({ title: g.toLowerCase(), count: keys.length, right: setN === keys.length ? Chip({ tone: 'ok', children: 'all set' }) : setN === 0 ? Chip({ tone: 'miss', children: 'none set' }) : Chip({ tone: 'warn', children: setN+'/'+keys.length }),
101
+ children: h('div', { class: 'fd-chips' }, ...keys.map(k => h('span', { key: k.key, onclick: () => { const v = prompt('set '+k.key+' (empty to unset):'); if (v == null) return; if (typeof h0.pi.env.set === 'function') { h0.pi.env.set(k.key, v); } }, class: 'fd-chip-wrap' }, Chip({ tone: k.set ? 'ok' : 'miss', children: k.key + (k.set ? ' ✓' : ' ·') }))))
102
+ });
103
+ })
104
+ ].filter(Boolean);
105
+ }
106
+
107
+ export async function tools(h0) {
108
+ const list = [...h0.pi.tools.values()];
109
+ const envIsSet = k => typeof h0.pi.env?.isSet === 'function' ? h0.pi.env.isSet(k) : false;
110
+ const bySet = list.reduce((a, t) => { (a[t.toolset||'core'] = a[t.toolset||'core'] || []).push(t); return a; }, {});
111
+ return [
112
+ Kpi({ items: [[list.length,'tools'],[Object.keys(bySet).length,'toolsets']] }),
113
+ ...Object.entries(bySet).map(([ts, items]) => Panel({ title: 'toolset · '+ts, count: items.length, children: items.map(t => {
114
+ const params = t.schema?.parameters?.properties ? Object.keys(t.schema.parameters.properties).length : 0;
115
+ const reqEnv = Array.isArray(t.requiresEnv) ? t.requiresEnv : [];
116
+ const meta = h('span', { class: 'fd-tool-meta' },
117
+ params > 0 ? Chip({ tone: 'neutral', children: params+' params' }) : null,
118
+ ...reqEnv.map(k => Chip({ tone: envIsSet(k) ? 'ok' : 'miss', children: k }))
119
+ );
120
+ return Row({ key: t.name, code: '⚒', title: t.name, sub: (t.description || (t.schema && t.schema.description) || '').slice(0,80), meta });
121
+ }) }))
122
+ ];
123
+ }
124
+
125
+ export async function batch(h0) {
126
+ const results = window.__fd_batchResults = window.__fd_batchResults || [];
127
+ const status = window.__fd_batchStatus = window.__fd_batchStatus || { running: false, error: null };
128
+ const onSubmit = async ev => {
129
+ const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
130
+ if (!prompts.length) return;
131
+ status.running = true; status.error = null; window.__fd_batchResults = [];
132
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('batch');
133
+ try {
134
+ const r = await h0.pi.batch.run(prompts, Number(ev.target.elements.concurrency.value) || 4);
135
+ const arr = Array.isArray(r) ? r : (r && typeof r === 'object' ? Object.entries(r).map(([k, v]) => ({ prompt: k, result: v })) : []);
136
+ window.__fd_batchResults = arr;
137
+ } catch (e) { status.error = e.message || String(e); }
138
+ status.running = false;
139
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('batch');
140
+ };
141
+ const resPanel = status.running ? Panel({ title: 'results', children: h('span', {}, 'running…') })
142
+ : status.error ? Panel({ title: 'results', children: h('span', { class: 'fd-muted' }, 'error: '+status.error) })
143
+ : results.length === 0 ? Panel({ title: 'results', children: EmptyState({ text: 'no results yet', glyph: '⊞' }) })
144
+ : 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']) }) });
145
+ return [
146
+ Panel({ title: 'run batch', children: Form({ fields: [
147
+ { name: 'prompts', kind: 'textarea', placeholder: 'one prompt per line', rows: 6 },
148
+ { name: 'concurrency', type: 'number', value: '4' }
149
+ ], submit: 'run', onSubmit }) }),
150
+ resPanel
151
+ ];
152
+ }
153
+
154
+ export async function gateway(h0) {
155
+ const platforms = typeof h0.pi.gateway?.platforms === 'function' ? h0.pi.gateway.platforms() : [];
156
+ const active = platforms.filter(p => p.enabled);
157
+ const envIsSet = k => typeof h0.pi.env?.isSet === 'function' ? h0.pi.env.isSet(k) : false;
158
+ return [
159
+ Kpi({ items: [[platforms.length,'platforms'],[active.length,'active']] }),
160
+ Panel({ title: 'platforms', count: platforms.length, right: active.length > 0 ? Chip({ tone: 'ok', children: active.length+' active' }) : Chip({ tone: 'miss', children: 'none active' }),
161
+ children: platforms.length === 0 ? EmptyState({ text: 'no platforms registered', glyph: '⇌' }) : platforms.map(p => {
162
+ const reqEnv = Array.isArray(p.requiresEnv) ? p.requiresEnv : [];
163
+ const setN = reqEnv.filter(envIsSet).length;
164
+ const envSummary = reqEnv.length === 0 ? '' : setN === reqEnv.length ? '✓ env ready' : 'missing '+(reqEnv.length-setN)+' / '+reqEnv.length;
165
+ return Row({ key: p.name, code: p.enabled ? '●' : '○', title: p.name, sub: p.note || '', meta: [p.enabled ? 'enabled' : '', envSummary].filter(Boolean).join(' · ') });
166
+ })
167
+ })
168
+ ];
169
+ }
@@ -0,0 +1,103 @@
1
+ import * as webjsx from '../../../vendor/webjsx/index.js';
2
+ import { Panel, Row, Hero, Receipt, Kpi, Table } from '../content.js';
3
+ import { EmptyState } from '../files.js';
4
+ import { skillLabel } from './helpers.js';
5
+ const h = webjsx.createElement;
6
+
7
+ export async function home(h0) {
8
+ const sessions = await h0.pi.sessions.list();
9
+ const health = h0.pi.health();
10
+ return [
11
+ Hero({ title: 'freddie', body: 'open js agent harness.', accent: h0.version || 'web' }),
12
+ Kpi({ items: [[sessions.length,'sessions'],[h0.pi.tools.size,'tools'],[h0.pi.skills.size,'skills']] }),
13
+ Panel({ title: 'quick start', children: Receipt({ rows: [
14
+ ['open chat',"click 'chat' — set a working directory"],
15
+ ['pick skill','software dev, research, planning'],
16
+ ['set api key','keys tab → click chip'],
17
+ ['add cron','cron tab → form']
18
+ ] }) }),
19
+ Panel({ title: 'host', children: Receipt({ rows: Object.entries(health).map(([k,v]) => [k, String(v)]) }) })
20
+ ];
21
+ }
22
+
23
+ export async function sessions(h0) {
24
+ const list = await h0.pi.sessions.list();
25
+ const rows = list.map(s => {
26
+ const cont = h('button', { class: 'btn-primary', onclick: async () => {
27
+ const msgs = await h0.pi.sessions.getMessages(s.id);
28
+ const cs = window.__fd_chatState = window.__fd_chatState || { messages: [], busy: false, sessionId: null, cwd: '', skill: '', provider: '', model: '' };
29
+ cs.sessionId = s.id;
30
+ cs.messages = msgs.map(m => ({ role: m.role, content: String(m.content || '') }));
31
+ if (s.cwd) cs.cwd = s.cwd;
32
+ if (s.skill) cs.skill = s.skill;
33
+ if (typeof window.__fd_nav === 'function') window.__fd_nav('chat');
34
+ } }, 'continue');
35
+ 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];
36
+ });
37
+ return [
38
+ Kpi({ items: [[list.length,'sessions']] }),
39
+ Panel({ title: 'sessions', count: list.length, children: list.length === 0 ? EmptyState({ text: 'no sessions yet', glyph: '✉' }) : Table({ headers: ['id','title','platform','model','cwd','skill',''], rows }) })
40
+ ];
41
+ }
42
+
43
+ export async function projects(h0) {
44
+ const list = h0.pi.projects.list();
45
+ const active = h0.pi.projects.active();
46
+ const rows = list.map(p => Row({
47
+ key: p.name,
48
+ code: p.name === active?.name ? '●' : '○',
49
+ title: p.name + (p.name === active?.name ? ' (active)' : ''),
50
+ meta: p.path,
51
+ onClick: () => { if (p.name !== active?.name) h0.pi.projects.setActive(p.name); }
52
+ }));
53
+ return [
54
+ Hero({ title: 'projects', body: 'each project is its own ~/.freddie home.', accent: active ? 'active · '+active.name : 'no active project' }),
55
+ Kpi({ items: [[list.length,'projects'],[active?.name||'—','active']] }),
56
+ Panel({ title: 'add project', children: h('form', { class: 'row-form', onsubmit: ev => { ev.preventDefault(); h0.pi.projects.create({ name: ev.target.elements.name.value, path: ev.target.elements.path.value }); } },
57
+ h('input', { name: 'name', placeholder: 'name', required: 'true' }),
58
+ h('input', { name: 'path', placeholder: '/abs/path' }),
59
+ h('button', { type: 'submit', class: 'btn-primary' }, 'add')
60
+ ) }),
61
+ Panel({ title: 'all projects', count: list.length, children: rows.length ? rows : EmptyState({ text: 'no projects', glyph: '◆' }) })
62
+ ];
63
+ }
64
+
65
+ export async function agents(h0) {
66
+ const a = typeof h0.pi.agents === 'function' ? await h0.pi.agents() : { count: 0, turns: 0, active: null };
67
+ const sList = await h0.pi.sessions.list();
68
+ const recent = sList.slice(0, 10);
69
+ return [
70
+ Kpi({ items: [[a.count||0,'active'],[a.turns||0,'turns'],[sList.length,'total sessions']] }),
71
+ Panel({ title: 'current agent', children: Receipt({ rows: [
72
+ ['active session', a.active || '(none)'],
73
+ ['total turns', String(a.turns || 0)],
74
+ ['count', String(a.count || 0)]
75
+ ] }) }),
76
+ Panel({ title: 'recent sessions', count: recent.length, children: recent.length === 0
77
+ ? EmptyState({ text: 'no recent sessions', glyph: '◈' })
78
+ : 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 ?? '—')]) })
79
+ })
80
+ ];
81
+ }
82
+
83
+ export async function analytics(h0) {
84
+ const list = await h0.pi.sessions.list();
85
+ const tools = [...h0.pi.tools.values()];
86
+ const skills = [...h0.pi.skills.values()];
87
+ const byPlat = list.reduce((a,s) => { const k = s.platform||'?'; a[k] = (a[k]||0)+1; return a; }, {});
88
+ const byModel = list.reduce((a,s) => { const k = s.model||'?'; a[k] = (a[k]||0)+1; return a; }, {});
89
+ const byToolset = tools.reduce((a,t) => { const k = t.toolset||'core'; a[k] = (a[k]||0)+1; return a; }, {});
90
+ const bySkillCat = skills.reduce((a,s) => { const k = s.category||'other'; a[k] = (a[k]||0)+1; return a; }, {});
91
+ const sortDesc = obj => Object.entries(obj).sort((a,b) => b[1]-a[1]).map(([k,v]) => [k, String(v)]);
92
+ const mkPanel = (title, obj, glyph) => Panel({ title, count: Object.keys(obj).length, children: Object.keys(obj).length === 0
93
+ ? EmptyState({ text: 'no data', glyph })
94
+ : Table({ headers: ['name','count'], rows: sortDesc(obj) })
95
+ });
96
+ return [
97
+ Kpi({ items: [[list.length,'sessions'],[tools.length,'tools'],[skills.length,'skills']] }),
98
+ mkPanel('by platform', byPlat, '◉'),
99
+ mkPanel('by model', byModel, '◎'),
100
+ mkPanel('by toolset', byToolset, '⚒'),
101
+ mkPanel('by skill category', bySkillCat, '◈')
102
+ ];
103
+ }