agentgui 1.0.926 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.926",
3
+ "version": "1.0.927",
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,53 +22,96 @@ 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 = 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
 
44
48
  const crumb = Crumb({
45
49
  trail: ['agentgui'],
46
50
  leaf: state.tab,
47
- right: [statusChip],
51
+ right: [dot],
48
52
  });
49
53
 
50
- const side = state.tab === 'history' ? historySide() : null;
51
- const main = mainContent();
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 || 'no model'],
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') return chatMain();
62
- if (state.tab === 'history') return historyDetail();
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
- text: m.content || '',
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
- modelBar,
93
- Chat({ title: 'chat', sub: state.selectedModel || undefined, messages: msgs, composer }),
94
- ];
95
- }
96
-
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(); },
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
- 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
- );
139
+ ];
131
140
  }
132
141
 
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
- );
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 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
- }
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
- state.chat.messages.push({ role: 'user', content: text });
163
- state.chat.messages.push({ role: 'assistant', content: '' });
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') { cur.content += ev.text; render(); }
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
- function cancelChat() { state.chat.abort?.abort(); }
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 { state.searchHits = await B.searchHistory(state.backend, state.searchQ, 50); render(); }
198
- catch (e) { state.searchHits = { query: state.searchQ, results: [], error: e.message }; render(); }
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; state.events = []; render();
333
+ state.selectedSid = sid;
334
+ state.events = [];
335
+ render();
203
336
  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(); }
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
- state.health = await B.probeBackend(state.backend)
209
- .then(r => r.ok ? { status: 'ok', ...r.info } : { status: 'down', ...r });
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);