@yemi33/minions 0.1.2144 → 0.1.2145

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,31 +1,18 @@
1
1
  // Completions Card helpers (W-betterHistory) — see the CSS block of the
2
2
  // same name in dashboard/slim.html for the component layout reference.
3
3
  // ─────────────────────────────────────────────────
4
- // The type chip is a single emoji glyph. Map covers every value the
5
- // engine can emit (14 WORK_TYPE constants + the synthetic 'qa-validate'
6
- // routed via routing.md). Unknown types fall back to a generic marker
7
- // and still get the raw type name in the hover tooltip.
8
- var COMPLETION_TYPE_EMOJI = {
9
- 'implement': '🔧',
10
- 'implement:large': '🛠️',
11
- 'fix': '🐛',
12
- 'review': '👀',
13
- 'verify': '',
14
- 'test': '🧪',
15
- 'plan': '📋',
16
- 'plan-to-prd': '📐',
17
- 'decompose': '🪓',
18
- 'meeting': '💬',
19
- 'explore': '🔍',
20
- 'ask': '❓',
21
- 'docs': '📝',
22
- 'setup': '⚙️',
23
- 'qa-validate': '🎯',
4
+ // The type chip renders a monochrome Fluent System Icon via CSS mask (see the
5
+ // .completions-card-type-chip[data-type="…"] rules in styles.css), keyed on
6
+ // the WORK_TYPE constant. This set lists every type that HAS an icon (14
7
+ // WORK_TYPE values + the synthetic 'qa-validate' routed via routing.md);
8
+ // 'review' is intentionally absent — it renders a "REVIEW" word chip instead.
9
+ // Unknown/absent types fall back to a generic "•" text glyph and still get the
10
+ // raw type name in the hover tooltip.
11
+ var TYPE_ICON_SET = {
12
+ 'implement': 1, 'implement:large': 1, 'fix': 1, 'verify': 1, 'test': 1,
13
+ 'plan': 1, 'plan-to-prd': 1, 'decompose': 1, 'meeting': 1, 'explore': 1,
14
+ 'ask': 1, 'docs': 1, 'setup': 1, 'qa-validate': 1,
24
15
  };
25
- function typeEmoji(t) {
26
- if (!t) return '•';
27
- return COMPLETION_TYPE_EMOJI[t] || '•';
28
- }
29
16
 
30
17
  // Pick the title for a completion card. Uses the dispatch description
31
18
  // (c.task) — that's what the agent was actually told to do — falling back
@@ -59,17 +46,23 @@
59
46
  return chip;
60
47
  }
61
48
 
62
- // Render a single completion card (DOM node). Returns the .history-item
63
- // element with attached click handlers. Used by renderHistoryFeed.
64
- function renderCompletionCard(c) {
65
- var status = completionStatus(c);
66
- var ts = c.completed_at || c.completedAt || c.endedAt || c.started_at || c.startedAt || c._ts;
49
+ // Render a single history card (DOM node). Returns the .history-item
50
+ // element with attached click handlers. Used by renderHistoryFeed for both
51
+ // finished completions AND in-flight dispatches — a running dispatch is just
52
+ // a dispatch without a result yet, so `isActive` swaps the terminal ✓/✕ for a
53
+ // live and anchors the time on `started_at` instead of `completed_at`.
54
+ function renderCompletionCard(c, isActive) {
55
+ var status = isActive ? 'active' : completionStatus(c);
56
+ var ts = isActive
57
+ ? (c.started_at || c.created_at || c._ts)
58
+ : (c.completed_at || c.completedAt || c.endedAt || c.started_at || c.startedAt || c._ts);
67
59
  var title = completionTitle(c);
68
60
  var item = (c.meta && c.meta.item) || {};
69
61
  var pr = (c.meta && c.meta.pr) || null;
62
+ var kindClass = isActive ? 'dispatch' : (status === 'fail' ? 'failure' : 'completion');
70
63
 
71
64
  var card = document.createElement('div');
72
- card.className = 'history-item completions-card kind-' + (status === 'fail' ? 'failure' : 'completion');
65
+ card.className = 'history-item completions-card kind-' + kindClass;
73
66
  card.setAttribute('role', 'button');
74
67
  card.setAttribute('tabindex', '0');
75
68
  card.title = title;
@@ -80,25 +73,38 @@
80
73
  rail.className = 'completions-card-rail';
81
74
  var railTop = document.createElement('div');
82
75
  railTop.className = 'completions-card-rail-top';
76
+ // Review cards render the word "REVIEW" as a chip (same box treatment as
77
+ // the WI/Plan/PR lineage chips, larger font) instead of the type emoji.
78
+ // Every other type keeps its single-glyph emoji.
83
79
  var chip = document.createElement('div');
84
- chip.className = 'completions-card-type-chip';
85
- chip.textContent = typeEmoji(c.type);
86
- if (c.type) chip.title = c.type;
80
+ if (c.type === 'review') {
81
+ chip.className = 'completions-card-type-word';
82
+ chip.textContent = 'REVIEW';
83
+ chip.title = 'review';
84
+ } else {
85
+ chip.className = 'completions-card-type-chip';
86
+ chip.textContent = typeEmoji(c.type);
87
+ if (c.type) chip.title = c.type;
88
+ }
87
89
  railTop.appendChild(chip);
88
90
 
89
91
  var railBottom = document.createElement('div');
90
92
  railBottom.className = 'completions-card-rail-bottom';
91
93
  var icon = document.createElement('span');
92
94
  icon.className = 'completions-card-status-icon ' + status;
93
- icon.textContent = status === 'ok' ? '✓' : (status === 'warn' ? '⚠' : '✕');
94
- icon.title = status === 'ok' ? 'Success' : (status === 'warn' ? 'Partial' : 'Failure');
95
+ icon.textContent = status === 'active' ? '●' : (status === 'ok' ? '✓' : (status === 'warn' ? '⚠' : '✕'));
96
+ icon.title = status === 'active' ? 'Running' : (status === 'ok' ? 'Success' : (status === 'warn' ? 'Partial' : 'Failure'));
95
97
  var sep = document.createElement('span');
96
98
  sep.className = 'completions-card-rail-sep';
97
99
  sep.textContent = '·';
98
100
  sep.setAttribute('aria-hidden', 'true');
99
101
  var when = document.createElement('span');
100
102
  when.className = 'completions-card-time';
101
- when.textContent = ts ? relTime(ts) : '';
103
+ // Active dispatches read as "running 5m" (elapsed since start); finished
104
+ // rows read as "5m ago" (relative to completion). Both reuse relTime.
105
+ when.textContent = ts
106
+ ? (status === 'active' ? 'running ' + relTime(ts).replace(' ago', '') : relTime(ts))
107
+ : '';
102
108
  if (ts) when.title = new Date(ts).toLocaleString();
103
109
  railBottom.appendChild(icon);
104
110
  railBottom.appendChild(sep);
@@ -115,6 +121,16 @@
115
121
  titleEl.textContent = title;
116
122
  bodyTop.appendChild(titleEl);
117
123
 
124
+ // Byline — which team member ran it (name · role). Present on every row so
125
+ // the feed answers "who did this" at a glance, not just "what / result".
126
+ var who = c.agentName || c.agent;
127
+ if (who) {
128
+ var byline = document.createElement('div');
129
+ byline.className = 'completions-card-byline';
130
+ byline.textContent = c.agentRole ? who + ' · ' + c.agentRole : who;
131
+ bodyTop.appendChild(byline);
132
+ }
133
+
118
134
  if (status === 'fail' && (c.failureClass || c.reason)) {
119
135
  var failLine = document.createElement('div');
120
136
  failLine.className = 'completions-card-fail-line';
@@ -310,20 +326,97 @@
310
326
  // Bind completions-modal close handlers once (shared helper above).
311
327
  bindModalClose('slim-completions-modal', 'slim-completions-close');
312
328
 
329
+ // Render a PR as a meta-rail card matching the dispatch/completion shape:
330
+ // a "PR" word chip in the rail (left), the title in the body, the activity
331
+ // time in the rail-bottom, and the PR status as a tile-chip — the SAME
332
+ // component the /pull-requests tile list uses — in the bottom row. The whole
333
+ // card is the click target (opens the PR URL); there's no separate "open"
334
+ // link anymore.
335
+ function renderPrCard(p) {
336
+ var num = p.prNumber || p.number || p.id;
337
+ var title = '#' + (num || '?') + ' — ' + (p.title || '(untitled PR)');
338
+ var ts = p.updatedAt || p.lastReviewedAt || p.lastBuildCheck || p._attachedAt || p.created || p.createdAt;
339
+ var build = p.buildStatus || '';
340
+ // Mirror renderPrTileBody's color logic so the chip matches the PR list:
341
+ // merged → green, failing build → red, active/linked → blue, else neutral.
342
+ var cls = p.status === 'merged' ? 'green'
343
+ : (build === 'failing' ? 'red'
344
+ : ((p.status === 'active' || p.status === 'linked') ? 'blue' : ''));
345
+
346
+ var card = document.createElement('div');
347
+ card.className = 'history-item completions-card kind-pr';
348
+ card.setAttribute('role', 'button');
349
+ card.setAttribute('tabindex', '0');
350
+ card.title = title;
351
+
352
+ var rail = document.createElement('div');
353
+ rail.className = 'completions-card-rail';
354
+ var railTop = document.createElement('div');
355
+ railTop.className = 'completions-card-rail-top';
356
+ var word = document.createElement('div');
357
+ word.className = 'completions-card-type-word';
358
+ word.textContent = 'PR';
359
+ word.title = 'pull request';
360
+ railTop.appendChild(word);
361
+ var railBottom = document.createElement('div');
362
+ railBottom.className = 'completions-card-rail-bottom';
363
+ var when = document.createElement('span');
364
+ when.className = 'completions-card-time';
365
+ when.textContent = ts ? relTime(ts) : '';
366
+ if (ts) when.title = new Date(ts).toLocaleString();
367
+ railBottom.appendChild(when);
368
+ rail.appendChild(railTop);
369
+ rail.appendChild(railBottom);
370
+
371
+ var bodyTop = document.createElement('div');
372
+ bodyTop.className = 'completions-card-body-top';
373
+ var titleEl = document.createElement('div');
374
+ titleEl.className = 'completions-card-title';
375
+ titleEl.textContent = title;
376
+ bodyTop.appendChild(titleEl);
377
+
378
+ // Status row — grid col 2, row 2 (aligns with rail-bottom). A single
379
+ // tile-chip, the same pill component (and color classes) as the PR list.
380
+ var statusRow = document.createElement('div');
381
+ statusRow.className = 'completions-card-pr-status';
382
+ var chip = document.createElement('span');
383
+ chip.className = 'tile-chip ' + cls;
384
+ chip.textContent = p.status || '—';
385
+ statusRow.appendChild(chip);
386
+
387
+ card.appendChild(rail);
388
+ card.appendChild(bodyTop);
389
+ card.appendChild(statusRow);
390
+
391
+ // Whole-card click (and keyboard activation) opens the PR — replaces the
392
+ // old inline "open" hyperlink.
393
+ if (p.url) {
394
+ var openPr = function() { window.open(p.url, '_blank', 'noopener'); };
395
+ card.addEventListener('click', openPr);
396
+ card.addEventListener('keydown', function(ev) {
397
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openPr(); }
398
+ });
399
+ }
400
+ return card;
401
+ }
402
+
313
403
  function renderHistoryFeed(input) {
314
404
  var listEl = document.getElementById('history-list');
315
405
  if (!listEl) return;
316
406
  var events = [];
317
407
 
318
- // Active dispatches (blue) — at the top so users know what's running.
408
+ // Active dispatches (blue) — rendered with the same meta-rail card as
409
+ // completions (a running dispatch is just a dispatch without a result yet).
410
+ // Carry the raw record so the card renderer reads task/agent/lineage from
411
+ // the canonical field paths. NOTE: the engine stamps `started_at`
412
+ // (snake_case) — reading `startedAt` here is the old bug that made every
413
+ // dispatch show "0s ago". Sorted purely chronologically by start time.
319
414
  (input.active || []).forEach(function(d) {
320
- var ts = d.startedAt || d.dispatchedAt || d.createdAt || d._ts;
415
+ var ts = d.started_at || d.created_at || d._ts;
321
416
  events.push({
322
417
  kind: 'dispatch',
323
- kindLabel: 'Dispatch',
324
- title: describeDispatch(d),
325
- meta: 'started ' + relTime(ts) + (d.workItemId ? ' · ' + d.workItemId : ''),
326
- ts: ts ? Date.parse(ts) || Date.now() : Date.now(),
418
+ raw: d,
419
+ ts: ts ? Date.parse(ts) || 0 : 0,
327
420
  });
328
421
  });
329
422
 
@@ -340,17 +433,18 @@
340
433
  });
341
434
  });
342
435
 
343
- // PR events (amber) — shows PRs we're tracking, anchored on updatedAt.
344
- // `metaUrl` carries the optional "open PR" link so the renderer can build
345
- // it as a real <a> node instead of splicing raw HTML into innerHTML.
436
+ // PR events (amber) — shows PRs we're tracking, anchored on the most
437
+ // recent activity. The slim PR shape does NOT carry updatedAt/createdAt
438
+ // reading those (the old code) yielded ts=0 and pinned every PR to the
439
+ // bottom of the feed regardless of recency. Fall through the timestamps the
440
+ // payload actually provides, newest-meaning first.
441
+ // Carry the raw PR record so renderPrCard reads its own fields (the card
442
+ // opens the PR on click — no inline link).
346
443
  (input.prs || []).slice(0, 6).forEach(function(p) {
347
- var ts = p.updatedAt || p.createdAt;
444
+ var ts = p.updatedAt || p.lastReviewedAt || p.lastBuildCheck || p._attachedAt || p.created || p.createdAt;
348
445
  events.push({
349
446
  kind: 'pr',
350
- kindLabel: 'PR',
351
- title: '#' + (p.prNumber || p.id || '?') + ' — ' + (p.title || '(untitled PR)'),
352
- meta: (p.buildStatus || 'no-build') + ' · ' + (p.reviewStatus || 'no-review'),
353
- metaUrl: p.url || null,
447
+ raw: p,
354
448
  ts: ts ? Date.parse(ts) || 0 : 0,
355
449
  });
356
450
  });
@@ -366,48 +460,19 @@
366
460
  return;
367
461
  }
368
462
  // Build the feed as DOM nodes so SEC-03's .innerHTML ratchet stays happy
369
- // (test/unit.test.js DYNAMIC_INNERHTML_BASELINE). Completion / failure
370
- // events use renderCompletionCard (meta-rail layout); dispatch / PR
371
- // events keep the legacy .history-head + .history-title + .history-meta
372
- // shape since the user-facing redesign is scoped to completions for now.
463
+ // (test/unit.test.js DYNAMIC_INNERHTML_BASELINE). Every event type now
464
+ // renders as a meta-rail card: dispatch / completion / failure via
465
+ // renderCompletionCard, PRs via renderPrCard.
373
466
  var frag = document.createDocumentFragment();
374
467
  for (var i = 0; i < events.length; i++) {
375
468
  var e = events[i];
376
- if ((e.kind === 'completion' || e.kind === 'failure') && e.raw) {
377
- frag.appendChild(renderCompletionCard(e.raw));
378
- continue;
379
- }
380
- var item = document.createElement('div');
381
- item.className = 'history-item kind-' + e.kind;
382
- var head = document.createElement('div');
383
- head.className = 'history-head';
384
- var kind = document.createElement('span');
385
- kind.className = 'history-kind';
386
- kind.textContent = e.kindLabel;
387
- var when = document.createElement('span');
388
- when.className = 'history-time';
389
- when.textContent = e.ts ? relTime(e.ts) : '';
390
- head.appendChild(kind);
391
- head.appendChild(when);
392
- var titleEl = document.createElement('div');
393
- titleEl.className = 'history-title';
394
- titleEl.textContent = e.title;
395
- var metaEl = document.createElement('div');
396
- metaEl.className = 'history-meta';
397
- metaEl.appendChild(document.createTextNode(e.meta || ''));
398
- if (e.metaUrl) {
399
- metaEl.appendChild(document.createTextNode(' · '));
400
- var a = document.createElement('a');
401
- a.href = e.metaUrl;
402
- a.target = '_blank';
403
- a.rel = 'noopener';
404
- a.textContent = 'open';
405
- metaEl.appendChild(a);
469
+ if (e.kind === 'dispatch' && e.raw) {
470
+ frag.appendChild(renderCompletionCard(e.raw, true));
471
+ } else if ((e.kind === 'completion' || e.kind === 'failure') && e.raw) {
472
+ frag.appendChild(renderCompletionCard(e.raw, false));
473
+ } else if (e.kind === 'pr' && e.raw) {
474
+ frag.appendChild(renderPrCard(e.raw));
406
475
  }
407
- item.appendChild(head);
408
- item.appendChild(titleEl);
409
- item.appendChild(metaEl);
410
- frag.appendChild(item);
411
476
  }
412
477
  listEl.replaceChildren(frag);
413
478
  }
@@ -0,0 +1,26 @@
1
+ diff a/dashboard/slim/js/history.js b/dashboard/slim/js/history.js (rejected hunks)
2
+ @@ -96,7 +83,12 @@
3
+ chip.title = 'review';
4
+ } else {
5
+ chip.className = 'completions-card-type-chip';
6
+ - chip.textContent = typeEmoji(c.type);
7
+ + // Known types → Fluent icon via CSS mask (data-type); unknown → "•" glyph.
8
+ + if (c.type && TYPE_ICON_SET[c.type]) {
9
+ + chip.setAttribute('data-type', c.type);
10
+ + } else {
11
+ + chip.textContent = '•';
12
+ + }
13
+ if (c.type) chip.title = c.type;
14
+ }
15
+ railTop.appendChild(chip);
16
+ @@ -105,7 +97,9 @@
17
+ railBottom.className = 'completions-card-rail-bottom';
18
+ var icon = document.createElement('span');
19
+ icon.className = 'completions-card-status-icon ' + status;
20
+ - icon.textContent = status === 'active' ? '●' : (status === 'ok' ? '✓' : (status === 'warn' ? '⚠' : '✕'));
21
+ + // ok/warn/fail render as Fluent icons via the status class (CSS mask in
22
+ + // styles.css); the live 'active' state keeps its pulsing dot.
23
+ + if (status === 'active') icon.textContent = '●';
24
+ icon.title = status === 'active' ? 'Running' : (status === 'ok' ? 'Success' : (status === 'warn' ? 'Partial' : 'Failure'));
25
+ var sep = document.createElement('span');
26
+ sep.className = 'completions-card-rail-sep';
@@ -1,7 +1,10 @@
1
- function bindModalClose(modalId, closeBtnId) {
1
+ // onClose (optional) runs on every dismiss path — close button, backdrop
2
+ // click, and Esc — so callers can attach a side effect (e.g. persisting a
3
+ // "seen" flag for a first-visit popup) without re-wiring the listeners.
4
+ function bindModalClose(modalId, closeBtnId, onClose) {
2
5
  var modal = document.getElementById(modalId);
3
6
  if (!modal) return;
4
- function close() { modal.classList.remove('open'); }
7
+ function close() { modal.classList.remove('open'); if (onClose) onClose(); }
5
8
  var closeBtn = document.getElementById(closeBtnId);
6
9
  if (closeBtn) closeBtn.addEventListener('click', close);
7
10
  modal.addEventListener('click', function(ev) { if (ev.target === modal) close(); });
@@ -123,6 +126,9 @@
123
126
  // Populate + open the cockpit-tile detail modal from the latest status
124
127
  // snapshot. Mirrors the corresponding old-dashboard tab for each tile.
125
128
  function openTileModal(key) {
129
+ // The Pinned-context tile has its own list/editor modal (view + edit + unpin)
130
+ // rather than the read-only tile detail view.
131
+ if (key === 'pinned') { openSlimPinnedList(); return; }
126
132
  var view = TILE_VIEWS[key];
127
133
  if (!view) return;
128
134
  var modal = document.getElementById('slim-tile-modal');
@@ -0,0 +1,182 @@
1
+ // ── Pinned context (create / view / edit / unpin) ──────────────
2
+ // Slim front-end over the same endpoints the classic dashboard uses:
3
+ // GET (via /api/status `pinned`), POST /api/pinned (create),
4
+ // POST /api/pinned/update (edit), POST /api/pinned/remove (unpin).
5
+ // The "Pin Content" action button and the "Pinned context" status tile
6
+ // both surface this. Editing reuses the atomic update endpoint.
7
+
8
+ // When non-null, the editor is editing the entry with this (original) title;
9
+ // null means it is creating a new pin.
10
+ var _pinEditOriginal = null;
11
+
12
+ function slimPinnedEntries() {
13
+ return (lastStatusData && Array.isArray(lastStatusData.pinned)) ? lastStatusData.pinned : [];
14
+ }
15
+
16
+ function pinLevelChipClass(level) {
17
+ return level === 'critical' ? 'red' : level === 'warning' ? 'amber' : 'blue';
18
+ }
19
+
20
+ // ── Editor (create + edit) ───────────────────────────────────────
21
+ function openSlimPinEditor(entry) {
22
+ _pinEditOriginal = entry ? entry.title : null;
23
+ var modal = document.getElementById('slim-pin-edit-modal');
24
+ if (!modal) return;
25
+ document.getElementById('slim-pin-edit-heading').textContent = entry ? 'Edit pinned content' : 'Pin content';
26
+ document.getElementById('slim-pin-title').value = entry ? (entry.title || '') : '';
27
+ document.getElementById('slim-pin-content').value = entry ? (entry.content || '') : '';
28
+ document.getElementById('slim-pin-level').value = (entry && entry.level) ? entry.level : 'info';
29
+ var msg = document.getElementById('slim-pin-msg');
30
+ if (msg) { msg.textContent = ''; msg.style.color = 'var(--muted)'; }
31
+ var submit = document.getElementById('slim-pin-submit');
32
+ if (submit) { submit.disabled = false; submit.textContent = entry ? 'Save' : 'Pin'; }
33
+ modal.classList.add('open');
34
+ setTimeout(function() { var t = document.getElementById('slim-pin-title'); if (t) t.focus(); }, 40);
35
+ }
36
+ function closeSlimPinEditor() {
37
+ var modal = document.getElementById('slim-pin-edit-modal');
38
+ if (modal) modal.classList.remove('open');
39
+ }
40
+
41
+ async function submitSlimPin() {
42
+ var title = (document.getElementById('slim-pin-title').value || '').trim();
43
+ var content = document.getElementById('slim-pin-content').value;
44
+ var level = document.getElementById('slim-pin-level').value;
45
+ var msg = document.getElementById('slim-pin-msg');
46
+ var submit = document.getElementById('slim-pin-submit');
47
+ if (!title || !content.trim()) {
48
+ if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Title and content are required.'; }
49
+ return;
50
+ }
51
+ var editing = _pinEditOriginal != null;
52
+ if (submit) { submit.disabled = true; submit.textContent = editing ? 'Saving…' : 'Pinning…'; }
53
+ try {
54
+ var res;
55
+ if (editing) {
56
+ res = await fetch('/api/pinned/update', {
57
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({ originalTitle: _pinEditOriginal, title: title, content: content, level: level }),
59
+ });
60
+ } else {
61
+ res = await fetch('/api/pinned', {
62
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ title: title, content: content, level: level }),
64
+ });
65
+ }
66
+ var d = await res.json().catch(function() { return {}; });
67
+ if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
68
+ closeSlimPinEditor();
69
+ scheduleStatusRefresh(400); // refresh the Pinned-context tile
70
+ // If the list modal is open, re-render it once the snapshot catches up.
71
+ setTimeout(function() {
72
+ var listModal = document.getElementById('slim-pinned-modal');
73
+ if (listModal && listModal.classList.contains('open')) renderSlimPinnedList();
74
+ }, 700);
75
+ } catch (e) {
76
+ if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
77
+ if (submit) { submit.disabled = false; submit.textContent = editing ? 'Save' : 'Pin'; }
78
+ }
79
+ }
80
+
81
+ // ── List / view / unpin ──────────────────────────────────────────
82
+ function openSlimPinnedList() {
83
+ var modal = document.getElementById('slim-pinned-modal');
84
+ if (!modal) return;
85
+ renderSlimPinnedList();
86
+ modal.classList.add('open');
87
+ }
88
+ function closeSlimPinnedList() {
89
+ var modal = document.getElementById('slim-pinned-modal');
90
+ if (modal) modal.classList.remove('open');
91
+ }
92
+
93
+ function renderSlimPinnedList() {
94
+ var body = document.getElementById('slim-pinned-body');
95
+ if (!body) return;
96
+ var entries = slimPinnedEntries();
97
+ body.textContent = '';
98
+ if (!entries.length) {
99
+ var empty = document.createElement('div');
100
+ empty.className = 'tile-empty';
101
+ empty.textContent = 'Nothing pinned. Pin context all agents should read first.';
102
+ body.appendChild(empty);
103
+ return;
104
+ }
105
+ entries.forEach(function(entry) {
106
+ var card = document.createElement('div');
107
+ card.className = 'pinned-row';
108
+
109
+ var top = document.createElement('div');
110
+ top.className = 'pinned-row-top';
111
+ var titleEl = document.createElement('span');
112
+ titleEl.className = 'pinned-row-title';
113
+ titleEl.textContent = entry.title || '(untitled)';
114
+ titleEl.title = entry.title || '';
115
+ top.appendChild(titleEl);
116
+ var chip = document.createElement('span');
117
+ chip.className = 'tile-chip ' + pinLevelChipClass(entry.level);
118
+ chip.textContent = entry.level || 'info';
119
+ top.appendChild(chip);
120
+ card.appendChild(top);
121
+
122
+ var preview = document.createElement('div');
123
+ preview.className = 'pinned-row-preview';
124
+ var text = String(entry.content || '');
125
+ preview.textContent = text.length > 220 ? text.slice(0, 220) + '…' : text;
126
+ card.appendChild(preview);
127
+
128
+ var actions = document.createElement('div');
129
+ actions.className = 'pinned-row-actions';
130
+ var editBtn = document.createElement('button');
131
+ editBtn.className = 'btn-secondary';
132
+ editBtn.type = 'button';
133
+ editBtn.textContent = 'Edit';
134
+ editBtn.addEventListener('click', function() { openSlimPinEditor(entry); });
135
+ var unpinBtn = document.createElement('button');
136
+ unpinBtn.className = 'btn-secondary pinned-row-unpin';
137
+ unpinBtn.type = 'button';
138
+ unpinBtn.textContent = 'Unpin';
139
+ unpinBtn.addEventListener('click', function() { removeSlimPin(entry.title); });
140
+ actions.appendChild(editBtn);
141
+ actions.appendChild(unpinBtn);
142
+ card.appendChild(actions);
143
+
144
+ body.appendChild(card);
145
+ });
146
+ }
147
+
148
+ async function removeSlimPin(title) {
149
+ if (!title) return;
150
+ if (!confirm('Unpin "' + title + '"?')) return;
151
+ try {
152
+ var res = await fetch('/api/pinned/remove', {
153
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
154
+ body: JSON.stringify({ title: title }),
155
+ });
156
+ if (!res.ok) { var d = await res.json().catch(function() { return {}; }); throw new Error(d.error || ('HTTP ' + res.status)); }
157
+ // Optimistically drop it from the cached snapshot so the list + tile update
158
+ // before the next poll, then reconcile.
159
+ if (lastStatusData && Array.isArray(lastStatusData.pinned)) {
160
+ lastStatusData.pinned = lastStatusData.pinned.filter(function(e) { return e.title !== title; });
161
+ }
162
+ renderSlimPinnedList();
163
+ scheduleStatusRefresh(400);
164
+ } catch (e) {
165
+ alert('Unpin failed: ' + (e && e.message ? e.message : e));
166
+ }
167
+ }
168
+
169
+ bindModalClose('slim-pinned-modal', 'slim-pinned-close');
170
+ bindModalClose('slim-pin-edit-modal', 'slim-pin-edit-close');
171
+ (function bindPinnedUi() {
172
+ var pinBtn = document.getElementById('slim-pin-btn');
173
+ if (pinBtn) pinBtn.addEventListener('click', function() { openSlimPinEditor(null); });
174
+ var addBtn = document.getElementById('slim-pinned-add');
175
+ if (addBtn) addBtn.addEventListener('click', function() { openSlimPinEditor(null); });
176
+ var tileChip = document.getElementById('slim-tile-pin-chip');
177
+ if (tileChip) tileChip.addEventListener('click', function(ev) { ev.stopPropagation(); openSlimPinEditor(null); });
178
+ var cancel = document.getElementById('slim-pin-cancel');
179
+ if (cancel) cancel.addEventListener('click', closeSlimPinEditor);
180
+ var submit = document.getElementById('slim-pin-submit');
181
+ if (submit) submit.addEventListener('click', submitSlimPin);
182
+ })();