agentgui 1.0.932 → 1.0.934
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 +8 -8
- package/README.md +6 -6
- package/lib/http-handler.js +26 -0
- package/lib/terminal.js +112 -0
- package/lib/ws-setup.js +7 -0
- package/package.json +1 -1
- package/site/app/index.html +3 -3
- package/site/app/js/app.js +34 -25
- package/site/app/js/backend.js +3 -1
package/AGENTS.md
CHANGED
|
@@ -25,7 +25,7 @@ Local server on PORT=3056 (default), `bun server.js`:
|
|
|
25
25
|
|
|
26
26
|
## Learning audit
|
|
27
27
|
|
|
28
|
-
- 2026-05-02 session: 5 items audited (CI bun, stream imports, windows fallback, GM blocker, ACP history), 0 removed (rs-learn retrieval not yet confirmed; safety default kept all), 1 new fact ingested (
|
|
28
|
+
- 2026-05-02 session: 5 items audited (CI bun, stream imports, windows fallback, GM blocker, ACP history), 0 removed (rs-learn retrieval not yet confirmed; safety default kept all), 1 new fact ingested (in-process ccsniff history integration)
|
|
29
29
|
|
|
30
30
|
## CI / GitHub Actions
|
|
31
31
|
|
|
@@ -74,11 +74,11 @@ The actual hook content is generated/templated from c:/dev/gm, likely from `lib/
|
|
|
74
74
|
|
|
75
75
|
Only after the real generator is patched will agentgui sessions run autonomously without per-tool ceremony.
|
|
76
76
|
|
|
77
|
-
##
|
|
77
|
+
## History Integration via ccsniff (2026-05-21 — reverted from acptoapi)
|
|
78
78
|
|
|
79
|
-
**
|
|
79
|
+
**agentgui mounts `ccsniff`'s history router in-process — no external proxy.**
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
`server.js` imports `createHistoryRouter` from the `ccsniff` package and mounts it on the internal Express app at `/`, exposing `GET /v1/history/{snapshot,sessions,sessions/:sid/events,search,reindex,stream}`. Reads `~/.claude/projects` by default; override with `CLAUDE_PROJECTS_DIR` env var. acptoapi's previously-bundled history routes were removed in acptoapi 1.0.103; ccsniff is now the canonical source. Browser client (`site/app/js/backend.js`) calls these same-origin via the agentgui server.
|
|
82
82
|
|
|
83
83
|
## buildSystemPrompt System Prompt for claude-code
|
|
84
84
|
|
|
@@ -124,7 +124,7 @@ Surfaced 2026-05-04 while validating the chat surface in `site/app/`. Fix applie
|
|
|
124
124
|
|
|
125
125
|
**`site/app/js/app.js` opens SSE EventSource on navTo('history'); first /v1/history/* request triggers 30-90s loadOnce() synchronous walk.**
|
|
126
126
|
|
|
127
|
-
The live history feature wires
|
|
127
|
+
The live history feature wires the in-process ccsniff `/v1/history/stream` SSE endpoint via `B.streamHistory(base, onEvent)` on tab entry and closes the EventSource on tab exit. State shape: `state.live = { es, connected, lastEventTs, error, eventCount }`.
|
|
128
128
|
|
|
129
129
|
Event dispatch loop:
|
|
130
130
|
- `hello` event: set `connected=true`
|
|
@@ -132,8 +132,8 @@ Event dispatch loop:
|
|
|
132
132
|
- `conversation` event: call `refreshHistory()` to reload session list
|
|
133
133
|
- `error` event: set `live.error`
|
|
134
134
|
|
|
135
|
-
Throttle renders via `requestAnimationFrame` to avoid event storm during burst loads
|
|
135
|
+
Throttle renders via `requestAnimationFrame` to avoid event storm during burst loads.
|
|
136
136
|
|
|
137
|
-
**First request to
|
|
137
|
+
**First request to `/v1/history/*` triggers loadOnce() that walks all JSONL files under ~/.claude/projects** (env default; override with `CLAUDE_PROJECTS_DIR`). In our test env: 299 files, 80MB, 69k events → 30-90s startup latency. Health check timeouts during this window are normal and expected. Subsequent requests are fast (cached index).
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
The endpoints are served by ccsniff's Express router mounted in-process from `server.js`. No external proxy.
|
package/README.md
CHANGED
|
@@ -10,22 +10,22 @@
|
|
|
10
10
|
|
|
11
11
|
Multi-agent GUI client for AI coding agents with real-time streaming, WebSocket sync, and SQLite persistence.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## How it works
|
|
14
14
|
|
|
15
|
-
AgentGUI
|
|
15
|
+
AgentGUI is a single Node server (`server.js`) plus a same-origin browser client (`site/app/`). The server speaks ACP directly to local agent daemons (Claude Code, OpenCode, Kilo, Gemini CLI, etc.) via `@agentclientprotocol/sdk` and hosts `ccsniff`'s `/v1/history/*` Express router in-process for Claude Code JSONL session browsing — no external proxy required.
|
|
16
16
|
|
|
17
17
|
- UI: [anentrypoint-design](https://www.npmjs.com/package/anentrypoint-design) (CDN, single-file ESM)
|
|
18
|
-
-
|
|
19
|
-
- Source: `site/app/` (
|
|
18
|
+
- Server: `server.js` — ACP daemon manager + WebSocket chat + ccsniff history mount
|
|
19
|
+
- Source: `site/app/` (browser) + `lib/` (server)
|
|
20
20
|
|
|
21
|
-
History endpoints
|
|
21
|
+
History endpoints served by [`ccsniff`](https://github.com/AnEntrypoint/ccsniff) directly:
|
|
22
22
|
|
|
23
23
|
- `GET /v1/history/sessions` — list Claude Code sessions on the host
|
|
24
24
|
- `GET /v1/history/sessions/:sid/events` — flattened events for one session
|
|
25
25
|
- `GET /v1/history/search?q=…` — BM25-ranked search across all events
|
|
26
26
|
- `GET /v1/history/stream` — Server-Sent Events for live tailing
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Chat streams over the `/sync` WebSocket using the `chat.sendMessage` method — see "WebSocket Chat Protocol" below.
|
|
29
29
|
|
|
30
30
|
### Supported Agents
|
|
31
31
|
|
package/lib/http-handler.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
|
+
import * as term from './terminal.js';
|
|
5
6
|
|
|
6
7
|
export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, getWss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap, routes, PORT }) {
|
|
7
8
|
return async function httpHandler(req, res) {
|
|
@@ -49,6 +50,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
49
50
|
else if (req.url.startsWith('/api/') || req.url.startsWith('/js/') || req.url.startsWith('/css/') ||
|
|
50
51
|
req.url.startsWith('/vendor/') || req.url.startsWith('/sync') || req.url === '/' ||
|
|
51
52
|
req.url === '/health' || req.url.startsWith('/v1/') ||
|
|
53
|
+
req.url.startsWith('/api/terminal/') ||
|
|
52
54
|
req.url.startsWith('/conversations/')) { routePath = req.url; }
|
|
53
55
|
else { res.writeHead(404); res.end('Not found'); return; }
|
|
54
56
|
|
|
@@ -66,6 +68,30 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
66
68
|
return;
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
// Terminal sessions — gated by the Basic-auth check at the top of this handler.
|
|
72
|
+
// Never expose these routes without PASSWORD set.
|
|
73
|
+
if (pathOnly === '/api/terminal/sessions' && req.method === 'GET') {
|
|
74
|
+
sendJSON(req, res, 200, term.listSessions()); return;
|
|
75
|
+
}
|
|
76
|
+
if (pathOnly === '/api/terminal/sessions' && req.method === 'POST') {
|
|
77
|
+
let body = ''; for await (const c of req) body += c;
|
|
78
|
+
let p = {}; try { p = body ? JSON.parse(body) : {}; } catch {}
|
|
79
|
+
const s = term.createSession({ shell: p.shell, cwd: p.cwd, cols: p.cols, rows: p.rows, env: p.env });
|
|
80
|
+
sendJSON(req, res, 200, { sid: s.sid, kind: s.kind, shell: s.shell, cwd: s.cwd, cols: s.cols, rows: s.rows, pid: s.proc.pid });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const termMatch = pathOnly.match(/^\/api\/terminal\/sessions\/([0-9a-f]+)$/);
|
|
84
|
+
if (termMatch && req.method === 'GET') {
|
|
85
|
+
const s = term.getSession(termMatch[1]);
|
|
86
|
+
if (!s) { sendJSON(req, res, 404, { error: 'session not found' }); return; }
|
|
87
|
+
sendJSON(req, res, 200, { sid: s.sid, kind: s.kind, shell: s.shell, cwd: s.cwd, cols: s.cols, rows: s.rows, pid: s.proc.pid, clients: s.clients.size });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (termMatch && req.method === 'DELETE') {
|
|
91
|
+
const ok = term.closeSession(termMatch[1]);
|
|
92
|
+
sendJSON(req, res, ok ? 200 : 404, { ok }); return;
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
// Legacy REST handlers removed. History served by ccsniff at /v1/history/*.
|
|
70
96
|
for (const key of Object.keys(routes)) {
|
|
71
97
|
try {
|
package/lib/terminal.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// WebSocket terminal sessions. Ported from acptoapi 2026-05-21.
|
|
2
|
+
// Auth: callers MUST gate /api/terminal/* + the WS upgrade behind agentgui's
|
|
3
|
+
// PASSWORD Basic-auth check. This module does no auth on its own — never
|
|
4
|
+
// expose without the gate.
|
|
5
|
+
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
const require_ = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
let _pty = null;
|
|
13
|
+
function getPty() {
|
|
14
|
+
if (_pty !== null) return _pty;
|
|
15
|
+
try { _pty = require_('node-pty'); } catch { _pty = false; }
|
|
16
|
+
return _pty;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sessions = new Map();
|
|
20
|
+
|
|
21
|
+
function newSid() { return crypto.randomBytes(8).toString('hex'); }
|
|
22
|
+
|
|
23
|
+
function defaultShell() {
|
|
24
|
+
if (os.platform() === 'win32') return process.env.COMSPEC || 'cmd.exe';
|
|
25
|
+
return process.env.SHELL || '/bin/bash';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createSession({ shell, cwd, cols = 80, rows = 24, env } = {}) {
|
|
29
|
+
const sid = newSid();
|
|
30
|
+
const pty = getPty();
|
|
31
|
+
const _shell = shell || defaultShell();
|
|
32
|
+
const _cwd = cwd || process.env.HOME || os.homedir();
|
|
33
|
+
const _env = { ...process.env, ...(env || {}), TERM: 'xterm-256color', COLORTERM: 'truecolor' };
|
|
34
|
+
let proc, kind;
|
|
35
|
+
if (pty) {
|
|
36
|
+
proc = pty.spawn(_shell, [], { name: 'xterm-256color', cols, rows, cwd: _cwd, env: _env });
|
|
37
|
+
kind = 'pty';
|
|
38
|
+
} else {
|
|
39
|
+
proc = spawn(_shell, os.platform() === 'win32' ? [] : ['-i'], { cwd: _cwd, env: _env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
40
|
+
kind = 'pipe';
|
|
41
|
+
}
|
|
42
|
+
const session = { sid, kind, proc, shell: _shell, cwd: _cwd, cols, rows, createdAt: Date.now(), clients: new Set(), exitCode: null };
|
|
43
|
+
sessions.set(sid, session);
|
|
44
|
+
const onData = (chunk) => {
|
|
45
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
|
|
46
|
+
for (const ws of session.clients) { if (ws.readyState === 1) try { ws.send(buf); } catch {} }
|
|
47
|
+
};
|
|
48
|
+
if (kind === 'pty') {
|
|
49
|
+
proc.on('data', onData);
|
|
50
|
+
proc.on('exit', (code) => { session.exitCode = code; for (const ws of session.clients) { try { ws.close(1000, 'exit:' + code); } catch {} } sessions.delete(sid); });
|
|
51
|
+
} else {
|
|
52
|
+
proc.stdout.on('data', onData);
|
|
53
|
+
proc.stderr.on('data', onData);
|
|
54
|
+
proc.stdout.on('error', () => {});
|
|
55
|
+
proc.stderr.on('error', () => {});
|
|
56
|
+
proc.stdin.on('error', () => {});
|
|
57
|
+
proc.on('exit', (code) => { session.exitCode = code; for (const ws of session.clients) { try { ws.close(1000, 'exit:' + code); } catch {} } sessions.delete(sid); });
|
|
58
|
+
proc.on('error', (err) => { session.exitCode = -1; for (const ws of session.clients) { try { ws.close(1011, 'error:' + err.message); } catch {} } sessions.delete(sid); });
|
|
59
|
+
}
|
|
60
|
+
return session;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getSession(sid) { return sessions.get(sid); }
|
|
64
|
+
|
|
65
|
+
export function listSessions() {
|
|
66
|
+
return [...sessions.values()].map(s => ({ sid: s.sid, kind: s.kind, shell: s.shell, cwd: s.cwd, cols: s.cols, rows: s.rows, createdAt: s.createdAt, clients: s.clients.size }));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function closeSession(sid) {
|
|
70
|
+
const s = sessions.get(sid);
|
|
71
|
+
if (!s) return false;
|
|
72
|
+
try { s.proc.kill(); } catch {}
|
|
73
|
+
sessions.delete(sid);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function writeToSession(sid, data) {
|
|
78
|
+
const s = sessions.get(sid);
|
|
79
|
+
if (!s) return false;
|
|
80
|
+
const buf = typeof data === 'string' ? data : Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
81
|
+
if (s.kind === 'pty') {
|
|
82
|
+
try { s.proc.write(buf); } catch { return false; }
|
|
83
|
+
} else {
|
|
84
|
+
if (!s.proc.stdin || !s.proc.stdin.writable) return false;
|
|
85
|
+
try { s.proc.stdin.write(buf); } catch { return false; }
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resizeSession(sid, cols, rows) {
|
|
91
|
+
const s = sessions.get(sid);
|
|
92
|
+
if (!s || s.kind !== 'pty' || typeof s.proc.resize !== 'function') return false;
|
|
93
|
+
s.cols = cols; s.rows = rows;
|
|
94
|
+
try { s.proc.resize(cols, rows); return true; } catch { return false; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function attachWs(sid, ws) {
|
|
98
|
+
const s = sessions.get(sid);
|
|
99
|
+
if (!s) { try { ws.close(4404, 'session-not-found'); } catch {} return false; }
|
|
100
|
+
s.clients.add(ws);
|
|
101
|
+
ws.on('close', () => { s.clients.delete(ws); });
|
|
102
|
+
ws.on('message', (msg, isBinary) => {
|
|
103
|
+
if (isBinary || Buffer.isBuffer(msg)) { writeToSession(sid, msg); return; }
|
|
104
|
+
const text = msg.toString();
|
|
105
|
+
if (text && text.length > 0 && text[0] === '{') {
|
|
106
|
+
try { const j = JSON.parse(text); if (j.type === 'resize' && j.cols && j.rows) { resizeSession(sid, j.cols, j.rows); return; } } catch {}
|
|
107
|
+
}
|
|
108
|
+
writeToSession(sid, text);
|
|
109
|
+
});
|
|
110
|
+
ws.on('error', () => { s.clients.delete(ws); });
|
|
111
|
+
return true;
|
|
112
|
+
}
|
package/lib/ws-setup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { WebSocketServer } from 'ws';
|
|
4
|
+
import * as term from './terminal.js';
|
|
4
5
|
// Legacy WS handlers removed; no-op shim kept for callsite compatibility.
|
|
5
6
|
const registerLegacyHandler = () => {};
|
|
6
7
|
|
|
@@ -22,6 +23,12 @@ export function createWsSetup(server, { BASE_URL, watch, staticDir, _assetCache,
|
|
|
22
23
|
if (wsRoute === '/hot-reload') {
|
|
23
24
|
hotReloadClients.push(ws);
|
|
24
25
|
ws.on('close', () => { const i = hotReloadClients.indexOf(ws); if (i > -1) hotReloadClients.splice(i, 1); });
|
|
26
|
+
} else if (wsRoute.startsWith('/api/terminal/sessions/')) {
|
|
27
|
+
// Terminal session WS — auth was already enforced by wss-level PASSWORD
|
|
28
|
+
// check at the top of this connection callback. attach to existing session.
|
|
29
|
+
const m = wsRoute.match(/^\/api\/terminal\/sessions\/([0-9a-f]+)$/);
|
|
30
|
+
if (!m) { ws.close(4400, 'bad-terminal-path'); return; }
|
|
31
|
+
term.attachWs(m[1], ws);
|
|
25
32
|
} else if (wsRoute === '/sync') {
|
|
26
33
|
syncClients.add(ws);
|
|
27
34
|
ws.isAlive = true;
|
package/package.json
CHANGED
package/site/app/index.html
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<title>agentgui</title>
|
|
7
|
-
<meta name="description" content="agentgui —
|
|
8
|
-
<link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@
|
|
7
|
+
<meta name="description" content="agentgui — multi-agent client with same-origin server, in-process ccsniff history, and ACP chat.">
|
|
8
|
+
<link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@0.0.127/dist/247420.css">
|
|
9
9
|
<script type="importmap">
|
|
10
|
-
{ "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@
|
|
10
|
+
{ "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@0.0.127/dist/247420.js" } }
|
|
11
11
|
</script>
|
|
12
12
|
<style>
|
|
13
13
|
html, body { margin: 0; height: 100%; }
|
package/site/app/js/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as B from './backend.js';
|
|
|
3
3
|
|
|
4
4
|
installStyles().catch(() => {});
|
|
5
5
|
|
|
6
|
-
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel, PageHeader, SearchInput, TextField, EventList } = C;
|
|
6
|
+
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, EventList } = C;
|
|
7
7
|
|
|
8
8
|
const state = {
|
|
9
9
|
backend: B.getBackend(),
|
|
@@ -114,18 +114,16 @@ function view() {
|
|
|
114
114
|
|
|
115
115
|
const crumbRight = state.tab === 'chat'
|
|
116
116
|
? [
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
),
|
|
125
|
-
),
|
|
117
|
+
Select({
|
|
118
|
+
key: 'modelsel',
|
|
119
|
+
value: state.selectedModel,
|
|
120
|
+
placeholder: '— model —',
|
|
121
|
+
options: state.models.map(m => ({ value: m.id, label: m.id })),
|
|
122
|
+
onChange: (v) => { state.selectedModel = v; render(); },
|
|
123
|
+
}),
|
|
126
124
|
state.chat.busy
|
|
127
|
-
?
|
|
128
|
-
:
|
|
125
|
+
? Btn({ key: 'stop', onClick: cancelChat, children: '◼ stop' })
|
|
126
|
+
: Btn({ key: 'new', onClick: newChat, children: '+ new' }),
|
|
129
127
|
dot,
|
|
130
128
|
]
|
|
131
129
|
: [dot];
|
|
@@ -169,13 +167,22 @@ function mainContent() {
|
|
|
169
167
|
|
|
170
168
|
// ── chat ───────────────────────────────────────────────────────────────────
|
|
171
169
|
function chatMain() {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
const lastIdx = state.chat.messages.length - 1;
|
|
171
|
+
const msgs = state.chat.messages.map((m, i) => {
|
|
172
|
+
const isAssistant = m.role === 'assistant';
|
|
173
|
+
const isStreaming = state.chat.busy && i === lastIdx && isAssistant;
|
|
174
|
+
const isEmptyStreaming = isStreaming && !m.content;
|
|
175
|
+
return {
|
|
176
|
+
key: String(i),
|
|
177
|
+
who: isAssistant ? 'them' : 'you',
|
|
178
|
+
name: isAssistant ? (state.selectedModel || 'agent') : 'you',
|
|
179
|
+
time: m.time || '',
|
|
180
|
+
typing: isEmptyStreaming,
|
|
181
|
+
parts: isEmptyStreaming
|
|
182
|
+
? undefined
|
|
183
|
+
: [{ kind: isAssistant ? 'md' : 'text', text: m.content || '' }],
|
|
184
|
+
};
|
|
185
|
+
});
|
|
179
186
|
|
|
180
187
|
const composer = ChatComposer({
|
|
181
188
|
value: state.chat.draft,
|
|
@@ -250,12 +257,11 @@ function historyMain() {
|
|
|
250
257
|
Panel({
|
|
251
258
|
title: state.events.length + ' events',
|
|
252
259
|
children: EventList({
|
|
253
|
-
|
|
260
|
+
items: state.events.map((e, i) => ({
|
|
254
261
|
key: 'ev' + i,
|
|
255
|
-
|
|
262
|
+
code: String(i + 1).padStart(3, '0'),
|
|
256
263
|
title: (e.text || '').slice(0, 200) || '(empty)',
|
|
257
264
|
sub: new Date(e.ts).toLocaleString() + ' · ' + (e.role || '?') + ' · ' + (e.type || '?') + (e.tool ? ' · ⌘ ' + e.tool : ''),
|
|
258
|
-
rail: e.role === 'error' ? 'flame' : (e.role === 'user' ? 'green' : 'purple'),
|
|
259
265
|
})),
|
|
260
266
|
}),
|
|
261
267
|
}),
|
|
@@ -334,16 +340,19 @@ function settingsMain() {
|
|
|
334
340
|
onInput: (v) => { state.backendDraft = v; render(); },
|
|
335
341
|
}),
|
|
336
342
|
h('p', { key: 'hp', class: 'lede' }, (ok ? '● ' : '○ ') + JSON.stringify(state.health)),
|
|
337
|
-
|
|
343
|
+
Btn({
|
|
338
344
|
key: 'savebtn',
|
|
339
|
-
|
|
345
|
+
primary: true,
|
|
346
|
+
onClick: (e) => {
|
|
347
|
+
e.preventDefault();
|
|
340
348
|
B.setBackend(state.backendDraft);
|
|
341
349
|
state.backend = state.backendDraft;
|
|
342
350
|
state.health = { status: 'unknown' };
|
|
343
351
|
render();
|
|
344
352
|
init();
|
|
345
353
|
},
|
|
346
|
-
|
|
354
|
+
children: 'save + reconnect',
|
|
355
|
+
}),
|
|
347
356
|
],
|
|
348
357
|
}),
|
|
349
358
|
Panel({
|
package/site/app/js/backend.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// agentgui backend client. Resolves base URL from ?backend= or localStorage or default
|
|
2
|
+
// (same-origin). Talks to the local agentgui server: ccsniff history routes, /health,
|
|
3
|
+
// and the /sync WebSocket for chat streaming.
|
|
2
4
|
const KEY = 'agentgui.backend';
|
|
3
5
|
const DEFAULT_BACKEND = '';
|
|
4
6
|
|