codehere 0.3.4 → 0.5.0-alpha.2
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/README.md +68 -11
- package/dist/index.js +43 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +18 -3
- package/dist/infrastructure/orchestration/aider-adapter.js +12 -2
- package/dist/infrastructure/orchestration/claude-code-adapter.js +9 -1
- package/dist/infrastructure/orchestration/codex-adapter.js +6 -1
- package/dist/infrastructure/orchestration/conductor-adapter.js +12 -2
- package/dist/infrastructure/orchestration/gemini-adapter.js +425 -0
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +10 -2
- package/dist/infrastructure/orchestration/memory-injector.js +337 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +34 -4
- package/dist/infrastructure/orchestration/progress-aggregator.js +157 -0
- package/dist/infrastructure/orchestration/readiness.js +2 -5
- package/dist/infrastructure/orchestration/session-manager.js +2 -1
- package/dist/infrastructure/orchestration/session-persistence.js +13 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +30 -3
- package/dist/infrastructure/protocols/ui-page.js +468 -59
- package/dist/presentation/cli/commands/init-command.js +342 -24
- package/dist/presentation/cli/commands/memory-command.js +1017 -0
- package/dist/presentation/cli/commands/protocols-builder.js +254 -0
- package/dist/presentation/cli/commands/protocols-command.js +84 -0
- package/dist/presentation/cli/commands/repo-map-builder.js +247 -0
- package/package.json +4 -3
|
@@ -696,6 +696,77 @@ ${SHARED_STYLES}
|
|
|
696
696
|
|
|
697
697
|
.shell { max-width: 960px; margin: 0 auto; padding: 90px 24px 24px; position: relative; }
|
|
698
698
|
|
|
699
|
+
/* ── Apr 29 — Slack-shape 3-pane layout ──────────────────────────────────
|
|
700
|
+
The app surface restructures from single-column dashboard to
|
|
701
|
+
channels (left) + threads (center) + members (right). Single
|
|
702
|
+
channel = current cwd; members = available agents from readiness API.
|
|
703
|
+
Multi-channel + live presence dots from F1d detection are a later release. */
|
|
704
|
+
.shell.slack-shape {
|
|
705
|
+
max-width: 1440px;
|
|
706
|
+
display: grid;
|
|
707
|
+
grid-template-columns: 200px 1fr 220px;
|
|
708
|
+
gap: 16px;
|
|
709
|
+
align-items: start;
|
|
710
|
+
}
|
|
711
|
+
@media (max-width: 1100px) {
|
|
712
|
+
.shell.slack-shape { grid-template-columns: 1fr; }
|
|
713
|
+
.shell.slack-shape .channels-pane, .shell.slack-shape .members-pane {
|
|
714
|
+
position: static; order: 99;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
.channels-pane, .members-pane {
|
|
718
|
+
background: rgba(255,255,255,0.025);
|
|
719
|
+
border: 1px solid var(--border-soft);
|
|
720
|
+
border-radius: 10px;
|
|
721
|
+
padding: 14px 12px;
|
|
722
|
+
position: sticky; top: 88px;
|
|
723
|
+
font-family: var(--mono);
|
|
724
|
+
z-index: 1;
|
|
725
|
+
}
|
|
726
|
+
.pane-head {
|
|
727
|
+
font-size: 9.5px; letter-spacing: 0.16em;
|
|
728
|
+
color: var(--fg-30); margin-bottom: 10px;
|
|
729
|
+
text-transform: uppercase;
|
|
730
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
731
|
+
}
|
|
732
|
+
.pane-head .pane-count { color: var(--fg-50); }
|
|
733
|
+
.pane-add {
|
|
734
|
+
background: transparent; border: 1px dashed var(--border);
|
|
735
|
+
color: var(--fg-50); font-family: var(--mono); font-size: 10px;
|
|
736
|
+
padding: 4px 8px; border-radius: 6px; cursor: pointer;
|
|
737
|
+
width: 100%; margin-top: 8px;
|
|
738
|
+
}
|
|
739
|
+
.pane-add:hover { color: var(--fg); border-color: var(--fg-30); border-style: solid; }
|
|
740
|
+
.channel, .member {
|
|
741
|
+
display: flex; align-items: center; gap: 8px;
|
|
742
|
+
padding: 6px 8px; border-radius: 6px;
|
|
743
|
+
font-size: 11.5px;
|
|
744
|
+
cursor: pointer;
|
|
745
|
+
color: var(--fg-70);
|
|
746
|
+
border: 1px solid transparent;
|
|
747
|
+
}
|
|
748
|
+
.channel:hover, .member:hover { background: rgba(255,255,255,0.04); color: var(--fg); }
|
|
749
|
+
.channel.active { background: rgba(255,154,118,0.08); color: var(--fg); border-color: rgba(255,154,118,0.25); }
|
|
750
|
+
.channel-glyph, .member-presence {
|
|
751
|
+
color: var(--fg-40); flex: 0 0 14px; text-align: center;
|
|
752
|
+
}
|
|
753
|
+
.member-presence.ready { color: var(--allow); }
|
|
754
|
+
.member-presence.warn { color: var(--warn); }
|
|
755
|
+
.member-presence.offline { color: var(--fg-30); }
|
|
756
|
+
.channel-name, .member-label {
|
|
757
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
758
|
+
flex: 1; min-width: 0;
|
|
759
|
+
}
|
|
760
|
+
.member-cwd-hint, .channel-cwd-hint {
|
|
761
|
+
font-size: 9.5px; color: var(--fg-30);
|
|
762
|
+
margin-top: 4px; padding-left: 22px;
|
|
763
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
764
|
+
}
|
|
765
|
+
.pane-empty {
|
|
766
|
+
color: var(--fg-30); font-size: 10.5px;
|
|
767
|
+
padding: 8px; text-align: center;
|
|
768
|
+
}
|
|
769
|
+
|
|
699
770
|
/* ── Readiness banner (F1a) ─────────────────────────────────────────── */
|
|
700
771
|
.readiness {
|
|
701
772
|
margin: 12px 0 0;
|
|
@@ -973,6 +1044,103 @@ ${SHARED_STYLES}
|
|
|
973
1044
|
.event-log .event.codehere-error .label { color: var(--block); display: block; margin-bottom: 4px; }
|
|
974
1045
|
.event-log .event.codehere-error .recovery { color: var(--fg-70); margin-top: 4px; display: block; }
|
|
975
1046
|
|
|
1047
|
+
/* Apr 29 — Slack-shape thread view.
|
|
1048
|
+
Expanded session card defaults to thread view; raw event-log opt-in via
|
|
1049
|
+
the Thread/Log toggle in the card head. Each event becomes a message:
|
|
1050
|
+
user prompt seeds it; agent text/tool/hook events stream in as replies.
|
|
1051
|
+
The reply input is a visual scaffold — wires up as a real follow-up
|
|
1052
|
+
queue in a later release. */
|
|
1053
|
+
.thread-messages {
|
|
1054
|
+
margin: 10px 0 0;
|
|
1055
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
1056
|
+
max-height: 420px; overflow-y: auto;
|
|
1057
|
+
padding-right: 4px;
|
|
1058
|
+
}
|
|
1059
|
+
.thread-message { display: flex; gap: 10px; padding: 6px 0; }
|
|
1060
|
+
.thread-message .avatar {
|
|
1061
|
+
flex: 0 0 28px; height: 28px;
|
|
1062
|
+
border-radius: 50%;
|
|
1063
|
+
display: flex; align-items: center; justify-content: center;
|
|
1064
|
+
font-size: 13px;
|
|
1065
|
+
background: rgba(255,255,255,0.04); border: 1px solid var(--border-soft);
|
|
1066
|
+
}
|
|
1067
|
+
.thread-message.user .avatar { background: rgba(74,144,226,0.18); border-color: rgba(74,144,226,0.45); }
|
|
1068
|
+
.thread-message.agent .avatar { background: rgba(255,154,118,0.18); border-color: rgba(255,154,118,0.45); }
|
|
1069
|
+
.thread-message.tool .avatar { background: rgba(255,154,118,0.08); }
|
|
1070
|
+
.thread-message.hook .avatar { background: rgba(127,193,154,0.18); border-color: rgba(127,193,154,0.4); }
|
|
1071
|
+
.thread-message.hook.blocked .avatar { background: rgba(255,95,87,0.18); border-color: rgba(255,95,87,0.45); }
|
|
1072
|
+
.thread-message.error .avatar { background: rgba(255,95,87,0.18); border-color: rgba(255,95,87,0.45); }
|
|
1073
|
+
.thread-message .body { flex: 1; min-width: 0; }
|
|
1074
|
+
.thread-message .head { display: flex; gap: 8px; align-items: baseline; font-family: var(--mono); font-size: 10.5px; }
|
|
1075
|
+
.thread-message .author { color: var(--fg); font-weight: 600; }
|
|
1076
|
+
.thread-message .time { color: var(--fg-40); }
|
|
1077
|
+
.thread-message .content {
|
|
1078
|
+
color: var(--fg-80); font-family: var(--mono); font-size: 11.5px;
|
|
1079
|
+
line-height: 1.55; margin-top: 3px; white-space: pre-wrap; word-break: break-word;
|
|
1080
|
+
}
|
|
1081
|
+
.thread-message.tool .content { color: var(--fg-60); }
|
|
1082
|
+
.thread-message.hook .content { color: var(--allow); }
|
|
1083
|
+
.thread-message.hook.blocked .content { color: var(--block); }
|
|
1084
|
+
.thread-message.error .content { color: var(--block); }
|
|
1085
|
+
|
|
1086
|
+
.thread-input {
|
|
1087
|
+
margin-top: 14px;
|
|
1088
|
+
padding: 10px 12px;
|
|
1089
|
+
background: rgba(255,255,255,0.02);
|
|
1090
|
+
border: 1px solid var(--border-soft); border-radius: 8px;
|
|
1091
|
+
color: var(--fg-50); font-size: 11px; font-family: var(--mono);
|
|
1092
|
+
display: flex; align-items: center; gap: 8px;
|
|
1093
|
+
}
|
|
1094
|
+
.thread-input::before { content: '↳'; color: var(--fg-30); }
|
|
1095
|
+
.thread-input .scaffold-note { color: var(--fg-30); font-size: 10px; margin-left: auto; }
|
|
1096
|
+
|
|
1097
|
+
.view-toggle {
|
|
1098
|
+
display: inline-flex; gap: 0;
|
|
1099
|
+
border: 1px solid var(--border-soft); border-radius: 6px;
|
|
1100
|
+
overflow: hidden;
|
|
1101
|
+
margin-right: 8px;
|
|
1102
|
+
}
|
|
1103
|
+
.view-toggle button {
|
|
1104
|
+
background: transparent; border: 0;
|
|
1105
|
+
color: var(--fg-60); padding: 3px 9px;
|
|
1106
|
+
font-family: var(--mono); font-size: 10px;
|
|
1107
|
+
cursor: pointer;
|
|
1108
|
+
}
|
|
1109
|
+
.view-toggle button.active { color: var(--fg); background: rgba(255,255,255,0.06); }
|
|
1110
|
+
.view-toggle button:hover { color: var(--fg); }
|
|
1111
|
+
|
|
1112
|
+
/* Toggle visibility based on data-view attribute on session-body */
|
|
1113
|
+
.session-body[data-view="thread"] .event-log { display: none; }
|
|
1114
|
+
.session-body[data-view="raw"] .thread-messages { display: none; }
|
|
1115
|
+
.session-body[data-view="raw"] .thread-input { display: none; }
|
|
1116
|
+
|
|
1117
|
+
/* Apr 29 — UX overhaul: collapsed single-line stats row (replaces stacked
|
|
1118
|
+
audit-summary + status-banner). Sits BELOW the intake, not above. Muted
|
|
1119
|
+
so it doesn't compete with the H1 / textarea for eye attention. */
|
|
1120
|
+
.stats-line {
|
|
1121
|
+
margin: 14px 0 0;
|
|
1122
|
+
font-family: var(--mono); font-size: 10.5px; color: var(--fg-40);
|
|
1123
|
+
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
|
1124
|
+
}
|
|
1125
|
+
.stats-line b { color: var(--fg-70); font-weight: 500; }
|
|
1126
|
+
.stats-line .sep { color: var(--fg-20); }
|
|
1127
|
+
.stats-line .blocked b { color: var(--block); }
|
|
1128
|
+
.stats-line.empty { display: none; }
|
|
1129
|
+
|
|
1130
|
+
/* Apr 29 — UX overhaul: readiness banner is now compact + below intake.
|
|
1131
|
+
Was: full-width yellow box with multi-line prose above the H1.
|
|
1132
|
+
Is: tighter padding, single-line preferred, sits as a subtle hint below
|
|
1133
|
+
the assign action. The detail text wraps when long but contrast is muted. */
|
|
1134
|
+
.readiness { margin-top: 14px; padding: 10px 12px; }
|
|
1135
|
+
.readiness .r-title { font-size: 11px; }
|
|
1136
|
+
.readiness .r-detail { font-size: 10.5px; }
|
|
1137
|
+
.readiness .r-action { margin-top: 6px; }
|
|
1138
|
+
|
|
1139
|
+
/* Apr 29 — UX overhaul: TRY chips fade out as soon as the user starts
|
|
1140
|
+
typing. They're a discovery aid for first-visit users, not a permanent
|
|
1141
|
+
fixture above an active textarea. */
|
|
1142
|
+
.starter-prompts.dim { opacity: 0.35; pointer-events: none; transition: opacity 200ms ease; }
|
|
1143
|
+
|
|
976
1144
|
#empty { padding: 48px 0; text-align: center; color: var(--fg-50); }
|
|
977
1145
|
#empty .pill { display: inline-block; padding: 6px 14px; border: 1px solid var(--border); border-radius: 999px; background: rgba(255,255,255,0.03); font-family: var(--mono); font-size: 11px; color: var(--fg-70); }
|
|
978
1146
|
</style>
|
|
@@ -984,62 +1152,80 @@ ${SHARED_STYLES}
|
|
|
984
1152
|
<a href="/history" class="nav-link">History</a>
|
|
985
1153
|
</nav>
|
|
986
1154
|
|
|
987
|
-
<div class="shell">
|
|
988
|
-
|
|
989
|
-
<
|
|
990
|
-
|
|
991
|
-
<div
|
|
1155
|
+
<div class="shell slack-shape">
|
|
1156
|
+
|
|
1157
|
+
<aside class="channels-pane">
|
|
1158
|
+
<div class="pane-head"><span>Projects</span><span class="pane-count" id="channels-count">1</span></div>
|
|
1159
|
+
<div id="channels-list">
|
|
1160
|
+
<div class="channel active" data-channel="current">
|
|
1161
|
+
<span class="channel-glyph">#</span>
|
|
1162
|
+
<div style="flex:1;min-width:0">
|
|
1163
|
+
<div class="channel-name" id="current-channel-name">project</div>
|
|
1164
|
+
<div class="channel-cwd-hint" id="current-channel-cwd">~</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
</div>
|
|
1168
|
+
<button class="pane-add" type="button" title="Coming soon">+ New project</button>
|
|
1169
|
+
</aside>
|
|
992
1170
|
|
|
993
|
-
<
|
|
994
|
-
<span class="eyebrow">TODAY</span>
|
|
995
|
-
<span class="sep">·</span>
|
|
996
|
-
<span><b id="as-actions">0</b> actions</span>
|
|
997
|
-
<span class="sep">·</span>
|
|
998
|
-
<span class="blocked"><b id="as-blocked">0</b> blocked</span>
|
|
999
|
-
<span class="sep">·</span>
|
|
1000
|
-
<span>avg trust <b id="as-trust">—</b></span>
|
|
1001
|
-
<span class="sep">·</span>
|
|
1002
|
-
<span><b id="as-agents">0</b> agents</span>
|
|
1003
|
-
</div>
|
|
1171
|
+
<section class="thread-pane">
|
|
1004
1172
|
|
|
1005
|
-
<div class="
|
|
1006
|
-
<div class="stat"><span class="label">Running</span><span class="value live" id="sb-running">0</span></div>
|
|
1007
|
-
<span class="sep">·</span>
|
|
1008
|
-
<div class="stat"><span class="label">On machine</span><span class="value" id="sb-detected">0</span></div>
|
|
1009
|
-
<span class="sep">·</span>
|
|
1010
|
-
<div class="stat blocked"><span class="label">Blocked</span><span class="value" id="sb-blocked">0</span></div>
|
|
1011
|
-
<div class="edge"><span class="label">Last edit</span> <span class="value" id="sb-last">—</span></div>
|
|
1012
|
-
</div>
|
|
1173
|
+
<div class="hero-beam"></div>
|
|
1013
1174
|
|
|
1014
1175
|
<div class="intake">
|
|
1015
|
-
<h1>What
|
|
1016
|
-
<
|
|
1017
|
-
<textarea id="prompt" placeholder="e.g. add a --verbose flag to the trust command and document it in the README"></textarea>
|
|
1176
|
+
<h1>What needs <span class="gradient-text-shimmer">doing</span>?</h1>
|
|
1177
|
+
<textarea id="prompt" placeholder="Describe a task. codehere picks the right agent and wraps it in a safety hook. Or pick one yourself below."></textarea>
|
|
1018
1178
|
<div class="starter-prompts" id="starter-prompts">
|
|
1019
1179
|
<span class="sp-label">TRY</span>
|
|
1020
|
-
<button type="button" data-prompt="Read the README and summarize what this project is in 3 short bullets.">Summarize
|
|
1180
|
+
<button type="button" data-prompt="Read the README and summarize what this project is in 3 short bullets.">Summarize this repo</button>
|
|
1021
1181
|
<button type="button" data-prompt="List the top-level files and give me a one-line note on what each is for.">Map the architecture</button>
|
|
1022
1182
|
<button type="button" data-prompt="Run the tests and tell me what (if anything) is failing.">Run the tests</button>
|
|
1023
1183
|
</div>
|
|
1024
1184
|
<div class="intake-row">
|
|
1025
|
-
<select id="agent"><option value="
|
|
1026
|
-
<input type="text" id="cwd" class="cwd" placeholder="
|
|
1027
|
-
<button class="spawn" id="spawn">Assign
|
|
1185
|
+
<select id="agent" title="Auto-pick agent · codehere routes. Override below."><option value="">Auto-pick agent</option></select>
|
|
1186
|
+
<input type="text" id="cwd" class="cwd" placeholder="folder" spellcheck="false" />
|
|
1187
|
+
<button class="spawn" id="spawn">Assign</button>
|
|
1028
1188
|
</div>
|
|
1029
1189
|
<div class="setup-panel" id="setup-panel" data-state=""></div>
|
|
1030
1190
|
<div class="intake-row" style="margin-top:6px">
|
|
1031
|
-
<span class="hint" style="margin-left:auto">⌘ + Enter
|
|
1191
|
+
<span class="hint" style="margin-left:auto">⌘ + Enter</span>
|
|
1032
1192
|
</div>
|
|
1033
1193
|
</div>
|
|
1034
1194
|
|
|
1195
|
+
<!-- Apr 29 — UX overhaul: readiness banner + stats demoted below intake.
|
|
1196
|
+
Single muted line of stats replaces the previous two stacked rows.
|
|
1197
|
+
Goal: textarea is the focal point above the fold; chrome subordinates. -->
|
|
1198
|
+
<div class="readiness" id="readiness" style="position:relative;z-index:1"></div>
|
|
1199
|
+
|
|
1200
|
+
<div class="stats-line empty" id="audit-summary" style="position:relative;z-index:1">
|
|
1201
|
+
<span><b id="as-actions">0</b> actions today</span>
|
|
1202
|
+
<span class="sep">·</span>
|
|
1203
|
+
<span><b id="sb-detected">0</b> agents on machine</span>
|
|
1204
|
+
<span class="sep">·</span>
|
|
1205
|
+
<span class="blocked"><b id="as-blocked">0</b> blocked</span>
|
|
1206
|
+
<span class="sep">·</span>
|
|
1207
|
+
<span>avg trust <b id="as-trust">—</b></span>
|
|
1208
|
+
<!-- Hidden but kept in DOM so existing JS that updates these IDs doesn't error -->
|
|
1209
|
+
<span style="display:none"><span id="sb-running">0</span><span id="sb-blocked">0</span><span id="sb-last">—</span><span id="as-agents">0</span></span>
|
|
1210
|
+
</div>
|
|
1211
|
+
|
|
1035
1212
|
<div class="glow-line" style="position:relative;z-index:1"></div>
|
|
1036
1213
|
|
|
1037
1214
|
<div class="section-label" style="position:relative;z-index:1">Sessions</div>
|
|
1038
1215
|
<div id="sessions" style="position:relative;z-index:1"></div>
|
|
1039
1216
|
<div id="empty">
|
|
1040
|
-
<div class="pill">No sessions yet
|
|
1217
|
+
<div class="pill">No sessions yet. Assign one above.</div>
|
|
1041
1218
|
</div>
|
|
1042
1219
|
|
|
1220
|
+
</section>
|
|
1221
|
+
|
|
1222
|
+
<aside class="members-pane">
|
|
1223
|
+
<div class="pane-head"><span>Agents</span><span class="pane-count" id="members-count">0</span></div>
|
|
1224
|
+
<div id="members-list">
|
|
1225
|
+
<div class="pane-empty">Loading agents…</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
</aside>
|
|
1228
|
+
|
|
1043
1229
|
</div>
|
|
1044
1230
|
|
|
1045
1231
|
<script>
|
|
@@ -1066,24 +1252,39 @@ ${SHARED_STYLES}
|
|
|
1066
1252
|
needs_install: '⤓',
|
|
1067
1253
|
};
|
|
1068
1254
|
|
|
1069
|
-
|
|
1255
|
+
// Apr 29: Conductor readiness state lives separately from the agents
|
|
1256
|
+
// array (the dropdown shows specialists only). When auto.state is
|
|
1257
|
+
// 'needs_install' (no specialists installed yet), we surface that on the
|
|
1258
|
+
// Auto option AND in updateSetupPanel so a fresh-install user gets the
|
|
1259
|
+
// setup prompt instead of a runtime spawn failure (codex Round 1 catch).
|
|
1260
|
+
let autoReadiness = null;
|
|
1261
|
+
|
|
1262
|
+
function renderAgentOptions(agents, auto) {
|
|
1070
1263
|
agentReadiness = agents || [];
|
|
1071
|
-
|
|
1264
|
+
autoReadiness = auto || null;
|
|
1265
|
+
if (agentReadiness.length === 0 && !autoReadiness) return;
|
|
1072
1266
|
// Sort: ready first, then needs_auth/billed, then needs_install
|
|
1073
1267
|
const weight = { ready: 0, billed_key_required: 1, needs_auth: 2, needs_install: 3 };
|
|
1074
1268
|
const sorted = [...agentReadiness].sort((a, b) => (weight[a.state] ?? 99) - (weight[b.state] ?? 99));
|
|
1075
|
-
|
|
1269
|
+
const autoState = autoReadiness ? autoReadiness.state : 'ready';
|
|
1270
|
+
const autoGlyph = autoState === 'ready' ? '⚡' : (STATE_GLYPH[autoState] || '⚠');
|
|
1271
|
+
const autoLabel = autoState === 'ready' ? 'Auto · codehere routes' : 'Auto · install an agent first';
|
|
1272
|
+
const autoOption = '<option value="" selected>' + autoGlyph + ' ' + autoLabel + '</option>';
|
|
1273
|
+
const specialistOptions = sorted.map(a =>
|
|
1076
1274
|
'<option value="' + escape(a.id) + '">' +
|
|
1077
1275
|
(STATE_GLYPH[a.state] || '·') + ' ' + escape(a.label) +
|
|
1078
1276
|
'</option>'
|
|
1079
1277
|
).join('');
|
|
1278
|
+
agentSelect.innerHTML = autoOption + specialistOptions;
|
|
1080
1279
|
// Trigger the panel render for whatever is selected first
|
|
1081
1280
|
updateSetupPanel();
|
|
1082
1281
|
}
|
|
1083
1282
|
|
|
1084
1283
|
function updateSetupPanel() {
|
|
1085
1284
|
const id = agentSelect.value;
|
|
1086
|
-
|
|
1285
|
+
// Apr 29: empty value = Auto option = use conductor's readiness (which
|
|
1286
|
+
// says "install at least one specialist" when none are installed).
|
|
1287
|
+
const agent = id === '' ? autoReadiness : agentReadiness.find(a => a.id === id);
|
|
1087
1288
|
if (!agent || agent.state === 'ready') {
|
|
1088
1289
|
setupPanel.style.display = 'none';
|
|
1089
1290
|
setupPanel.removeAttribute('data-state');
|
|
@@ -1175,9 +1376,12 @@ ${SHARED_STYLES}
|
|
|
1175
1376
|
if (evt.code === 0) {
|
|
1176
1377
|
btnEl.classList.add('done');
|
|
1177
1378
|
btnEl.textContent = '✓ Installed';
|
|
1178
|
-
// Re-fetch readiness so the dropdown glyph updates
|
|
1379
|
+
// Re-fetch readiness so the dropdown glyph updates.
|
|
1380
|
+
// Apr 29 codex Round 2: pass d.auto too — otherwise the Auto
|
|
1381
|
+
// option's readiness gets stale after install-success refresh
|
|
1382
|
+
// (the new shape needs both fields preserved on every render).
|
|
1179
1383
|
fetch('/api/readiness/agents').then(r => r.json()).then(d => {
|
|
1180
|
-
renderAgentOptions(d.agents || []);
|
|
1384
|
+
renderAgentOptions(d.agents || [], d.auto);
|
|
1181
1385
|
// Select the now-installed agent so user can run the task immediately
|
|
1182
1386
|
if ([...agentSelect.options].some(o => o.value === agentId)) {
|
|
1183
1387
|
agentSelect.value = agentId;
|
|
@@ -1237,7 +1441,35 @@ ${SHARED_STYLES}
|
|
|
1237
1441
|
});
|
|
1238
1442
|
agentSelect.addEventListener('change', updateSetupPanel);
|
|
1239
1443
|
|
|
1240
|
-
|
|
1444
|
+
// Apr 29 — Slack-shape: render the members pane (right) from the same
|
|
1445
|
+
// /api/readiness/agents response that populates the override dropdown.
|
|
1446
|
+
// Single fetch, two surfaces. A later release replaces this with live presence
|
|
1447
|
+
// (F1d-detected agents + spawned-by-codehere sessions joined by id).
|
|
1448
|
+
function renderMembersPane(agents, auto) {
|
|
1449
|
+
const list = document.getElementById('members-list');
|
|
1450
|
+
const count = document.getElementById('members-count');
|
|
1451
|
+
if (!list || !count) return;
|
|
1452
|
+
const all = [];
|
|
1453
|
+
if (auto) all.push({ ...auto, id: 'conductor', label: 'codehere · watches the others' });
|
|
1454
|
+
for (const a of (agents || [])) all.push(a);
|
|
1455
|
+
count.textContent = String(all.length);
|
|
1456
|
+
if (all.length === 0) { list.innerHTML = '<div class="pane-empty">No agents detected</div>'; return; }
|
|
1457
|
+
const presenceCls = (s) => s === 'ready' ? 'ready' : (s === 'needs_install' ? 'offline' : 'warn');
|
|
1458
|
+
const presenceGlyph = (s) => s === 'ready' ? '●' : (s === 'needs_install' ? '○' : '◐');
|
|
1459
|
+
list.innerHTML = all.map((a) => {
|
|
1460
|
+
const cls = presenceCls(a.state);
|
|
1461
|
+
const glyph = presenceGlyph(a.state);
|
|
1462
|
+
return '<div class="member" title="' + escape(a.state || 'unknown') + '">' +
|
|
1463
|
+
'<span class="member-presence ' + cls + '">' + glyph + '</span>' +
|
|
1464
|
+
'<span class="member-label">' + escape(a.label || a.id) + '</span>' +
|
|
1465
|
+
'</div>';
|
|
1466
|
+
}).join('');
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
fetch('/api/readiness/agents').then(r => r.json()).then(d => {
|
|
1470
|
+
renderAgentOptions(d.agents || [], d.auto);
|
|
1471
|
+
renderMembersPane(d.agents || [], d.auto);
|
|
1472
|
+
}).catch(() => { /* keep defaults */ });
|
|
1241
1473
|
|
|
1242
1474
|
// Working directory: persist user's pick, auto-fill with daemon cwd on first run.
|
|
1243
1475
|
// Gate A1: strangers shouldn't have to type a path just to hit "Assign"; the
|
|
@@ -1249,6 +1481,15 @@ ${SHARED_STYLES}
|
|
|
1249
1481
|
if (daemonCwd) {
|
|
1250
1482
|
cwdInput.placeholder = daemonCwd;
|
|
1251
1483
|
if (!cwdInput.value.trim()) cwdInput.value = daemonCwd; // auto-fill when nothing saved
|
|
1484
|
+
// Apr 29 — Slack-shape channels pane: surface the daemon cwd as the
|
|
1485
|
+
// current "channel" name + path. Multi-project ships in a later release.
|
|
1486
|
+
const nameEl = document.getElementById('current-channel-name');
|
|
1487
|
+
const cwdEl = document.getElementById('current-channel-cwd');
|
|
1488
|
+
if (nameEl) {
|
|
1489
|
+
const parts = String(daemonCwd).split('/').filter(Boolean);
|
|
1490
|
+
nameEl.textContent = parts[parts.length - 1] || 'project';
|
|
1491
|
+
}
|
|
1492
|
+
if (cwdEl) cwdEl.textContent = daemonCwd;
|
|
1252
1493
|
}
|
|
1253
1494
|
}).catch(() => { /* placeholder unchanged */ });
|
|
1254
1495
|
cwdInput.addEventListener('change', () => {
|
|
@@ -1257,11 +1498,21 @@ ${SHARED_STYLES}
|
|
|
1257
1498
|
|
|
1258
1499
|
// Gate A1: starter-prompt chips prefill the textarea + focus it so the user
|
|
1259
1500
|
// can edit or hit ⌘-Enter immediately. No scroll-jumping, no redirect.
|
|
1260
|
-
document.getElementById('starter-prompts')
|
|
1501
|
+
const starterPromptsEl = document.getElementById('starter-prompts');
|
|
1502
|
+
starterPromptsEl.addEventListener('click', (ev) => {
|
|
1261
1503
|
const btn = ev.target && ev.target.closest('button[data-prompt]');
|
|
1262
1504
|
if (!btn) return;
|
|
1263
1505
|
promptEl.value = btn.getAttribute('data-prompt') || '';
|
|
1264
1506
|
promptEl.focus();
|
|
1507
|
+
starterPromptsEl.classList.add('dim'); // user picked one; chips fade
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
// Apr 29 — UX overhaul: TRY chips fade out once the user starts typing.
|
|
1511
|
+
// They serve as a discovery aid for first-visit users; a typing user has
|
|
1512
|
+
// already committed to a task and doesn't need the chips above the page.
|
|
1513
|
+
promptEl.addEventListener('input', () => {
|
|
1514
|
+
if (promptEl.value.length > 0) starterPromptsEl.classList.add('dim');
|
|
1515
|
+
else starterPromptsEl.classList.remove('dim');
|
|
1265
1516
|
});
|
|
1266
1517
|
|
|
1267
1518
|
function escape(s) {
|
|
@@ -1464,12 +1715,23 @@ ${SHARED_STYLES}
|
|
|
1464
1715
|
'<div class="steps-mini"></div>' +
|
|
1465
1716
|
'</div>' +
|
|
1466
1717
|
'<div class="session-controls">' +
|
|
1718
|
+
'<div class="view-toggle" title="Switch between thread view and raw event log">' +
|
|
1719
|
+
'<button class="view-thread active" data-view="thread">Thread</button>' +
|
|
1720
|
+
'<button class="view-raw" data-view="raw">Log</button>' +
|
|
1721
|
+
'</div>' +
|
|
1467
1722
|
'<span class="status-pill ' + session.status + runningClass + '">' + session.status + '</span>' +
|
|
1468
1723
|
'<button class="kill">Kill</button>' +
|
|
1469
1724
|
'</div>' +
|
|
1470
1725
|
'</div>' +
|
|
1471
|
-
'<div class="session-body">' +
|
|
1726
|
+
'<div class="session-body" data-view="thread">' +
|
|
1727
|
+
// Order: thread messages (chronological) → final report (closing
|
|
1728
|
+
// summary, rendered as a block when artifacts arrive) → reply input.
|
|
1729
|
+
// Apr 29 bug fix: previously final-report was BEFORE thread-messages
|
|
1730
|
+
// so on completed sessions the Final Report appeared above the user's
|
|
1731
|
+
// seed prompt, breaking chronological order in thread view.
|
|
1732
|
+
'<div class="thread-messages"></div>' +
|
|
1472
1733
|
'<div class="final-report" style="display:none"></div>' +
|
|
1734
|
+
'<div class="thread-input">Reply to thread <span class="scaffold-note">(coming soon)</span></div>' +
|
|
1473
1735
|
'<div class="event-log"></div>' +
|
|
1474
1736
|
'</div>';
|
|
1475
1737
|
|
|
@@ -1482,6 +1744,8 @@ ${SHARED_STYLES}
|
|
|
1482
1744
|
const headEl = card.querySelector('.session-head');
|
|
1483
1745
|
const stepsEl = card.querySelector('.steps-mini');
|
|
1484
1746
|
const finalReportEl = card.querySelector('.final-report');
|
|
1747
|
+
const threadEl = card.querySelector('.thread-messages');
|
|
1748
|
+
const bodyEl = card.querySelector('.session-body');
|
|
1485
1749
|
const safetyPillEl = card.querySelector('.safety-pill');
|
|
1486
1750
|
const safetySafeEl = safetyPillEl.querySelector('.safe');
|
|
1487
1751
|
const safetyBlockedEl = safetyPillEl.querySelector('.blocked');
|
|
@@ -1511,6 +1775,146 @@ ${SHARED_STYLES}
|
|
|
1511
1775
|
card.classList.toggle('expanded');
|
|
1512
1776
|
});
|
|
1513
1777
|
|
|
1778
|
+
// Apr 29 — Slack-shape thread view.
|
|
1779
|
+
// Thread/Raw toggle in the card head. Default = thread; Raw stays available
|
|
1780
|
+
// for power users who want the unfiltered event stream.
|
|
1781
|
+
const viewBtns = card.querySelectorAll('.view-toggle button');
|
|
1782
|
+
viewBtns.forEach((btn) => {
|
|
1783
|
+
btn.addEventListener('click', (ev) => {
|
|
1784
|
+
ev.stopPropagation(); // don't collapse the card
|
|
1785
|
+
const view = btn.dataset.view;
|
|
1786
|
+
bodyEl.dataset.view = view;
|
|
1787
|
+
viewBtns.forEach((b) => b.classList.toggle('active', b === btn));
|
|
1788
|
+
});
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// Build a single thread message DOM string.
|
|
1792
|
+
function threadMessage(opts) {
|
|
1793
|
+
const cls = opts.cls || 'agent';
|
|
1794
|
+
const av = opts.avatar || (cls === 'user' ? '👤' : (cls === 'tool' ? '⚙' : (cls === 'hook' ? '🛡' : (cls === 'error' ? '✗' : '🤖'))));
|
|
1795
|
+
const time = opts.time || new Date();
|
|
1796
|
+
return '<div class="thread-message ' + escape(cls) + (opts.modifier ? ' ' + escape(opts.modifier) : '') + '">' +
|
|
1797
|
+
'<div class="avatar">' + escape(av) + '</div>' +
|
|
1798
|
+
'<div class="body">' +
|
|
1799
|
+
'<div class="head">' +
|
|
1800
|
+
'<span class="author">' + escape(opts.author || 'agent') + '</span>' +
|
|
1801
|
+
'<span class="time">' + time.toLocaleTimeString() + '</span>' +
|
|
1802
|
+
'</div>' +
|
|
1803
|
+
'<div class="content">' + escape(String(opts.content || '')) + '</div>' +
|
|
1804
|
+
'</div>' +
|
|
1805
|
+
'</div>';
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
function appendThreadMessage(html) {
|
|
1809
|
+
if (!html) return;
|
|
1810
|
+
threadEl.insertAdjacentHTML('beforeend', html);
|
|
1811
|
+
// Auto-scroll to bottom of thread (within the card)
|
|
1812
|
+
threadEl.scrollTop = threadEl.scrollHeight;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Seed: user prompt as the first thread message
|
|
1816
|
+
threadEl.innerHTML = threadMessage({
|
|
1817
|
+
cls: 'user',
|
|
1818
|
+
author: 'You',
|
|
1819
|
+
time: new Date(session.startedAt),
|
|
1820
|
+
content: session.prompt,
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
// Parallel event → thread renderer. Initial pass duplicates parts of
|
|
1824
|
+
// renderEvent's branching; a follow-up refactor extracts a shared
|
|
1825
|
+
// eventToLines() primitive feeding both renderers.
|
|
1826
|
+
function renderEventToThread(event) {
|
|
1827
|
+
const type = String(event.type || '');
|
|
1828
|
+
const data = event.data ?? {};
|
|
1829
|
+
const t = new Date(event.timestamp || Date.now());
|
|
1830
|
+
const agent = agentLabel(session.agent);
|
|
1831
|
+
|
|
1832
|
+
if (type === 'stream_event') return;
|
|
1833
|
+
if (type === 'system' && (data.subtype === 'init' || data.subtype === 'status' || data.subtype === 'hook_started')) return;
|
|
1834
|
+
if (type === 'stdout' && !String(data.line || '').trim()) return;
|
|
1835
|
+
if (type === 'thread.started' || type === 'turn.started') return;
|
|
1836
|
+
if (type === 'item.started') return;
|
|
1837
|
+
|
|
1838
|
+
if (type === 'assistant' && data.message && Array.isArray(data.message.content)) {
|
|
1839
|
+
for (const block of data.message.content) {
|
|
1840
|
+
if (block.type === 'text') {
|
|
1841
|
+
const txt = String(block.text || '').trim();
|
|
1842
|
+
if (txt) appendThreadMessage(threadMessage({ cls: 'agent', author: agent, time: t, content: txt }));
|
|
1843
|
+
} else if (block.type === 'tool_use') {
|
|
1844
|
+
const name = String(block.name || 'tool');
|
|
1845
|
+
const input = block.input && typeof block.input === 'object' ? JSON.stringify(block.input) : '';
|
|
1846
|
+
const preview = input.length > 200 ? input.slice(0, 200) + '…' : input;
|
|
1847
|
+
appendThreadMessage(threadMessage({ cls: 'tool', author: agent + ' · ' + name, time: t, content: preview, avatar: '⚙' }));
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
} else if (type === 'user' && data.message && Array.isArray(data.message.content)) {
|
|
1851
|
+
for (const block of data.message.content) {
|
|
1852
|
+
if (block.type === 'tool_result') {
|
|
1853
|
+
const raw = Array.isArray(block.content) ? block.content.map((c) => c.text || '').join('') : String(block.content || '');
|
|
1854
|
+
const txt = raw.replace(/\\s+/g, ' ').trim();
|
|
1855
|
+
if (txt) {
|
|
1856
|
+
const preview = txt.length > 240 ? txt.slice(0, 240) + '…' : txt;
|
|
1857
|
+
const cls = block.is_error ? 'error' : 'tool';
|
|
1858
|
+
const author = block.is_error ? agent + ' · error' : agent + ' · result';
|
|
1859
|
+
appendThreadMessage(threadMessage({ cls, author, time: t, content: preview }));
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
} else if (type === 'system' && data.subtype === 'hook_response') {
|
|
1864
|
+
const name = String(data.hook_name || 'hook');
|
|
1865
|
+
const blocked = Number(data.exit_code ?? 0) !== 0;
|
|
1866
|
+
const stderr = String(data.stderr || '').trim();
|
|
1867
|
+
const content = blocked ? (stderr || 'blocked') : 'allowed';
|
|
1868
|
+
appendThreadMessage(threadMessage({ cls: 'hook', modifier: blocked ? 'blocked' : '', author: 'codehere · ' + name, time: t, content }));
|
|
1869
|
+
} else if (type === 'item.completed') {
|
|
1870
|
+
const item = data.item && typeof data.item === 'object' ? data.item : {};
|
|
1871
|
+
const itemType = String(item.type || 'item');
|
|
1872
|
+
if (itemType === 'agent_message' && item.text) {
|
|
1873
|
+
const txt = String(item.text).trim();
|
|
1874
|
+
if (txt) appendThreadMessage(threadMessage({ cls: 'agent', author: agent, time: t, content: txt.slice(0, 320) }));
|
|
1875
|
+
} else if (itemType === 'command_execution') {
|
|
1876
|
+
const cmd = String(item.command || '').replace(/\\n/g, ' ').trim();
|
|
1877
|
+
const cmdShort = cmd.length > 160 ? cmd.slice(0, 160) + '…' : cmd;
|
|
1878
|
+
appendThreadMessage(threadMessage({ cls: 'tool', author: agent + ' · shell', time: t, content: cmdShort, avatar: '⚙' }));
|
|
1879
|
+
const out = String(item.aggregated_output || '').trim();
|
|
1880
|
+
if (out) {
|
|
1881
|
+
const outShort = out.length > 200 ? out.slice(0, 200) + '…' : out;
|
|
1882
|
+
const ok = item.exit_code === 0 || item.status === 'completed';
|
|
1883
|
+
appendThreadMessage(threadMessage({
|
|
1884
|
+
cls: ok ? 'tool' : 'error',
|
|
1885
|
+
author: agent + ' · ' + (ok ? 'result' : 'exit ' + (item.exit_code ?? '?')),
|
|
1886
|
+
time: t,
|
|
1887
|
+
content: outShort,
|
|
1888
|
+
}));
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
} else if (type === 'stdout') {
|
|
1892
|
+
// Aider and generic CLI adapters stream via plain stdout. Codex
|
|
1893
|
+
// Round 1 (Apr 29) caught: without this branch, those sessions
|
|
1894
|
+
// showed only the seed prompt + exit marker in thread view.
|
|
1895
|
+
const line = String(data.line || '').trim();
|
|
1896
|
+
if (line) appendThreadMessage(threadMessage({ cls: 'agent', author: agent, time: t, content: line.length > 320 ? line.slice(0, 320) + '…' : line }));
|
|
1897
|
+
} else if (type === 'stderr') {
|
|
1898
|
+
const line = String(data.line || '').trim();
|
|
1899
|
+
if (line) appendThreadMessage(threadMessage({ cls: 'tool', author: agent + ' · stderr', time: t, content: line.length > 240 ? line.slice(0, 240) + '…' : line, avatar: '⚠' }));
|
|
1900
|
+
} else if (type === 'error') {
|
|
1901
|
+
const msg = String(data.message || data.error || 'error');
|
|
1902
|
+
appendThreadMessage(threadMessage({ cls: 'error', author: agent, time: t, content: msg }));
|
|
1903
|
+
} else if (type === 'codehere:error') {
|
|
1904
|
+
const msg = String(data.message || data.kind || 'error');
|
|
1905
|
+
appendThreadMessage(threadMessage({ cls: 'error', author: 'codehere', time: t, content: msg }));
|
|
1906
|
+
} else if (type === 'exit') {
|
|
1907
|
+
const status = String(data.status || 'completed');
|
|
1908
|
+
appendThreadMessage(threadMessage({
|
|
1909
|
+
cls: status === 'error' ? 'error' : 'agent',
|
|
1910
|
+
author: agent + ' · ' + status,
|
|
1911
|
+
time: t,
|
|
1912
|
+
content: 'session ended',
|
|
1913
|
+
avatar: status === 'error' ? '✗' : '✓',
|
|
1914
|
+
}));
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1514
1918
|
// Layer 2: render the semantic step list inside the collapsed card.
|
|
1515
1919
|
// Principles #1 (web is not a terminal) + #2 (info density) — show last 3
|
|
1516
1920
|
// steps with pass/active/fail glyphs; hide the rest behind "+ N more" so
|
|
@@ -1604,6 +2008,7 @@ ${SHARED_STYLES}
|
|
|
1604
2008
|
bumpSafety(event);
|
|
1605
2009
|
|
|
1606
2010
|
renderEvent(logEl, event);
|
|
2011
|
+
renderEventToThread(event); // Apr 29 · Slack-shape thread v0 (Day 2)
|
|
1607
2012
|
if (event.type === 'exit') {
|
|
1608
2013
|
updateStatus(pillEl, event.data?.status || 'completed');
|
|
1609
2014
|
killBtn.disabled = true; killBtn.style.opacity = '0.4';
|
|
@@ -1633,18 +2038,20 @@ ${SHARED_STYLES}
|
|
|
1633
2038
|
async function assignTask() {
|
|
1634
2039
|
const prompt = promptEl.value.trim();
|
|
1635
2040
|
if (!prompt) return;
|
|
1636
|
-
|
|
2041
|
+
// Apr 29: empty value = "Auto" option = backend defaults to conductor
|
|
2042
|
+
// (the supervisor routes). Specialists are explicit overrides.
|
|
2043
|
+
const agent = agentSelect.value;
|
|
1637
2044
|
const cwd = cwdInput.value.trim();
|
|
1638
2045
|
spawnBtn.disabled = true; spawnBtn.textContent = '...';
|
|
1639
2046
|
try {
|
|
1640
2047
|
const res = await fetch('/api/task', {
|
|
1641
2048
|
method: 'POST',
|
|
1642
2049
|
headers: { 'Content-Type': 'application/json' },
|
|
1643
|
-
body: JSON.stringify({ prompt, agent, cwd: cwd || undefined }),
|
|
2050
|
+
body: JSON.stringify({ prompt, agent: agent || undefined, cwd: cwd || undefined }),
|
|
1644
2051
|
});
|
|
1645
2052
|
const data = await res.json();
|
|
1646
2053
|
if (!res.ok) { alert('Failed: ' + (data.error || res.status)); return; }
|
|
1647
|
-
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent });
|
|
2054
|
+
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent || 'conductor' });
|
|
1648
2055
|
promptEl.value = '';
|
|
1649
2056
|
} catch (e) {
|
|
1650
2057
|
alert('Network error: ' + (e && e.message ? e.message : e));
|
|
@@ -1725,9 +2132,10 @@ ${SHARED_STYLES}
|
|
|
1725
2132
|
refreshAudit();
|
|
1726
2133
|
setInterval(refreshAudit, 30000); // audit log isn't hot; 30s is plenty
|
|
1727
2134
|
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
|
|
2135
|
+
// Apr 29 — UX overhaul: status-banner merged into the single stats-line
|
|
2136
|
+
// (id="audit-summary"). updateBanner still updates the agents-on-machine
|
|
2137
|
+
// count + hidden running/blocked/last-edit counters; updateAudit controls
|
|
2138
|
+
// visibility of the whole row via the .empty class.
|
|
1731
2139
|
let latestLastEditAt = null;
|
|
1732
2140
|
function agoStr(ms) {
|
|
1733
2141
|
if (!ms) return '—';
|
|
@@ -1742,19 +2150,20 @@ ${SHARED_STYLES}
|
|
|
1742
2150
|
const running = spawned.filter(s => s.status === 'running').length;
|
|
1743
2151
|
const blocked = spawned.filter(s => s.status === 'error').length;
|
|
1744
2152
|
const detected = (d.activeAgents || []).length;
|
|
1745
|
-
document.getElementById(
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
blockedEl.classList.toggle('nonzero', blocked > 0);
|
|
2153
|
+
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = String(val); };
|
|
2154
|
+
setText('sb-running', running);
|
|
2155
|
+
setText('sb-detected', detected);
|
|
2156
|
+
setText('sb-blocked', blocked);
|
|
1750
2157
|
latestLastEditAt = d.lastEditAt || null;
|
|
1751
|
-
|
|
1752
|
-
//
|
|
1753
|
-
|
|
2158
|
+
setText('sb-last', agoStr(latestLastEditAt));
|
|
2159
|
+
// Reveal the unified stats-line once we have anything to show.
|
|
2160
|
+
if (running + detected + blocked + spawned.length > 0) {
|
|
2161
|
+
auditRow.classList.remove('empty');
|
|
2162
|
+
}
|
|
1754
2163
|
}
|
|
1755
|
-
// Tick the "last edit" relative label every second even without a refetch
|
|
1756
2164
|
setInterval(() => {
|
|
1757
|
-
|
|
2165
|
+
const el = document.getElementById('sb-last');
|
|
2166
|
+
if (el && latestLastEditAt) el.textContent = agoStr(latestLastEditAt);
|
|
1758
2167
|
}, 1000);
|
|
1759
2168
|
|
|
1760
2169
|
function loadSessions() {
|
|
@@ -1804,7 +2213,7 @@ ${SHARED_STYLES}
|
|
|
1804
2213
|
actionHtml += ' <a href="' + escape(report.action.url) + '" target="_blank" rel="noreferrer">' + escape(report.action.label || 'Open') + ' →</a>';
|
|
1805
2214
|
}
|
|
1806
2215
|
}
|
|
1807
|
-
actionHtml += ' <button id="readiness-refresh">
|
|
2216
|
+
actionHtml += ' <button id="readiness-refresh">Recheck</button>';
|
|
1808
2217
|
readinessEl.innerHTML =
|
|
1809
2218
|
'<div class="r-title">' + escape(report.title) + '</div>' +
|
|
1810
2219
|
'<div class="r-detail">' + escape(report.detail) + '</div>' +
|