agentgui 1.0.926 → 1.0.928

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": "agentgui",
3
- "version": "1.0.926",
3
+ "version": "1.0.928",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -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 — live client</title>
7
- <meta name="description" content="agentgui live client talks to any acptoapi backend, anywhere.">
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>
@@ -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, Chip, Btn } = C;
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,51 +22,92 @@ const state = {
23
22
 
24
23
  let render;
25
24
 
26
- function topbarItems() {
27
- return [['chat', '#'], ['history', '#'], ['settings', '#']];
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 statusChip = ok
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 = h('span', { key: 'dot' }, ok ? '● connected' : '○ offline');
35
39
 
36
40
  const topbar = Topbar({
37
41
  brand: 'agentgui',
38
42
  leaf: state.tab,
39
- items: topbarItems(),
43
+ items: [['chat', '#'], ['history', '#'], ['settings', '#']],
40
44
  active: state.tab,
41
- onNav: (label) => { state.tab = label; if (label === 'history') refreshHistory(); render(); },
45
+ onNav: (label) => navTo(label),
42
46
  });
43
47
 
48
+ const crumbRight = state.tab === 'chat'
49
+ ? [
50
+ h('select', {
51
+ key: 'modelsel',
52
+ onchange: (e) => { state.selectedModel = e.target.value; render(); },
53
+ },
54
+ h('option', { key: '__', value: '' }, '— model —'),
55
+ ...state.models.map(m =>
56
+ h('option', { key: m.id, value: m.id, selected: m.id === state.selectedModel }, m.id)
57
+ ),
58
+ ),
59
+ state.chat.busy
60
+ ? h('a', { key: 'stop', onclick: cancelChat, style: 'cursor:pointer' }, '◼ stop')
61
+ : h('a', { key: 'new', onclick: newChat, style: 'cursor:pointer' }, '+ new'),
62
+ dot,
63
+ ]
64
+ : [dot];
65
+
44
66
  const crumb = Crumb({
45
67
  trail: ['agentgui'],
46
68
  leaf: state.tab,
47
- right: [statusChip],
69
+ right: crumbRight,
70
+ });
71
+
72
+ const navSide = Side({
73
+ sections: [
74
+ {
75
+ group: 'navigate',
76
+ items: [
77
+ { glyph: '▣', label: 'chat', key: 'chat', active: state.tab === 'chat',
78
+ onClick: (e) => { e.preventDefault(); navTo('chat'); } },
79
+ { glyph: '§', label: 'history', key: 'history', active: state.tab === 'history',
80
+ onClick: (e) => { e.preventDefault(); navTo('history'); } },
81
+ { glyph: '⌘', label: 'settings', key: 'settings', active: state.tab === 'settings',
82
+ onClick: (e) => { e.preventDefault(); navTo('settings'); } },
83
+ ],
84
+ },
85
+ ],
48
86
  });
87
+ const side = state.tab === 'history' ? historySide() : navSide;
49
88
 
50
- const side = state.tab === 'history' ? historySide() : null;
51
- const main = mainContent();
52
89
  const status = Status({
53
- left: [state.backend],
54
- right: [state.selectedModel || 'no model'],
90
+ left: [state.backend, ok ? '● live' : '○ offline'],
91
+ right: [state.selectedModel ? '⌘ ' + state.selectedModel : '○ no model'],
55
92
  });
56
93
 
57
- return AppShell({ topbar, crumb, side, main, status });
94
+ return AppShell({ topbar, crumb, side, main: mainContent(), status });
58
95
  }
59
96
 
60
97
  function mainContent() {
61
- if (state.tab === 'chat') return chatMain();
62
- if (state.tab === 'history') return historyDetail();
98
+ if (state.tab === 'chat') return chatMain();
99
+ if (state.tab === 'history') return historyMain();
63
100
  return settingsMain();
64
101
  }
65
102
 
103
+ // ── chat ───────────────────────────────────────────────────────────────────
66
104
  function chatMain() {
67
105
  const msgs = state.chat.messages.map((m, i) => ({
68
- key: i,
106
+ key: String(i),
69
107
  who: m.role === 'user' ? 'you' : 'them',
70
- text: m.content || '',
108
+ name: m.role === 'assistant' ? (state.selectedModel || 'agent') : 'you',
109
+ time: m.time || '',
110
+ parts: [{ kind: 'text', text: m.content || '' }],
71
111
  }));
72
112
 
73
113
  const composer = ChatComposer({
@@ -78,89 +118,30 @@ function chatMain() {
78
118
  onSend: (v) => { state.chat.draft = v; sendChat(); },
79
119
  });
80
120
 
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
121
  return [
92
- modelBar,
93
- Chat({ title: 'chat', sub: state.selectedModel || undefined, messages: msgs, composer }),
122
+ Chat({
123
+ title: state.selectedModel || 'agent',
124
+ sub: state.chat.busy ? 'streaming…' : (state.chat.messages.length + ' messages'),
125
+ messages: msgs,
126
+ composer,
127
+ }),
94
128
  ];
95
129
  }
96
130
 
97
- function historySide() {
98
- const rows = state.searchHits
99
- ? state.searchHits.results.slice(0, 30).map((r, i) =>
100
- Row({
101
- key: 'sr' + i,
102
- title: r.snippet || '(no snippet)',
103
- sub: r.project + ' · ' + r.role + (r.tool ? ' · ' + r.tool : ''),
104
- active: false,
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(); },
125
- }),
126
- ),
127
- h('div', { style: 'flex:1;overflow-y:auto;padding:8px' }, ...rows,
128
- rows.length === 0 ? h('div', { style: 'opacity:.6;padding:16px;font-size:13px' }, 'no sessions') : null,
129
- ),
130
- );
131
- }
132
-
133
- function historyDetail() {
134
- if (!state.selectedSid) return h('div', { style: 'padding:24px;opacity:.6' }, 'pick a session');
135
- if (state.events.length === 0) return h('div', { style: 'padding:24px;opacity:.6' }, 'loading…');
136
- return h('div', {},
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
- );
131
+ function newChat() {
132
+ state.chat.abort?.abort();
133
+ state.chat = { messages: [], busy: false, abort: null, draft: '' };
134
+ render();
146
135
  }
147
136
 
148
- function settingsMain() {
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
- }
137
+ function cancelChat() { state.chat.abort?.abort(); }
158
138
 
159
139
  async function sendChat() {
160
140
  const text = (state.chat.draft || '').trim();
161
141
  if (!text || !state.selectedModel || state.chat.busy) return;
162
- state.chat.messages.push({ role: 'user', content: text });
163
- state.chat.messages.push({ role: 'assistant', content: '' });
142
+ const t = timeNow();
143
+ state.chat.messages.push({ role: 'user', content: text, time: t });
144
+ state.chat.messages.push({ role: 'assistant', content: '', time: t });
164
145
  state.chat.draft = '';
165
146
  state.chat.busy = true;
166
147
  const ctrl = new AbortController();
@@ -173,11 +154,11 @@ async function sendChat() {
173
154
  messages: state.chat.messages.slice(0, -1).map(m => ({ role: m.role, content: m.content })),
174
155
  signal: ctrl.signal,
175
156
  })) {
176
- if (ev.type === 'text') { cur.content += ev.text; render(); }
157
+ if (ev.type === 'text') { cur.content += ev.text; render(); }
177
158
  if (ev.type === 'error') { cur.content += '\n[error] ' + JSON.stringify(ev.error); render(); }
178
159
  }
179
160
  } catch (e) {
180
- cur.content += '\n[error] ' + e.message;
161
+ if (e.name !== 'AbortError') cur.content += '\n[error] ' + e.message;
181
162
  } finally {
182
163
  state.chat.busy = false;
183
164
  state.chat.abort = null;
@@ -185,8 +166,142 @@ async function sendChat() {
185
166
  }
186
167
  }
187
168
 
188
- function cancelChat() { state.chat.abort?.abort(); }
169
+ // ── history ────────────────────────────────────────────────────────────────
170
+ function historyMain() {
171
+ const head = h('div', { class: 'ds-section' },
172
+ h('h1', {}, '§ history'),
173
+ h('p', { class: 'lede' },
174
+ state.selectedSid
175
+ ? 'session ' + state.selectedSid
176
+ : 'pick a session from the sidebar — events stream from acptoapi /v1/history.',
177
+ ),
178
+ );
179
+
180
+ if (!state.selectedSid) return [head];
181
+ if (state.events.length === 0) return [head, Panel({ title: 'events', children: h('p', { class: 'lede' }, '◌ loading…') })];
182
+
183
+ const rows = state.events.map((e, i) =>
184
+ Row({
185
+ key: 'ev' + i,
186
+ rank: String(i + 1).padStart(3, '0'),
187
+ title: (e.text || '').slice(0, 200) || '(empty)',
188
+ sub: new Date(e.ts).toLocaleString() + ' · ' + (e.role || '?') + ' · ' + (e.type || '?') + (e.tool ? ' · ⌘ ' + e.tool : ''),
189
+ rail: e.role === 'error' ? 'flame' : (e.role === 'user' ? 'green' : 'purple'),
190
+ })
191
+ );
192
+
193
+ return [
194
+ head,
195
+ Panel({
196
+ title: state.events.length + ' events',
197
+ children: h('div', { class: 'ds-section' }, ...rows),
198
+ }),
199
+ ];
200
+ }
201
+
202
+ function historySide() {
203
+ const searching = !!state.searchHits;
204
+ const rows = searching
205
+ ? state.searchHits.results.slice(0, 60).map((r, i) =>
206
+ Row({
207
+ key: 'sr' + i,
208
+ rank: String(i + 1).padStart(3, '0'),
209
+ title: r.snippet || '(no snippet)',
210
+ sub: (r.project || '?') + ' · ' + (r.role || '?') + (r.tool ? ' · ' + r.tool : ''),
211
+ rail: 'purple',
212
+ onClick: () => loadSession(r.sid),
213
+ })
214
+ )
215
+ : state.sessions.slice(0, 120).map((s, i) =>
216
+ Row({
217
+ key: 'sess' + i,
218
+ rank: String(i + 1).padStart(3, '0'),
219
+ title: s.title || s.project || s.sid,
220
+ sub: s.events + ' ev · ' + s.tools + ' tools' + (s.errors ? ' · ' + s.errors + ' err' : ''),
221
+ rail: s.errors ? 'flame' : 'green',
222
+ active: s.sid === state.selectedSid,
223
+ onClick: () => loadSession(s.sid),
224
+ })
225
+ );
226
+
227
+ return [
228
+ Side({
229
+ sections: [
230
+ {
231
+ group: 'navigate',
232
+ items: [
233
+ { glyph: '▣', label: 'chat', key: 'chat', onClick: (e) => { e.preventDefault(); navTo('chat'); } },
234
+ { glyph: '§', label: 'history', key: 'history', active: true },
235
+ { glyph: '⌘', label: 'settings', key: 'settings', onClick: (e) => { e.preventDefault(); navTo('settings'); } },
236
+ ],
237
+ },
238
+ ],
239
+ }),
240
+ Panel({
241
+ title: searching ? 'matches' : 'sessions',
242
+ children: h('div', { class: 'ds-section' },
243
+ h('input', {
244
+ type: 'search',
245
+ placeholder: 'search sessions…',
246
+ value: state.searchQ,
247
+ oninput: (e) => { state.searchQ = e.target.value; runSearch(); },
248
+ }),
249
+ rows.length ? h('div', {}, ...rows) : h('p', { class: 'lede' }, 'no sessions yet'),
250
+ ),
251
+ }),
252
+ ];
253
+ }
254
+
255
+ // ── settings ───────────────────────────────────────────────────────────────
256
+ function settingsMain() {
257
+ const ok = state.health.status === 'ok';
258
+ return [
259
+ h('div', { class: 'ds-section' },
260
+ h('h1', {}, '⌘ settings'),
261
+ h('p', { class: 'lede' }, 'point agentgui at any acptoapi backend. ?backend=… in the URL or the field below — both persist via localStorage.'),
262
+ Panel({
263
+ title: 'backend',
264
+ children: h('div', { class: 'ds-section' },
265
+ h('p', { class: 'lede' }, 'backend url'),
266
+ h('input', {
267
+ type: 'text',
268
+ value: state.backendDraft,
269
+ oninput: (e) => { state.backendDraft = e.target.value; render(); },
270
+ }),
271
+ h('p', { class: 'lede' }, (ok ? '● ' : '○ ') + JSON.stringify(state.health)),
272
+ h('button', {
273
+ onclick: () => {
274
+ B.setBackend(state.backendDraft);
275
+ state.backend = state.backendDraft;
276
+ state.health = { status: 'unknown' };
277
+ render();
278
+ init();
279
+ },
280
+ }, 'save + reconnect'),
281
+ ),
282
+ }),
283
+ Panel({
284
+ title: 'models',
285
+ children: h('div', { class: 'ds-section' },
286
+ state.models.length
287
+ ? h('div', {}, ...state.models.slice(0, 40).map((m, i) =>
288
+ Row({
289
+ key: 'm' + i,
290
+ rank: String(i + 1).padStart(3, '0'),
291
+ title: m.id,
292
+ sub: m.owned_by || m.object || 'model',
293
+ rail: m.id === state.selectedModel ? 'green' : 'purple',
294
+ onClick: () => { state.selectedModel = m.id; render(); },
295
+ })
296
+ ))
297
+ : h('p', { class: 'lede' }, 'no models loaded'),
298
+ ),
299
+ }),
300
+ ),
301
+ ];
302
+ }
189
303
 
304
+ // ── data ──────────────────────────────────────────────────────────────────
190
305
  async function refreshHistory() {
191
306
  try { state.sessions = await B.listSessions(state.backend); render(); }
192
307
  catch (e) { console.warn('history fetch failed:', e.message); }
@@ -194,19 +309,33 @@ async function refreshHistory() {
194
309
 
195
310
  async function runSearch() {
196
311
  if (!state.searchQ.trim()) { state.searchHits = null; render(); return; }
197
- try { state.searchHits = await B.searchHistory(state.backend, state.searchQ, 50); render(); }
198
- catch (e) { state.searchHits = { query: state.searchQ, results: [], error: e.message }; render(); }
312
+ try {
313
+ state.searchHits = await B.searchHistory(state.backend, state.searchQ, 50);
314
+ render();
315
+ } catch (e) {
316
+ state.searchHits = { query: state.searchQ, results: [], error: e.message };
317
+ render();
318
+ }
199
319
  }
200
320
 
201
321
  async function loadSession(sid) {
202
- state.selectedSid = sid; state.events = []; render();
322
+ state.selectedSid = sid;
323
+ state.events = [];
324
+ render();
203
325
  try { state.events = await B.getSessionEvents(state.backend, sid); render(); }
204
- catch (e) { state.events = [{ ts: Date.now(), role: 'error', type: 'fetch', text: e.message }]; render(); }
326
+ catch (e) {
327
+ state.events = [{ ts: Date.now(), role: 'error', type: 'fetch', text: e.message }];
328
+ render();
329
+ }
205
330
  }
206
331
 
207
332
  async function init() {
208
- state.health = await B.probeBackend(state.backend)
209
- .then(r => r.ok ? { status: 'ok', ...r.info } : { status: 'down', ...r });
333
+ try {
334
+ const r = await B.probeBackend(state.backend);
335
+ state.health = r.ok ? { status: 'ok', ...r.info } : { status: 'down', ...r };
336
+ } catch (e) {
337
+ state.health = { status: 'error', error: e.message };
338
+ }
210
339
  render();
211
340
  try {
212
341
  state.models = await B.listModels(state.backend);