agentgui 1.0.925 → 1.0.927
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 +18 -0
- package/package.json +1 -1
- package/site/app/index.html +3 -17
- package/site/app/js/app.js +241 -101
package/AGENTS.md
CHANGED
|
@@ -86,3 +86,21 @@ The function previously returned "Model: X." when agentId was 'claude-code' and
|
|
|
86
86
|
**WebSocket `/sync` endpoint — message ordering requires registering handler BEFORE sending.**
|
|
87
87
|
|
|
88
88
|
Server sends `sync_connected` with `clientId` on connect. Legacy handler (`lib/ws-legacy-handlers.js`) handles `ping→pong`, `subscribe→subscription_confirmed`, `get_subscriptions→subscriptions`, `unsubscribe`, `latency_report`. All responses use codec encode/decode (`lib/codec.js`). Pattern: queue outbound messages and use a waiters array + sequential promises to avoid race between send and handler registration. Test structure: `const queued = []; let waiting; ws.on('message', ...); queued.forEach(msg => ws.send(msg)); waiting.resolve(...)` ensures the handler is live before messages flow.
|
|
89
|
+
|
|
90
|
+
## better-sqlite3 & Node v24 Startup (2026-05-03)
|
|
91
|
+
|
|
92
|
+
**Node v24 has no prebuilt binary for better-sqlite3 (module version 137); npm rebuild silently fails.**
|
|
93
|
+
|
|
94
|
+
When npm install runs on Node v24, the postinstall hook for better-sqlite3 silently fails because no prebuilt `.node` binary exists for that module version. The server then fails to require better-sqlite3 and crashes. npm rebuild also silently fails.
|
|
95
|
+
|
|
96
|
+
Fix: compile from source in postinstall:
|
|
97
|
+
```json
|
|
98
|
+
"postinstall": "node scripts/patch-fsbrowse.js && node scripts/copy-vendor.js && (cd node_modules/better-sqlite3 && node-gyp rebuild 2>/dev/null) || true"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Paired changes:
|
|
102
|
+
- `package.json` `start` script: `"bun server.js || node server.js"` (prefer bun, fall back to node)
|
|
103
|
+
- `bin/gmgui.cjs` runtime detection: `spawnSync('bun', ['--version'], { shell: true })` to auto-detect bun availability, fallback to node (lines 46-47)
|
|
104
|
+
- better-sqlite3 bumped ^12.6.2 → ^12.9.0
|
|
105
|
+
|
|
106
|
+
bun start was already working (bun has native sqlite support via `bun:sqlite`). The node path was broken due to the missing native binding. Only compile from source fixes the node path.
|
package/package.json
CHANGED
package/site/app/index.html
CHANGED
|
@@ -3,29 +3,15 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>agentgui
|
|
7
|
-
<meta name="description" content="agentgui live client
|
|
6
|
+
<title>agentgui</title>
|
|
7
|
+
<meta name="description" content="agentgui — live client for any acptoapi backend.">
|
|
8
8
|
<link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@latest/dist/247420.css">
|
|
9
9
|
<script type="importmap">
|
|
10
|
-
{ "imports": {
|
|
11
|
-
"anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js"
|
|
12
|
-
} }
|
|
10
|
+
{ "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js" } }
|
|
13
11
|
</script>
|
|
14
12
|
<style>
|
|
15
13
|
html, body { margin: 0; height: 100%; }
|
|
16
14
|
#app { height: 100vh; }
|
|
17
|
-
.app { height: 100vh; }
|
|
18
|
-
.app-main { display: flex; flex-direction: column; min-height: 0; }
|
|
19
|
-
.chat { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
|
20
|
-
.chat-thread { flex: 1; overflow-y: auto; }
|
|
21
|
-
.agentgui-history-pane { display: grid; grid-template-columns: 300px 1fr; height: 100%; min-height: 0; overflow: hidden; }
|
|
22
|
-
.agentgui-history-list { overflow-y: auto; border-right: 1px solid var(--panel-3); padding: 8px; }
|
|
23
|
-
.agentgui-history-detail { overflow-y: auto; padding: 16px; }
|
|
24
|
-
.agentgui-ev { padding: 6px 0; border-bottom: 1px solid var(--panel-2); font-family: var(--ff-mono); font-size: 12px; }
|
|
25
|
-
.agentgui-ev .h { opacity: .55; margin-bottom: 2px; }
|
|
26
|
-
.agentgui-ev pre { white-space: pre-wrap; margin: 0; }
|
|
27
|
-
.agentgui-model-bar { display: flex; gap: 8px; padding: 8px 16px; border-bottom: 1px solid var(--panel-3); align-items: center; }
|
|
28
|
-
.agentgui-model-bar select { padding: 5px 8px; border-radius: 8px; border: 1px solid var(--panel-3); background: var(--panel-1); color: var(--ink); font-family: var(--ff-ui); font-size: 13px; }
|
|
29
15
|
</style>
|
|
30
16
|
</head>
|
|
31
17
|
<body>
|
package/site/app/js/app.js
CHANGED
|
@@ -3,12 +3,11 @@ import * as B from './backend.js';
|
|
|
3
3
|
|
|
4
4
|
installStyles().catch(() => {});
|
|
5
5
|
|
|
6
|
-
const { AppShell, Topbar, Crumb, Side, Status,
|
|
7
|
-
const { Chat, ChatComposer, ChatMessage } = C;
|
|
8
|
-
const { Row } = C;
|
|
6
|
+
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel } = C;
|
|
9
7
|
|
|
10
8
|
const state = {
|
|
11
9
|
backend: B.getBackend(),
|
|
10
|
+
backendDraft: B.getBackend(),
|
|
12
11
|
health: { status: 'unknown' },
|
|
13
12
|
tab: 'chat',
|
|
14
13
|
models: [],
|
|
@@ -23,53 +22,96 @@ const state = {
|
|
|
23
22
|
|
|
24
23
|
let render;
|
|
25
24
|
|
|
26
|
-
function
|
|
27
|
-
|
|
25
|
+
function timeNow() {
|
|
26
|
+
const d = new Date();
|
|
27
|
+
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function navTo(tab) {
|
|
31
|
+
state.tab = tab;
|
|
32
|
+
if (tab === 'history') refreshHistory();
|
|
33
|
+
render();
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
function view() {
|
|
31
37
|
const ok = state.health.status === 'ok';
|
|
32
|
-
const
|
|
33
|
-
? h('span', { key: 'hc', style: 'color:var(--live);font-size:12px' }, '● connected')
|
|
34
|
-
: h('span', { key: 'hc', style: 'color:var(--flame);font-size:12px' }, '○ offline');
|
|
38
|
+
const dot = ok ? '● connected' : '○ offline';
|
|
35
39
|
|
|
36
40
|
const topbar = Topbar({
|
|
37
41
|
brand: 'agentgui',
|
|
38
42
|
leaf: state.tab,
|
|
39
|
-
items:
|
|
43
|
+
items: [['chat', '#'], ['history', '#'], ['settings', '#']],
|
|
40
44
|
active: state.tab,
|
|
41
|
-
onNav: (label) =>
|
|
45
|
+
onNav: (label) => navTo(label),
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
const crumb = Crumb({
|
|
45
49
|
trail: ['agentgui'],
|
|
46
50
|
leaf: state.tab,
|
|
47
|
-
right: [
|
|
51
|
+
right: [dot],
|
|
48
52
|
});
|
|
49
53
|
|
|
50
|
-
const
|
|
51
|
-
|
|
54
|
+
const navSide = Side({
|
|
55
|
+
sections: [
|
|
56
|
+
{
|
|
57
|
+
group: 'navigate',
|
|
58
|
+
items: [
|
|
59
|
+
{ glyph: '▣', label: 'chat', key: 'chat', active: state.tab === 'chat',
|
|
60
|
+
onClick: (e) => { e.preventDefault(); navTo('chat'); } },
|
|
61
|
+
{ glyph: '§', label: 'history', key: 'history', active: state.tab === 'history',
|
|
62
|
+
onClick: (e) => { e.preventDefault(); navTo('history'); } },
|
|
63
|
+
{ glyph: '⌘', label: 'settings', key: 'settings', active: state.tab === 'settings',
|
|
64
|
+
onClick: (e) => { e.preventDefault(); navTo('settings'); } },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
const side = state.tab === 'history' ? historySide() : navSide;
|
|
70
|
+
|
|
52
71
|
const status = Status({
|
|
53
|
-
left: [state.backend],
|
|
54
|
-
right: [state.selectedModel
|
|
72
|
+
left: [state.backend, ok ? '● live' : '○ offline'],
|
|
73
|
+
right: [state.selectedModel ? '⌘ ' + state.selectedModel : '○ no model'],
|
|
55
74
|
});
|
|
56
75
|
|
|
57
|
-
return AppShell({ topbar, crumb, side, main, status });
|
|
76
|
+
return AppShell({ topbar, crumb, side, main: mainContent(), status });
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
function mainContent() {
|
|
61
|
-
if (state.tab === 'chat')
|
|
62
|
-
if (state.tab === 'history') return
|
|
80
|
+
if (state.tab === 'chat') return chatMain();
|
|
81
|
+
if (state.tab === 'history') return historyMain();
|
|
63
82
|
return settingsMain();
|
|
64
83
|
}
|
|
65
84
|
|
|
85
|
+
// ── chat ───────────────────────────────────────────────────────────────────
|
|
66
86
|
function chatMain() {
|
|
67
87
|
const msgs = state.chat.messages.map((m, i) => ({
|
|
68
|
-
key: i,
|
|
88
|
+
key: String(i),
|
|
69
89
|
who: m.role === 'user' ? 'you' : 'them',
|
|
70
|
-
|
|
90
|
+
name: m.role === 'assistant' ? (state.selectedModel || 'agent') : 'you',
|
|
91
|
+
time: m.time || '',
|
|
92
|
+
parts: [{ kind: 'text', text: m.content || '' }],
|
|
71
93
|
}));
|
|
72
94
|
|
|
95
|
+
const modelPanel = Panel({
|
|
96
|
+
title: 'model',
|
|
97
|
+
children: h('div', { class: 'ds-section' },
|
|
98
|
+
h('select', {
|
|
99
|
+
value: state.selectedModel,
|
|
100
|
+
onchange: (e) => { state.selectedModel = e.target.value; render(); },
|
|
101
|
+
},
|
|
102
|
+
h('option', { value: '' }, '— choose model —'),
|
|
103
|
+
...state.models.map(m =>
|
|
104
|
+
h('option', { value: m.id, selected: m.id === state.selectedModel }, m.id)
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
h('p', { class: 'lede' },
|
|
108
|
+
state.chat.busy
|
|
109
|
+
? h('button', { onclick: cancelChat }, '◼ stop')
|
|
110
|
+
: h('button', { onclick: newChat }, '+ new chat'),
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
});
|
|
114
|
+
|
|
73
115
|
const composer = ChatComposer({
|
|
74
116
|
value: state.chat.draft,
|
|
75
117
|
disabled: state.chat.busy,
|
|
@@ -78,89 +120,39 @@ function chatMain() {
|
|
|
78
120
|
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
79
121
|
});
|
|
80
122
|
|
|
81
|
-
const modelBar = h('div', { class: 'agentgui-model-bar' },
|
|
82
|
-
h('select', { onchange: (e) => { state.selectedModel = e.target.value; render(); } },
|
|
83
|
-
h('option', { value: '' }, '— choose model —'),
|
|
84
|
-
...state.models.map(m => h('option', { value: m.id, selected: m.id === state.selectedModel }, m.id)),
|
|
85
|
-
),
|
|
86
|
-
state.chat.busy
|
|
87
|
-
? h('a', { style: 'cursor:pointer;font-size:12px;color:var(--flame)', onclick: cancelChat }, 'stop ✕')
|
|
88
|
-
: null,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
123
|
return [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
onClick: () => loadSession(r.sid),
|
|
106
|
-
})
|
|
107
|
-
)
|
|
108
|
-
: state.sessions.slice(0, 80).map((s, i) =>
|
|
109
|
-
Row({
|
|
110
|
-
key: 'sess' + i,
|
|
111
|
-
title: s.title || s.project || s.sid,
|
|
112
|
-
sub: s.events + ' ev · ' + s.tools + ' tools · ' + (s.errors ? s.errors + ' err' : 'ok'),
|
|
113
|
-
active: s.sid === state.selectedSid,
|
|
114
|
-
onClick: () => loadSession(s.sid),
|
|
115
|
-
})
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
return h('div', { style: 'display:flex;flex-direction:column;height:100%;overflow:hidden' },
|
|
119
|
-
h('div', { style: 'padding:8px' },
|
|
120
|
-
h('input', {
|
|
121
|
-
style: 'width:100%;padding:6px 10px;border-radius:8px;border:1px solid var(--panel-3);background:var(--panel-1);color:var(--ink);font-family:var(--ff-ui);font-size:13px',
|
|
122
|
-
placeholder: 'search…',
|
|
123
|
-
value: state.searchQ,
|
|
124
|
-
onchange: (e) => { state.searchQ = e.target.value; runSearch(); },
|
|
124
|
+
h('div', { class: 'ds-section' },
|
|
125
|
+
h('h1', {}, '# ' + (state.selectedModel || 'agent')),
|
|
126
|
+
h('p', { class: 'lede' },
|
|
127
|
+
state.chat.messages.length
|
|
128
|
+
? state.chat.messages.length + ' messages in this thread'
|
|
129
|
+
: 'start a conversation with the selected model.',
|
|
130
|
+
),
|
|
131
|
+
modelPanel,
|
|
132
|
+
Chat({
|
|
133
|
+
title: state.selectedModel || 'agent',
|
|
134
|
+
sub: state.chat.busy ? 'streaming…' : (state.chat.messages.length + ' messages'),
|
|
135
|
+
messages: msgs,
|
|
136
|
+
composer,
|
|
125
137
|
}),
|
|
126
138
|
),
|
|
127
|
-
|
|
128
|
-
rows.length === 0 ? h('div', { style: 'opacity:.6;padding:16px;font-size:13px' }, 'no sessions') : null,
|
|
129
|
-
),
|
|
130
|
-
);
|
|
139
|
+
];
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
...state.events.map((e, i) =>
|
|
138
|
-
h('div', { key: 'ev' + i, class: 'agentgui-ev' },
|
|
139
|
-
h('div', { class: 'h' },
|
|
140
|
-
new Date(e.ts).toLocaleString() + ' · ' + (e.role || '?') + ' · ' + (e.type || '?') + (e.tool ? ' · ' + e.tool : ''),
|
|
141
|
-
),
|
|
142
|
-
h('pre', {}, (e.text || '').slice(0, 4000)),
|
|
143
|
-
)
|
|
144
|
-
),
|
|
145
|
-
);
|
|
142
|
+
function newChat() {
|
|
143
|
+
state.chat.abort?.abort();
|
|
144
|
+
state.chat = { messages: [], busy: false, abort: null, draft: '' };
|
|
145
|
+
render();
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
function
|
|
149
|
-
return h('div', { style: 'padding:24px;max-width:480px' },
|
|
150
|
-
h('h2', {}, 'settings'),
|
|
151
|
-
h('p', {}, 'backend: ', h('code', {}, state.backend)),
|
|
152
|
-
h('p', {}, 'health: ', h('code', {}, JSON.stringify(state.health))),
|
|
153
|
-
h('p', { style: 'opacity:.7;font-size:13px' },
|
|
154
|
-
'pass ', h('code', {}, '?backend=https://your-acptoapi-host'), ' or set localStorage[\'agentgui.backend\'] to override',
|
|
155
|
-
),
|
|
156
|
-
);
|
|
157
|
-
}
|
|
148
|
+
function cancelChat() { state.chat.abort?.abort(); }
|
|
158
149
|
|
|
159
150
|
async function sendChat() {
|
|
160
151
|
const text = (state.chat.draft || '').trim();
|
|
161
152
|
if (!text || !state.selectedModel || state.chat.busy) return;
|
|
162
|
-
|
|
163
|
-
state.chat.messages.push({ role: '
|
|
153
|
+
const t = timeNow();
|
|
154
|
+
state.chat.messages.push({ role: 'user', content: text, time: t });
|
|
155
|
+
state.chat.messages.push({ role: 'assistant', content: '', time: t });
|
|
164
156
|
state.chat.draft = '';
|
|
165
157
|
state.chat.busy = true;
|
|
166
158
|
const ctrl = new AbortController();
|
|
@@ -173,11 +165,11 @@ async function sendChat() {
|
|
|
173
165
|
messages: state.chat.messages.slice(0, -1).map(m => ({ role: m.role, content: m.content })),
|
|
174
166
|
signal: ctrl.signal,
|
|
175
167
|
})) {
|
|
176
|
-
if (ev.type === 'text')
|
|
168
|
+
if (ev.type === 'text') { cur.content += ev.text; render(); }
|
|
177
169
|
if (ev.type === 'error') { cur.content += '\n[error] ' + JSON.stringify(ev.error); render(); }
|
|
178
170
|
}
|
|
179
171
|
} catch (e) {
|
|
180
|
-
cur.content += '\n[error] ' + e.message;
|
|
172
|
+
if (e.name !== 'AbortError') cur.content += '\n[error] ' + e.message;
|
|
181
173
|
} finally {
|
|
182
174
|
state.chat.busy = false;
|
|
183
175
|
state.chat.abort = null;
|
|
@@ -185,8 +177,142 @@ async function sendChat() {
|
|
|
185
177
|
}
|
|
186
178
|
}
|
|
187
179
|
|
|
188
|
-
|
|
180
|
+
// ── history ────────────────────────────────────────────────────────────────
|
|
181
|
+
function historyMain() {
|
|
182
|
+
const head = h('div', { class: 'ds-section' },
|
|
183
|
+
h('h1', {}, '§ history'),
|
|
184
|
+
h('p', { class: 'lede' },
|
|
185
|
+
state.selectedSid
|
|
186
|
+
? 'session ' + state.selectedSid
|
|
187
|
+
: 'pick a session from the sidebar — events stream from acptoapi /v1/history.',
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!state.selectedSid) return [head];
|
|
192
|
+
if (state.events.length === 0) return [head, Panel({ title: 'events', children: h('p', { class: 'lede' }, '◌ loading…') })];
|
|
189
193
|
|
|
194
|
+
const rows = state.events.map((e, i) =>
|
|
195
|
+
Row({
|
|
196
|
+
key: 'ev' + i,
|
|
197
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
198
|
+
title: (e.text || '').slice(0, 200) || '(empty)',
|
|
199
|
+
sub: new Date(e.ts).toLocaleString() + ' · ' + (e.role || '?') + ' · ' + (e.type || '?') + (e.tool ? ' · ⌘ ' + e.tool : ''),
|
|
200
|
+
rail: e.role === 'error' ? 'flame' : (e.role === 'user' ? 'green' : 'purple'),
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return [
|
|
205
|
+
head,
|
|
206
|
+
Panel({
|
|
207
|
+
title: state.events.length + ' events',
|
|
208
|
+
children: h('div', { class: 'ds-section' }, ...rows),
|
|
209
|
+
}),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function historySide() {
|
|
214
|
+
const searching = !!state.searchHits;
|
|
215
|
+
const rows = searching
|
|
216
|
+
? state.searchHits.results.slice(0, 60).map((r, i) =>
|
|
217
|
+
Row({
|
|
218
|
+
key: 'sr' + i,
|
|
219
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
220
|
+
title: r.snippet || '(no snippet)',
|
|
221
|
+
sub: (r.project || '?') + ' · ' + (r.role || '?') + (r.tool ? ' · ' + r.tool : ''),
|
|
222
|
+
rail: 'purple',
|
|
223
|
+
onClick: () => loadSession(r.sid),
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
: state.sessions.slice(0, 120).map((s, i) =>
|
|
227
|
+
Row({
|
|
228
|
+
key: 'sess' + i,
|
|
229
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
230
|
+
title: s.title || s.project || s.sid,
|
|
231
|
+
sub: s.events + ' ev · ' + s.tools + ' tools' + (s.errors ? ' · ' + s.errors + ' err' : ''),
|
|
232
|
+
rail: s.errors ? 'flame' : 'green',
|
|
233
|
+
active: s.sid === state.selectedSid,
|
|
234
|
+
onClick: () => loadSession(s.sid),
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
return [
|
|
239
|
+
Side({
|
|
240
|
+
sections: [
|
|
241
|
+
{
|
|
242
|
+
group: 'navigate',
|
|
243
|
+
items: [
|
|
244
|
+
{ glyph: '▣', label: 'chat', key: 'chat', onClick: (e) => { e.preventDefault(); navTo('chat'); } },
|
|
245
|
+
{ glyph: '§', label: 'history', key: 'history', active: true },
|
|
246
|
+
{ glyph: '⌘', label: 'settings', key: 'settings', onClick: (e) => { e.preventDefault(); navTo('settings'); } },
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
}),
|
|
251
|
+
Panel({
|
|
252
|
+
title: searching ? 'matches' : 'sessions',
|
|
253
|
+
children: h('div', { class: 'ds-section' },
|
|
254
|
+
h('input', {
|
|
255
|
+
type: 'search',
|
|
256
|
+
placeholder: 'search sessions…',
|
|
257
|
+
value: state.searchQ,
|
|
258
|
+
oninput: (e) => { state.searchQ = e.target.value; runSearch(); },
|
|
259
|
+
}),
|
|
260
|
+
rows.length ? h('div', {}, ...rows) : h('p', { class: 'lede' }, 'no sessions yet'),
|
|
261
|
+
),
|
|
262
|
+
}),
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── settings ───────────────────────────────────────────────────────────────
|
|
267
|
+
function settingsMain() {
|
|
268
|
+
const ok = state.health.status === 'ok';
|
|
269
|
+
return [
|
|
270
|
+
h('div', { class: 'ds-section' },
|
|
271
|
+
h('h1', {}, '⌘ settings'),
|
|
272
|
+
h('p', { class: 'lede' }, 'point agentgui at any acptoapi backend. ?backend=… in the URL or the field below — both persist via localStorage.'),
|
|
273
|
+
Panel({
|
|
274
|
+
title: 'backend',
|
|
275
|
+
children: h('div', { class: 'ds-section' },
|
|
276
|
+
h('p', { class: 'lede' }, 'backend url'),
|
|
277
|
+
h('input', {
|
|
278
|
+
type: 'text',
|
|
279
|
+
value: state.backendDraft,
|
|
280
|
+
oninput: (e) => { state.backendDraft = e.target.value; render(); },
|
|
281
|
+
}),
|
|
282
|
+
h('p', { class: 'lede' }, (ok ? '● ' : '○ ') + JSON.stringify(state.health)),
|
|
283
|
+
h('button', {
|
|
284
|
+
onclick: () => {
|
|
285
|
+
B.setBackend(state.backendDraft);
|
|
286
|
+
state.backend = state.backendDraft;
|
|
287
|
+
state.health = { status: 'unknown' };
|
|
288
|
+
render();
|
|
289
|
+
init();
|
|
290
|
+
},
|
|
291
|
+
}, 'save + reconnect'),
|
|
292
|
+
),
|
|
293
|
+
}),
|
|
294
|
+
Panel({
|
|
295
|
+
title: 'models',
|
|
296
|
+
children: h('div', { class: 'ds-section' },
|
|
297
|
+
state.models.length
|
|
298
|
+
? h('div', {}, ...state.models.slice(0, 40).map((m, i) =>
|
|
299
|
+
Row({
|
|
300
|
+
key: 'm' + i,
|
|
301
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
302
|
+
title: m.id,
|
|
303
|
+
sub: m.owned_by || m.object || 'model',
|
|
304
|
+
rail: m.id === state.selectedModel ? 'green' : 'purple',
|
|
305
|
+
onClick: () => { state.selectedModel = m.id; render(); },
|
|
306
|
+
})
|
|
307
|
+
))
|
|
308
|
+
: h('p', { class: 'lede' }, 'no models loaded'),
|
|
309
|
+
),
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── data ──────────────────────────────────────────────────────────────────
|
|
190
316
|
async function refreshHistory() {
|
|
191
317
|
try { state.sessions = await B.listSessions(state.backend); render(); }
|
|
192
318
|
catch (e) { console.warn('history fetch failed:', e.message); }
|
|
@@ -194,19 +320,33 @@ async function refreshHistory() {
|
|
|
194
320
|
|
|
195
321
|
async function runSearch() {
|
|
196
322
|
if (!state.searchQ.trim()) { state.searchHits = null; render(); return; }
|
|
197
|
-
try {
|
|
198
|
-
|
|
323
|
+
try {
|
|
324
|
+
state.searchHits = await B.searchHistory(state.backend, state.searchQ, 50);
|
|
325
|
+
render();
|
|
326
|
+
} catch (e) {
|
|
327
|
+
state.searchHits = { query: state.searchQ, results: [], error: e.message };
|
|
328
|
+
render();
|
|
329
|
+
}
|
|
199
330
|
}
|
|
200
331
|
|
|
201
332
|
async function loadSession(sid) {
|
|
202
|
-
state.selectedSid = sid;
|
|
333
|
+
state.selectedSid = sid;
|
|
334
|
+
state.events = [];
|
|
335
|
+
render();
|
|
203
336
|
try { state.events = await B.getSessionEvents(state.backend, sid); render(); }
|
|
204
|
-
catch (e) {
|
|
337
|
+
catch (e) {
|
|
338
|
+
state.events = [{ ts: Date.now(), role: 'error', type: 'fetch', text: e.message }];
|
|
339
|
+
render();
|
|
340
|
+
}
|
|
205
341
|
}
|
|
206
342
|
|
|
207
343
|
async function init() {
|
|
208
|
-
|
|
209
|
-
|
|
344
|
+
try {
|
|
345
|
+
const r = await B.probeBackend(state.backend);
|
|
346
|
+
state.health = r.ok ? { status: 'ok', ...r.info } : { status: 'down', ...r };
|
|
347
|
+
} catch (e) {
|
|
348
|
+
state.health = { status: 'error', error: e.message };
|
|
349
|
+
}
|
|
210
350
|
render();
|
|
211
351
|
try {
|
|
212
352
|
state.models = await B.listModels(state.backend);
|