codehere 0.4.1 → 0.5.0-alpha.3
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 +64 -7
- package/dist/application/use-cases/generate-final-report.js +144 -0
- package/dist/index.js +47 -4
- 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 +70 -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 +41 -5
- 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 +119 -0
- package/dist/infrastructure/orchestration/readiness.js +2 -5
- package/dist/infrastructure/orchestration/session-manager.js +65 -2
- package/dist/infrastructure/orchestration/session-persistence.js +13 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +86 -14
- package/dist/infrastructure/protocols/ui-page.js +665 -67
- package/dist/presentation/cli/commands/init-command.js +345 -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
|
@@ -253,6 +253,31 @@ const SHARED_STYLES = `
|
|
|
253
253
|
color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.12em; font-size: 10px;
|
|
254
254
|
margin-bottom: 8px;
|
|
255
255
|
}
|
|
256
|
+
/* Iter 12 — prose summary block. Plain English for non-technical users.
|
|
257
|
+
Sans-serif breaks the monospace context so it reads as narrative, not data. */
|
|
258
|
+
.final-report .fr-summary {
|
|
259
|
+
font-family: var(--sans);
|
|
260
|
+
font-size: 13px;
|
|
261
|
+
line-height: 1.55;
|
|
262
|
+
color: var(--fg);
|
|
263
|
+
margin-bottom: 12px;
|
|
264
|
+
padding-bottom: 12px;
|
|
265
|
+
border-bottom: 1px solid var(--border-soft);
|
|
266
|
+
}
|
|
267
|
+
.final-report .fr-summary p {
|
|
268
|
+
margin: 0 0 10px 0;
|
|
269
|
+
}
|
|
270
|
+
.final-report .fr-summary p:last-child { margin-bottom: 0; }
|
|
271
|
+
.final-report .fr-summary-pending p {
|
|
272
|
+
color: var(--fg-50);
|
|
273
|
+
font-style: italic;
|
|
274
|
+
animation: shimmer 1.6s linear infinite;
|
|
275
|
+
background: linear-gradient(90deg, var(--fg-30) 0%, var(--fg-70) 50%, var(--fg-30) 100%);
|
|
276
|
+
background-size: 200% auto;
|
|
277
|
+
-webkit-background-clip: text;
|
|
278
|
+
background-clip: text;
|
|
279
|
+
color: transparent;
|
|
280
|
+
}
|
|
256
281
|
.final-report .fr-files { display: flex; flex-direction: column; gap: 3px; }
|
|
257
282
|
.final-report .fr-file {
|
|
258
283
|
display: grid;
|
|
@@ -696,6 +721,77 @@ ${SHARED_STYLES}
|
|
|
696
721
|
|
|
697
722
|
.shell { max-width: 960px; margin: 0 auto; padding: 90px 24px 24px; position: relative; }
|
|
698
723
|
|
|
724
|
+
/* ── Apr 29 — Slack-shape 3-pane layout ──────────────────────────────────
|
|
725
|
+
The app surface restructures from single-column dashboard to
|
|
726
|
+
channels (left) + threads (center) + members (right). Single
|
|
727
|
+
channel = current cwd; members = available agents from readiness API.
|
|
728
|
+
Multi-channel + live presence dots from F1d detection are a later release. */
|
|
729
|
+
.shell.slack-shape {
|
|
730
|
+
max-width: 1440px;
|
|
731
|
+
display: grid;
|
|
732
|
+
grid-template-columns: 200px 1fr 220px;
|
|
733
|
+
gap: 16px;
|
|
734
|
+
align-items: start;
|
|
735
|
+
}
|
|
736
|
+
@media (max-width: 1100px) {
|
|
737
|
+
.shell.slack-shape { grid-template-columns: 1fr; }
|
|
738
|
+
.shell.slack-shape .channels-pane, .shell.slack-shape .members-pane {
|
|
739
|
+
position: static; order: 99;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
.channels-pane, .members-pane {
|
|
743
|
+
background: rgba(255,255,255,0.025);
|
|
744
|
+
border: 1px solid var(--border-soft);
|
|
745
|
+
border-radius: 10px;
|
|
746
|
+
padding: 14px 12px;
|
|
747
|
+
position: sticky; top: 88px;
|
|
748
|
+
font-family: var(--mono);
|
|
749
|
+
z-index: 1;
|
|
750
|
+
}
|
|
751
|
+
.pane-head {
|
|
752
|
+
font-size: 9.5px; letter-spacing: 0.16em;
|
|
753
|
+
color: var(--fg-30); margin-bottom: 10px;
|
|
754
|
+
text-transform: uppercase;
|
|
755
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
756
|
+
}
|
|
757
|
+
.pane-head .pane-count { color: var(--fg-50); }
|
|
758
|
+
.pane-add {
|
|
759
|
+
background: transparent; border: 1px dashed var(--border);
|
|
760
|
+
color: var(--fg-50); font-family: var(--mono); font-size: 10px;
|
|
761
|
+
padding: 4px 8px; border-radius: 6px; cursor: pointer;
|
|
762
|
+
width: 100%; margin-top: 8px;
|
|
763
|
+
}
|
|
764
|
+
.pane-add:hover { color: var(--fg); border-color: var(--fg-30); border-style: solid; }
|
|
765
|
+
.channel, .member {
|
|
766
|
+
display: flex; align-items: center; gap: 8px;
|
|
767
|
+
padding: 6px 8px; border-radius: 6px;
|
|
768
|
+
font-size: 11.5px;
|
|
769
|
+
cursor: pointer;
|
|
770
|
+
color: var(--fg-70);
|
|
771
|
+
border: 1px solid transparent;
|
|
772
|
+
}
|
|
773
|
+
.channel:hover, .member:hover { background: rgba(255,255,255,0.04); color: var(--fg); }
|
|
774
|
+
.channel.active { background: rgba(255,154,118,0.08); color: var(--fg); border-color: rgba(255,154,118,0.25); }
|
|
775
|
+
.channel-glyph, .member-presence {
|
|
776
|
+
color: var(--fg-40); flex: 0 0 14px; text-align: center;
|
|
777
|
+
}
|
|
778
|
+
.member-presence.ready { color: var(--allow); }
|
|
779
|
+
.member-presence.warn { color: var(--warn); }
|
|
780
|
+
.member-presence.offline { color: var(--fg-30); }
|
|
781
|
+
.channel-name, .member-label {
|
|
782
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
783
|
+
flex: 1; min-width: 0;
|
|
784
|
+
}
|
|
785
|
+
.member-cwd-hint, .channel-cwd-hint {
|
|
786
|
+
font-size: 9.5px; color: var(--fg-30);
|
|
787
|
+
margin-top: 4px; padding-left: 22px;
|
|
788
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
789
|
+
}
|
|
790
|
+
.pane-empty {
|
|
791
|
+
color: var(--fg-30); font-size: 10.5px;
|
|
792
|
+
padding: 8px; text-align: center;
|
|
793
|
+
}
|
|
794
|
+
|
|
699
795
|
/* ── Readiness banner (F1a) ─────────────────────────────────────────── */
|
|
700
796
|
.readiness {
|
|
701
797
|
margin: 12px 0 0;
|
|
@@ -973,6 +1069,125 @@ ${SHARED_STYLES}
|
|
|
973
1069
|
.event-log .event.codehere-error .label { color: var(--block); display: block; margin-bottom: 4px; }
|
|
974
1070
|
.event-log .event.codehere-error .recovery { color: var(--fg-70); margin-top: 4px; display: block; }
|
|
975
1071
|
|
|
1072
|
+
/* Apr 29 — Slack-shape thread view.
|
|
1073
|
+
Expanded session card defaults to thread view; raw event-log opt-in via
|
|
1074
|
+
the Thread/Log toggle in the card head. Each event becomes a message:
|
|
1075
|
+
user prompt seeds it; agent text/tool/hook events stream in as replies.
|
|
1076
|
+
The reply input is a visual scaffold — wires up as a real follow-up
|
|
1077
|
+
queue in a later release. */
|
|
1078
|
+
.thread-messages {
|
|
1079
|
+
margin: 10px 0 0;
|
|
1080
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
1081
|
+
max-height: 420px; overflow-y: auto;
|
|
1082
|
+
padding-right: 4px;
|
|
1083
|
+
}
|
|
1084
|
+
.thread-message { display: flex; gap: 10px; padding: 6px 0; }
|
|
1085
|
+
.thread-message .avatar {
|
|
1086
|
+
flex: 0 0 28px; height: 28px;
|
|
1087
|
+
border-radius: 50%;
|
|
1088
|
+
display: flex; align-items: center; justify-content: center;
|
|
1089
|
+
font-size: 13px;
|
|
1090
|
+
background: rgba(255,255,255,0.04); border: 1px solid var(--border-soft);
|
|
1091
|
+
}
|
|
1092
|
+
.thread-message.user .avatar { background: rgba(74,144,226,0.18); border-color: rgba(74,144,226,0.45); }
|
|
1093
|
+
.thread-message.agent .avatar { background: rgba(255,154,118,0.18); border-color: rgba(255,154,118,0.45); }
|
|
1094
|
+
.thread-message.tool .avatar { background: rgba(255,154,118,0.08); }
|
|
1095
|
+
.thread-message.hook .avatar { background: rgba(127,193,154,0.18); border-color: rgba(127,193,154,0.4); }
|
|
1096
|
+
.thread-message.hook.blocked .avatar { background: rgba(255,95,87,0.18); border-color: rgba(255,95,87,0.45); }
|
|
1097
|
+
.thread-message.error .avatar { background: rgba(255,95,87,0.18); border-color: rgba(255,95,87,0.45); }
|
|
1098
|
+
.thread-message .body { flex: 1; min-width: 0; }
|
|
1099
|
+
.thread-message .head { display: flex; gap: 8px; align-items: baseline; font-family: var(--mono); font-size: 10.5px; }
|
|
1100
|
+
.thread-message .author { color: var(--fg); font-weight: 600; }
|
|
1101
|
+
.thread-message .time { color: var(--fg-40); }
|
|
1102
|
+
.thread-message .content {
|
|
1103
|
+
color: var(--fg-80); font-family: var(--mono); font-size: 11.5px;
|
|
1104
|
+
line-height: 1.55; margin-top: 3px; white-space: pre-wrap; word-break: break-word;
|
|
1105
|
+
}
|
|
1106
|
+
.thread-message.tool .content { color: var(--fg-60); }
|
|
1107
|
+
.thread-message.hook .content { color: var(--allow); }
|
|
1108
|
+
.thread-message.hook.blocked .content { color: var(--block); }
|
|
1109
|
+
.thread-message.error .content { color: var(--block); }
|
|
1110
|
+
|
|
1111
|
+
.thread-input {
|
|
1112
|
+
margin-top: 14px;
|
|
1113
|
+
padding: 10px 12px;
|
|
1114
|
+
background: rgba(255,255,255,0.02);
|
|
1115
|
+
border: 1px solid var(--border-soft); border-radius: 8px;
|
|
1116
|
+
color: var(--fg-50); font-size: 11px; font-family: var(--mono);
|
|
1117
|
+
display: flex; align-items: center; gap: 8px;
|
|
1118
|
+
}
|
|
1119
|
+
/* Iter 12 — reply surface now functional. Wraps textarea + send button. */
|
|
1120
|
+
.thread-input { padding: 8px; flex-direction: column; align-items: stretch; gap: 6px; }
|
|
1121
|
+
.thread-input::before { display: none; }
|
|
1122
|
+
.thread-input .reply-text {
|
|
1123
|
+
width: 100%; min-height: 38px; resize: vertical;
|
|
1124
|
+
background: transparent; border: 0; outline: none;
|
|
1125
|
+
color: var(--fg); font-family: var(--sans); font-size: 12px; line-height: 1.5;
|
|
1126
|
+
padding: 4px 0;
|
|
1127
|
+
}
|
|
1128
|
+
.thread-input .reply-text::placeholder { color: var(--fg-30); }
|
|
1129
|
+
.thread-input .reply-send {
|
|
1130
|
+
align-self: flex-end;
|
|
1131
|
+
padding: 5px 14px; border-radius: 6px;
|
|
1132
|
+
background: var(--accent-wash); color: var(--accent);
|
|
1133
|
+
border: 1px solid rgba(255,154,118,0.25);
|
|
1134
|
+
font-family: var(--sans); font-size: 11px; font-weight: 600;
|
|
1135
|
+
cursor: pointer;
|
|
1136
|
+
}
|
|
1137
|
+
.thread-input .reply-send:hover { background: rgba(255,154,118,0.18); }
|
|
1138
|
+
.thread-input .reply-send:disabled { opacity: 0.4; cursor: default; }
|
|
1139
|
+
.thread-input .reply-status { color: var(--fg-50); font-size: 10px; font-family: var(--mono); }
|
|
1140
|
+
.thread-input .reply-status.error { color: var(--block); }
|
|
1141
|
+
.thread-input.disabled .reply-text,
|
|
1142
|
+
.thread-input.disabled .reply-send { opacity: 0.5; cursor: not-allowed; }
|
|
1143
|
+
|
|
1144
|
+
.view-toggle {
|
|
1145
|
+
display: inline-flex; gap: 0;
|
|
1146
|
+
border: 1px solid var(--border-soft); border-radius: 6px;
|
|
1147
|
+
overflow: hidden;
|
|
1148
|
+
margin-right: 8px;
|
|
1149
|
+
}
|
|
1150
|
+
.view-toggle button {
|
|
1151
|
+
background: transparent; border: 0;
|
|
1152
|
+
color: var(--fg-60); padding: 3px 9px;
|
|
1153
|
+
font-family: var(--mono); font-size: 10px;
|
|
1154
|
+
cursor: pointer;
|
|
1155
|
+
}
|
|
1156
|
+
.view-toggle button.active { color: var(--fg); background: rgba(255,255,255,0.06); }
|
|
1157
|
+
.view-toggle button:hover { color: var(--fg); }
|
|
1158
|
+
|
|
1159
|
+
/* Toggle visibility based on data-view attribute on session-body */
|
|
1160
|
+
.session-body[data-view="thread"] .event-log { display: none; }
|
|
1161
|
+
.session-body[data-view="raw"] .thread-messages { display: none; }
|
|
1162
|
+
.session-body[data-view="raw"] .thread-input { display: none; }
|
|
1163
|
+
|
|
1164
|
+
/* Apr 29 — UX overhaul: collapsed single-line stats row (replaces stacked
|
|
1165
|
+
audit-summary + status-banner). Sits BELOW the intake, not above. Muted
|
|
1166
|
+
so it doesn't compete with the H1 / textarea for eye attention. */
|
|
1167
|
+
.stats-line {
|
|
1168
|
+
margin: 14px 0 0;
|
|
1169
|
+
font-family: var(--mono); font-size: 10.5px; color: var(--fg-40);
|
|
1170
|
+
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
|
1171
|
+
}
|
|
1172
|
+
.stats-line b { color: var(--fg-70); font-weight: 500; }
|
|
1173
|
+
.stats-line .sep { color: var(--fg-20); }
|
|
1174
|
+
.stats-line .blocked b { color: var(--block); }
|
|
1175
|
+
.stats-line.empty { display: none; }
|
|
1176
|
+
|
|
1177
|
+
/* Apr 29 — UX overhaul: readiness banner is now compact + below intake.
|
|
1178
|
+
Was: full-width yellow box with multi-line prose above the H1.
|
|
1179
|
+
Is: tighter padding, single-line preferred, sits as a subtle hint below
|
|
1180
|
+
the assign action. The detail text wraps when long but contrast is muted. */
|
|
1181
|
+
.readiness { margin-top: 14px; padding: 10px 12px; }
|
|
1182
|
+
.readiness .r-title { font-size: 11px; }
|
|
1183
|
+
.readiness .r-detail { font-size: 10.5px; }
|
|
1184
|
+
.readiness .r-action { margin-top: 6px; }
|
|
1185
|
+
|
|
1186
|
+
/* Apr 29 — UX overhaul: TRY chips fade out as soon as the user starts
|
|
1187
|
+
typing. They're a discovery aid for first-visit users, not a permanent
|
|
1188
|
+
fixture above an active textarea. */
|
|
1189
|
+
.starter-prompts.dim { opacity: 0.35; pointer-events: none; transition: opacity 200ms ease; }
|
|
1190
|
+
|
|
976
1191
|
#empty { padding: 48px 0; text-align: center; color: var(--fg-50); }
|
|
977
1192
|
#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
1193
|
</style>
|
|
@@ -984,62 +1199,80 @@ ${SHARED_STYLES}
|
|
|
984
1199
|
<a href="/history" class="nav-link">History</a>
|
|
985
1200
|
</nav>
|
|
986
1201
|
|
|
987
|
-
<div class="shell">
|
|
988
|
-
|
|
989
|
-
<
|
|
990
|
-
|
|
991
|
-
<div
|
|
1202
|
+
<div class="shell slack-shape">
|
|
1203
|
+
|
|
1204
|
+
<aside class="channels-pane">
|
|
1205
|
+
<div class="pane-head"><span>Projects</span><span class="pane-count" id="channels-count">1</span></div>
|
|
1206
|
+
<div id="channels-list">
|
|
1207
|
+
<div class="channel active" data-channel="current">
|
|
1208
|
+
<span class="channel-glyph">#</span>
|
|
1209
|
+
<div style="flex:1;min-width:0">
|
|
1210
|
+
<div class="channel-name" id="current-channel-name">project</div>
|
|
1211
|
+
<div class="channel-cwd-hint" id="current-channel-cwd">~</div>
|
|
1212
|
+
</div>
|
|
1213
|
+
</div>
|
|
1214
|
+
</div>
|
|
1215
|
+
<button class="pane-add" type="button" id="new-project-btn" title="Set a new working folder for the next task">+ New project</button>
|
|
1216
|
+
</aside>
|
|
992
1217
|
|
|
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>
|
|
1218
|
+
<section class="thread-pane">
|
|
1004
1219
|
|
|
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>
|
|
1220
|
+
<div class="hero-beam"></div>
|
|
1013
1221
|
|
|
1014
1222
|
<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>
|
|
1223
|
+
<h1>What needs <span class="gradient-text-shimmer">doing</span>?</h1>
|
|
1224
|
+
<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
1225
|
<div class="starter-prompts" id="starter-prompts">
|
|
1019
1226
|
<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
|
|
1227
|
+
<button type="button" data-prompt="Read the README and summarize what this project is in 3 short bullets.">Summarize this repo</button>
|
|
1021
1228
|
<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
1229
|
<button type="button" data-prompt="Run the tests and tell me what (if anything) is failing.">Run the tests</button>
|
|
1023
1230
|
</div>
|
|
1024
1231
|
<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
|
|
1232
|
+
<select id="agent" title="Auto-pick agent · codehere routes. Override below."><option value="">Auto-pick agent</option></select>
|
|
1233
|
+
<input type="text" id="cwd" class="cwd" placeholder="folder" spellcheck="false" />
|
|
1234
|
+
<button class="spawn" id="spawn">Assign</button>
|
|
1028
1235
|
</div>
|
|
1029
1236
|
<div class="setup-panel" id="setup-panel" data-state=""></div>
|
|
1030
1237
|
<div class="intake-row" style="margin-top:6px">
|
|
1031
|
-
<span class="hint" style="margin-left:auto">⌘ + Enter
|
|
1238
|
+
<span class="hint" style="margin-left:auto">⌘ + Enter</span>
|
|
1032
1239
|
</div>
|
|
1033
1240
|
</div>
|
|
1034
1241
|
|
|
1242
|
+
<!-- Apr 29 — UX overhaul: readiness banner + stats demoted below intake.
|
|
1243
|
+
Single muted line of stats replaces the previous two stacked rows.
|
|
1244
|
+
Goal: textarea is the focal point above the fold; chrome subordinates. -->
|
|
1245
|
+
<div class="readiness" id="readiness" style="position:relative;z-index:1"></div>
|
|
1246
|
+
|
|
1247
|
+
<div class="stats-line empty" id="audit-summary" style="position:relative;z-index:1">
|
|
1248
|
+
<span><b id="as-actions">0</b> actions today</span>
|
|
1249
|
+
<span class="sep">·</span>
|
|
1250
|
+
<span><b id="sb-detected">0</b> agents on machine</span>
|
|
1251
|
+
<span class="sep">·</span>
|
|
1252
|
+
<span class="blocked"><b id="as-blocked">0</b> blocked</span>
|
|
1253
|
+
<span class="sep">·</span>
|
|
1254
|
+
<span>avg trust <b id="as-trust">—</b></span>
|
|
1255
|
+
<!-- Hidden but kept in DOM so existing JS that updates these IDs doesn't error -->
|
|
1256
|
+
<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>
|
|
1257
|
+
</div>
|
|
1258
|
+
|
|
1035
1259
|
<div class="glow-line" style="position:relative;z-index:1"></div>
|
|
1036
1260
|
|
|
1037
1261
|
<div class="section-label" style="position:relative;z-index:1">Sessions</div>
|
|
1038
1262
|
<div id="sessions" style="position:relative;z-index:1"></div>
|
|
1039
1263
|
<div id="empty">
|
|
1040
|
-
<div class="pill">No sessions yet
|
|
1264
|
+
<div class="pill">No sessions yet. Assign one above.</div>
|
|
1041
1265
|
</div>
|
|
1042
1266
|
|
|
1267
|
+
</section>
|
|
1268
|
+
|
|
1269
|
+
<aside class="members-pane">
|
|
1270
|
+
<div class="pane-head"><span>Agents</span><span class="pane-count" id="members-count">0</span></div>
|
|
1271
|
+
<div id="members-list">
|
|
1272
|
+
<div class="pane-empty">Loading agents…</div>
|
|
1273
|
+
</div>
|
|
1274
|
+
</aside>
|
|
1275
|
+
|
|
1043
1276
|
</div>
|
|
1044
1277
|
|
|
1045
1278
|
<script>
|
|
@@ -1066,24 +1299,39 @@ ${SHARED_STYLES}
|
|
|
1066
1299
|
needs_install: '⤓',
|
|
1067
1300
|
};
|
|
1068
1301
|
|
|
1069
|
-
|
|
1302
|
+
// Apr 29: Conductor readiness state lives separately from the agents
|
|
1303
|
+
// array (the dropdown shows specialists only). When auto.state is
|
|
1304
|
+
// 'needs_install' (no specialists installed yet), we surface that on the
|
|
1305
|
+
// Auto option AND in updateSetupPanel so a fresh-install user gets the
|
|
1306
|
+
// setup prompt instead of a runtime spawn failure (codex Round 1 catch).
|
|
1307
|
+
let autoReadiness = null;
|
|
1308
|
+
|
|
1309
|
+
function renderAgentOptions(agents, auto) {
|
|
1070
1310
|
agentReadiness = agents || [];
|
|
1071
|
-
|
|
1311
|
+
autoReadiness = auto || null;
|
|
1312
|
+
if (agentReadiness.length === 0 && !autoReadiness) return;
|
|
1072
1313
|
// Sort: ready first, then needs_auth/billed, then needs_install
|
|
1073
1314
|
const weight = { ready: 0, billed_key_required: 1, needs_auth: 2, needs_install: 3 };
|
|
1074
1315
|
const sorted = [...agentReadiness].sort((a, b) => (weight[a.state] ?? 99) - (weight[b.state] ?? 99));
|
|
1075
|
-
|
|
1316
|
+
const autoState = autoReadiness ? autoReadiness.state : 'ready';
|
|
1317
|
+
const autoGlyph = autoState === 'ready' ? '⚡' : (STATE_GLYPH[autoState] || '⚠');
|
|
1318
|
+
const autoLabel = autoState === 'ready' ? 'Auto · codehere routes' : 'Auto · install an agent first';
|
|
1319
|
+
const autoOption = '<option value="" selected>' + autoGlyph + ' ' + autoLabel + '</option>';
|
|
1320
|
+
const specialistOptions = sorted.map(a =>
|
|
1076
1321
|
'<option value="' + escape(a.id) + '">' +
|
|
1077
1322
|
(STATE_GLYPH[a.state] || '·') + ' ' + escape(a.label) +
|
|
1078
1323
|
'</option>'
|
|
1079
1324
|
).join('');
|
|
1325
|
+
agentSelect.innerHTML = autoOption + specialistOptions;
|
|
1080
1326
|
// Trigger the panel render for whatever is selected first
|
|
1081
1327
|
updateSetupPanel();
|
|
1082
1328
|
}
|
|
1083
1329
|
|
|
1084
1330
|
function updateSetupPanel() {
|
|
1085
1331
|
const id = agentSelect.value;
|
|
1086
|
-
|
|
1332
|
+
// Apr 29: empty value = Auto option = use conductor's readiness (which
|
|
1333
|
+
// says "install at least one specialist" when none are installed).
|
|
1334
|
+
const agent = id === '' ? autoReadiness : agentReadiness.find(a => a.id === id);
|
|
1087
1335
|
if (!agent || agent.state === 'ready') {
|
|
1088
1336
|
setupPanel.style.display = 'none';
|
|
1089
1337
|
setupPanel.removeAttribute('data-state');
|
|
@@ -1175,9 +1423,12 @@ ${SHARED_STYLES}
|
|
|
1175
1423
|
if (evt.code === 0) {
|
|
1176
1424
|
btnEl.classList.add('done');
|
|
1177
1425
|
btnEl.textContent = '✓ Installed';
|
|
1178
|
-
// Re-fetch readiness so the dropdown glyph updates
|
|
1426
|
+
// Re-fetch readiness so the dropdown glyph updates.
|
|
1427
|
+
// Apr 29 codex Round 2: pass d.auto too — otherwise the Auto
|
|
1428
|
+
// option's readiness gets stale after install-success refresh
|
|
1429
|
+
// (the new shape needs both fields preserved on every render).
|
|
1179
1430
|
fetch('/api/readiness/agents').then(r => r.json()).then(d => {
|
|
1180
|
-
renderAgentOptions(d.agents || []);
|
|
1431
|
+
renderAgentOptions(d.agents || [], d.auto);
|
|
1181
1432
|
// Select the now-installed agent so user can run the task immediately
|
|
1182
1433
|
if ([...agentSelect.options].some(o => o.value === agentId)) {
|
|
1183
1434
|
agentSelect.value = agentId;
|
|
@@ -1237,7 +1488,61 @@ ${SHARED_STYLES}
|
|
|
1237
1488
|
});
|
|
1238
1489
|
agentSelect.addEventListener('change', updateSetupPanel);
|
|
1239
1490
|
|
|
1240
|
-
|
|
1491
|
+
// Apr 29 — Slack-shape: render the members pane (right) from the same
|
|
1492
|
+
// /api/readiness/agents response that populates the override dropdown.
|
|
1493
|
+
// Single fetch, two surfaces. A later release replaces this with live presence
|
|
1494
|
+
// (F1d-detected agents + spawned-by-codehere sessions joined by id).
|
|
1495
|
+
function renderMembersPane(agents, auto) {
|
|
1496
|
+
const list = document.getElementById('members-list');
|
|
1497
|
+
const count = document.getElementById('members-count');
|
|
1498
|
+
if (!list || !count) return;
|
|
1499
|
+
const all = [];
|
|
1500
|
+
if (auto) all.push({ ...auto, id: 'conductor', label: 'codehere · watches the others' });
|
|
1501
|
+
for (const a of (agents || [])) all.push(a);
|
|
1502
|
+
count.textContent = String(all.length);
|
|
1503
|
+
if (all.length === 0) { list.innerHTML = '<div class="pane-empty">No agents detected</div>'; return; }
|
|
1504
|
+
const presenceCls = (s) => s === 'ready' ? 'ready' : (s === 'needs_install' ? 'offline' : 'warn');
|
|
1505
|
+
const presenceGlyph = (s) => s === 'ready' ? '●' : (s === 'needs_install' ? '○' : '◐');
|
|
1506
|
+
list.innerHTML = all.map((a) => {
|
|
1507
|
+
const cls = presenceCls(a.state);
|
|
1508
|
+
const glyph = presenceGlyph(a.state);
|
|
1509
|
+
// Iter 12 — members are now clickable. Non-technical user expects
|
|
1510
|
+
// clicking an agent in the right pane to pick that agent for the
|
|
1511
|
+
// next task. Click handler below sets the agent <select> + scrolls
|
|
1512
|
+
// intake into view + focuses the prompt textarea.
|
|
1513
|
+
const stateTitle = a.state === 'ready' ? 'ready · click to select' :
|
|
1514
|
+
a.state === 'needs_install' ? 'needs install · click for setup' :
|
|
1515
|
+
a.state === 'needs_auth' ? 'needs auth · click for setup' :
|
|
1516
|
+
(a.state || 'unknown');
|
|
1517
|
+
return '<div class="member" data-agent-id="' + escape(a.id) + '" title="' + escape(stateTitle) + '">' +
|
|
1518
|
+
'<span class="member-presence ' + cls + '">' + glyph + '</span>' +
|
|
1519
|
+
'<span class="member-label">' + escape(a.label || a.id) + '</span>' +
|
|
1520
|
+
'</div>';
|
|
1521
|
+
}).join('');
|
|
1522
|
+
// Wire click → select-this-agent. One delegated handler is cheaper than
|
|
1523
|
+
// attaching one per item, and survives re-renders without leak.
|
|
1524
|
+
list.onclick = (ev) => {
|
|
1525
|
+
const item = ev.target && ev.target.closest && ev.target.closest('.member');
|
|
1526
|
+
if (!item) return;
|
|
1527
|
+
const id = item.getAttribute('data-agent-id');
|
|
1528
|
+
if (!id) return;
|
|
1529
|
+
// Set the agent dropdown to this agent. Trigger 'change' so the
|
|
1530
|
+
// updateSetupPanel code (existing) fires and shows install/auth help
|
|
1531
|
+
// for needs_install / needs_auth states.
|
|
1532
|
+
if (agentSelect) {
|
|
1533
|
+
agentSelect.value = id;
|
|
1534
|
+
try { agentSelect.dispatchEvent(new Event('change')); } catch { /* old browsers */ }
|
|
1535
|
+
}
|
|
1536
|
+
// Scroll intake into view + focus the prompt textarea.
|
|
1537
|
+
try { promptEl && promptEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* noop */ }
|
|
1538
|
+
if (promptEl) promptEl.focus();
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
fetch('/api/readiness/agents').then(r => r.json()).then(d => {
|
|
1543
|
+
renderAgentOptions(d.agents || [], d.auto);
|
|
1544
|
+
renderMembersPane(d.agents || [], d.auto);
|
|
1545
|
+
}).catch(() => { /* keep defaults */ });
|
|
1241
1546
|
|
|
1242
1547
|
// Working directory: persist user's pick, auto-fill with daemon cwd on first run.
|
|
1243
1548
|
// Gate A1: strangers shouldn't have to type a path just to hit "Assign"; the
|
|
@@ -1249,19 +1554,55 @@ ${SHARED_STYLES}
|
|
|
1249
1554
|
if (daemonCwd) {
|
|
1250
1555
|
cwdInput.placeholder = daemonCwd;
|
|
1251
1556
|
if (!cwdInput.value.trim()) cwdInput.value = daemonCwd; // auto-fill when nothing saved
|
|
1557
|
+
// Apr 29 — Slack-shape channels pane: surface the daemon cwd as the
|
|
1558
|
+
// current "channel" name + path. Multi-project ships in a later release.
|
|
1559
|
+
const nameEl = document.getElementById('current-channel-name');
|
|
1560
|
+
const cwdEl = document.getElementById('current-channel-cwd');
|
|
1561
|
+
if (nameEl) {
|
|
1562
|
+
const parts = String(daemonCwd).split('/').filter(Boolean);
|
|
1563
|
+
nameEl.textContent = parts[parts.length - 1] || 'project';
|
|
1564
|
+
}
|
|
1565
|
+
if (cwdEl) cwdEl.textContent = daemonCwd;
|
|
1252
1566
|
}
|
|
1253
1567
|
}).catch(() => { /* placeholder unchanged */ });
|
|
1254
1568
|
cwdInput.addEventListener('change', () => {
|
|
1255
1569
|
localStorage.setItem('codehere:cwd', cwdInput.value.trim());
|
|
1256
1570
|
});
|
|
1257
1571
|
|
|
1572
|
+
// Iter 12 — "+ New project" button now wired. Previously dead chrome with
|
|
1573
|
+
// title="Coming soon". Multi-project sidebar is iter 13+; for now the button
|
|
1574
|
+
// does the simplest useful thing: clear the cwd input + scroll-and-focus
|
|
1575
|
+
// so the user can type a new folder path. The intake form's existing Assign
|
|
1576
|
+
// flow handles spawning the task in that path.
|
|
1577
|
+
const newProjectBtn = document.getElementById('new-project-btn');
|
|
1578
|
+
if (newProjectBtn) {
|
|
1579
|
+
newProjectBtn.addEventListener('click', () => {
|
|
1580
|
+
cwdInput.value = '';
|
|
1581
|
+
localStorage.removeItem('codehere:cwd');
|
|
1582
|
+
// Smooth-scroll the intake form into view (project sidebar is on the left,
|
|
1583
|
+
// intake is on the right; on narrow screens the user might be scrolled).
|
|
1584
|
+
try { cwdInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* old browsers */ }
|
|
1585
|
+
cwdInput.focus();
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1258
1589
|
// Gate A1: starter-prompt chips prefill the textarea + focus it so the user
|
|
1259
1590
|
// can edit or hit ⌘-Enter immediately. No scroll-jumping, no redirect.
|
|
1260
|
-
document.getElementById('starter-prompts')
|
|
1591
|
+
const starterPromptsEl = document.getElementById('starter-prompts');
|
|
1592
|
+
starterPromptsEl.addEventListener('click', (ev) => {
|
|
1261
1593
|
const btn = ev.target && ev.target.closest('button[data-prompt]');
|
|
1262
1594
|
if (!btn) return;
|
|
1263
1595
|
promptEl.value = btn.getAttribute('data-prompt') || '';
|
|
1264
1596
|
promptEl.focus();
|
|
1597
|
+
starterPromptsEl.classList.add('dim'); // user picked one; chips fade
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
// Apr 29 — UX overhaul: TRY chips fade out once the user starts typing.
|
|
1601
|
+
// They serve as a discovery aid for first-visit users; a typing user has
|
|
1602
|
+
// already committed to a task and doesn't need the chips above the page.
|
|
1603
|
+
promptEl.addEventListener('input', () => {
|
|
1604
|
+
if (promptEl.value.length > 0) starterPromptsEl.classList.add('dim');
|
|
1605
|
+
else starterPromptsEl.classList.remove('dim');
|
|
1265
1606
|
});
|
|
1266
1607
|
|
|
1267
1608
|
function escape(s) {
|
|
@@ -1464,12 +1805,31 @@ ${SHARED_STYLES}
|
|
|
1464
1805
|
'<div class="steps-mini"></div>' +
|
|
1465
1806
|
'</div>' +
|
|
1466
1807
|
'<div class="session-controls">' +
|
|
1808
|
+
'<div class="view-toggle" title="Switch between thread view and raw event log">' +
|
|
1809
|
+
'<button class="view-thread active" data-view="thread">Thread</button>' +
|
|
1810
|
+
'<button class="view-raw" data-view="raw">Log</button>' +
|
|
1811
|
+
'</div>' +
|
|
1467
1812
|
'<span class="status-pill ' + session.status + runningClass + '">' + session.status + '</span>' +
|
|
1468
1813
|
'<button class="kill">Kill</button>' +
|
|
1469
1814
|
'</div>' +
|
|
1470
1815
|
'</div>' +
|
|
1471
|
-
'<div class="session-body">' +
|
|
1816
|
+
'<div class="session-body" data-view="thread">' +
|
|
1817
|
+
// Order: thread messages (chronological) → final report (closing
|
|
1818
|
+
// summary, rendered as a block when artifacts arrive) → reply input.
|
|
1819
|
+
// Apr 29 bug fix: previously final-report was BEFORE thread-messages
|
|
1820
|
+
// so on completed sessions the Final Report appeared above the user's
|
|
1821
|
+
// seed prompt, breaking chronological order in thread view.
|
|
1822
|
+
'<div class="thread-messages"></div>' +
|
|
1472
1823
|
'<div class="final-report" style="display:none"></div>' +
|
|
1824
|
+
// Iter 12 — reply surface now functional; replaced the prior dead
|
|
1825
|
+
// placeholder. Active for adapters with canAppendMessage=true
|
|
1826
|
+
// (claude-code today); disabled state for adapters that don't yet
|
|
1827
|
+
// implement appendMessage.
|
|
1828
|
+
'<div class="thread-input">' +
|
|
1829
|
+
'<textarea class="reply-text" placeholder="Reply to thread… (Cmd+Enter to send)" rows="2"></textarea>' +
|
|
1830
|
+
'<button class="reply-send" type="button">Send</button>' +
|
|
1831
|
+
'<div class="reply-status" style="display:none"></div>' +
|
|
1832
|
+
'</div>' +
|
|
1473
1833
|
'<div class="event-log"></div>' +
|
|
1474
1834
|
'</div>';
|
|
1475
1835
|
|
|
@@ -1482,6 +1842,59 @@ ${SHARED_STYLES}
|
|
|
1482
1842
|
const headEl = card.querySelector('.session-head');
|
|
1483
1843
|
const stepsEl = card.querySelector('.steps-mini');
|
|
1484
1844
|
const finalReportEl = card.querySelector('.final-report');
|
|
1845
|
+
const threadEl = card.querySelector('.thread-messages');
|
|
1846
|
+
const bodyEl = card.querySelector('.session-body');
|
|
1847
|
+
// Iter 12 — reply-to-thread refs + handler.
|
|
1848
|
+
const threadInputEl = card.querySelector('.thread-input');
|
|
1849
|
+
const replyTextEl = card.querySelector('.reply-text');
|
|
1850
|
+
const replySendBtn = card.querySelector('.reply-send');
|
|
1851
|
+
const replyStatusEl = card.querySelector('.reply-status');
|
|
1852
|
+
if (!session.canAppendMessage) {
|
|
1853
|
+
threadInputEl.classList.add('disabled');
|
|
1854
|
+
replyTextEl.placeholder = 'Reply unsupported for ' + agentLabel(session.agent) + ' yet';
|
|
1855
|
+
replyTextEl.disabled = true;
|
|
1856
|
+
replySendBtn.disabled = true;
|
|
1857
|
+
} else {
|
|
1858
|
+
const sendReply = async () => {
|
|
1859
|
+
const text = (replyTextEl.value || '').trim();
|
|
1860
|
+
if (!text) return;
|
|
1861
|
+
replySendBtn.disabled = true;
|
|
1862
|
+
replySendBtn.textContent = '...';
|
|
1863
|
+
replyStatusEl.style.display = 'none';
|
|
1864
|
+
replyStatusEl.classList.remove('error');
|
|
1865
|
+
// Optimistic: render the user's message immediately in the thread.
|
|
1866
|
+
appendThreadMessage(threadMessage({
|
|
1867
|
+
cls: 'user', author: 'You', time: new Date(), content: text,
|
|
1868
|
+
}));
|
|
1869
|
+
replyTextEl.value = '';
|
|
1870
|
+
try {
|
|
1871
|
+
const r = await fetch('/api/task/' + session.sessionId + '/message', {
|
|
1872
|
+
method: 'POST',
|
|
1873
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1874
|
+
body: JSON.stringify({ text }),
|
|
1875
|
+
});
|
|
1876
|
+
const data = await r.json();
|
|
1877
|
+
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
|
1878
|
+
// Render assistant response in the thread.
|
|
1879
|
+
appendThreadMessage(threadMessage({
|
|
1880
|
+
cls: 'agent', author: agentLabel(session.agent), time: new Date(),
|
|
1881
|
+
content: String(data.result || '(empty response)'),
|
|
1882
|
+
}));
|
|
1883
|
+
} catch (err) {
|
|
1884
|
+
replyStatusEl.textContent = 'Reply failed: ' + (err && err.message ? err.message : String(err));
|
|
1885
|
+
replyStatusEl.classList.add('error');
|
|
1886
|
+
replyStatusEl.style.display = 'block';
|
|
1887
|
+
} finally {
|
|
1888
|
+
replySendBtn.disabled = false;
|
|
1889
|
+
replySendBtn.textContent = 'Send';
|
|
1890
|
+
replyTextEl.focus();
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
replySendBtn.addEventListener('click', sendReply);
|
|
1894
|
+
replyTextEl.addEventListener('keydown', (ev) => {
|
|
1895
|
+
if ((ev.metaKey || ev.ctrlKey) && ev.key === 'Enter') { ev.preventDefault(); sendReply(); }
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1485
1898
|
const safetyPillEl = card.querySelector('.safety-pill');
|
|
1486
1899
|
const safetySafeEl = safetyPillEl.querySelector('.safe');
|
|
1487
1900
|
const safetyBlockedEl = safetyPillEl.querySelector('.blocked');
|
|
@@ -1511,6 +1924,146 @@ ${SHARED_STYLES}
|
|
|
1511
1924
|
card.classList.toggle('expanded');
|
|
1512
1925
|
});
|
|
1513
1926
|
|
|
1927
|
+
// Apr 29 — Slack-shape thread view.
|
|
1928
|
+
// Thread/Raw toggle in the card head. Default = thread; Raw stays available
|
|
1929
|
+
// for power users who want the unfiltered event stream.
|
|
1930
|
+
const viewBtns = card.querySelectorAll('.view-toggle button');
|
|
1931
|
+
viewBtns.forEach((btn) => {
|
|
1932
|
+
btn.addEventListener('click', (ev) => {
|
|
1933
|
+
ev.stopPropagation(); // don't collapse the card
|
|
1934
|
+
const view = btn.dataset.view;
|
|
1935
|
+
bodyEl.dataset.view = view;
|
|
1936
|
+
viewBtns.forEach((b) => b.classList.toggle('active', b === btn));
|
|
1937
|
+
});
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
// Build a single thread message DOM string.
|
|
1941
|
+
function threadMessage(opts) {
|
|
1942
|
+
const cls = opts.cls || 'agent';
|
|
1943
|
+
const av = opts.avatar || (cls === 'user' ? '👤' : (cls === 'tool' ? '⚙' : (cls === 'hook' ? '🛡' : (cls === 'error' ? '✗' : '🤖'))));
|
|
1944
|
+
const time = opts.time || new Date();
|
|
1945
|
+
return '<div class="thread-message ' + escape(cls) + (opts.modifier ? ' ' + escape(opts.modifier) : '') + '">' +
|
|
1946
|
+
'<div class="avatar">' + escape(av) + '</div>' +
|
|
1947
|
+
'<div class="body">' +
|
|
1948
|
+
'<div class="head">' +
|
|
1949
|
+
'<span class="author">' + escape(opts.author || 'agent') + '</span>' +
|
|
1950
|
+
'<span class="time">' + time.toLocaleTimeString() + '</span>' +
|
|
1951
|
+
'</div>' +
|
|
1952
|
+
'<div class="content">' + escape(String(opts.content || '')) + '</div>' +
|
|
1953
|
+
'</div>' +
|
|
1954
|
+
'</div>';
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function appendThreadMessage(html) {
|
|
1958
|
+
if (!html) return;
|
|
1959
|
+
threadEl.insertAdjacentHTML('beforeend', html);
|
|
1960
|
+
// Auto-scroll to bottom of thread (within the card)
|
|
1961
|
+
threadEl.scrollTop = threadEl.scrollHeight;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// Seed: user prompt as the first thread message
|
|
1965
|
+
threadEl.innerHTML = threadMessage({
|
|
1966
|
+
cls: 'user',
|
|
1967
|
+
author: 'You',
|
|
1968
|
+
time: new Date(session.startedAt),
|
|
1969
|
+
content: session.prompt,
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
// Parallel event → thread renderer. Initial pass duplicates parts of
|
|
1973
|
+
// renderEvent's branching; a follow-up refactor extracts a shared
|
|
1974
|
+
// eventToLines() primitive feeding both renderers.
|
|
1975
|
+
function renderEventToThread(event) {
|
|
1976
|
+
const type = String(event.type || '');
|
|
1977
|
+
const data = event.data ?? {};
|
|
1978
|
+
const t = new Date(event.timestamp || Date.now());
|
|
1979
|
+
const agent = agentLabel(session.agent);
|
|
1980
|
+
|
|
1981
|
+
if (type === 'stream_event') return;
|
|
1982
|
+
if (type === 'system' && (data.subtype === 'init' || data.subtype === 'status' || data.subtype === 'hook_started')) return;
|
|
1983
|
+
if (type === 'stdout' && !String(data.line || '').trim()) return;
|
|
1984
|
+
if (type === 'thread.started' || type === 'turn.started') return;
|
|
1985
|
+
if (type === 'item.started') return;
|
|
1986
|
+
|
|
1987
|
+
if (type === 'assistant' && data.message && Array.isArray(data.message.content)) {
|
|
1988
|
+
for (const block of data.message.content) {
|
|
1989
|
+
if (block.type === 'text') {
|
|
1990
|
+
const txt = String(block.text || '').trim();
|
|
1991
|
+
if (txt) appendThreadMessage(threadMessage({ cls: 'agent', author: agent, time: t, content: txt }));
|
|
1992
|
+
} else if (block.type === 'tool_use') {
|
|
1993
|
+
const name = String(block.name || 'tool');
|
|
1994
|
+
const input = block.input && typeof block.input === 'object' ? JSON.stringify(block.input) : '';
|
|
1995
|
+
const preview = input.length > 200 ? input.slice(0, 200) + '…' : input;
|
|
1996
|
+
appendThreadMessage(threadMessage({ cls: 'tool', author: agent + ' · ' + name, time: t, content: preview, avatar: '⚙' }));
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
} else if (type === 'user' && data.message && Array.isArray(data.message.content)) {
|
|
2000
|
+
for (const block of data.message.content) {
|
|
2001
|
+
if (block.type === 'tool_result') {
|
|
2002
|
+
const raw = Array.isArray(block.content) ? block.content.map((c) => c.text || '').join('') : String(block.content || '');
|
|
2003
|
+
const txt = raw.replace(/\\s+/g, ' ').trim();
|
|
2004
|
+
if (txt) {
|
|
2005
|
+
const preview = txt.length > 240 ? txt.slice(0, 240) + '…' : txt;
|
|
2006
|
+
const cls = block.is_error ? 'error' : 'tool';
|
|
2007
|
+
const author = block.is_error ? agent + ' · error' : agent + ' · result';
|
|
2008
|
+
appendThreadMessage(threadMessage({ cls, author, time: t, content: preview }));
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
} else if (type === 'system' && data.subtype === 'hook_response') {
|
|
2013
|
+
const name = String(data.hook_name || 'hook');
|
|
2014
|
+
const blocked = Number(data.exit_code ?? 0) !== 0;
|
|
2015
|
+
const stderr = String(data.stderr || '').trim();
|
|
2016
|
+
const content = blocked ? (stderr || 'blocked') : 'allowed';
|
|
2017
|
+
appendThreadMessage(threadMessage({ cls: 'hook', modifier: blocked ? 'blocked' : '', author: 'codehere · ' + name, time: t, content }));
|
|
2018
|
+
} else if (type === 'item.completed') {
|
|
2019
|
+
const item = data.item && typeof data.item === 'object' ? data.item : {};
|
|
2020
|
+
const itemType = String(item.type || 'item');
|
|
2021
|
+
if (itemType === 'agent_message' && item.text) {
|
|
2022
|
+
const txt = String(item.text).trim();
|
|
2023
|
+
if (txt) appendThreadMessage(threadMessage({ cls: 'agent', author: agent, time: t, content: txt.slice(0, 320) }));
|
|
2024
|
+
} else if (itemType === 'command_execution') {
|
|
2025
|
+
const cmd = String(item.command || '').replace(/\\n/g, ' ').trim();
|
|
2026
|
+
const cmdShort = cmd.length > 160 ? cmd.slice(0, 160) + '…' : cmd;
|
|
2027
|
+
appendThreadMessage(threadMessage({ cls: 'tool', author: agent + ' · shell', time: t, content: cmdShort, avatar: '⚙' }));
|
|
2028
|
+
const out = String(item.aggregated_output || '').trim();
|
|
2029
|
+
if (out) {
|
|
2030
|
+
const outShort = out.length > 200 ? out.slice(0, 200) + '…' : out;
|
|
2031
|
+
const ok = item.exit_code === 0 || item.status === 'completed';
|
|
2032
|
+
appendThreadMessage(threadMessage({
|
|
2033
|
+
cls: ok ? 'tool' : 'error',
|
|
2034
|
+
author: agent + ' · ' + (ok ? 'result' : 'exit ' + (item.exit_code ?? '?')),
|
|
2035
|
+
time: t,
|
|
2036
|
+
content: outShort,
|
|
2037
|
+
}));
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
} else if (type === 'stdout') {
|
|
2041
|
+
// Aider and generic CLI adapters stream via plain stdout. Codex
|
|
2042
|
+
// Round 1 (Apr 29) caught: without this branch, those sessions
|
|
2043
|
+
// showed only the seed prompt + exit marker in thread view.
|
|
2044
|
+
const line = String(data.line || '').trim();
|
|
2045
|
+
if (line) appendThreadMessage(threadMessage({ cls: 'agent', author: agent, time: t, content: line.length > 320 ? line.slice(0, 320) + '…' : line }));
|
|
2046
|
+
} else if (type === 'stderr') {
|
|
2047
|
+
const line = String(data.line || '').trim();
|
|
2048
|
+
if (line) appendThreadMessage(threadMessage({ cls: 'tool', author: agent + ' · stderr', time: t, content: line.length > 240 ? line.slice(0, 240) + '…' : line, avatar: '⚠' }));
|
|
2049
|
+
} else if (type === 'error') {
|
|
2050
|
+
const msg = String(data.message || data.error || 'error');
|
|
2051
|
+
appendThreadMessage(threadMessage({ cls: 'error', author: agent, time: t, content: msg }));
|
|
2052
|
+
} else if (type === 'codehere:error') {
|
|
2053
|
+
const msg = String(data.message || data.kind || 'error');
|
|
2054
|
+
appendThreadMessage(threadMessage({ cls: 'error', author: 'codehere', time: t, content: msg }));
|
|
2055
|
+
} else if (type === 'exit') {
|
|
2056
|
+
const status = String(data.status || 'completed');
|
|
2057
|
+
appendThreadMessage(threadMessage({
|
|
2058
|
+
cls: status === 'error' ? 'error' : 'agent',
|
|
2059
|
+
author: agent + ' · ' + status,
|
|
2060
|
+
time: t,
|
|
2061
|
+
content: 'session ended',
|
|
2062
|
+
avatar: status === 'error' ? '✗' : '✓',
|
|
2063
|
+
}));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1514
2067
|
// Layer 2: render the semantic step list inside the collapsed card.
|
|
1515
2068
|
// Principles #1 (web is not a terminal) + #2 (info density) — show last 3
|
|
1516
2069
|
// steps with pass/active/fail glyphs; hide the rest behind "+ N more" so
|
|
@@ -1535,12 +2088,35 @@ ${SHARED_STYLES}
|
|
|
1535
2088
|
// Layer 3: render the final report block when the task completes.
|
|
1536
2089
|
// Principles #2 + #7 — density (files/tests/trust in one view) and
|
|
1537
2090
|
// agent-provenance (the report attributes to a specific agent).
|
|
1538
|
-
|
|
1539
|
-
|
|
2091
|
+
// Iter 12 — prose summary (3 paragraphs, plain English) prepended above
|
|
2092
|
+
// the artifact list. Non-technical users see prose first; raw artifact
|
|
2093
|
+
// detail (files/tests/tokens) below as supporting context.
|
|
2094
|
+
function renderArtifacts(artifacts, finalReport, status) {
|
|
2095
|
+
// Show the block if either prose summary OR artifacts exist OR if a
|
|
2096
|
+
// completed task is still generating its report (placeholder state).
|
|
2097
|
+
const isCompleted = status === 'completed' || status === 'killed' || status === 'error';
|
|
2098
|
+
const hasContent = artifacts || finalReport;
|
|
2099
|
+
const isAwaitingReport = isCompleted && !finalReport;
|
|
2100
|
+
if (!hasContent && !isAwaitingReport) { finalReportEl.style.display = 'none'; return; }
|
|
1540
2101
|
finalReportEl.style.display = 'block';
|
|
1541
|
-
const files = Array.isArray(artifacts
|
|
1542
|
-
const tests = artifacts
|
|
2102
|
+
const files = Array.isArray(artifacts?.files) ? artifacts.files : [];
|
|
2103
|
+
const tests = artifacts?.tests;
|
|
1543
2104
|
let html = '<div class="fr-label">Final report · ' + escape(agentLabel(session.agent)) + '</div>';
|
|
2105
|
+
if (finalReport && finalReport.summary) {
|
|
2106
|
+
// Plain-English prose. Split paragraphs on blank lines, render each as <p>.
|
|
2107
|
+
// NOTE: this code is inside a TS template literal — backslashes in
|
|
2108
|
+
// regex literals must be doubled so the served JS gets the right
|
|
2109
|
+
// sequence (test/cli/ui-served-html-syntax.test.ts guards this).
|
|
2110
|
+
const paragraphs = String(finalReport.summary).split(/\\n\\s*\\n/).filter(p => p.trim());
|
|
2111
|
+
html += '<div class="fr-summary">' +
|
|
2112
|
+
paragraphs.map(p => '<p>' + escape(p.trim()) + '</p>').join('') +
|
|
2113
|
+
'</div>';
|
|
2114
|
+
} else if (isAwaitingReport) {
|
|
2115
|
+
// Placeholder — task done, report still generating (or provider failed silently).
|
|
2116
|
+
// After ~30s with no report, the placeholder persists. Iter 13 will replace
|
|
2117
|
+
// with explicit "report unavailable — start FCC proxy or set ANTHROPIC_API_KEY".
|
|
2118
|
+
html += '<div class="fr-summary fr-summary-pending"><p>Generating report…</p></div>';
|
|
2119
|
+
}
|
|
1544
2120
|
if (files.length > 0) {
|
|
1545
2121
|
html += '<div class="fr-files">' + files.map(f => {
|
|
1546
2122
|
const lines = '<span class="add">+' + (f.added || 0) + '</span> <span class="rem">−' + (f.removed || 0) + '</span>';
|
|
@@ -1574,7 +2150,11 @@ ${SHARED_STYLES}
|
|
|
1574
2150
|
|
|
1575
2151
|
// Hydrate from initial snapshot (late-joiner support + persisted sessions)
|
|
1576
2152
|
if (Array.isArray(session.steps) && session.steps.length) renderSteps(session.steps);
|
|
1577
|
-
if (
|
|
2153
|
+
// Render Final Report block if there's anything to show (prose summary,
|
|
2154
|
+
// artifacts, or "generating…" placeholder for a freshly-completed session).
|
|
2155
|
+
if (session.artifacts || session.finalReport || session.status === 'completed' || session.status === 'killed' || session.status === 'error') {
|
|
2156
|
+
renderArtifacts(session.artifacts, session.finalReport, session.status);
|
|
2157
|
+
}
|
|
1578
2158
|
|
|
1579
2159
|
// Persisted sessions have no live event stream and no subprocess to kill.
|
|
1580
2160
|
if (isPersisted) {
|
|
@@ -1590,7 +2170,7 @@ ${SHARED_STYLES}
|
|
|
1590
2170
|
|
|
1591
2171
|
// Layer 2/3 synthetic events — handle before renderEvent (so they don't leak into the raw log)
|
|
1592
2172
|
if (event.type === 'codehere:steps') { renderSteps(event.data?.steps); return; }
|
|
1593
|
-
if (event.type === 'codehere:artifacts') { renderArtifacts(event.data); return; }
|
|
2173
|
+
if (event.type === 'codehere:artifacts') { renderArtifacts(event.data, undefined, session.status); return; }
|
|
1594
2174
|
|
|
1595
2175
|
// Any structured error flips the status pill immediately — don't wait for
|
|
1596
2176
|
// the subprocess to exit. Many CLIs print a logical error to stdout and
|
|
@@ -1604,6 +2184,7 @@ ${SHARED_STYLES}
|
|
|
1604
2184
|
bumpSafety(event);
|
|
1605
2185
|
|
|
1606
2186
|
renderEvent(logEl, event);
|
|
2187
|
+
renderEventToThread(event); // Apr 29 · Slack-shape thread v0 (Day 2)
|
|
1607
2188
|
if (event.type === 'exit') {
|
|
1608
2189
|
updateStatus(pillEl, event.data?.status || 'completed');
|
|
1609
2190
|
killBtn.disabled = true; killBtn.style.opacity = '0.4';
|
|
@@ -1627,24 +2208,29 @@ ${SHARED_STYLES}
|
|
|
1627
2208
|
killBtn.textContent = 'Kill';
|
|
1628
2209
|
});
|
|
1629
2210
|
|
|
1630
|
-
|
|
2211
|
+
// Iter 12 — expose renderArtifacts so the polling loop can refresh the
|
|
2212
|
+
// Final Report block when the prose summary arrives async (~2-5s after
|
|
2213
|
+
// task exit). Without this, non-technical users would have to refresh.
|
|
2214
|
+
sessions.set(session.sessionId, { card, logEl, es, renderArtifacts });
|
|
1631
2215
|
}
|
|
1632
2216
|
|
|
1633
2217
|
async function assignTask() {
|
|
1634
2218
|
const prompt = promptEl.value.trim();
|
|
1635
2219
|
if (!prompt) return;
|
|
1636
|
-
|
|
2220
|
+
// Apr 29: empty value = "Auto" option = backend defaults to conductor
|
|
2221
|
+
// (the supervisor routes). Specialists are explicit overrides.
|
|
2222
|
+
const agent = agentSelect.value;
|
|
1637
2223
|
const cwd = cwdInput.value.trim();
|
|
1638
2224
|
spawnBtn.disabled = true; spawnBtn.textContent = '...';
|
|
1639
2225
|
try {
|
|
1640
2226
|
const res = await fetch('/api/task', {
|
|
1641
2227
|
method: 'POST',
|
|
1642
2228
|
headers: { 'Content-Type': 'application/json' },
|
|
1643
|
-
body: JSON.stringify({ prompt, agent, cwd: cwd || undefined }),
|
|
2229
|
+
body: JSON.stringify({ prompt, agent: agent || undefined, cwd: cwd || undefined }),
|
|
1644
2230
|
});
|
|
1645
2231
|
const data = await res.json();
|
|
1646
2232
|
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 });
|
|
2233
|
+
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent || 'conductor' });
|
|
1648
2234
|
promptEl.value = '';
|
|
1649
2235
|
} catch (e) {
|
|
1650
2236
|
alert('Network error: ' + (e && e.message ? e.message : e));
|
|
@@ -1725,9 +2311,10 @@ ${SHARED_STYLES}
|
|
|
1725
2311
|
refreshAudit();
|
|
1726
2312
|
setInterval(refreshAudit, 30000); // audit log isn't hot; 30s is plenty
|
|
1727
2313
|
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
|
|
2314
|
+
// Apr 29 — UX overhaul: status-banner merged into the single stats-line
|
|
2315
|
+
// (id="audit-summary"). updateBanner still updates the agents-on-machine
|
|
2316
|
+
// count + hidden running/blocked/last-edit counters; updateAudit controls
|
|
2317
|
+
// visibility of the whole row via the .empty class.
|
|
1731
2318
|
let latestLastEditAt = null;
|
|
1732
2319
|
function agoStr(ms) {
|
|
1733
2320
|
if (!ms) return '—';
|
|
@@ -1742,19 +2329,20 @@ ${SHARED_STYLES}
|
|
|
1742
2329
|
const running = spawned.filter(s => s.status === 'running').length;
|
|
1743
2330
|
const blocked = spawned.filter(s => s.status === 'error').length;
|
|
1744
2331
|
const detected = (d.activeAgents || []).length;
|
|
1745
|
-
document.getElementById(
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
blockedEl.classList.toggle('nonzero', blocked > 0);
|
|
2332
|
+
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = String(val); };
|
|
2333
|
+
setText('sb-running', running);
|
|
2334
|
+
setText('sb-detected', detected);
|
|
2335
|
+
setText('sb-blocked', blocked);
|
|
1750
2336
|
latestLastEditAt = d.lastEditAt || null;
|
|
1751
|
-
|
|
1752
|
-
//
|
|
1753
|
-
|
|
2337
|
+
setText('sb-last', agoStr(latestLastEditAt));
|
|
2338
|
+
// Reveal the unified stats-line once we have anything to show.
|
|
2339
|
+
if (running + detected + blocked + spawned.length > 0) {
|
|
2340
|
+
auditRow.classList.remove('empty');
|
|
2341
|
+
}
|
|
1754
2342
|
}
|
|
1755
|
-
// Tick the "last edit" relative label every second even without a refetch
|
|
1756
2343
|
setInterval(() => {
|
|
1757
|
-
|
|
2344
|
+
const el = document.getElementById('sb-last');
|
|
2345
|
+
if (el && latestLastEditAt) el.textContent = agoStr(latestLastEditAt);
|
|
1758
2346
|
}, 1000);
|
|
1759
2347
|
|
|
1760
2348
|
function loadSessions() {
|
|
@@ -1772,7 +2360,10 @@ ${SHARED_STYLES}
|
|
|
1772
2360
|
}).catch(() => { /* fine */ });
|
|
1773
2361
|
}
|
|
1774
2362
|
loadSessions();
|
|
1775
|
-
// Refresh detected-agents list + banner every 5s
|
|
2363
|
+
// Refresh detected-agents list + banner every 5s. Iter 12 — also patches
|
|
2364
|
+
// existing session cards with new finalReport prose when it arrives async.
|
|
2365
|
+
// Cheap re-render (innerHTML on a small div); non-technical user sees the
|
|
2366
|
+
// report appear on its own, no manual refresh.
|
|
1776
2367
|
setInterval(() => {
|
|
1777
2368
|
fetch('/api/tasks').then(r => r.json()).then(d => {
|
|
1778
2369
|
const seen = new Set();
|
|
@@ -1782,6 +2373,13 @@ ${SHARED_STYLES}
|
|
|
1782
2373
|
}
|
|
1783
2374
|
pruneStaleAgents(seen);
|
|
1784
2375
|
updateBanner(d);
|
|
2376
|
+
// Patch existing session cards with finalReport when it arrives.
|
|
2377
|
+
for (const s of (d.spawned || d.sessions || [])) {
|
|
2378
|
+
const entry = sessions.get(s.sessionId);
|
|
2379
|
+
if (entry && entry.renderArtifacts) {
|
|
2380
|
+
entry.renderArtifacts(s.artifacts, s.finalReport, s.status);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
1785
2383
|
}).catch(() => { /* fine */ });
|
|
1786
2384
|
}, 5000);
|
|
1787
2385
|
|
|
@@ -1804,7 +2402,7 @@ ${SHARED_STYLES}
|
|
|
1804
2402
|
actionHtml += ' <a href="' + escape(report.action.url) + '" target="_blank" rel="noreferrer">' + escape(report.action.label || 'Open') + ' →</a>';
|
|
1805
2403
|
}
|
|
1806
2404
|
}
|
|
1807
|
-
actionHtml += ' <button id="readiness-refresh">
|
|
2405
|
+
actionHtml += ' <button id="readiness-refresh">Recheck</button>';
|
|
1808
2406
|
readinessEl.innerHTML =
|
|
1809
2407
|
'<div class="r-title">' + escape(report.title) + '</div>' +
|
|
1810
2408
|
'<div class="r-detail">' + escape(report.detail) + '</div>' +
|