freddie 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/AGENTS.md +20 -1
- package/package.json +2 -2
- package/src/web/app.js +70 -568
- package/src/web/server.js +4 -0
- package/src/web/vendor/anentrypoint-design/desktop/freddie-dashboard.css +32 -0
- package/src/web/vendor/anentrypoint-design/desktop/freddie-dashboard.js +405 -0
- package/src/web/vendor/anentrypoint-design/desktop/icons.js +17 -0
- package/src/web/vendor/anentrypoint-design/desktop/index.js +3 -0
- package/src/web/vendor/anentrypoint-design/desktop/launcher.css +44 -0
- package/src/web/vendor/anentrypoint-design/desktop/shell.js +187 -0
- package/src/web/vendor/anentrypoint-design/desktop/theme.css +409 -0
- package/src/web/vendor/anentrypoint-design/desktop/validate.css +19 -0
- package/src/web/vendor/anentrypoint-design/desktop/wm.css +115 -0
package/src/web/app.js
CHANGED
|
@@ -1,578 +1,80 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
Brand, EmptyState, RowLink, Receipt, Changelog, Hero, ConfirmDialog, Section, Install, Kpi, Table } = components
|
|
4
|
-
const kpi = (items) => Kpi({ items })
|
|
5
|
-
const table = (headers, rows, opts = {}) => Table({ headers, rows, onRowClick: opts.onRowClick })
|
|
1
|
+
import { createFreddieDashboard } from '/vendor/anentrypoint-design/desktop/freddie-dashboard.js';
|
|
2
|
+
import { installStyles } from 'anentrypoint-design';
|
|
6
3
|
|
|
7
|
-
await installStyles()
|
|
4
|
+
await installStyles();
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
const link = document.createElement('link');
|
|
7
|
+
link.rel = 'stylesheet';
|
|
8
|
+
link.href = '/vendor/anentrypoint-design/desktop/freddie-dashboard.css';
|
|
9
|
+
document.head.appendChild(link);
|
|
12
10
|
|
|
13
|
-
const j = async (u, opts) => {
|
|
11
|
+
const j = async (u, opts) => { const r = await fetch(u, opts); if (!r.ok) throw new Error(r.status + ' ' + r.statusText); return await r.json(); };
|
|
12
|
+
const post = (u, body) => j(u, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
{ path: '#/sessions', label: 'Sessions', glyph: '✉' },
|
|
20
|
-
{ path: '#/agents', label: 'Agents', glyph: '◈' },
|
|
21
|
-
{ path: '#/analytics', label: 'Analytics', glyph: '◉' },
|
|
22
|
-
{ path: '#/models', label: 'Models', glyph: '◎' },
|
|
23
|
-
{ path: '#/logs', label: 'Logs', glyph: '☰' },
|
|
24
|
-
{ path: '#/cron', label: 'Cron', glyph: '◷' },
|
|
25
|
-
{ path: '#/skills', label: 'Skills', glyph: '◈' },
|
|
26
|
-
{ path: '#/config', label: 'Config', glyph: '⚙' },
|
|
27
|
-
{ path: '#/env', label: 'Keys', glyph: '⚿' },
|
|
28
|
-
{ path: '#/tools', label: 'Tools', glyph: '⚒' },
|
|
29
|
-
{ path: '#/batch', label: 'Batch', glyph: '⊞' },
|
|
30
|
-
{ path: '#/gateway', label: 'Gateway', glyph: '⇌' },
|
|
31
|
-
]
|
|
32
|
-
window.__debug.routes = () => ROUTES.map(r => r.path)
|
|
33
|
-
|
|
34
|
-
const AppState = {
|
|
35
|
-
hash: location.hash || '#/home',
|
|
36
|
-
body: null,
|
|
37
|
-
ts: new Date().toLocaleTimeString(),
|
|
38
|
-
theme: localStorage.getItem('freddie-theme') || 'dark',
|
|
39
|
-
search: { query: '', results: [] },
|
|
40
|
-
sessionsFilter: '',
|
|
41
|
-
chat: { messages: [], draft: '', streaming: false },
|
|
42
|
-
batch: { results: null, running: false },
|
|
43
|
-
agents: { count: 0, active: null },
|
|
44
|
-
projects: { active: null, all: [] },
|
|
45
|
-
}
|
|
46
|
-
function applyTheme() { document.body.setAttribute('data-theme', AppState.theme) }
|
|
47
|
-
applyTheme()
|
|
48
|
-
window.__debug.state = () => AppState
|
|
49
|
-
|
|
50
|
-
function pre(obj) { return h('pre', {}, typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2)) }
|
|
51
|
-
|
|
52
|
-
function timeNow() {
|
|
53
|
-
const d = new Date()
|
|
54
|
-
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function toChatMsg(m, key) {
|
|
58
|
-
const time = m.time || ''
|
|
59
|
-
if (m.role === 'user') {
|
|
60
|
-
return { who: 'you', avatar: 'u', time, receipt: 'delivered', key,
|
|
61
|
-
parts: [{ kind: 'text', text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }] }
|
|
62
|
-
}
|
|
63
|
-
if (m.role === 'tool') {
|
|
64
|
-
const body = typeof m.content === 'string' ? m.content : JSON.stringify(m.content, null, 2)
|
|
65
|
-
return { who: 'them', avatar: '⚒', name: 'tool' + (m.tool_call_id ? ' · ' + String(m.tool_call_id).slice(0, 8) : ''), time, key,
|
|
66
|
-
parts: [{ kind: 'code', lang: 'json', filename: 'tool result', code: body }] }
|
|
67
|
-
}
|
|
68
|
-
const parts = []
|
|
69
|
-
const text = typeof m.content === 'string' ? m.content : ''
|
|
70
|
-
if (text) parts.push({ kind: 'md', text })
|
|
71
|
-
if (Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
72
|
-
for (const c of m.tool_calls) {
|
|
73
|
-
parts.push({ kind: 'code', lang: 'json', filename: 'call · ' + (c.name || c.function?.name || '?'),
|
|
74
|
-
code: JSON.stringify(c.arguments || c.function?.arguments || {}, null, 2) })
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (parts.length === 0) parts.push({ kind: 'text', text: '' })
|
|
78
|
-
return { who: 'them', avatar: '◉', name: 'freddie', time, key, parts }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const PAGES = {
|
|
82
|
-
'#/projects': async () => {
|
|
83
|
-
const data = await j('/api/projects')
|
|
84
|
-
const all = data.projects || []
|
|
85
|
-
const active = data.active || null
|
|
86
|
-
AppState.projects = { active, all }
|
|
87
|
-
return [
|
|
88
|
-
Hero({ title: 'Projects', body: 'Each project is its own ~/.freddie home: separate sessions, agents, skills, config, env, cron, batches. Switch the active project to swap everything.', accent: active ? 'active · ' + active.name : 'no project active' }),
|
|
89
|
-
kpi([[all.length, 'Projects'], [active?.name || '—', 'Active'], [active?.path?.length > 30 ? '…'+active.path.slice(-28) : (active?.path || '—'), 'Path']]),
|
|
90
|
-
Panel({ title: 'Add a project', children: h('form', { class: 'row-form', onsubmit: async (ev) => {
|
|
91
|
-
ev.preventDefault()
|
|
92
|
-
const f = ev.target.elements
|
|
93
|
-
const r = await j('/api/projects', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: f.name.value, path: f.path.value }) })
|
|
94
|
-
if (r.__error || r.error) { alert('Error: ' + (r.error || r.__error)); return }
|
|
95
|
-
f.name.value = ''; f.path.value = ''; rerender()
|
|
96
|
-
} },
|
|
97
|
-
h('input', { name: 'name', placeholder: 'project name (e.g. penguins)', required: true }),
|
|
98
|
-
h('input', { name: 'path', placeholder: 'absolute path (e.g. C:\\dev\\penguins)', required: true, style: 'flex:2' }),
|
|
99
|
-
h('button', { type: 'submit', class: 'primary' }, 'Add')) }),
|
|
100
|
-
Panel({ title: 'All projects', count: all.length, right: active ? Chip({ tone: 'ok', children: 'active: ' + active.name }) : null,
|
|
101
|
-
children: h('div', {}, ...all.map(p => h('div', { class: 'row', style: 'cursor:pointer', onclick: async () => {
|
|
102
|
-
if (p.name === active?.name) return
|
|
103
|
-
const r = await j('/api/projects/active', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: p.name }) })
|
|
104
|
-
if (r.__error || r.error) { alert('Switch failed: ' + (r.error || r.__error)); return }
|
|
105
|
-
alert('Switched to project "' + p.name + '". Restart the dashboard to load its plugins/data, then reload this page.')
|
|
106
|
-
rerender()
|
|
107
|
-
} },
|
|
108
|
-
h('span', { class: 'code' }, p.name === active?.name ? '●' : '○'),
|
|
109
|
-
h('span', { class: 'title' }, p.name + (p.name === active?.name ? ' (active)' : '')),
|
|
110
|
-
h('span', { class: 'meta' }, p.path),
|
|
111
|
-
p.name !== 'default' ? h('button', { class: 'danger', style: 'margin-left:8px',
|
|
112
|
-
onclick: async (ev) => { ev.stopPropagation(); if (!confirm('Remove project "'+p.name+'" from registry? Files on disk are kept.')) return; await fetch('/api/projects/' + p.name, { method: 'DELETE' }); rerender() } }, 'remove') : null,
|
|
113
|
-
))) }),
|
|
114
|
-
Panel({ title: 'How encapsulation works', children: Receipt({ rows: [
|
|
115
|
-
['Sessions DB', '<project>/sessions.db'],
|
|
116
|
-
['Config', '<project>/config.json'],
|
|
117
|
-
['Skills', '<project>/skills/'],
|
|
118
|
-
['Plugins', '<project>/plugins/ + repo/plugins'],
|
|
119
|
-
['Cron jobs', '<project>/cron.db'],
|
|
120
|
-
['Batches', '<project>/batches/'],
|
|
121
|
-
['Logs', '<project>/logs/'],
|
|
122
|
-
['Auth credentials', '<project>/auth.json'],
|
|
123
|
-
]}) }),
|
|
124
|
-
]
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
'#/chat': async () => {
|
|
128
|
-
const messages = AppState.chat.messages.map((m, i) => toChatMsg(m, 'm' + i))
|
|
129
|
-
return AICat({
|
|
130
|
-
name: 'freddie',
|
|
131
|
-
status: AppState.chat.streaming ? 'thinking…' : 'online · live runTurn via SSE',
|
|
132
|
-
messages,
|
|
133
|
-
thinking: AppState.chat.streaming,
|
|
134
|
-
composer: ChatComposer({
|
|
135
|
-
value: AppState.chat.draft,
|
|
136
|
-
placeholder: 'Ask freddie — runs through registered tools and the configured LLM…',
|
|
137
|
-
disabled: AppState.chat.streaming,
|
|
138
|
-
onInput: (v) => { AppState.chat.draft = v; rerender() },
|
|
139
|
-
onSend: (text) => { AppState.chat.draft = ''; sendChat(text) },
|
|
140
|
-
}),
|
|
141
|
-
})
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
'#/home': async () => {
|
|
145
|
-
const [sessions, tools, skills] = await Promise.all([j('/api/sessions'), j('/api/tools'), j('/api/skills')])
|
|
146
|
-
const sessionCount = Array.isArray(sessions) ? sessions.length : 0
|
|
147
|
-
const toolCount = Array.isArray(tools) ? tools.length : 0
|
|
148
|
-
const skillCount = ((skills.home || []).length + (skills.bundled || []).length)
|
|
149
|
-
return [
|
|
150
|
-
Hero({ title: 'freddie', body: 'Open JS agent harness built on pi-mono, xstate, floosie, and anentrypoint-design.', accent: 'v0.0.1' }),
|
|
151
|
-
kpi([[sessionCount, 'Sessions'], [toolCount, 'Tools'], [skillCount, 'Skills']]),
|
|
152
|
-
Panel({ title: 'Quick start', children: Receipt({ rows: [
|
|
153
|
-
['Run interactive REPL', 'freddie run'],
|
|
154
|
-
['Start dashboard', 'freddie dashboard --port 3000'],
|
|
155
|
-
['List tools', 'freddie tools'],
|
|
156
|
-
['List skills', 'freddie skills'],
|
|
157
|
-
['Start gateway', 'freddie gateway --port 4000'],
|
|
158
|
-
]}) }),
|
|
159
|
-
]
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
'#/sessions': async () => {
|
|
163
|
-
const sessions = await j('/api/sessions')
|
|
164
|
-
const all = sessions.__error ? [] : sessions
|
|
165
|
-
const q = AppState.sessionsFilter.toLowerCase()
|
|
166
|
-
const filtered = q ? all.filter(s => JSON.stringify(s).toLowerCase().includes(q)) : all
|
|
167
|
-
return [
|
|
168
|
-
kpi([[all.length || 0, 'Total sessions'], [filtered.length, 'After filter']]),
|
|
169
|
-
Panel({ title: 'Filter', children: h('div', { class: 'row-form' },
|
|
170
|
-
h('input', { type: 'text', placeholder: 'filter by platform/title/model/id…', value: AppState.sessionsFilter,
|
|
171
|
-
oninput: (ev) => { AppState.sessionsFilter = ev.target.value; rerender() } })) }),
|
|
172
|
-
Panel({ title: 'Recent sessions (click row → detail)', count: filtered.length,
|
|
173
|
-
children: filtered.length === 0
|
|
174
|
-
? EmptyState({ text: 'no sessions yet — start a chat', glyph: '✉' })
|
|
175
|
-
: h('div', {}, ...filtered.map(s =>
|
|
176
|
-
RowLink({ key: s.id, href: '#/session/' + s.id,
|
|
177
|
-
code: s.id?.slice(0, 8), title: s.title || s.platform || 'untitled',
|
|
178
|
-
sub: s.model || '', meta: new Date(s.updated_at || 0).toLocaleString() }))) }),
|
|
179
|
-
]
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
'#/agents': async () => {
|
|
183
|
-
const agents = await j('/api/agents')
|
|
184
|
-
const count = agents.__error ? 0 : (agents.count || 0)
|
|
185
|
-
const active = agents.active || null
|
|
186
|
-
return [
|
|
187
|
-
kpi([[count || 0, 'Total agents'], [active ? 1 : 0, 'Active']]),
|
|
188
|
-
Panel({ title: 'Agent overview', children: Receipt({ rows: [
|
|
189
|
-
['Total agent turns', String(agents.turns || 0)],
|
|
190
|
-
['Active agent', active || '(none)'],
|
|
191
|
-
['Last activity', agents.last_activity ? new Date(agents.last_activity).toLocaleString() : '—'],
|
|
192
|
-
]}) }),
|
|
193
|
-
]
|
|
194
|
-
},
|
|
195
|
-
|
|
196
|
-
'#/analytics': async () => {
|
|
197
|
-
const [sessions, tools, debug] = await Promise.all([j('/api/sessions'), j('/api/tools'), j('/api/debug')])
|
|
198
|
-
const all = Array.isArray(sessions) ? sessions : []
|
|
199
|
-
const ts = Array.isArray(tools) ? tools : []
|
|
200
|
-
const byPlatform = all.reduce((acc, s) => { const k = s.platform || 'unknown'; acc[k] = (acc[k] || 0) + 1; return acc }, {})
|
|
201
|
-
const byModel = all.reduce((acc, s) => { const k = s.model || 'unknown'; acc[k] = (acc[k] || 0) + 1; return acc }, {})
|
|
202
|
-
return [
|
|
203
|
-
kpi([
|
|
204
|
-
[all.length || 0, 'Sessions'],
|
|
205
|
-
[ts.length || 0, 'Tools'],
|
|
206
|
-
[Array.isArray(debug) ? debug.length : 0, 'Debug subsystems'],
|
|
207
|
-
]),
|
|
208
|
-
Panel({ title: 'Sessions by platform', children: Object.keys(byPlatform).length === 0
|
|
209
|
-
? EmptyState({ text: 'no sessions yet', glyph: '◉' })
|
|
210
|
-
: table(['platform', 'count'], Object.entries(byPlatform).sort((a,b) => b[1]-a[1])) }),
|
|
211
|
-
Panel({ title: 'Sessions by model', children: Object.keys(byModel).length === 0
|
|
212
|
-
? EmptyState({ text: 'no sessions yet', glyph: '◎' })
|
|
213
|
-
: table(['model', 'count'], Object.entries(byModel).sort((a,b) => b[1]-a[1])) }),
|
|
214
|
-
Panel({ title: 'Tool distribution by toolset', children: table(['toolset', 'count', 'tools'],
|
|
215
|
-
Object.entries(ts.reduce((acc, t) => { acc[t.toolset] = acc[t.toolset] || []; acc[t.toolset].push(t.name); return acc }, {}))
|
|
216
|
-
.map(([k, v]) => [k, v.length, v.slice(0,4).join(', ') + (v.length > 4 ? '…' : '')])) }),
|
|
217
|
-
]
|
|
218
|
-
},
|
|
219
|
-
|
|
220
|
-
'#/models': async () => {
|
|
221
|
-
const config = await j('/api/config')
|
|
222
|
-
const agent = config.agent || {}
|
|
223
|
-
return [
|
|
224
|
-
kpi([[agent.provider || '—', 'Provider'], [agent.model || '—', 'Model']]),
|
|
225
|
-
Panel({ title: 'Active model config', children: Receipt({ rows: [
|
|
226
|
-
['provider', agent.provider || '(not set)'],
|
|
227
|
-
['model', agent.model || '(not set)'],
|
|
228
|
-
['max_iterations', String(agent.max_iterations || '—')],
|
|
229
|
-
['max_tokens', String(agent.max_tokens || '—')],
|
|
230
|
-
['temperature', String(agent.temperature ?? '—')],
|
|
231
|
-
]}) }),
|
|
232
|
-
Panel({ title: 'Change model', children: h('form', { class: 'row-form', onsubmit: async (ev) => {
|
|
233
|
-
ev.preventDefault()
|
|
234
|
-
const f = ev.target.elements
|
|
235
|
-
await j('/api/config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ key: 'agent.model', value: f.model.value }) })
|
|
236
|
-
await j('/api/config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ key: 'agent.provider', value: f.provider.value }) })
|
|
237
|
-
rerender()
|
|
238
|
-
} },
|
|
239
|
-
h('input', { name: 'provider', placeholder: 'provider (anthropic / openai / groq)', value: agent.provider || '' }),
|
|
240
|
-
h('input', { name: 'model', placeholder: 'model id (e.g. claude-opus-4-5)', value: agent.model || '' }),
|
|
241
|
-
h('button', { type: 'submit', class: 'primary' }, 'Update')) }),
|
|
242
|
-
]
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
'#/logs': async () => {
|
|
246
|
-
const subs = await j('/api/logs')
|
|
247
|
-
const list = Array.isArray(subs) ? subs : []
|
|
248
|
-
const first = list[0]
|
|
249
|
-
const recent = first ? await j(`/api/logs/${first}?max=50`) : []
|
|
250
|
-
return [
|
|
251
|
-
kpi([[list.length, 'Log subsystems']]),
|
|
252
|
-
Panel({ title: 'Subsystems', children: list.length === 0
|
|
253
|
-
? EmptyState({ text: 'no logs yet — run freddie and observe', glyph: '☰' })
|
|
254
|
-
: h('div', {}, ...list.map(s => Row({ key: s, code: '☰', title: s, meta: '' }))) }),
|
|
255
|
-
first
|
|
256
|
-
? Panel({ title: `Latest entries · ${first}`, children: pre(recent) })
|
|
257
|
-
: null,
|
|
258
|
-
].filter(Boolean)
|
|
259
|
-
},
|
|
260
|
-
|
|
261
|
-
'#/cron': async () => {
|
|
262
|
-
const jobs = await j('/api/cron')
|
|
263
|
-
const list = Array.isArray(jobs) ? jobs : []
|
|
264
|
-
return [
|
|
265
|
-
kpi([[list.length, 'Cron jobs']]),
|
|
266
|
-
Panel({ title: 'Add job', children: h('form', { class: 'row-form', onsubmit: async (ev) => {
|
|
267
|
-
ev.preventDefault()
|
|
268
|
-
const f = ev.target.elements
|
|
269
|
-
await j('/api/cron', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ cron: f.cron.value, prompt: f.prompt.value }) })
|
|
270
|
-
f.cron.value = ''; f.prompt.value = ''; rerender()
|
|
271
|
-
} },
|
|
272
|
-
h('input', { name: 'cron', placeholder: 'cron expr (* * * * *)' }),
|
|
273
|
-
h('input', { name: 'prompt', placeholder: 'prompt' }),
|
|
274
|
-
h('button', { type: 'submit', class: 'primary' }, 'Create')) }),
|
|
275
|
-
Panel({ title: 'Scheduled jobs', count: list.length, children: list.length === 0
|
|
276
|
-
? EmptyState({ text: 'no cron jobs — add one above', glyph: '◷' })
|
|
277
|
-
: h('table', {},
|
|
278
|
-
h('thead', {}, h('tr', {}, ...['id', 'cron', 'prompt', 'enabled', ''].map(c => h('th', {}, c)))),
|
|
279
|
-
h('tbody', {}, ...list.map(job => h('tr', {},
|
|
280
|
-
h('td', {}, String(job.id)),
|
|
281
|
-
h('td', {}, job.cron),
|
|
282
|
-
h('td', {}, (job.prompt || '').slice(0, 60)),
|
|
283
|
-
h('td', {}, job.enabled ? 'yes' : 'no'),
|
|
284
|
-
h('td', {}, h('button', {
|
|
285
|
-
class: 'danger',
|
|
286
|
-
onclick: async () => { await fetch('/api/cron/' + job.id, { method: 'DELETE' }); rerender() }
|
|
287
|
-
}, 'delete')))))) }),
|
|
288
|
-
]
|
|
289
|
-
},
|
|
290
|
-
|
|
291
|
-
'#/skills': async () => {
|
|
292
|
-
const data = await j('/api/skills')
|
|
293
|
-
const home = data.home || []
|
|
294
|
-
const bundled = data.bundled || []
|
|
295
|
-
return [
|
|
296
|
-
kpi([[home.length, 'User skills'], [bundled.length, 'Bundled skills']]),
|
|
297
|
-
Panel({ title: 'User skills (~/.freddie/skills)', count: home.length,
|
|
298
|
-
children: home.length === 0
|
|
299
|
-
? EmptyState({ text: 'drop SKILL.md files in ~/.freddie/skills/ to add', glyph: '◈' })
|
|
300
|
-
: h('div', {}, ...home.map(s => Row({ key: s.name, code: '◈', title: s.name, sub: s.description || '', meta: '' }))) }),
|
|
301
|
-
Panel({ title: 'Bundled skills', count: bundled.length,
|
|
302
|
-
children: h('div', {}, ...bundled.map(s => Row({ key: s.name, code: '◈', title: s.name, sub: s.description || '', meta: '' }))) }),
|
|
303
|
-
]
|
|
304
|
-
},
|
|
305
|
-
|
|
306
|
-
'#/config': async () => {
|
|
307
|
-
const config = await j('/api/config')
|
|
308
|
-
const profiles = await j('/api/profiles')
|
|
309
|
-
const commands = await j('/api/commands')
|
|
310
|
-
return [
|
|
311
|
-
kpi([
|
|
312
|
-
[(profiles || []).length, 'Profiles'],
|
|
313
|
-
[(commands || []).length, 'Commands'],
|
|
314
|
-
[config._config_version || 0, 'Config version'],
|
|
315
|
-
]),
|
|
316
|
-
Panel({ title: 'Set config value', children: h('form', { class: 'row-form', onsubmit: async (ev) => {
|
|
317
|
-
ev.preventDefault()
|
|
318
|
-
const f = ev.target.elements
|
|
319
|
-
await j('/api/config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ key: f.key.value, value: f.value.value }) })
|
|
320
|
-
f.value.value = ''; rerender()
|
|
321
|
-
} },
|
|
322
|
-
h('input', { name: 'key', placeholder: 'dotted.key (e.g. display.skin)' }),
|
|
323
|
-
h('input', { name: 'value', placeholder: 'value' }),
|
|
324
|
-
h('button', { type: 'submit', class: 'primary' }, 'Save')) }),
|
|
325
|
-
Panel({ title: 'Profiles', count: (profiles || []).length,
|
|
326
|
-
children: (profiles || []).length === 0
|
|
327
|
-
? EmptyState({ text: 'no profiles — using HOME', glyph: '◎' })
|
|
328
|
-
: h('div', {}, ...(profiles || []).map(p => Row({ key: p, code: '◎', title: p, meta: '' }))) }),
|
|
329
|
-
Panel({ title: 'Slash commands', count: (commands || []).length,
|
|
330
|
-
children: table(['name', 'category', 'description'], (commands || []).map(c => [c.name, c.category || '', c.description || ''])) }),
|
|
331
|
-
Panel({ title: 'Active config', children: pre(config) }),
|
|
332
|
-
]
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
'#/env': async () => {
|
|
336
|
-
const keys = await j('/api/env')
|
|
337
|
-
const list = Array.isArray(keys) ? keys : []
|
|
338
|
-
const set = list.filter(k => k.set).length
|
|
339
|
-
return [
|
|
340
|
-
kpi([[set, 'Keys set'], [list.length - set, 'Keys missing'], [list.length, 'Total known']]),
|
|
341
|
-
Panel({ title: 'Environment variables',
|
|
342
|
-
right: h('span', {}, Chip({ tone: 'ok', children: set + ' set' }), ' ', Chip({ tone: 'miss', children: (list.length - set) + ' missing' })),
|
|
343
|
-
children: h('div', { style: 'padding:8px 4px;display:flex;flex-wrap:wrap;gap:6px' },
|
|
344
|
-
...list.map(k => Chip({ tone: k.set ? 'ok' : 'miss', children: k.key + (k.set ? ' ✓' : ' ·') }))) }),
|
|
345
|
-
]
|
|
346
|
-
},
|
|
347
|
-
|
|
348
|
-
'#/tools': async () => {
|
|
349
|
-
const tools = await j('/api/tools')
|
|
350
|
-
const list = Array.isArray(tools) ? tools : []
|
|
351
|
-
const byToolset = list.reduce((acc, t) => { (acc[t.toolset] = acc[t.toolset] || []).push(t); return acc }, {})
|
|
352
|
-
return [
|
|
353
|
-
kpi([[list.length, 'Total tools'], [Object.keys(byToolset).length, 'Toolsets']]),
|
|
354
|
-
...Object.entries(byToolset).map(([ts, ts_tools]) =>
|
|
355
|
-
Panel({ title: 'Toolset · ' + ts, count: ts_tools.length,
|
|
356
|
-
children: h('div', {}, ...ts_tools.map(t =>
|
|
357
|
-
Row({ key: t.name, code: '⚒', title: t.name, sub: (t.schema?.description || '').slice(0, 80), meta: '' }))) }))
|
|
358
|
-
]
|
|
359
|
-
},
|
|
360
|
-
|
|
361
|
-
'#/batch': async () => {
|
|
362
|
-
const results = AppState.batch.results
|
|
363
|
-
const running = AppState.batch.running
|
|
364
|
-
return [
|
|
365
|
-
Section({ title: '// batch runner', children: [
|
|
366
|
-
Panel({ title: 'Run prompts', children: h('div', {},
|
|
367
|
-
h('p', { style: 'margin-bottom:12px;opacity:0.7' }, 'Submit multiple prompts in parallel. Results stream back as JSONL. Each prompt runs a full agent turn.'),
|
|
368
|
-
h('form', { class: 'row-form', style: 'flex-direction:column;gap:8px', onsubmit: async (ev) => {
|
|
369
|
-
ev.preventDefault()
|
|
370
|
-
const f = ev.target.elements
|
|
371
|
-
const prompts = f.prompts.value.split('\n').map(l => l.trim()).filter(Boolean)
|
|
372
|
-
if (!prompts.length) return
|
|
373
|
-
AppState.batch.running = true; AppState.batch.results = null; rerender()
|
|
374
|
-
const res = await j('/api/batch', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
375
|
-
body: JSON.stringify({ prompts, concurrency: Number(f.concurrency.value) || 4 }) })
|
|
376
|
-
AppState.batch.results = res; AppState.batch.running = false; rerender()
|
|
377
|
-
} },
|
|
378
|
-
h('textarea', { name: 'prompts', rows: 5, placeholder: 'One prompt per line…', style: 'width:100%;font-family:monospace;resize:vertical' }),
|
|
379
|
-
h('div', { style: 'display:flex;gap:8px;align-items:center' },
|
|
380
|
-
h('label', { style: 'font-size:12px;opacity:0.6' }, 'concurrency'),
|
|
381
|
-
h('input', { name: 'concurrency', type: 'number', value: '4', style: 'width:60px' }),
|
|
382
|
-
h('button', { type: 'submit', class: 'primary', disabled: running }, running ? 'running…' : 'Run batch')))) }),
|
|
383
|
-
running ? Panel({ title: 'Running…', children: EmptyState({ text: 'batch in progress', glyph: '⊞' }) }) : null,
|
|
384
|
-
results ? Panel({ title: results.__error ? 'Error' : 'Results · ' + (results.results?.length || 0),
|
|
385
|
-
children: results.__error
|
|
386
|
-
? h('p', { style: 'color:var(--error,red)' }, results.__error)
|
|
387
|
-
: h('div', {}, ...(results.results || []).map((r, i) =>
|
|
388
|
-
Row({ key: i, code: String(i+1), title: (r.prompt || '').slice(0, 60), sub: (r.output || r.error || '').slice(0, 100), meta: r.error ? 'error' : 'ok' }))) }) : null,
|
|
389
|
-
Panel({ title: 'CLI usage', children: Receipt({ rows: [
|
|
390
|
-
['run batch file', 'freddie batch prompts.txt'],
|
|
391
|
-
['set concurrency', 'freddie batch prompts.txt --concurrency 8'],
|
|
392
|
-
['JSONL output', 'freddie batch prompts.txt > results.jsonl'],
|
|
393
|
-
]}) }),
|
|
394
|
-
].filter(Boolean) }),
|
|
395
|
-
]
|
|
396
|
-
},
|
|
397
|
-
|
|
398
|
-
'#/gateway': async () => {
|
|
399
|
-
const data = await j('/api/gateway')
|
|
400
|
-
const platforms = Array.isArray(data?.platforms) ? data.platforms : []
|
|
401
|
-
const active = platforms.filter(p => p.enabled)
|
|
402
|
-
return [
|
|
403
|
-
kpi([[platforms.length, 'Platforms'], [active.length, 'Active']]),
|
|
404
|
-
Panel({ title: 'Platforms', right: active.length > 0 ? Chip({ tone: 'ok', children: active.length + ' active' }) : Chip({ tone: 'miss', children: 'none active' }),
|
|
405
|
-
children: h('div', {}, ...platforms.map(p =>
|
|
406
|
-
Row({ key: p.name, code: p.enabled ? '●' : '○', title: p.name, sub: p.note || '', meta: p.enabled ? 'enabled' : '' }))) }),
|
|
407
|
-
Panel({ title: 'Start gateway', children: Receipt({ rows: [
|
|
408
|
-
['webhook + api_server', 'freddie gateway --port 3000'],
|
|
409
|
-
['specific platform', 'TELEGRAM_BOT_TOKEN=xxx freddie gateway'],
|
|
410
|
-
['all platforms', 'set env vars per platform, then freddie gateway'],
|
|
411
|
-
]}) }),
|
|
412
|
-
]
|
|
413
|
-
},
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function pageSessionDetail(id) {
|
|
418
|
-
const messages = await j('/api/sessions/' + id + '/messages')
|
|
419
|
-
const list = Array.isArray(messages) ? messages : []
|
|
420
|
-
return [
|
|
421
|
-
Panel({ title: 'Session ' + id.slice(0, 8), children: kpi([[list.length, 'messages']]) }),
|
|
422
|
-
list.length === 0
|
|
423
|
-
? Panel({ title: 'Messages', children: EmptyState({ text: 'no messages in this session', glyph: '✉' }) })
|
|
424
|
-
: Chat({ title: 'session ' + id.slice(0, 8), sub: 'replay', messages: list.map((m, i) => toChatMsg(m, 's' + i)) }),
|
|
425
|
-
Panel({ title: 'Back', children: h('a', { href: '#/sessions' }, '← all sessions') }),
|
|
426
|
-
]
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
async function sendChat(prompt) {
|
|
430
|
-
if (!prompt || !prompt.trim() || AppState.chat.streaming) return
|
|
431
|
-
AppState.chat.messages.push({ role: 'user', content: prompt, time: timeNow() })
|
|
432
|
-
AppState.chat.streaming = true
|
|
433
|
-
rerender()
|
|
434
|
-
try {
|
|
435
|
-
const r = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ prompt }) })
|
|
436
|
-
const reader = r.body.getReader(), dec = new TextDecoder()
|
|
437
|
-
let buf = ''
|
|
438
|
-
while (true) {
|
|
439
|
-
const { value, done } = await reader.read()
|
|
440
|
-
if (done) break
|
|
441
|
-
buf += dec.decode(value, { stream: true })
|
|
442
|
-
let idx
|
|
443
|
-
while ((idx = buf.indexOf('\n\n')) >= 0) {
|
|
444
|
-
const block = buf.slice(0, idx); buf = buf.slice(idx + 2)
|
|
445
|
-
const ev = (block.match(/^event: (.+)$/m) || [, ''])[1]
|
|
446
|
-
const data = (block.match(/^data: (.+)$/m) || [, '{}'])[1]
|
|
447
|
-
let parsed; try { parsed = JSON.parse(data) } catch { parsed = { raw: data } }
|
|
448
|
-
if (ev === 'message') {
|
|
449
|
-
if (parsed.role !== 'user') AppState.chat.messages.push({ ...parsed, time: timeNow() })
|
|
450
|
-
} else if (ev === 'error') {
|
|
451
|
-
AppState.chat.messages.push({ role: 'assistant', content: '**[error]** ' + parsed.error, time: timeNow() })
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
} catch (e) {
|
|
456
|
-
AppState.chat.messages.push({ role: 'assistant', content: '**[network error]** ' + e.message, time: timeNow() })
|
|
457
|
-
}
|
|
458
|
-
AppState.chat.streaming = false
|
|
459
|
-
rerender()
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
async function doSearch(q) {
|
|
463
|
-
AppState.search.query = q
|
|
464
|
-
if (!q.trim()) { AppState.search.results = []; rerender(); return }
|
|
465
|
-
const r = await j('/api/search?q=' + encodeURIComponent(q))
|
|
466
|
-
AppState.search.results = Array.isArray(r) ? r : []
|
|
467
|
-
rerender()
|
|
14
|
+
function regFromArray(arr, keyFn = (x) => x.name) {
|
|
15
|
+
const m = new Map();
|
|
16
|
+
for (const it of arr || []) m.set(keyFn(it), it);
|
|
17
|
+
return { get: (k) => m.get(k), has: (k) => m.has(k), list: () => [...m.values()], values: () => m.values(), get size() { return m.size; } };
|
|
468
18
|
}
|
|
469
19
|
|
|
470
|
-
function
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
20
|
+
async function fetchHost() {
|
|
21
|
+
const [tools, skillsResp, cron, projectsResp, env, gateway, health, commands] = await Promise.all([
|
|
22
|
+
j('/api/tools/detail').catch(() => []),
|
|
23
|
+
j('/api/skills').catch(() => ({ home: [], bundled: [] })),
|
|
24
|
+
j('/api/cron').catch(() => []),
|
|
25
|
+
j('/api/projects').catch(() => ({ active: null, projects: [] })),
|
|
26
|
+
j('/api/env').catch(() => []),
|
|
27
|
+
j('/api/gateway').catch(() => ({ platforms: [] })),
|
|
28
|
+
j('/api/health').catch(() => ({ ok: false })),
|
|
29
|
+
j('/api/commands').catch(() => []),
|
|
30
|
+
]);
|
|
31
|
+
const skillList = [...(skillsResp.home || []), ...(skillsResp.bundled || [])];
|
|
32
|
+
const projList = projectsResp.projects || [];
|
|
33
|
+
return {
|
|
34
|
+
kind: 'freddie-web', version: 'web',
|
|
35
|
+
pi: {
|
|
36
|
+
tools: regFromArray(tools),
|
|
37
|
+
skills: regFromArray(skillList, (s) => s.name || s.id),
|
|
38
|
+
cli: regFromArray(commands),
|
|
39
|
+
projects: {
|
|
40
|
+
list: () => projList,
|
|
41
|
+
active: () => projectsResp.active,
|
|
42
|
+
create: ({ name, path }) => post('/api/projects', { name, path }).then(() => location.reload()),
|
|
43
|
+
remove: (name) => fetch('/api/projects/' + encodeURIComponent(name), { method: 'DELETE' }).then(() => location.reload()),
|
|
44
|
+
setActive: (name) => post('/api/projects/active', { name }).then(() => location.reload()),
|
|
45
|
+
},
|
|
46
|
+
sessions: {
|
|
47
|
+
list: () => j('/api/sessions').catch(() => []),
|
|
48
|
+
getMessages: (id) => j('/api/sessions/' + encodeURIComponent(id) + '/messages').catch(() => []),
|
|
49
|
+
search: (q) => j('/api/search?q=' + encodeURIComponent(q)).catch(() => []),
|
|
50
|
+
},
|
|
51
|
+
cron: {
|
|
52
|
+
list: () => Promise.resolve(cron),
|
|
53
|
+
create: (job) => post('/api/cron', job),
|
|
54
|
+
delete: (id) => fetch('/api/cron/' + id, { method: 'DELETE' }),
|
|
55
|
+
},
|
|
56
|
+
env: { list: () => env, isSet: (k) => (env.find(e => e.key === k) || {}).set || false },
|
|
57
|
+
gateway: { platforms: () => gateway.platforms || [] },
|
|
58
|
+
agents: () => j('/api/agents').catch(() => ({ count: 0, turns: 0, active: null })),
|
|
59
|
+
health: () => health,
|
|
60
|
+
config: {
|
|
61
|
+
load: () => j('/api/config').catch(() => ({})),
|
|
62
|
+
saveValue: (path, value) => post('/api/config', { path, value }),
|
|
63
|
+
},
|
|
64
|
+
chat: { send: (text) => post('/api/chat', { text }) },
|
|
65
|
+
batch: { run: (prompts, conc) => post('/api/batch', { prompts, concurrency: conc }) },
|
|
66
|
+
hooks: {},
|
|
495
67
|
},
|
|
496
|
-
|
|
497
|
-
}, themeLabel)
|
|
498
|
-
const searchInput = h('input', {
|
|
499
|
-
type: 'search',
|
|
500
|
-
placeholder: 'search messages…',
|
|
501
|
-
value: state.search.query,
|
|
502
|
-
onkeydown: (ev) => { if (ev.key === 'Enter') doSearch(ev.target.value) },
|
|
503
|
-
style: 'min-width:240px',
|
|
504
|
-
})
|
|
505
|
-
const projectPill = h('a', {
|
|
506
|
-
href: '#/projects',
|
|
507
|
-
class: 'project-pill',
|
|
508
|
-
title: state.projects.active ? 'Active project: ' + state.projects.active.name + ' — ' + state.projects.active.path : 'No active project',
|
|
509
|
-
style: 'display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;background:var(--panel-2);color:var(--panel-text);text-decoration:none;font-size:12px;font-weight:500',
|
|
510
|
-
}, h('span', { style: 'opacity:0.6' }, '◆'), h('span', {}, state.projects.active?.name || 'default'))
|
|
511
|
-
const topbarWithControls = h('header', { class: 'app-topbar' },
|
|
512
|
-
Brand({ name: 'freddie', leaf: 'dashboard' }),
|
|
513
|
-
projectPill,
|
|
514
|
-
h('div', { style: 'flex:1' }),
|
|
515
|
-
searchInput,
|
|
516
|
-
themeBtn,
|
|
517
|
-
)
|
|
518
|
-
const crumbRight = state.search.results.length > 0
|
|
519
|
-
? h('span', { class: 'meta' }, state.search.results.length + ' hits')
|
|
520
|
-
: null
|
|
521
|
-
const crumb = Crumb({ trail: ['freddie'], leaf: isSessionDetail ? state.hash.replace('#/', '') : route.path.replace('#/', ''), right: crumbRight })
|
|
522
|
-
const searchResults = state.search.results.length > 0
|
|
523
|
-
? Panel({ title: `search results · ${state.search.results.length}`, children: state.search.results.slice(0, 8).map((r, i) =>
|
|
524
|
-
Row({ key: i, code: (r.session_id || '?').slice(0, 8), title: (r.content || '').slice(0, 80),
|
|
525
|
-
meta: 'open', onClick: () => { location.hash = '#/session/' + r.session_id } })) })
|
|
526
|
-
: null
|
|
527
|
-
const main = [searchResults, state.body || EmptyState({ text: 'loading…' })].filter(Boolean)
|
|
528
|
-
const status = Status({
|
|
529
|
-
left: ['ds-247420 · webjsx · ' + ROUTES.length + ' routes', 'theme=' + state.theme],
|
|
530
|
-
right: [state.ts],
|
|
531
|
-
})
|
|
532
|
-
return AppShell({ topbar: topbarWithControls, crumb, side: buildSide(state), main, status })
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
let _mount
|
|
536
|
-
|
|
537
|
-
async function refreshProject() {
|
|
538
|
-
const data = await j('/api/projects')
|
|
539
|
-
if (!data.__error) AppState.projects = { active: data.active || null, all: data.projects || [] }
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
async function go() {
|
|
543
|
-
AppState.hash = location.hash || '#/projects'
|
|
544
|
-
AppState.ts = new Date().toLocaleTimeString()
|
|
545
|
-
AppState.body = EmptyState({ text: 'loading…', glyph: '◌' })
|
|
546
|
-
if (_mount) _mount()
|
|
547
|
-
refreshProject()
|
|
548
|
-
let body
|
|
549
|
-
if (AppState.hash.startsWith('#/session/')) {
|
|
550
|
-
body = await pageSessionDetail(AppState.hash.slice('#/session/'.length))
|
|
551
|
-
} else {
|
|
552
|
-
const page = PAGES[AppState.hash] || PAGES['#/home']
|
|
553
|
-
body = await page()
|
|
554
|
-
}
|
|
555
|
-
AppState.body = body
|
|
556
|
-
AppState.ts = new Date().toLocaleTimeString()
|
|
557
|
-
if (_mount) _mount()
|
|
558
|
-
window.__debug.lastRoute = AppState.hash
|
|
559
|
-
requestAnimationFrame(() => motion.animateSelector('.app-main', 'fadeIn', { duration: 'var(--motion-base)' }))
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function rerender() {
|
|
563
|
-
AppState.ts = new Date().toLocaleTimeString()
|
|
564
|
-
if (AppState.hash === '#/chat') {
|
|
565
|
-
Promise.resolve(PAGES['#/chat']()).then(b => { AppState.body = b; if (_mount) _mount() })
|
|
566
|
-
return
|
|
567
|
-
}
|
|
568
|
-
if (_mount) _mount()
|
|
68
|
+
};
|
|
569
69
|
}
|
|
570
70
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
71
|
+
const root = document.getElementById('app');
|
|
72
|
+
root.textContent = 'loading…';
|
|
73
|
+
const host = await fetchHost();
|
|
74
|
+
root.innerHTML = '';
|
|
75
|
+
const inst = { id: 'web', fs: { list: () => Promise.resolve([]) }, host };
|
|
76
|
+
const { node } = createFreddieDashboard({ instance: inst });
|
|
77
|
+
root.appendChild(node);
|
|
574
78
|
|
|
575
|
-
window.__debug
|
|
576
|
-
window.__debug.
|
|
577
|
-
window.__debug.doSearch = doSearch
|
|
578
|
-
window.__debug.chat = () => ({ messages: AppState.chat.messages.length, streaming: AppState.chat.streaming, draft: AppState.chat.draft })
|
|
79
|
+
window.__debug = window.__debug || {};
|
|
80
|
+
window.__debug.dashboard = () => ({ booted: true, mode: 'fetchHost', tools: host.pi.tools.size, skills: host.pi.skills.size });
|