bosun 0.29.2 → 0.29.4
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.
- package/.env.example +1 -1
- package/agent-pool.mjs +21 -3
- package/agent-sdk.mjs +2 -2
- package/cli.mjs +12 -0
- package/codex-config.mjs +261 -39
- package/codex-shell.mjs +3 -3
- package/config-doctor.mjs +244 -0
- package/monitor.mjs +31 -0
- package/package.json +1 -1
- package/setup.mjs +227 -75
- package/task-executor.mjs +4 -0
- package/ui/components/chat-view.js +69 -53
- package/ui/components/session-list.js +4 -4
- package/ui/components/shared.js +4 -0
- package/ui/components/workspace-switcher.js +17 -6
- package/ui/demo.html +19 -1
- package/ui/styles/sessions.css +167 -21
- package/ui/styles/workspace-switcher.css +21 -0
- package/ui/tabs/chat.js +10 -0
- package/ui/tabs/dashboard.js +26 -32
- package/ui/tabs/tasks.js +43 -6
- package/ui-server.mjs +12 -0
- package/workspace-manager.mjs +37 -0
|
@@ -469,19 +469,30 @@ export function WorkspaceSwitcher() {
|
|
|
469
469
|
|
|
470
470
|
const activeWs = workspaces.value.find((ws) => ws.id === activeWorkspaceId.value);
|
|
471
471
|
const wsList = workspaces.value;
|
|
472
|
+
const isLoading = workspacesLoading.value;
|
|
472
473
|
|
|
473
|
-
|
|
474
|
+
// Always render — even with no workspaces, show a manage button so users can add one
|
|
474
475
|
|
|
475
476
|
return html`
|
|
476
477
|
<div class="ws-switcher">
|
|
477
478
|
<button
|
|
478
|
-
class="ws-switcher-btn"
|
|
479
|
-
onClick=${(e) => {
|
|
480
|
-
|
|
479
|
+
class="ws-switcher-btn ${!wsList.length ? 'ws-switcher-btn-empty' : ''}"
|
|
480
|
+
onClick=${(e) => {
|
|
481
|
+
e.stopPropagation();
|
|
482
|
+
haptic("light");
|
|
483
|
+
if (!wsList.length) {
|
|
484
|
+
setManagerOpen(true);
|
|
485
|
+
} else {
|
|
486
|
+
setOpen(!open);
|
|
487
|
+
}
|
|
488
|
+
}}
|
|
489
|
+
title=${wsList.length ? "Switch workspace" : "Set up a workspace"}
|
|
481
490
|
>
|
|
482
491
|
<span class="ws-switcher-icon">⬡</span>
|
|
483
|
-
<span class="ws-switcher-name"
|
|
484
|
-
|
|
492
|
+
<span class="ws-switcher-name">
|
|
493
|
+
${isLoading ? "Loading…" : (activeWs?.name || (wsList.length ? "Select Workspace" : "Set up workspace"))}
|
|
494
|
+
</span>
|
|
495
|
+
${wsList.length ? html`<span class="ws-switcher-chevron ${open ? "open" : ""}">${open ? "▴" : "▾"}</span>` : null}
|
|
485
496
|
</button>
|
|
486
497
|
|
|
487
498
|
${open && html`
|
package/ui/demo.html
CHANGED
|
@@ -237,6 +237,24 @@
|
|
|
237
237
|
version: '0.26.2',
|
|
238
238
|
tasks: { completed: 13, failed: 1, retried: 2, active: 2, queued: 3 },
|
|
239
239
|
prs: { created: 15, merged: 11, pending: 2, ciFailures: 1 },
|
|
240
|
+
// counts consumed by dashboard metrics
|
|
241
|
+
counts: {
|
|
242
|
+
running: 2,
|
|
243
|
+
inprogress: 2,
|
|
244
|
+
inreview: 1,
|
|
245
|
+
review: 1,
|
|
246
|
+
done: 2,
|
|
247
|
+
todo: 2,
|
|
248
|
+
draft: 1,
|
|
249
|
+
error: 0,
|
|
250
|
+
},
|
|
251
|
+
backlog_remaining: 2,
|
|
252
|
+
// consumed by dashboard quality section
|
|
253
|
+
success_metrics: {
|
|
254
|
+
first_shot_rate: 72,
|
|
255
|
+
needed_fix: 3,
|
|
256
|
+
failed: 1,
|
|
257
|
+
},
|
|
240
258
|
};
|
|
241
259
|
|
|
242
260
|
const SEED_EXECUTORS = [
|
|
@@ -668,7 +686,7 @@
|
|
|
668
686
|
if (route === '/api/status')
|
|
669
687
|
return { data: STATE.status };
|
|
670
688
|
if (route === '/api/executor')
|
|
671
|
-
return { data: { ...STATE.status, maxParallel: STATE.maxParallel, paused: STATE.paused, executors: STATE.executors } };
|
|
689
|
+
return { data: { ...STATE.status, maxParallel: STATE.maxParallel, paused: STATE.paused, executors: STATE.executors, activeSlots: STATE.executors.filter(e => e.status === 'active').length, sdk: 'auto' } };
|
|
672
690
|
if (route === '/api/telemetry/summary')
|
|
673
691
|
return { data: { status: 'ok', updatedAt: Date.now(), totals: { tasks: STATE.tasks.length, executors: STATE.executors.length } } };
|
|
674
692
|
if (route === '/api/telemetry/errors')
|
package/ui/styles/sessions.css
CHANGED
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
background: rgba(5, 6, 8, 0.5);
|
|
36
36
|
opacity: 0;
|
|
37
37
|
pointer-events: none;
|
|
38
|
-
transition: opacity 0.
|
|
38
|
+
transition: opacity 0.28s cubic-bezier(0.32, 0.72, 0, 1);
|
|
39
39
|
z-index: 140;
|
|
40
|
+
-webkit-tap-highlight-color: transparent;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
.session-drawer-backdrop.open {
|
|
@@ -93,9 +94,11 @@
|
|
|
93
94
|
bottom: calc(var(--nav-height) + var(--safe-bottom));
|
|
94
95
|
width: min(88vw, 360px);
|
|
95
96
|
transform: translateX(-100%);
|
|
96
|
-
transition: transform 0.
|
|
97
|
+
transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
|
|
98
|
+
will-change: transform;
|
|
97
99
|
z-index: 160;
|
|
98
100
|
box-shadow: 12px 0 40px rgba(0, 0, 0, 0.35);
|
|
101
|
+
background: var(--bg-secondary);
|
|
99
102
|
}
|
|
100
103
|
.session-split.drawer-open .session-pane {
|
|
101
104
|
transform: translateX(0);
|
|
@@ -150,7 +153,7 @@
|
|
|
150
153
|
display: flex;
|
|
151
154
|
align-items: center;
|
|
152
155
|
padding: 0;
|
|
153
|
-
border-bottom: 1px solid var(--border
|
|
156
|
+
border-bottom: 1px solid var(--border);
|
|
154
157
|
background: var(--bg-primary);
|
|
155
158
|
flex-shrink: 0;
|
|
156
159
|
}
|
|
@@ -177,6 +180,7 @@
|
|
|
177
180
|
font-size: 12px;
|
|
178
181
|
font-weight: 600;
|
|
179
182
|
cursor: pointer;
|
|
183
|
+
transition: color var(--transition-fast), border-color var(--transition-fast);
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
.session-drawer-btn:hover {
|
|
@@ -184,6 +188,17 @@
|
|
|
184
188
|
border-color: var(--border-strong);
|
|
185
189
|
}
|
|
186
190
|
|
|
191
|
+
.session-drawer-btn:active {
|
|
192
|
+
transform: scale(0.96);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* On tablet+, the session pane is inline — hide the hamburger drawer toggle */
|
|
196
|
+
@media (min-width: 769px) {
|
|
197
|
+
.session-drawer-btn {
|
|
198
|
+
display: none;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
187
202
|
.chat-shell-title {
|
|
188
203
|
display: flex;
|
|
189
204
|
flex-direction: column;
|
|
@@ -569,6 +584,12 @@
|
|
|
569
584
|
.chat-stream-dot.executing,
|
|
570
585
|
.chat-stream-dot.streaming {
|
|
571
586
|
background: var(--accent);
|
|
587
|
+
animation: streamDotPulse 1.8s ease-in-out infinite;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
@keyframes streamDotPulse {
|
|
591
|
+
0%, 100% { box-shadow: 0 0 0 4px rgba(218, 119, 86, 0.1); }
|
|
592
|
+
50% { box-shadow: 0 0 0 6px rgba(218, 119, 86, 0.25); }
|
|
572
593
|
}
|
|
573
594
|
|
|
574
595
|
.chat-stream-dot.paused {
|
|
@@ -749,7 +770,12 @@
|
|
|
749
770
|
flex: 1;
|
|
750
771
|
min-height: 0;
|
|
751
772
|
overflow-y: auto;
|
|
773
|
+
overflow-x: hidden;
|
|
752
774
|
-webkit-overflow-scrolling: touch;
|
|
775
|
+
overscroll-behavior-y: contain;
|
|
776
|
+
scroll-padding-bottom: 80px;
|
|
777
|
+
scrollbar-width: thin;
|
|
778
|
+
scrollbar-color: rgba(255, 255, 255, 0.08) transparent;
|
|
753
779
|
padding: 20px 16px 48px;
|
|
754
780
|
display: flex;
|
|
755
781
|
flex-direction: column;
|
|
@@ -757,6 +783,23 @@
|
|
|
757
783
|
align-items: stretch;
|
|
758
784
|
}
|
|
759
785
|
|
|
786
|
+
.chat-messages::-webkit-scrollbar {
|
|
787
|
+
width: 6px;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.chat-messages::-webkit-scrollbar-track {
|
|
791
|
+
background: transparent;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.chat-messages::-webkit-scrollbar-thumb {
|
|
795
|
+
background: rgba(255, 255, 255, 0.1);
|
|
796
|
+
border-radius: 3px;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.chat-messages::-webkit-scrollbar-thumb:hover {
|
|
800
|
+
background: rgba(255, 255, 255, 0.18);
|
|
801
|
+
}
|
|
802
|
+
|
|
760
803
|
.chat-load-earlier {
|
|
761
804
|
display: flex;
|
|
762
805
|
align-items: center;
|
|
@@ -821,6 +864,11 @@
|
|
|
821
864
|
background: var(--bg-card);
|
|
822
865
|
align-self: flex-start;
|
|
823
866
|
box-shadow: var(--shadow-sm);
|
|
867
|
+
/* CSS containment — tells the browser each bubble is layout-independent,
|
|
868
|
+
reducing style recalc and layout thrashing on long message lists */
|
|
869
|
+
content-visibility: auto;
|
|
870
|
+
contain-intrinsic-size: auto 80px;
|
|
871
|
+
contain: layout style;
|
|
824
872
|
}
|
|
825
873
|
|
|
826
874
|
.chat-bubble.user {
|
|
@@ -888,19 +936,50 @@
|
|
|
888
936
|
margin-top: 4px;
|
|
889
937
|
}
|
|
890
938
|
|
|
939
|
+
/* Subtle entrance for bubbles — only the last few get the animation
|
|
940
|
+
(content-visibility: auto on older bubbles skips them for free) */
|
|
941
|
+
.chat-bubble:last-child,
|
|
942
|
+
.chat-bubble:nth-last-child(2),
|
|
943
|
+
.chat-bubble:nth-last-child(3) {
|
|
944
|
+
animation: chatBubbleIn 0.18s ease-out;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
@keyframes chatBubbleIn {
|
|
948
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
949
|
+
to { opacity: 1; transform: translateY(0); }
|
|
950
|
+
}
|
|
951
|
+
|
|
891
952
|
.chat-jump-latest {
|
|
892
953
|
position: absolute;
|
|
893
954
|
left: 0;
|
|
894
955
|
right: 0;
|
|
895
|
-
|
|
956
|
+
/* position above the input bar (~60px) + gap */
|
|
957
|
+
bottom: 72px;
|
|
896
958
|
display: flex;
|
|
897
959
|
justify-content: center;
|
|
898
960
|
pointer-events: none;
|
|
899
961
|
z-index: 6;
|
|
962
|
+
animation: fadeSlideUp 0.2s ease;
|
|
900
963
|
}
|
|
901
964
|
|
|
902
965
|
.chat-jump-latest .btn {
|
|
903
966
|
pointer-events: auto;
|
|
967
|
+
border-radius: var(--radius-full);
|
|
968
|
+
padding: 6px 16px;
|
|
969
|
+
font-size: 12px;
|
|
970
|
+
font-weight: 600;
|
|
971
|
+
box-shadow: var(--shadow-md);
|
|
972
|
+
backdrop-filter: blur(8px);
|
|
973
|
+
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.chat-jump-latest .btn:hover {
|
|
977
|
+
transform: translateY(-1px);
|
|
978
|
+
box-shadow: var(--shadow-lg);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.chat-jump-latest .btn:active {
|
|
982
|
+
transform: scale(0.96);
|
|
904
983
|
}
|
|
905
984
|
|
|
906
985
|
/* ─── Code blocks ─── */
|
|
@@ -1253,8 +1332,8 @@ ul.md-list li::before {
|
|
|
1253
1332
|
right: 0;
|
|
1254
1333
|
max-height: 240px;
|
|
1255
1334
|
overflow-y: auto;
|
|
1256
|
-
background: var(--bg-card
|
|
1257
|
-
border: 1px solid var(--border
|
|
1335
|
+
background: var(--bg-card);
|
|
1336
|
+
border: 1px solid var(--border);
|
|
1258
1337
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
1259
1338
|
box-shadow: var(--shadow-lg, 0 4px 24px rgba(0,0,0,.35));
|
|
1260
1339
|
z-index: 9999;
|
|
@@ -1345,7 +1424,7 @@ ul.md-list li::before {
|
|
|
1345
1424
|
.chat-input-area {
|
|
1346
1425
|
position: relative;
|
|
1347
1426
|
padding: 16px 16px;
|
|
1348
|
-
border-top: 1px solid var(--border
|
|
1427
|
+
border-top: 1px solid var(--border);
|
|
1349
1428
|
background: var(--bg-surface, var(--bg-card));
|
|
1350
1429
|
flex-shrink: 0;
|
|
1351
1430
|
}
|
|
@@ -1391,20 +1470,12 @@ ul.md-list li::before {
|
|
|
1391
1470
|
padding: 0 0 calc(12px + var(--safe-bottom, 0px));
|
|
1392
1471
|
}
|
|
1393
1472
|
|
|
1394
|
-
.app-shell[data-tab="chat"] .chat-view-embedded .chat-messages {
|
|
1395
|
-
padding: 0 0 16px;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
.app-shell[data-tab="chat"] .chat-input-area {
|
|
1399
|
-
padding: 0 0 calc(12px + var(--safe-bottom, 0px));
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
1473
|
.chat-input-wrapper {
|
|
1403
1474
|
display: flex;
|
|
1404
1475
|
align-items: flex-end;
|
|
1405
1476
|
gap: 8px;
|
|
1406
|
-
background: var(--bg-card
|
|
1407
|
-
border: 1px solid var(--border
|
|
1477
|
+
background: var(--bg-card);
|
|
1478
|
+
border: 1px solid var(--border);
|
|
1408
1479
|
border-radius: 24px;
|
|
1409
1480
|
padding: 8px 8px 8px 16px;
|
|
1410
1481
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
@@ -1413,8 +1484,8 @@ ul.md-list li::before {
|
|
|
1413
1484
|
}
|
|
1414
1485
|
|
|
1415
1486
|
.chat-input-wrapper:focus-within {
|
|
1416
|
-
border-color: var(--accent
|
|
1417
|
-
box-shadow: 0 0 0 2px rgba(
|
|
1487
|
+
border-color: var(--accent);
|
|
1488
|
+
box-shadow: 0 0 0 2px var(--accent-soft, rgba(218, 119, 86, 0.12));
|
|
1418
1489
|
}
|
|
1419
1490
|
|
|
1420
1491
|
.chat-textarea {
|
|
@@ -1430,6 +1501,7 @@ ul.md-list li::before {
|
|
|
1430
1501
|
min-height: 24px;
|
|
1431
1502
|
max-height: 120px;
|
|
1432
1503
|
padding: 4px 0;
|
|
1504
|
+
scrollbar-width: thin;
|
|
1433
1505
|
}
|
|
1434
1506
|
|
|
1435
1507
|
.chat-textarea::placeholder {
|
|
@@ -1514,8 +1586,8 @@ ul.md-list li::before {
|
|
|
1514
1586
|
justify-content: center;
|
|
1515
1587
|
border: none;
|
|
1516
1588
|
cursor: pointer;
|
|
1517
|
-
padding: 0
|
|
1518
|
-
min-width:
|
|
1589
|
+
padding: 0 10px;
|
|
1590
|
+
min-width: 68px;
|
|
1519
1591
|
font-size: 11px;
|
|
1520
1592
|
font-family: inherit;
|
|
1521
1593
|
gap: 2px;
|
|
@@ -1556,6 +1628,7 @@ ul.md-list li::before {
|
|
|
1556
1628
|
font-weight: 600;
|
|
1557
1629
|
text-transform: uppercase;
|
|
1558
1630
|
letter-spacing: 0.3px;
|
|
1631
|
+
white-space: nowrap;
|
|
1559
1632
|
}
|
|
1560
1633
|
|
|
1561
1634
|
@keyframes session-pulse {
|
|
@@ -1752,3 +1825,76 @@ ul.md-list li::before {
|
|
|
1752
1825
|
background: rgba(245, 158, 11, 0.1);
|
|
1753
1826
|
border-radius: 4px;
|
|
1754
1827
|
}
|
|
1828
|
+
|
|
1829
|
+
/* ═══ Mobile Chat Refinements ═══ */
|
|
1830
|
+
@media (max-width: 768px) {
|
|
1831
|
+
.chat-shell-inner {
|
|
1832
|
+
padding: 8px 10px;
|
|
1833
|
+
gap: 8px;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
.chat-shell-name {
|
|
1837
|
+
font-size: 13px;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
.chat-shell-meta {
|
|
1841
|
+
font-size: 10px;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
.chat-input-wrapper {
|
|
1845
|
+
padding: 6px 6px 6px 12px;
|
|
1846
|
+
border-radius: 20px;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
.chat-textarea {
|
|
1850
|
+
font-size: 16px; /* prevent iOS zoom on focus */
|
|
1851
|
+
max-height: 100px;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
.chat-input-area .chat-send-btn {
|
|
1855
|
+
width: 34px;
|
|
1856
|
+
height: 34px;
|
|
1857
|
+
font-size: 15px;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
.chat-input-hint {
|
|
1861
|
+
font-size: 10px;
|
|
1862
|
+
padding: 3px 2px 0;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
.chat-bubble {
|
|
1866
|
+
padding: 8px 12px;
|
|
1867
|
+
font-size: 14px;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
.chat-bubble.user {
|
|
1871
|
+
max-width: min(85%, 480px);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
.chat-messages {
|
|
1875
|
+
padding: 12px 10px 40px;
|
|
1876
|
+
gap: 8px;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/* ═══ Desktop Chat Refinements ═══ */
|
|
1881
|
+
@media (min-width: 1200px) {
|
|
1882
|
+
.chat-messages {
|
|
1883
|
+
padding: 24px 20px 48px;
|
|
1884
|
+
gap: 10px;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
.chat-shell-inner {
|
|
1888
|
+
max-width: 1080px;
|
|
1889
|
+
padding: 12px 16px;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/* Wider input area on large screens */
|
|
1893
|
+
.chat-input-wrapper {
|
|
1894
|
+
max-width: min(840px, 100%);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
.chat-input-hint {
|
|
1898
|
+
max-width: min(840px, 100%);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
@@ -27,6 +27,17 @@
|
|
|
27
27
|
border-color: var(--color-primary, #10b981);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
.ws-switcher-btn.ws-switcher-btn-empty {
|
|
31
|
+
border-style: dashed;
|
|
32
|
+
opacity: 0.8;
|
|
33
|
+
font-style: italic;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.ws-switcher-btn.ws-switcher-btn-empty:hover {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
font-style: normal;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
.ws-switcher-icon {
|
|
31
42
|
font-size: 14px;
|
|
32
43
|
opacity: 0.7;
|
|
@@ -660,6 +671,16 @@
|
|
|
660
671
|
to { transform: rotate(360deg); }
|
|
661
672
|
}
|
|
662
673
|
|
|
674
|
+
/* ── Modal toolbar (inside shared Modal body) ──────────────── */
|
|
675
|
+
.ws-manager-modal-toolbar {
|
|
676
|
+
display: flex;
|
|
677
|
+
align-items: center;
|
|
678
|
+
gap: 8px;
|
|
679
|
+
margin-bottom: 12px;
|
|
680
|
+
padding-bottom: 12px;
|
|
681
|
+
border-bottom: 1px solid var(--border);
|
|
682
|
+
}
|
|
683
|
+
|
|
663
684
|
/* ── Responsive ──────────────────────────────────────────── */
|
|
664
685
|
@media (max-width: 540px) {
|
|
665
686
|
.ws-manager-panel {
|
package/ui/tabs/chat.js
CHANGED
|
@@ -386,6 +386,16 @@ export function ChatTab() {
|
|
|
386
386
|
} catch { /* signal read error - ignore */ }
|
|
387
387
|
}, [isMobile]);
|
|
388
388
|
|
|
389
|
+
/* ── Auto-focus textarea when switching sessions (desktop only) ── */
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (!sessionId || isMobile) return;
|
|
392
|
+
// Delay focus to let the ChatView mount first
|
|
393
|
+
const timer = setTimeout(() => {
|
|
394
|
+
if (textareaRef.current) textareaRef.current.focus();
|
|
395
|
+
}, 150);
|
|
396
|
+
return () => clearTimeout(timer);
|
|
397
|
+
}, [sessionId, isMobile]);
|
|
398
|
+
|
|
389
399
|
/* ── Slash command filtering ──
|
|
390
400
|
Use useMemo with no signal deps to avoid subscribing ChatTab to
|
|
391
401
|
activeAgentInfo. The commands list only matters when the user is
|
package/ui/tabs/dashboard.js
CHANGED
|
@@ -521,39 +521,33 @@ export function DashboardTab() {
|
|
|
521
521
|
`,
|
|
522
522
|
)}
|
|
523
523
|
</div>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
<span
|
|
539
|
-
class="dashboard-work-dot"
|
|
540
|
-
style="background: ${item.color}"
|
|
541
|
-
></span>
|
|
542
|
-
<span class="dashboard-work-label">${item.label}</span>
|
|
524
|
+
${segments.length > 0 && html`
|
|
525
|
+
<div class="dashboard-work-layout" style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border)">
|
|
526
|
+
<div class="dashboard-work-list">
|
|
527
|
+
${workItems.map(
|
|
528
|
+
(item) => html`
|
|
529
|
+
<div class="dashboard-work-item">
|
|
530
|
+
<div class="dashboard-work-left">
|
|
531
|
+
<span
|
|
532
|
+
class="dashboard-work-dot"
|
|
533
|
+
style="background: ${item.color}"
|
|
534
|
+
></span>
|
|
535
|
+
<span class="dashboard-work-label">${item.label}</span>
|
|
536
|
+
</div>
|
|
537
|
+
<span class="dashboard-work-value">${item.value}</span>
|
|
543
538
|
</div>
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
539
|
+
`,
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
<div class="dashboard-work-chart">
|
|
543
|
+
<${DonutChart} segments=${segments} size=${90} strokeWidth=${9} />
|
|
544
|
+
<div class="dashboard-work-meta">
|
|
545
|
+
${progressPct}% engaged
|
|
546
|
+
</div>
|
|
547
|
+
<${ProgressBar} percent=${progressPct} />
|
|
553
548
|
</div>
|
|
554
|
-
<${ProgressBar} percent=${progressPct} />
|
|
555
549
|
</div>
|
|
556
|
-
|
|
550
|
+
`}
|
|
557
551
|
<//>
|
|
558
552
|
|
|
559
553
|
<${Card}
|
|
@@ -683,8 +677,8 @@ export function DashboardTab() {
|
|
|
683
677
|
${truncate(task.title || "(untitled)", 50)}
|
|
684
678
|
</div>
|
|
685
679
|
<div class="meta-text">
|
|
686
|
-
${task.id}${task.updated_at
|
|
687
|
-
? ` · ${formatRelative(task.updated_at)}`
|
|
680
|
+
${task.id}${(task.updated_at || task.updated)
|
|
681
|
+
? ` · ${formatRelative(task.updated_at || task.updated)}`
|
|
688
682
|
: ""}
|
|
689
683
|
</div>
|
|
690
684
|
</div>
|
package/ui/tabs/tasks.js
CHANGED
|
@@ -89,6 +89,15 @@ const SORT_OPTIONS = [
|
|
|
89
89
|
{ value: "title", label: "Title" },
|
|
90
90
|
];
|
|
91
91
|
|
|
92
|
+
/* Maps snapshot-bar labels → tasksFilter values */
|
|
93
|
+
const SNAPSHOT_STATUS_MAP = {
|
|
94
|
+
Backlog: "todo",
|
|
95
|
+
Active: "inprogress",
|
|
96
|
+
Review: "inreview",
|
|
97
|
+
Done: "done",
|
|
98
|
+
Errors: "error",
|
|
99
|
+
};
|
|
100
|
+
|
|
92
101
|
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, "": 4 };
|
|
93
102
|
|
|
94
103
|
const SYSTEM_TAGS = new Set([
|
|
@@ -994,8 +1003,10 @@ export function TasksTab() {
|
|
|
994
1003
|
return dir * (av - bv);
|
|
995
1004
|
}
|
|
996
1005
|
if (listSortCol === "updated") {
|
|
997
|
-
|
|
998
|
-
|
|
1006
|
+
const tsA = a.updated_at || a.updated;
|
|
1007
|
+
const tsB = b.updated_at || b.updated;
|
|
1008
|
+
av = tsA ? (typeof tsA === 'number' ? tsA : new Date(tsA).getTime()) : 0;
|
|
1009
|
+
bv = tsB ? (typeof tsB === 'number' ? tsB : new Date(tsB).getTime()) : 0;
|
|
999
1010
|
return dir * (av - bv);
|
|
1000
1011
|
}
|
|
1001
1012
|
if (listSortCol === "status") { av = a.status || ""; bv = b.status || ""; }
|
|
@@ -1609,11 +1620,20 @@ export function TasksTab() {
|
|
|
1609
1620
|
|
|
1610
1621
|
<div class="snapshot-bar">
|
|
1611
1622
|
${summaryMetrics.map((m) => html`
|
|
1612
|
-
<
|
|
1623
|
+
<button
|
|
1624
|
+
key=${m.label}
|
|
1625
|
+
class="snapshot-pill snapshot-pill-btn ${!isKanban && filterVal === SNAPSHOT_STATUS_MAP[m.label] ? 'snapshot-pill-active' : ''}"
|
|
1626
|
+
onClick=${() => {
|
|
1627
|
+
if (isKanban) return;
|
|
1628
|
+
const statusVal = SNAPSHOT_STATUS_MAP[m.label];
|
|
1629
|
+
if (statusVal !== undefined) handleFilter(filterVal === statusVal ? 'all' : statusVal);
|
|
1630
|
+
}}
|
|
1631
|
+
title=${isKanban ? m.label : `Filter by ${m.label}`}
|
|
1632
|
+
>
|
|
1613
1633
|
<span class="snapshot-dot" style="background:${m.color};" />
|
|
1614
1634
|
<strong class="snapshot-val">${m.value}</strong>
|
|
1615
1635
|
<span class="snapshot-lbl">${m.label}</span>
|
|
1616
|
-
</
|
|
1636
|
+
</button>
|
|
1617
1637
|
`)}
|
|
1618
1638
|
<span class="snapshot-view-tag">${isKanban ? "⬛ Board" : "☰ List"}</span>
|
|
1619
1639
|
</div>
|
|
@@ -1632,6 +1652,23 @@ export function TasksTab() {
|
|
|
1632
1652
|
cursor:pointer;
|
|
1633
1653
|
}
|
|
1634
1654
|
.actions-dropdown-item:hover { background:var(--hover-bg, rgba(255,255,255,.08)); }
|
|
1655
|
+
.snapshot-pill-btn {
|
|
1656
|
+
background: none;
|
|
1657
|
+
border: 1px solid transparent;
|
|
1658
|
+
border-radius: 999px;
|
|
1659
|
+
cursor: pointer;
|
|
1660
|
+
padding: 2px 8px;
|
|
1661
|
+
transition: border-color 0.15s, background 0.15s;
|
|
1662
|
+
font: inherit;
|
|
1663
|
+
}
|
|
1664
|
+
.snapshot-pill-btn:hover {
|
|
1665
|
+
border-color: var(--border);
|
|
1666
|
+
background: var(--bg-card-hover, rgba(255,255,255,0.05));
|
|
1667
|
+
}
|
|
1668
|
+
.snapshot-pill-active {
|
|
1669
|
+
border-color: var(--accent) !important;
|
|
1670
|
+
background: rgba(59,130,246,0.12) !important;
|
|
1671
|
+
}
|
|
1635
1672
|
@media (max-width: 640px) {
|
|
1636
1673
|
.actions-label { display:none; }
|
|
1637
1674
|
}
|
|
@@ -1706,8 +1743,8 @@ export function TasksTab() {
|
|
|
1706
1743
|
: html`<span class="task-td-empty">—</span>`}
|
|
1707
1744
|
</td>
|
|
1708
1745
|
<td class="task-td task-td-updated">
|
|
1709
|
-
${task.updated_at
|
|
1710
|
-
? html`<span class="task-td-date">${formatRelative(task.updated_at)}</span>`
|
|
1746
|
+
${(task.updated_at || task.updated)
|
|
1747
|
+
? html`<span class="task-td-date">${formatRelative(task.updated_at || task.updated)}</span>`
|
|
1711
1748
|
: html`<span class="task-td-empty">—</span>`}
|
|
1712
1749
|
</td>
|
|
1713
1750
|
</tr>
|
package/ui-server.mjs
CHANGED
|
@@ -3435,6 +3435,18 @@ async function handleApi(req, res, url) {
|
|
|
3435
3435
|
return;
|
|
3436
3436
|
}
|
|
3437
3437
|
|
|
3438
|
+
if (path === "/api/workspace-health") {
|
|
3439
|
+
try {
|
|
3440
|
+
const { runWorkspaceHealthCheck } = await import("./config-doctor.mjs");
|
|
3441
|
+
const configDir = resolveUiConfigDir();
|
|
3442
|
+
const result = runWorkspaceHealthCheck({ configDir });
|
|
3443
|
+
jsonResponse(res, 200, { ok: result.ok, data: result });
|
|
3444
|
+
} catch (err) {
|
|
3445
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
3446
|
+
}
|
|
3447
|
+
return;
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3438
3450
|
if (path === "/api/worktrees") {
|
|
3439
3451
|
try {
|
|
3440
3452
|
const worktrees = listActiveWorktrees(repoRoot);
|
package/workspace-manager.mjs
CHANGED
|
@@ -26,6 +26,39 @@ import { execSync, spawnSync } from "node:child_process";
|
|
|
26
26
|
|
|
27
27
|
const TAG = "[workspace-manager]";
|
|
28
28
|
|
|
29
|
+
// Lazy-loaded reference to repo-config.mjs (resolved on first use)
|
|
30
|
+
let _repoConfigModule = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensure repo-level AI executor configs exist after clone/pull.
|
|
34
|
+
* Uses synchronous import cache — repo-config.mjs is pure sync.
|
|
35
|
+
* @param {string} repoPath Absolute path to the repo directory
|
|
36
|
+
*/
|
|
37
|
+
function ensureRepoAIConfigs(repoPath) {
|
|
38
|
+
try {
|
|
39
|
+
if (!_repoConfigModule) {
|
|
40
|
+
// repo-config.mjs is ESM but fully synchronous internally.
|
|
41
|
+
// We pre-populate the cache via dynamic import at module init.
|
|
42
|
+
return; // Will be populated after first async import
|
|
43
|
+
}
|
|
44
|
+
const { ensureRepoConfigs, printRepoConfigSummary } = _repoConfigModule;
|
|
45
|
+
const result = ensureRepoConfigs(repoPath);
|
|
46
|
+
// Only log if something was created/updated
|
|
47
|
+
const anyChange = Object.values(result).some((r) => r.created || r.updated);
|
|
48
|
+
if (anyChange) {
|
|
49
|
+
console.log(TAG, `Repo-level AI configs for ${basename(repoPath)}:`);
|
|
50
|
+
printRepoConfigSummary(result, (msg) => console.log(TAG, msg));
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.warn(TAG, `Could not ensure repo AI configs: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Pre-load repo-config.mjs asynchronously at module init time
|
|
58
|
+
import("./repo-config.mjs")
|
|
59
|
+
.then((mod) => { _repoConfigModule = mod; })
|
|
60
|
+
.catch(() => { /* repo-config not available — skip */ });
|
|
61
|
+
|
|
29
62
|
// ── Path Helpers ─────────────────────────────────────────────────────────────
|
|
30
63
|
|
|
31
64
|
/**
|
|
@@ -313,8 +346,12 @@ export function addRepoToWorkspace(configDir, workspaceId, { url, name, branch,
|
|
|
313
346
|
}
|
|
314
347
|
cloned = true;
|
|
315
348
|
console.log(TAG, `Cloned ${repoName} successfully`);
|
|
349
|
+
// Ensure repo-level AI executor configs exist after fresh clone
|
|
350
|
+
ensureRepoAIConfigs(repoPath);
|
|
316
351
|
} else {
|
|
317
352
|
console.log(TAG, `Repository ${repoName} already exists at ${repoPath}`);
|
|
353
|
+
// Ensure repo-level configs are up to date even for existing repos
|
|
354
|
+
ensureRepoAIConfigs(repoPath);
|
|
318
355
|
}
|
|
319
356
|
|
|
320
357
|
const slug = extractSlug(url);
|