@agentproto/apps 0.8.2 → 0.9.1

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.
Files changed (60) hide show
  1. package/README.md +28 -1
  2. package/dist/agents-overview/index.d.ts +54 -0
  3. package/dist/agents-overview/index.d.ts.map +1 -0
  4. package/dist/agents-overview/panel.d.ts +14 -0
  5. package/dist/agents-overview/panel.d.ts.map +1 -0
  6. package/dist/agents-overview/panel.mjs +306 -0
  7. package/dist/agents-overview/panel.mjs.map +1 -0
  8. package/dist/agents-overview.mjs +336 -0
  9. package/dist/agents-overview.mjs.map +1 -0
  10. package/dist/bin/sync.mjs +25 -11
  11. package/dist/bin/sync.mjs.map +1 -1
  12. package/dist/bureau-sessions/index.d.ts +52 -0
  13. package/dist/bureau-sessions/index.d.ts.map +1 -0
  14. package/dist/bureau-sessions/panel.d.ts +10 -0
  15. package/dist/bureau-sessions/panel.d.ts.map +1 -0
  16. package/dist/bureau-sessions/panel.mjs +297 -0
  17. package/dist/bureau-sessions/panel.mjs.map +1 -0
  18. package/dist/bureau-sessions.mjs +329 -0
  19. package/dist/bureau-sessions.mjs.map +1 -0
  20. package/dist/content-team/workflows/produce-content.d.ts +14 -2
  21. package/dist/content-team/workflows/produce-content.d.ts.map +1 -1
  22. package/dist/content-team/workflows/produce-cover.d.ts +13 -1
  23. package/dist/content-team/workflows/produce-cover.d.ts.map +1 -1
  24. package/dist/content-team.mjs +25 -11
  25. package/dist/content-team.mjs.map +1 -1
  26. package/dist/index.d.ts +24 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.mjs +2635 -12
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/live-session/index.d.ts +46 -0
  31. package/dist/live-session/index.d.ts.map +1 -0
  32. package/dist/live-session/logic.d.ts +125 -0
  33. package/dist/live-session/logic.d.ts.map +1 -0
  34. package/dist/live-session/panel.d.ts +61 -0
  35. package/dist/live-session/panel.d.ts.map +1 -0
  36. package/dist/live-session/panel.mjs +993 -0
  37. package/dist/live-session/panel.mjs.map +1 -0
  38. package/dist/live-session.mjs +1032 -0
  39. package/dist/live-session.mjs.map +1 -0
  40. package/dist/mcp-app-types.d.ts +33 -0
  41. package/dist/mcp-app-types.d.ts.map +1 -0
  42. package/dist/panel-bridge.d.ts +38 -0
  43. package/dist/panel-bridge.d.ts.map +1 -0
  44. package/dist/session-story/index.d.ts +66 -0
  45. package/dist/session-story/index.d.ts.map +1 -0
  46. package/dist/session-story/panel.d.ts +31 -0
  47. package/dist/session-story/panel.d.ts.map +1 -0
  48. package/dist/session-story/panel.mjs +970 -0
  49. package/dist/session-story/panel.mjs.map +1 -0
  50. package/dist/session-story.mjs +1002 -0
  51. package/dist/session-story.mjs.map +1 -0
  52. package/dist/sessions-panel/index.d.ts +51 -0
  53. package/dist/sessions-panel/index.d.ts.map +1 -0
  54. package/dist/sessions-panel/panel.d.ts +20 -0
  55. package/dist/sessions-panel/panel.d.ts.map +1 -0
  56. package/dist/sessions-panel/panel.mjs +511 -0
  57. package/dist/sessions-panel/panel.mjs.map +1 -0
  58. package/dist/sessions-panel.mjs +545 -0
  59. package/dist/sessions-panel.mjs.map +1 -0
  60. package/package.json +56 -5
package/README.md CHANGED
@@ -53,7 +53,34 @@ npx agentproto-apps-sync [--base-dir <dir>]
53
53
  The default base directory is `~/.agentproto/apps`. It writes each app's
54
54
  manifests under `<baseDir>/<slug>/` and a summary catalog to
55
55
  `<baseDir>/../app-catalog.json` (so `~/.agentproto/app-catalog.json` by
56
- default).
56
+ default). The five builtin daemon panels above are **not** part of this
57
+ sync/emit flow — they aren't `AppHandle`s, so there's nothing to `.emit(dir)`.
58
+ A running daemon lists them in `app_catalog` directly (see "Builtin daemon
59
+ panels" above), independent of this file and of `~/.agentproto/apps.json`.
60
+
61
+ ## Builtin daemon panels
62
+
63
+ Five of `@agentproto/runtime`'s daemon-builtin MCP-Apps widgets live here too,
64
+ as house-app-quality code — but they are **not** `AppHandle`s (`defineApp`
65
+ requires a non-empty `agents` array, and these are pure read-only viewers with
66
+ no agent of their own):
67
+
68
+ | Panel | Import | MCP tool id |
69
+ | --- | --- | --- |
70
+ | `sessions-panel` | `@agentproto/apps/sessions-panel` | `agentproto_sessions` |
71
+ | `agents-overview` | `@agentproto/apps/agents-overview` | `agentproto_agents_overview` |
72
+ | `bureau-sessions` | `@agentproto/apps/bureau-sessions` | `agentproto_bureau_sessions` |
73
+ | `session-story` | `@agentproto/apps/session-story` | `agentproto_session_story` |
74
+ | `live-session` | `@agentproto/apps/live-session` | `live_session` |
75
+
76
+ Each exports a `make<Name>App(ops)` factory producing an `AgnoMcpApp` — the
77
+ shape `@agentproto/runtime`'s `mcp-apps-adapter.ts` mounts on the daemon's MCP
78
+ server. `@agentproto/runtime`'s `builtin-apps.ts` wraps these five with the
79
+ daemon's own session registry and mounts them at boot, unconditionally — no
80
+ `app_install` step, and they're always listed in `app_catalog` under
81
+ `category: "builtin"`. A sixth builtin panel, the terminal, stays in
82
+ `@agentproto/runtime` (`terminal-panel-app.ts`) — it needs a live PTY
83
+ WebSocket, not a portable tool-call-driven `AgnoMcpApp`.
57
84
 
58
85
  ## Generic by design
59
86
 
@@ -0,0 +1,54 @@
1
+ /**
2
+ * McpApp definition for the agentproto "agents — vue claire" panel.
3
+ *
4
+ * A plain-language overview of agent-cli sessions: one card per session
5
+ * with a single human sentence (what the agent is doing/last said) plus a
6
+ * coarse state — `à traiter` / `au travail` / `en attente` / `terminé`.
7
+ *
8
+ * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does
9
+ * NOT depend on @agstudio/mcp-apps). The host (@agentproto/runtime) wires
10
+ * this factory's output via `registerMcpApps` at boot time — see runtime's
11
+ * builtin-apps.ts.
12
+ *
13
+ * Protocol flow:
14
+ * 1. Tool `agentproto_agents_overview` is called by the host.
15
+ * 2. execute() returns the agent-session snapshot → injected into the
16
+ * HTML as the initial render via the tool result JSON.
17
+ * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls
18
+ * `session_list` every ~12 s, then calls `summarize_session` for
19
+ * each visible session to fetch the plain sentence + state.
20
+ *
21
+ * The per-session summary is generated SERVER-SIDE by the
22
+ * `summarize_session` tool, which stays in @agentproto/runtime
23
+ * (summarize-session-tool.ts) — it's tied to the runtime's session
24
+ * ring-buffer/hot path, not to this panel's UI. Only the panel factory +
25
+ * HTML live here.
26
+ *
27
+ * This module also exports `agentsOverviewApp`, a real `defineApp()`
28
+ * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.
29
+ * It's separate from `makeAgentsOverviewApp` above, which the daemon mounts
30
+ * directly at boot: that factory closes over LIVE `listSessions`, something
31
+ * a static emitted `ui.html` snapshot can't carry.
32
+ */
33
+ import { z } from "zod";
34
+ import { type AppHandle } from "@agentproto/app-kit";
35
+ import type { AgnoMcpApp } from "../mcp-app-types.js";
36
+ export declare const agentsOverviewInputSchema: z.ZodObject<{
37
+ filter: z.ZodOptional<z.ZodEnum<{
38
+ running: "running";
39
+ all: "all";
40
+ }>>;
41
+ }, z.core.$strip>;
42
+ export type AgentsOverviewInput = z.infer<typeof agentsOverviewInputSchema>;
43
+ export type AgentsOverviewOutput<TSession = unknown> = {
44
+ sessions: TSession[];
45
+ };
46
+ /** Generic over the host's own session-descriptor shape — see
47
+ * sessions-panel/index.ts's SessionsPanelOps for why. */
48
+ export interface AgentsOverviewOps<TSession = unknown> {
49
+ listSessions(filter?: "running" | "all"): TSession[];
50
+ }
51
+ export type { AgnoMcpApp };
52
+ export declare function makeAgentsOverviewApp<TSession = unknown>(ops: AgentsOverviewOps<TSession>): AgnoMcpApp<AgentsOverviewInput, AgentsOverviewOutput<TSession>>;
53
+ export declare const agentsOverviewApp: AppHandle;
54
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agents-overview/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAErD,eAAO,MAAM,yBAAyB;;;;;iBAKpC,CAAA;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAC3E,MAAM,MAAM,oBAAoB,CAAC,QAAQ,GAAG,OAAO,IAAI;IAAE,QAAQ,EAAE,QAAQ,EAAE,CAAA;CAAE,CAAA;AAE/E;0DAC0D;AAC1D,MAAM,WAAW,iBAAiB,CAAC,QAAQ,GAAG,OAAO;IACnD,YAAY,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,KAAK,GAAG,QAAQ,EAAE,CAAA;CACrD;AAED,YAAY,EAAE,UAAU,EAAE,CAAA;AAE1B,wBAAgB,qBAAqB,CAAC,QAAQ,GAAG,OAAO,EACtD,GAAG,EAAE,iBAAiB,CAAC,QAAQ,CAAC,GAC/B,UAAU,CAAC,mBAAmB,EAAE,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAajE;AAED,eAAO,MAAM,iBAAiB,EAAE,SAY9B,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * HTML bundle for the agentproto "agents — vue claire" panel — the
3
+ * `AGENTS_OVERVIEW_HTML` served as the `ui://agentproto_agents_overview/view`
4
+ * resource. Mounted via the `agentproto_agents_overview` tool defined in
5
+ * ./index.ts.
6
+ *
7
+ * The panel polls `session_list` every ~12s, then calls the host's
8
+ * `summarize_session` tool (registered server-side by
9
+ * @agentproto/runtime's summarize-session-tool.ts, NOT part of this
10
+ * package — that tool is tied to the runtime's session ring-buffer/hot
11
+ * path) for each visible session to fetch the plain sentence + state.
12
+ */
13
+ export declare const AGENTS_OVERVIEW_HTML: string;
14
+ //# sourceMappingURL=panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/agents-overview/panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,eAAO,MAAM,oBAAoB,QAuJzB,CAAA"}
@@ -0,0 +1,306 @@
1
+ /**
2
+ * @agentproto/apps v0.1.0
3
+ * Ready-made agentproto apps (teams of agents + workflows) built on app-kit.
4
+ */
5
+
6
+ // src/panel-bridge.ts
7
+ function panelBridgeScript(appName) {
8
+ return `// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500
9
+ // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
10
+ var _nextId = 1, _pending = {}, _notifyHandlers = [];
11
+ var _hostContext = null, _hostContextHandlers = [];
12
+ function post(msg){ window.parent.postMessage(msg, '*'); }
13
+ function getHostContext(){ return _hostContext; }
14
+ function onHostContext(cb){
15
+ _hostContextHandlers.push(cb);
16
+ // Replay the last context so a late subscriber isn't stuck blind.
17
+ if (_hostContext){ try { cb(_hostContext); } catch(_) {} }
18
+ }
19
+ function _setHostContext(ctx){
20
+ if (!ctx || typeof ctx !== 'object') return;
21
+ // ui/notifications/host-context-changed carries only the changed keys \u2014
22
+ // merge, matching the official ext-apps App behaviour.
23
+ _hostContext = Object.assign({}, _hostContext || {}, ctx);
24
+ for (var i = 0; i < _hostContextHandlers.length; i++){
25
+ try { _hostContextHandlers[i](_hostContext); } catch(_) {}
26
+ }
27
+ }
28
+ function rpcRequest(method, params){
29
+ return new Promise(function(resolve, reject){
30
+ var id = _nextId++;
31
+ _pending[id] = {resolve: resolve, reject: reject};
32
+ post({jsonrpc: '2.0', id: id, method: method, params: params || {}});
33
+ });
34
+ }
35
+ function rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }
36
+ function onHostNotification(cb){ _notifyHandlers.push(cb); }
37
+ window.addEventListener('message', function(evt){
38
+ var msg = evt.data;
39
+ if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;
40
+ if (msg.id != null && msg.method == null){
41
+ var p = _pending[msg.id];
42
+ if (!p) return;
43
+ delete _pending[msg.id];
44
+ if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));
45
+ else p.resolve(msg.result);
46
+ return;
47
+ }
48
+ if (msg.method){
49
+ if (msg.method === 'ui/notifications/host-context-changed'){
50
+ _setHostContext(msg.params || {});
51
+ }
52
+ for (var i = 0; i < _notifyHandlers.length; i++){
53
+ try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}
54
+ }
55
+ }
56
+ });
57
+ function initBridge(){
58
+ return rpcRequest('ui/initialize', {
59
+ appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},
60
+ appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},
61
+ protocolVersion: '2026-01-26'
62
+ }).then(function(result){
63
+ // The initialize result carries the initial hostContext (displayMode +
64
+ // availableDisplayModes) \u2014 capture it before notifying the host.
65
+ if (result && result.hostContext) _setHostContext(result.hostContext);
66
+ rpcNotify('ui/notifications/initialized', {});
67
+ });
68
+ }
69
+ function requestDisplayMode(mode){
70
+ return rpcRequest('ui/request-display-mode', {mode: mode});
71
+ }
72
+ function callTool(name, args){
73
+ return rpcRequest('tools/call', {name: name, arguments: args || {}}).then(function(result){
74
+ if (result.isError){
75
+ var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';
76
+ throw new Error(e);
77
+ }
78
+ var text = (result.content && result.content[0] && result.content[0].text) || '{}';
79
+ return JSON.parse(text);
80
+ });
81
+ }
82
+
83
+ // \u2500\u2500 Display-mode toggle buttons (NO auto-request) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
84
+ // Injected by the shared bridge so every panel gets them without touching
85
+ // its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
86
+ // panel stays inline by default; the user expands on demand. Buttons only
87
+ // appear for modes the host advertises in hostContext.availableDisplayModes.
88
+ (function(){
89
+ function mount(){
90
+ var style = document.createElement('style');
91
+ style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
92
+ + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
93
+ + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
94
+ + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
95
+ + '#dm{right:8px}#pin{right:118px}'
96
+ + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
97
+ + '@media (prefers-color-scheme:dark){'
98
+ + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
99
+ + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
100
+ document.head.appendChild(style);
101
+
102
+ var btn = document.createElement('button');
103
+ btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
104
+ var pin = document.createElement('button');
105
+ pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
106
+ document.body.appendChild(pin);
107
+ document.body.appendChild(btn);
108
+
109
+ function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
110
+
111
+ // Re-sync button visibility + label from the current host context.
112
+ function syncBtn(ctx){
113
+ ctx = ctx || {};
114
+ var avail = ctx.availableDisplayModes;
115
+ // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
116
+ console.log('[mcp-app] displayMode=', ctx.displayMode,
117
+ 'availableDisplayModes=', avail);
118
+
119
+ // Fullscreen toggle button.
120
+ if (has(avail, 'fullscreen')){
121
+ btn.style.display = 'block';
122
+ btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
123
+ } else { btn.style.display = 'none'; }
124
+
125
+ // Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
126
+ if (has(avail, 'pip')){
127
+ pin.style.display = 'block';
128
+ pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
129
+ } else { pin.style.display = 'none'; }
130
+ }
131
+
132
+ onHostContext(syncBtn);
133
+
134
+ btn.addEventListener('click', function(){
135
+ var ctx = getHostContext() || {};
136
+ var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
137
+ requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
138
+ });
139
+
140
+ pin.addEventListener('click', function(){
141
+ var ctx = getHostContext() || {};
142
+ requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
143
+ });
144
+ }
145
+ if (document.body) mount();
146
+ else document.addEventListener('DOMContentLoaded', mount);
147
+ })();`;
148
+ }
149
+
150
+ // src/agents-overview/panel.ts
151
+ var AGENTS_OVERVIEW_HTML = `<!DOCTYPE html>
152
+ <html lang="fr">
153
+ <head>
154
+ <meta charset="UTF-8">
155
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
156
+ <title>agents \u2014 vue claire</title>
157
+ <style>
158
+ *{box-sizing:border-box;margin:0;padding:0}
159
+ :root{
160
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
161
+ --text:#e6edf3;--text2:#8b949e;
162
+ --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
163
+ }
164
+ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
165
+ #app{display:flex;flex-direction:column;height:100%}
166
+ #hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}
167
+ #hdr h1{font-size:13px;font-weight:600;letter-spacing:.02em}
168
+ #hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}
169
+ #refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}
170
+ #refresh-btn:hover{color:var(--text);background:var(--bg3)}
171
+ #grid{flex:1;overflow-y:auto;padding:14px 16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;align-content:start}
172
+ .card{background:var(--bg2);border:1px solid var(--border);border-left:3px solid var(--border);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;min-height:96px}
173
+ .card.s-todo{border-left-color:var(--purple)}
174
+ .card.s-work{border-left-color:var(--green)}
175
+ .card.s-wait{border-left-color:var(--yellow)}
176
+ .card.s-done{border-left-color:var(--text2)}
177
+ .card-top{display:flex;align-items:center;justify-content:space-between;gap:8px}
178
+ .title{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
179
+ .state{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap;letter-spacing:.02em}
180
+ .state.s-todo{background:rgba(188,140,255,.16);color:var(--purple)}
181
+ .state.s-work{background:rgba(63,185,80,.16);color:var(--green)}
182
+ .state.s-wait{background:rgba(210,153,34,.16);color:var(--yellow)}
183
+ .state.s-done{background:rgba(139,148,158,.14);color:var(--text2)}
184
+ .summary{font-size:12.5px;line-height:1.5;color:var(--text);word-break:break-word}
185
+ .summary.muted{color:var(--text2);font-style:italic}
186
+ .meta{font-size:10.5px;color:var(--text2);display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto}
187
+ .dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}
188
+ #empty{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}
189
+ #empty .ico{font-size:30px}
190
+ #statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
191
+ </style>
192
+ </head>
193
+ <body>
194
+ <div id="app">
195
+ <div id="hdr">
196
+ <div>
197
+ <h1>Agents \u2014 vue claire</h1>
198
+ <div class="sub">une carte par agent \xB7 r\xE9sum\xE9 + \xE9tat</div>
199
+ </div>
200
+ <button id="refresh-btn" title="Rafra\xEEchir" onclick="doRefresh()">&#8635;</button>
201
+ </div>
202
+ <div id="grid"><div id="empty"><div class="ico">&#9889;</div><div>Connexion au bridge&#8230;</div></div></div>
203
+ <div id="statusbar">Connexion&#8230;</div>
204
+ </div>
205
+ <script>
206
+ ${panelBridgeScript("agentproto-agents-overview")}
207
+
208
+ // \u2500\u2500 State \u2500\u2500
209
+ var REFRESH_MS = 12000;
210
+ var pollTimer = null, polling = false;
211
+ function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
212
+ function setStatus(m){ document.getElementById('statusbar').textContent = m; }
213
+
214
+ var STATE_CLASS = {
215
+ '\xE0 traiter':'s-todo', 'au travail':'s-work', 'en attente':'s-wait', 'termin\xE9':'s-done'
216
+ };
217
+ function stateClass(st){ return STATE_CLASS[st] || 's-wait'; }
218
+
219
+ function fmtAgo(iso){
220
+ if (!iso) return '';
221
+ var t = Date.parse(iso);
222
+ if (isNaN(t)) return '';
223
+ var s = Math.max(0, Math.round((Date.now() - t) / 1000));
224
+ if (s < 60) return 'il y a ' + s + 's';
225
+ var m = Math.round(s/60);
226
+ if (m < 60) return 'il y a ' + m + 'min';
227
+ var h = Math.round(m/60);
228
+ return 'il y a ' + h + 'h';
229
+ }
230
+
231
+ function titleOf(s){
232
+ return s.label || s.name || (s.command ? s.command.split(/\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);
233
+ }
234
+
235
+ function render(sessions, summaries){
236
+ var grid = document.getElementById('grid');
237
+ if (!sessions.length){
238
+ grid.innerHTML = '<div id="empty"><div class="ico">&#128564;</div><div>Aucune session d\\'agent</div></div>';
239
+ return;
240
+ }
241
+ var html = '';
242
+ for (var i=0;i<sessions.length;i++){
243
+ var s = sessions[i];
244
+ var sum = summaries[s.id] || {};
245
+ var st = sum.state || 'en attente';
246
+ var cls = stateClass(st);
247
+ var summary = sum.summary || 'R\xE9sum\xE9 indisponible.';
248
+ var muted = !sum.summary ? ' muted' : '';
249
+ html += '<div class="card ' + cls + '">'
250
+ + '<div class="card-top">'
251
+ + '<span class="title">' + esc(titleOf(s)) + '</span>'
252
+ + '<span class="state ' + cls + '">' + esc(st) + '</span>'
253
+ + '</div>'
254
+ + '<div class="summary' + muted + '">' + esc(summary) + '</div>'
255
+ + '<div class="meta">'
256
+ + '<span>' + esc(s.kind || '') + '</span><span class="dot"></span>'
257
+ + '<span>' + esc(s.status || '') + '</span>'
258
+ + (s.lastOutputAt ? '<span class="dot"></span><span>' + esc(fmtAgo(s.lastOutputAt)) + '</span>' : '')
259
+ + '</div>'
260
+ + '</div>';
261
+ }
262
+ grid.innerHTML = html;
263
+ }
264
+
265
+ function loadAndRender(){
266
+ return callTool('session_list', {kind:'all'}).then(function(data){
267
+ var all = data.sessions || [];
268
+ var agents = all.filter(function(s){ return s.kind === 'agent-cli'; });
269
+ // Render shells immediately, then fill summaries as they arrive.
270
+ var summaries = {};
271
+ render(agents, summaries);
272
+ setStatus(agents.length + ' agent' + (agents.length===1?'':'s') + ' \xB7 ' + new Date().toLocaleTimeString('fr-FR'));
273
+ return Promise.all(agents.map(function(s){
274
+ return callTool('summarize_session', {sessionId: s.id}).then(function(sum){
275
+ summaries[s.id] = sum;
276
+ }).catch(function(){ /* leave shell */ });
277
+ })).then(function(){ render(agents, summaries); });
278
+ }).catch(function(e){ setStatus('Erreur : ' + e.message); });
279
+ }
280
+
281
+ function doPoll(){
282
+ if (polling) return;
283
+ polling = true;
284
+ loadAndRender().then(function(){
285
+ polling = false;
286
+ pollTimer = setTimeout(doPoll, REFRESH_MS);
287
+ }).catch(function(){
288
+ polling = false;
289
+ pollTimer = setTimeout(doPoll, REFRESH_MS);
290
+ });
291
+ }
292
+ function doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }
293
+
294
+ initBridge().then(loadAndRender).then(function(){
295
+ pollTimer = setTimeout(doPoll, REFRESH_MS);
296
+ }).catch(function(e){
297
+ setStatus('Bridge : ' + e.message);
298
+ document.getElementById('grid').innerHTML = '<div id="empty"><div class="ico">&#9888;</div><div>\xC9chec connexion bridge : ' + esc(e.message) + '</div></div>';
299
+ });
300
+ </script>
301
+ </body>
302
+ </html>`;
303
+
304
+ export { AGENTS_OVERVIEW_HTML };
305
+ //# sourceMappingURL=panel.mjs.map
306
+ //# sourceMappingURL=panel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/panel-bridge.ts","../../src/agents-overview/panel.ts"],"names":[],"mappings":";;;;;;AAqCO,SAAS,kBAAkB,OAAA,EAAyB;AACzD,EAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAA,EAmDa,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAAA,CAAA;AAyF7C;;;ACjKO,IAAM,oBAAA,GAAuB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuDlC,iBAAA,CAAkB,4BAA4B,CAAC;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA","file":"panel.mjs","sourcesContent":["/**\n * Shared MCP Apps client bridge for agentproto panels.\n *\n * One spec-correct copy of the JSON-RPC-over-postMessage bridge that every\n * ui:// panel embeds in its <script> block. Mirrors the behaviour of\n * @agstudio/mcp-apps' mcpAppClientScript() — which we cannot import here:\n * agentproto is a standalone pnpm workspace with no @agstudio/* dependency\n * (same isolation invariant as mcp-apps-adapter.ts).\n *\n * Handshake (MCP Apps ext spec 2026-01-26, ext-apps McpUiInitializeRequest):\n * params REQUIRE `appInfo` (Implementation: {name, version}),\n * `appCapabilities`, and `protocolVersion`. Sending `clientInfo` instead\n * of `appInfo` fails the host's schema validation — hosts either reply\n * with an error or silently drop the request, so the panel's initBridge()\n * promise never resolves and the panel renders blank while every\n * server-side surface (tools, resources) works. That exact bug shipped in\n * 4 copy-pasted bridges; this module is the single corrected source.\n *\n * The script defines, in panel scope:\n * post / rpcRequest / rpcNotify — raw JSON-RPC plumbing\n * onHostNotification(cb) — subscribe to host-pushed\n * notifications (method, params)\n * initBridge() — ui/initialize → ui/notifications/initialized\n * callTool(name, args) — tools/call + JSON text unwrap\n * getHostContext() / onHostContext(cb) — host context (displayMode, theme, …),\n * captured from the initialize result and\n * merged on ui/notifications/host-context-changed\n * requestDisplayMode(mode) — ui/request-display-mode (inline|fullscreen|pip)\n *\n * Display-mode toggle: the bridge also injects two floating buttons\n * (fullscreen + pip) in the top-right corner of every panel, shown only for\n * the modes the host actually advertises via hostContext.availableDisplayModes\n * — same pattern as guilde's canvas.app.ts canvasShellHtml() (the reference\n * implementation that renders correctly in Claude hosts). No auto-request:\n * panels stay inline until the user clicks.\n */\n\nexport function panelBridgeScript(appName: string): string {\n return `// ── MCP Apps bridge (shared: panel-bridge.ts) ──\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys —\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n return rpcRequest('ui/initialize', {\n appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) — capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction callTool(name, args){\n return rpcRequest('tools/call', {name: name, arguments: args || {}}).then(function(result){\n if (result.isError){\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\n throw new Error(e);\n }\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\n return JSON.parse(text);\n });\n}\n\n// ── Display-mode toggle buttons (NO auto-request) ──────────────────────\n// Injected by the shared bridge so every panel gets them without touching\n// its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the\n// panel stays inline by default; the user expands on demand. Buttons only\n// appear for modes the host advertises in hostContext.availableDisplayModes.\n(function(){\n function mount(){\n var style = document.createElement('style');\n style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'\n + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'\n + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'\n + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'\n + '#dm{right:8px}#pin{right:118px}'\n + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'\n + '@media (prefers-color-scheme:dark){'\n + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'\n + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';\n document.head.appendChild(style);\n\n var btn = document.createElement('button');\n btn.id = 'dm'; btn.type = 'button'; btn.title = \"Basculer l'affichage\";\n var pin = document.createElement('button');\n pin.id = 'pin'; pin.type = 'button'; pin.title = 'Épingler sur le côté (pip)';\n document.body.appendChild(pin);\n document.body.appendChild(btn);\n\n function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }\n\n // Re-sync button visibility + label from the current host context.\n function syncBtn(ctx){\n ctx = ctx || {};\n var avail = ctx.availableDisplayModes;\n // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)\n console.log('[mcp-app] displayMode=', ctx.displayMode,\n 'availableDisplayModes=', avail);\n\n // Fullscreen toggle button.\n if (has(avail, 'fullscreen')){\n btn.style.display = 'block';\n btn.textContent = (ctx.displayMode === 'fullscreen') ? '⤡ Réduire' : '⤢ Agrandir';\n } else { btn.style.display = 'none'; }\n\n // Dedicated pip (\"pinned on side\") button — only if the host advertises pip.\n if (has(avail, 'pip')){\n pin.style.display = 'block';\n pin.textContent = (ctx.displayMode === 'pip') ? '⤡ Détacher' : '📌 Épingler';\n } else { pin.style.display = 'none'; }\n }\n\n onHostContext(syncBtn);\n\n btn.addEventListener('click', function(){\n var ctx = getHostContext() || {};\n var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');\n requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});\n });\n\n pin.addEventListener('click', function(){\n var ctx = getHostContext() || {};\n requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});\n });\n }\n if (document.body) mount();\n else document.addEventListener('DOMContentLoaded', mount);\n})();`\n}\n","/**\n * HTML bundle for the agentproto \"agents — vue claire\" panel — the\n * `AGENTS_OVERVIEW_HTML` served as the `ui://agentproto_agents_overview/view`\n * resource. Mounted via the `agentproto_agents_overview` tool defined in\n * ./index.ts.\n *\n * The panel polls `session_list` every ~12s, then calls the host's\n * `summarize_session` tool (registered server-side by\n * @agentproto/runtime's summarize-session-tool.ts, NOT part of this\n * package — that tool is tied to the runtime's session ring-buffer/hot\n * path) for each visible session to fetch the plain sentence + state.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\n// ── HTML bundle (zero CDN, CSP-safe inline) ──────────────────────────────────\n\nexport const AGENTS_OVERVIEW_HTML = `<!DOCTYPE html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agents — vue claire</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;flex-direction:column;height:100%}\n#hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}\n#hdr h1{font-size:13px;font-weight:600;letter-spacing:.02em}\n#hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#grid{flex:1;overflow-y:auto;padding:14px 16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;align-content:start}\n.card{background:var(--bg2);border:1px solid var(--border);border-left:3px solid var(--border);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;min-height:96px}\n.card.s-todo{border-left-color:var(--purple)}\n.card.s-work{border-left-color:var(--green)}\n.card.s-wait{border-left-color:var(--yellow)}\n.card.s-done{border-left-color:var(--text2)}\n.card-top{display:flex;align-items:center;justify-content:space-between;gap:8px}\n.title{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}\n.state{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap;letter-spacing:.02em}\n.state.s-todo{background:rgba(188,140,255,.16);color:var(--purple)}\n.state.s-work{background:rgba(63,185,80,.16);color:var(--green)}\n.state.s-wait{background:rgba(210,153,34,.16);color:var(--yellow)}\n.state.s-done{background:rgba(139,148,158,.14);color:var(--text2)}\n.summary{font-size:12.5px;line-height:1.5;color:var(--text);word-break:break-word}\n.summary.muted{color:var(--text2);font-style:italic}\n.meta{font-size:10.5px;color:var(--text2);display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto}\n.dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}\n#empty{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}\n#empty .ico{font-size:30px}\n#statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"hdr\">\n <div>\n <h1>Agents — vue claire</h1>\n <div class=\"sub\">une carte par agent · résumé + état</div>\n </div>\n <button id=\"refresh-btn\" title=\"Rafraîchir\" onclick=\"doRefresh()\">&#8635;</button>\n </div>\n <div id=\"grid\"><div id=\"empty\"><div class=\"ico\">&#9889;</div><div>Connexion au bridge&#8230;</div></div></div>\n <div id=\"statusbar\">Connexion&#8230;</div>\n</div>\n<script>\n${panelBridgeScript(\"agentproto-agents-overview\")}\n\n// ── State ──\nvar REFRESH_MS = 12000;\nvar pollTimer = null, polling = false;\nfunction esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\nfunction setStatus(m){ document.getElementById('statusbar').textContent = m; }\n\nvar STATE_CLASS = {\n 'à traiter':'s-todo', 'au travail':'s-work', 'en attente':'s-wait', 'terminé':'s-done'\n};\nfunction stateClass(st){ return STATE_CLASS[st] || 's-wait'; }\n\nfunction fmtAgo(iso){\n if (!iso) return '';\n var t = Date.parse(iso);\n if (isNaN(t)) return '';\n var s = Math.max(0, Math.round((Date.now() - t) / 1000));\n if (s < 60) return 'il y a ' + s + 's';\n var m = Math.round(s/60);\n if (m < 60) return 'il y a ' + m + 'min';\n var h = Math.round(m/60);\n return 'il y a ' + h + 'h';\n}\n\nfunction titleOf(s){\n return s.label || s.name || (s.command ? s.command.split(/\\\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);\n}\n\nfunction render(sessions, summaries){\n var grid = document.getElementById('grid');\n if (!sessions.length){\n grid.innerHTML = '<div id=\"empty\"><div class=\"ico\">&#128564;</div><div>Aucune session d\\\\'agent</div></div>';\n return;\n }\n var html = '';\n for (var i=0;i<sessions.length;i++){\n var s = sessions[i];\n var sum = summaries[s.id] || {};\n var st = sum.state || 'en attente';\n var cls = stateClass(st);\n var summary = sum.summary || 'Résumé indisponible.';\n var muted = !sum.summary ? ' muted' : '';\n html += '<div class=\"card ' + cls + '\">'\n + '<div class=\"card-top\">'\n + '<span class=\"title\">' + esc(titleOf(s)) + '</span>'\n + '<span class=\"state ' + cls + '\">' + esc(st) + '</span>'\n + '</div>'\n + '<div class=\"summary' + muted + '\">' + esc(summary) + '</div>'\n + '<div class=\"meta\">'\n + '<span>' + esc(s.kind || '') + '</span><span class=\"dot\"></span>'\n + '<span>' + esc(s.status || '') + '</span>'\n + (s.lastOutputAt ? '<span class=\"dot\"></span><span>' + esc(fmtAgo(s.lastOutputAt)) + '</span>' : '')\n + '</div>'\n + '</div>';\n }\n grid.innerHTML = html;\n}\n\nfunction loadAndRender(){\n return callTool('session_list', {kind:'all'}).then(function(data){\n var all = data.sessions || [];\n var agents = all.filter(function(s){ return s.kind === 'agent-cli'; });\n // Render shells immediately, then fill summaries as they arrive.\n var summaries = {};\n render(agents, summaries);\n setStatus(agents.length + ' agent' + (agents.length===1?'':'s') + ' · ' + new Date().toLocaleTimeString('fr-FR'));\n return Promise.all(agents.map(function(s){\n return callTool('summarize_session', {sessionId: s.id}).then(function(sum){\n summaries[s.id] = sum;\n }).catch(function(){ /* leave shell */ });\n })).then(function(){ render(agents, summaries); });\n }).catch(function(e){ setStatus('Erreur : ' + e.message); });\n}\n\nfunction doPoll(){\n if (polling) return;\n polling = true;\n loadAndRender().then(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n }).catch(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n });\n}\nfunction doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }\n\ninitBridge().then(loadAndRender).then(function(){\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n}).catch(function(e){\n setStatus('Bridge : ' + e.message);\n document.getElementById('grid').innerHTML = '<div id=\"empty\"><div class=\"ico\">&#9888;</div><div>Échec connexion bridge : ' + esc(e.message) + '</div></div>';\n});\n</script>\n</body>\n</html>`\n"]}