@agentproto/apps 0.9.3 → 0.10.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.
@@ -0,0 +1,464 @@
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/work-board/panel.ts
151
+ var WORK_BOARD_HTML = `<!DOCTYPE html>
152
+ <html lang="en">
153
+ <head>
154
+ <meta charset="UTF-8">
155
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
156
+ <title>agentproto work board</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:Menlo,Monaco,'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
165
+ #app{display:flex;flex-direction:column;height:100%}
166
+ #toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2);flex-shrink:0}
167
+ #board-label{font-size:11px;color:var(--text2)}
168
+ #board-id{font-weight:600;color:var(--text);font-size:12px}
169
+ #board-input{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 8px;border-radius:4px;font-size:12px;font-family:inherit;width:220px}
170
+ .tbtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}
171
+ .tbtn:hover{background:var(--border)}
172
+ #new-title{flex:1;min-width:120px;background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 8px;border-radius:4px;font-size:12px;font-family:inherit}
173
+ #columns{flex:1;display:flex;overflow-x:auto;overflow-y:hidden}
174
+ .col{width:260px;min-width:220px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border)}
175
+ .col-hdr{padding:8px 10px;border-bottom:1px solid var(--border);background:var(--bg2);font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.05em;display:flex;justify-content:space-between}
176
+ .col-body{flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:8px}
177
+ .card{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:8px 10px}
178
+ .c-title{font-size:12px;font-weight:500;margin-bottom:4px;word-break:break-word}
179
+ .c-id{font-size:10px;color:var(--text2);margin-bottom:6px}
180
+ .c-row{display:flex;flex-wrap:wrap;gap:4px;align-items:center;margin-bottom:6px}
181
+ .tag{display:inline-block;padding:1px 6px;border-radius:10px;font-size:10px;font-weight:600;white-space:nowrap}
182
+ .t-unclaimed{background:rgba(139,148,158,.15);color:var(--text2);border:1px dashed var(--text2)}
183
+ .t-owner{background:rgba(88,166,255,.15);color:var(--blue)}
184
+ .t-gate{background:rgba(63,185,80,.18);color:var(--green)}
185
+ .t-self{background:rgba(210,153,34,.18);color:var(--yellow)}
186
+ .t-human{background:rgba(88,166,255,.18);color:var(--blue)}
187
+ .t-gated{background:rgba(139,148,158,.12);color:var(--text2)}
188
+ .t-cancelled{background:rgba(139,148,158,.12);color:var(--text2)}
189
+ .c-err{font-size:10px;color:var(--red);margin-bottom:6px;word-break:break-word}
190
+ .c-actions{display:flex;flex-wrap:wrap;gap:4px}
191
+ .abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px;font-family:inherit}
192
+ .abtn:hover{background:var(--border)}
193
+ .abtn.danger{border-color:var(--red);color:var(--red)}
194
+ .abtn.primary{border-color:var(--green);color:var(--green)}
195
+ #statusbar{padding:4px 12px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
196
+ .empty{padding:16px 8px;color:var(--text2);font-size:11px;text-align:center}
197
+ </style>
198
+ </head>
199
+ <body>
200
+ <div id="app">
201
+ <div id="toolbar">
202
+ <span id="board-label">Board:</span>
203
+ <span id="board-id">\u2014</span>
204
+ <input id="board-input" type="text" placeholder="tree:&lt;sessionId&gt; or ws:&lt;slug&gt;">
205
+ <button class="tbtn" id="go-btn">Go</button>
206
+ <button class="tbtn" id="refresh-btn" title="Refresh">&#8635;</button>
207
+ <input id="new-title" type="text" placeholder="New task title&#8230;">
208
+ <button class="tbtn" id="add-btn">+ Add</button>
209
+ </div>
210
+ <div id="columns"></div>
211
+ <div id="statusbar">Connecting to bridge&#8230;</div>
212
+ </div>
213
+ <script>
214
+ ${panelBridgeScript("agentproto-work-board-panel")}
215
+
216
+ // ============================================================
217
+ // State
218
+ // ============================================================
219
+
220
+ var COLUMNS = [
221
+ {key: 'pending', label: 'Pending'},
222
+ {key: 'in_progress', label: 'In Progress'},
223
+ {key: 'done', label: 'Done'},
224
+ {key: 'failed', label: 'Failed'}
225
+ ];
226
+
227
+ var boardId = null;
228
+ var tasks = [];
229
+ var pollTimer = null;
230
+ var pollActive = false;
231
+
232
+ function setStatus(msg) {
233
+ document.getElementById('statusbar').textContent = msg;
234
+ }
235
+
236
+ function esc(s) {
237
+ return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
238
+ }
239
+
240
+ // A cancelled task folds into the Failed column, tagged distinctly \u2014 v1
241
+ // treats both as terminal scrap, and a fifth column for a rarely-used
242
+ // status isn't worth the width.
243
+ function columnOf(status) {
244
+ if (status === 'cancelled') return 'failed';
245
+ return status;
246
+ }
247
+
248
+ // ============================================================
249
+ // Verification tell \u2014 never render an unverified done as gate-passed.
250
+ // ============================================================
251
+
252
+ function verificationTag(t) {
253
+ var v = t.verification;
254
+ if (v) {
255
+ if (v.kind === 'gate') return '<span class="tag t-gate">&#10003; gate</span>';
256
+ if (v.kind === 'self-report') return '<span class="tag t-self">self-report</span>';
257
+ if (v.kind === 'human') return '<span class="tag t-human">human</span>';
258
+ }
259
+ if (t.status === 'in_progress' && t.verify) {
260
+ return '<span class="tag t-gated">gated</span>';
261
+ }
262
+ return '';
263
+ }
264
+
265
+ function ownerTag(t) {
266
+ if (!t.owner) return '<span class="tag t-unclaimed">Unclaimed</span>';
267
+ return '<span class="tag t-owner">' + esc(t.owner) + '</span>';
268
+ }
269
+
270
+ // ============================================================
271
+ // Card actions \u2014 explicit status moves, no drag-and-drop (read-only-safe
272
+ // first: see module docblock).
273
+ // ============================================================
274
+
275
+ function actionsFor(t) {
276
+ var a = [];
277
+ if (t.status === 'pending') {
278
+ if (!t.owner) a.push({label: 'Claim', action: 'claim', cls: 'primary'});
279
+ else a.push({label: 'Start', action: 'start', cls: 'primary'});
280
+ a.push({label: 'Cancel', action: 'cancel', cls: 'danger'});
281
+ } else if (t.status === 'in_progress') {
282
+ a.push({label: 'Done', action: 'done', cls: 'primary'});
283
+ a.push({label: 'Fail', action: 'fail', cls: 'danger'});
284
+ a.push({label: 'Release', action: 'release', cls: ''});
285
+ a.push({label: 'Cancel', action: 'cancel', cls: 'danger'});
286
+ } else if (t.status === 'done') {
287
+ a.push({label: 'Reopen', action: 'reopen', cls: ''});
288
+ }
289
+ return a;
290
+ }
291
+
292
+ function applyAction(taskId, action) {
293
+ var t = null;
294
+ for (var i = 0; i < tasks.length; i++) {
295
+ if (tasks[i].taskId === taskId) { t = tasks[i]; break; }
296
+ }
297
+ if (!t) return;
298
+ var p;
299
+ if (action === 'claim') {
300
+ p = callTool('task_claim', {taskId: t.taskId, rev: t.rev});
301
+ } else if (action === 'start') {
302
+ p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'in_progress'});
303
+ } else if (action === 'done') {
304
+ p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'done'});
305
+ } else if (action === 'fail') {
306
+ p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'failed'});
307
+ } else if (action === 'release') {
308
+ p = callTool('task_update', {taskId: t.taskId, rev: t.rev, owner: null});
309
+ } else if (action === 'cancel') {
310
+ p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'cancelled'});
311
+ } else if (action === 'reopen') {
312
+ p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'pending'});
313
+ } else {
314
+ return;
315
+ }
316
+ p.then(function (result) {
317
+ if (result && result.conflict) {
318
+ setStatus('Someone else moved "' + t.title + '" first \u2014 refreshed.');
319
+ } else if (result && result.error) {
320
+ setStatus('Action failed: ' + result.error);
321
+ } else {
322
+ setStatus('Updated "' + t.title + '".');
323
+ }
324
+ return loadBoard();
325
+ }).catch(function (e) {
326
+ setStatus('Action failed: ' + e.message);
327
+ });
328
+ }
329
+
330
+ // ============================================================
331
+ // Render
332
+ // ============================================================
333
+
334
+ function renderCard(t) {
335
+ var html = '<div class="card" data-taskid="' + esc(t.taskId) + '">';
336
+ html += '<div class="c-title">' + esc(t.title) + '</div>';
337
+ html += '<div class="c-id">#' + esc(t.taskId) + '</div>';
338
+ html += '<div class="c-row">' + ownerTag(t) + verificationTag(t);
339
+ if (t.status === 'cancelled') html += '<span class="tag t-cancelled">cancelled</span>';
340
+ html += '</div>';
341
+ if (t.lastVerifyError) {
342
+ html += '<div class="c-err">verify failed: ' + esc(t.lastVerifyError) + '</div>';
343
+ }
344
+ var actions = actionsFor(t);
345
+ if (actions.length > 0) {
346
+ html += '<div class="c-actions">';
347
+ for (var i = 0; i < actions.length; i++) {
348
+ var a = actions[i];
349
+ html += '<button class="abtn ' + a.cls + '" data-taskid="' + esc(t.taskId) + '" data-action="' + a.action + '">' + a.label + '</button>';
350
+ }
351
+ html += '</div>';
352
+ }
353
+ html += '</div>';
354
+ return html;
355
+ }
356
+
357
+ function render() {
358
+ document.getElementById('board-id').textContent = boardId || '\u2014';
359
+ if (!document.getElementById('board-input').value) {
360
+ document.getElementById('board-input').value = boardId || '';
361
+ }
362
+ var el = document.getElementById('columns');
363
+ var html = '';
364
+ for (var c = 0; c < COLUMNS.length; c++) {
365
+ var col = COLUMNS[c];
366
+ var rows = tasks.filter(function (t) { return columnOf(t.status) === col.key; });
367
+ html += '<div class="col"><div class="col-hdr"><span>' + col.label + '</span><span>' + rows.length + '</span></div><div class="col-body">';
368
+ if (rows.length === 0) {
369
+ html += '<div class="empty">No tasks</div>';
370
+ } else {
371
+ for (var i = 0; i < rows.length; i++) html += renderCard(rows[i]);
372
+ }
373
+ html += '</div></div>';
374
+ }
375
+ el.innerHTML = html;
376
+ }
377
+
378
+ // ============================================================
379
+ // Data
380
+ // ============================================================
381
+
382
+ function loadBoard() {
383
+ var args = {includeClosed: true, full: true};
384
+ if (boardId) args.boardId = boardId;
385
+ return callTool('task_list', args).then(function (data) {
386
+ boardId = data.boardId || boardId;
387
+ tasks = data.tasks || [];
388
+ render();
389
+ setStatus(tasks.length + ' task' + (tasks.length === 1 ? '' : 's') + ' \xB7 ' + new Date().toLocaleTimeString());
390
+ }).catch(function (e) {
391
+ setStatus('Error: ' + e.message);
392
+ });
393
+ }
394
+
395
+ function doPoll() {
396
+ if (pollActive) return;
397
+ pollActive = true;
398
+ loadBoard().then(function () {
399
+ pollActive = false;
400
+ pollTimer = setTimeout(doPoll, 4000);
401
+ }).catch(function () {
402
+ pollActive = false;
403
+ pollTimer = setTimeout(doPoll, 4000);
404
+ });
405
+ }
406
+
407
+ // ============================================================
408
+ // Toolbar wiring
409
+ // ============================================================
410
+
411
+ document.getElementById('columns').addEventListener('click', function (evt) {
412
+ var btn = evt.target.closest ? evt.target.closest('button[data-action]') : null;
413
+ if (!btn) return;
414
+ applyAction(btn.getAttribute('data-taskid'), btn.getAttribute('data-action'));
415
+ });
416
+
417
+ document.getElementById('refresh-btn').addEventListener('click', function () {
418
+ loadBoard();
419
+ });
420
+
421
+ document.getElementById('go-btn').addEventListener('click', function () {
422
+ var v = document.getElementById('board-input').value.trim();
423
+ if (!v) return;
424
+ boardId = v;
425
+ loadBoard();
426
+ });
427
+
428
+ document.getElementById('add-btn').addEventListener('click', function () {
429
+ var input = document.getElementById('new-title');
430
+ var title = input.value.trim();
431
+ if (!title) return;
432
+ var args = {title: title};
433
+ if (boardId) args.boardId = boardId;
434
+ callTool('task_create', args).then(function (result) {
435
+ if (result && result.error) {
436
+ setStatus('Create failed: ' + result.error);
437
+ return;
438
+ }
439
+ input.value = '';
440
+ setStatus('Created "' + title + '".');
441
+ return loadBoard();
442
+ }).catch(function (e) {
443
+ setStatus('Create failed: ' + e.message);
444
+ });
445
+ });
446
+
447
+ // ============================================================
448
+ // Boot
449
+ // ============================================================
450
+
451
+ initBridge().then(function () {
452
+ return loadBoard();
453
+ }).then(function () {
454
+ pollTimer = setTimeout(doPoll, 4000);
455
+ }).catch(function (e) {
456
+ setStatus('Bridge error: ' + e.message);
457
+ });
458
+ </script>
459
+ </body>
460
+ </html>`;
461
+
462
+ export { WORK_BOARD_HTML };
463
+ //# sourceMappingURL=panel.mjs.map
464
+ //# sourceMappingURL=panel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/panel-bridge.ts","../../src/work-board/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;;;AC1IO,IAAM,eAAA,GAAkB,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,EA+D7B,iBAAA,CAAkB,6BAA6B,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;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;;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 work-board panel — the `WORK_BOARD_HTML`\n * served as the `ui://agentproto_work_board/view` resource by\n * `registerMcpApps` (see ../mcp-app-types.ts and runtime's\n * mcp-apps-adapter.ts). Mounted via the `agentproto_work_board` tool defined\n * in ./index.ts.\n *\n * A kanban over the daemon's Task ledger (`packages/runtime/src/\n * task-ledger.ts`) — the one writable multi-party entity the daemon exposes\n * (Activity is a read-only projection; a session has a lifecycle, not an\n * intention). Columns are the ledger's own status enum, folding `cancelled`\n * into the `failed` column (tagged distinctly — see `columnOf` below)\n * rather than adding a fifth column for a status v1 treats as terminal\n * scrap. The board/swimlane selector is the ledger's own board id\n * (`tree:<rootSessionId>` or `ws:<slug>`) — always shown verbatim, never\n * hidden behind a friendly name, per the ledger's scoping model.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → task_list (`full: true` — the compact projection\n * drops `verification`, which the verification tell needs) on a ~4 s poll\n * – Actions: tools/call → task_claim / task_update / task_create\n *\n * Read-only-safe first: no drag-and-drop. Each card gets the explicit status\n * actions valid from its current state (Claim/Start/Done/Fail/Release/\n * Cancel/Reopen) — a correct read-only-plus-explicit-actions board beats a\n * half-wired drag, per the brief.\n *\n * The verification tell (never render an unverified done as gate-passed):\n * - `verification.kind === \"gate\"` → solid green \"✓ gate\"\n * - `verification.kind === \"self-report\"` → amber outline \"self-report\"\n * - `verification.kind === \"human\"` → blue \"human\"\n * - a declared `verify` with no verification yet → grey \"gated\" tag\n * An owner-less task always reads \"Unclaimed\", never \"pending\" (that word is\n * reserved for the Activity vocabulary's \"blocked\" meaning elsewhere).\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\nexport const WORK_BOARD_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto work board</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:Menlo,Monaco,'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;flex-direction:column;height:100%}\n#toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2);flex-shrink:0}\n#board-label{font-size:11px;color:var(--text2)}\n#board-id{font-weight:600;color:var(--text);font-size:12px}\n#board-input{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 8px;border-radius:4px;font-size:12px;font-family:inherit;width:220px}\n.tbtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}\n.tbtn:hover{background:var(--border)}\n#new-title{flex:1;min-width:120px;background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 8px;border-radius:4px;font-size:12px;font-family:inherit}\n#columns{flex:1;display:flex;overflow-x:auto;overflow-y:hidden}\n.col{width:260px;min-width:220px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border)}\n.col-hdr{padding:8px 10px;border-bottom:1px solid var(--border);background:var(--bg2);font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.05em;display:flex;justify-content:space-between}\n.col-body{flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:8px}\n.card{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:8px 10px}\n.c-title{font-size:12px;font-weight:500;margin-bottom:4px;word-break:break-word}\n.c-id{font-size:10px;color:var(--text2);margin-bottom:6px}\n.c-row{display:flex;flex-wrap:wrap;gap:4px;align-items:center;margin-bottom:6px}\n.tag{display:inline-block;padding:1px 6px;border-radius:10px;font-size:10px;font-weight:600;white-space:nowrap}\n.t-unclaimed{background:rgba(139,148,158,.15);color:var(--text2);border:1px dashed var(--text2)}\n.t-owner{background:rgba(88,166,255,.15);color:var(--blue)}\n.t-gate{background:rgba(63,185,80,.18);color:var(--green)}\n.t-self{background:rgba(210,153,34,.18);color:var(--yellow)}\n.t-human{background:rgba(88,166,255,.18);color:var(--blue)}\n.t-gated{background:rgba(139,148,158,.12);color:var(--text2)}\n.t-cancelled{background:rgba(139,148,158,.12);color:var(--text2)}\n.c-err{font-size:10px;color:var(--red);margin-bottom:6px;word-break:break-word}\n.c-actions{display:flex;flex-wrap:wrap;gap:4px}\n.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px;font-family:inherit}\n.abtn:hover{background:var(--border)}\n.abtn.danger{border-color:var(--red);color:var(--red)}\n.abtn.primary{border-color:var(--green);color:var(--green)}\n#statusbar{padding:4px 12px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n.empty{padding:16px 8px;color:var(--text2);font-size:11px;text-align:center}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"toolbar\">\n <span id=\"board-label\">Board:</span>\n <span id=\"board-id\">—</span>\n <input id=\"board-input\" type=\"text\" placeholder=\"tree:&lt;sessionId&gt; or ws:&lt;slug&gt;\">\n <button class=\"tbtn\" id=\"go-btn\">Go</button>\n <button class=\"tbtn\" id=\"refresh-btn\" title=\"Refresh\">&#8635;</button>\n <input id=\"new-title\" type=\"text\" placeholder=\"New task title&#8230;\">\n <button class=\"tbtn\" id=\"add-btn\">+ Add</button>\n </div>\n <div id=\"columns\"></div>\n <div id=\"statusbar\">Connecting to bridge&#8230;</div>\n</div>\n<script>\n${panelBridgeScript(\"agentproto-work-board-panel\")}\n\n// ============================================================\n// State\n// ============================================================\n\nvar COLUMNS = [\n {key: 'pending', label: 'Pending'},\n {key: 'in_progress', label: 'In Progress'},\n {key: 'done', label: 'Done'},\n {key: 'failed', label: 'Failed'}\n];\n\nvar boardId = null;\nvar tasks = [];\nvar pollTimer = null;\nvar pollActive = false;\n\nfunction setStatus(msg) {\n document.getElementById('statusbar').textContent = msg;\n}\n\nfunction esc(s) {\n return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n}\n\n// A cancelled task folds into the Failed column, tagged distinctly — v1\n// treats both as terminal scrap, and a fifth column for a rarely-used\n// status isn't worth the width.\nfunction columnOf(status) {\n if (status === 'cancelled') return 'failed';\n return status;\n}\n\n// ============================================================\n// Verification tell — never render an unverified done as gate-passed.\n// ============================================================\n\nfunction verificationTag(t) {\n var v = t.verification;\n if (v) {\n if (v.kind === 'gate') return '<span class=\"tag t-gate\">&#10003; gate</span>';\n if (v.kind === 'self-report') return '<span class=\"tag t-self\">self-report</span>';\n if (v.kind === 'human') return '<span class=\"tag t-human\">human</span>';\n }\n if (t.status === 'in_progress' && t.verify) {\n return '<span class=\"tag t-gated\">gated</span>';\n }\n return '';\n}\n\nfunction ownerTag(t) {\n if (!t.owner) return '<span class=\"tag t-unclaimed\">Unclaimed</span>';\n return '<span class=\"tag t-owner\">' + esc(t.owner) + '</span>';\n}\n\n// ============================================================\n// Card actions — explicit status moves, no drag-and-drop (read-only-safe\n// first: see module docblock).\n// ============================================================\n\nfunction actionsFor(t) {\n var a = [];\n if (t.status === 'pending') {\n if (!t.owner) a.push({label: 'Claim', action: 'claim', cls: 'primary'});\n else a.push({label: 'Start', action: 'start', cls: 'primary'});\n a.push({label: 'Cancel', action: 'cancel', cls: 'danger'});\n } else if (t.status === 'in_progress') {\n a.push({label: 'Done', action: 'done', cls: 'primary'});\n a.push({label: 'Fail', action: 'fail', cls: 'danger'});\n a.push({label: 'Release', action: 'release', cls: ''});\n a.push({label: 'Cancel', action: 'cancel', cls: 'danger'});\n } else if (t.status === 'done') {\n a.push({label: 'Reopen', action: 'reopen', cls: ''});\n }\n return a;\n}\n\nfunction applyAction(taskId, action) {\n var t = null;\n for (var i = 0; i < tasks.length; i++) {\n if (tasks[i].taskId === taskId) { t = tasks[i]; break; }\n }\n if (!t) return;\n var p;\n if (action === 'claim') {\n p = callTool('task_claim', {taskId: t.taskId, rev: t.rev});\n } else if (action === 'start') {\n p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'in_progress'});\n } else if (action === 'done') {\n p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'done'});\n } else if (action === 'fail') {\n p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'failed'});\n } else if (action === 'release') {\n p = callTool('task_update', {taskId: t.taskId, rev: t.rev, owner: null});\n } else if (action === 'cancel') {\n p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'cancelled'});\n } else if (action === 'reopen') {\n p = callTool('task_update', {taskId: t.taskId, rev: t.rev, status: 'pending'});\n } else {\n return;\n }\n p.then(function (result) {\n if (result && result.conflict) {\n setStatus('Someone else moved \"' + t.title + '\" first — refreshed.');\n } else if (result && result.error) {\n setStatus('Action failed: ' + result.error);\n } else {\n setStatus('Updated \"' + t.title + '\".');\n }\n return loadBoard();\n }).catch(function (e) {\n setStatus('Action failed: ' + e.message);\n });\n}\n\n// ============================================================\n// Render\n// ============================================================\n\nfunction renderCard(t) {\n var html = '<div class=\"card\" data-taskid=\"' + esc(t.taskId) + '\">';\n html += '<div class=\"c-title\">' + esc(t.title) + '</div>';\n html += '<div class=\"c-id\">#' + esc(t.taskId) + '</div>';\n html += '<div class=\"c-row\">' + ownerTag(t) + verificationTag(t);\n if (t.status === 'cancelled') html += '<span class=\"tag t-cancelled\">cancelled</span>';\n html += '</div>';\n if (t.lastVerifyError) {\n html += '<div class=\"c-err\">verify failed: ' + esc(t.lastVerifyError) + '</div>';\n }\n var actions = actionsFor(t);\n if (actions.length > 0) {\n html += '<div class=\"c-actions\">';\n for (var i = 0; i < actions.length; i++) {\n var a = actions[i];\n html += '<button class=\"abtn ' + a.cls + '\" data-taskid=\"' + esc(t.taskId) + '\" data-action=\"' + a.action + '\">' + a.label + '</button>';\n }\n html += '</div>';\n }\n html += '</div>';\n return html;\n}\n\nfunction render() {\n document.getElementById('board-id').textContent = boardId || '—';\n if (!document.getElementById('board-input').value) {\n document.getElementById('board-input').value = boardId || '';\n }\n var el = document.getElementById('columns');\n var html = '';\n for (var c = 0; c < COLUMNS.length; c++) {\n var col = COLUMNS[c];\n var rows = tasks.filter(function (t) { return columnOf(t.status) === col.key; });\n html += '<div class=\"col\"><div class=\"col-hdr\"><span>' + col.label + '</span><span>' + rows.length + '</span></div><div class=\"col-body\">';\n if (rows.length === 0) {\n html += '<div class=\"empty\">No tasks</div>';\n } else {\n for (var i = 0; i < rows.length; i++) html += renderCard(rows[i]);\n }\n html += '</div></div>';\n }\n el.innerHTML = html;\n}\n\n// ============================================================\n// Data\n// ============================================================\n\nfunction loadBoard() {\n var args = {includeClosed: true, full: true};\n if (boardId) args.boardId = boardId;\n return callTool('task_list', args).then(function (data) {\n boardId = data.boardId || boardId;\n tasks = data.tasks || [];\n render();\n setStatus(tasks.length + ' task' + (tasks.length === 1 ? '' : 's') + ' · ' + new Date().toLocaleTimeString());\n }).catch(function (e) {\n setStatus('Error: ' + e.message);\n });\n}\n\nfunction doPoll() {\n if (pollActive) return;\n pollActive = true;\n loadBoard().then(function () {\n pollActive = false;\n pollTimer = setTimeout(doPoll, 4000);\n }).catch(function () {\n pollActive = false;\n pollTimer = setTimeout(doPoll, 4000);\n });\n}\n\n// ============================================================\n// Toolbar wiring\n// ============================================================\n\ndocument.getElementById('columns').addEventListener('click', function (evt) {\n var btn = evt.target.closest ? evt.target.closest('button[data-action]') : null;\n if (!btn) return;\n applyAction(btn.getAttribute('data-taskid'), btn.getAttribute('data-action'));\n});\n\ndocument.getElementById('refresh-btn').addEventListener('click', function () {\n loadBoard();\n});\n\ndocument.getElementById('go-btn').addEventListener('click', function () {\n var v = document.getElementById('board-input').value.trim();\n if (!v) return;\n boardId = v;\n loadBoard();\n});\n\ndocument.getElementById('add-btn').addEventListener('click', function () {\n var input = document.getElementById('new-title');\n var title = input.value.trim();\n if (!title) return;\n var args = {title: title};\n if (boardId) args.boardId = boardId;\n callTool('task_create', args).then(function (result) {\n if (result && result.error) {\n setStatus('Create failed: ' + result.error);\n return;\n }\n input.value = '';\n setStatus('Created \"' + title + '\".');\n return loadBoard();\n }).catch(function (e) {\n setStatus('Create failed: ' + e.message);\n });\n});\n\n// ============================================================\n// Boot\n// ============================================================\n\ninitBridge().then(function () {\n return loadBoard();\n}).then(function () {\n pollTimer = setTimeout(doPoll, 4000);\n}).catch(function (e) {\n setStatus('Bridge error: ' + e.message);\n});\n</script>\n</body>\n</html>`\n"]}