@beastmode-develeap/beastmode 0.1.133 → 0.1.135

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.
@@ -15,7 +15,7 @@
15
15
  }
16
16
  </script>
17
17
  <!--BOARD_DATA-->
18
- <script>window.__BUILD_STAMP__ = "20260419-211451-b246bc0";</script>
18
+ <script>window.__BUILD_STAMP__ = "20260420-073821-483e679";</script>
19
19
  <link rel="preconnect" href="https://fonts.googleapis.com">
20
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
21
  <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -609,11 +609,12 @@ body {
609
609
  content: '';
610
610
  position: absolute;
611
611
  top: -50%;
612
- right: -10%;
613
- width: 300px;
614
- height: 300px;
615
- background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%);
612
+ right: -20%;
613
+ width: 250px;
614
+ height: 250px;
615
+ background: radial-gradient(circle, var(--accent-glow) 0%, transparent 60%);
616
616
  pointer-events: none;
617
+ opacity: 0.6;
617
618
  }
618
619
 
619
620
  .welcome-title {
@@ -1070,6 +1071,39 @@ input[type="range"]::-webkit-slider-thumb {
1070
1071
  padding: 8px;
1071
1072
  flex: 1;
1072
1073
  min-height: 60px;
1074
+ max-height: calc(100vh - 300px);
1075
+ overflow-y: auto;
1076
+ scrollbar-width: thin;
1077
+ scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
1078
+ }
1079
+
1080
+ .kanban-items::-webkit-scrollbar {
1081
+ width: 6px;
1082
+ }
1083
+
1084
+ .kanban-items::-webkit-scrollbar-track {
1085
+ background: transparent;
1086
+ }
1087
+
1088
+ .kanban-items::-webkit-scrollbar-thumb {
1089
+ background: rgba(255, 255, 255, 0.15);
1090
+ border-radius: 3px;
1091
+ }
1092
+
1093
+ .kanban-items::-webkit-scrollbar-thumb:hover {
1094
+ background: rgba(255, 255, 255, 0.25);
1095
+ }
1096
+
1097
+ [data-theme="light"] .kanban-items {
1098
+ scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
1099
+ }
1100
+
1101
+ [data-theme="light"] .kanban-items::-webkit-scrollbar-thumb {
1102
+ background: rgba(0, 0, 0, 0.15);
1103
+ }
1104
+
1105
+ [data-theme="light"] .kanban-items::-webkit-scrollbar-thumb:hover {
1106
+ background: rgba(0, 0, 0, 0.25);
1073
1107
  }
1074
1108
 
1075
1109
  .kanban-card {
@@ -1176,6 +1210,23 @@ input[type="range"]::-webkit-slider-thumb {
1176
1210
  color: var(--text-muted);
1177
1211
  font-family: var(--font-mono);
1178
1212
  }
1213
+ /* Status-age badge — time spent in the CURRENT status. Color-coded so
1214
+ operators can scan which items have been waiting/working too long.
1215
+ "ok" fresh, "info" 1-6h, "warn" 6-24h, "stale" 24h+. Designed for
1216
+ the 2026-04-19 "not sure why waiting so long" complaint. */
1217
+ .kanban-card .card-status-age {
1218
+ margin-left: auto;
1219
+ font-size: 11px;
1220
+ font-family: var(--font-mono);
1221
+ padding: 1px 6px;
1222
+ border-radius: 3px;
1223
+ font-weight: 500;
1224
+ letter-spacing: 0.2px;
1225
+ }
1226
+ .card-status-age.status-age-ok { color: var(--text-muted); }
1227
+ .card-status-age.status-age-info { color: var(--accent); background: var(--accent-subtle); }
1228
+ .card-status-age.status-age-warn { color: #fb923c; background: rgba(249, 115, 22, 0.15); }
1229
+ .card-status-age.status-age-stale { color: #f87171; background: rgba(239, 68, 68, 0.15); }
1179
1230
  .card-badge {
1180
1231
  display: inline-block;
1181
1232
  padding: 2px 8px;
@@ -1674,6 +1725,43 @@ input[type="range"]::-webkit-slider-thumb {
1674
1725
  }
1675
1726
  .filter-toggle:hover { border-color: var(--text-muted); color: var(--text); }
1676
1727
  .filter-toggle.active { border-color: var(--accent); color: var(--accent); background: var(--accent-subtle); }
1728
+
1729
+ .view-toggle {
1730
+ display: inline-flex;
1731
+ border: 1px solid var(--border);
1732
+ border-radius: var(--radius-sm);
1733
+ overflow: hidden;
1734
+ flex-shrink: 0;
1735
+ }
1736
+ .view-toggle-btn {
1737
+ display: inline-flex;
1738
+ align-items: center;
1739
+ padding: 0 12px;
1740
+ height: 36px;
1741
+ font-size: 13px;
1742
+ font-family: var(--font-sans);
1743
+ font-weight: 500;
1744
+ background: var(--bg-card);
1745
+ color: var(--text-secondary);
1746
+ border: none;
1747
+ cursor: pointer;
1748
+ transition: all 0.15s ease;
1749
+ white-space: nowrap;
1750
+ user-select: none;
1751
+ }
1752
+ .view-toggle-btn:hover {
1753
+ color: var(--text);
1754
+ background: var(--bg-input);
1755
+ }
1756
+ .view-toggle-btn.active {
1757
+ background: var(--accent-subtle);
1758
+ color: var(--accent);
1759
+ cursor: default;
1760
+ }
1761
+ .view-toggle-btn + .view-toggle-btn {
1762
+ border-left: 1px solid var(--border);
1763
+ }
1764
+
1677
1765
  .filter-active-count {
1678
1766
  display: inline-flex;
1679
1767
  align-items: center;
@@ -2400,7 +2488,44 @@ input[type="range"]::-webkit-slider-thumb {
2400
2488
  ACTIVITY SIDEBAR (Dashboard)
2401
2489
  ================================================================ */
2402
2490
 
2403
- .activity-list { display: flex; flex-direction: column; gap: 2px; }
2491
+ .activity-list {
2492
+ display: flex;
2493
+ flex-direction: column;
2494
+ gap: 2px;
2495
+ max-height: 400px;
2496
+ overflow-y: auto;
2497
+ scrollbar-width: thin;
2498
+ scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
2499
+ }
2500
+
2501
+ .activity-list::-webkit-scrollbar {
2502
+ width: 6px;
2503
+ }
2504
+
2505
+ .activity-list::-webkit-scrollbar-track {
2506
+ background: transparent;
2507
+ }
2508
+
2509
+ .activity-list::-webkit-scrollbar-thumb {
2510
+ background: rgba(255, 255, 255, 0.15);
2511
+ border-radius: 3px;
2512
+ }
2513
+
2514
+ .activity-list::-webkit-scrollbar-thumb:hover {
2515
+ background: rgba(255, 255, 255, 0.25);
2516
+ }
2517
+
2518
+ [data-theme="light"] .activity-list {
2519
+ scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
2520
+ }
2521
+
2522
+ [data-theme="light"] .activity-list::-webkit-scrollbar-thumb {
2523
+ background: rgba(0, 0, 0, 0.15);
2524
+ }
2525
+
2526
+ [data-theme="light"] .activity-list::-webkit-scrollbar-thumb:hover {
2527
+ background: rgba(0, 0, 0, 0.25);
2528
+ }
2404
2529
  .activity-item {
2405
2530
  padding: 10px 12px;
2406
2531
  border-radius: var(--radius-sm);
@@ -3580,6 +3705,35 @@ function timeAgo(dateString) {
3580
3705
  return months + 'mo ago';
3581
3706
  }
3582
3707
 
3708
+ // "In status" age — shorter render (no "ago"), with a severity class
3709
+ // so the UI can color-code long waits. Designed for inline card badges:
3710
+ // "3h", "2d 5h", "1w". Returns {text, severity} where severity is
3711
+ // "ok" (<1h), "info" (<6h), "warn" (<24h), or "stale" (>=24h). Empty
3712
+ // dateString returns {text: '', severity: 'ok'} — caller suppresses
3713
+ // the badge entirely.
3714
+ function statusAge(dateString) {
3715
+ if (!dateString) return { text: '', severity: 'ok' };
3716
+ const now = new Date();
3717
+ const date = new Date(dateString);
3718
+ const totalMin = Math.max(0, Math.floor((now - date) / 60000));
3719
+ let text;
3720
+ if (totalMin < 1) text = 'just now';
3721
+ else if (totalMin < 60) text = totalMin + 'm';
3722
+ else if (totalMin < 1440) text = Math.floor(totalMin / 60) + 'h';
3723
+ else if (totalMin < 10080) {
3724
+ const d = Math.floor(totalMin / 1440);
3725
+ const h = Math.floor((totalMin % 1440) / 60);
3726
+ text = h ? d + 'd ' + h + 'h' : d + 'd';
3727
+ } else {
3728
+ text = Math.floor(totalMin / 10080) + 'w';
3729
+ }
3730
+ let severity = 'ok';
3731
+ if (totalMin >= 60 && totalMin < 360) severity = 'info';
3732
+ else if (totalMin >= 360 && totalMin < 1440) severity = 'warn';
3733
+ else if (totalMin >= 1440) severity = 'stale';
3734
+ return { text, severity };
3735
+ }
3736
+
3583
3737
  function priorityBadgeClass(priority) {
3584
3738
  if (!priority) return '';
3585
3739
  const p = priority.toLowerCase();
@@ -4591,7 +4745,16 @@ function PipelineView({
4591
4745
  const label = cost ? formatCost(cost.total_cost_usd) : null;
4592
4746
  return label ? html`<span class="card-badge badge-cost" title=${'Total cost: ' + label}>${label}</span>` : null;
4593
4747
  })()}
4594
- <span class="card-time">${timeAgo(item.updated_at || item.created_at)}</span>
4748
+ ${(() => {
4749
+ // Time-in-status badge (2026-04-20): answers "how long has
4750
+ // this been waiting?" without the user having to click into
4751
+ // the task. Falls back to updated_at for pre-v5 rows.
4752
+ const src = item.status_changed_at || item.updated_at || item.created_at;
4753
+ const age = statusAge(src);
4754
+ if (!age.text) return null;
4755
+ return html`<span class=${'card-status-age status-age-' + age.severity}
4756
+ title=${'In status since ' + (src || 'unknown') + ' (updated ' + timeAgo(item.updated_at || item.created_at) + ')'}>${age.text}</span>`;
4757
+ })()}
4595
4758
  </div>
4596
4759
  </div>
4597
4760
  `;
@@ -4826,6 +4989,35 @@ function CollapseAllButton({ allCollapsed, onToggle }) {
4826
4989
  `;
4827
4990
  }
4828
4991
 
4992
+ function ViewToggle({ viewMode, onToggle }) {
4993
+ const isPipeline = viewMode === 'pipeline';
4994
+ return html`
4995
+ <div class="view-toggle" data-testid="view-toggle"
4996
+ role="radiogroup" aria-label="View mode">
4997
+ <button
4998
+ class=${'view-toggle-btn' + (!isPipeline ? ' active' : '')}
4999
+ onClick=${() => { if (isPipeline) onToggle(); }}
5000
+ role="radio"
5001
+ aria-checked=${String(!isPipeline)}
5002
+ aria-pressed=${String(!isPipeline)}
5003
+ data-testid="view-toggle-board"
5004
+ title="Board view">
5005
+ Board
5006
+ </button>
5007
+ <button
5008
+ class=${'view-toggle-btn' + (isPipeline ? ' active' : '')}
5009
+ onClick=${() => { if (!isPipeline) onToggle(); }}
5010
+ role="radio"
5011
+ aria-checked=${String(isPipeline)}
5012
+ aria-pressed=${String(isPipeline)}
5013
+ data-testid="view-toggle-pipeline"
5014
+ title="Pipeline view">
5015
+ Pipeline
5016
+ </button>
5017
+ </div>
5018
+ `;
5019
+ }
5020
+
4829
5021
  // ── Board Page ──
4830
5022
 
4831
5023
  function BoardPage({ selectedProject }) {
@@ -4852,13 +5044,61 @@ function BoardPage({ selectedProject }) {
4852
5044
  const [columnSorts, setColumnSorts] = useState({});
4853
5045
  const [epicCollapseKey, setEpicCollapseKey] = useState(0);
4854
5046
  const [costsByItem, setCostsByItem] = useState({});
4855
- const viewMode = (() => {
5047
+ const [viewMode, setViewMode] = useState(() => {
5048
+ try {
5049
+ const saved = localStorage.getItem('beastmode-view-mode');
5050
+ if (saved === 'pipeline' || saved === 'board') return saved;
5051
+ } catch {}
4856
5052
  try {
4857
5053
  const params = new URLSearchParams(window.location.search);
4858
5054
  if (params.get('view') === 'pipeline') return 'pipeline';
4859
5055
  } catch {}
4860
5056
  return 'board';
4861
- })();
5057
+ });
5058
+
5059
+ useEffect(() => {
5060
+ try {
5061
+ localStorage.setItem('beastmode-view-mode', viewMode);
5062
+ } catch {}
5063
+ }, [viewMode]);
5064
+
5065
+ useEffect(() => {
5066
+ try {
5067
+ const url = new URL(window.location.href);
5068
+ if (viewMode === 'pipeline') {
5069
+ url.searchParams.set('view', 'pipeline');
5070
+ } else {
5071
+ url.searchParams.delete('view');
5072
+ }
5073
+ if (url.href !== window.location.href) {
5074
+ window.history.pushState({ viewMode }, '', url.href);
5075
+ }
5076
+ } catch {}
5077
+ }, [viewMode]);
5078
+
5079
+ useEffect(() => {
5080
+ const onPop = (e) => {
5081
+ try {
5082
+ if (e.state && (e.state.viewMode === 'pipeline' || e.state.viewMode === 'board')) {
5083
+ setViewMode(e.state.viewMode);
5084
+ return;
5085
+ }
5086
+ const saved = localStorage.getItem('beastmode-view-mode');
5087
+ if (saved === 'pipeline' || saved === 'board') {
5088
+ setViewMode(saved);
5089
+ return;
5090
+ }
5091
+ const params = new URLSearchParams(window.location.search);
5092
+ setViewMode(params.get('view') === 'pipeline' ? 'pipeline' : 'board');
5093
+ } catch {}
5094
+ };
5095
+ window.addEventListener('popstate', onPop);
5096
+ return () => window.removeEventListener('popstate', onPop);
5097
+ }, []);
5098
+
5099
+ const toggleView = useCallback(() => {
5100
+ setViewMode(prev => prev === 'board' ? 'pipeline' : 'board');
5101
+ }, []);
4862
5102
 
4863
5103
  const fetchItems = useCallback(() => {
4864
5104
  setLoading(true);
@@ -5024,6 +5264,19 @@ function BoardPage({ selectedProject }) {
5024
5264
  }, []);
5025
5265
 
5026
5266
  const deleteItem = async (id) => {
5267
+ // Confirmation guard (2026-04-20, Agent C finding #10). The card
5268
+ // delete button was a bare onClick — one accidental click and a
5269
+ // task is gone. Add a native confirm() dialog so accidental
5270
+ // mid-drag clicks have an escape hatch. Kept simple (no custom
5271
+ // modal) to avoid touching the existing Preact component tree.
5272
+ const item = items.find(i => String(i.id) === String(id));
5273
+ const title = item ? (item.name || item.title || '(untitled)') : 'this task';
5274
+ const truncated = title.length > 60 ? title.slice(0, 60) + '…' : title;
5275
+ if (!window.confirm('Delete task #' + id + '?\n\n' + truncated +
5276
+ '\n\nThis cannot be undone. Updates, costs, and attachments ' +
5277
+ 'will be lost.')) {
5278
+ return;
5279
+ }
5027
5280
  try {
5028
5281
  setError(null);
5029
5282
  await api('DELETE', '/api/board/items/' + id);
@@ -5139,6 +5392,35 @@ function BoardPage({ selectedProject }) {
5139
5392
  };
5140
5393
  }, [activeSwimlanesSet]);
5141
5394
 
5395
+ // Layout test surface: expose column metrics on window for scenario
5396
+ // verification. Guards on !loading and items.length > 0 so metrics are
5397
+ // only exposed once the board has actually rendered content.
5398
+ // requestAnimationFrame defers measurement until after the browser has
5399
+ // painted, ensuring scrollHeight/clientHeight reflect the real layout.
5400
+ useEffect(() => {
5401
+ if (loading || items.length === 0) return;
5402
+ const raf = requestAnimationFrame(() => {
5403
+ const columns = document.querySelectorAll('.kanban-items');
5404
+ if (columns.length === 0) return;
5405
+ const metrics = Array.from(columns).map(col => {
5406
+ const cs = getComputedStyle(col);
5407
+ return {
5408
+ scrollHeight: col.scrollHeight,
5409
+ clientHeight: col.clientHeight,
5410
+ isScrollable: col.scrollHeight > col.clientHeight,
5411
+ hasOverflowAuto: cs.overflowY === 'auto',
5412
+ maxHeight: cs.maxHeight,
5413
+ };
5414
+ });
5415
+ window.__BEASTMODE_BOARD_LAYOUT__ = {
5416
+ columnCount: columns.length,
5417
+ columns: metrics,
5418
+ timestamp: Date.now(),
5419
+ };
5420
+ });
5421
+ return () => cancelAnimationFrame(raf);
5422
+ }, [items, loading]);
5423
+
5142
5424
  const toggleSwimlane = (key) => {
5143
5425
  setActiveSwimlanesSet(prev => {
5144
5426
  const next = new Set(prev);
@@ -5257,7 +5539,7 @@ function BoardPage({ selectedProject }) {
5257
5539
 
5258
5540
  <div class="board-header-row">
5259
5541
  <div class="board-stats-bar">
5260
- <span class="stat-label">Board</span>
5542
+ <span class="stat-label">${viewMode === 'pipeline' ? 'Pipeline' : 'Board'}</span>
5261
5543
  <span class="stat-item"><strong>${totalItems}</strong>${isFiltered ? '/' + items.length : ''} total</span>
5262
5544
  <span class="stat-item"><strong>${activeItems}</strong> active</span>
5263
5545
  <span class="stat-item" style="color: var(--success);"><strong style="color: var(--success);">${doneItems}</strong> done</span>
@@ -5265,6 +5547,8 @@ function BoardPage({ selectedProject }) {
5265
5547
  <span class="stat-item" style="color: var(--accent);"><strong style="color: var(--accent); font-family: var(--font-mono);">${progressPct}%</strong> complete</span>
5266
5548
  </div>
5267
5549
 
5550
+ <${ViewToggle} viewMode=${viewMode} onToggle=${toggleView} />
5551
+
5268
5552
  <div class="board-search-wrap">
5269
5553
  <input type="text" placeholder="Search tasks..." value=${searchTerm} onInput=${(e) => setSearchTerm(e.target.value)} />
5270
5554
  ${searchTerm && html`<button class="board-search-clear" onClick=${() => setSearchTerm('')}>\u00d7</button>`}
@@ -5438,7 +5722,16 @@ function BoardPage({ selectedProject }) {
5438
5722
  const label = cost ? formatCost(cost.total_cost_usd) : null;
5439
5723
  return label ? html`<span class="card-badge badge-cost" title=${'Total cost: ' + label}>${label}</span>` : null;
5440
5724
  })()}
5441
- <span class="card-time">${timeAgo(item.updated_at || item.created_at)}</span>
5725
+ ${(() => {
5726
+ // Time-in-status badge (2026-04-20): answers "how long has
5727
+ // this been waiting?" without the user having to click into
5728
+ // the task. Falls back to updated_at for pre-v5 rows.
5729
+ const src = item.status_changed_at || item.updated_at || item.created_at;
5730
+ const age = statusAge(src);
5731
+ if (!age.text) return null;
5732
+ return html`<span class=${'card-status-age status-age-' + age.severity}
5733
+ title=${'In status since ' + (src || 'unknown') + ' (updated ' + timeAgo(item.updated_at || item.created_at) + ')'}>${age.text}</span>`;
5734
+ })()}
5442
5735
  </div>
5443
5736
  </div>
5444
5737
  `;
@@ -8486,7 +8779,7 @@ function App() {
8486
8779
 
8487
8780
  render(html`<${App} />`, document.getElementById('app'));
8488
8781
  </script>
8489
- <div id="build-stamp-footer" style="position:fixed;bottom:4px;right:8px;font-size:10px;color:rgba(255,255,255,0.2);font-family:'JetBrains Mono',monospace;pointer-events:none;z-index:1;user-select:all"></div>
8782
+ <div id="build-stamp-footer" style="position:fixed;bottom:4px;right:8px;font-size:10px;color:rgba(255,255,255,0.2);font-family:'JetBrains Mono',monospace;pointer-events:none;z-index:1;user-select:all;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></div>
8490
8783
  <script>if(window.__BUILD_STAMP__){document.getElementById('build-stamp-footer').textContent=window.__BUILD_STAMP__}</script>
8491
8784
  </body>
8492
8785
  </html>
@@ -1 +1 @@
1
- b246bc0f7d538fb6ed16ab1834d7323cbd6268dd
1
+ 483e679664e425769ab39366cf74c472bf72c938
@@ -1 +1 @@
1
- 20260419-211451-b246bc0
1
+ 20260420-073821-483e679
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.133",
3
+ "version": "0.1.135",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {