@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.
@@ -1,21 +1,16 @@
1
1
  // Translate a coding tool's hook event into Agent Guild reports.
2
2
  //
3
- // Claude Code, Codex CLI, Gemini CLI and Grok Build all run hook commands
4
- // with one JSON event on stdin. They spell the event name and the sub-agent
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
- // - A tool call that runs a sub-agent: Gemini CLI's `invoke_agent`
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. Gemini's call is a foreground
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', 'invoke_agent']);
31
- const TOOL_START_EVENTS = new Set(['PreToolUse', 'BeforeTool']);
32
- const TOOL_END_EVENTS = new Set(['PostToolUse', 'PostToolUseFailure', 'AfterTool']);
33
- const TURN_BOUNDARY_EVENTS = new Set(['BeforeAgent', 'AfterAgent', 'UserPromptSubmit', 'Stop']);
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
- if ((TOOL_START_EVENTS.has(event) || TOOL_END_EVENTS.has(event)) && SUBAGENT_TOOLS.has(toolName)) {
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
- if (toolInput.run_in_background !== true) {
89
- // tool_use_id links the start and end events; fall back to hashing the
90
- // identical tool_input both events carry (Gemini CLI sends no id).
91
- const key = text(input.tool_use_id, input.toolUseId) ||
92
- crypto.createHash('sha1').update(JSON.stringify(toolInput)).digest('hex').slice(0, 16);
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
- // Gemini CLI skips AfterTool for an invoke_agent call that was cancelled
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.llm_request?.model);
170
+ : text(input.model, input.modelId, input.modelName);
128
171
  if (model) reports.push({ model });
129
172
 
130
- // Gemini CLI has no session-start event; its turn start stands in.
131
- const toolSessionId = insideSubagent ? null : text(input.session_id, input.sessionId);
132
- if (toolSessionId && (event === 'SessionStart' || event === 'BeforeAgent')) reports.push({ toolSessionId });
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
- function renderAgents(container, agents) {
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
- container.replaceChildren(...agents.map((agent) => {
382
+ const familiar = (id, name, hue) => {
361
383
  const el = document.createElement('span');
362
- el.className = `agent ${agent.status}`;
363
- el.dataset.agent = agent.id;
364
- if (known && !known.has(agent.id)) el.classList.add('summon');
365
- el.style.setProperty('--c', `hsl(${hueFor(agent.name)} 65% 50%)`);
366
- el.dataset.familiar = FAMILIARS[hueFor(agent.name) % FAMILIARS.length];
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 and Gemini CLI only find a session from there;
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
- node.setAttribute('aria-label', `${s.name}, ${s.provider.vendor}${accountName}${modelLabel}, ${statusText(s)}, ${s.agents.length} agents`);
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:empty::after { content: "none reported"; font-size: 0.75rem; color: var(--muted); }
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
- }