codehere 0.3.4 → 0.5.0-alpha.2

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