agentgui 1.0.942 → 1.0.943
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 +14 -4
- package/CHANGELOG.md +7 -0
- package/lib/agent-descriptors.js +1 -0
- package/lib/agent-discovery.js +1 -0
- package/lib/claude-runner-agents.js +27 -0
- package/lib/claude-runner-direct.js +9 -3
- package/lib/claude-runner.js +22 -1
- package/package.json +1 -1
- package/site/app/index.html +14 -7
- package/site/app/js/app.js +67 -53
package/AGENTS.md
CHANGED
|
@@ -16,13 +16,23 @@ Dependencies:
|
|
|
16
16
|
- `ccsniff` (>=1.1.0) — exports `createHistoryRouter({projectsDir})` mountable on Express; serves `/v1/history/{sessions,sessions/:sid/events,search,snapshot,reindex,stream}`. Reads `~/.claude/projects` (override via `CLAUDE_PROJECTS_DIR`).
|
|
17
17
|
- `anentrypoint-design` (>=0.0.119) — kit library, single-file ESM from unpkg
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Orchestration agents
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The four flagship agents the GUI drives are **Claude Code, OpenCode, Kilo, and Antigravity (`agy`)**; the agent picker (`site/app/js/app.js`, `PRIMARY_AGENTS`) sorts these first, then other-available, then npx-installable, then not-installed.
|
|
22
|
+
|
|
23
|
+
Two runner protocols exist. **Direct** (`lib/claude-runner-direct.js`): claude-code and agy — spawn the CLI per turn, parse stdout. **ACP** (`lib/acp-sdk-manager.js`): opencode/kilo/codex — an on-demand long-lived server on ports 18100/18101/18102, health-checked via `/provider`. The on-demand start + restart-backoff is correct even when an ACP agent lacks provider auth (it reports running-not-healthy rather than crashing).
|
|
24
|
+
|
|
25
|
+
`agy` (Antigravity) is a Gemini-backed Go CLI. Its invocation is `agy --print "<prompt>" --dangerously-skip-permissions [--continue]` — `--print` is a **value flag** (the prompt is its argument; a positional prompt exits 2). It emits **plain text** (not stream-json) and prints no session id, so its `parseOutput` wraps each line into an assistant-text event and resume is `--continue`-only. A live model response needs an authenticated Antigravity session; without it `agy` returns empty (the direct runner resolves gracefully, no hang). Detail in rs-learn (recall "agentgui agy Antigravity agent").
|
|
26
|
+
|
|
27
|
+
**The direct runner spawns with `shell:false` against a resolved binary path — never `shell:true`.** `shell:true` on Windows concatenates argv without escaping, so a chat prompt containing `&`/`|`/`>`/backticks executes as shell commands (arbitrary command execution). `lib/claude-runner.js` `resolveBinaryPath` resolves the command to an absolute `.exe`; `getSpawnOptions` only defaults `shell:true` when a caller passes no explicit `shell`. Detail in rs-learn (recall "agentgui SECURITY direct runner shell:false").
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
**Agent availability comes from `registry.isAvailable(id)`** (`lib/ws-handlers-util.js`, `agents.list`), which runs `where`/`which`. A binary installed outside the system PATH reads as "(not installed)"; the fix is an `npxPackage` on the registry entry so it falls back to bun/npx presence. Detail in rs-learn (recall "agentgui agent availability where npxPackage").
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
**Output and source carry no decorative glyphs.** The GUI uses ASCII words and CSS-drawn discs (`.status-dot-disc`) for status, never `●/◌/○/⌘/§/▶` etc. Convert any decorative glyph to its ASCII equivalent on sight.
|
|
32
|
+
|
|
33
|
+
## Browser Witness
|
|
34
|
+
|
|
35
|
+
`bun server.js`. Default `PORT=3000` (server.js); the SPA is served under `BASE_URL` (default `/gm/`), so the live app is **http://localhost:3000/gm/** — `/health` and `/` answer at root, the app is under `/gm/`. First request to `/gm/` or `/v1/history/*` triggers a 30-90s ccsniff JSONL walk (curl with a short timeout returns 000 during warmup). AppShell renders nav=[chat,history,settings], SSE `hello`, 0 console errors, backend resolves to `''` (same origin).
|
|
26
36
|
|
|
27
37
|
## CI / GitHub Actions
|
|
28
38
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [Unreleased] - Antigravity (agy) orchestration + glyph-to-ASCII sweep + spawn-injection fix
|
|
2
|
+
|
|
3
|
+
- lib/agent-discovery.js + lib/claude-runner-agents.js + lib/agent-descriptors.js: added `agy` (Antigravity) as a direct-protocol CLI agent — `agy --print "<prompt>" --dangerously-skip-permissions [--continue]`, plain-text parseOutput, no model picker. Completes the four flagship orchestration targets (Claude Code, OpenCode, Kilo, Antigravity). Why: the GUI could not drive Antigravity at all.
|
|
4
|
+
- lib/claude-runner.js + lib/claude-runner-direct.js: SECURITY — the direct runner now spawns a resolved binary with `shell:false`. Previously `shell:true` on Windows concatenated argv unescaped, so a prompt containing `&`/`|`/`>`/backticks executed as shell commands (verified injection). Why: arbitrary command execution from chat input.
|
|
5
|
+
- lib/claude-runner-agents.js: claude-code registry entry gained `npxPackage` so it reports available via npx fallback when `where claude` misses a non-PATH install. Why: the flagship agent showed "(not installed)" despite being present.
|
|
6
|
+
- site/app/js/app.js + site/app/index.html: removed every decorative glyph (status dots, cmd/section/box/arrow/check/cross marks) in favour of ASCII words and a CSS-drawn status disc; agent picker now sorts the four flagship targets first, then available, then npx-installable, then not-installed. Why: machine-shaped glyph tells and a flat 17-item list obscured the orchestration targets.
|
|
7
|
+
|
|
1
8
|
## [Unreleased] - site/app live client migrated to 247420 SDK components
|
|
2
9
|
|
|
3
10
|
- site/app/index.html: removed 35 lines of bespoke CSS overriding design tokens; now uses SDK panel/font tokens with only structural overrides (chat flex layout, history pane grid)
|
package/lib/agent-descriptors.js
CHANGED
|
@@ -2,6 +2,7 @@ const agentDescriptorCache = new Map();
|
|
|
2
2
|
|
|
3
3
|
const AGENT_DESCRIPTIONS = {
|
|
4
4
|
'claude-code': 'Claude Code is an AI coding agent that can read, write, and execute code with streaming output support. It provides comprehensive code editing, file management, and terminal execution capabilities.',
|
|
5
|
+
'agy': 'Antigravity (agy) is a Gemini-backed coding agent CLI. It runs prompts non-interactively with plain-text streaming output, workspace directory scoping, and conversation resume.',
|
|
5
6
|
'gemini': 'Gemini CLI is Google AI coding agent with streaming support, code execution, and file management capabilities.',
|
|
6
7
|
'opencode': 'OpenCode is a multi-provider AI coding agent with streaming support and comprehensive code manipulation capabilities.',
|
|
7
8
|
};
|
package/lib/agent-discovery.js
CHANGED
|
@@ -6,6 +6,7 @@ import { execSync } from 'child_process';
|
|
|
6
6
|
|
|
7
7
|
const BINARIES = [
|
|
8
8
|
{ cmd: 'claude', id: 'claude-code', name: 'Claude Code', icon: 'C', protocol: 'cli' },
|
|
9
|
+
{ cmd: 'agy', id: 'agy', name: 'Antigravity', icon: 'Y', protocol: 'cli' },
|
|
9
10
|
{ cmd: 'opencode', id: 'opencode', name: 'OpenCode', icon: 'O', protocol: 'acp', npxPackage: 'opencode-ai' },
|
|
10
11
|
{ cmd: 'gemini', id: 'gemini', name: 'Gemini CLI', icon: 'G', protocol: 'acp', npxPackage: '@google/gemini-cli' },
|
|
11
12
|
{ cmd: 'kilo', id: 'kilo', name: 'Kilo Code', icon: 'K', protocol: 'acp', npxPackage: '@kilocode/cli' },
|
|
@@ -97,6 +97,10 @@ function parseClaudeOutput(line) {
|
|
|
97
97
|
registry.register({
|
|
98
98
|
id: 'claude-code', name: 'Claude Code', command: 'claude', protocol: 'direct', supportsStdin: false, closeStdin: true,
|
|
99
99
|
useJsonRpcStdin: false, supportedFeatures: ['streaming', 'resume', 'system-prompt', 'permissions-skip', 'steering'],
|
|
100
|
+
// claude has an npx fallback (matches agent-discovery), so when `where claude`
|
|
101
|
+
// misses a non-PATH install the agent still reports as runnable rather than
|
|
102
|
+
// "(not installed)".
|
|
103
|
+
npxPackage: '@anthropic-ai/claude-code',
|
|
100
104
|
spawnEnv: { MAX_THINKING_TOKENS: '0', AGENTGUI_SUBPROCESS: '1' },
|
|
101
105
|
buildArgs(prompt, config) {
|
|
102
106
|
const { verbose = true, outputFormat = 'stream-json', print = true, resumeSessionId = null, systemPrompt = null, model = null } = config;
|
|
@@ -114,6 +118,29 @@ registry.register({
|
|
|
114
118
|
parseOutput: parseClaudeOutput
|
|
115
119
|
});
|
|
116
120
|
|
|
121
|
+
// Antigravity (`agy`): a direct-protocol CLI like claude-code, but emits PLAIN
|
|
122
|
+
// TEXT (no stream-json, no model flag, no printed session_id). Each stdout line
|
|
123
|
+
// is wrapped into the assistant-text event the non-JSONL stream handler expects.
|
|
124
|
+
// Resume uses --continue (most-recent conversation) since agy prints no session id.
|
|
125
|
+
registry.register({
|
|
126
|
+
id: 'agy', name: 'Antigravity', command: 'agy', protocol: 'direct',
|
|
127
|
+
supportsStdin: false, closeStdin: true,
|
|
128
|
+
supportedFeatures: ['streaming', 'resume'],
|
|
129
|
+
spawnEnv: { AGENTGUI_SUBPROCESS: '1' },
|
|
130
|
+
buildArgs(prompt, config) {
|
|
131
|
+
const { resumeSessionId = null } = config;
|
|
132
|
+
// agy's --print is a VALUE flag (the prompt is its argument), not a boolean
|
|
133
|
+
// followed by a positional. A positional prompt exits with code 2.
|
|
134
|
+
const flags = ['--print', typeof prompt === 'string' ? prompt : String(prompt), '--dangerously-skip-permissions'];
|
|
135
|
+
if (resumeSessionId) flags.push('--continue');
|
|
136
|
+
return flags;
|
|
137
|
+
},
|
|
138
|
+
parseOutput(line) {
|
|
139
|
+
const text = line.replace(/\r$/, '');
|
|
140
|
+
if (!text.trim()) return null;
|
|
141
|
+
return { type: 'assistant', message: { content: [{ type: 'text', text }] } };
|
|
142
|
+
},
|
|
143
|
+
});
|
|
117
144
|
registry.register({ id: 'opencode', name: 'OpenCode', command: 'opencode', protocol: 'acp', supportsStdin: false, npxPackage: 'opencode-ai', supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
118
145
|
registry.register({ id: 'gemini', name: 'Gemini CLI', command: 'gemini', protocol: 'acp', supportsStdin: false, npxPackage: '@google/gemini-cli', supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs(prompt, config) { const args = ['--experimental-acp', '--yolo']; if (config?.model) args.push('--model', config.model); return args; }, protocolHandler: acpProtocolHandler });
|
|
119
146
|
registry.register({ id: 'goose', name: 'Goose', command: 'goose', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import { AgentRunner, getSpawnOptions } from './claude-runner.js';
|
|
2
|
+
import { AgentRunner, getSpawnOptions, resolveBinaryPath } from './claude-runner.js';
|
|
3
3
|
|
|
4
4
|
AgentRunner.prototype.runDirect = function(prompt, cwd, config = {}) {
|
|
5
5
|
return new Promise((resolve, reject) => {
|
|
6
6
|
const { timeout = 300000, onEvent = null, onError = null, onRateLimit = null } = config;
|
|
7
7
|
const args = this.buildArgs(prompt, config);
|
|
8
|
-
|
|
8
|
+
// The direct runner passes the untrusted user prompt as an argv element.
|
|
9
|
+
// Spawn with shell:false against a resolved binary so the prompt cannot be
|
|
10
|
+
// interpreted by a shell (shell:true would let "& cmd" inject). If the
|
|
11
|
+
// binary resolves to a .cmd/.bat shim, resolveBinaryPath returns the name
|
|
12
|
+
// unchanged and we fall back to the shell (npx-style fallback launchers).
|
|
13
|
+
const binary = resolveBinaryPath(this.command);
|
|
14
|
+
const spawnOpts = getSpawnOptions(cwd, { shell: false });
|
|
9
15
|
if (Object.keys(this.spawnEnv).length > 0) spawnOpts.env = { ...spawnOpts.env, ...this.spawnEnv };
|
|
10
16
|
if (this.closeStdin) spawnOpts.stdio = ['ignore', 'pipe', 'pipe'];
|
|
11
|
-
const proc = spawn(
|
|
17
|
+
const proc = spawn(binary, args, spawnOpts);
|
|
12
18
|
console.log(`[${this.id}] Spawned PID ${proc.pid} closeStdin=${this.closeStdin}`);
|
|
13
19
|
|
|
14
20
|
if (config.onPid) { try { config.onPid(proc.pid); } catch (e) { console.error(`[${this.id}] onPid callback failed:`, e.message); } }
|
package/lib/claude-runner.js
CHANGED
|
@@ -4,12 +4,33 @@ const isWindows = process.platform === 'win32';
|
|
|
4
4
|
|
|
5
5
|
export function getSpawnOptions(cwd, additionalOptions = {}) {
|
|
6
6
|
const options = { cwd, ...additionalOptions };
|
|
7
|
-
|
|
7
|
+
// shell:true on Windows concatenates argv WITHOUT escaping, so a user prompt
|
|
8
|
+
// containing &, |, >, or backticks executes as a shell command (verified
|
|
9
|
+
// injection). Callers that pass untrusted args (the direct runner) must set
|
|
10
|
+
// shell:false and spawn a resolved binary. Default keeps the legacy shell
|
|
11
|
+
// behaviour only when a caller has not already chosen.
|
|
12
|
+
if (isWindows && options.shell === undefined) options.shell = true;
|
|
8
13
|
if (!options.env) options.env = { ...process.env };
|
|
9
14
|
delete options.env.CLAUDECODE;
|
|
10
15
|
return options;
|
|
11
16
|
}
|
|
12
17
|
|
|
18
|
+
// Resolve a bare command name to an absolute executable path so it can be
|
|
19
|
+
// spawned with shell:false (no argv concatenation, no injection surface).
|
|
20
|
+
// Returns the original command if resolution fails (Node will still PATH-resolve).
|
|
21
|
+
export function resolveBinaryPath(command) {
|
|
22
|
+
try {
|
|
23
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
24
|
+
const r = spawnSync(whichCmd, [command], { encoding: 'utf-8', timeout: 3000 });
|
|
25
|
+
if (r.status === 0) {
|
|
26
|
+
const first = (r.stdout || '').trim().split(/\r?\n/)[0].trim();
|
|
27
|
+
// A .cmd/.bat shim cannot be exec'd without a shell; leave it to the caller.
|
|
28
|
+
if (first && !/\.(cmd|bat)$/i.test(first)) return first;
|
|
29
|
+
}
|
|
30
|
+
} catch (_) {}
|
|
31
|
+
return command;
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
export function resolveCommand(command, npxPackage) {
|
|
14
35
|
const whichCmd = isWindows ? 'where' : 'which';
|
|
15
36
|
const check = spawnSync(whichCmd, [command], { encoding: 'utf-8', timeout: 3000 });
|
package/package.json
CHANGED
package/site/app/index.html
CHANGED
|
@@ -63,21 +63,28 @@
|
|
|
63
63
|
}
|
|
64
64
|
.skip-link:focus { left: 8px; top: 8px; outline: 2px solid #fff; outline-offset: 2px; }
|
|
65
65
|
|
|
66
|
-
/* status connection dot
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
/* status connection dot — a CSS-drawn disc (real UI affordance, not a text
|
|
67
|
+
glyph). The .status-dot-disc child is always present; the parent's state
|
|
68
|
+
class colours it, and only is-live pulses. */
|
|
69
|
+
.status-dot { display: inline-flex; align-items: center; gap: .4em; white-space: nowrap; }
|
|
70
|
+
.status-dot-disc {
|
|
71
|
+
flex: none; width: 8px; height: 8px; border-radius: 50%;
|
|
72
|
+
background: var(--muted, #8a8f98);
|
|
73
|
+
}
|
|
74
|
+
.status-dot.is-live .status-dot-disc,
|
|
75
|
+
.status-dot-live .status-dot-disc {
|
|
76
|
+
background: var(--accent, var(--agentgui-accent, #50c878));
|
|
72
77
|
animation: agentgui-pulse 2s infinite;
|
|
73
78
|
}
|
|
79
|
+
.status-dot.is-connecting .status-dot-disc { background: #d9a441; }
|
|
80
|
+
.status-dot.is-offline .status-dot-disc { background: #c0504d; animation: none; }
|
|
74
81
|
@keyframes agentgui-pulse {
|
|
75
82
|
0% { box-shadow: 0 0 0 0 rgba(80, 200, 120, .5); }
|
|
76
83
|
70% { box-shadow: 0 0 0 6px rgba(80, 200, 120, 0); }
|
|
77
84
|
100% { box-shadow: 0 0 0 0 rgba(80, 200, 120, 0); }
|
|
78
85
|
}
|
|
79
86
|
@media (prefers-reduced-motion: reduce) {
|
|
80
|
-
.status-dot-
|
|
87
|
+
.status-dot-disc { animation: none !important; }
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
/* resume banner */
|
package/site/app/js/app.js
CHANGED
|
@@ -263,21 +263,20 @@ function closeLiveStream() {
|
|
|
263
263
|
function view() {
|
|
264
264
|
const ok = state.health.status === 'ok';
|
|
265
265
|
const liveActive = state.tab === 'history' && state.live.connected && (Date.now() - state.live.lastEventTs < 30000);
|
|
266
|
-
const
|
|
266
|
+
const dotLabel = state.tab === 'history'
|
|
267
267
|
? (state.live.error
|
|
268
|
-
?
|
|
269
|
-
: (liveActive ? '
|
|
270
|
-
: (ok ? (state.health.ws === 'reconnecting' ? '
|
|
268
|
+
? state.live.error + (state.live.reconnects ? ' · ' + state.live.reconnects + ' reconnects' : '')
|
|
269
|
+
: (liveActive ? 'live · ' + state.live.eventCount : (state.live.connected ? 'live' : 'connecting…')))
|
|
270
|
+
: (ok ? (state.health.ws === 'reconnecting' ? 'ws reconnecting' : 'connected') : 'offline');
|
|
271
271
|
const dotLive = state.tab === 'history' ? (liveActive || state.live.connected) : ok;
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
(!dotLive && dotGlyph) ? h('span', { key: 'dg', 'aria-hidden': 'true' }, dotGlyph + ' ') : null,
|
|
272
|
+
// The status dot is drawn entirely by CSS (.status-dot::before) — a small
|
|
273
|
+
// colored disc, real product design, not a text glyph. State drives its colour
|
|
274
|
+
// via the modifier class; the label carries only words so AT reads "live", and
|
|
275
|
+
// there are no literal status-glyph characters in the DOM.
|
|
276
|
+
const dotState = state.live.error || (!dotLive) ? 'is-offline'
|
|
277
|
+
: (dotLive && state.tab !== 'history' && state.health.ws === 'reconnecting' ? 'is-connecting' : 'is-live');
|
|
278
|
+
const dot = h('span', { key: 'dot', class: 'status-dot ' + dotState + (dotLive ? ' status-dot-live' : ''), role: 'status', 'aria-live': 'polite' },
|
|
279
|
+
h('span', { key: 'dd', class: 'status-dot-disc', 'aria-hidden': 'true' }),
|
|
281
280
|
h('span', { key: 'dl' }, dotLabel));
|
|
282
281
|
|
|
283
282
|
const topbar = Topbar({
|
|
@@ -288,11 +287,27 @@ function view() {
|
|
|
288
287
|
onNav: (label) => navTo(label),
|
|
289
288
|
});
|
|
290
289
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
290
|
+
// The four flagship orchestration targets surface first, then the rest of the
|
|
291
|
+
// available agents, then unavailable ones — so the agents the GUI exists to
|
|
292
|
+
// drive are reachable without scanning a flat 17-item list. Ordering only;
|
|
293
|
+
// the DS Select renders a plain option list.
|
|
294
|
+
const PRIMARY_AGENTS = ['claude-code', 'opencode', 'kilo', 'agy'];
|
|
295
|
+
const agentRank = (a) => {
|
|
296
|
+
const primary = PRIMARY_AGENTS.indexOf(a.id);
|
|
297
|
+
const avail = a.available !== false;
|
|
298
|
+
if (primary !== -1 && avail) return primary; // 0..3 flagship + available
|
|
299
|
+
if (avail) return 10; // other available
|
|
300
|
+
if (a.npxInstallable) return 20; // installable via npx
|
|
301
|
+
return 30; // not installed
|
|
302
|
+
};
|
|
303
|
+
const agentOptions = state.agents
|
|
304
|
+
.map(a => ({ a, rank: agentRank(a) }))
|
|
305
|
+
.sort((x, y) => x.rank - y.rank || x.a.name.localeCompare(y.a.name))
|
|
306
|
+
.map(({ a }) => ({
|
|
307
|
+
value: a.id,
|
|
308
|
+
label: a.name + (a.available === false ? (a.npxInstallable ? ' (via npx)' : ' (not installed)') : ''),
|
|
309
|
+
disabled: a.available === false && !a.npxInstallable,
|
|
310
|
+
}));
|
|
296
311
|
const showModelPicker = state.tab === 'chat' && state.agentModels.length > 0;
|
|
297
312
|
|
|
298
313
|
const crumbRight = state.tab === 'chat'
|
|
@@ -316,7 +331,7 @@ function view() {
|
|
|
316
331
|
})
|
|
317
332
|
: null,
|
|
318
333
|
state.chat.busy
|
|
319
|
-
? Btn({ key: 'stop', onClick: cancelChat, children: '
|
|
334
|
+
? Btn({ key: 'stop', onClick: cancelChat, children: 'stop', title: 'Stop streaming' })
|
|
320
335
|
: Btn({ key: 'new', onClick: newChat, children: '+ new', title: 'Start new chat (clears history)' }),
|
|
321
336
|
dot,
|
|
322
337
|
)]
|
|
@@ -335,10 +350,10 @@ function view() {
|
|
|
335
350
|
const side = state.tab === 'history' ? historySide() : null;
|
|
336
351
|
|
|
337
352
|
const agentLabel = state.selectedAgent
|
|
338
|
-
? '
|
|
339
|
-
: '
|
|
353
|
+
? 'agent: ' + (agentById(state.selectedAgent)?.name || state.selectedAgent) + (state.selectedModel ? ' · ' + state.selectedModel : '')
|
|
354
|
+
: 'no agent';
|
|
340
355
|
const status = Status({
|
|
341
|
-
left: [state.backend || 'same-origin', ok ? '
|
|
356
|
+
left: [state.backend || 'same-origin', ok ? 'live' : 'offline'],
|
|
342
357
|
right: [agentLabel],
|
|
343
358
|
});
|
|
344
359
|
|
|
@@ -374,14 +389,14 @@ function toolSummary(block) {
|
|
|
374
389
|
arg = inp.command || inp.file_path || inp.path || inp.pattern || inp.query || inp.url || '';
|
|
375
390
|
if (!arg) { try { arg = JSON.stringify(inp).slice(0, 120); } catch {} }
|
|
376
391
|
}
|
|
377
|
-
return '
|
|
392
|
+
return 'tool: ' + name + (arg ? ' · ' + String(arg).slice(0, 120) : '');
|
|
378
393
|
}
|
|
379
394
|
|
|
380
395
|
function toolResultSummary(block) {
|
|
381
396
|
const c = block?.content ?? block?.output ?? block;
|
|
382
397
|
let s = typeof c === 'string' ? c : (() => { try { return JSON.stringify(c); } catch { return String(c); } })();
|
|
383
398
|
s = s.replace(/\s+/g, ' ').trim();
|
|
384
|
-
return (block?.is_error ? '
|
|
399
|
+
return (block?.is_error ? '[error] ' : '') + s.slice(0, 160);
|
|
385
400
|
}
|
|
386
401
|
|
|
387
402
|
function errText(e) {
|
|
@@ -426,8 +441,8 @@ function chatMain() {
|
|
|
426
441
|
const banners = [];
|
|
427
442
|
if (state.chat.resumeSid) {
|
|
428
443
|
banners.push(h('div', { key: 'rb', class: 'resume-banner', role: 'status' },
|
|
429
|
-
h('span', { key: 'rbtxt', class: 'lede' }, '
|
|
430
|
-
Btn({ key: 'rclr', onClick: () => { state.chat.resumeSid = null; state.chat.resumeNote = null; render(); }, children: '
|
|
444
|
+
h('span', { key: 'rbtxt', class: 'lede' }, 'resuming session ' + state.chat.resumeSid.slice(0, 8) + '… via --resume'),
|
|
445
|
+
Btn({ key: 'rclr', onClick: () => { state.chat.resumeSid = null; state.chat.resumeNote = null; render(); }, children: 'clear' })));
|
|
431
446
|
if (state.chat.resumeNote) {
|
|
432
447
|
banners.push(Alert({ key: 'rnote', kind: 'info', title: 'Agent switched', children: state.chat.resumeNote }));
|
|
433
448
|
}
|
|
@@ -488,9 +503,9 @@ function cwdBanner() {
|
|
|
488
503
|
}
|
|
489
504
|
return h('div', { key: 'cwdb', class: 'cwd-bar', role: 'group', 'aria-label': 'Working directory' },
|
|
490
505
|
h('span', { key: 'cwdtxt', class: 'lede cwd-bar-text', title: state.chatCwd || 'server default working directory' },
|
|
491
|
-
state.chatCwd ? '
|
|
506
|
+
state.chatCwd ? 'cwd: ' + truncate(state.chatCwd, 28, 60) : 'cwd: server default'),
|
|
492
507
|
h('button', { key: 'cwdset', type: 'button', class: 'cwd-bar-btn', onClick: () => { state.cwdEditing = true; state.cwdDraft = state.chatCwd || ''; render(); } }, state.chatCwd ? 'change' : 'set'),
|
|
493
|
-
state.chatCwd ? h('button', { key: 'cwdclr', type: 'button', class: 'cwd-bar-btn', onClick: () => { state.chatCwd = ''; lsRemove('agentgui.cwd'); render(); } }, '
|
|
508
|
+
state.chatCwd ? h('button', { key: 'cwdclr', type: 'button', class: 'cwd-bar-btn', onClick: () => { state.chatCwd = ''; lsRemove('agentgui.cwd'); render(); } }, 'use default') : null);
|
|
494
509
|
}
|
|
495
510
|
|
|
496
511
|
function newChat() {
|
|
@@ -553,7 +568,7 @@ async function sendChat() {
|
|
|
553
568
|
})) {
|
|
554
569
|
if (ev.type === 'text') { cur.content += ev.text; render(); scrollChatToBottom(); }
|
|
555
570
|
else if (ev.type === 'tool') { cur.parts.push(toolSummary(ev.block)); render(); scrollChatToBottom(); }
|
|
556
|
-
else if (ev.type === 'tool_result') { cur.parts.push('
|
|
571
|
+
else if (ev.type === 'tool_result') { cur.parts.push('-> ' + toolResultSummary(ev.block)); render(); scrollChatToBottom(); }
|
|
557
572
|
else if (ev.type === 'result') { /* terminal usage/summary block — already reflected via text */ }
|
|
558
573
|
else if (ev.type === 'error') { cur.error = errText(ev.error); render(); }
|
|
559
574
|
}
|
|
@@ -585,11 +600,10 @@ function historyMain() {
|
|
|
585
600
|
return [
|
|
586
601
|
reconnectAlert(),
|
|
587
602
|
PageHeader({
|
|
588
|
-
title: '
|
|
603
|
+
title: 'history',
|
|
589
604
|
lede: 'pick a session from the sidebar — events stream live from ccsniff /v1/history.',
|
|
590
605
|
}),
|
|
591
606
|
h('div', { key: 'histempty', class: 'history-empty', role: 'status' },
|
|
592
|
-
h('div', { key: 'ge', class: 'history-empty-glyph', 'aria-hidden': 'true' }, '§'),
|
|
593
607
|
h('p', { key: 'gt', class: 'history-empty-title' },
|
|
594
608
|
count ? 'Select a session to view its events' : 'No sessions yet'),
|
|
595
609
|
h('p', { key: 'gs', class: 'lede history-empty-sub' },
|
|
@@ -605,13 +619,13 @@ function historyMain() {
|
|
|
605
619
|
: state.selectedSid;
|
|
606
620
|
|
|
607
621
|
const head = PageHeader({
|
|
608
|
-
title:
|
|
622
|
+
title: truncate(projectLabel(sess?.title) || projectLabel(sess?.project) || state.selectedSid, 40, 80),
|
|
609
623
|
lede,
|
|
610
624
|
});
|
|
611
625
|
|
|
612
626
|
const actions = h('div', { key: 'acts', class: 'history-actions' },
|
|
613
|
-
Btn({ key: 'resume', primary: true, onClick: () => resumeInChat(sess || { sid: state.selectedSid }), children: '
|
|
614
|
-
Btn({ key: 'copy', onClick: copySid, children: copyToast || '
|
|
627
|
+
Btn({ key: 'resume', primary: true, onClick: () => resumeInChat(sess || { sid: state.selectedSid }), children: 'open in chat' }),
|
|
628
|
+
Btn({ key: 'copy', onClick: copySid, children: copyToast || 'copy sid' }),
|
|
615
629
|
);
|
|
616
630
|
|
|
617
631
|
if (state.events.length === 0) {
|
|
@@ -641,8 +655,8 @@ function historyMain() {
|
|
|
641
655
|
const key = e.i != null ? 'ev' + e.i : 'ev-' + (e.ts || 0) + '-' + (total - shown.length + i);
|
|
642
656
|
const role = e.role || '?';
|
|
643
657
|
const type = e.type || '?';
|
|
644
|
-
const tool = e.tool ? ' ·
|
|
645
|
-
const errMark = e.isError ? ' ·
|
|
658
|
+
const tool = e.tool ? ' · tool: ' + e.tool : '';
|
|
659
|
+
const errMark = e.isError ? ' · error' : '';
|
|
646
660
|
const raw = e.text || '';
|
|
647
661
|
const text = raw.replace(/\s+/g, ' ').trim();
|
|
648
662
|
const expanded = state.expandedEvents.has(key);
|
|
@@ -664,9 +678,9 @@ let copyToast = null;
|
|
|
664
678
|
function copySid() {
|
|
665
679
|
const sid = state.selectedSid;
|
|
666
680
|
if (!sid) return;
|
|
667
|
-
const done = () => { copyToast = '
|
|
681
|
+
const done = () => { copyToast = 'copied'; render(); setTimeout(() => { copyToast = null; render(); }, 1500); };
|
|
668
682
|
if (navigator.clipboard?.writeText) {
|
|
669
|
-
navigator.clipboard.writeText(sid).then(done).catch(() => { copyToast = '
|
|
683
|
+
navigator.clipboard.writeText(sid).then(done).catch(() => { copyToast = 'copy failed'; render(); setTimeout(() => { copyToast = null; render(); }, 1500); });
|
|
670
684
|
} else {
|
|
671
685
|
// Fallback for insecure (http) origins where navigator.clipboard is absent.
|
|
672
686
|
try {
|
|
@@ -674,7 +688,7 @@ function copySid() {
|
|
|
674
688
|
ta.value = sid; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
|
675
689
|
document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();
|
|
676
690
|
done();
|
|
677
|
-
} catch { copyToast = '
|
|
691
|
+
} catch { copyToast = 'copy failed'; render(); setTimeout(() => { copyToast = null; render(); }, 1500); }
|
|
678
692
|
}
|
|
679
693
|
}
|
|
680
694
|
|
|
@@ -746,7 +760,7 @@ function historySide() {
|
|
|
746
760
|
Row({
|
|
747
761
|
key: 'sess' + s.sid,
|
|
748
762
|
rank: String(i + 1).padStart(3, '0'),
|
|
749
|
-
title: (s.isSubagent ? '
|
|
763
|
+
title: (s.isSubagent ? '- ' : '') + (projectLabel(s.title) || projectLabel(s.project) || s.sid),
|
|
750
764
|
sub: fmtRelTime(s.last) + ' · ' + (s.events || 0) + ' ev · ' + (s.tools || 0) + ' tools' + (s.errors ? ' · ' + s.errors + ' err' : ''),
|
|
751
765
|
rail: s.errors ? 'flame' : (s.isSubagent ? 'purple' : 'green'),
|
|
752
766
|
active: s.sid === state.selectedSid,
|
|
@@ -761,13 +775,14 @@ function historySide() {
|
|
|
761
775
|
running.length
|
|
762
776
|
? Panel({
|
|
763
777
|
key: 'runningPanel',
|
|
764
|
-
title: '
|
|
778
|
+
title: 'running · ' + running.length,
|
|
765
779
|
children: running.map((r, i) => {
|
|
766
780
|
const agentName = agentById(r.agentId)?.name || r.agentId || 'agent';
|
|
767
781
|
const elapsed = r.startedAt ? Math.round((Date.now() - r.startedAt) / 1000) : 0;
|
|
768
782
|
return h('div', { key: 'run' + r.sessionId, class: 'resume-banner', role: 'group' },
|
|
769
|
-
h('span', {
|
|
770
|
-
|
|
783
|
+
h('span', { key: 'rd', class: 'status-dot-disc status-dot-live', 'aria-hidden': 'true' }),
|
|
784
|
+
h('span', { class: 'lede' }, agentName + (r.model ? ' · ' + r.model : '') + ' · ' + elapsed + 's' + (r.cwd ? ' · ' + r.cwd.split(/[/\\]/).filter(Boolean).slice(-1)[0] : '')),
|
|
785
|
+
Btn({ key: 'stop' + r.sessionId, onClick: () => stopActiveChat(r.sessionId), children: 'stop' }));
|
|
771
786
|
}),
|
|
772
787
|
})
|
|
773
788
|
: null,
|
|
@@ -796,7 +811,7 @@ function historySide() {
|
|
|
796
811
|
? h('p', { key: 'min2', class: 'lede empty-state' }, 'type at least 2 characters to search')
|
|
797
812
|
: null,
|
|
798
813
|
state.searchQ
|
|
799
|
-
? Btn({ key: 'clearq', onClick: () => { state.searchQ = ''; state.searchHits = null; state.searchBusy = false; render(); }, children: '
|
|
814
|
+
? Btn({ key: 'clearq', onClick: () => { state.searchQ = ''; state.searchHits = null; state.searchBusy = false; render(); }, children: 'clear search' })
|
|
800
815
|
: null,
|
|
801
816
|
!searching && projects.length > 1
|
|
802
817
|
? h('div', { key: 'projfilter', class: 'pill-row', role: 'group', 'aria-label': 'Filter sessions by project' },
|
|
@@ -810,10 +825,10 @@ function historySide() {
|
|
|
810
825
|
'show subagents (' + subagentCount + ')')
|
|
811
826
|
: null,
|
|
812
827
|
state.historyError
|
|
813
|
-
? h('p', { key: 'err', class: 'lede field-error', role: 'alert' },
|
|
828
|
+
? h('p', { key: 'err', class: 'lede field-error', role: 'alert' }, state.historyError)
|
|
814
829
|
: (rows.length ? h('div', { key: 'rows' }, ...rows) : h('p', { key: 'empty', class: 'lede empty-state' }, 'no sessions yet')),
|
|
815
830
|
!searching && truncatedBy > 0
|
|
816
|
-
? Btn({ key: 'more', onClick: () => { state.sessionsLimit += 60; render(); }, children: '
|
|
831
|
+
? Btn({ key: 'more', onClick: () => { state.sessionsLimit += 60; render(); }, children: 'show '+Math.min(60, truncatedBy)+' more ('+truncatedBy+' hidden)' })
|
|
817
832
|
: null,
|
|
818
833
|
],
|
|
819
834
|
}),
|
|
@@ -842,9 +857,8 @@ async function saveBackend() {
|
|
|
842
857
|
function healthSummary() {
|
|
843
858
|
const hh = state.health || {};
|
|
844
859
|
const ok = hh.status === 'ok';
|
|
845
|
-
const dot = ok ? '●' : (hh.status === 'unknown' ? '◌' : '○');
|
|
846
860
|
const bits = [];
|
|
847
|
-
bits.push(
|
|
861
|
+
bits.push(hh.status || 'unknown');
|
|
848
862
|
if (hh.version) bits.push('v' + hh.version);
|
|
849
863
|
if (typeof hh.agents === 'number') bits.push(hh.agents + ' agents');
|
|
850
864
|
if (typeof hh.activeExecutions === 'number') bits.push(hh.activeExecutions + ' active');
|
|
@@ -858,7 +872,7 @@ function settingsMain() {
|
|
|
858
872
|
const isValid = isValidUrl(state.backendDraft);
|
|
859
873
|
return [
|
|
860
874
|
PageHeader({
|
|
861
|
-
title: '
|
|
875
|
+
title: 'settings',
|
|
862
876
|
lede: 'point agentgui at any backend. blank = same-origin (ccsniff in-process). ?backend=… or the field below persists via localStorage.',
|
|
863
877
|
}),
|
|
864
878
|
h('div', { key: 'settings-grid', class: 'settings-grid' }, [
|
|
@@ -878,10 +892,10 @@ function settingsMain() {
|
|
|
878
892
|
title: isValid ? 'Enter a valid URL or leave blank for same-origin' : 'Invalid URL format',
|
|
879
893
|
onInput: (v) => { state.backendDraft = v; render(); },
|
|
880
894
|
}),
|
|
881
|
-
!isValid ? h('p', { key: 'err', id: 'backend-url-error', class: 'lede field-error', role: 'alert' }, '
|
|
882
|
-
state.backendStatus === 'connecting' ? h('p', { key: 'bst', class: 'lede', role: 'status' }, '
|
|
883
|
-
state.backendStatus === 'ok' ? h('p', { key: 'bst', class: 'lede', role: 'status' }, '
|
|
884
|
-
state.backendStatus === 'failed' ? h('p', { key: 'bst', class: 'lede field-error', role: 'alert' }, '
|
|
895
|
+
!isValid ? h('p', { key: 'err', id: 'backend-url-error', class: 'lede field-error', role: 'alert' }, 'Invalid URL format') : null,
|
|
896
|
+
state.backendStatus === 'connecting' ? h('p', { key: 'bst', class: 'lede', role: 'status' }, 'connecting…') : null,
|
|
897
|
+
state.backendStatus === 'ok' ? h('p', { key: 'bst', class: 'lede', role: 'status' }, 'connected') : null,
|
|
898
|
+
state.backendStatus === 'failed' ? h('p', { key: 'bst', class: 'lede field-error', role: 'alert' }, 'connection failed — check the URL') : null,
|
|
885
899
|
healthSummary(),
|
|
886
900
|
Btn({
|
|
887
901
|
key: 'savebtn',
|