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.
Files changed (24) hide show
  1. package/README.md +64 -7
  2. package/dist/application/use-cases/generate-final-report.js +144 -0
  3. package/dist/index.js +47 -4
  4. package/dist/infrastructure/hooks/claude-sdk-hook.js +18 -3
  5. package/dist/infrastructure/orchestration/aider-adapter.js +12 -2
  6. package/dist/infrastructure/orchestration/claude-code-adapter.js +70 -1
  7. package/dist/infrastructure/orchestration/codex-adapter.js +6 -1
  8. package/dist/infrastructure/orchestration/conductor-adapter.js +12 -2
  9. package/dist/infrastructure/orchestration/gemini-adapter.js +41 -5
  10. package/dist/infrastructure/orchestration/generic-cli-adapter.js +10 -2
  11. package/dist/infrastructure/orchestration/memory-injector.js +337 -0
  12. package/dist/infrastructure/orchestration/opencode-adapter.js +34 -4
  13. package/dist/infrastructure/orchestration/progress-aggregator.js +119 -0
  14. package/dist/infrastructure/orchestration/readiness.js +2 -5
  15. package/dist/infrastructure/orchestration/session-manager.js +65 -2
  16. package/dist/infrastructure/orchestration/session-persistence.js +13 -1
  17. package/dist/infrastructure/protocols/mcp-server-impl.js +86 -14
  18. package/dist/infrastructure/protocols/ui-page.js +665 -67
  19. package/dist/presentation/cli/commands/init-command.js +345 -24
  20. package/dist/presentation/cli/commands/memory-command.js +1017 -0
  21. package/dist/presentation/cli/commands/protocols-builder.js +254 -0
  22. package/dist/presentation/cli/commands/protocols-command.js +84 -0
  23. package/dist/presentation/cli/commands/repo-map-builder.js +247 -0
  24. 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
- <div class="hero-beam"></div>
990
-
991
- <div class="readiness" id="readiness" style="position:relative;z-index:1"></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
- <div class="audit-summary empty" id="audit-summary" style="position:relative;z-index:1">
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="status-banner" id="status-banner" style="position:relative;z-index:1;display:none">
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 do you need <span class="gradient-text-shimmer">built or fixed</span>?</h1>
1016
- <p class="sub">Describe a task. codehere delegates to Claude Code, wraps every action with the safety hook, streams it back here.</p>
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 the project</button>
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="claude-code">Claude Code</option></select>
1026
- <input type="text" id="cwd" class="cwd" placeholder="working directory" spellcheck="false" />
1027
- <button class="spawn" id="spawn">Assign task</button>
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 to assign</span>
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 — assign a task above</div>
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
- function renderAgentOptions(agents) {
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
- if (agentReadiness.length === 0) return;
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
- agentSelect.innerHTML = sorted.map(a =>
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
- const agent = agentReadiness.find(a => a.id === id);
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
- fetch('/api/readiness/agents').then(r => r.json()).then(d => renderAgentOptions(d.agents || [])).catch(() => { /* keep defaults */ });
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').addEventListener('click', (ev) => {
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
- function renderArtifacts(artifacts) {
1539
- if (!artifacts) { finalReportEl.style.display = 'none'; return; }
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.files) ? artifacts.files : [];
1542
- const tests = artifacts.tests;
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 (session.artifacts) renderArtifacts(session.artifacts);
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
- sessions.set(session.sessionId, { card, logEl, es });
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
- const agent = agentSelect.value || 'claude-code';
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
- // Layer 7: status-at-a-glance banner — one row answers "what's happening".
1729
- // Principles #3 (status at a glance) + #4 (multi-session default: one row scales from 1 to N sessions).
1730
- const banner = document.getElementById('status-banner');
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('sb-running').textContent = String(running);
1746
- document.getElementById('sb-detected').textContent = String(detected);
1747
- const blockedEl = document.getElementById('sb-blocked');
1748
- blockedEl.textContent = String(blocked);
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
- document.getElementById('sb-last').textContent = agoStr(latestLastEditAt);
1752
- // Show the banner once we have anything to show (sessions or detected agents)
1753
- banner.style.display = (running + detected + blocked + spawned.length > 0) ? 'flex' : 'none';
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
- if (latestLastEditAt) document.getElementById('sb-last').textContent = agoStr(latestLastEditAt);
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">Check again</button>';
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>' +