@oddessentials/agent-guild 0.10.0 → 0.12.0
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/README.md +22 -26
- package/bin/agent-guild-report.mjs +67 -25
- package/config/providers.default.json +18 -14
- package/examples/grok-hooks.json +43 -0
- package/package.json +1 -1
- package/src/manager/command-resolver.mjs +6 -2
- package/src/manager/config.mjs +1 -0
- package/src/manager/install-channels.mjs +7 -0
- package/src/manager/main.mjs +8 -2
- package/src/manager/news.mjs +1 -1
- package/src/manager/providers.mjs +14 -9
- package/src/manager/report-shims.mjs +6 -4
- package/src/manager/server.mjs +18 -3
- package/src/manager/session-history.mjs +32 -74
- package/src/manager/session-hooks.mjs +351 -0
- package/src/manager/session-manager.mjs +27 -19
- package/src/manager/session.mjs +183 -25
- package/src/manager/usage.mjs +3 -258
- package/src/report/hooks.mjs +87 -38
- package/web/app.js +89 -13
- package/web/index.html +5 -0
- package/web/styles.css +10 -2
- package/examples/gemini-settings.json +0 -61
package/src/report/hooks.mjs
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
// Translate a coding tool's hook event into Agent Guild reports.
|
|
2
2
|
//
|
|
3
|
-
// Claude Code, Codex CLI,
|
|
4
|
-
// with one JSON event on stdin. They spell the event name and the
|
|
5
|
-
// and model fields differently, so every spelling is accepted here.
|
|
3
|
+
// Claude Code, Codex CLI, Antigravity CLI and Grok Build all run hook
|
|
4
|
+
// commands with one JSON event on stdin. They spell the event name and the
|
|
5
|
+
// sub-agent and model fields differently, so every spelling is accepted here.
|
|
6
6
|
//
|
|
7
7
|
// Sub-agents come from:
|
|
8
8
|
// - SubagentStart / SubagentStop (Claude Code, Codex CLI, Grok Build).
|
|
9
|
-
// -
|
|
10
|
-
// (BeforeTool / AfterTool), which returns when the agent is finished, and
|
|
11
|
-
// Claude Code's "Agent" tool (formerly "Task") on PreToolUse / PostToolUse
|
|
9
|
+
// - Claude Code's "Agent" tool (formerly "Task") on PreToolUse / PostToolUse
|
|
12
10
|
// for versions without the sub-agent events. A launch marked as background
|
|
13
11
|
// returns immediately, so its PostToolUse says nothing about when the
|
|
14
12
|
// agent finishes; those launches are skipped. Configure one style, not
|
|
15
|
-
// both, or each sub-agent appears twice.
|
|
16
|
-
// agent: the parent waits for it, and when its end event never comes (a
|
|
17
|
-
// cancelled or denied call) it is closed at the next turn boundary of the
|
|
18
|
-
// main session.
|
|
13
|
+
// both, or each sub-agent appears twice.
|
|
19
14
|
// - Codex CLI ends every turn of a sub-agent with SubagentStop; a follow-up
|
|
20
15
|
// turn of the same agent starts with a UserPromptSubmit inside it, or,
|
|
21
16
|
// with Codex's multi_agent_v2 tools, straight with tool calls, which
|
|
@@ -27,25 +22,61 @@
|
|
|
27
22
|
import path from 'node:path';
|
|
28
23
|
import crypto from 'node:crypto';
|
|
29
24
|
|
|
30
|
-
const SUBAGENT_TOOLS = new Set(['Task', 'Agent'
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
25
|
+
const SUBAGENT_TOOLS = new Set(['Task', 'Agent']);
|
|
26
|
+
const SHELL_TOOLS = new Set(['Bash', 'PowerShell', 'run_terminal_command']);
|
|
27
|
+
const TOOL_START_EVENTS = new Set(['PreToolUse']);
|
|
28
|
+
const TOOL_END_EVENTS = new Set(['PostToolUse', 'PostToolUseFailure']);
|
|
29
|
+
const TURN_BOUNDARY_EVENTS = new Set(['UserPromptSubmit', 'Stop', 'Interrupt']);
|
|
34
30
|
const MAX_DETAIL = 200;
|
|
35
31
|
|
|
32
|
+
const FINISHED_TASKS = new Set(['completed', 'failed', 'killed']);
|
|
33
|
+
const LIVE_TASKS = new Set(['pending', 'running', 'paused']);
|
|
34
|
+
const MAX_RUNNING_TASKS = 256;
|
|
35
|
+
|
|
36
36
|
const text = (...values) => values.find((v) => typeof v === 'string' && v.trim())?.trim() ?? null;
|
|
37
37
|
|
|
38
|
+
function commandHash(command) {
|
|
39
|
+
return crypto.createHash('sha256').update(command.replace(/\s+/g, ' ').trim()).digest('hex').slice(0, 32);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function shellReport(event, input, subagentId) {
|
|
43
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
44
|
+
const id = text(input.tool_use_id, input.toolUseId);
|
|
45
|
+
const agent = subagentId ? { agentId: `hook-${subagentId}` } : {};
|
|
46
|
+
const match = typeof toolInput.command === 'string' ? { match: commandHash(toolInput.command) } : {};
|
|
47
|
+
// Codex CLI (its events carry turn_id) keeps a command running past its turn, and reports no end for one it refused.
|
|
48
|
+
const codex = Boolean(text(input.turn_id));
|
|
49
|
+
if (event === 'PermissionRequest') return { shell: codex ? 'asked' : 'waiting', ...agent, ...match };
|
|
50
|
+
if (!id) return null;
|
|
51
|
+
const key = id.slice(0, 128);
|
|
52
|
+
if (TOOL_START_EVENTS.has(event)) return { shell: 'start', key, ...agent, ...match, ...(codex ? { persist: true } : {}) };
|
|
53
|
+
const response = input.tool_response ?? input.toolResponse;
|
|
54
|
+
const task = text(response?.backgroundTaskId);
|
|
55
|
+
if (task) return { shell: 'background', key, task: task.slice(0, 128), ...(response.backgroundEndsWithFinalResponse === true ? { endsWithAgent: true } : {}) };
|
|
56
|
+
return { shell: 'end', key };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Claude Code reports a background task ending as a prompt of <task-notification> blocks.
|
|
60
|
+
function taskNotifications(prompt) {
|
|
61
|
+
if (typeof prompt !== 'string' || !/^\s*<task-notification>/.test(prompt)) return null;
|
|
62
|
+
return [...prompt.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)].map(([, body]) => {
|
|
63
|
+
const tag = (name) => body.match(new RegExp(`<${name}>([^<]*)</${name}>`))?.[1].trim() || null;
|
|
64
|
+
return { taskId: tag('task-id'), toolUseId: tag('tool-use-id'), status: tag('status') };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function runningShells(tasks) {
|
|
69
|
+
const ids = tasks.filter((task) => task?.type === 'shell' && !FINISHED_TASKS.has(task.status)).map((task) => text(task.id)).filter(Boolean);
|
|
70
|
+
return { shell: 'running', tasks: ids.slice(0, MAX_RUNNING_TASKS).map((id) => id.slice(0, 128)) };
|
|
71
|
+
}
|
|
72
|
+
|
|
38
73
|
/** "subagent_start", "subagentStart" and "SubagentStart" all become "SubagentStart". */
|
|
39
74
|
function eventName(input) {
|
|
40
75
|
const raw = text(input.hook_event_name, input.hookEventName) || '';
|
|
41
76
|
return raw.replace(/(?:^|[_-])([a-z])/g, (_m, c) => c.toUpperCase());
|
|
42
77
|
}
|
|
43
78
|
|
|
44
|
-
/**
|
|
45
|
-
* Reports for one hook event: an agent report (has agentId) when a
|
|
46
|
-
* sub-agent starts or stops, and a model report (has model) when the event
|
|
47
|
-
* names the main model. Empty for events that carry neither.
|
|
48
|
-
*/
|
|
79
|
+
/** Reports for one hook event: its sub-agents, shell commands, model and the tool's own session id. */
|
|
49
80
|
export function hookToReports(input) {
|
|
50
81
|
if (!input || typeof input !== 'object') return [];
|
|
51
82
|
const event = eventName(input);
|
|
@@ -69,6 +100,19 @@ export function hookToReports(input) {
|
|
|
69
100
|
if (detail) report.detail = detail.slice(0, MAX_DETAIL);
|
|
70
101
|
reports.push(report);
|
|
71
102
|
}
|
|
103
|
+
if (Array.isArray(input.background_tasks)) reports.push(runningShells(input.background_tasks));
|
|
104
|
+
return reports;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (event === 'UserPromptSubmit' && !text(input.turn_id)) {
|
|
108
|
+
// Claude Code (turn_id is Codex's) fires this for a prompt typed while a turn runs, too, so it is no turn boundary.
|
|
109
|
+
// It also brings each background task's end; a sub-agent's task id is its agent id, and one stopped with TaskStop
|
|
110
|
+
// gets no SubagentStop.
|
|
111
|
+
for (const { taskId, toolUseId, status } of taskNotifications(input.prompt) ?? []) {
|
|
112
|
+
if (!taskId || LIVE_TASKS.has(status)) continue;
|
|
113
|
+
reports.push({ shell: 'end', task: taskId.slice(0, 128), ...(toolUseId ? { key: toolUseId.slice(0, 128) } : {}) });
|
|
114
|
+
if (FINISHED_TASKS.has(status)) reports.push({ agentId: `hook-${taskId}`, status: 'done' });
|
|
115
|
+
}
|
|
72
116
|
return reports;
|
|
73
117
|
}
|
|
74
118
|
|
|
@@ -79,26 +123,29 @@ export function hookToReports(input) {
|
|
|
79
123
|
const childType = text(input.subagent_type, input.subagentType);
|
|
80
124
|
const childId = text(input.session_id, input.sessionId);
|
|
81
125
|
if (childType && childId) reports.push({ agentId: `hook-${childId}`, name: childType, kind: 'subagent', status: 'done' });
|
|
126
|
+
else if (event === 'SessionEnd') reports.push({ shell: 'reset' });
|
|
82
127
|
return reports;
|
|
83
128
|
}
|
|
84
129
|
|
|
85
130
|
const toolName = text(input.tool_name, input.toolName);
|
|
86
|
-
|
|
131
|
+
const toolEvent = TOOL_START_EVENTS.has(event) || TOOL_END_EVENTS.has(event);
|
|
132
|
+
const shell = (toolEvent || event === 'PermissionRequest') && SHELL_TOOLS.has(toolName) ? shellReport(event, input, subagentId) : null;
|
|
133
|
+
if (shell) reports.push(shell);
|
|
134
|
+
if (event === 'PostToolUse' && toolName === 'TaskStop') {
|
|
87
135
|
const toolInput = input.tool_input || input.toolInput || {};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
136
|
+
const task = text(toolInput.task_id, toolInput.shell_id);
|
|
137
|
+
if (task) reports.push({ shell: 'end', task: task.slice(0, 128) }, { agentId: `hook-${task}`, status: 'done' });
|
|
138
|
+
}
|
|
139
|
+
if (toolEvent && SUBAGENT_TOOLS.has(toolName)) {
|
|
140
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
141
|
+
const key = text(input.tool_use_id, input.toolUseId);
|
|
142
|
+
if (key && toolInput.run_in_background !== true) {
|
|
93
143
|
reports.push({
|
|
94
144
|
agentId: `hook-task-${key}`,
|
|
95
145
|
name: text(toolInput.subagent_type, toolInput.agent_name) || 'subagent',
|
|
96
146
|
kind: 'subagent',
|
|
97
147
|
detail: (text(toolInput.description, toolInput.prompt) || '').slice(0, MAX_DETAIL),
|
|
98
148
|
status: TOOL_START_EVENTS.has(event) ? 'working' : 'done',
|
|
99
|
-
// Gemini CLI's call returns when the agent is finished, so the
|
|
100
|
-
// parent waits: a model reported meanwhile is the sub-agent's.
|
|
101
|
-
foreground: toolName === 'invoke_agent',
|
|
102
149
|
});
|
|
103
150
|
}
|
|
104
151
|
}
|
|
@@ -115,24 +162,26 @@ export function hookToReports(input) {
|
|
|
115
162
|
// agent_id, Grok Build by subagentType); its model is not the session's.
|
|
116
163
|
const insideSubagent = Boolean(subagentId || text(input.subagent_type, input.subagentType));
|
|
117
164
|
|
|
118
|
-
|
|
119
|
-
// or denied. A turn boundary of the main session (BeforeAgent and
|
|
120
|
-
// AfterAgent in Gemini CLI; a main-thread prompt or Stop elsewhere) cannot
|
|
121
|
-
// happen while the parent waits for a foreground agent, so one still
|
|
122
|
-
// working then has been orphaned.
|
|
165
|
+
if (event === 'Stop' && Array.isArray(input.background_tasks)) reports.push(runningShells(input.background_tasks));
|
|
123
166
|
if (!insideSubagent && TURN_BOUNDARY_EVENTS.has(event)) reports.push({ finishForeground: true });
|
|
124
167
|
|
|
125
|
-
const model = insideSubagent ? null : event === 'PostModelSwitch'
|
|
168
|
+
const model = insideSubagent || toolEvent ? null : event === 'PostModelSwitch'
|
|
126
169
|
? text(input.to_model)
|
|
127
|
-
: text(input.model, input.modelId, input.
|
|
170
|
+
: text(input.model, input.modelId, input.modelName);
|
|
128
171
|
if (model) reports.push({ model });
|
|
129
172
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
173
|
+
if (!insideSubagent && event === 'SessionStart') reports.push({ hello: true });
|
|
174
|
+
|
|
175
|
+
const toolSessionId = insideSubagent ? null : text(input.session_id, input.sessionId, input.conversationId);
|
|
176
|
+
if (toolSessionId && (event === 'SessionStart' || event === 'PreInvocation')) reports.push({ toolSessionId });
|
|
133
177
|
return reports;
|
|
134
178
|
}
|
|
135
179
|
|
|
180
|
+
/** Whether an Antigravity CLI conversation starts with the user's own request; a sub-agent's starts with its parent's message. */
|
|
181
|
+
export function antigravityUserConversation(firstRecord) {
|
|
182
|
+
return firstRecord?.type === 'USER_INPUT' && firstRecord.source === 'USER_EXPLICIT';
|
|
183
|
+
}
|
|
184
|
+
|
|
136
185
|
/** Model report from the JSON Claude Code feeds to a status line command. */
|
|
137
186
|
export function claudeStatuslineToReport(input) {
|
|
138
187
|
const id = input?.model?.id;
|
package/web/app.js
CHANGED
|
@@ -354,23 +354,69 @@ function paintProviderIcon(el, provider) {
|
|
|
354
354
|
|
|
355
355
|
const FAMILIARS = ['flame', 'leaf', 'night', 'aether'];
|
|
356
356
|
|
|
357
|
-
|
|
357
|
+
const REPORTING_TEXT = {
|
|
358
|
+
pending: 'waiting for hooks',
|
|
359
|
+
unavailable: 'no reports yet',
|
|
360
|
+
setup_required: 'reporting off',
|
|
361
|
+
unsupported: 'not supported',
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
function paintReporting(row, s) {
|
|
365
|
+
const reporting = s.status === 'running' ? s.reporting : null;
|
|
366
|
+
const agents = row.querySelector('.agents');
|
|
367
|
+
agents.dataset.empty = REPORTING_TEXT[reporting?.state] || 'none reported';
|
|
368
|
+
agents.classList.toggle('reporting-attention', ['unavailable', 'setup_required', 'unsupported'].includes(reporting?.state));
|
|
369
|
+
const why = row.querySelector('.reporting-why');
|
|
370
|
+
const reason = reporting?.state !== 'active' ? reporting?.reason || '' : '';
|
|
371
|
+
why.hidden = !reason;
|
|
372
|
+
why.title = reason;
|
|
373
|
+
why.setAttribute('aria-label', `Why agent reporting says ${agents.dataset.empty}: ${reason}`);
|
|
374
|
+
why.onclick = () => toast(reason, 12000);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const MAX_SHELLS_SHOWN = 16;
|
|
378
|
+
|
|
379
|
+
function renderAgents(container, agents, shells = []) {
|
|
358
380
|
const known = container.dataset.rendered ? new Set([...container.children].map((el) => el.dataset.agent)) : null;
|
|
359
381
|
container.dataset.rendered = 'true';
|
|
360
|
-
|
|
382
|
+
const familiar = (id, name, hue) => {
|
|
361
383
|
const el = document.createElement('span');
|
|
362
|
-
el.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
el.
|
|
366
|
-
el.
|
|
384
|
+
el.dataset.agent = id;
|
|
385
|
+
if (known && !known.has(id)) el.classList.add('summon');
|
|
386
|
+
el.style.setProperty('--c', `hsl(${hue} 65% 50%)`);
|
|
387
|
+
el.dataset.familiar = FAMILIARS[hue % FAMILIARS.length];
|
|
388
|
+
el.setAttribute('role', 'img');
|
|
389
|
+
return el;
|
|
390
|
+
};
|
|
391
|
+
const agentEls = agents.map((agent) => {
|
|
392
|
+
const el = familiar(agent.id, agent.name, hueFor(agent.name));
|
|
393
|
+
el.classList.add('agent', agent.status);
|
|
367
394
|
el.textContent = (agent.name || '?').charAt(0).toUpperCase();
|
|
368
395
|
const detail = agent.detail ? ` — ${agent.detail}` : '';
|
|
369
396
|
el.title = `${agent.name} (${agent.status})${detail}`;
|
|
370
|
-
el.setAttribute('role', 'img');
|
|
371
397
|
el.setAttribute('aria-label', el.title);
|
|
372
398
|
return el;
|
|
373
|
-
})
|
|
399
|
+
});
|
|
400
|
+
const shellEls = shells.slice(0, MAX_SHELLS_SHOWN).map((shell) => {
|
|
401
|
+
const el = familiar(shell.id, 'Shell', hueFor(shell.id));
|
|
402
|
+
el.classList.add('agent', 'working', 'shell');
|
|
403
|
+
el.textContent = '>';
|
|
404
|
+
el.title = 'Shell command (running)';
|
|
405
|
+
el.setAttribute('aria-label', el.title);
|
|
406
|
+
return el;
|
|
407
|
+
});
|
|
408
|
+
if (shells.length > MAX_SHELLS_SHOWN) {
|
|
409
|
+
const more = document.createElement('span');
|
|
410
|
+
const hidden = shells.length - MAX_SHELLS_SHOWN;
|
|
411
|
+
more.className = 'agent-overflow';
|
|
412
|
+
more.dataset.agent = 'shell-overflow';
|
|
413
|
+
more.textContent = `+${hidden}`;
|
|
414
|
+
more.title = `${shells.length} shell commands running; ${hidden} more not drawn`;
|
|
415
|
+
more.setAttribute('role', 'img');
|
|
416
|
+
more.setAttribute('aria-label', more.title);
|
|
417
|
+
shellEls.push(more);
|
|
418
|
+
}
|
|
419
|
+
container.replaceChildren(...agentEls, ...shellEls);
|
|
374
420
|
}
|
|
375
421
|
|
|
376
422
|
// ---- API ------------------------------------------------------------------
|
|
@@ -431,6 +477,7 @@ function renderAccounts(card, provider) {
|
|
|
431
477
|
selectAccount(provider, account.id);
|
|
432
478
|
renderAccounts(card, provider);
|
|
433
479
|
renderUsage(card, provider);
|
|
480
|
+
renderReportingSetup(card, provider);
|
|
434
481
|
});
|
|
435
482
|
return chip;
|
|
436
483
|
}));
|
|
@@ -444,6 +491,31 @@ function renderAccounts(card, provider) {
|
|
|
444
491
|
});
|
|
445
492
|
}
|
|
446
493
|
|
|
494
|
+
function renderReportingSetup(card, provider) {
|
|
495
|
+
const row = card.querySelector('.reporting-row');
|
|
496
|
+
row.hidden = !provider.available || typeof provider.reportingEnabled !== 'boolean';
|
|
497
|
+
if (row.hidden) return;
|
|
498
|
+
const on = provider.reportingEnabled;
|
|
499
|
+
row.querySelector('.reporting-text').textContent = `Agent reporting ${on ? 'on' : 'off'}`;
|
|
500
|
+
const button = row.querySelector('.reporting-toggle');
|
|
501
|
+
button.textContent = on ? 'Turn off' : 'Turn on';
|
|
502
|
+
button.title = on
|
|
503
|
+
? `Remove the Agent Guild plugin from ${provider.tool}. New sessions stop reporting their model.`
|
|
504
|
+
: `Install the Agent Guild plugin into ${provider.tool} with "${provider.command} plugin install", so new sessions show their model and can be resumed. It does nothing in sessions started outside Agent Guild.`;
|
|
505
|
+
button.onclick = async () => {
|
|
506
|
+
button.disabled = true;
|
|
507
|
+
try {
|
|
508
|
+
await api('POST', `/providers/${provider.id}/reporting`, { enabled: !on });
|
|
509
|
+
toast(on ? `Agent reporting is off for ${provider.tool}.` : `Agent reporting is on for ${provider.tool}. It applies to new sessions.`);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
if (err instanceof AuthError) return showAuth(err.message);
|
|
512
|
+
toast(err.message, 10000);
|
|
513
|
+
} finally {
|
|
514
|
+
button.disabled = false;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
447
519
|
let dealt = false;
|
|
448
520
|
|
|
449
521
|
function renderProviders() {
|
|
@@ -487,6 +559,7 @@ function renderProviders() {
|
|
|
487
559
|
renderConsoleLinks(node, provider);
|
|
488
560
|
renderAccounts(node, provider);
|
|
489
561
|
renderUsage(node, provider);
|
|
562
|
+
renderReportingSetup(node, provider);
|
|
490
563
|
renderModelStats(node, provider);
|
|
491
564
|
return node;
|
|
492
565
|
}));
|
|
@@ -1619,7 +1692,7 @@ async function installProvider(provider, card, { force = false } = {}) {
|
|
|
1619
1692
|
|
|
1620
1693
|
/**
|
|
1621
1694
|
* Start a session. A resumed session starts in the folder its transcript
|
|
1622
|
-
* names, since Claude Code
|
|
1695
|
+
* names, since Claude Code only finds a session from there;
|
|
1623
1696
|
* when that folder is gone, the working folder is used instead.
|
|
1624
1697
|
*/
|
|
1625
1698
|
async function startSession(provider, card, { resume, cwd, account = selectedAccount(provider).id } = {}) {
|
|
@@ -2541,7 +2614,8 @@ function updateCard(node, s) {
|
|
|
2541
2614
|
// The LRM keeps a leading "/" in place under the right-to-left truncation style.
|
|
2542
2615
|
cwd.textContent = `\u200E${s.cwd}`;
|
|
2543
2616
|
cwd.title = s.cwd;
|
|
2544
|
-
renderAgents(node.querySelector('.agents'), s.agents);
|
|
2617
|
+
renderAgents(node.querySelector('.agents'), s.agents, s.shells || []);
|
|
2618
|
+
paintReporting(node.querySelector('.agents-row'), s);
|
|
2545
2619
|
node.classList.toggle('exited', s.status === 'exited');
|
|
2546
2620
|
node.querySelector('.stop').hidden = s.status !== 'running';
|
|
2547
2621
|
node.querySelector('.remove').hidden = s.status === 'running';
|
|
@@ -2553,7 +2627,9 @@ function updateCard(node, s) {
|
|
|
2553
2627
|
resume.title = `Start ${s.provider.tool} again on this session${id ? ` (${id})` : ''} in ${s.cwd}`;
|
|
2554
2628
|
const modelLabel = s.model ? `, model ${modelText(s)}` : '';
|
|
2555
2629
|
const accountName = accountLabel(s) ? `, ${accountLabel(s)} account` : '';
|
|
2556
|
-
|
|
2630
|
+
const shellCount = (s.shells || []).length;
|
|
2631
|
+
const reportingNote = s.status === 'running' && REPORTING_TEXT[s.reporting?.state] ? `, agent reporting: ${REPORTING_TEXT[s.reporting.state]}` : '';
|
|
2632
|
+
node.setAttribute('aria-label', `${s.name}, ${s.provider.vendor}${accountName}${modelLabel}, ${statusText(s)}, ${s.agents.length} agents${shellCount ? `, ${shellCount} shell command${shellCount === 1 ? '' : 's'} running` : ''}${reportingNote}`);
|
|
2557
2633
|
}
|
|
2558
2634
|
|
|
2559
2635
|
function renderSessions() {
|
|
@@ -2810,7 +2886,7 @@ function updatePanel() {
|
|
|
2810
2886
|
const id = toolSessionId(s);
|
|
2811
2887
|
$('panel-sub').textContent = [s.provider.tool, accountLabel(s), modelText(s), statusText(s), s.cwd, id && `session ${id}`].filter(Boolean).join(' · ');
|
|
2812
2888
|
$('panel-sub').title = modelTitle(s);
|
|
2813
|
-
renderAgents($('panel-agents'), s.agents);
|
|
2889
|
+
renderAgents($('panel-agents'), s.agents, s.shells || []);
|
|
2814
2890
|
const stop = $('panel-stop');
|
|
2815
2891
|
stop.textContent = s.status === 'running' ? 'Stop' : 'Remove';
|
|
2816
2892
|
}
|
package/web/index.html
CHANGED
|
@@ -262,6 +262,10 @@
|
|
|
262
262
|
<div class="accounts" role="tablist" aria-label="Accounts" hidden></div>
|
|
263
263
|
<div class="usage"></div>
|
|
264
264
|
<div class="model-stats"></div>
|
|
265
|
+
<div class="reporting-row" hidden>
|
|
266
|
+
<span class="reporting-text"></span>
|
|
267
|
+
<button class="btn reporting-toggle" type="button"></button>
|
|
268
|
+
</div>
|
|
265
269
|
<div class="provider-links">
|
|
266
270
|
<a class="console-link usage-link" target="_blank" rel="noopener noreferrer">Usage</a>
|
|
267
271
|
<a class="console-link billing-link" target="_blank" rel="noopener noreferrer">Billing</a>
|
|
@@ -349,6 +353,7 @@
|
|
|
349
353
|
<div class="agents-row">
|
|
350
354
|
<span class="agents-label">Agents</span>
|
|
351
355
|
<div class="agents"></div>
|
|
356
|
+
<button class="reporting-why" type="button" hidden>Why?</button>
|
|
352
357
|
</div>
|
|
353
358
|
<div class="card-actions">
|
|
354
359
|
<button class="btn primary open" type="button">Open</button>
|
package/web/styles.css
CHANGED
|
@@ -196,6 +196,8 @@ main { max-width: 1200px; margin: 0 auto; padding: 26px 16px 56px; display: grid
|
|
|
196
196
|
.tier-d { --tier: #94a3b8; --tier-ink: #d4dbe4; }
|
|
197
197
|
}
|
|
198
198
|
.provider-links { display: flex; gap: 14px; font-size: 0.78rem; }
|
|
199
|
+
.reporting-row { display: flex; align-items: center; gap: 10px; font-size: 0.78rem; color: var(--muted); }
|
|
200
|
+
.reporting-row .btn { padding: 2px 10px; font-size: 0.75rem; white-space: nowrap; }
|
|
199
201
|
.console-link { color: var(--accent); text-decoration: none; }
|
|
200
202
|
.console-link:hover { text-decoration: underline; }
|
|
201
203
|
.console-link:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 3px; }
|
|
@@ -223,7 +225,7 @@ main { max-width: 1200px; margin: 0 auto; padding: 26px 16px 56px; display: grid
|
|
|
223
225
|
|
|
224
226
|
.session-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; }
|
|
225
227
|
.session-card {
|
|
226
|
-
display: grid; gap: 12px; padding: 14px; border: 1px solid var(--border); border-radius: var(--radius);
|
|
228
|
+
display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; padding: 14px; border: 1px solid var(--border); border-radius: var(--radius);
|
|
227
229
|
background: var(--surface); box-shadow: var(--shadow); cursor: pointer;
|
|
228
230
|
}
|
|
229
231
|
.session-card:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
@@ -256,7 +258,11 @@ main { max-width: 1200px; margin: 0 auto; padding: 26px 16px 56px; display: grid
|
|
|
256
258
|
.agents-row { display: flex; align-items: center; gap: 10px; min-height: 30px; }
|
|
257
259
|
.agents-label { font-size: 0.75rem; color: var(--muted); }
|
|
258
260
|
.agents { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
259
|
-
.agents
|
|
261
|
+
.agents-row .agents { flex: 1; min-width: 0; }
|
|
262
|
+
.agents:empty::after { content: attr(data-empty); font-size: 0.75rem; color: var(--muted); }
|
|
263
|
+
.agents.reporting-attention:empty::after { color: var(--warn); }
|
|
264
|
+
.reporting-why { align-self: center; padding: 0 6px; border: 1px solid var(--border); border-radius: 999px; background: none; color: var(--muted); font: inherit; font-size: 0.7rem; cursor: pointer; }
|
|
265
|
+
.reporting-why:hover, .reporting-why:focus-visible { color: var(--text); border-color: var(--muted); }
|
|
260
266
|
.agent {
|
|
261
267
|
--c: #64748b;
|
|
262
268
|
width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; position: relative;
|
|
@@ -270,6 +276,8 @@ main { max-width: 1200px; margin: 0 auto; padding: 26px 16px 56px; display: grid
|
|
|
270
276
|
.agent.waiting { border-style: dashed; }
|
|
271
277
|
.agent.done, .agent.idle { opacity: 0.5; }
|
|
272
278
|
.panel-title .agents:empty::after { content: none; }
|
|
279
|
+
.agent.shell { font-family: var(--font-mono, monospace); font-size: 0.8rem; }
|
|
280
|
+
.agent-overflow { align-self: center; font-size: 0.75rem; font-weight: 600; color: var(--muted); }
|
|
273
281
|
|
|
274
282
|
.card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
275
283
|
.btn {
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"BeforeTool": [
|
|
4
|
-
{
|
|
5
|
-
"matcher": "invoke_agent",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"name": "Agent Guild agent start",
|
|
10
|
-
"command": "agent-guild-report --hook"
|
|
11
|
-
}
|
|
12
|
-
]
|
|
13
|
-
}
|
|
14
|
-
],
|
|
15
|
-
"AfterTool": [
|
|
16
|
-
{
|
|
17
|
-
"matcher": "invoke_agent",
|
|
18
|
-
"hooks": [
|
|
19
|
-
{
|
|
20
|
-
"type": "command",
|
|
21
|
-
"name": "Agent Guild agent stop",
|
|
22
|
-
"command": "agent-guild-report --hook"
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
],
|
|
27
|
-
"BeforeAgent": [
|
|
28
|
-
{
|
|
29
|
-
"hooks": [
|
|
30
|
-
{
|
|
31
|
-
"type": "command",
|
|
32
|
-
"name": "Agent Guild turn start",
|
|
33
|
-
"command": "agent-guild-report --hook"
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
}
|
|
37
|
-
],
|
|
38
|
-
"AfterAgent": [
|
|
39
|
-
{
|
|
40
|
-
"hooks": [
|
|
41
|
-
{
|
|
42
|
-
"type": "command",
|
|
43
|
-
"name": "Agent Guild turn end",
|
|
44
|
-
"command": "agent-guild-report --hook"
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
"BeforeModel": [
|
|
50
|
-
{
|
|
51
|
-
"hooks": [
|
|
52
|
-
{
|
|
53
|
-
"type": "command",
|
|
54
|
-
"name": "Agent Guild model",
|
|
55
|
-
"command": "agent-guild-report --hook"
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
}
|