codehere 0.6.1 → 0.7.0

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 (81) hide show
  1. package/README.md +5 -3
  2. package/dist/application/use-cases/domain-trust.js +110 -0
  3. package/dist/application/use-cases/generate-final-report.js +84 -5
  4. package/dist/application/use-cases/generate-narration.js +20 -3
  5. package/dist/application/use-cases/generate-thread-title.js +4 -0
  6. package/dist/application/use-cases/thread-export.js +155 -0
  7. package/dist/application/use-cases/verify-claim.js +274 -0
  8. package/dist/application/use-cases/write-pass.js +277 -0
  9. package/dist/index.js +218 -19
  10. package/dist/infrastructure/completion/bash-completion.js +2 -2
  11. package/dist/infrastructure/config/config-manager.js +1 -1
  12. package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
  13. package/dist/infrastructure/context/adapters/figma-source.js +163 -0
  14. package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
  15. package/dist/infrastructure/context/adapters/linear-source.js +190 -0
  16. package/dist/infrastructure/context/context-resolver.js +144 -0
  17. package/dist/infrastructure/context/context-source.js +114 -0
  18. package/dist/infrastructure/context/oauth-flow.js +207 -0
  19. package/dist/infrastructure/context/oauth-store.js +119 -0
  20. package/dist/infrastructure/context/registry-bootstrap.js +99 -0
  21. package/dist/infrastructure/context/write-pass-resolver.js +106 -0
  22. package/dist/infrastructure/cost/budget-config.js +1 -1
  23. package/dist/infrastructure/cost/spend-aggregator.js +5 -5
  24. package/dist/infrastructure/hooks/claude-sdk-hook.js +79 -59
  25. package/dist/infrastructure/hooks/session-analyzer.js +65 -8
  26. package/dist/infrastructure/observability/execution-tracer.js +0 -5
  27. package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
  28. package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
  29. package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
  30. package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
  31. package/dist/infrastructure/orchestration/codex-adapter.js +157 -19
  32. package/dist/infrastructure/orchestration/context-handoff.js +4 -3
  33. package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
  34. package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
  35. package/dist/infrastructure/orchestration/gemini-adapter.js +15 -17
  36. package/dist/infrastructure/orchestration/graphify-context.js +6 -13
  37. package/dist/infrastructure/orchestration/memory-injector.js +11 -16
  38. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
  39. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
  40. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
  41. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
  42. package/dist/infrastructure/orchestration/opencode-adapter.js +23 -3
  43. package/dist/infrastructure/orchestration/progress-aggregator.js +178 -5
  44. package/dist/infrastructure/orchestration/session-manager.js +142 -29
  45. package/dist/infrastructure/orchestration/session-persistence.js +139 -30
  46. package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
  47. package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
  48. package/dist/infrastructure/protocols/cloud-sync.js +10 -11
  49. package/dist/infrastructure/protocols/mcp-server-impl.js +1295 -17
  50. package/dist/infrastructure/protocols/thread-phase.js +105 -0
  51. package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
  52. package/dist/infrastructure/protocols/ui-page.js +12 -3299
  53. package/dist/infrastructure/protocols/verify-page.js +941 -0
  54. package/dist/infrastructure/security/trust-scorer.js +76 -1
  55. package/dist/infrastructure/storage/cross-tool-agents.js +20 -3
  56. package/dist/infrastructure/storage/project-thread-migration.js +14 -9
  57. package/dist/infrastructure/storage/project-thread-reader.js +11 -1
  58. package/dist/infrastructure/storage/project-thread-title-backfill.js +7 -3
  59. package/dist/monitoring.js +1 -1
  60. package/dist/presentation/cli/commands/audit-command.js +126 -0
  61. package/dist/presentation/cli/commands/budget-command.js +2 -2
  62. package/dist/presentation/cli/commands/config-command.js +2 -2
  63. package/dist/presentation/cli/commands/context-source-command.js +198 -0
  64. package/dist/presentation/cli/commands/help-command.js +51 -1
  65. package/dist/presentation/cli/commands/init-command.js +5 -4
  66. package/dist/presentation/cli/commands/memory-command.js +29 -15
  67. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  68. package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
  69. package/dist/presentation/cli/commands/report-command.js +6 -4
  70. package/dist/presentation/cli/commands/setup-command.js +71 -18
  71. package/dist/presentation/cli/commands/status-command.js +0 -4
  72. package/dist/presentation/cli/commands/thread-command.js +288 -0
  73. package/dist/presentation/cli/commands/verify-command.js +233 -0
  74. package/dist/presentation/cli/error-display.js +1 -1
  75. package/dist/presentation/cli/repl.js +52 -2
  76. package/dist/presentation/cli/task-deliverable.js +127 -0
  77. package/dist/testing/cassette.js +125 -0
  78. package/dist/testing/replay-adapter.js +166 -0
  79. package/dist/ui.js +54 -29
  80. package/dist/utils/typo-guard.js +1 -1
  81. package/package.json +4 -2
@@ -1,3 +1,4 @@
1
+ import { PHASE_CLASSIFIER_SRC } from './thread-phase.js';
1
2
  const SHARED_STYLES = `
2
3
  :root {
3
4
  --bg: hsl(0 0% 3.9%); --bg-1: hsl(0 0% 6%); --bg-2: hsl(0 0% 9%);
@@ -7,6 +8,13 @@ const SHARED_STYLES = `
7
8
  --border: rgba(255,255,255,0.10); --border-soft: rgba(255,255,255,0.06);
8
9
  --accent: #ff9a76; --accent-wash: rgba(255,154,118,0.10);
9
10
  --allow: #28c840; --block: #ff5f57; --warn: #febc2e;
11
+ --info: #76b7ff;
12
+ /* Per-agent identity colors — AGENTIC_UX_FRAMEWORK primitive #1 */
13
+ --agent-claude: #d97757;
14
+ --agent-codex: #10a37f;
15
+ --agent-aider: #4a90e2;
16
+ --agent-gemini: #fbbc05;
17
+ --agent-opencode: #06b6d4;
10
18
  --radius: 8px;
11
19
  --sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
12
20
  --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
@@ -16,19 +24,833 @@ const SHARED_STYLES = `
16
24
  margin: 0; background: var(--bg); color: var(--fg);
17
25
  font-family: var(--sans); font-size: 13px; line-height: 1.55;
18
26
  -webkit-font-smoothing: antialiased; height: 100vh; overflow: hidden;
27
+ position: relative;
19
28
  }
20
- .app { height: 100vh; display: flex; flex-direction: column; }
29
+ /* Ambient dot-grid backdrop · matches demo v12 + landing globals.css.
30
+ * Very faint blue dots on 24px grid · adds depth without distraction so
31
+ * the cards above visibly "lift" off a textured surface. Fixed position
32
+ * so it stays put while content scrolls. */
33
+ body::before {
34
+ content: ''; position: fixed; inset: 0;
35
+ background-image: radial-gradient(circle, rgba(74,144,226,0.045) 1px, transparent 1px);
36
+ background-size: 24px 24px;
37
+ pointer-events: none; z-index: 0; opacity: 0.7;
38
+ }
39
+ .app { height: 100vh; display: flex; flex-direction: column; position: relative; z-index: 1; }
21
40
 
41
+ /* Header · glass · matches demo v12 + landing globals · darker tint
42
+ * with backdrop blur + saturate so the header sits visibly above the
43
+ * scroll content beneath it. */
22
44
  .header {
23
- display: flex; align-items: center; gap: 16px;
45
+ display: flex; align-items: center; gap: 14px;
24
46
  padding: 10px 20px; border-bottom: 1px solid var(--border-soft);
25
- background: rgba(0,0,0,0.3); flex-shrink: 0;
47
+ background: rgba(0,0,0,0.55);
48
+ backdrop-filter: blur(16px) saturate(140%);
49
+ -webkit-backdrop-filter: blur(16px) saturate(140%);
50
+ flex-shrink: 0;
51
+ }
52
+ /* Brand wordmark · matches landing + demo v12 spec · gradient name + pulsing
53
+ * brand-color dot. First thing every user sees on every screen. */
54
+ .logo { font-size: 15px; font-weight: 700; display: inline-flex; align-items: center; gap: 7px; letter-spacing: -0.01em; }
55
+ .logo .name {
56
+ background: linear-gradient(135deg, #ff9a76 0%, #4a90e2 100%);
57
+ -webkit-background-clip: text; background-clip: text;
58
+ -webkit-text-fill-color: transparent; color: transparent;
59
+ letter-spacing: -0.01em;
60
+ }
61
+ .logo .dot {
62
+ width: 6px; height: 6px; border-radius: 50%;
63
+ background: var(--accent); animation: pulse 1.6s ease infinite;
64
+ box-shadow: 0 0 0 3px rgba(255,154,118,0.18);
65
+ flex-shrink: 0;
66
+ }
67
+
68
+ /* A-04 · local-first status pill · pinned next to the logo so the privacy
69
+ * promise is the SECOND thing every user sees · zero telemetry, all data
70
+ * on this machine. The pulsing dot says "still alive" without occupying
71
+ * focus. */
72
+ .local-pill {
73
+ display: inline-flex; align-items: center; gap: 6px;
74
+ font-family: var(--mono); font-size: 10px; color: var(--fg-50);
75
+ padding: 3px 8px; border: 1px solid var(--border); border-radius: 999px;
76
+ background: rgba(40,200,64,0.04);
77
+ }
78
+ .local-pill .dot {
79
+ width: 6px; height: 6px; border-radius: 50%;
80
+ background: var(--allow); animation: pulse 2.4s ease infinite;
81
+ }
82
+ .local-pill:hover { color: var(--fg-70); border-color: var(--fg-30); }
83
+
84
+ /* Beta badge · honest product-stage signal pinned to the wordmark. codehere
85
+ * is pre-1.0 and heading to invite-only validation; the badge sets that
86
+ * expectation up front rather than implying a finished GA product. Brand
87
+ * tint (the only colored chip besides the green local-pill) so it reads as
88
+ * intentional, not a warning. */
89
+ .beta-badge {
90
+ font-family: var(--mono); font-size: 9px; font-weight: 700;
91
+ letter-spacing: 0.06em; text-transform: uppercase; line-height: 1;
92
+ color: var(--accent, #ff9a76); padding: 3px 7px; border-radius: 999px;
93
+ border: 1px solid rgba(255, 154, 118, 0.45);
94
+ background: rgba(255, 154, 118, 0.12);
95
+ align-self: center; cursor: default;
96
+ }
97
+
98
+ /* Header budget chip · matches demo v12 spec · bordered chip with padding
99
+ * so the cost signal looks like a deliberate object, not a stray number. */
100
+ .budget {
101
+ margin-left: auto; font-family: var(--mono); font-size: 11px;
102
+ color: var(--fg-70); padding: 3px 9px;
103
+ border: 1px solid var(--border); border-radius: 4px;
104
+ letter-spacing: 0.02em;
105
+ }
106
+ .budget.safe { color: var(--fg-70); border-color: var(--border); }
107
+ .budget.warn { color: var(--warn); border-color: rgba(254,188,46,0.4); background: rgba(254,188,46,0.04); }
108
+ .budget.block { color: var(--block); border-color: rgba(255,95,87,0.4); background: rgba(255,95,87,0.05); font-weight: 600; }
109
+
110
+ /* D-01 · file tree with trust grades · in the task detail · render each
111
+ * file as a chip with +N/-M diff counts and a trust grade pill. */
112
+ .file-chip {
113
+ display: inline-flex; align-items: center; gap: 8px;
114
+ padding: 5px 10px; margin: 4px 6px 4px 0;
115
+ border: 1px solid var(--border-soft); border-radius: 6px;
116
+ background: rgba(255,255,255,0.02);
117
+ font-family: var(--mono); font-size: 11px;
118
+ }
119
+ .file-chip .path { color: var(--fg-90); }
120
+ .file-chip .added { color: var(--allow); font-size: 10px; }
121
+ .file-chip .removed { color: var(--block); font-size: 10px; }
122
+ .file-chip .trust { font-size: 9px; padding: 1px 5px; border-radius: 3px; font-weight: 600; }
123
+ .file-chip .trust.A { color: var(--allow); background: rgba(40,200,64,0.10); }
124
+ .file-chip .trust.B { color: #8be88b; background: rgba(40,200,64,0.06); }
125
+ .file-chip .trust.C { color: var(--warn); background: rgba(254,188,46,0.10); }
126
+ .file-chip .trust.F { color: var(--block); background: rgba(255,95,87,0.10); }
127
+ /* File tree · dir-grouped with connectors + per-row trust (demo 2.5) */
128
+ .file-tree { font-family: var(--mono); font-size: 11px; margin-bottom: 8px; }
129
+ .ft-dir { color: var(--fg-50); font-size: 10.5px; margin: 6px 0 2px; }
130
+ .ft-row { display: flex; align-items: center; gap: 6px; padding: 2px 0 2px 8px; }
131
+ .ft-branch { color: var(--fg-30); }
132
+ .ft-name { color: var(--fg-80); }
133
+ .ft-row .added { color: var(--allow); font-size: 10px; }
134
+ .ft-row .removed { color: var(--block); font-size: 10px; }
135
+ .ft-row .trust { font-size: 9px; padding: 1px 5px; border-radius: 3px; font-weight: 600; margin-left: auto; }
136
+ .ft-row .trust.A { color: var(--allow); background: rgba(40,200,64,0.10); }
137
+ .ft-row .trust.B { color: #8be88b; background: rgba(40,200,64,0.06); }
138
+ .ft-row .trust.C { color: var(--warn); background: rgba(254,188,46,0.10); }
139
+ .ft-row .trust.F { color: var(--block); background: rgba(255,95,87,0.10); }
140
+
141
+ /* Inline code-card · matches demo v12 (lines 441-454) · renders when an
142
+ * artifact file carries a .snippet. Dark VS-Code-ish surface with mono
143
+ * font, line numbers, lightweight syntax tinting via .kw/.fn/.str/.num/.cm.
144
+ * Lives directly under the file-chip strip so the reviewer can SEE what
145
+ * the agent wrote, not just "+118 lines". */
146
+ .code-card {
147
+ background: hsl(0 0% 12%); border: 1px solid var(--border);
148
+ border-radius: 6px; overflow: hidden; margin: 6px 0 10px;
149
+ }
150
+ .code-card .head {
151
+ padding: 7px 12px; background: rgba(255,255,255,0.03);
152
+ border-bottom: 1px solid var(--border-soft);
153
+ display: flex; align-items: center; gap: 8px;
154
+ font-family: var(--mono); font-size: 10.5px; color: var(--fg-50);
155
+ }
156
+ .code-card .head .lang { color: var(--fg-30); }
157
+ .code-card .head .path { color: var(--fg-70); }
158
+ .code-card .head .meta { margin-left: auto; color: var(--fg-30); font-size: 10px; }
159
+ .code-card pre {
160
+ margin: 0; padding: 11px 14px;
161
+ font-family: var(--mono); font-size: 11.5px; line-height: 1.6;
162
+ color: var(--fg-70); overflow-x: auto; white-space: pre;
163
+ }
164
+ .code-card .ln {
165
+ display: inline-block; width: 26px; text-align: right; margin-right: 12px;
166
+ color: var(--fg-20); user-select: none;
167
+ }
168
+ .code-card .kw { color: #c586c0; }
169
+ .code-card .fn { color: #dcdcaa; }
170
+ .code-card .str { color: #ce9178; }
171
+ .code-card .num { color: #b5cea8; }
172
+ .code-card .cm { color: #6a9955; font-style: italic; }
173
+ .code-card .tp { color: #4ec9b0; }
174
+
175
+ /* D-04 · test result chip · single line with pass/fail counts + duration */
176
+ .test-chip {
177
+ display: inline-flex; align-items: center; gap: 8px;
178
+ padding: 5px 10px; margin: 4px 0;
179
+ border: 1px solid var(--border-soft); border-radius: 6px;
180
+ font-family: var(--mono); font-size: 11px;
181
+ background: rgba(40,200,64,0.04);
182
+ }
183
+ .test-chip.has-failures { background: rgba(255,95,87,0.05); }
184
+ .test-chip .passed { color: var(--allow); }
185
+ .test-chip .failed { color: var(--block); }
186
+ .test-chip .dur { color: var(--fg-40); }
187
+
188
+ /* D-05 · diagnose+fix loop · groups consecutive test-failure → edit → re-test
189
+ * sequences visually so the reviewer can see "tried, failed, fixed, re-ran" */
190
+ .fix-loop {
191
+ border-left: 2px solid var(--warn); padding-left: 10px;
192
+ margin: 8px 0;
193
+ }
194
+ .fix-loop-label {
195
+ font-family: var(--mono); font-size: 9px; color: var(--warn);
196
+ text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px;
197
+ }
198
+
199
+ /* C-01 · first-run indexing banner · shows once per first-task, then fades */
200
+ .indexing-banner {
201
+ display: none;
202
+ padding: 6px 14px; margin-bottom: 10px;
203
+ background: rgba(74,144,226,0.06); border: 1px solid rgba(74,144,226,0.18);
204
+ border-radius: 6px;
205
+ font-family: var(--mono); font-size: 11px; color: var(--info, #4a90e2);
206
+ }
207
+ .indexing-banner .what { color: var(--fg-70); font-weight: 600; margin-right: 6px; }
208
+
209
+ /* C-02 · tool-call card · matches demo v12 .tc + .tc-out (lines 312-318).
210
+ * Each tool call gets a darker card surface with a bolded tool name, dim
211
+ * target path, right-aligned duration · output block below uses brighter
212
+ * fg so the agent's report is readable rather than disappearing into the
213
+ * card body. */
214
+ .tc-card {
215
+ border: 1px solid var(--border); border-radius: 5px;
216
+ padding: 8px 11px; margin: 7px 0;
217
+ background: hsl(0 0% 9%);
218
+ }
219
+ .tc-card .head {
220
+ display: flex; align-items: baseline; gap: 7px;
221
+ font-family: var(--mono); font-size: 11px; flex-wrap: wrap;
222
+ }
223
+ .tc-card .tool { color: var(--fg); font-weight: 600; }
224
+ .tc-card .target { color: var(--fg-70); }
225
+ .tc-card .hook-badge {
226
+ font-family: var(--mono); font-size: 9px; padding: 1px 5px; border-radius: 3px;
227
+ text-transform: uppercase; letter-spacing: 0.04em;
228
+ }
229
+ .tc-card .hook-badge.allow { color: var(--allow); background: rgba(40,200,64,0.10); }
230
+ .tc-card .hook-badge.clean { color: #8be88b; background: rgba(40,200,64,0.06); }
231
+ .tc-card .hook-badge.block { color: var(--block); background: rgba(255,95,87,0.10); }
232
+ .tc-card .hook-badge.override { color: var(--warn); background: rgba(254,188,46,0.10); }
233
+ .tc-card .dur {
234
+ color: var(--fg-30); margin-left: auto; font-family: var(--mono); font-size: 10px;
235
+ }
236
+ .tc-card .output {
237
+ margin: 5px 0 0; padding: 6px 9px;
238
+ font-family: var(--mono); font-size: 10.5px;
239
+ color: var(--fg-70); line-height: 1.55;
240
+ background: rgba(0,0,0,0.25); border-radius: 4px;
241
+ white-space: pre-wrap; max-height: 120px; overflow: auto;
242
+ }
243
+
244
+ /* E-03 · hook BLOCK affordance · override-with-reason · red-bordered card
245
+ * with 3 action buttons. */
246
+ .hook-block-card {
247
+ border: 1px solid rgba(255,95,87,0.35); border-radius: 8px;
248
+ padding: 12px 14px; margin: 10px 0;
249
+ background: rgba(255,95,87,0.04);
250
+ }
251
+ .hook-block-title { font-family: var(--mono); font-size: 10px; color: var(--block); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
252
+ .hook-block-msg { color: var(--fg-90); font-size: 13px; margin-bottom: 10px; }
253
+ .hook-block-actions { display: flex; gap: 8px; }
254
+ .hook-block-actions button {
255
+ font-family: var(--mono); font-size: 11px;
256
+ padding: 5px 12px; border-radius: 5px; cursor: pointer;
257
+ border: 1px solid var(--border); background: transparent; color: var(--fg-70);
258
+ }
259
+ .hook-block-actions button:hover { color: var(--fg); border-color: var(--fg-30); }
260
+
261
+ /* E-07 · push gate · same hook-block pattern but warn-tinted for git push */
262
+ .push-gate-card {
263
+ border: 1px solid rgba(254,188,46,0.35); border-radius: 8px;
264
+ padding: 12px 14px; margin: 10px 0;
265
+ background: rgba(254,188,46,0.04);
266
+ }
267
+ .push-gate-title { font-family: var(--mono); font-size: 10px; color: var(--warn); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
268
+
269
+ /* P3 · Recovery affordance · session-level failure with explanatory copy
270
+ + user options (retry · diagnose · mark resolved). Demo v12 U10 spec.
271
+ Distinct from hook-block (agent BLOCKED by safety) — recovery covers
272
+ agent CRASHED / errored without a deliberate block. */
273
+ .recovery-card {
274
+ border: 1px solid rgba(255,95,87,0.35); border-radius: 8px;
275
+ padding: 14px 16px; margin: 10px 0;
276
+ background: rgba(255,95,87,0.04);
277
+ border-left: 3px solid var(--block);
278
+ }
279
+ .recovery-title { font-family: var(--mono); font-size: 10px; color: var(--block); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
280
+ .recovery-title .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--block); }
281
+ .recovery-cause { color: var(--fg-90); font-size: 13px; line-height: 1.5; margin-bottom: 6px; }
282
+ .recovery-cause b { color: var(--fg); font-weight: 600; }
283
+ .git-note .git-note-body { color: var(--fg-70); font-size: 12.5px; line-height: 1.55; }
284
+ .git-note .git-note-body code { font-family: var(--mono); font-size: 11.5px; background: var(--bg-2); padding: 1px 5px; border-radius: 3px; }
285
+ .recovery-resolve { color: var(--fg-70); font-size: 12.5px; line-height: 1.55; margin: 6px 0 10px; padding: 8px 11px; background: rgba(255,255,255,0.025); border-radius: 5px; border-left: 2px solid var(--fg-30); }
286
+ .recovery-resolve b { color: var(--fg); font-weight: 500; }
287
+ .recovery-actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
288
+ .recovery-actions button {
289
+ font-family: var(--sans); font-size: 12px; padding: 7px 14px; border-radius: 5px; cursor: pointer;
290
+ border: 1px solid var(--border); background: transparent; color: var(--fg-70); transition: all 160ms ease;
291
+ }
292
+ .recovery-actions button:hover { color: var(--fg); border-color: var(--fg-30); }
293
+ .recovery-actions button.primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 600; }
294
+ .recovery-actions button.primary:hover { box-shadow: 0 0 0 3px rgba(255,154,118,0.20); }
295
+
296
+ /* G-U10 · crash recovery surface · shown when session was orphaned · "the
297
+ * daemon was killed mid-task · you can resume or discard" */
298
+ .crash-recovery {
299
+ border: 1px solid var(--border); border-radius: 8px;
300
+ padding: 10px 14px; margin: 10px 0;
301
+ background: rgba(255,255,255,0.02);
302
+ }
303
+ .crash-recovery-title { font-family: var(--mono); font-size: 10px; color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px; }
304
+
305
+ /* v0.8 S3 · Reshape view · ported from demo v12 lines 73-132 + 2486-2534.
306
+ * Demo v12's W-W4 differentiator · user describes the view they want,
307
+ * agent "composes" it from shared primitives. Backed by body.shape-*
308
+ * classes + a custom-view banner. Three preset shapes (ship-only,
309
+ * velocity, security) plus a free-text input that naive-classifies to
310
+ * the closest preset. */
311
+ .reshape-btn {
312
+ background: linear-gradient(135deg, rgba(255,154,118,0.14), rgba(74,144,226,0.10));
313
+ border: 1px solid rgba(255,154,118,0.4); border-radius: 5px;
314
+ padding: 5px 12px; font-family: var(--mono); font-size: 10.5px;
315
+ color: var(--fg); cursor: pointer; letter-spacing: 0.04em;
316
+ transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1);
317
+ display: inline-flex; align-items: center; gap: 6px; font-weight: 500;
318
+ }
319
+ .reshape-btn:hover {
320
+ background: linear-gradient(135deg, rgba(255,154,118,0.22), rgba(74,144,226,0.16));
321
+ border-color: var(--accent);
322
+ box-shadow: 0 0 0 3px rgba(255,154,118,0.15);
323
+ }
324
+
325
+ /* v0.8 S4 · share-thread button + popover · per demo v12 W-W2 */
326
+ .share-btn-hdr {
327
+ background: transparent; border: 1px solid var(--border); border-radius: 5px;
328
+ padding: 5px 11px; font-family: var(--mono); font-size: 10.5px;
329
+ color: var(--fg-50); cursor: pointer; letter-spacing: 0.04em;
330
+ transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1);
331
+ display: inline-flex; align-items: center; gap: 6px;
332
+ }
333
+ .share-btn-hdr:hover { color: var(--fg); border-color: var(--fg-30); background: rgba(255,255,255,0.03); }
334
+ .share-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(6px); display: none; align-items: center; justify-content: center; z-index: 310; padding: 24px; }
335
+ .share-modal.open { display: flex; animation: rs-fade 240ms ease; }
336
+ .share-modal.open .share-card { animation: wp-card-in 380ms cubic-bezier(0.16, 1, 0.3, 1); }
337
+ /* Share card · gradient border via mask trick (same pattern as
338
+ * reshape-card · same brand alpha 0.45 demo uses for recomposing card
339
+ * lines 89-90). Consistency · both "agent acts on your behalf" modals
340
+ * read as the same family. */
341
+ .share-card { width: 100%; max-width: 540px; background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; padding: 20px 22px; position: relative; overflow: hidden; }
342
+ .share-card::before {
343
+ content: ''; position: absolute; inset: -1px;
344
+ border-radius: inherit; padding: 1px;
345
+ background: linear-gradient(135deg, rgba(255,154,118,0.45), rgba(74,144,226,0.45));
346
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
347
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
348
+ -webkit-mask-composite: xor; mask-composite: exclude;
349
+ pointer-events: none;
350
+ }
351
+ .share-card .head { display: flex; align-items: center; gap: 8px; color: var(--fg); font-weight: 600; font-size: 13px; }
352
+ .share-card .head .tag { color: var(--fg-40); font-weight: 400; font-size: 11px; font-family: var(--mono); }
353
+ .share-card .blurb { color: var(--fg-70); font-size: 12.5px; margin: 10px 0; line-height: 1.55; }
354
+ /* Signed-payload link · brand-tinted so the value reads as "this is the
355
+ * codehere artifact you're about to share" · not a generic URL. The
356
+ * canonical brand var is --accent in product, --brand in demo — same
357
+ * #ff9a76. The original css here referenced --brand (undefined in
358
+ * product) and silently fell back to default fg-light. */
359
+ .share-link-box { background: hsl(0 0% 9%); border: 1px solid var(--border); border-radius: 6px; padding: 9px 12px; font-family: var(--mono); font-size: 11px; color: var(--accent); margin: 10px 0; word-break: break-all; max-height: 96px; overflow: auto; }
360
+ .share-actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
361
+ .share-actions button { font-family: var(--sans); font-size: 12px; padding: 7px 14px; border-radius: 5px; cursor: pointer; }
362
+ .share-btn-primary { background: var(--accent); color: #000; border: 0; font-weight: 600; }
363
+ .share-btn-primary:hover { filter: brightness(1.08); }
364
+ .share-btn-secondary { background: transparent; color: var(--fg-50); border: 1px solid var(--border); }
365
+ .share-btn-secondary:hover { color: var(--fg); border-color: var(--fg-30); }
366
+ .share-foot { color: var(--fg-40); font-size: 11px; margin-top: 12px; font-family: var(--mono); line-height: 1.5; }
367
+ .share-toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--bg-1); border: 1px solid var(--border); color: var(--fg); padding: 10px 18px; border-radius: 6px; font-size: 12px; box-shadow: 0 6px 24px rgba(0,0,0,0.4); z-index: 320; opacity: 0; pointer-events: none; transition: opacity 240ms ease; }
368
+ .share-toast.show { opacity: 1; }
369
+
370
+ /* v0.9 S6 · W-PASS approval modal · demo v12 lines 2030-2050 */
371
+ .wp-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; z-index: 330; padding: 24px; }
372
+ .wp-modal.open { display: flex; animation: rs-fade 280ms ease; }
373
+ .wp-modal.open .wp-card { animation: wp-card-in 380ms cubic-bezier(0.16, 1, 0.3, 1); }
374
+ @keyframes wp-card-in {
375
+ from { opacity: 0; transform: translateY(12px) scale(0.985); }
376
+ to { opacity: 1; transform: translateY(0) scale(1); }
377
+ }
378
+ .wp-card { width: 100%; max-width: 620px; background: var(--bg-1); border: 1px solid rgba(255,154,118,0.40); border-radius: 12px; padding: 22px 24px; position: relative; box-shadow: 0 12px 48px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,154,118,0.06); }
379
+ .wp-banner { display: flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 10.5px; color: var(--accent); letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 4px; }
380
+ .wp-banner .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px rgba(255,154,118,0.18); animation: wp-pulse 1.6s ease-in-out infinite; }
381
+ @keyframes wp-pulse { 0%,100% { box-shadow: 0 0 0 3px rgba(255,154,118,0.18); } 50% { box-shadow: 0 0 0 7px rgba(255,154,118,0.04); } }
382
+ .wp-title { font-size: 16px; font-weight: 600; color: var(--fg); margin: 4px 0 6px; letter-spacing: -0.01em; }
383
+ .wp-reason { font-size: 12.5px; color: var(--fg-70); line-height: 1.5; margin: 0 0 14px; padding: 8px 11px; background: rgba(255,255,255,0.025); border-left: 2px solid var(--accent); border-radius: 0 4px 4px 0; }
384
+ .wp-meta { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); margin: 8px 0; line-height: 1.6; }
385
+ .wp-meta b { color: var(--fg-90); }
386
+ .wp-payload-wrap { background: var(--bg-2); border: 1px solid var(--border); border-radius: 6px; margin: 12px 0; max-height: 240px; overflow: auto; }
387
+ .wp-payload-head { display: flex; align-items: center; gap: 8px; padding: 6px 11px; background: rgba(255,255,255,0.025); border-bottom: 1px solid var(--border); font-family: var(--mono); font-size: 10px; color: var(--fg-50); text-transform: uppercase; letter-spacing: 0.04em; }
388
+ .wp-payload-head .method { color: var(--accent); font-weight: 600; }
389
+ .wp-payload { padding: 10px 12px; font-family: var(--mono); font-size: 11px; color: var(--fg-90); white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
390
+ .wp-payload-edit { width: 100%; min-height: 140px; background: var(--bg-2); border: 1px solid var(--accent); border-radius: 6px; padding: 10px 12px; font-family: var(--mono); font-size: 11px; color: var(--fg-90); resize: vertical; outline: none; }
391
+ .wp-actions { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
392
+ .wp-actions button { font-family: var(--sans); font-size: 12px; padding: 8px 16px; border-radius: 5px; cursor: pointer; font-weight: 500; transition: all 160ms ease; }
393
+ .wp-btn-approve { background: var(--accent); color: #000; border: 0; }
394
+ .wp-btn-approve:hover { box-shadow: 0 0 0 3px rgba(255,154,118,0.20); }
395
+ .wp-btn-edit { background: transparent; color: var(--fg); border: 1px solid var(--border); }
396
+ .wp-btn-edit:hover { border-color: var(--accent); color: var(--accent); }
397
+ .wp-btn-cancel { background: transparent; color: var(--fg-50); border: 1px solid var(--border); }
398
+ .wp-btn-cancel:hover { color: var(--block); border-color: var(--block); }
399
+ .wp-btn-save { background: var(--accent); color: #000; border: 0; }
400
+ .wp-foot { font-family: var(--mono); font-size: 10.5px; color: var(--fg-40); margin-top: 12px; line-height: 1.55; }
401
+ .wp-result { margin-top: 12px; padding: 8px 11px; border-radius: 4px; font-size: 11.5px; line-height: 1.5; }
402
+ .wp-result.ok { background: rgba(40,200,64,0.08); border: 1px solid rgba(40,200,64,0.30); color: var(--allow); }
403
+ .wp-result.fail { background: rgba(254,80,80,0.08); border: 1px solid rgba(254,80,80,0.30); color: var(--block); }
404
+ .wp-result.cancelled { background: rgba(255,255,255,0.04); border: 1px solid var(--border); color: var(--fg-50); }
405
+ .wp-pending-badge { position: fixed; top: 14px; right: 220px; background: var(--accent); color: #000; padding: 4px 10px; border-radius: 5px; font-family: var(--mono); font-size: 10.5px; font-weight: 600; cursor: pointer; display: none; z-index: 305; box-shadow: 0 4px 16px rgba(255,154,118,0.30); animation: wp-pulse 1.6s ease-in-out infinite; letter-spacing: 0.03em; }
406
+
407
+ /* Per-domain mediated-write trust grade chip · v0.9 must-ship */
408
+ .wp-trust-grade { display: inline-block; padding: 2px 7px; border-radius: 3px; font-family: var(--mono); font-size: 10.5px; font-weight: 700; letter-spacing: 0.03em; margin: 0 4px; }
409
+ .wp-trust-grade.A { color: var(--allow); background: rgba(40,200,64,0.12); }
410
+ .wp-trust-grade.B { color: #8be88b; background: rgba(40,200,64,0.08); }
411
+ .wp-trust-grade.C { color: var(--warn); background: rgba(254,188,46,0.10); }
412
+ .wp-trust-grade.D { color: var(--warn); background: rgba(254,188,46,0.15); }
413
+ .wp-trust-grade.F { color: var(--block); background: rgba(255,95,87,0.12); }
414
+ .wp-trust-prompt { margin-top: 10px; padding: 9px 11px; background: rgba(255,95,87,0.06); border: 1px solid rgba(255,95,87,0.30); border-radius: 5px; font-size: 12px; color: var(--fg-90); }
415
+ .wp-trust-prompt input { margin-right: 6px; }
416
+ .wp-pending-badge.show { display: inline-block; }
417
+
418
+ /* v0.9 S7 · overlay-renderer · inspect-and-act for ctx-source URLs */
419
+ .or-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.72); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; z-index: 295; padding: 24px; }
420
+ .or-overlay.open { display: flex; animation: rs-fade 280ms ease; }
421
+ .or-overlay.open .or-card { animation: wp-card-in 380ms cubic-bezier(0.16, 1, 0.3, 1); }
422
+ .or-card { width: 100%; max-width: 720px; max-height: calc(100vh - 48px); background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
423
+ .or-card::before { content: ''; position: absolute; inset: -1px; border-radius: inherit; padding: 1px; background: linear-gradient(135deg, rgba(74,144,226,0.4), rgba(255,154,118,0.4)); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; }
424
+ .or-head { display: flex; align-items: center; gap: 10px; padding: 11px 15px; border-bottom: 1px solid var(--border); font-family: var(--mono); font-size: 11px; color: var(--fg-70); flex-shrink: 0; }
425
+ .or-head .src { color: var(--brand-blue, #4a90e2); font-weight: 600; }
426
+ .or-head .meta { color: var(--fg-50); }
427
+ .or-head .audit { margin-left: auto; padding: 2px 8px; background: rgba(40,200,64,0.06); border: 1px solid rgba(40,200,64,0.25); border-radius: 3px; font-size: 10px; color: var(--fg-50); }
428
+ .or-head .audit b { color: var(--allow); }
429
+ .or-head .or-close { background: transparent; border: 0; color: var(--fg-50); cursor: pointer; padding: 2px 8px; border-radius: 4px; font-size: 16px; line-height: 1; }
430
+ .or-head .or-close:hover { color: var(--fg); background: var(--bg-2); }
431
+ .or-body { padding: 16px 20px; overflow-y: auto; flex: 1; }
432
+ .or-body h3 { font-size: 17px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; margin: 0 0 4px; }
433
+ .or-body .url-row { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); margin-bottom: 12px; word-break: break-all; }
434
+ .or-body .meta-row { display: flex; gap: 6px; flex-wrap: wrap; margin: 6px 0 14px; font-family: var(--mono); font-size: 10.5px; }
435
+ .or-body .meta-row .pill { padding: 2px 7px; border: 1px solid var(--border); border-radius: 3px; color: var(--fg-70); background: var(--bg-2); }
436
+ .or-body .meta-row .pill.state-open { color: var(--allow); border-color: rgba(40,200,64,0.30); background: rgba(40,200,64,0.06); }
437
+ .or-body .meta-row .pill.state-closed { color: var(--block); border-color: rgba(254,80,80,0.30); background: rgba(254,80,80,0.06); }
438
+ .or-body .preview-text { padding: 12px 14px; background: rgba(255,255,255,0.025); border: 1px solid var(--border); border-radius: 6px; font-family: var(--mono); font-size: 11.5px; color: var(--fg-90); line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 360px; overflow-y: auto; }
439
+ .or-body .preview-empty { padding: 18px; text-align: center; color: var(--fg-50); font-size: 12px; }
440
+ .or-body .preview-error { padding: 14px 16px; background: rgba(254,80,80,0.06); border: 1px solid rgba(254,80,80,0.30); border-radius: 6px; color: var(--block); font-size: 12.5px; line-height: 1.5; }
441
+ .or-body .preview-error .hint { color: var(--fg-50); font-family: var(--mono); font-size: 10.5px; margin-top: 6px; }
442
+ .or-body .preview-loading { display: flex; align-items: center; gap: 8px; padding: 16px; color: var(--fg-50); font-family: var(--mono); font-size: 11.5px; }
443
+ .or-body .preview-loading::before { content: ''; width: 10px; height: 10px; border: 2px solid var(--fg-30); border-top-color: var(--accent); border-radius: 50%; animation: or-spin 700ms linear infinite; }
444
+ @keyframes or-spin { to { transform: rotate(360deg); } }
445
+ .or-composer { padding: 12px 14px; background: var(--bg-2); border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; }
446
+ .or-composer .hint { font-family: var(--mono); font-size: 10px; color: var(--fg-50); letter-spacing: 0.06em; }
447
+ .or-composer .row { display: flex; gap: 8px; align-items: center; }
448
+ .or-composer input { flex: 1; background: var(--bg-1); border: 1px solid var(--border); border-radius: 6px; padding: 8px 11px; color: var(--fg); font-family: var(--sans); font-size: 13px; outline: none; transition: border-color 160ms ease; }
449
+ .or-composer input:focus { border-color: var(--accent); }
450
+ .or-composer button { background: var(--accent); color: #000; border: 0; border-radius: 6px; padding: 8px 14px; font-family: var(--sans); font-size: 12.5px; font-weight: 600; cursor: pointer; }
451
+ .or-composer button:hover { box-shadow: 0 0 0 3px rgba(255,154,118,0.18); }
452
+ .or-composer button:disabled { opacity: 0.40; cursor: default; box-shadow: none; }
453
+
454
+ /* Linear inline render · per demo v12 lines 569-600. Authentic Linear
455
+ ticket card displayed inside the overlay when previewing linear://. */
456
+ .linear-render { background: #1c1c1e; color: #ebeaef; padding: 24px 28px; border-radius: 6px; font-family: 'Inter', -apple-system, sans-serif; }
457
+ .linear-id { font-family: var(--mono); font-size: 11px; color: #8a8a8e; letter-spacing: 0.04em; margin-bottom: 8px; }
458
+ .linear-title { font-size: 19px; font-weight: 600; color: #ebeaef; line-height: 1.35; margin-bottom: 14px; letter-spacing: -0.01em; }
459
+ .linear-props { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 16px; }
460
+ .linear-pill { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 4px; font-size: 11.5px; background: rgba(255,255,255,0.04); color: #b8b7bc; }
461
+ .linear-pri-urgent { color: #f87171; }
462
+ .linear-pri-bars { display: inline-flex; align-items: flex-end; gap: 1.5px; width: 12px; height: 12px; }
463
+ .linear-pri-bars div { width: 2px; background: currentColor; border-radius: 0.5px; }
464
+ .linear-pri-bars div:nth-child(1) { height: 4px; }
465
+ .linear-pri-bars div:nth-child(2) { height: 7px; }
466
+ .linear-pri-bars div:nth-child(3) { height: 10px; }
467
+ .linear-status-progress { color: #fbbf24; }
468
+ .linear-status-circle { width: 12px; height: 12px; border-radius: 50%; border: 1.5px solid currentColor; position: relative; box-sizing: border-box; }
469
+ .linear-status-circle::after { content: ''; position: absolute; inset: 0; background: currentColor; border-radius: 50%; clip-path: polygon(0 0, 50% 0, 50% 100%, 0% 100%); }
470
+ .linear-assignee { background: rgba(255,255,255,0.06); }
471
+ .linear-assignee .av { width: 16px; height: 16px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 9px; font-weight: 700; color: #fff; font-family: var(--mono); }
472
+ .linear-label { font-weight: 500; }
473
+ .linear-desc { font-size: 13.5px; color: #b8b7bc; line-height: 1.65; padding: 14px 16px; background: rgba(255,255,255,0.025); border-radius: 6px; margin-bottom: 18px; }
474
+ .linear-desc code { background: rgba(255,255,255,0.07); padding: 1px 5px; border-radius: 3px; font-family: var(--mono); font-size: 12px; color: #ebeaef; }
475
+ .linear-sep { display: flex; align-items: center; gap: 10px; margin: 14px 0 12px; }
476
+ .linear-sep .line { flex: 1; height: 1px; background: rgba(255,255,255,0.08); }
477
+ .linear-sep .lbl { font-family: var(--mono); font-size: 10px; color: #6f6e74; letter-spacing: 0.1em; text-transform: uppercase; }
478
+ .linear-comment { display: flex; gap: 11px; padding: 10px 0; }
479
+ .linear-comment .av { width: 24px; height: 24px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; color: #fff; font-family: var(--mono); flex-shrink: 0; background: #5e6ad2; }
480
+ .linear-comment .col { flex: 1; min-width: 0; }
481
+ .linear-comment .who { display: flex; align-items: baseline; gap: 7px; font-size: 12.5px; font-weight: 600; color: #ebeaef; margin-bottom: 2px; }
482
+ .linear-comment .ts { color: #6f6e74; font-size: 11px; font-weight: 400; }
483
+ .linear-comment .body { font-size: 13px; color: #b8b7bc; line-height: 1.55; }
484
+
485
+ /* Make ctx-source URL chips throughout the UI clickable + discoverable.
486
+ Matches demo v12 lines 690-694. */
487
+ .ctx-url-chip { cursor: pointer; transition: all 180ms ease; padding: 2px 7px 2px 22px; border-radius: 4px; background: rgba(74,144,226,0.08); border: 1px solid rgba(74,144,226,0.30); color: var(--brand-blue, #4a90e2); position: relative; font-family: var(--mono); font-size: 10.5px; display: inline-block; }
488
+ .ctx-url-chip::before { content: '\\1F441'; position: absolute; left: 6px; top: 50%; transform: translateY(-50%); font-size: 10px; opacity: 0.85; }
489
+ .ctx-url-chip:hover { background: rgba(74,144,226,0.18); border-color: rgba(74,144,226,0.55); }
490
+
491
+ .reshape-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(6px); display: none; align-items: center; justify-content: center; z-index: 300; padding: 24px; }
492
+ .reshape-modal.open { display: flex; animation: rs-fade 280ms ease; }
493
+ .reshape-modal.open .reshape-card { animation: wp-card-in 380ms cubic-bezier(0.16, 1, 0.3, 1); }
494
+ @keyframes rs-fade { from { opacity: 0; } to { opacity: 1; } }
495
+ /* Reshape card · matches demo v12 (lines 103-138) · gradient border via
496
+ * mask trick so the card looks like the agent itself composed it (orange
497
+ * for codehere brand, blue for primitives). Without this the modal feels
498
+ * like a generic dialog · with it, the modal reads as the moment the
499
+ * agent gets to work. */
500
+ .reshape-card { width: 100%; max-width: 580px; background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; padding: 22px 24px; position: relative; overflow: hidden; }
501
+ .reshape-card::before {
502
+ content: ''; position: absolute; inset: -1px;
503
+ border-radius: inherit; padding: 1px;
504
+ background: linear-gradient(135deg, rgba(255,154,118,0.5) 0%, rgba(74,144,226,0.5) 100%);
505
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
506
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
507
+ -webkit-mask-composite: xor; mask-composite: exclude;
508
+ pointer-events: none;
509
+ }
510
+ /* Primitives note · blue info box at the modal bottom explaining what
511
+ * "primitives" the agent composes from · demo v12 lines 134-137. */
512
+ .primitives-note {
513
+ margin-top: 14px; padding: 8px 12px;
514
+ background: rgba(74,144,226,0.05); border: 1px solid rgba(74,144,226,0.2);
515
+ border-radius: 5px;
516
+ font-size: 11.5px; color: var(--fg-70); line-height: 1.55;
517
+ }
518
+ .primitives-note b { color: #4a90e2; font-weight: 500; }
519
+ .primitives-note .prims { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); margin-top: 5px; }
520
+ .primitives-note .prims .p {
521
+ display: inline-block; padding: 1px 6px; margin: 2px 3px 0 0;
522
+ background: hsl(0 0% 9%); border: 1px solid var(--border);
523
+ border-radius: 3px; color: var(--fg-70);
524
+ }
525
+ .reshape-card .k { font-family: var(--mono); font-size: 10px; color: var(--accent); letter-spacing: 0.14em; text-transform: uppercase; }
526
+ .reshape-card h2 { font-size: 18px; font-weight: 600; margin: 4px 0 6px; color: var(--fg); letter-spacing: -0.01em; }
527
+ .reshape-card p.sub { color: var(--fg-50); font-size: 12.5px; margin: 0 0 14px; line-height: 1.55; }
528
+ .reshape-input { display: flex; gap: 8px; margin: 8px 0 16px; }
529
+ .reshape-input input { flex: 1; background: var(--bg-2); border: 1px solid var(--border); border-radius: 6px; padding: 9px 12px; color: var(--fg); font-size: 13px; outline: none; }
530
+ .reshape-input input:focus { border-color: var(--accent); }
531
+ .reshape-input button { background: var(--accent); color: #000; border: 0; border-radius: 6px; padding: 0 16px; font-size: 13px; font-weight: 600; cursor: pointer; }
532
+ .reshape-input button:hover { filter: brightness(1.08); }
533
+ .reshape-suggestions { display: flex; flex-direction: column; gap: 5px; margin: 6px 0 14px; }
534
+ .reshape-suggestions .lbl { font-family: var(--mono); font-size: 9.5px; color: var(--fg-40); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 3px; }
535
+ .reshape-suggestions .pill { padding: 6px 11px; background: rgba(255,255,255,0.025); border: 1px solid var(--border); border-radius: 5px; font-size: 12px; color: var(--fg-90); cursor: pointer; text-align: left; }
536
+ .reshape-suggestions .pill:hover { border-color: var(--accent); background: rgba(255,154,118,0.05); }
537
+ .reshape-suggestions .pill .what { color: var(--fg-50); margin-left: 8px; font-size: 11px; }
538
+ .reshape-card .close-x { position: absolute; top: 14px; right: 14px; background: transparent; border: 0; color: var(--fg-50); cursor: pointer; font-size: 18px; padding: 4px 6px; border-radius: 4px; }
539
+ .reshape-card .close-x:hover { color: var(--fg); background: var(--bg-2); }
540
+ .agent-compose { display: none; padding: 12px 14px; background: var(--bg-2); border: 1px solid var(--border); border-radius: 5px; font-family: var(--mono); font-size: 11.5px; color: var(--fg-70); margin-top: 12px; }
541
+ .agent-compose.show { display: block; animation: rs-fade 200ms ease; }
542
+ .agent-compose .agent-line { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
543
+ .agent-compose .line { opacity: 0; animation: rs-slide 240ms ease forwards; }
544
+ @keyframes rs-slide { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } }
545
+ .agent-compose .line:nth-child(1) { animation-delay: 200ms; }
546
+ .agent-compose .line:nth-child(2) { animation-delay: 600ms; }
547
+ .agent-compose .line:nth-child(3) { animation-delay: 1000ms; }
548
+ .agent-compose .line:nth-child(4) { animation-delay: 1400ms; }
549
+ .agent-compose .line:nth-child(5) { animation-delay: 1800ms; }
550
+ .agent-compose .ok { color: var(--allow); }
551
+ .agent-compose .info { color: #4a90e2; }
552
+ .agent-compose .kw { color: var(--accent); }
553
+
554
+ /* Custom-view banner · appears when reshape is active */
555
+ .custom-view-banner {
556
+ display: none;
557
+ padding: 8px 20px;
558
+ background: linear-gradient(90deg, rgba(255,154,118,0.06), rgba(74,144,226,0.04));
559
+ border-bottom: 1px solid rgba(255,154,118,0.2);
560
+ font-family: var(--mono); font-size: 11px; color: var(--fg-70);
561
+ display: none;
562
+ }
563
+ body.custom-view .custom-view-banner { display: flex; align-items: center; gap: 10px; }
564
+ .custom-view-banner .lbl { color: var(--accent); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; font-size: 9.5px; }
565
+ .custom-view-banner .prompt { color: var(--fg-90); font-style: italic; }
566
+ .custom-view-banner .reset { margin-left: auto; color: var(--fg-50); cursor: pointer; padding: 2px 8px; border: 1px solid var(--border); border-radius: 3px; font-size: 10px; }
567
+ .custom-view-banner .reset:hover { color: var(--fg); border-color: var(--fg-30); }
568
+
569
+ /* Failover banner · ported from v1 ui-page.ts on 2026-05-23 as part of /v1 kill.
570
+ Polls /api/failover-offers · when a rate-limited agent has a viable fallback,
571
+ amber strip appears between header and task list with "Switch / Dismiss". */
572
+ .failover-banner {
573
+ margin: 12px 24px 0; padding: 12px 18px;
574
+ border: 1px solid rgba(254,188,46,0.30);
575
+ background: rgba(254,188,46,0.06);
576
+ border-radius: 8px;
577
+ font-size: 13px; line-height: 1.5;
578
+ display: none; align-items: center; gap: 12px; flex-wrap: wrap;
579
+ }
580
+ .failover-banner.show { display: flex; }
581
+ .failover-banner .fb-msg { flex: 1; color: var(--fg-70); min-width: 200px; }
582
+ .failover-banner .fb-msg b { color: var(--fg); font-weight: 600; }
583
+ .failover-banner .fb-actions { display: flex; gap: 8px; flex-shrink: 0; }
584
+ .failover-banner .fb-actions button {
585
+ font-family: var(--mono); font-size: 11px;
586
+ padding: 5px 12px; border-radius: 5px; cursor: pointer;
587
+ border: 1px solid var(--border); background: transparent;
588
+ color: var(--fg-70); transition: color 0.15s, border-color 0.15s;
589
+ }
590
+ .failover-banner .fb-actions button:hover { color: var(--fg); border-color: var(--fg-30); }
591
+ .failover-banner .fb-actions button.fb-accept {
592
+ background: rgba(254,188,46,0.12); border-color: rgba(254,188,46,0.40);
593
+ color: var(--warn); font-weight: 600;
594
+ }
595
+ .failover-banner .fb-actions button.fb-accept:hover { background: rgba(254,188,46,0.20); }
596
+
597
+ /* SHAPE: ship-only · hide tool-call cards, steps blob, hook blocks, etc. */
598
+ body.shape-ship-only .tc-card,
599
+ body.shape-ship-only details.collapsible,
600
+ body.shape-ship-only .hook-block-card,
601
+ body.shape-ship-only .push-gate-card { display: none !important; }
602
+
603
+ /* REPL primitives · thinking dots + streaming caret + repl-beat narrative.
604
+ * Match demo v12 lines 299-309 + 455-457. The thinking pill appears
605
+ * while the agent is mid-reasoning; the streaming caret appends to text
606
+ * being streamed in; the repl-beat is an italic narrative paragraph
607
+ * between tool calls (italic = "agent's voice", not output). All three
608
+ * use the same brand-color blink so live state reads consistently. */
609
+ .thinking {
610
+ display: inline-flex; align-items: center; gap: 5px;
611
+ padding: 5px 10px; margin: 4px 0;
612
+ background: hsl(0 0% 9%); border: 1px solid var(--border);
613
+ border-radius: 12px;
614
+ font-family: var(--mono); font-size: 11px; color: var(--fg-50);
615
+ }
616
+ .thinking .dots { display: inline-flex; gap: 3px; }
617
+ .thinking .dots span {
618
+ width: 4px; height: 4px; border-radius: 50%; background: var(--fg-40);
619
+ animation: cb-blink 1.4s ease infinite;
620
+ }
621
+ .thinking .dots span:nth-child(2) { animation-delay: 0.2s; }
622
+ .thinking .dots span:nth-child(3) { animation-delay: 0.4s; }
623
+ @keyframes cb-blink { 0%, 80%, 100% { opacity: 0.3; } 40% { opacity: 1; } }
624
+ .streaming { display: inline; }
625
+ .streaming::after {
626
+ content: '\\258D'; color: var(--accent); margin-left: 2px;
627
+ animation: cb-cursor 1s steps(2) infinite;
628
+ }
629
+ @keyframes cb-cursor { 50% { opacity: 0; } }
630
+ .repl-beat {
631
+ padding: 7px 11px; margin: 8px 0;
632
+ background: rgba(0,0,0,0.18);
633
+ border-left: 2px solid var(--border); border-radius: 0 4px 4px 0;
634
+ font-style: italic; color: var(--fg-70); font-size: 12.5px;
635
+ line-height: 1.55;
636
+ }
637
+ .repl-beat .cursor {
638
+ display: inline-block; width: 7px; height: 12px;
639
+ background: var(--accent); margin-left: 3px; vertical-align: text-bottom;
640
+ animation: cb-cursor 1s steps(2) infinite;
641
+ }
642
+
643
+ /* Audit-line · narrative beat between tool cards · matches demo v12
644
+ * lines 405-407. Dim mono row with a #NNNN seq prefix. When the chain
645
+ * records a notable event (pre-prompt scan, override, budget warning,
646
+ * W-PASS approval) the audit chain becomes VISIBLE in the timeline ·
647
+ * not buried in the footer. The line is the proof; the footer is the
648
+ * summary. */
649
+ .audit-line {
650
+ font-family: var(--mono); font-size: 10.5px; color: var(--fg-40);
651
+ padding: 6px 12px; margin: 8px 0;
652
+ background: rgba(0,0,0,0.2);
653
+ border-left: 2px solid var(--border); border-radius: 3px;
654
+ line-height: 1.55;
655
+ }
656
+ .audit-line .seq { color: var(--fg-30); margin-right: 8px; }
657
+ .audit-line.warn {
658
+ color: var(--warn); border-left-color: var(--warn);
659
+ background: rgba(254,188,46,0.03);
660
+ }
661
+ .audit-line.allow { border-left-color: rgba(40,200,64,0.4); }
662
+ .audit-line b { color: var(--fg-90); font-weight: 600; }
663
+ .audit-line code { background: hsl(0 0% 9%); padding: 1px 5px; border-radius: 3px; color: var(--fg-70); }
664
+
665
+ /* auditor persona · audit-chain + verifier + hook-badges amplified,
666
+ summary muted (per demo v12 line 2462: "amplify audit-lines +
667
+ hook-badges + trust-grades") */
668
+ body.auditor .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; color: var(--fg-70); font-size: 11.5px; padding: 8px 14px; }
669
+ body.auditor .verifier-panel { box-shadow: 0 0 0 2px rgba(40,200,64,0.18); }
670
+ body.auditor .hook-badge { transform: scale(1.05); }
671
+ body.auditor .file-chip .trust { font-weight: 700; padding: 2px 7px; }
672
+ body.auditor .panel:has(p) { opacity: 0.7; }
673
+
674
+ /* SHAPE: velocity · hide tool-call cards, summary prose, tokens, verifier · keep outcome + files */
675
+ body.shape-velocity .tc-card,
676
+ body.shape-velocity .token-table,
677
+ body.shape-velocity details.collapsible { display: none !important; }
678
+ body.shape-velocity .panel:has(.verifier-panel) { display: none; }
679
+
680
+ /* SHAPE: security · amplify verifier + hook blocks; hide summary prose */
681
+ body.shape-security .panel:has(.token-table),
682
+ body.shape-security details.collapsible { display: none !important; }
683
+ body.shape-security .verifier-panel { box-shadow: 0 0 0 2px rgba(255,95,87,0.18); padding: 14px; }
684
+ body.shape-security .hook-block-card { box-shadow: 0 0 0 2px rgba(255,95,87,0.3); }
685
+
686
+ /* v0.8 S2.5 · Settings drawer · ported from demo v12 lines 178-192 + A-09
687
+ * in the backlog. Right-side off-canvas panel · holds Context Sources +
688
+ * future section-visibility toggles. */
689
+ .drawer { position: fixed; top: 0; right: 0; width: 360px; height: 100vh; background: var(--bg-1); border-left: 1px solid var(--border); transform: translateX(100%); transition: transform 360ms cubic-bezier(0.16, 1, 0.3, 1); z-index: 200; padding: 20px 18px; overflow-y: auto; }
690
+ .drawer.open { transform: translateX(0); }
691
+ .drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); opacity: 0; pointer-events: none; transition: opacity 300ms cubic-bezier(0.16, 1, 0.3, 1); z-index: 199; }
692
+ .drawer-overlay.open { opacity: 1; pointer-events: auto; }
693
+ .drawer h3 { font-family: var(--mono); font-size: 11px; color: var(--fg-50); letter-spacing: 0.12em; text-transform: uppercase; margin: 16px 0 8px; }
694
+ .drawer h3:first-child { margin-top: 0; }
695
+ .drawer p.sub { font-size: 11px; color: var(--fg-50); margin: 2px 0 12px; }
696
+ .drawer-close { position: absolute; top: 14px; right: 14px; background: transparent; border: 0; color: var(--fg-50); cursor: pointer; font-size: 18px; line-height: 1; padding: 4px 6px; border-radius: 4px; }
697
+ .drawer-close:hover { color: var(--fg); background: var(--bg-2); }
698
+ .drawer-title { font-size: 15px; font-weight: 600; color: var(--fg); margin-bottom: 12px; padding-right: 30px; }
699
+
700
+ /* Settings-button (cog) in the header · opens the drawer */
701
+ .settings-btn { background: transparent; border: 1px solid var(--border); color: var(--fg-50); cursor: pointer; padding: 4px 8px; border-radius: 6px; font-size: 12px; }
702
+ .settings-btn:hover { color: var(--fg); border-color: var(--fg-30); }
703
+
704
+ /* Context-source rows inside the drawer */
705
+ .cs-row { display: flex; align-items: flex-start; gap: 10px; padding: 10px 0; font-size: 12px; border-bottom: 1px solid var(--border-soft); }
706
+ .cs-row .cs-icon { margin-top: 1px; }
707
+ .cs-row:last-child { border-bottom: none; }
708
+ .cs-row .cs-icon { width: 24px; height: 24px; border-radius: 5px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; font-family: var(--mono); }
709
+ .cs-row .cs-icon.linear { background: #5e6ad2; color: #fff; }
710
+ .cs-row .cs-icon.figma { background: #1e1e1e; color: #fff; border: 1px solid rgba(255,255,255,0.2); }
711
+ .cs-row .cs-label { font-weight: 500; color: var(--fg-90); }
712
+ .cs-row .cs-meta { font-size: 10px; color: var(--fg-50); font-family: var(--mono); }
713
+ .cs-row .cs-status { font-family: var(--mono); font-size: 9px; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.04em; }
714
+ .cs-row .cs-status.connected { color: var(--allow); background: rgba(40,200,64,0.10); }
715
+ .cs-row .cs-status.expired { color: var(--warn); background: rgba(254,188,46,0.10); }
716
+ .cs-row .cs-status.manual { color: var(--fg-50); background: rgba(255,255,255,0.04); }
717
+ .cs-row .cs-status.disconnected { color: var(--fg-40); background: rgba(255,255,255,0.02); }
718
+ .cs-row .cs-status.available { color: var(--accent); background: rgba(255,154,118,0.10); }
719
+ .cs-row .cs-actions { margin-left: auto; display: flex; gap: 6px; }
720
+ .cs-btn { font-family: var(--mono); font-size: 11px; padding: 3px 9px; border-radius: 4px; cursor: pointer; background: transparent; color: var(--fg-70); border: 1px solid var(--border); }
721
+ .cs-btn:hover { color: var(--fg); border-color: var(--fg-30); }
722
+ .cs-btn.primary { color: var(--accent); border-color: rgba(255,154,118,0.4); }
723
+ .cs-btn.primary:hover { background: rgba(255,154,118,0.08); }
724
+ .cs-btn.danger:hover { color: var(--block); border-color: var(--block); }
725
+ .cs-btn:disabled { opacity: 0.35; cursor: not-allowed; }
726
+ .cs-empty-hint { font-size: 11px; color: var(--fg-50); padding: 8px 10px; background: rgba(255,255,255,0.02); border-radius: 4px; margin-top: 4px; }
727
+ .cs-test-result { font-family: var(--mono); font-size: 10px; padding: 4px 8px; margin-top: 4px; border-radius: 3px; }
728
+ .cs-test-result.ok { color: var(--allow); background: rgba(40,200,64,0.06); }
729
+ .cs-test-result.fail { color: var(--block); background: rgba(255,95,87,0.06); }
730
+
731
+ /* Demo v12 line 322 + 518-521 · context-source rendering on tool-call cards */
732
+ .tc-card.ctx-source { border-left: 2px solid #4a90e2; }
733
+ .tc-card .hook-badge.ctx { color: #4a90e2; background: rgba(74,144,226,0.08); border: 1px solid rgba(74,144,226,0.3); }
734
+
735
+ /* P10 · per-step hook badges in the trace details (same styles, different parent). */
736
+ .tc .hook-badge { font-family: var(--mono); font-size: 9px; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; margin-right: 6px; vertical-align: middle; }
737
+ .tc .hook-badge.allow { color: var(--allow); background: rgba(40,200,64,0.10); }
738
+ .tc .hook-badge.block { color: var(--block); background: rgba(255,95,87,0.10); }
739
+ .tc .hook-badge.override { color: var(--warn); background: rgba(254,188,46,0.10); }
740
+ .tc .hook-badge.ctx { color: #4a90e2; background: rgba(74,144,226,0.08); border: 1px solid rgba(74,144,226,0.3); }
741
+ .ctx-source-banner { padding: 8px 12px; background: rgba(74,144,226,0.04); border: 1px solid rgba(74,144,226,0.25); border-left: 3px solid #4a90e2; border-radius: 5px; margin: 8px 0; font-size: 11.5px; color: var(--fg-70); }
742
+ .ctx-source-banner b { color: #4a90e2; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; margin-right: 6px; }
743
+
744
+ /* F-06 · interactive audit chain panel · dashboard footer · click "Verify"
745
+ * to call /api/audit/verify and render the verdict in plain English. The
746
+ * chain is the source of truth, NOT codehere itself · the button proves
747
+ * the daemon isn't lying to you. */
748
+ /* RADICAL bottom-chrome collapse (2026-05-24 · Ega flagged "audit
749
+ * section consuming space"). Pre-fix: audit-chain panel ate ~90px ·
750
+ * OSS footer ate ~25px · total bottom chrome ~115px on every screen.
751
+ * Post-fix: panel collapsed to inline + co-lives with OSS footer in
752
+ * the same 28px status-bar line. Audit info stays load-bearing
753
+ * (dots + stats + Verify button preserved · only the SHA-256
754
+ * subtitle and the panel-card chrome removed). Net ~85px reclaimed.
755
+ * No info-loss · just spatial economy. */
756
+ .audit-chain-panel {
757
+ display: inline-flex; align-items: center; gap: 12px;
758
+ padding: 0; margin: 0; background: none; border: none;
759
+ }
760
+ .audit-chain-head { display: inline-flex; align-items: center; gap: 10px; }
761
+ .audit-chain-title { font-family: var(--mono); font-size: 10px; color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.06em; }
762
+ /* audit-chain stats line + activity dots · denser footer per demo v12 */
763
+ .audit-chain-dots .dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
764
+ .audit-chain-dots .dot.allow { background: var(--allow); }
765
+ .audit-chain-dots .dot.block { background: var(--block); }
766
+ .audit-chain-dots .dot.override { background: var(--warn); }
767
+ .audit-chain-dots .dot.muted { background: var(--fg-30); }
768
+ .audit-chain-btn {
769
+ font-family: var(--mono); font-size: 11px;
770
+ padding: 4px 12px; border-radius: 5px; cursor: pointer;
771
+ background: rgba(255,154,118,0.08); color: var(--accent);
772
+ border: 1px solid rgba(255,154,118,0.40); font-weight: 600;
773
+ transition: all 160ms ease;
774
+ }
775
+ .audit-chain-btn:hover { background: rgba(255,154,118,0.20); box-shadow: 0 0 0 3px rgba(255,154,118,0.10); }
776
+ .audit-chain-btn.verified { color: var(--allow); border-color: rgba(40,200,64,0.3); }
777
+ .audit-chain-btn.broken { color: var(--block); border-color: rgba(255,95,87,0.3); }
778
+ /* audit-chain-result starts collapsed (the subtitle "SHA-256
779
+ * hash-chain · run verify…" ate 25px on every screen). Only renders
780
+ * when verifyAuditChain() runs and writes a verdict · gets shown
781
+ * inline temporarily after click then fades back. */
782
+ .audit-chain-result { display: none; margin-top: 0; font-family: var(--mono); font-size: 11px; }
783
+ .audit-chain-result.show { display: inline-block; margin-left: 12px; }
784
+ .audit-chain-result .ok { color: var(--allow); }
785
+ .audit-chain-result .fail { color: var(--block); }
786
+ .audit-chain-counts { color: var(--fg-50); margin-top: 4px; }
787
+ .audit-chain-error { color: var(--block); margin-top: 4px; padding-left: 12px; }
788
+
789
+ /* F-04 · time-gap separator · slides between two consecutive items in the
790
+ * task list when their times differ by more than 2 hours. Says "Nh later"
791
+ * with an optional "· daemon restart" tail when there's a known restart in
792
+ * the gap. Helps the reviewer parse 17h+ histories without scrolling
793
+ * forever. */
794
+ .time-gap {
795
+ display: flex; align-items: center; gap: 16px;
796
+ padding: 14px 0; margin: 16px 0;
797
+ font-family: var(--mono); font-size: 11px;
798
+ color: var(--accent); text-transform: uppercase; letter-spacing: 0.18em;
799
+ }
800
+ .time-gap .line {
801
+ flex: 1; height: 1px;
802
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
803
+ opacity: 0.45;
804
+ }
805
+ .time-gap .label {
806
+ padding: 7px 16px; border: 1px solid rgba(255,154,118,0.3);
807
+ border-radius: 100px; background: rgba(255,154,118,0.04);
808
+ text-align: center; line-height: 1.3;
26
809
  }
27
- .logo { font-family: var(--mono); font-size: 14px; font-weight: 600; color: var(--fg); }
28
- .budget { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--fg-50); }
29
- .budget.safe { color: var(--fg-50); }
30
- .budget.warn { color: var(--warn); }
31
- .budget.block { color: var(--block); font-weight: 600; }
810
+ .time-gap .label b { color: var(--fg); font-weight: 600; }
811
+ .time-gap .label .sub { display: block; margin-top: 3px; font-size: 9px; color: var(--fg-50); letter-spacing: 0.12em; }
812
+
813
+ /* F-08 · token table · per-agent breakdown of inputs/outputs/cache/cost in
814
+ * task detail · only renders when artifacts.usage has tokens. Mono numbers
815
+ * column-aligned, total row bold-line-above. */
816
+ .token-table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 11px; }
817
+ .token-table th { text-align: right; color: var(--fg-40); font-size: 9px; text-transform: uppercase; padding: 4px 8px; font-weight: 500; }
818
+ .token-table th:first-child { text-align: left; }
819
+ .token-table td { padding: 4px 8px; text-align: right; color: var(--fg-90); }
820
+ .token-table td:first-child { text-align: left; color: var(--fg-70); }
821
+ .token-table tr.total td { border-top: 1px solid var(--border); color: var(--fg); font-weight: 600; padding-top: 6px; }
822
+ .token-table td.dim { color: var(--fg-40); }
823
+
824
+ /* F-02 · cross-worktree alert · pulsing dot + label in the squad-row when
825
+ * any session in a different cwd has been running > 5 minutes. Click to
826
+ * jump to that detail. */
827
+ .worktree-alert {
828
+ display: none;
829
+ font-family: var(--mono); font-size: 10px; color: var(--warn);
830
+ background: rgba(254,188,46,0.06); border: 1px solid rgba(254,188,46,0.18);
831
+ padding: 2px 8px; border-radius: 4px; cursor: pointer;
832
+ margin-left: 8px;
833
+ }
834
+ .worktree-alert:hover { background: rgba(254,188,46,0.12); }
835
+ .worktree-alert .pulse-dot {
836
+ display: inline-block; width: 6px; height: 6px; border-radius: 50%;
837
+ background: var(--warn); margin-right: 6px; animation: pulse 1.6s ease infinite;
838
+ }
839
+
840
+ /* F-01 · budget threshold warning · drops down under the header when daily
841
+ * spend crosses 80% of the cap. Color tracks safe/warn/block. The audit-
842
+ * line copy is intentionally plain English: who, how much, what to do. */
843
+ .budget-banner {
844
+ display: none;
845
+ padding: 8px 20px; border-bottom: 1px solid var(--border-soft);
846
+ font-family: var(--mono); font-size: 11px;
847
+ background: rgba(254,188,46,0.04); color: var(--warn);
848
+ }
849
+ .budget-banner.block {
850
+ background: rgba(255,95,87,0.06); color: var(--block);
851
+ }
852
+ .budget-banner .what { color: var(--fg-90); margin-right: 8px; font-weight: 600; }
853
+ .budget-banner .hint { color: var(--fg-50); margin-left: 8px; }
32
854
  .v1-link {
33
855
  font-family: var(--mono); font-size: 11px; color: var(--fg-40);
34
856
  text-decoration: none; padding: 4px 10px; border: 1px solid var(--border);
@@ -36,6 +858,28 @@ const SHARED_STYLES = `
36
858
  }
37
859
  .v1-link:hover { color: var(--fg); border-color: var(--fg-30); }
38
860
 
861
+ /* A-10 · OSS · MIT footer · proof-of-real strip at the very bottom of the
862
+ * dashboard. The marketing line is "codehere is OSS, audit it yourself" ·
863
+ * if the running daemon doesn't say so somewhere visible, the line is
864
+ * empty marketing. Test count + version come from the server. */
865
+ /* OSS footer · proof-of-real strip · matches demo .proof-footer (lines
866
+ * 497-505). Darker bg so it reads as ground floor. Codehere wordmark gets
867
+ * the brand tint · the only colored item · everything else dim. Hover on
868
+ * any link goes to full fg, not 70 · the click target deserves full
869
+ * contrast when the cursor's actually there. */
870
+ .oss-footer {
871
+ flex-shrink: 0; padding: 9px 20px;
872
+ border-top: 1px solid var(--border-soft);
873
+ background: rgba(0,0,0,0.35);
874
+ font-family: var(--mono); font-size: 10.5px; color: var(--fg-40);
875
+ display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
876
+ }
877
+ .oss-footer .brand { color: var(--accent); }
878
+ .oss-footer .right { margin-left: auto; }
879
+ .oss-footer a { color: var(--fg-50); text-decoration: none; transition: color 180ms cubic-bezier(0.16,1,0.3,1); }
880
+ .oss-footer a:hover { color: var(--fg); }
881
+ .oss-footer .sep { color: var(--fg-30); }
882
+
39
883
  .squad-row {
40
884
  display: flex; align-items: center; gap: 12px;
41
885
  padding: 6px 20px; border-bottom: 1px solid var(--border-soft);
@@ -46,6 +890,10 @@ const SHARED_STYLES = `
46
890
  .squad-dot.ready { background: var(--allow); }
47
891
  .squad-dot.busy { background: var(--warn); }
48
892
  .squad-dot.offline { background: var(--fg-30); }
893
+ /* setup = installed/known but can't run yet · hollow ring, not a filled dot,
894
+ so it reads "not ready" at a glance and never like an active green/amber. */
895
+ .squad-dot.setup { background: transparent; border: 1.5px solid var(--fg-40); box-sizing: border-box; }
896
+ .squad-setup { font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--fg-40); border: 1px solid var(--fg-20); border-radius: 3px; padding: 0 4px; margin-left: 2px; }
49
897
 
50
898
  .bar {
51
899
  display: flex; align-items: center; gap: 8px;
@@ -73,15 +921,55 @@ const SHARED_STYLES = `
73
921
  .section-label {
74
922
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
75
923
  color: var(--fg-40); text-transform: uppercase; margin: 16px 0 8px;
924
+ display: flex; align-items: center; gap: 8px;
76
925
  }
77
926
  .section-label:first-child { margin-top: 0; }
927
+ /* Featured section gets the brand tint · pinned demo + recent-work
928
+ * sections deserve the visual signal that says "this is what to look
929
+ * at first". Sub-dot precedes the label so the eye lands there before
930
+ * reading the text. */
931
+ .section-label.featured { color: var(--accent); }
932
+ .section-label.featured::before {
933
+ content: ''; display: inline-block;
934
+ width: 5px; height: 5px; border-radius: 50%;
935
+ background: var(--accent);
936
+ box-shadow: 0 0 0 3px rgba(255,154,118,0.18);
937
+ }
78
938
 
939
+ /* Task card · glass surface · matches demo v12 .glass-card pattern.
940
+ * Faint white-on-dark fill + 1px low-contrast border · sits on top of
941
+ * the ambient dot-grid. Hover lifts 2px with a bigger shadow + inner
942
+ * ring · the card visibly comes forward when the cursor's on it. */
79
943
  .task-card {
80
- background: rgba(255,255,255,0.02); border: 1px solid var(--border);
81
- border-radius: 8px; padding: 12px 14px; margin-bottom: 6px;
82
- cursor: pointer; transition: border-color 0.15s, background 0.15s;
944
+ background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);
945
+ border-left: 2px solid rgba(255,255,255,0.06);
946
+ border-radius: 8px; padding: 13px 15px; margin-bottom: 8px;
947
+ cursor: pointer;
948
+ transition:
949
+ transform 300ms cubic-bezier(0.16, 1, 0.3, 1),
950
+ border-color 300ms cubic-bezier(0.16, 1, 0.3, 1),
951
+ background 300ms cubic-bezier(0.16, 1, 0.3, 1),
952
+ box-shadow 300ms cubic-bezier(0.16, 1, 0.3, 1);
953
+ }
954
+ .task-card:hover {
955
+ border-color: rgba(255,255,255,0.12); background: rgba(255,255,255,0.05);
956
+ transform: translateY(-2px);
957
+ box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.05);
83
958
  }
84
- .task-card:hover { border-color: var(--fg-30); background: rgba(255,255,255,0.04); }
959
+ .task-card.tc-pinned { border-left-color: var(--accent); background: linear-gradient(90deg, rgba(255,154,118,0.04), rgba(255,255,255,0.02) 30%); }
960
+ .task-card.agent-claude-code { border-left-color: #76b7ff; }
961
+ .task-card.agent-codex { border-left-color: #28c840; }
962
+ .task-card.agent-aider { border-left-color: #ff9a76; }
963
+ .task-card.agent-opencode { border-left-color: #c4b5fd; }
964
+ .task-card.agent-gemini { border-left-color: #febc2e; }
965
+ .task-card.agent-conductor { border-left-color: var(--fg-30); }
966
+ /* Outcome chip on the right of card-row2 · ✓ / ⚠ / ↻ */
967
+ .card-outcome { margin-left: auto; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em; padding: 2px 8px; border-radius: 3px; }
968
+ .card-outcome.ok { color: var(--allow); background: rgba(40,200,64,0.10); }
969
+ .card-outcome.warn { color: var(--warn); background: rgba(254,188,46,0.10); }
970
+ .card-outcome.fail { color: var(--block); background: rgba(255,95,87,0.10); }
971
+ .card-outcome.queued { color: var(--fg-50); background: rgba(255,255,255,0.03); }
972
+ .card-stalled { margin-top: 6px; font-size: 11px; line-height: 1.4; color: var(--block); background: rgba(255,95,87,0.07); border-left: 2px solid var(--block); padding: 5px 8px; border-radius: 3px; }
85
973
  .card-row1 { display: flex; align-items: center; gap: 8px; }
86
974
  .status-icon { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
87
975
  .status-icon.running { background: var(--accent); animation: pulse 1.5s ease infinite; }
@@ -95,7 +983,23 @@ const SHARED_STYLES = `
95
983
  @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
96
984
 
97
985
  .card-title { font-weight: 600; color: var(--fg); font-size: 13px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
98
- .card-agent { font-family: var(--mono); font-size: 10px; color: var(--fg-50); background: rgba(255,255,255,0.04); padding: 2px 6px; border-radius: 4px; }
986
+ .card-agent { font-family: var(--mono); font-size: 10px; color: var(--fg-50); background: rgba(255,255,255,0.04); padding: 2px 6px; border-radius: 4px; display: inline-flex; align-items: center; gap: 5px; }
987
+ /* Agent identity badge (squircle with initials) — used in cards, squad row, detail header */
988
+ .ab { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 5px; font-family: var(--mono); font-size: 9px; font-weight: 700; flex-shrink: 0; letter-spacing: -0.04em; }
989
+ .ab.claude { background: var(--agent-claude); color: #000; }
990
+ .ab.codex { background: var(--agent-codex); color: #fff; }
991
+ .ab.aider { background: var(--agent-aider); color: #fff; }
992
+ .ab.gemini { background: var(--agent-gemini); color: #000; }
993
+ .ab.opencode { background: var(--agent-opencode); color: #000; }
994
+ .ab.conductor { background: var(--accent); color: #000; }
995
+ .ab.auto { background: var(--fg-30); color: #000; }
996
+ .ab.user { background: #5a5a62; color: #fff; }
997
+ .ab.lg { width: 22px; height: 22px; font-size: 10px; border-radius: 6px; }
998
+ .ab.sm { width: 14px; height: 14px; font-size: 8px; border-radius: 3px; }
999
+ /* Cost signal pill — primitive #2 */
1000
+ .cost-signal { display: inline-flex; align-items: center; gap: 4px; font-family: var(--mono); font-size: 9px; color: var(--fg-40); padding: 1px 6px; border: 1px solid var(--border); border-radius: 3px; }
1001
+ .cost-signal.free { color: var(--allow); border-color: rgba(40,200,64,0.25); background: rgba(40,200,64,0.04); }
1002
+ .cost-signal.paid { color: var(--fg-50); }
99
1003
  .card-row2 { display: flex; align-items: center; gap: 10px; margin-top: 6px; font-family: var(--mono); font-size: 10px; color: var(--fg-40); }
100
1004
  .card-row3 { margin-top: 6px; display: flex; gap: 6px; }
101
1005
  .card-btn {
@@ -124,24 +1028,289 @@ const SHARED_STYLES = `
124
1028
  }
125
1029
  @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
126
1030
 
1031
+ /* Detail header · matches demo v12 .phase-head pattern · uppercase brand
1032
+ * kicker label · 22px shimmering gradient title · meta line beneath.
1033
+ * The shimmer is the same 6-stop gradient as demo h1, scaled down for
1034
+ * inside-a-page use (vs full-phase headline). */
127
1035
  .detail-header {
128
- display: flex; align-items: center; gap: 10px; margin-bottom: 12px;
1036
+ display: flex; align-items: flex-start; gap: 12px; margin-bottom: 6px;
129
1037
  }
130
1038
  .back-btn {
131
1039
  font-family: var(--mono); font-size: 12px; color: var(--accent);
132
1040
  cursor: pointer; padding: 4px 8px; border-radius: 4px;
133
1041
  border: 1px solid rgba(255,154,118,0.2);
1042
+ flex-shrink: 0; margin-top: 14px;
134
1043
  }
135
1044
  .back-btn:hover { background: var(--accent-wash); }
136
- .detail-title { font-weight: 600; font-size: 15px; flex: 1; }
1045
+ .detail-title-wrap { flex: 1; min-width: 0; }
1046
+ .detail-kicker {
1047
+ font-family: var(--mono); font-size: 10.5px; color: var(--accent);
1048
+ letter-spacing: 0.18em; text-transform: uppercase; line-height: 1;
1049
+ margin-bottom: 4px;
1050
+ }
1051
+ .detail-title {
1052
+ font-weight: 600; font-size: 22px; line-height: 1.25;
1053
+ letter-spacing: -0.015em;
1054
+ background: linear-gradient(90deg, #ff9a76 0%, #f5c6b0 22%, #ff9a76 38%, #4a90e2 55%, #8ab8ee 70%, #4a90e2 85%, #ff9a76 100%);
1055
+ background-size: 200% auto;
1056
+ -webkit-background-clip: text; background-clip: text;
1057
+ -webkit-text-fill-color: transparent; color: transparent;
1058
+ animation: titleShimmer 6s linear infinite;
1059
+ }
1060
+ @keyframes titleShimmer {
1061
+ 0% { background-position: 0% center; }
1062
+ 100% { background-position: 200% center; }
1063
+ }
137
1064
  .detail-meta { font-family: var(--mono); font-size: 11px; color: var(--fg-50); margin-bottom: 16px; }
1065
+ .detail-meta .phase-pos { color: var(--accent); }
1066
+ /* Artifacts drawer (task #51) · header trigger + slide-in side panel */
1067
+ .art-trig { color: #4a90e2; cursor: pointer; }
1068
+ .art-trig:hover { text-decoration: underline; }
1069
+ .art-scrim { position: fixed; inset: 0; background: rgba(0,0,0,.62); opacity: 0; pointer-events: none; transition: opacity .18s; z-index: 90; }
1070
+ .art-scrim.open { opacity: 1; pointer-events: auto; }
1071
+ /* Centered preview popup · scales up on large screens so diagrams/tables get
1072
+ room (vs a cramped right drawer). */
1073
+ .art-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-48%) scale(.98); opacity: 0; pointer-events: none;
1074
+ width: min(880px, 92vw); max-height: 86vh; background: #16171d; border: 1px solid var(--border); border-radius: 12px;
1075
+ box-shadow: 0 24px 80px rgba(0,0,0,.6); transition: opacity .18s ease, transform .18s ease; z-index: 100; display: flex; flex-direction: column; }
1076
+ .art-modal.open { opacity: 1; pointer-events: auto; transform: translate(-50%,-50%) scale(1); }
1077
+ .art-modal-h { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; font-family: var(--mono); font-size: 12px; font-weight: 600; color: var(--fg-80); }
1078
+ .art-modal-h .x { margin-left: auto; cursor: pointer; color: var(--fg-50); font-size: 16px; }
1079
+ .art-modal-h .x:hover { color: var(--fg-80); }
1080
+ .art-modal-body { overflow: auto; padding: 16px 18px; }
1081
+ .art-modal-body .panel { border: none; padding: 0; margin: 0; background: transparent; }
1082
+ .art-modal-body .panel-title { display: none; }
1083
+ /* Legible canvas: each artifact lifts onto an elevated surface so it's not
1084
+ lost on near-black, and diagram/chart strokes + table text get contrast. */
1085
+ .art-modal .artifact-card, .art-modal .art-mock, .art-modal .art-pick { background: #1c1e26; border: 1px solid #2c2f3a; border-radius: 10px; padding: 14px; margin-bottom: 14px; }
1086
+ .art-modal .art-pick { border-left: 3px solid var(--accent); }
1087
+ .art-modal .artifact-h { color: var(--fg-70); font-size: 11px; margin-bottom: 10px; }
1088
+ .art-modal .art-seq { background: #21242e; border: 1px solid #333744; border-radius: 8px; padding: 12px; }
1089
+ .art-modal .art-cmp .hd { background: #2a2d38; color: var(--fg-80); }
1090
+ .art-modal .art-cmp .rl { background: #23262f; color: var(--fg-70); }
1091
+ .art-modal .art-cmp .cell { background: #1a1c23; color: #d2d4de; }
1092
+ .art-modal .art-mock-body { color: #cdd0da; }
1093
+ /* brighter diagram strokes/text so the graph actually reads */
1094
+ .art-modal .seq-svg .ll { stroke: #4a4d5a; }
1095
+ .art-modal .seq-svg .box { fill: #2a2e3a; stroke: #444858; }
1096
+ .art-modal .seq-svg .msg { stroke: #c2c4d0; }
1097
+ .art-modal .seq-svg .msg.ret { stroke: #8a8d9c; }
1098
+ .art-modal .seq-svg .mt { fill: #d2d4de; }
1099
+ .art-modal .seq-svg .ah { fill: #c2c4d0; }
1100
+ .art-modal .seq-svg .ah-ret { fill: #8a8d9c; }
1101
+ @media (max-width: 560px){ .art-modal { width: 94vw; max-height: 90vh; } }
138
1102
  .detail-actions { display: flex; gap: 6px; }
1103
+ /* Chapter divider · phase transition in the thread narrative (demo v12) */
1104
+ .chapter-divider { display: flex; align-items: center; gap: 10px; margin: 14px 0 10px; font-family: var(--mono); font-size: 10.5px; }
1105
+ .chapter-divider .ch-prev { color: var(--allow); white-space: nowrap; }
1106
+ .chapter-divider .ch-prev:empty { display: none; }
1107
+ .chapter-divider .ch-line { flex: 1; height: 1px; background: var(--border); }
1108
+ .chapter-divider .ch-next { color: var(--fg-50); white-space: nowrap; text-transform: uppercase; letter-spacing: 0.4px; }
1109
+ .chapter-divider .ch-next b { color: var(--accent); }
1110
+ /* Persona selector · "show only what matters to your role" */
1111
+ .persona-select { background: var(--bg-2,#1a1a1f); color: var(--fg-70); border: 1px solid var(--border); border-radius: 6px; font-size: 11px; padding: 4px 8px; font-family: inherit; cursor: pointer; }
1112
+ .persona-select:hover { border-color: var(--fg-30); }
1113
+ .density-toggle { background: var(--bg-2,#1a1a1f); color: var(--fg-70); border: 1px solid var(--border); border-radius: 6px; font-size: 11px; padding: 4px 9px; font-family: inherit; cursor: pointer; }
1114
+ .density-toggle:hover { border-color: var(--fg-30); }
1115
+ .density-toggle.on { border-color: var(--accent); color: var(--fg); background: rgba(255,154,118,.08); }
1116
+ /* Compact density · tightens the thread for daily-driver scanning (demo W8) */
1117
+ body.density-compact .panel { padding: 8px 11px; margin-bottom: 8px; }
1118
+ body.density-compact .tc-card { padding: 5px 9px; }
1119
+ body.density-compact .tc-out, body.density-compact .output { padding: 4px 8px; font-size: 10px; }
1120
+ body.density-compact .conv-turn { padding: 6px 0; }
1121
+ body.density-compact .conv-body { font-size: 12px; }
1122
+ body.density-compact .artifact-card { margin-bottom: 7px; }
1123
+ body[data-persona="pm"] .p-eng, body[data-persona="designer"] .p-eng { display: none; }
1124
+ /* Per-role granularity · PM wants decisions/outcomes (hide the technical
1125
+ request-flow); Designer wants the visual flow (hide the data tables). */
1126
+ body[data-persona="pm"] .p-flow { display: none; }
1127
+ body[data-persona="designer"] .p-data { display: none; }
1128
+ /* User-override result line (E-03 real flow) */
1129
+ .override-result { margin-top: 8px; font-size: 11.5px; color: var(--fg-60); }
1130
+ .override-result .ok { color: var(--allow); margin-right: 4px; }
1131
+ .override-result code { background: var(--bg-2,#1a1a1f); padding: 1px 5px; border-radius: 3px; color: var(--accent); font-size: 10.5px; }
1132
+ /* Structured artifacts · comparison / mock / pick (demo v12 parity) */
1133
+ .artifact-card { margin-bottom: 12px; }
1134
+ .artifact-h { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); margin-bottom: 6px; }
1135
+ .art-cmp { display: grid; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; font-size: 11px; }
1136
+ .art-cmp .hd { background: var(--bg-2,#1a1a1f); color: var(--fg-60); font-family: var(--mono); font-size: 10px; padding: 6px 8px; font-weight: 600; }
1137
+ .art-cmp .rl { background: var(--bg-1,#141418); color: var(--fg-50); font-family: var(--mono); font-size: 10px; padding: 6px 8px; }
1138
+ .art-cmp .cell { background: var(--bg,#0f0f12); color: var(--fg-70); padding: 6px 8px; }
1139
+ .art-cmp .cell .pick { color: var(--allow); }
1140
+ .art-cmp .cell .meh { color: var(--warn,#e0b341); }
1141
+ .art-mock { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; margin-bottom: 12px; }
1142
+ .art-mock-head { display: flex; align-items: center; background: var(--bg-2,#1a1a1f); padding: 6px 10px; font-family: var(--mono); font-size: 10.5px; }
1143
+ .art-mock-head .status { color: var(--warn,#e0b341); }
1144
+ .art-mock-head .ct { margin-left: auto; color: var(--fg-40); font-size: 10px; }
1145
+ .art-mock-body { padding: 10px 12px; font-family: var(--mono); font-size: 11px; color: var(--fg-70); white-space: pre; overflow-x: auto; }
1146
+ .art-mock-body .jk { color: var(--fg-50); }
1147
+ .art-mock-body .js { color: var(--allow); }
1148
+ .art-mock-body .jn { color: var(--warn,#e0b341); }
1149
+ .art-pick { border-left: 2px solid var(--accent); background: linear-gradient(90deg, rgba(255,154,118,0.05), transparent); border-radius: 0 6px 6px 0; padding: 10px 12px; margin-bottom: 12px; }
1150
+ .art-pick .k { color: var(--accent); font-family: var(--mono); font-size: 11.5px; font-weight: 600; margin-bottom: 5px; }
1151
+ .art-pick .what { color: var(--fg-80); font-size: 12px; line-height: 1.5; margin-bottom: 5px; }
1152
+ .art-pick .why { color: var(--fg-50); font-size: 11.5px; line-height: 1.5; }
1153
+ /* Benchmark chart · horizontal bars (demo v12 .bench) */
1154
+ .art-bench { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; font-size: 11px; }
1155
+ .art-bench .bl { font-family: var(--mono); color: var(--fg-50); min-width: 96px; font-size: 10.5px; }
1156
+ .art-bench .bb { flex: 1; height: 8px; background: var(--bg-2,#1a1a1f); border-radius: 4px; overflow: hidden; }
1157
+ .art-bench .bf { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
1158
+ .art-bench .bf.allow { background: var(--allow); }
1159
+ .art-bench .bf.warn { background: var(--warn,#e0b341); }
1160
+ .art-bench .bv { font-family: var(--mono); color: var(--fg-70); min-width: 56px; text-align: right; font-size: 10.5px; }
1161
+ /* Verifier severity checklist · ✓/⚠ per check */
1162
+ .art-checklist { display: flex; flex-direction: column; gap: 1px; }
1163
+ .art-check { display: flex; align-items: baseline; gap: 9px; padding: 5px 0; border-top: 1px solid rgba(42,42,50,.5); font-size: 12px; }
1164
+ .art-check:first-child { border-top: none; }
1165
+ .art-check .ci { color: var(--allow); flex-shrink: 0; font-size: 12px; }
1166
+ .art-check.warn .ci { color: var(--warn,#e0b341); }
1167
+ .art-check .cl { color: var(--fg-80); }
1168
+ .art-check .cn { color: var(--fg-40); font-family: var(--mono); font-size: 10.5px; margin-left: auto; text-align: right; }
1169
+ /* Sequence diagram · inline SVG · var() resolves in CSS rules, never in SVG
1170
+ presentation attributes — so every fill/stroke is classed, not inlined. */
1171
+ .art-seq { overflow-x: auto; padding: 2px 0; }
1172
+ .seq-svg { display: block; max-width: 100%; }
1173
+ .seq-svg .ll { stroke: var(--border); stroke-dasharray: 3 3; }
1174
+ .seq-svg .box { fill: var(--bg-2,#1a1a1f); stroke: var(--border); }
1175
+ .seq-svg .at { fill: var(--fg-80); }
1176
+ .seq-svg .msg { stroke: var(--fg-50); fill: none; }
1177
+ .seq-svg .msg.ret { stroke: var(--fg-40); stroke-dasharray: 4 3; }
1178
+ .seq-svg .msg.allow { stroke: var(--allow); }
1179
+ .seq-svg .msg.warn { stroke: var(--warn,#e0b341); }
1180
+ .seq-svg .mt { fill: var(--fg-60); }
1181
+ .seq-svg .mt.allow { fill: var(--allow); }
1182
+ .seq-svg .mt.warn { fill: var(--warn,#e0b341); }
1183
+ .seq-svg .ah { fill: var(--fg-50); }
1184
+ .seq-svg .ah-allow { fill: var(--allow); }
1185
+ .seq-svg .ah-warn { fill: var(--warn,#e0b341); }
1186
+ .seq-svg .ah-ret { fill: var(--fg-40); }
1187
+ /* Multi-speaker conversation · me + team + agents (demo v12 .turn/.speaker) */
1188
+ .conv-turn { padding: 9px 0; border-top: 1px solid rgba(42,42,50,.5); }
1189
+ .conv-turn:first-child { border-top: none; }
1190
+ .conv-head { display: flex; align-items: center; gap: 7px; margin-bottom: 5px; }
1191
+ .conv-name { font-size: 12px; font-weight: 600; color: var(--fg-80); }
1192
+ .conv-ts { font-family: var(--mono); font-size: 10px; color: var(--fg-30); margin-left: auto; }
1193
+ .conv-body { font-size: 13px; color: var(--fg-80); line-height: 1.55; padding-left: 25px; }
1194
+ .conv-body .at-route { color: var(--accent); font-family: var(--mono); font-size: 12px; }
1195
+ .conv-body .route-line { color: var(--fg-50); font-size: 12px; }
1196
+ .conv-body .route-line .ab { vertical-align: middle; margin: 0 3px; }
1197
+
1198
+ /* Unified timeline (task #51) · human/agent tier chips + inline collapsed
1199
+ tool events + emergent section dividers. Reuses .conv-* + .ab. */
1200
+ .conv-tier { font-family: var(--mono); font-size: 8.5px; letter-spacing: .06em; text-transform: uppercase; padding: 1px 6px; border-radius: 4px; border: 1px solid var(--border); color: var(--fg-50); }
1201
+ .conv-turn.is-human .conv-tier { color: var(--fg-80); border-color: rgba(255,255,255,.18); }
1202
+ .conv-turn.is-human { background: rgba(255,255,255,.012); border-radius: 8px; }
1203
+ .tl-div { display: flex; align-items: center; gap: 9px; margin: 15px 0 8px; }
1204
+ .tl-verb { font-family: var(--mono); font-size: 10px; letter-spacing: .03em; color: var(--fg-70); background: rgba(255,255,255,.03); border: 1px solid var(--border); border-radius: 999px; padding: 2px 10px; white-space: nowrap; }
1205
+ .tl-cnt { font-family: var(--mono); font-size: 9.5px; color: var(--fg-30); white-space: nowrap; }
1206
+ .tl-ln { flex: 1; height: 1px; background: var(--border); }
1207
+ .tl-acts { padding-left: 25px; display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }
1208
+ .tl-evt { font-family: var(--mono); font-size: 11px; }
1209
+ .tl-evt > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 7px; padding: 4px 9px; border: 1px solid var(--border); border-radius: 6px; color: var(--fg-50); }
1210
+ .tl-evt > summary::-webkit-details-marker { display: none; }
1211
+ .tl-evt > summary:hover { color: var(--fg-70); border-color: var(--fg-30); }
1212
+ .tl-evt .tl-caret { color: var(--fg-30); transition: transform .12s; font-size: 9px; }
1213
+ .tl-evt[open] .tl-caret { transform: rotate(90deg); }
1214
+ .tl-evt .tl-lbl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1215
+ .tl-evt .tl-dur { color: var(--fg-30); }
1216
+ .tl-evt .tl-badge { font-size: 8.5px; padding: 1px 6px; border-radius: 4px; margin-left: auto; }
1217
+ .tl-evt .tl-lbl + .tl-badge { margin-left: auto; }
1218
+ .tl-evt .tl-badge.allow { color: var(--allow); background: rgba(52,208,88,.12); }
1219
+ .tl-evt .tl-badge.block { color: #e5534b; background: rgba(229,83,75,.14); }
1220
+ .tl-evt .tl-badge.clean { color: var(--accent); background: rgba(255,154,118,.14); }
1221
+ .tl-evt.block > summary { border-color: rgba(229,83,75,.35); }
1222
+ .tl-out { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); white-space: pre-wrap; padding: 7px 10px 2px 20px; }
1223
+ /* Agent's own words (kind:'say') · the "watch it work" reasoning, in the agent's voice */
1224
+ .tl-say { display: flex; gap: 9px; align-items: flex-start; margin: 9px 0; }
1225
+ .tl-say .ab { margin-top: 2px; }
1226
+ .tl-say-text { flex: 1; min-width: 0; font-size: 13px; color: var(--fg-80); line-height: 1.5; background: rgba(255,255,255,.025); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; }
139
1227
 
140
1228
  .panel {
141
1229
  border: 1px solid var(--border); border-radius: 8px;
142
1230
  padding: 12px 14px; margin-bottom: 12px;
143
1231
  }
144
1232
  .panel-title { font-family: var(--mono); font-size: 10px; color: var(--fg-40); text-transform: uppercase; margin-bottom: 8px; }
1233
+ /* Observed (host) sessions — the coding agents codehere watches in your own
1234
+ terminal · distinct from spawned tasks below. Proves the INWARD axis:
1235
+ every agent, even the one you're talking to right now, is a member. */
1236
+ .obs-strip { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 14px; background: linear-gradient(180deg, rgba(255,154,118,0.04), transparent); }
1237
+ .obs-head { display: flex; align-items: center; gap: 7px; font-size: 11.5px; color: var(--fg-60); margin-bottom: 2px; }
1238
+ .obs-head .eye { opacity: 0.7; }
1239
+ .obs-head b { color: var(--fg-80); font-weight: 600; }
1240
+ .obs-sub { font-size: 10.5px; color: var(--fg-40); margin-bottom: 9px; }
1241
+ .obs-row { display: flex; align-items: center; gap: 10px; padding: 6px 8px; margin: 0 -8px; border-top: 1px solid var(--border); font-size: 11.5px; cursor: pointer; border-radius: 5px; transition: background 0.12s; }
1242
+ .obs-row:first-of-type { border-top: none; }
1243
+ .obs-row:hover { background: rgba(255,154,118,0.06); }
1244
+ .obs-id { font-family: var(--mono); color: var(--accent); font-size: 11px; min-width: 66px; }
1245
+ .obs-mix { color: var(--fg-50); flex: 1; font-family: var(--mono); font-size: 10.5px; }
1246
+ .obs-dur { color: var(--fg-40); font-size: 10.5px; }
1247
+ .obs-live { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
1248
+ .obs-live.live { background: var(--allow); animation: cb-blink 1.4s ease infinite; }
1249
+ .obs-live.recent { background: var(--warn,#e0b341); }
1250
+ .obs-live.idle { background: var(--fg-30); }
1251
+ .obs-badge { font-size: 9.5px; text-transform: uppercase; font-family: var(--mono); padding: 1px 6px; border-radius: 4px; letter-spacing: 0.3px; }
1252
+ .obs-badge.low { color: var(--allow); border: 1px solid color-mix(in srgb, var(--allow) 40%, transparent); }
1253
+ .obs-badge.medium { color: var(--warn, #e0b341); border: 1px solid color-mix(in srgb, var(--warn, #e0b341) 40%, transparent); }
1254
+ .obs-badge.high { color: var(--block); border: 1px solid color-mix(in srgb, var(--block) 40%, transparent); }
1255
+ .obs-blocked { color: var(--block); font-size: 10.5px; font-family: var(--mono); }
1256
+ /* Host-session thread · tool-call timeline */
1257
+ .obs-timeline { display: flex; flex-direction: column; }
1258
+ .obs-call { display: flex; align-items: baseline; gap: 10px; padding: 5px 0; border-top: 1px solid var(--border); font-size: 11.5px; }
1259
+ .obs-call:first-child { border-top: none; }
1260
+ .obs-call.is-block { background: color-mix(in srgb, var(--block) 8%, transparent); margin: 0 -8px; padding: 5px 8px; border-radius: 4px; border-top-color: transparent; }
1261
+ .obs-call-time { font-family: var(--mono); color: var(--fg-30); font-size: 10px; min-width: 64px; flex-shrink: 0; }
1262
+ .obs-call-tool { font-family: var(--mono); color: var(--accent); font-size: 10.5px; min-width: 56px; flex-shrink: 0; }
1263
+ .obs-call-prev { color: var(--fg-60); font-family: var(--mono); font-size: 10.5px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1264
+ .obs-call-tag { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: 0.3px; padding: 1px 5px; border-radius: 3px; flex-shrink: 0; }
1265
+ .obs-call-tag.ok { color: var(--fg-40); }
1266
+ .obs-call-tag.warn { color: var(--warn, #e0b341); }
1267
+ .obs-call-tag.block { color: var(--block); border: 1px solid color-mix(in srgb, var(--block) 40%, transparent); }
1268
+ .obs-call-tag.crit { color: #fff; background: var(--block); }
1269
+ /* Chain-flow · audit chain as connected nodes (demo v12 .chain-flow) */
1270
+ .chain-flow-wrap { padding: 4px 0 14px; }
1271
+ .chain-flow { display: flex; align-items: center; gap: 0; overflow-x: auto; padding: 4px 2px 8px; }
1272
+ .cf-node { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; min-width: 34px; height: 34px; border-radius: 7px; border: 1px solid var(--border); background: var(--bg-2,#1a1a1f); flex-shrink: 0; cursor: default; }
1273
+ .cf-node .cf-seq { font-family: var(--mono); font-size: 7.5px; color: var(--fg-40); line-height: 1; }
1274
+ .cf-node .cf-tool { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--fg-70); line-height: 1.2; }
1275
+ .cf-node.ok { border-color: color-mix(in srgb, var(--allow) 35%, var(--border)); }
1276
+ .cf-node.ok .cf-tool { color: var(--allow); }
1277
+ .cf-node.block { border-color: var(--block); background: color-mix(in srgb, var(--block) 12%, var(--bg-2,#1a1a1f)); }
1278
+ .cf-node.block .cf-tool { color: var(--block); }
1279
+ .cf-node.crit { border-color: var(--block); background: var(--block); }
1280
+ .cf-node.crit .cf-tool, .cf-node.crit .cf-seq { color: #fff; }
1281
+ .cf-link { width: 14px; height: 2px; background: linear-gradient(90deg, var(--allow), color-mix(in srgb, var(--allow) 30%, transparent)); flex-shrink: 0; opacity: 0.6; }
1282
+ .cf-gap { font-family: var(--mono); font-size: 11px; color: var(--fg-30); padding: 0 8px; flex-shrink: 0; letter-spacing: 1px; }
1283
+ .cf-legend { font-family: var(--mono); font-size: 10px; color: var(--fg-40); }
1284
+ /* Observed session · honest read-only affordance (no fake composer) */
1285
+ .obs-readonly-note { margin-top: 14px; padding: 12px 14px; border: 1px dashed var(--border); border-radius: 8px; background: rgba(255,255,255,0.015); }
1286
+ .obs-readonly-note .t { font-size: 12px; color: var(--fg-50); line-height: 1.5; margin-bottom: 10px; }
1287
+ .obs-fork-btn { background: var(--accent); color: #1a1a1f; border: none; border-radius: 6px; font-size: 12px; font-weight: 600; padding: 7px 12px; cursor: pointer; }
1288
+ .obs-fork-btn:hover { filter: brightness(1.08); }
1289
+ .obs-fork-btn:disabled { opacity: 0.6; cursor: default; }
1290
+ /* Remote-control panel · drive a managed agent (inject into running turn) */
1291
+ #control-panel:empty { display: none; }
1292
+ .ctl-head { font-size: 11px; color: var(--fg-50); font-family: var(--mono); margin-bottom: 8px; }
1293
+ .ctl-head .ag { color: var(--agent-codex); font-weight: 600; }
1294
+ .ctl-head code { background: var(--bg-2,#1a1a1f); padding: 1px 5px; border-radius: 3px; color: var(--fg-70); }
1295
+ .ctl-feed { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; max-height: 240px; overflow-y: auto; }
1296
+ .ctl-evt { font-size: 12px; color: var(--fg-80); padding: 5px 9px; background: var(--bg-2,#1a1a1f); border-radius: 6px; border-left: 2px solid var(--agent-codex); }
1297
+ .ctl-evt.dim { color: var(--fg-40); border-left-color: var(--border); font-style: italic; }
1298
+ .ctl-composer { display: flex; gap: 7px; }
1299
+ .ctl-composer input { flex: 1; background: var(--bg-2,#1a1a1f); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); padding: 7px 10px; font-size: 12px; }
1300
+ .ctl-composer input:focus { outline: none; border-color: var(--accent); }
1301
+ .ctl-composer button { background: var(--accent); color: #1a1a1f; border: none; border-radius: 6px; font-size: 12px; font-weight: 600; padding: 7px 12px; cursor: pointer; }
1302
+ /* Remote Control · honest live badge + steer chips + channel line. The badge
1303
+ renders ONLY inside the real-channel branch (controlManagedAgent success) —
1304
+ no live channel, no badge. Anti-screen-lie (the F-AGENT-SWITCHER class). */
1305
+ .ctl-badge { display: inline-flex; align-items: center; gap: 7px; background: rgba(40,200,64,0.08); border: 1px solid rgba(40,200,64,0.35); border-radius: 999px; padding: 5px 12px; font-size: 12px; color: var(--allow); font-weight: 600; margin-bottom: 10px; }
1306
+ .ctl-badge .lv { width: 8px; height: 8px; border-radius: 50%; background: var(--allow); box-shadow: 0 0 7px var(--allow); animation: pulse 1.6s infinite; }
1307
+ .ctl-steer { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 9px; }
1308
+ .ctl-steer .chip { font-size: 11.5px; color: var(--fg-70); border: 1px solid var(--border); border-radius: 999px; padding: 4px 11px; cursor: pointer; }
1309
+ .ctl-steer .chip:hover { border-color: var(--accent); color: var(--fg); }
1310
+ .ctl-steer .chip.stop { border-color: rgba(255,95,87,0.4); color: var(--block); }
1311
+ .ctl-channel { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--fg-40); margin-top: 8px; }
1312
+ .ctl-channel .lv { width: 7px; height: 7px; border-radius: 50%; background: var(--allow); box-shadow: 0 0 7px var(--allow); }
1313
+ .ctl-channel b { color: var(--allow); }
145
1314
  .outcome-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--fg-70); margin-bottom: 4px; }
146
1315
  .outcome-item .check { color: var(--allow); }
147
1316
  .outcome-item .cross { color: var(--block); }
@@ -157,6 +1326,135 @@ const SHARED_STYLES = `
157
1326
  .verdict.verified { color: var(--allow); }
158
1327
  .verdict.failed { color: var(--block); }
159
1328
 
1329
+ /* F-07 · verifier-agent panel (verified celebration / broken alert / partial warn) */
1330
+ /* Verifier-agent as killer-moment step · accent border, bigger type,
1331
+ soft glow · this is THE differentiator per claim_market_position. */
1332
+ .verifier-killer-step {
1333
+ border: 1px solid rgba(40,200,64,0.30) !important;
1334
+ border-left-width: 3px !important;
1335
+ background: linear-gradient(135deg, rgba(40,200,64,0.04), rgba(255,154,118,0.02)) !important;
1336
+ box-shadow: 0 0 0 3px rgba(40,200,64,0.04);
1337
+ padding: 12px 14px !important;
1338
+ }
1339
+ .verifier-killer-title { color: var(--allow); font-weight: 700; font-size: 12.5px; letter-spacing: 0.04em; font-family: var(--mono); }
1340
+ .verifier-killer-body { color: var(--fg-90); font-size: 13px; line-height: 1.55; margin-top: 6px; padding-left: 0; }
1341
+ .verifier-killer-body code { background: var(--bg-2); padding: 1px 5px; border-radius: 3px; font-family: var(--mono); font-size: 11.5px; color: var(--accent); }
1342
+
1343
+ /* W-PASS payload diff coloring · old values red strike-through, new values green */
1344
+ .wp-payload .key { color: var(--fg-50); }
1345
+ .wp-payload .val { color: var(--allow); }
1346
+ .wp-payload .from { color: var(--block); text-decoration: line-through; }
1347
+ .wp-payload .arrow { color: var(--fg-40); margin: 0 4px; }
1348
+ .wp-payload .added { color: var(--allow); }
1349
+
1350
+ /* W-PASS inline write-pass card · matches demo v12 lines 691-704 · blue
1351
+ * accent (not orange) because writes to third-party systems are the rare
1352
+ * moment when codehere mediates instead of just observes. Lives inline in
1353
+ * the step timeline · primary button opens the modal for actual approval. */
1354
+ .write-pass {
1355
+ padding: 12px 14px; margin: 10px 0;
1356
+ background: rgba(74,144,226,0.05);
1357
+ border: 1px solid rgba(74,144,226,0.3);
1358
+ border-left: 3px solid #4a90e2;
1359
+ border-radius: 6px;
1360
+ }
1361
+ .write-pass .head {
1362
+ display: flex; align-items: baseline; gap: 8px;
1363
+ margin-bottom: 6px;
1364
+ font-family: var(--mono); font-size: 10.5px;
1365
+ color: #4a90e2; letter-spacing: 0.12em; text-transform: uppercase;
1366
+ }
1367
+ .write-pass h4 {
1368
+ font-size: 13.5px; font-weight: 600; color: var(--fg);
1369
+ margin: 2px 0 6px;
1370
+ }
1371
+ .write-pass .target { font-family: var(--mono); font-size: 11.5px; color: var(--fg-70); margin-bottom: 6px; }
1372
+ .write-pass .target b { color: var(--fg); }
1373
+ .write-pass .payload {
1374
+ padding: 8px 11px; margin: 6px 0;
1375
+ background: hsl(0 0% 9%); border: 1px solid var(--border);
1376
+ border-radius: 4px;
1377
+ font-family: var(--mono); font-size: 11px;
1378
+ color: var(--fg-70); white-space: pre-wrap; line-height: 1.55;
1379
+ }
1380
+ .write-pass .payload .key { color: var(--fg-50); }
1381
+ .write-pass .payload .val { color: var(--allow); font-weight: 600; }
1382
+ .write-pass .payload .from { color: var(--block); text-decoration: line-through; }
1383
+ .write-pass .payload .arrow { color: var(--fg-40); margin: 0 4px; }
1384
+ .write-pass .actions {
1385
+ display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap;
1386
+ }
1387
+ .write-pass button {
1388
+ font-family: var(--mono); font-size: 11px; padding: 5px 11px;
1389
+ border-radius: 4px; cursor: pointer;
1390
+ border: 1px solid var(--border); background: transparent; color: var(--fg-70);
1391
+ transition: filter 150ms, border-color 150ms, color 150ms;
1392
+ }
1393
+ .write-pass button.primary {
1394
+ background: #4a90e2; color: #000; border-color: #4a90e2; font-weight: 600;
1395
+ }
1396
+ .write-pass button.primary:hover { filter: brightness(1.08); }
1397
+ .write-pass button.secondary:hover { color: var(--fg); border-color: var(--fg-30); }
1398
+
1399
+ .verifier-panel { border-left: 2px solid var(--border); padding-left: 12px; }
1400
+ .gotcha { border-left-color: var(--block); background: rgba(255,95,87,0.04); padding: 6px 10px; border-radius: 4px; margin-top: 4px; font-size: 12px; }
1401
+ .verifier-panel.broken { border-left-color: var(--block); }
1402
+ .verifier-panel.partial { border-left-color: var(--warn); }
1403
+
1404
+ /* Verified celebration · matches demo v12 .verified-block (lines 429-438).
1405
+ * Centered card with gradient green bg, 38px ✓ badge, uppercase VERIFIED
1406
+ * kicker, larger headline, mono stats line. Arrival animation slides up
1407
+ * + shimmer sweep so the moment lands. The single most important moment
1408
+ * in the detail view — the agent's claim survived verification. */
1409
+ .verified-block {
1410
+ padding: 18px 22px; text-align: center;
1411
+ background: linear-gradient(135deg, rgba(40,200,64,0.08), rgba(40,200,64,0.02));
1412
+ border: 1px solid rgba(40,200,64,0.35);
1413
+ border-left: 3px solid var(--allow); border-radius: 8px;
1414
+ margin: 8px 0; position: relative; overflow: hidden;
1415
+ opacity: 0; transform: translateY(12px);
1416
+ animation: verifiedIn 600ms cubic-bezier(0.16,1,0.3,1) 300ms forwards,
1417
+ verifiedShimmer 1.2s ease 900ms;
1418
+ }
1419
+ @keyframes verifiedIn { to { opacity: 1; transform: translateY(0); } }
1420
+ @keyframes verifiedShimmer {
1421
+ 0% { background-image: linear-gradient(110deg, transparent 30%, rgba(40,200,64,0.18) 50%, transparent 70%); background-size: 250% 100%; background-position: -50% 0; background-repeat: no-repeat; }
1422
+ 100% { background-image: linear-gradient(110deg, transparent 30%, rgba(40,200,64,0.18) 50%, transparent 70%); background-size: 250% 100%; background-position: 150% 0; background-repeat: no-repeat; }
1423
+ }
1424
+ .verified-block .badge {
1425
+ display: inline-flex; align-items: center; justify-content: center;
1426
+ width: 38px; height: 38px; border-radius: 50%;
1427
+ background: var(--allow); color: #000;
1428
+ font-size: 18px; font-weight: 700;
1429
+ margin-bottom: 8px; box-shadow: 0 0 0 4px rgba(40,200,64,0.15);
1430
+ }
1431
+ .verified-block .k {
1432
+ font-family: var(--mono); font-size: 10.5px; color: var(--allow);
1433
+ letter-spacing: 0.18em; text-transform: uppercase; margin-bottom: 5px;
1434
+ }
1435
+ .verified-block .what {
1436
+ font-size: 18px; font-weight: 600; color: var(--fg);
1437
+ margin-bottom: 6px; letter-spacing: -0.01em; line-height: 1.35;
1438
+ }
1439
+ .verified-block .stats {
1440
+ color: var(--fg-50); font-size: 11.5px;
1441
+ font-family: var(--mono); letter-spacing: 0.04em;
1442
+ }
1443
+ .verifier-summary { font-size: 13px; color: var(--fg-90); margin-bottom: 6px; }
1444
+ .verifier-summary .v-icon { display: inline-block; margin-right: 6px; font-weight: bold; }
1445
+ .verifier-panel.broken .v-icon { color: var(--block); }
1446
+ .verifier-panel.verified .v-icon { color: var(--allow); }
1447
+ .verifier-panel.partial .v-icon { color: var(--warn); }
1448
+ .finding { padding: 8px 10px; margin-top: 6px; border-radius: 4px; background: rgba(255,255,255,0.02); font-size: 12px; }
1449
+ .finding-sev { font-family: var(--mono); font-size: 9px; padding: 1px 5px; border-radius: 3px; margin-right: 6px; text-transform: uppercase; }
1450
+ .finding-sev.fail { color: var(--block); background: rgba(255,95,87,0.10); }
1451
+ .finding-sev.warn { color: var(--warn); background: rgba(254,188,46,0.10); }
1452
+ .finding-sev.info { color: var(--fg-50); background: rgba(255,255,255,0.04); }
1453
+ .finding-kind { font-family: var(--mono); font-size: 10px; color: var(--fg-40); margin-right: 6px; }
1454
+ .finding-msg { color: var(--fg-90); }
1455
+ .finding-detail { font-family: var(--mono); font-size: 11px; color: var(--fg-50); margin-top: 4px; padding-left: 18px; }
1456
+ .finding-detail .label { color: var(--fg-30); margin-right: 4px; }
1457
+
160
1458
  details.collapsible { margin-bottom: 12px; }
161
1459
  details.collapsible summary {
162
1460
  font-family: var(--mono); font-size: 11px; color: var(--fg-50);
@@ -165,9 +1463,11 @@ const SHARED_STYLES = `
165
1463
  details.collapsible summary:hover { color: var(--fg); }
166
1464
  .turn { padding: 6px 0; border-bottom: 1px solid var(--border-soft); font-size: 12px; }
167
1465
  .turn.user { color: var(--accent); }
168
- .turn.agent { color: var(--fg-70); }
169
- .tc { font-size: 11px; padding: 4px 0; color: var(--fg-50); font-family: var(--mono); }
170
- .tc code { color: var(--accent); }
1466
+ .turn.assistant { color: var(--fg-70); }
1467
+ .tc { font-size: 12px; padding: 8px 0; border-bottom: 1px solid var(--border-soft); }
1468
+ .tc-label { color: var(--fg); font-weight: 500; }
1469
+ .tc-output { margin: 4px 0 0; padding: 6px 10px; background: var(--bg-2); border-radius: 4px; font-family: var(--mono); font-size: 11px; color: var(--fg-50); white-space: pre-wrap; word-break: break-all; max-height: 100px; overflow: auto; }
1470
+ .tc-dur { font-size: 10px; color: var(--fg-30); margin-left: 8px; }
171
1471
 
172
1472
  .input-area {
173
1473
  border-top: 1px solid var(--border-soft); padding: 12px 20px;
@@ -178,9 +1478,35 @@ const SHARED_STYLES = `
178
1478
  background: var(--bg-2); border: 1px solid var(--border);
179
1479
  border-radius: 10px; padding: 10px 14px; max-width: 900px; margin: 0 auto;
180
1480
  }
181
- .input-bar:focus-within { border-color: rgba(255,154,118,0.4); }
1481
+ /* Composer · typing affordance · matches demo v12 .composer-bar.typing
1482
+ * (line 329). The border goes brand-orange the moment the user starts
1483
+ * typing · the input visibly "wakes up." :focus-within handles the
1484
+ * focus state, .typing handles the value-has-chars state. */
1485
+ .input-bar:focus-within { border-color: var(--accent); }
1486
+ .input-bar.typing { border-color: var(--accent); }
182
1487
  .input-bar.warn { border-color: var(--warn); }
183
1488
  .input-bar.block { border-color: var(--block); }
1489
+ /* R10 · one canonical composer at a time. When detail-view is open the
1490
+ * detail follow-up composer ("Ask a follow-up about this task...") is
1491
+ * the only input that should accept text · hide the global main
1492
+ * composer + kbd-hint. Pre-fix bug: BOTH composers stacked, both with
1493
+ * "↳ Auto · ~$0.02" chips, user couldn't tell which one typed to which.
1494
+ * Surfaced 2026-05-24 by Ega. */
1495
+ body.detail-open > .app > .input-area { display: none; }
1496
+ /* Keyboard hint · matches demo v12 lines 418-419. Subtle row of mono
1497
+ * shortcuts under the composer · ↵ send · ⇧↵ newline · ⌘K focus.
1498
+ * Discoverable without being noise. */
1499
+ .kbd-hint {
1500
+ font-family: var(--mono); font-size: 10px; color: var(--fg-30);
1501
+ padding: 4px 0 0; max-width: 900px; margin: 0 auto;
1502
+ display: flex; gap: 14px; align-items: center; flex-wrap: wrap;
1503
+ }
1504
+ .kbd-hint .item { display: inline-flex; gap: 5px; align-items: center; }
1505
+ .kbd {
1506
+ padding: 1px 5px; background: hsl(0 0% 12%);
1507
+ border: 1px solid var(--border); border-radius: 3px;
1508
+ color: var(--fg-50); font-size: 9px; line-height: 1.4;
1509
+ }
184
1510
  .input-ta {
185
1511
  flex: 1; background: transparent; border: none;
186
1512
  color: var(--fg); font-family: var(--sans); font-size: 13px;
@@ -188,7 +1514,10 @@ const SHARED_STYLES = `
188
1514
  }
189
1515
  .input-ta::placeholder { color: var(--fg-40); }
190
1516
  .input-ta:disabled { color: var(--fg-30); }
191
- .input-meta { font-family: var(--mono); font-size: 10px; color: var(--fg-40); white-space: nowrap; }
1517
+ .input-meta { font-family: var(--mono); font-size: 10px; color: var(--fg-40); white-space: nowrap; display: inline-flex; gap: 8px; align-items: center; }
1518
+ .input-meta .pick-agent { color: var(--fg-70); cursor: pointer; padding: 2px 6px; border-radius: 3px; transition: background 0.15s; }
1519
+ .input-meta .pick-agent:hover { background: rgba(255,255,255,0.05); color: var(--fg); }
1520
+ .input-meta .cost-est { color: var(--allow); padding: 2px 6px; border-radius: 3px; background: rgba(40,200,64,0.06); }
192
1521
  .send-btn {
193
1522
  background: var(--accent); color: #000; border: none; border-radius: 6px;
194
1523
  width: 28px; height: 28px; display: flex; align-items: center;
@@ -214,7 +1543,129 @@ const FILTERS = { all: s=>true, running: s=>['running','queued','paused','blocke
214
1543
 
215
1544
  function ms(t) { return t?'\\u2713':'\\u2715'; }
216
1545
  function dur(ms) { if(!ms&&ms!==0)return''; const s=Math.floor(ms/1000); if(s<60)return s+'s'; return Math.floor(s/60)+'m '+(s%60)+'s'; }
217
- function title(p) { return (p||'Untitled').split('\\n')[0].slice(0,80); }
1546
+ // Strip internal context-injection prefixes from a prompt before showing it to
1547
+ // the user. Real spawns prepend "[graphify context: ...]\\n\\n" (and failover
1548
+ // adds "[Previous session context — ...]") — functional for the agent, noise
1549
+ // for the human. Found via real-execution dogfood #45: the prefix was becoming
1550
+ // the thread title. Display-only; the actual spawn prompt keeps the prefix.
1551
+ function displayPrompt(p) {
1552
+ return String(p == null ? '' : p)
1553
+ .replace(/^\\[graphify context:[\\s\\S]*?\\]\\s*\\n+/, '')
1554
+ .replace(/^\\[Previous session context[\\s\\S]*?\\]\\s*\\n+/, '')
1555
+ .replace(/^\\s+/, '');
1556
+ }
1557
+ function title(p) {
1558
+ // Build a headline from whole CLAUSES (split on · — and sentence-enders),
1559
+ // accumulating up to ~72 chars — so we never chop mid-phrase like the old
1560
+ // "...200 rps · Lua INCR in". Trailing punctuation stripped per clause.
1561
+ var s=(displayPrompt(p)||'Untitled').split('\\n')[0].trim();
1562
+ var parts=s.split(/\\s*[\\u00B7\\u2014]\\s*|(?<=[.!?])\\s+/)
1563
+ .map(function(x){return x.replace(/[.!?,;:\\s]+$/,'').trim();}).filter(Boolean);
1564
+ var out='';
1565
+ for(var i=0;i<parts.length;i++){
1566
+ var nx=out?out+' \\u00B7 '+parts[i]:parts[i];
1567
+ if(out && nx.length>72) break;
1568
+ out=nx; if(out.length>=72) break;
1569
+ }
1570
+ out=(out||s).trim();
1571
+ if(out.length>80) out=out.slice(0,80).replace(/\\s+\\S*$/,'')+'\\u2026';
1572
+ return out||'Untitled';
1573
+ }
1574
+
1575
+ // Per-agent identity badge — primitive #1 from AGENTIC_UX_FRAMEWORK.
1576
+ // Returns a squircle with the agent's initials in its identity color.
1577
+ // Recognized: claude-code, codex, aider, gemini, opencode, conductor, auto.
1578
+ function agentKey(id) {
1579
+ const s = String(id||'').toLowerCase();
1580
+ if (s === 'claude-code' || s.startsWith('claude')) return 'claude';
1581
+ if (s === 'codex' || s.startsWith('codex')) return 'codex';
1582
+ if (s === 'aider' || s.startsWith('aider')) return 'aider';
1583
+ if (s === 'gemini' || s.startsWith('gemini')) return 'gemini';
1584
+ if (s === 'opencode' || s.startsWith('opencode')) return 'opencode';
1585
+ if (s === 'conductor') return 'conductor';
1586
+ if (s === 'auto') return 'auto';
1587
+ return 'auto';
1588
+ }
1589
+ function agentInitials(id) {
1590
+ const k = agentKey(id);
1591
+ if (k === 'claude') return 'CC';
1592
+ if (k === 'codex') return 'CD';
1593
+ if (k === 'aider') return 'AI';
1594
+ if (k === 'gemini') return 'GM';
1595
+ if (k === 'opencode') return 'OC';
1596
+ if (k === 'conductor') return 'CH';
1597
+ return '?';
1598
+ }
1599
+ function ab(id, size) {
1600
+ const k = agentKey(id);
1601
+ const sz = size === 'sm' ? ' sm' : (size === 'lg' ? ' lg' : '');
1602
+ return '<span class="ab ' + k + sz + '">' + agentInitials(id) + '</span>';
1603
+ }
1604
+ function agentLabel(id) {
1605
+ const k = agentKey(id);
1606
+ if (k === 'claude') return 'Claude Code';
1607
+ if (k === 'codex') return 'Codex';
1608
+ if (k === 'aider') return 'Aider';
1609
+ if (k === 'gemini') return 'Gemini';
1610
+ if (k === 'opencode') return 'OpenCode';
1611
+ if (k === 'conductor') return 'Conductor';
1612
+ if (k === 'auto') return 'Auto';
1613
+ return id || '?';
1614
+ }
1615
+ // Cost signal pill — primitive #2.
1616
+ function costPill(cents, paid) {
1617
+ if (cents === 0 || cents === null || cents === undefined) {
1618
+ return '<span class="cost-signal free">$0.00</span>';
1619
+ }
1620
+ const dollars = '$' + (cents/100).toFixed(cents < 10 ? 4 : 2);
1621
+ return '<span class="cost-signal ' + (paid === false ? 'free' : 'paid') + '">' + dollars + '</span>';
1622
+ }
1623
+ // Inline code-card renderer · single-pass tokenizer · processes one token at
1624
+ // a time so substitution markup never gets re-matched on the next replace.
1625
+ // Lightweight intentionally · "looks like code", not a real lexer.
1626
+ function codeCard(path, snippet, lang) {
1627
+ const ext = (lang || path.split('.').pop() || '').toLowerCase();
1628
+ const langLabel = ext === 'ts' || ext === 'tsx' ? 'typescript'
1629
+ : ext === 'js' || ext === 'jsx' ? 'javascript'
1630
+ : ext === 'py' ? 'python'
1631
+ : ext === 'rs' ? 'rust'
1632
+ : ext === 'go' ? 'go'
1633
+ : ext === 'lua' ? 'lua'
1634
+ : (ext || 'text');
1635
+ function esc(s){ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
1636
+ const KW = /^(const|let|var|function|return|if|else|for|while|import|export|from|class|extends|new|await|async|throw|try|catch|finally|of|in|typeof|null|true|false|this|interface|type|public|private|readonly|static|enum|as)\\b/;
1637
+ const TP = /^(string|number|boolean|void|any|unknown|Promise|Array|Record|Map|Set|Request|Response|NextFunction)\\b/;
1638
+ // Priority alternation · line-comment / single-quote string / double-quote string / number / identifier.
1639
+ const TOKEN = /^(\\/\\/[^\\n]*|'(?:\\\\.|[^'\\\\])*'|"(?:\\\\.|[^"\\\\])*"|\\d+(?:\\.\\d+)?|[A-Za-z_$][A-Za-z0-9_$]*)/;
1640
+ function tokenize(line){
1641
+ let out = '', i = 0;
1642
+ while(i < line.length){
1643
+ const rest = line.slice(i);
1644
+ const m = TOKEN.exec(rest);
1645
+ if(!m){ out += esc(rest[0]); i += 1; continue; }
1646
+ const tok = m[0];
1647
+ if(tok.charAt(0)==='/') out += '<span class="cm">'+esc(tok)+'</span>';
1648
+ else if(tok.charAt(0)==='"' || tok.charAt(0)==='\\'') out += '<span class="str">'+esc(tok)+'</span>';
1649
+ else if(/^\\d/.test(tok)) out += '<span class="num">'+esc(tok)+'</span>';
1650
+ else if(KW.test(tok)) out += '<span class="kw">'+esc(tok)+'</span>';
1651
+ else if(TP.test(tok)) out += '<span class="tp">'+esc(tok)+'</span>';
1652
+ else if(line.charAt(i + tok.length) === '(') out += '<span class="fn">'+esc(tok)+'</span>';
1653
+ else out += esc(tok);
1654
+ i += tok.length;
1655
+ }
1656
+ return out;
1657
+ }
1658
+ const lines = String(snippet || '').split('\\n');
1659
+ const body = lines.map(function(raw, i){
1660
+ return '<span class="ln">'+String(i+1).padStart(2,' ')+'</span>'+tokenize(raw);
1661
+ }).join('\\n');
1662
+ const meta = lines.length + (lines.length===1?' line':' lines');
1663
+ return '<div class="code-card">'
1664
+ + '<div class="head"><span class="lang">'+langLabel+'</span>'
1665
+ + '<span class="path">'+esc(path)+'</span>'
1666
+ + '<span class="meta">'+meta+'</span></div>'
1667
+ + '<pre>'+body+'</pre></div>';
1668
+ }
218
1669
  function allS(d) { const sp=d.spawned||d.sessions||[], pe=d.persisted||[], seen=new Set(sp.map(s=>s.sessionId)); return [...sp,...pe.filter(p=>!seen.has(p.sessionId))]; }
219
1670
  function cardActions(s) {
220
1671
  const m={
@@ -242,19 +1693,116 @@ function findS(id) { const s=allS(_cached.tasks||{}); return s.find(x=>x.session
242
1693
  function render() {
243
1694
  if(_detail){ const t=findS(_detail); if(t)renderD(t); else{ _detail=null; renderL(); } }
244
1695
  else renderL();
245
- upCnt(); upBud(); upSq();
1696
+ upCnt(); upBud(); upSq(); upWk(); upIdx();
1697
+ }
1698
+
1699
+ // C-01 · first-run indexing banner · auto-show when session count < 3 +
1700
+ // only once per browser session (sessionStorage tracks the dismissal).
1701
+ function upIdx(){
1702
+ const banner=document.getElementById('indexing-banner');
1703
+ if(!banner)return;
1704
+ try{
1705
+ if(sessionStorage.getItem('codehere_indexing_dismissed')==='1'){banner.style.display='none'; return;}
1706
+ }catch{}
1707
+ const all=allS(_cached.tasks||{});
1708
+ banner.style.display = all.length<3 ? 'block' : 'none';
1709
+ if(all.length>=3){
1710
+ try{sessionStorage.setItem('codehere_indexing_dismissed','1');}catch{}
1711
+ }
1712
+ }
1713
+
1714
+ // F-02 · cross-worktree alert · scan for any running session whose cwd
1715
+ // differs from the most-common cwd AND has been running > 5 minutes. Pulses
1716
+ // in the squad row · click to jump to that session's detail.
1717
+ function upWk(){
1718
+ const all=allS(_cached.tasks||{});
1719
+ const running=all.filter(x=>['running','queued','paused','blocked'].includes(x.status) && x.cwd);
1720
+ const ae=document.getElementById('worktree-alert');
1721
+ const at=document.getElementById('worktree-alert-text');
1722
+ if(!ae||!at){return;}
1723
+ if(running.length<2){ae.style.display='none'; window._wkTarget=null; return;}
1724
+ // Pick the cwd that's most-common · everything else is "other"
1725
+ const cwdCounts={};
1726
+ for(const r of running) cwdCounts[r.cwd]=(cwdCounts[r.cwd]||0)+1;
1727
+ let topCwd='', topN=0;
1728
+ for(const k in cwdCounts) if(cwdCounts[k]>topN){topN=cwdCounts[k]; topCwd=k;}
1729
+ const now=Date.now();
1730
+ const oddOnes=running.filter(r=>r.cwd!==topCwd && (now-r.startedAt) > 5*60*1000);
1731
+ if(!oddOnes.length){ae.style.display='none'; window._wkTarget=null; return;}
1732
+ const t=oddOnes[0];
1733
+ window._wkTarget=t.sessionId;
1734
+ // Shorten cwd to its last path component for display
1735
+ const short=String(t.cwd||'').split('/').filter(Boolean).slice(-1)[0] || t.cwd;
1736
+ const mins=Math.floor((now-t.startedAt)/60000);
1737
+ ae.style.display='inline-flex';
1738
+ at.textContent='running '+mins+'m in '+short;
1739
+ }
1740
+
1741
+ function fmtGap(ms){
1742
+ // Two-line label · big "4h 38m later" + small relative-time sub
1743
+ // ("≈ 5 hours" · "≈ 2 days"). Demo v12 lines 425-426 ·
1744
+ // .time-gap .label b / .time-gap .label .sub. R10 retro 2026-05-24:
1745
+ // earlier adjective sub ("between sessions") was banked as a
1746
+ // "numbers over adjectives" violation · replaced with a real
1747
+ // approximate delta so the gap reads as a measurement, not a feeling.
1748
+ let main;
1749
+ const m=Math.floor(ms/60000);
1750
+ if(m<60) main = m+'m later';
1751
+ else {
1752
+ const h=Math.floor(m/60); const rm=m%60;
1753
+ if(h<24) main = rm ? h+'h '+rm+'m later' : h+'h later';
1754
+ else main = Math.floor(h/24)+'d later';
1755
+ }
1756
+ let sub;
1757
+ const hours = ms / 3600000;
1758
+ if(hours < 1) {
1759
+ const mins = Math.round(ms/60000);
1760
+ sub = '\\u2248 ' + mins + ' minute' + (mins===1?'':'s');
1761
+ } else if(hours < 24) {
1762
+ const h = Math.round(hours);
1763
+ sub = '\\u2248 ' + h + ' hour' + (h===1?'':'s');
1764
+ } else if(hours < 24*30) {
1765
+ const days = Math.round(hours/24);
1766
+ sub = '\\u2248 ' + days + ' day' + (days===1?'':'s');
1767
+ } else {
1768
+ const wks = Math.round(hours/(24*7));
1769
+ sub = '\\u2248 ' + wks + ' week' + (wks===1?'':'s');
1770
+ }
1771
+ return '<b>'+main+'</b><span class="sub">'+sub+'</span>';
246
1772
  }
247
1773
 
248
1774
  function renderL() {
249
1775
  const c=document.getElementById('task-list'), all=allS(_cached.tasks||{});
250
1776
  const fl=all.filter(FILTERS[_filter]||FILTERS.all);
251
1777
  let html='';
1778
+ // Pin demo-shape sessions (id prefix 'demo-') to the very top — they
1779
+ // exist to demonstrate what the product does · always relevant.
1780
+ const demo=fl.filter(x=>/^demo-/.test(x.sessionId||''));
1781
+ const rest=fl.filter(x=>!/^demo-/.test(x.sessionId||''));
1782
+ if(demo.length){
1783
+ html+='<div class="section-label featured">Featured</div>';
1784
+ for(const t of demo) html+=cardH(t);
1785
+ }
252
1786
  for(const g of STAT_ORDER){
253
- const items=fl.filter(x=>aStat(x.status)===g||(g==='running'&&['running','queued'].includes(x.status))||(g==='failed'&&['crashed','killed','error'].includes(x.status)));
1787
+ const items=rest.filter(x=>aStat(x.status)===g||(g==='running'&&['running','queued'].includes(x.status))||(g==='failed'&&['crashed','killed','error'].includes(x.status)));
254
1788
  if(!items.length)continue;
255
1789
  const label={running:'Running',queued:'Running',paused:'Paused',blocked:'Blocked',done:'Done',failed:'Failed',crashed:'Failed',killed:'Failed'}[g]||g;
256
1790
  html+='<div class="section-label">'+label+'</div>';
257
- for(const t of items)html+=cardH(t);
1791
+ let prevTs=null;
1792
+ for(const t of items){
1793
+ // F-04 · time-gap separator · inject before this card when there's a
1794
+ // > 2h gap between its startedAt and the previous card's. Skips for
1795
+ // 'running' (still alive, no gap) and the first card (no predecessor).
1796
+ const ts = t.startedAt || 0;
1797
+ if(g!=='running' && prevTs!==null && ts>0 && prevTs>0){
1798
+ const gap=Math.abs(prevTs-ts);
1799
+ if(gap > 2*3600*1000){
1800
+ html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gap)+'</span><span class="line"></span></div>';
1801
+ }
1802
+ }
1803
+ html+=cardH(t);
1804
+ prevTs=ts;
1805
+ }
258
1806
  }
259
1807
  if(!html){
260
1808
  const h={all:'No tasks yet. Type a task below to get started.',running:'No running tasks.',done:'No completed tasks.',failed:'No failed tasks.'};
@@ -263,12 +1811,77 @@ function renderL() {
263
1811
  c.innerHTML=html;
264
1812
  }
265
1813
 
1814
+ function renderHostSessions(data){
1815
+ const strip=document.getElementById('host-sessions-strip');
1816
+ if(!strip)return;
1817
+ const sessions=(data&&data.sessions)||[];
1818
+ if(!sessions.length){ strip.style.display='none'; strip.innerHTML=''; return; }
1819
+ function he(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
1820
+ function obsDur(ms){ if(!ms||ms<1000)return'just now'; const s=Math.floor(ms/1000); if(s<60)return s+'s'; const m=Math.floor(s/60); if(m<60)return m+'m'; const h=Math.floor(m/60); return h+'h '+(m%60)+'m'; }
1821
+ function mix(tm){ return Object.entries(tm||{}).sort((a,b)=>b[1]-a[1]).slice(0,4).map(([k,v])=>he(k)+' '+v).join(' · '); }
1822
+ // live/recent/idle from last-event age · so a session that ended hours ago
1823
+ // stops claiming "observing" (honest · OPEN-HOST-SESSION-WEB-LIVE).
1824
+ function liveState(ls){ const age=Date.now()-new Date(ls).getTime(); if(!(age>=0))return'idle'; if(age<120000)return'live'; if(age<3600000)return'recent'; return'idle'; }
1825
+ function liveLabel(st){ return st==='live'?'live now':st==='recent'?'recent':'idle'; }
1826
+ // Explain the badge in plain English from the actual data · a bare "MEDIUM"
1827
+ // tells the user nothing about why. (per plain-English + no-screen-lies)
1828
+ function riskWhy(s){
1829
+ if(s.criticalCalls>0) return s.criticalCalls+' critical finding'+(s.criticalCalls===1?'':'s')+' · review';
1830
+ if(s.blockedCalls>0) return s.blockedCalls+' call'+(s.blockedCalls===1?'':'s')+' blocked before running · review';
1831
+ if(s.risk==='medium') return 'high-density session ('+s.totalCalls+' calls) · worth a glance if it ran unattended';
1832
+ return 'clean · '+s.totalCalls+' calls, nothing blocked';
1833
+ }
1834
+ let rows='', liveN=0;
1835
+ for(const s of sessions){
1836
+ const st=liveState(s.lastSeen); if(st==='live')liveN++;
1837
+ const blocked = s.blockedCalls>0 ? '<span class="obs-blocked" title="tool calls codehere blocked before they ran">'+s.blockedCalls+' blocked</span>' : '';
1838
+ rows+='<div class="obs-row" onclick="showHostSession(\\''+he(s.shortId)+'\\')" title="open this session as a thread · see every tool call codehere observed">'
1839
+ +'<span class="obs-live '+st+'" title="'+liveLabel(st)+'"></span>'
1840
+ +'<span class="obs-id" title="'+he(s.sessionId)+'">'+he(s.shortId)+'</span>'
1841
+ +'<span class="obs-mix">'+mix(s.toolMix)+'</span>'
1842
+ +blocked
1843
+ +'<span class="obs-dur">'+obsDur(s.durationMs)+'</span>'
1844
+ +'<span class="obs-badge '+he(s.risk)+'" title="'+he(riskWhy(s))+'">'+he(s.risk)+'</span>'
1845
+ +'</div>';
1846
+ }
1847
+ const n=sessions.length;
1848
+ const liveTxt = liveN>0 ? ' ('+liveN+' live now)' : ' (none active right now)';
1849
+ strip.innerHTML='<div class="obs-head"><span class="eye">&#128065;</span><span>codehere is observing <b>'+n+' coding-agent session'+(n===1?'':'s')+'</b> in your own terminal'+liveTxt+'</span></div>'
1850
+ +'<div class="obs-sub">Spawned tasks appear below. These run in your shell · every tool call is scanned by the hook before it executes.</div>'
1851
+ +rows;
1852
+ strip.style.display='block';
1853
+ }
1854
+
266
1855
  function cardH(t) {
1856
+ const he=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
267
1857
  const s=st(t.status), steps=(t.steps||[]).length, files=t.artifacts?.files?.length??0, d=dur(t.durationMs||(t.completedAt?t.completedAt-t.startedAt:Date.now()-t.startedAt));
268
1858
  const co=t.cost?t.cost.spent:null;
269
- return '<div class="task-card" onclick="showD(\\''+t.sessionId+'\\')">'
270
- +'<div class="card-row1">'+statIcon(s)+'<span class="card-title">'+title(t.prompt)+'</span>'+tagHtml(t.tags||[])+'<span class="card-agent">'+(t.agent||'?')+'</span></div>'
271
- +'<div class="card-row2"><span>'+d+'</span>'+(co!==null?'<span>· $'+(co/100).toFixed(2)+'</span>':'')+'<span>· '+steps+' tools</span><span>· '+files+' files</span>'+verB(t.verification)+'</div>'
1859
+ // Per-card outcome chip · ✓ Done · ⚠ Failed · ↻ Running · queued
1860
+ let outcomeHtml='';
1861
+ if(s==='done'||t.status==='completed'){
1862
+ const tests=t.artifacts && t.artifacts.tests;
1863
+ const passed=tests && tests.passed||0, failed=tests && tests.failed||0;
1864
+ outcomeHtml = '<span class="card-outcome ok">✓ '+(failed>0?(failed+' failed'):(passed>0?passed+' passed':'shipped'))+'</span>';
1865
+ } else if(['failed','error','crashed','killed'].includes(t.status)){
1866
+ outcomeHtml = '<span class="card-outcome fail">✗ '+(t.stalledReason?'stopped':t.status)+'</span>';
1867
+ } else if(s==='running'||t.status==='queued'||t.status==='paused'){
1868
+ outcomeHtml = '<span class="card-outcome queued">↻ '+t.status+'</span>';
1869
+ }
1870
+ // Agent-tinted left border + pin demo session per its id pattern
1871
+ const agentClass = 'agent-' + String(t.agent||'').toLowerCase().replace(/[^a-z0-9]/g,'-');
1872
+ const pinClass = /^demo-/.test(t.sessionId||'') ? ' tc-pinned' : '';
1873
+ const agentChip = '<span class="card-agent">' + ab(t.agent, 'sm') + agentLabel(t.agent) + '</span>';
1874
+ // Meta line · drop noisy zeros · show only what's nonzero
1875
+ const metaParts = [];
1876
+ metaParts.push('<span>'+d+'</span>');
1877
+ if(co!==null) metaParts.push('<span>· '+costPill(co)+'</span>');
1878
+ if(steps>0) metaParts.push('<span>· '+steps+' step'+(steps===1?'':'s')+'</span>');
1879
+ if(files>0) metaParts.push('<span>· '+files+' file'+(files===1?'':'s')+'</span>');
1880
+ const stalledHtml = t.stalledReason ? '<div class="card-stalled" title="the watchdog stopped this task because the agent went silent">'+he(t.stalledReason)+'</div>' : '';
1881
+ return '<div class="task-card '+agentClass+pinClass+'" onclick="showD(\\''+t.sessionId+'\\')">'
1882
+ +'<div class="card-row1">'+statIcon(s)+'<span class="card-title" title="'+displayPrompt(t.prompt).replace(/"/g,'&quot;').slice(0,400)+'">'+title(t.prompt)+'</span>'+tagHtml(t.tags||[])+agentChip+'</div>'
1883
+ +'<div class="card-row2">'+metaParts.join('')+verB(t.verification)+outcomeHtml+'</div>'
1884
+ +stalledHtml
272
1885
  +(cardActions(t.sessionId)?'<div class="card-row3">'+cardActions(t.sessionId)+'</div>':'')+'</div>';
273
1886
  }
274
1887
 
@@ -281,21 +1894,65 @@ function upCnt() {
281
1894
  function upBud() {
282
1895
  const p=_cached.pricing||_cached.tasks?.pricing;
283
1896
  const e=document.getElementById('budget');
284
- if(!p||!p.dailyBudgetCents){e.textContent='';e.className='budget';return;}
1897
+ const banner=document.getElementById('budget-banner');
1898
+ const bwhat=document.getElementById('budget-banner-what');
1899
+ const bmsg=document.getElementById('budget-banner-msg');
1900
+ if(!p||!p.dailyBudgetCents){
1901
+ e.textContent='';e.className='budget';
1902
+ if(banner) banner.style.display='none';
1903
+ return;
1904
+ }
285
1905
  const d=p.dailyBudgetCents,s=p.spentTodayCents||0;
286
1906
  e.textContent='$'+(s/100).toFixed(2)+'/$'+(d/100).toFixed(2);
287
1907
  e.className='budget'+(s>=d?' block':s>=d*0.8?' warn':' safe');
288
1908
  const ib=document.querySelector('.input-bar');
289
1909
  if(ib){ib.className='input-bar'+(s>=d?' block':s>=d*0.8?' warn':'');}
1910
+ // F-01 · budget threshold banner · only shows when crossing 80%
1911
+ if(banner && bwhat && bmsg){
1912
+ if(s>=d){
1913
+ banner.style.display='block';
1914
+ banner.className='budget-banner block';
1915
+ bwhat.textContent='budget exhausted';
1916
+ bmsg.textContent='daily cap $'+(d/100).toFixed(2)+' spent · paid spawns will block until tomorrow.';
1917
+ } else if(s>=d*0.8){
1918
+ banner.style.display='block';
1919
+ banner.className='budget-banner';
1920
+ bwhat.textContent='budget threshold';
1921
+ bmsg.textContent='$'+(s/100).toFixed(2)+' of $'+(d/100).toFixed(2)+' spent today · paid spawns require confirm.';
1922
+ } else {
1923
+ banner.style.display='none';
1924
+ }
1925
+ }
290
1926
  }
291
1927
 
292
1928
  function upSq() {
293
- const d=_cached.readiness||{}, a=d.agents||[], auto=d.auto?[{id:'auto',label:'Auto (Conductor)',state:d.auto.state}]:[], all=[...auto,...a];
1929
+ const d=_cached.readiness||{}, a=d.agents||[], auto=d.auto?[{id:'auto',label:'Auto',state:d.auto.state}]:[], all=[...auto,...a];
294
1930
  const e=document.getElementById('squad');
295
- e.innerHTML=all.map(x=>{const r=agentS(x.state),n=x.label||x.name||x.id||'?';return'<span class="squad-agent"><span class="squad-dot '+r+'"></span>'+n+'</span>';}).join('');
1931
+ // Per-agent identity badge in the squad row so vendor diversity is visible at a glance.
1932
+ e.innerHTML=all.map(x=>{
1933
+ const r=agentS(x.state);
1934
+ const tip=agentStateTip(x.state);
1935
+ const badge = x.id === 'auto' ? '<span class="squad-dot '+r+'"></span>' : ab(x.id, 'sm');
1936
+ const n = agentLabel(x.id) || x.label || x.name || x.id || '?';
1937
+ const dotAfter = x.id === 'auto' ? '' : '<span class="squad-dot '+r+'"></span>';
1938
+ const setupTag = r==='setup' ? '<span class="squad-setup">needs setup</span>' : '';
1939
+ return '<span class="squad-agent" title="'+tip.replace(/"/g,'&quot;')+'">'+badge+' '+n+' '+dotAfter+setupTag+'</span>';
1940
+ }).join('');
296
1941
  if(!all.length)e.innerHTML='<span style="color:var(--fg-40)">No agents configured</span>';
297
1942
  }
298
- function agentS(state){if(!state||state==='error')return'offline';if(state==='installed'||state==='ready')return'ready';return'busy';}
1943
+ // Map readiness state → dot class. 'setup' is distinct from 'busy': busy means
1944
+ // actively working a task, setup means installed/known but can't run until the
1945
+ // user does something (auth, key, install). Conflating them was a screen lie —
1946
+ // a dead-without-setup agent looked like it was working (dogfood 2026-05-27 #4).
1947
+ function agentS(state){if(!state||state==='error')return'offline';if(state==='installed'||state==='ready')return'ready';if(state==='needs_install'||state==='needs_auth'||state==='billed_key_required')return'setup';return'busy';}
1948
+ function agentStateTip(state){
1949
+ if(state==='ready'||state==='installed')return'ready · can run now';
1950
+ if(state==='needs_install')return'needs setup · not installed on this machine';
1951
+ if(state==='needs_auth')return'needs setup · sign in / add credentials first';
1952
+ if(state==='billed_key_required')return'needs setup · requires a billed API key';
1953
+ if(!state||state==='error')return'offline';
1954
+ return'busy · working on a task';
1955
+ }
299
1956
 
300
1957
  function setFilter(f){
301
1958
  _filter=f;
@@ -309,65 +1966,1223 @@ function showD(id){
309
1966
  if(!t){_detail=null;return;}
310
1967
  document.getElementById('task-list-view').style.display='none';
311
1968
  document.getElementById('task-detail-view').style.display='block';
1969
+ // R10 fix · when detail-view opens, hide the global main composer so the
1970
+ // detail-view follow-up composer ("Ask a follow-up about this task...")
1971
+ // is the ONLY input visible. Pre-fix · BOTH composers rendered stacked,
1972
+ // both with "↳ Auto · ~$0.02" chips · user couldn't tell which one
1973
+ // typed to which target. Surfaced 2026-05-24 by Ega.
1974
+ document.body.classList.add('detail-open');
312
1975
  renderD(t);
313
1976
  }
314
1977
  function hideD(){
315
1978
  _detail=null;
1979
+ if(_ctlPoll){ clearInterval(_ctlPoll); _ctlPoll=null; } _ctlId=null; // stop control polling
316
1980
  document.getElementById('task-list-view').style.display='';
317
1981
  document.getElementById('task-detail-view').style.display='none';
1982
+ document.body.classList.remove('detail-open');
318
1983
  render();
319
1984
  }
320
1985
 
1986
+ // Open an observed host session as a THREAD · the meta-dogfood payoff: the
1987
+ // coding-agent session running in your own terminal, with every tool call
1988
+ // codehere scanned, in order, hash-chained.
1989
+ async function showHostSession(shortId){
1990
+ _detail='host:'+shortId;
1991
+ document.getElementById('task-list-view').style.display='none';
1992
+ document.getElementById('task-detail-view').style.display='block';
1993
+ document.body.classList.add('detail-open');
1994
+ const dv=document.getElementById('detail-view');
1995
+ dv.innerHTML='<div class="detail-header"><span class="back-btn" onclick="hideD()">\\u2190 Back</span></div><div style="color:var(--fg-40);font-size:12px;padding:20px 0">loading observed session...</div>';
1996
+ let d;
1997
+ try{ d=await fetch('/api/host-sessions/'+encodeURIComponent(shortId)).then(x=>x.json()); }
1998
+ catch(e){ dv.innerHTML='<div class="detail-header"><span class="back-btn" onclick="hideD()">\\u2190 Back</span></div><div style="color:var(--block);padding:20px 0">Could not load session.</div>'; return; }
1999
+ if(!d||d.error){ dv.innerHTML='<div class="detail-header"><span class="back-btn" onclick="hideD()">\\u2190 Back</span></div><div style="color:var(--fg-40);padding:20px 0">'+he(d&&d.error||'No such session')+'</div>'; return; }
2000
+ function he(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
2001
+ function ago(ts){ const m=Math.floor((Date.now()-new Date(ts).getTime())/60000); if(m<1)return'just now'; if(m<60)return m+'m ago'; const h=Math.floor(m/60); if(h<24)return h+'h ago'; return Math.floor(h/24)+'d ago'; }
2002
+ function clock(ts){ try{ return new Date(ts).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}); }catch{return'';} }
2003
+ const calls=d.calls||[];
2004
+ // Tool-call timeline · one row per observed call · time · tool · preview · state
2005
+ // No phase narrative here · raw terminal tool calls don't carry lifecycle
2006
+ // semantics (a dev session building codehere mentions "audit"/"build"/"test"
2007
+ // as domain words, so the classifier lies). Phases are a spawned-task concept.
2008
+ // Instead, group calls by tool so the timeline reads as activity, not noise.
2009
+ let timeline='';
2010
+ for(const c of calls){
2011
+ const tag = c.criticalFindings>0 ? '<span class="obs-call-tag crit">critical</span>'
2012
+ : c.blocked ? '<span class="obs-call-tag block">blocked</span>'
2013
+ : c.findings>0 ? '<span class="obs-call-tag warn">'+c.findings+' finding'+(c.findings===1?'':'s')+'</span>'
2014
+ : '<span class="obs-call-tag ok">scanned</span>';
2015
+ timeline+='<div class="obs-call'+(c.blocked?' is-block':'')+'">'
2016
+ +'<span class="obs-call-time">'+he(clock(c.timestamp))+'</span>'
2017
+ +'<span class="obs-call-tool">'+he(c.toolName||'')+'</span>'
2018
+ +'<span class="obs-call-prev">'+(c.preview?he(c.preview):'<span style="color:var(--fg-30)">\\u2014</span>')+'</span>'
2019
+ +tag
2020
+ +'</div>';
2021
+ }
2022
+ // Tool mix summary · gives the observed thread a real "outcome"-style header
2023
+ // (what KIND of work happened) without inventing phases that aren't there.
2024
+ const _mix={};
2025
+ for(const c of calls){ const k=c.toolName||'?'; _mix[k]=(_mix[k]||0)+1; }
2026
+ const _mixStr=Object.entries(_mix).sort((a,b)=>b[1]-a[1]).map(([k,v])=>he(k)+' '+v).join(' · ');
2027
+ const chainOk=d.chainValid;
2028
+ // Chain-flow · the real audit chain as connected nodes (demo v12 .chain-flow,
2029
+ // gap 4.8). Condensed to KEY events: the first/last few + every blocked or
2030
+ // critical call (never hide those), with a gap marker for the elided middle.
2031
+ // Each node is a real chain entry — seq + tool + state — linked left→right.
2032
+ function chainFlow(cs){
2033
+ if(!cs.length) return '';
2034
+ function he(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
2035
+ function init(tool){ return (String(tool||'?').replace(/[^A-Za-z]/g,'')[0]||'?').toUpperCase(); }
2036
+ var keep=new Set();
2037
+ for(var i=0;i<cs.length;i++){ if(i<2||i>=cs.length-2||cs[i].blocked||cs[i].criticalFindings>0) keep.add(i); }
2038
+ var nodes='';
2039
+ var prevKept=-1;
2040
+ for(var j=0;j<cs.length;j++){
2041
+ if(!keep.has(j)) continue;
2042
+ if(prevKept>=0 && j-prevKept>1) nodes+='<span class="cf-gap" title="'+(j-prevKept-1)+' more scanned">···</span>';
2043
+ else if(prevKept>=0) nodes+='<span class="cf-link"></span>';
2044
+ var c=cs[j];
2045
+ var cls=c.criticalFindings>0?'crit':c.blocked?'block':'ok';
2046
+ var t=c.preview?he(c.preview).slice(0,60):he(c.toolName||'');
2047
+ nodes+='<span class="cf-node '+cls+'" title="'+(c.seq!=null?'#'+c.seq+' · ':'')+he(c.toolName||'')+' · '+t+'">'
2048
+ +'<span class="cf-seq">'+(c.seq!=null?'#'+c.seq:'')+'</span><span class="cf-tool">'+init(c.toolName)+'</span></span>';
2049
+ prevKept=j;
2050
+ }
2051
+ return '<div class="chain-flow-wrap"><div class="chain-flow">'+nodes+'</div>'
2052
+ +'<div class="cf-legend">spawn → latest · '+cs.length+' calls · each node is a hash-linked entry</div></div>';
2053
+ }
2054
+ const chainHtml='<div class="panel"><div class="panel-title">Audit chain</div>'
2055
+ +chainFlow(calls)
2056
+ +'<div class="verified-block" style="padding:14px 0">'
2057
+ +'<div class="badge" style="background:'+(chainOk?'var(--allow)':'var(--block)')+'">'+(chainOk?'\\u2713':'\\u2717')+'</div>'
2058
+ +'<div class="k">'+(chainOk?'hash chain verified':'chain broken')+'</div>'
2059
+ +'<div class="what">'+d.chainEntries+' entries · SHA-256 linked · every tool call in this session is tamper-evident</div>'
2060
+ +'</div></div>';
2061
+ const oHtml='<div class="panel"><div class="panel-title">Observed</div>'
2062
+ +'<div class="outcome-item"><span class="check">'+ms(true)+'</span> '+d.totalCalls+' tool calls scanned by the hook</div>'
2063
+ +(d.blockedCalls>0?'<div class="outcome-item"><span class="cross">'+ms(false)+'</span> '+d.blockedCalls+' blocked before running</div>':'<div class="outcome-item"><span class="check">'+ms(true)+'</span> nothing blocked</div>')
2064
+ +(d.criticalCalls>0?'<div class="outcome-item"><span class="cross">'+ms(false)+'</span> '+d.criticalCalls+' critical finding'+(d.criticalCalls===1?'':'s')+'</div>':'')
2065
+ +(_mixStr?'<div class="outcome-item" style="color:var(--fg-50);font-family:var(--mono);font-size:11px"><span class="pending">\\u2022</span> '+_mixStr+'</div>':'')
2066
+ +'</div>';
2067
+ dv.innerHTML='<div class="detail-header">'
2068
+ +'<span class="back-btn" onclick="hideD()">\\u2190 Back</span>'
2069
+ +'<div class="detail-title-wrap">'
2070
+ +'<div class="detail-kicker">observed · runs in your terminal · read-only</div>'
2071
+ +'<h1 class="detail-title">'+he(d.sessionId)+'</h1>'
2072
+ +'</div></div>'
2073
+ +'<div class="detail-meta"><span class="status-icon done"></span> last activity '+he(ago(calls.length?calls[calls.length-1].timestamp:Date.now()))+' · '+d.totalCalls+' calls</div>'
2074
+ +oHtml+chainHtml
2075
+ +'<div class="panel"><div class="panel-title">Tool calls ('+calls.length+')</div><div class="obs-timeline">'+timeline+'</div></div>'
2076
+ // Honest interaction affordance · an observed session runs in the user's own
2077
+ // terminal; codehere watches it read-only via the hook and cannot inject a
2078
+ // prompt into it. So NO fake composer (that would be a screen-lie). Instead,
2079
+ // an honest note + a real action: fork the context into a managed agent the
2080
+ // user CAN drive from here.
2081
+ +'<div class="obs-readonly-note">'
2082
+ +'<div class="t">This session runs in your terminal. codehere observes it read-only — it can\\'t send a prompt back into your shell. Continue the work in a managed agent codehere CAN drive:</div>'
2083
+ +'<button class="obs-fork-btn" id="ctl-open-btn" onclick="controlManagedAgent(\\''+he(d.sessionId)+'\\')">Continue in a managed agent →</button>'
2084
+ +'<div id="control-panel"></div>'
2085
+ +'</div>';
2086
+ }
2087
+
2088
+ // Remote-control · open a codehere-managed agent (codex) and DRIVE it from here:
2089
+ // open a control session, then inject() messages into the running turn via the
2090
+ // daemon control endpoints. The observed terminal session stays read-only; this
2091
+ // is a real agent codehere owns end-to-end (turn/steer).
2092
+ let _ctlId=null, _ctlPoll=null;
2093
+ async function controlManagedAgent(sessionId){
2094
+ const btn=document.getElementById('ctl-open-btn');
2095
+ const panel=document.getElementById('control-panel');
2096
+ if(!panel)return;
2097
+ if(btn)btn.disabled=true, btn.textContent='opening managed agent…';
2098
+ try{
2099
+ const r=await fetch('/api/control/open',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent:'codex',prompt:'Continue the work from terminal session '+sessionId})}).then(x=>x.json());
2100
+ if(!r.controlId){ if(btn)btn.disabled=false,btn.textContent='Continue in a managed agent →'; al('Control open failed: '+(r.error||'unknown')); return; }
2101
+ _ctlId=r.controlId;
2102
+ if(btn)btn.style.display='none';
2103
+ // Honest "Remote Control · live" affordance — rendered ONLY here, after a
2104
+ // real channel opened. data-verify-channel="live" lets /verify assert the
2105
+ // badge can never exist without a live channel (anti-screen-lie invariant).
2106
+ panel.innerHTML='<div class="ctl-badge" data-verify-unit="remote-control" data-verify-channel="live"><span class="lv"></span> Remote Control \\u00b7 live</div>'
2107
+ +'<div class="ctl-head">managed agent \\u00b7 <span class="ag">codex</span> \\u00b7 thread <code>'+r.threadId+'</code> \\u00b7 steer it below</div>'
2108
+ +'<div class="ctl-steer">'
2109
+ + '<span class="chip" data-steer="continue">\\u21b3 continue</span>'
2110
+ + '<span class="chip" data-steer="run the tests">run the tests</span>'
2111
+ + '<span class="chip" data-steer="show me the diff">show me the diff</span>'
2112
+ + '<span class="chip stop" data-steer="__stop">\\u23f9 stop steering</span>'
2113
+ +'</div>'
2114
+ +'<div class="ctl-feed" id="ctl-feed"></div>'
2115
+ +'<div class="ctl-composer"><input id="ctl-input" placeholder="Steer this live session\\u2026 (goes to the real running agent)"/><button id="ctl-send">Send \\u2191</button></div>'
2116
+ +'<div class="ctl-channel"><span class="lv"></span> <b>live channel attached</b> \\u00b7 steers recorded in audit chain \\u00b7 reachable from any device you\\'re signed in on</div>';
2117
+ document.getElementById('ctl-send').onclick=ctlInject;
2118
+ document.getElementById('ctl-input').addEventListener('keydown',e=>{ if(e.key==='Enter')ctlInject(); });
2119
+ panel.querySelectorAll('.ctl-steer .chip').forEach(function(c){ c.onclick=function(){ var s=c.getAttribute('data-steer'); if(s==='__stop'){ctlStop();return;} var inp=document.getElementById('ctl-input'); if(inp){ inp.value=s; ctlInject(); } }; });
2120
+ ctlPollEvents();
2121
+ _ctlPoll=setInterval(ctlPollEvents,1500);
2122
+ }catch(e){ if(btn)btn.disabled=false,btn.textContent='Continue in a managed agent →'; al('Network error: '+e.message); }
2123
+ }
2124
+ async function ctlInject(){
2125
+ const inp=document.getElementById('ctl-input'); if(!inp||!_ctlId)return;
2126
+ const msg=inp.value.trim(); if(!msg)return;
2127
+ inp.value='';
2128
+ try{ const r=await fetch('/api/control/'+_ctlId+'/inject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg})}).then(x=>x.json());
2129
+ if(!r.accepted)al('Inject not accepted');
2130
+ setTimeout(ctlPollEvents,300);
2131
+ }catch(e){ al('Inject failed: '+e.message); }
2132
+ }
2133
+ // Stop steering from here. Honest scope: detaches the control channel (stops
2134
+ // polling + closes the panel). It does NOT claim to kill the remote process —
2135
+ // that would be an unverifiable overclaim. The badge disappears with the panel.
2136
+ function ctlStop(){
2137
+ if(_ctlPoll){ clearInterval(_ctlPoll); _ctlPoll=null; }
2138
+ _ctlId=null;
2139
+ const panel=document.getElementById('control-panel');
2140
+ if(panel)panel.innerHTML='<div class="ctl-evt dim">stopped steering \\u00b7 channel detached \\u00b7 the agent keeps running in its own session</div>';
2141
+ }
2142
+ async function ctlPollEvents(){
2143
+ if(!_ctlId)return;
2144
+ try{ const r=await fetch('/api/control/'+_ctlId+'/events').then(x=>x.json());
2145
+ const feed=document.getElementById('ctl-feed'); if(!feed)return;
2146
+ function he(s){ return String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
2147
+ feed.innerHTML=(r.events||[]).map(e=>{ const it=(e.params||{}).item||{}; const t=it.text||it.command||it.type||e.method; return '<div class="ctl-evt">'+he(t)+'</div>'; }).join('')||'<div class="ctl-evt dim">agent working…</div>';
2148
+ }catch{}
2149
+ }
2150
+
2151
+ // ── Phase model · the demo v12 lifecycle spine ────────────────────────────
2152
+ // A thread reads as a phased narrative (research → build → ship → audit → live),
2153
+ // not a flat step list. Steps don't carry phase labels, so classify by the
2154
+ // dominant signal in the label/output. Order matters: test later phases first
2155
+ // so a "run tests" step lands in ship, not research.
2156
+ // Phase classifier · PHASE_ORDER / PHASE_META / stepPhase / classifySteps are
2157
+ // injected from the unit-tested module (thread-phase.ts) — one source of truth.
2158
+ // The \\bcode\\b bug that mislabeled read steps as BUILD is why this is no longer
2159
+ // inline-untested.
2160
+ ${PHASE_CLASSIFIER_SRC}
2161
+ // Chapter-card divider between phases · demo v12 .chapter-card pattern.
2162
+ function chapterCard(prevPhase,nextPhase){
2163
+ var prev=PHASE_META[prevPhase], next=PHASE_META[nextPhase];
2164
+ return '<div class="chapter-divider">'
2165
+ +(prev?'<span class="ch-prev">\\u2713 '+prev.done+'</span>':'<span class="ch-prev"></span>')
2166
+ +'<span class="ch-line"></span>'
2167
+ +'<span class="ch-next">Phase '+next.n+' of 5 \\u00B7 <b>'+next.label+'</b></span>'
2168
+ +'</div>';
2169
+ }
2170
+
2171
+ // Structured artifacts · render the comparison table / response mock /
2172
+ // recommendation pick that an agent produces, from a typed data array — NOT
2173
+ // hand-authored HTML. The demo v12 weaves these into Phase 1; here they render
2174
+ // from t.artifacts.structured[] so the primitive is real (seeded today, real
2175
+ // adapter output later · demo-synth pattern).
2176
+ function renderArtifacts(arr){
2177
+ if(!Array.isArray(arr)||!arr.length)return'';
2178
+ function he(s){ return String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
2179
+ function tintJson(raw){
2180
+ return he(raw)
2181
+ .replace(/&quot;([^&]*?)&quot;(\\s*:)/g, '<span class="jk">&quot;$1&quot;</span>$2')
2182
+ .replace(/:\\s*&quot;([^&]*?)&quot;/g, ': <span class="js">&quot;$1&quot;</span>')
2183
+ .replace(/:\\s*(-?\\d+(?:\\.\\d+)?)/g, ': <span class="jn">$1</span>');
2184
+ }
2185
+ let out='<div class="panel"><div class="panel-title">Artifacts produced</div>';
2186
+ for(const a of arr){
2187
+ if(a.type==='comparison'){
2188
+ const cols=a.columns||[];
2189
+ let g='<div class="art-cmp" style="grid-template-columns:130px repeat('+cols.length+',1fr)">';
2190
+ g+='<div class="hd"></div>'+cols.map(c=>'<div class="hd">'+he(c)+'</div>').join('');
2191
+ for(const r of (a.rows||[])){
2192
+ g+='<div class="rl">'+he(r.label)+'</div>';
2193
+ for(const cell of (r.cells||[])){
2194
+ const mk = cell.mark==='pick'?'pick':cell.mark==='meh'?'meh':'';
2195
+ g+='<div class="cell">'+(mk?'<span class="'+mk+'">'+he(cell.text)+'</span>':he(cell.text))+'</div>';
2196
+ }
2197
+ }
2198
+ g+='</div>';
2199
+ out+='<div class="artifact-card p-data">'+(a.title?'<div class="artifact-h">'+he(a.title)+'</div>':'')+g+'</div>';
2200
+ } else if(a.type==='mock'){
2201
+ out+='<div class="art-mock">'
2202
+ +'<div class="art-mock-head"><span class="status">'+he(a.status||'')+'</span>'+(a.contentType?'<span class="ct">'+he(a.contentType)+'</span>':'')+'</div>'
2203
+ +'<div class="art-mock-body">'+tintJson(a.body||'')+'</div>'
2204
+ +'</div>';
2205
+ } else if(a.type==='pick'){
2206
+ out+='<div class="art-pick">'
2207
+ +'<div class="k">'+he(a.title||'')+'</div>'
2208
+ +(a.what?'<div class="what">'+he(a.what)+'</div>':'')
2209
+ +(a.why?'<div class="why">'+he(a.why)+'</div>':'')
2210
+ +'</div>';
2211
+ } else if(a.type==='bench'){
2212
+ // Benchmark chart · horizontal bars · demo v12 .bench (phase audit 2.10)
2213
+ let rows=(a.rows||[]).map(function(r){
2214
+ const mk=r.mark==='allow'?' allow':r.mark==='warn'?' warn':'';
2215
+ const pct=Math.max(0,Math.min(100,Number(r.pct)||0));
2216
+ return '<div class="art-bench"><span class="bl">'+he(r.label)+'</span>'
2217
+ +'<span class="bb"><span class="bf'+mk+'" style="width:'+pct+'%"></span></span>'
2218
+ +'<span class="bv">'+he(r.value)+'</span></div>';
2219
+ }).join('');
2220
+ out+='<div class="artifact-card">'+(a.title?'<div class="artifact-h">'+he(a.title)+'</div>':'')+rows+'</div>';
2221
+ } else if(a.type==='checklist'){
2222
+ // Verifier severity grid · ✓/⚠ per check · demo v12 (phase audit 4.4/3.3)
2223
+ let items=(a.items||[]).map(function(it){
2224
+ const ok=it.ok!==false;
2225
+ return '<div class="art-check'+(ok?'':' warn')+'">'
2226
+ +'<span class="ci">'+(ok?'\\u2713':'\\u26A0')+'</span>'
2227
+ +'<span class="cl">'+he(it.label)+'</span>'
2228
+ +(it.note?'<span class="cn">'+he(it.note)+'</span>':'')
2229
+ +'</div>';
2230
+ }).join('');
2231
+ out+='<div class="artifact-card p-data">'+(a.title?'<div class="artifact-h">'+he(a.title)+'</div>':'')+'<div class="art-checklist">'+items+'</div></div>';
2232
+ } else if(a.type==='sequence'){
2233
+ // Sequence diagram · inline SVG · auto-layout actors + messages from a typed
2234
+ // directive — NOT hand-authored SVG (demo v12 Artifact 2 · phase audit 1.13 · C-13).
2235
+ const parts=(a.participants||[]).map(String);
2236
+ const msgs=a.messages||[];
2237
+ const n=parts.length;
2238
+ if(n>=2){
2239
+ const colW=148, padX=14, padTop=12, boxH=26, headGap=20, rowH=34, padBot=16, loopW=42;
2240
+ const cx=i=>padX+74+colW*i; // center x of lifeline i
2241
+ const w=padX*2+148+colW*(n-1)+loopW; // right margin covers a trailing self-loop
2242
+ const topY=padTop+boxH;
2243
+ const h=topY+headGap+rowH*msgs.length+padBot;
2244
+ const cls=m=>m.mark==='allow'?'allow':m.mark==='warn'?'warn':m.ret?'ret':'';
2245
+ const head=m=>m.mark==='allow'?'ah-allow':m.mark==='warn'?'ah-warn':m.ret?'ah-ret':'ah';
2246
+ let svg='<svg class="seq-svg" viewBox="0 0 '+w+' '+h+'" width="'+w+'" height="'+h+'" preserveAspectRatio="xMidYMin meet">';
2247
+ svg+='<defs>';
2248
+ ['ah','ah-allow','ah-warn','ah-ret'].forEach(function(id){
2249
+ svg+='<marker id="seq-'+id+'" markerWidth="9" markerHeight="9" refX="6.5" refY="3.5" orient="auto"><path class="'+id+'" d="M0,0 L7,3.5 L0,7 Z"/></marker>';
2250
+ });
2251
+ svg+='</defs>';
2252
+ for(let i=0;i<n;i++){
2253
+ const x=cx(i), bw=Math.max(76,parts[i].length*7+18);
2254
+ svg+='<line class="ll" x1="'+x+'" y1="'+topY+'" x2="'+x+'" y2="'+(h-padBot)+'"/>';
2255
+ svg+='<rect class="box" x="'+(x-bw/2)+'" y="'+padTop+'" width="'+bw+'" height="'+boxH+'" rx="5"/>';
2256
+ svg+='<text class="at" x="'+x+'" y="'+(padTop+boxH/2+4)+'" text-anchor="middle" font-size="11" font-family="ui-monospace,monospace">'+he(parts[i])+'</text>';
2257
+ }
2258
+ msgs.forEach(function(m,mi){
2259
+ const fi=typeof m.from==='number'?m.from:parts.indexOf(m.from);
2260
+ const ti=typeof m.to==='number'?m.to:parts.indexOf(m.to);
2261
+ if(fi<0||ti<0)return;
2262
+ const y=topY+headGap+rowH*mi+rowH/2, x1=cx(fi), x2=cx(ti), c=cls(m), tc=(c==='allow'||c==='warn')?' '+c:'';
2263
+ if(fi===ti){ // self-message · small loop to the right
2264
+ svg+='<path class="msg '+c+'" d="M'+x1+','+(y-5)+' h'+loopW+' v10 h-'+loopW+'" marker-end="url(#seq-'+head(m)+')"/>';
2265
+ svg+='<text class="mt'+tc+'" x="'+(x1+loopW+5)+'" y="'+(y-8)+'" font-size="10" font-family="ui-monospace,monospace">'+he(m.label||'')+'</text>';
2266
+ } else {
2267
+ svg+='<line class="msg '+c+'" x1="'+x1+'" y1="'+y+'" x2="'+x2+'" y2="'+y+'" marker-end="url(#seq-'+head(m)+')"/>';
2268
+ svg+='<text class="mt'+tc+'" x="'+((x1+x2)/2)+'" y="'+(y-6)+'" text-anchor="middle" font-size="10" font-family="ui-monospace,monospace">'+he(m.label||'')+'</text>';
2269
+ }
2270
+ });
2271
+ svg+='</svg>';
2272
+ out+='<div class="artifact-card p-flow">'+(a.title?'<div class="artifact-h">'+he(a.title)+'</div>':'')+'<div class="art-seq">'+svg+'</div></div>';
2273
+ }
2274
+ }
2275
+ }
2276
+ out+='</div>';
2277
+ return out;
2278
+ }
2279
+
2280
+ // Multi-speaker conversation · the demo v12 thread model · turns from the human
2281
+ // (me), other team members, and agents (claude-code/codex/aider/...) — each with
2282
+ // an avatar badge. A turn's speaker is an agent key, user/conductor, or a
2283
+ // human name. This is the thread spine, not a collapsed afterthought.
2284
+ function isAgentSpeaker(sp){
2285
+ var s=String(sp||'').toLowerCase();
2286
+ return /^(claude|codex|aider|gemini|opencode|conductor)/.test(s);
2287
+ }
2288
+ function speakerAvatar(sp,name){
2289
+ if(isAgentSpeaker(sp)) return ab(sp);
2290
+ var src=(name||sp||'?').trim();
2291
+ var init=src.split(/\\s+/).map(function(w){return w[0]||'';}).join('').slice(0,2).toUpperCase()||'?';
2292
+ return '<span class="ab user">'+init.replace(/[<>&]/g,'')+'</span>';
2293
+ }
2294
+ function speakerName(sp,name){
2295
+ if(name) return name;
2296
+ if(isAgentSpeaker(sp)) return agentLabel(sp);
2297
+ return sp||'unknown';
2298
+ }
2299
+ function convHe(s){ return String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
2300
+ // One conversation row · reused by the conversation panel AND the unified
2301
+ // timeline so a turn looks identical wherever it appears.
2302
+ function renderTurn(u){
2303
+ var sp=u.speaker||u.role||'assistant';
2304
+ var body=convHe(u.content||u.body||'').replace(/@(claude-code|claude|codex|aider|gemini|opencode)/g,'<span class="at-route">@$1</span>');
2305
+ var human=!isAgentSpeaker(sp);
2306
+ return '<div class="conv-turn'+(human?' is-human':' is-agent')+'">'
2307
+ +'<div class="conv-head">'+speakerAvatar(sp,u.name)+'<span class="conv-name">'+convHe(speakerName(sp,u.name))+'</span>'
2308
+ +'<span class="conv-tier">'+(human?(sp==='user'?'you':'teammate'):'agent')+'</span>'
2309
+ +(u.ts?'<span class="conv-ts">'+convHe(u.ts)+'</span>':'')+'</div>'
2310
+ +'<div class="conv-body'+(u.routeLine?' route':'')+'">'+(u.routeLine?'<span class="route-line">'+body+'</span>':body)+'</div>'
2311
+ +'</div>';
2312
+ }
2313
+ function renderConversation(turns){
2314
+ if(!Array.isArray(turns)||!turns.length) return '';
2315
+ return '<div class="panel"><div class="panel-title">Conversation</div>'+turns.map(renderTurn).join('')+'</div>';
2316
+ }
2317
+ // Unified timeline (task #51) · the conversation IS the spine, and the tool
2318
+ // activity is woven INTO it as inline, collapsed-by-default events (native
2319
+ // <details>, no JS), grouped by EMERGENT sections (segmentSteps — dynamic, not
2320
+ // a fixed pipeline). Turn timestamps are display strings and step times are
2321
+ // epoch (not clock-comparable), so we place the activity block right where
2322
+ // execution starts: after the first non-user turn (routing / first agent),
2323
+ // before that agent reports back. Reads correct for seeded dialogues and for
2324
+ // real single-agent runs (where turns are sparse and the steps dominate).
2325
+ function renderTimeline(t, wrapCtxUrls){
2326
+ var turns=Array.isArray(t.turns)?t.turns:[];
2327
+ var steps=(t.toolTrace||t.steps||[]);
2328
+ if(!turns.length && !steps.length) return '';
2329
+ function evtRow(z){
2330
+ var raw=String(z.label||'');
2331
+ // Write-pass · the one moment the human MUST act. Keep the rich primitive
2332
+ // (Approve+send opens the write-pass modal) rather than collapsing it to a
2333
+ // one-liner — collapsing would orphan the approval trigger.
2334
+ if(/^\\s*w-?pass\\b|^\\s*write-pass\\b/i.test(raw)){
2335
+ var parts=raw.split(/\\s*·\\s*/);
2336
+ var wtitle=(parts[1]||raw||'write awaits').replace(/</g,'&lt;');
2337
+ var wtail=(parts.slice(2).join(' · ')||(z.state==='active'?'awaiting approval':'')).replace(/</g,'&lt;');
2338
+ var pHtml=(z.output?String(z.output):'').replace(/</g,'&lt;')
2339
+ .replace(/(in-progress|pending|todo|backlog)/g,'<span class="from">$1</span>')
2340
+ .replace(/(\\u2192|->|→)/g,'<span class="arrow">$1</span>')
2341
+ .replace(/(done|approved|merged|shipped|sent)/g,'<span class="val">$1</span>');
2342
+ return '<div class="write-pass" style="margin-left:25px">'
2343
+ +'<div class="head"><span>write-pass</span><span class="tail" style="color:var(--fg-50);letter-spacing:normal;text-transform:none">'+wtail+'</span></div>'
2344
+ +'<h4>'+wtitle+'</h4>'
2345
+ +(pHtml?'<div class="payload">'+pHtml+'</div>':'')
2346
+ +'<div class="actions"><button class="primary" onclick="if(window.openWpModal)openWpModal()">Approve + send</button></div>'
2347
+ +'</div>';
2348
+ }
2349
+ var lbl=wrapCtxUrls(raw.replace(/</g,'&lt;'));
2350
+ var cls='allow', txt='L0';
2351
+ if(z.state==='failed'){ cls='block'; txt='BLOCKED'; }
2352
+ else if(z.state==='active'){ cls='clean'; txt='RUN'; }
2353
+ var dur=(z.completedAt&&z.startedAt)?'<span class="tl-dur">'+(Math.round((z.completedAt-z.startedAt)/100)/10)+'s</span>':'';
2354
+ var out=z.output?'<div class="tl-out">'+wrapCtxUrls(String(z.output).slice(0,400).replace(/</g,'&lt;'))+'</div>':'';
2355
+ return '<details class="tl-evt '+cls+'"><summary><span class="tl-caret">\\u25B8</span><span class="tl-lbl">'+lbl+'</span>'+dur+'<span class="tl-badge '+cls+'">'+txt+'</span></summary>'+out+'</details>';
2356
+ }
2357
+ // The agent's own words (kind:'say') — rendered as the agent speaking, with
2358
+ // its avatar, distinct from the collapsed tool events.
2359
+ function sayRow(st){
2360
+ return '<div class="tl-say" data-verify-unit="say" data-verify-textlen="'+String((st.label||'').trim().length)+'">'+ab(t.agent,'sm')+'<div class="tl-say-text">'+convHe(st.label).replace(/\\n/g,'<br>')+'</div></div>';
2361
+ }
2362
+ // Single in-order pass so the agent's words + its tool actions interleave the
2363
+ // way they actually happened ("explains → acts"). Dynamic ⧁ section dividers
2364
+ // appear when a RUN of tool actions shifts activity (segmentSteps logic,
2365
+ // inline); a 'say' breaks the run so the next tool reopens a section.
2366
+ function activityBlock(){
2367
+ if(!steps.length) return '';
2368
+ var html='', curPhase=null, openActs=false;
2369
+ var close=function(){ if(openActs){ html+='</div>'; openActs=false; } };
2370
+ for(var i=0;i<steps.length;i++){
2371
+ var st=steps[i];
2372
+ if(st.kind==='say'){ close(); curPhase=null; html+=sayRow(st); continue; }
2373
+ var ph=(typeof stepPhase==='function'?stepPhase(st):null)||curPhase||'research';
2374
+ if(ph!==curPhase){ close(); var verb=(typeof PHASE_META!=='undefined'&&PHASE_META[ph]&&PHASE_META[ph].verb)||ph; html+='<div class="tl-div"><span class="tl-verb">\\u29C1 '+convHe(verb)+'</span><span class="tl-ln"></span></div>'; curPhase=ph; }
2375
+ if(!openActs){ html+='<div class="tl-acts">'; openActs=true; }
2376
+ html+=evtRow(st);
2377
+ }
2378
+ close();
2379
+ return html;
2380
+ }
2381
+ // split point: just after the first non-user (routing / agent) turn
2382
+ var splitAt=0;
2383
+ for(var i=0;i<turns.length;i++){ var sp=turns[i].speaker||turns[i].role; if(isAgentSpeaker(sp)){ splitAt=i+1; break; } }
2384
+ if(!turns.length) splitAt=0;
2385
+ var head=turns.slice(0,splitAt).map(renderTurn).join('');
2386
+ var tail=turns.slice(splitAt).map(renderTurn).join('');
2387
+ return '<div class="panel tl-panel"><div class="panel-title">Conversation</div>'+head+activityBlock()+tail+'</div>';
2388
+ }
2389
+
321
2390
  function renderD(t) {
322
2391
  const s=st(t.status), steps=(t.steps||[]), files=t.artifacts?.files||[], d=dur(t.durationMs||(t.completedAt?t.completedAt-t.startedAt:Date.now()-t.startedAt));
323
2392
  const co=t.cost?t.cost.spent:null, v=t.verification||'unverified';
2393
+ // Phase spine · classify once, reuse for the meta position + the timeline.
2394
+ const _phaseSeq = classifySteps(steps);
2395
+ const _maxPhase = _phaseSeq.reduce((m,p)=>Math.max(m,PHASE_META[p].n),1);
2396
+ const _phasePos = steps.length ? 'Phase '+_maxPhase+' of 5 · '+PHASE_META[PHASE_ORDER[_maxPhase-1]].label : '';
324
2397
  const nStep=steps.length, cStep=steps.filter(x=>x.status==='completed').length;
325
2398
  const progDots=Array.from({length:Math.max(nStep,3)},(_,i)=>i<cStep?'<span class="pdot done"></span>':i===cStep?'<span class="pdot current"></span>':'<span class="pdot pending"></span>').join('');
326
2399
 
327
- let oHtml='<div class="panel"><div class="panel-title">Outcome</div>';
2400
+ // R8/R9/R10/R11 — Final Report outcome only ✓ when prose summary actually exists.
2401
+ // Empty finalReport object or null/empty summary => honest "unavailable" outcome,
2402
+ // not a green check. Composer must offer a re-spawn affordance for these cases.
2403
+ const hasReportText = !!(t.finalReport && typeof t.finalReport.summary === 'string' && t.finalReport.summary.trim());
2404
+ const isTerminal = ['completed','done','killed','failed','error','crashed'].includes(t.status);
2405
+ // The headline "N files changed" number. Normally derived from the actual
2406
+ // artifact list, so claim === rendered count, always. The optional
2407
+ // artifacts.filesClaimed override is the ONE seam where a headline number can
2408
+ // diverge from the rendered file rows — the honesty probe seeds exactly that
2409
+ // divergence so /verify's counts-consistent invariant has a real lie to catch.
2410
+ const claimedFiles = (t.artifacts && typeof t.artifacts.filesClaimed === 'number') ? t.artifacts.filesClaimed : files.length;
2411
+
2412
+ let oHtml='<div class="panel" data-verify-unit="outcome" data-verify-files-claimed="'+claimedFiles+'" data-verify-steps="'+nStep+'" data-verify-terminal="'+(isTerminal?'true':'false')+'" data-verify-has-report="'+(hasReportText?'true':'false')+'"><div class="panel-title">Outcome</div>';
328
2413
  if(t.status==='running'||t.status==='queued'){
329
2414
  oHtml+='<div class="progress-dots">'+progDots+'</div><div class="outcome-item pending">'+cStep+'/'+Math.max(nStep,3)+' steps</div>';
330
2415
  } else {
331
- oHtml+=(files.length?'<div class="outcome-item"><span class="check">'+ms(true)+'</span> '+files.length+' files changed</div>':'');
332
- oHtml+='<div class="outcome-item"><span class="'+(nStep>0?'check':'pending')+'">'+(nStep>0?ms(true):'\\u2022')+'</span> '+nStep+' tool calls</div>';
333
- oHtml+=t.finalReport?'<div class="outcome-item"><span class="check">'+ms(true)+'</span> Final Report</div>':'';
334
- oHtml+='<div class="verdict '+v+'">Status: '+v+(v==='unverified'?' \\u2014 review the output and verify manually':'')+'</div>';
335
- if(v==='unverified')oHtml+='<button class="card-btn verify" style="margin-top:8px" onclick="vfy(\\''+t.sessionId+'\\')">Verify</button>';
2416
+ oHtml+=(claimedFiles?'<div class="outcome-item"><span class="check">'+ms(true)+'</span> '+claimedFiles+' file'+(claimedFiles===1?'':'s')+' changed</div>':'');
2417
+ oHtml+='<div class="outcome-item"><span class="'+(nStep>0?'check':'pending')+'">'+(nStep>0?ms(true):'\\u2022')+'</span> '+nStep+' step'+(nStep===1?'':'s')+'</div>';
2418
+ if(hasReportText) {
2419
+ oHtml+='<div class="outcome-item"><span class="check">'+ms(true)+'</span> Final Report</div>';
2420
+ } else if(isTerminal) {
2421
+ oHtml+='<div class="outcome-item"><span class="pending">\\u26A0</span> Final Report unavailable</div>';
2422
+ }
336
2423
  }
337
2424
  oHtml+='</div>';
338
2425
 
339
- const turns=t.turns||t.steps||[], tc=t.toolTrace||[];
340
- const convHtml='<details class="collapsible"><summary>'+(turns.length>0?turns.length+' messages':'No messages')+'</summary>'
341
- +turns.map(u=>'<div class="turn '+(u.role||'agent')+'"><b>'+(u.role||'agent')+':</b> '+(u.content||u.output||JSON.stringify(u))+'</div>').join('')+'</details>';
342
- const traceHtml='<details class="collapsible"><summary>'+(tc.length>0?tc.length+' tool calls':'No tool trace')+'</summary>'
343
- +tc.map(z=>'<div class="tc"><code>'+z.tool+'</code> '+(z.args||'')+' <span style="color:var(--fg-30)">'+(z.durationMs?z.durationMs+'ms':'')+'</span></div>').join('')+'</details>';
2426
+ // R10 — render the actual 3-paragraph prose summary when present, in its own
2427
+ // panel below Outcome. Without this the user has no synthesis of what just
2428
+ // happened, only a green check on a missing artifact.
2429
+ let summHtml='';
2430
+ if(hasReportText) {
2431
+ const paras = String(t.finalReport.summary).split(/\\n\\s*\\n/).map(p=>p.trim()).filter(Boolean);
2432
+ summHtml = '<div class="panel"><div class="panel-title">Summary</div>'
2433
+ + paras.map(p=>'<p style="margin:6px 0;color:var(--fg-90);font-size:13px;line-height:1.55">'+p.replace(/</g,'&lt;')+'</p>').join('')
2434
+ + '</div>';
2435
+ }
2436
+
2437
+ // E-03 · hook BLOCK affordance · render a red-bordered card when the session
2438
+ // was killed by the safety hook (status='error' with a 'blocked' tag or any
2439
+ // step in state='failed' carrying a known block signature). The user gets
2440
+ // three named actions; the audit chain captures whichever they pick.
2441
+ let blockHtml='';
2442
+ const blockedStep = (t.steps||[]).find(s => s.state==='failed' && /block|denied|refused/i.test(String(s.output||s.label||'')));
2443
+ if(blockedStep){
2444
+ const bSid=String(t.sessionId||'').replace(/'/g,'');
2445
+ const bTool=String(blockedStep.label||'blocked-op').replace(/"/g,'').slice(0,80);
2446
+ blockHtml = '<div class="hook-block-card" id="hook-block-card">'
2447
+ + '<div class="hook-block-title">safety hook · BLOCKED</div>'
2448
+ + '<div class="hook-block-msg">'+String(blockedStep.label||'').replace(/</g,'&lt;')+'</div>'
2449
+ + '<div class="hook-block-actions" id="hook-block-actions">'
2450
+ + '<button onclick="overrideBlock(\\''+bSid+'\\',\\''+bTool.replace(/'/g,'')+'\\')">Override + log reason</button>'
2451
+ + '<button onclick="a(\\'kill\\',\\''+bSid+'\\')">Kill session</button>'
2452
+ + '</div>'
2453
+ + '<div id="override-result" class="override-result"></div>'
2454
+ + '</div>';
2455
+ }
2456
+
2457
+ // E-07 · push gate · if a session attempted git push (detect via step label),
2458
+ // surface the gate. Same shape as hook-block but warn-tinted.
2459
+ let pushHtml='';
2460
+ const pushStep = (t.steps||[]).find(s => /git\\s*push/i.test(String(s.label||'')));
2461
+ if(pushStep && pushStep.state==='active'){
2462
+ pushHtml = '<div class="push-gate-card">'
2463
+ + '<div class="push-gate-title">push gate · awaiting confirm</div>'
2464
+ + '<div class="hook-block-msg">'+String(pushStep.label||'').replace(/</g,'&lt;')+'</div>'
2465
+ + '<div class="hook-block-actions">'
2466
+ + '<button onclick="al(\\'Approve and push\\')">Push</button>'
2467
+ + '<button onclick="al(\\'Print command only\\')">Print command</button>'
2468
+ + '<button onclick="al(\\'Cancel\\')">Cancel</button>'
2469
+ + '</div>'
2470
+ + '</div>';
2471
+ }
2472
+
2473
+ // G-U10 · crash recovery · when a persisted session shows orphaned state
2474
+ // (status='error' from a daemon death) or the session has no completedAt
2475
+ // despite status being non-running, show a recovery affordance.
2476
+ let crashHtml='';
2477
+ const looksOrphaned = (t.status==='error' || t.status==='crashed') && !t.completedAt;
2478
+ if(looksOrphaned){
2479
+ crashHtml = '<div class="crash-recovery">'
2480
+ + '<div class="crash-recovery-title">orphaned session</div>'
2481
+ + '<div style="color:var(--fg-70);font-size:12px">The daemon died before this task completed. Audit chain has the partial trail; rerun the prompt to continue.</div>'
2482
+ + '</div>';
2483
+ }
2484
+
2485
+ // P3 · Recovery affordance · session-level failure with named options.
2486
+ // Triggers when the session FAILED for reasons other than a deliberate
2487
+ // hook block (E-03 owns that case) and isn't an orphan (G-U10 owns
2488
+ // that). Common cases: agent crashed mid-run, provider error, test
2489
+ // suite failed, the task itself returned non-zero.
2490
+ let recoveryHtml='';
2491
+ const isFailed = ['failed','error','crashed','killed'].includes(t.status);
2492
+ const isOrphan = looksOrphaned;
2493
+ const hasBlock = !!blockedStep;
2494
+ if(isFailed && !isOrphan && !hasBlock){
2495
+ const lastStep = (t.steps||[])[(t.steps||[]).length-1];
2496
+ const lastErr = lastStep && (lastStep.error || lastStep.label || '');
2497
+ const reasonText = t.stalledReason
2498
+ ? '<b>What happened:</b> '+String(t.stalledReason).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]))
2499
+ : lastErr
2500
+ ? '<b>What failed:</b> '+String(lastErr).slice(0,200).replace(/</g,'&lt;')
2501
+ : '<b>What failed:</b> the agent exited with status <code style="background:var(--bg-2);padding:1px 5px;border-radius:3px;font-family:var(--mono);font-size:11.5px">'+t.status+'</code> · no specific error captured';
2502
+ recoveryHtml = '<div class="recovery-card">'
2503
+ + '<div class="recovery-title"><span class="dot"></span>recovery · this task did not complete cleanly</div>'
2504
+ + '<div class="recovery-cause">'+reasonText+'</div>'
2505
+ + '<div class="recovery-resolve"><b>codehere:</b> the audit chain captured every step that DID run · choose what to do next. The chain stays intact whichever option you pick.</div>'
2506
+ + '<div class="recovery-actions">'
2507
+ + '<button class="primary" onclick="al(\\'Retry queued · this respawns the same prompt against the same agent\\')">Retry</button>'
2508
+ + '<button onclick="al(\\'Diagnose · opens the failing step in detail view · v1.0.1\\')">Diagnose</button>'
2509
+ + '<button onclick="al(\\'Marked resolved · audit captures the human acknowledgement\\')">Mark resolved</button>'
2510
+ + '</div>'
2511
+ + '</div>';
2512
+ }
2513
+
2514
+ // D-01 + D-04 · file tree with trust grades + test result chip · render
2515
+ // both as compact strip directly under Outcome when they have data. The
2516
+ // chips are part of the existing "outcome" semantics but visually richer.
2517
+ let chipsHtml='';
2518
+ const filesArr = (t.artifacts && t.artifacts.files) || [];
2519
+ const testsObj = t.artifacts && t.artifacts.tests;
2520
+ if(filesArr.length || testsObj){
2521
+ chipsHtml = '<div class="panel"><div class="panel-title">Files &amp; tests</div>';
2522
+ if(filesArr.length){
2523
+ // File TREE · group by directory with ├─ └─ connectors + per-row trust
2524
+ // (demo v12 2.5) · richer than the flat chip strip when files span dirs.
2525
+ const esc=s=>String(s||'').replace(/</g,'&lt;');
2526
+ const grade=f=>(typeof f.trust==='number')?(f.trust>=85?'A':f.trust>=70?'B':f.trust>=50?'C':'F'):null;
2527
+ const byDir={};
2528
+ filesArr.forEach(function(f){
2529
+ const path=String(f.path||''); const sl=path.lastIndexOf('/');
2530
+ const dir=sl>=0?path.slice(0,sl):'.'; const name=sl>=0?path.slice(sl+1):path;
2531
+ (byDir[dir]=byDir[dir]||[]).push({name:name, f:f});
2532
+ });
2533
+ let tree='<div class="file-tree" data-verify-unit="filetree" data-verify-rendered="'+filesArr.length+'">';
2534
+ Object.keys(byDir).sort().forEach(function(dir){
2535
+ tree+='<div class="ft-dir">'+esc(dir)+'/</div>';
2536
+ const items=byDir[dir];
2537
+ items.forEach(function(it,i){
2538
+ const f=it.f, g=grade(f);
2539
+ const tr=g?'<span class="trust '+g+'">'+g+'</span>':'';
2540
+ const add=f.added>0?'<span class="added">+'+f.added+'</span>':'';
2541
+ const rem=f.removed>0?'<span class="removed">-'+f.removed+'</span>':'';
2542
+ tree+='<div class="ft-row"><span class="ft-branch">'+(i===items.length-1?'\\u2514\\u2500':'\\u251C\\u2500')+'</span>'
2543
+ +'<span class="ft-name">'+esc(it.name)+'</span>'+add+rem+tr+'</div>';
2544
+ });
2545
+ });
2546
+ tree+='</div>';
2547
+ chipsHtml += tree;
2548
+
2549
+ // Inline code-card · render under the chip strip for any file carrying
2550
+ // a .snippet. The reviewer sees WHAT got written, not just "+N lines".
2551
+ const cards = filesArr.filter(function(f){ return f && f.snippet; }).map(function(f){
2552
+ return codeCard(String(f.path||''), f.snippet, f.lang);
2553
+ }).join('');
2554
+ if(cards) chipsHtml += cards;
2555
+ }
2556
+ if(testsObj){
2557
+ const passed=testsObj.passed||0, failed=testsObj.failed||0;
2558
+ const failCls = failed>0 ? ' has-failures' : '';
2559
+ const durTxt = testsObj.durationMs ? ' <span class="dur">&middot; '+Math.round(testsObj.durationMs/100)/10+'s</span>' : '';
2560
+ chipsHtml += '<div class="test-chip'+failCls+'">'
2561
+ + '<span class="passed">&check; '+passed+' passed</span>'
2562
+ + (failed>0 ? '<span class="failed">&times; '+failed+' failed</span>' : '')
2563
+ + durTxt
2564
+ + '</div>';
2565
+ }
2566
+ chipsHtml += '</div>';
2567
+ }
2568
+
2569
+ // Non-git honesty · codehere derives file changes from git diff. In a folder
2570
+ // that isn't a git repo it captures zero file artifacts — NOT because nothing
2571
+ // changed, but because it can't see what did. Say so plainly rather than
2572
+ // letting an empty Files panel imply "no changes".
2573
+ let gitNoteHtml='';
2574
+ if(t.artifacts && t.artifacts.noGitTracking){
2575
+ gitNoteHtml = '<div class="panel git-note"><div class="panel-title">File tracking</div>'
2576
+ + '<div class="git-note-body">This folder isn\\'t a git repository, so codehere couldn\\'t track which files the agent changed. The steps below are what the agent reported. To see file changes and trust scores, run the task in a git repo (<code>git init</code> first).</div>'
2577
+ + '</div>';
2578
+ }
2579
+
2580
+ // C-02 · tool-call cards · render steps as structured cards with hook
2581
+ // badges + output preview · richer alternative to the existing collapsible
2582
+ // "N steps" blob (which still gets rendered below for the verbose path).
2583
+ // Wrap context-source URLs (linear://X · github://Y · figma://Z · drawio://W)
2584
+ // in .ctx-url-chip so the existing overlay-renderer (window.codehereOpenOverlay)
2585
+ // can preview them. Otherwise the URLs render as plain mono · dead text in a
2586
+ // primitive we already shipped to act on.
2587
+ function wrapCtxUrls(escapedText){
2588
+ return escapedText.replace(
2589
+ /(linear|github|figma|drawio):\\/\\/[A-Za-z0-9_\\/\\-.#?=&]+/g,
2590
+ function(m){ return '<span class="ctx-url-chip" data-url="'+m+'" title="click to preview">'+m+'</span>'; }
2591
+ );
2592
+ }
2593
+ const stepsArr = t.steps || [];
2594
+ let toolCardsHtml='';
2595
+ if(stepsArr.length && stepsArr.length <= 12){
2596
+ // Only render the rich cards for short traces. Long traces (>12 steps)
2597
+ // would visually overwhelm; they stay in the collapsible block below.
2598
+ toolCardsHtml = '<div class="panel"><div class="panel-title">Activity · phased</div>';
2599
+ // Inline audit-line · narrative beat at the start of the timeline so the
2600
+ // audit chain is VISIBLE in the flow, not just summarized in the footer.
2601
+ // Demo v12 pattern (line 1316). Synthesized from the actual task state
2602
+ // so the line stays honest: agent name comes from t.agent, file count
2603
+ // from artifacts.files.length, hook mode is L0 (no API key needed).
2604
+ const auditAgent = (agentLabel(t.agent)||'agent').toLowerCase();
2605
+ const auditFiles = ((t.artifacts && t.artifacts.files) || []).length;
2606
+ toolCardsHtml += '<div class="audit-line allow">'
2607
+ + '<span class="seq">#0001</span>'
2608
+ + 'pre-prompt scan clean · routed to <b>'+auditAgent+'</b>'
2609
+ + (auditFiles ? ' · context: <b>'+auditFiles+' file'+(auditFiles===1?'':'s')+'</b>' : '')
2610
+ + ' · L0 hooks loaded · read-only by default · writes require approval'
2611
+ + '</div>';
2612
+ let auditSeq = 2;
2613
+ // Phased narrative · inject a chapter divider whenever the phase changes,
2614
+ // so the step timeline reads as research → build → ship → audit → live.
2615
+ let _curPhase = null;
2616
+ let _stepIdx = -1;
2617
+ for(const step of stepsArr){
2618
+ _stepIdx++;
2619
+ const _ph = _phaseSeq[_stepIdx];
2620
+ if(_ph !== _curPhase){
2621
+ toolCardsHtml += chapterCard(_curPhase, _ph);
2622
+ _curPhase = _ph;
2623
+ }
2624
+ const lbl=wrapCtxUrls(String(step.label||'').replace(/</g,'&lt;'));
2625
+ // Heuristic hook-badge derivation · steps don't carry hook state yet,
2626
+ // but failed steps clearly map to "blocked" or "error" semantics.
2627
+ let hookCls='allow', hookTxt='L0';
2628
+ if(step.state==='failed'){ hookCls='block'; hookTxt='BLOCKED'; }
2629
+ else if(step.state==='active'){ hookCls='clean'; hookTxt='RUN'; }
2630
+ const durTxt = step.completedAt && step.startedAt
2631
+ ? '<span class="dur">'+(Math.round((step.completedAt-step.startedAt)/100)/10)+'s</span>'
2632
+ : '';
2633
+ const outHtml = step.output ? '<div class="output tc-out">'+wrapCtxUrls(String(step.output).slice(0,400).replace(/</g,'&lt;'))+'</div>' : '';
2634
+ // Killer-moment block · the verifier-agent step gets accent border +
2635
+ // bigger type · it's the #1 demo claim (claim vs execution diff).
2636
+ const isVerifier = /verifier-agent|claim vs execution|claim vs reality/i.test(String(step.label||''));
2637
+ // Write-pass step · agent action awaiting human approval · render as
2638
+ // demo v12 .write-pass primitive (blue-accented inline card with target
2639
+ // + payload + actions row) instead of a generic tool-card. The step is
2640
+ // the one moment the human MUST act, so it earns its own primitive.
2641
+ const isWritePass = /^\\s*w-?pass\\b|^\\s*write-pass\\b/i.test(String(step.label||''));
2642
+ if(isWritePass){
2643
+ // Parse "W-PASS · Linear INFRA-247 status update · awaiting approval"
2644
+ // into the kicker / title / state pieces · fall back to raw label.
2645
+ const parts = String(step.label||'').split(/\\s*·\\s*/);
2646
+ const title = parts[1] || step.label || 'write awaits';
2647
+ const tail = parts.slice(2).join(' · ') || (step.state==='active' ? 'awaiting approval' : '');
2648
+ // Output line often contains "METHOD target · diff: old → new · ..."
2649
+ // Render it inside .payload so the diff coloring kicks in.
2650
+ const payloadRaw = step.output ? String(step.output) : '';
2651
+ const payloadHtml = payloadRaw
2652
+ .replace(/</g,'&lt;')
2653
+ .replace(/(in-progress|pending|todo|backlog)/g, '<span class="from">$1</span>')
2654
+ .replace(/(\\u2192|->|→)/g, '<span class="arrow">$1</span>')
2655
+ .replace(/(done|approved|merged|shipped|sent)/g, '<span class="val">$1</span>');
2656
+ toolCardsHtml += '<div class="write-pass">'
2657
+ + '<div class="head"><span>write-pass</span><span class="tail" style="color:var(--fg-50);letter-spacing:normal;text-transform:none">'+tail.replace(/</g,'&lt;')+'</span></div>'
2658
+ + '<h4>'+title.replace(/</g,'&lt;')+'</h4>'
2659
+ + (payloadHtml ? '<div class="payload">'+payloadHtml+'</div>' : '')
2660
+ + '<div class="actions">'
2661
+ + '<button class="primary" onclick="if(window.openWpModal)openWpModal()">Approve + send</button>'
2662
+ + '</div>'
2663
+ + '</div>';
2664
+ } else {
2665
+ const cardCls = isVerifier ? 'tc-card verifier-killer-step' : 'tc-card';
2666
+ // Active step · the agent is working on this RIGHT NOW. Render the
2667
+ // .thinking dots + a streaming .repl-beat (the demo's live-reasoning
2668
+ // primitive) instead of a static output block. SSE refetch swaps it
2669
+ // for the completed card when the step finishes. Flips the
2670
+ // runtime-emitter gap: thinking/repl-beat now render on a real active
2671
+ // step, not only on seeded data.
2672
+ let body;
2673
+ if(step.state==='active' && !isVerifier){
2674
+ const beat = step.output ? wrapCtxUrls(String(step.output).slice(0,300).replace(/</g,'&lt;')) : lbl;
2675
+ body = '<div class="thinking"><span class="dots"><span></span><span></span><span></span></span>working</div>'
2676
+ + '<div class="repl-beat">'+beat+'<span class="cursor"></span></div>';
2677
+ } else {
2678
+ body = isVerifier ? '<div class="verifier-killer-body">'+(step.output?String(step.output).slice(0,400).replace(/</g,'&lt;'):lbl)+'</div>' : outHtml;
2679
+ }
2680
+ toolCardsHtml += '<div class="'+cardCls+'">'
2681
+ + '<div class="head tc-head">'
2682
+ + (isVerifier ? '<span class="verifier-killer-title">✓ VERIFIER-AGENT · claim vs reality</span>' : '<span class="tool">'+lbl+'</span>')
2683
+ + '<span class="hook-badge '+hookCls+'">'+hookTxt+'</span>'
2684
+ + durTxt
2685
+ + '</div>'
2686
+ + body
2687
+ + '</div>';
2688
+ }
2689
+ // Audit-line after the verifier step · the verdict gets a chain entry
2690
+ // so the timeline shows the audit chain RECEIVED the claim/reality
2691
+ // diff, not just that the verifier ran. Pads the seq into a 4-digit
2692
+ // chain-entry id so it reads like a real audit hash.
2693
+ if(isVerifier){
2694
+ // Synthesized seq + hash for demo · transition plan (banked 2026-05-24):
2695
+ // when v0.7 verifier-agent writes REAL chain entries to AUDIT_FILE,
2696
+ // swap (t.sessionId).slice(0,8) for the actual sha256 prefix of the
2697
+ // chain entry that captured the verdict. Pull via /api/audit/chain
2698
+ // matched on verdict.sessionId + verdict.completedAt. The "a4e1"
2699
+ // suffix and "#NNNN" seq are placeholder until the real chain is
2700
+ // queryable from the UI.
2701
+ const seqId = '#' + String(40 + auditSeq).padStart(4,'0');
2702
+ auditSeq += 1;
2703
+ toolCardsHtml += '<div class="audit-line allow">'
2704
+ + '<span class="seq">'+seqId+'</span>'
2705
+ + 'verifier-agent verdict captured · <b>chain entry final</b> · sha256:'
2706
+ + '<code>'+(t.sessionId||'').slice(0,8).padEnd(8,'0')+'...'+'a4e1</code>'
2707
+ + ' · entry hashed + signed locally · no upload'
2708
+ + '</div>';
2709
+ }
2710
+ }
2711
+ toolCardsHtml += '</div>';
2712
+ }
2713
+
2714
+ // F-08 · token table · per-agent token + cost breakdown when artifacts.usage
2715
+ // is captured. Single row when one adapter ran, multi-row + total when
2716
+ // multiple agents handled this task (e.g., conductor → specialist).
2717
+ let tokenHtml='';
2718
+ const u = t.artifacts && t.artifacts.usage;
2719
+ if(u && (u.inputTokens || u.outputTokens || u.cacheReadTokens)){
2720
+ const fmt=n=>n>0?n.toLocaleString():'<span class="dim">-</span>';
2721
+ const inT=u.inputTokens||0, outT=u.outputTokens||0;
2722
+ const cr=u.cacheReadTokens||0, cc=u.cacheCreationTokens||0;
2723
+ const total=inT+outT+cr+cc;
2724
+ tokenHtml = '<div class="panel"><div class="panel-title">Tokens</div>'
2725
+ + '<table class="token-table"><thead><tr>'
2726
+ + '<th>agent</th><th>input</th><th>output</th><th>cache read</th><th>cache new</th><th>total</th>'
2727
+ + '</tr></thead><tbody>'
2728
+ + '<tr><td>'+(t.agent||'agent')+'</td>'
2729
+ + '<td>'+fmt(inT)+'</td>'
2730
+ + '<td>'+fmt(outT)+'</td>'
2731
+ + '<td>'+fmt(cr)+'</td>'
2732
+ + '<td>'+fmt(cc)+'</td>'
2733
+ + '<td>'+fmt(total)+'</td></tr>'
2734
+ + '</tbody></table></div>';
2735
+ }
2736
+
2737
+ // F-07 · verifier-agent panel (demo v12's #1 differentiator, finally visible).
2738
+ // Server runs verifyClaim() against (finalReport.summary, artifacts, steps)
2739
+ // and ships the result as t.verifyResult. We render the verdict + every
2740
+ // finding inline so the reviewer sees the gaps without leaving the page.
2741
+ // We only render for terminal sessions with a Final Report · running tasks
2742
+ // have nothing to verify yet, no-report sessions already say so in Outcome.
2743
+ let verifyHtml='';
2744
+ const vr=t.verifyResult;
2745
+ if(vr && isTerminal && hasReportText && vr.status!=='no-report'){
2746
+ const icon = vr.status==='verified'?'\\u2713'
2747
+ : vr.status==='broken'?'\\u2715'
2748
+ : '\\u26A0';
2749
+ const findingsHtml = (vr.findings||[]).map(function(f){
2750
+ // audit-line · demo v12's name for a row of audit-chain narrative
2751
+ return '<div class="finding audit-line">'
2752
+ + '<span class="finding-sev '+f.severity+'">'+f.severity+'</span>'
2753
+ + '<span class="finding-kind">['+f.kind+']</span>'
2754
+ + '<span class="finding-msg">'+String(f.message||'').replace(/</g,'&lt;')+'</span>'
2755
+ + '<div class="finding-detail"><span class="label">claim:</span>'+String(f.claim||'').replace(/</g,'&lt;')+'</div>'
2756
+ + '<div class="finding-detail"><span class="label">reality:</span>'+String(f.reality||'').replace(/</g,'&lt;')+'</div>'
2757
+ + '</div>';
2758
+ }).join('');
2759
+ if(vr.status==='verified'){
2760
+ // Demo v12 .verified-block · centered celebration with arrival anim.
2761
+ // Synthesize a stats line from artifacts when available so the card
2762
+ // shows numbers ("18/18 tests · 3 files · trust A") not just prose.
2763
+ const arts = t.artifacts || {};
2764
+ const tests = arts.tests || {};
2765
+ const files = arts.files || [];
2766
+ const tParts = [];
2767
+ if(tests.passed != null) tParts.push((tests.passed||0)+'/'+((tests.passed||0)+(tests.failed||0))+' tests');
2768
+ if(files.length) tParts.push(files.length+' file'+(files.length===1?'':'s'));
2769
+ if(files.length){
2770
+ const grades = files.map(function(f){
2771
+ if(typeof f.trust!=='number') return null;
2772
+ return f.trust>=85?'A':f.trust>=70?'B':f.trust>=50?'C':'F';
2773
+ }).filter(Boolean);
2774
+ if(grades.length) tParts.push('trust '+grades.join('/'));
2775
+ }
2776
+ const stats = tParts.join(' \\u00B7 ');
2777
+ const noGit = !!(t.artifacts && t.artifacts.noGitTracking);
2778
+ verifyHtml = '<div class="panel"><div class="panel-title">Verifier</div>'
2779
+ + (noGit
2780
+ // R8 honesty: in a non-git folder the verifier can't observe the
2781
+ // file changes, so a green "verified · claims match reality" would
2782
+ // overclaim. Soften to an honest "couldn't fully verify" verdict.
2783
+ ? '<div class="verifier-panel partial" data-verify-unit="verifier" data-verify-verdict="partial" data-verify-gittracked="false">'
2784
+ + '<div class="verifier-summary"><span class="v-icon">\\u26A0</span>'
2785
+ + 'Couldn\\u2019t fully verify \\u2014 not a git repo, so codehere couldn\\u2019t see the file changes. Nothing in the report contradicts what was recorded.'
2786
+ + '</div>'
2787
+ + '</div>'
2788
+ : '<div class="verified-block" data-verify-unit="verifier" data-verify-verdict="verified" data-verify-gittracked="'+(t.artifacts&&t.artifacts.noGitTracking?'false':'true')+'">'
2789
+ + '<div class="badge">\\u2713</div>'
2790
+ + '<div class="k">verified</div>'
2791
+ + '<div class="what">'+String(vr.summary||'Claims match the recorded reality').replace(/</g,'&lt;')+'</div>'
2792
+ + (stats ? '<div class="stats">'+stats+'</div>' : '')
2793
+ + '</div>')
2794
+ + '</div>';
2795
+ } else {
2796
+ const blockClass = vr.status==='broken' ? 'gotcha' : 'partial';
2797
+ verifyHtml = '<div class="panel"><div class="panel-title">Verifier</div>'
2798
+ + '<div class="verifier-panel '+vr.status+' '+blockClass+'" data-verify-unit="verifier" data-verify-verdict="'+vr.status+'" data-verify-gittracked="'+(t.artifacts&&t.artifacts.noGitTracking?'false':'true')+'">'
2799
+ + '<div class="verifier-summary"><span class="v-icon">'+icon+'</span>'
2800
+ + String(vr.summary||'').replace(/</g,'&lt;')
2801
+ + '</div>'
2802
+ + findingsHtml
2803
+ + '</div>'
2804
+ + '</div>';
2805
+ }
2806
+ }
2807
+
2808
+ const tc=t.toolTrace||t.steps||[];
2809
+ // P10 · per-step hook badges in trace. Derive same heuristic as the
2810
+ // tool-card hook-badge code (line 1176+): block when step.state===failed
2811
+ // OR /blocked|denied|refused/, override when /override/, ctx when the
2812
+ // step touched a context-source URL, otherwise allow.
2813
+ function stepHookBadge(z){
2814
+ const out = String(z.output||z.label||'').toLowerCase();
2815
+ if(z.state==='failed' || /block|denied|refused/i.test(out)) return '<span class="hook-badge block">block</span>';
2816
+ if(/override/i.test(out)) return '<span class="hook-badge override">override</span>';
2817
+ if(/(linear|github|figma|drawio):\\/\\//i.test(out)) return '<span class="hook-badge ctx">ctx</span>';
2818
+ return '<span class="hook-badge allow">allow</span>';
2819
+ }
2820
+ const traceHtml=tc.length>0
2821
+ ?'<details class="collapsible"><summary>'+tc.length+' steps</summary>'
2822
+ +tc.map(z=>'<div class="tc">'
2823
+ + stepHookBadge(z)
2824
+ + '<div class="tc-label">'+(z.label||z.tool||'?')+'</div>'
2825
+ + (z.output?'<pre class="tc-output">'+String(z.output).slice(0,400)+'</pre>':'')
2826
+ + (z.durationMs?'<span class="tc-dur">'+z.durationMs+'ms</span>':'')
2827
+ + '</div>').join('')+'</details>'
2828
+ :'';
344
2829
 
345
- const inputDisabled=t.status!=='running'&&t.status!=='done'&&t.status!=='failed'&&t.status!=='crashed';
2830
+ // R11 — composer enable check uses RAW backend statuses (the st() mapper
2831
+ // converts 'completed'→'done' for display but t.status is still raw here).
2832
+ // Enable on every known state including terminal ones so the user can ask
2833
+ // "what happened?" / request changes; the server-side appendMessage endpoint
2834
+ // is the authority on whether the session can still accept input.
2835
+ const knownStatuses = ['running','queued','done','completed','paused','blocked','failed','error','crashed','killed'];
2836
+ const inputDisabled = !knownStatuses.includes(t.status);
346
2837
  const ta=document.getElementById('detail-ta'), btn=document.getElementById('detail-btn');
347
- if(ta){ta.disabled=inputDisabled;ta.placeholder=inputDisabled?'Follow-up unavailable for this state':'Request changes or ask a question...';}
2838
+ if(ta){
2839
+ ta.disabled=inputDisabled;
2840
+ if(inputDisabled) ta.placeholder='Follow-up unavailable for this state';
2841
+ else if(t.status==='running') ta.placeholder='Interrupt or ask a question...';
2842
+ else ta.placeholder='Ask a follow-up about this task...';
2843
+ }
348
2844
  if(btn)btn.disabled=inputDisabled;
349
2845
 
2846
+ // Failover-aware kicker · when a session crossed vendors (rate-limit
2847
+ // failover · context handoff), surface the lineage in the title kicker
2848
+ // so the cross-vendor wedge is visible at a glance. PHASE4 §H sharpened
2849
+ // moat: "cross-vendor failover with context handoff" is THE unique
2850
+ // differentiator. Show it.
2851
+ let kicker;
2852
+ if(t.failoverFrom && t.failoverTo){
2853
+ const from=(agentLabel(t.failoverFrom)||t.failoverFrom).toUpperCase();
2854
+ const to=(agentLabel(t.failoverTo)||t.failoverTo).toUpperCase();
2855
+ const reason=t.failoverReason ? ' \\u00B7 ' + String(t.failoverReason).replace(/_/g,' ').toUpperCase() : '';
2856
+ kicker=from+' \\u2192 '+to+reason+' \\u00B7 '+s.toUpperCase();
2857
+ } else {
2858
+ kicker=(agentLabel(t.agent)||'task').toUpperCase()+' \\u00B7 '+s.toUpperCase();
2859
+ }
2860
+ // Artifacts → side drawer (task #51): accessible via a header trigger +
2861
+ // openable on demand, not stacked inline. Reuses renderArtifacts content.
2862
+ var _artStruct=(t.artifacts&&t.artifacts.structured)||[];
2863
+ var _artHtml=renderArtifacts(_artStruct);
2864
+ var _artN=_artStruct.length;
2865
+ var _artTrig=_artN?' \\u00B7 <span class="art-trig" onclick="openArtD()">\\uD83D\\uDCCE '+_artN+' artifact'+(_artN===1?'':'s')+'</span>':'';
2866
+ var _artDrawer=_artHtml?'<div class="art-scrim" id="artScrim" onclick="closeArtD()"></div>'
2867
+ +'<div class="art-modal" id="artDrawer" role="dialog" aria-label="Artifacts"><div class="art-modal-h"><span>Artifacts \\u00B7 '+_artN+'</span><span class="x" onclick="closeArtD()">\\u2715</span></div>'
2868
+ +'<div class="art-modal-body">'+_artHtml+'</div></div>':'';
350
2869
  document.getElementById('detail-view').innerHTML='<div class="detail-header">'
351
2870
  +'<span class="back-btn" onclick="hideD()">\\u2190 Back</span>'
352
- +'<span class="detail-title">'+title(t.prompt)+'</span>'
353
- +'<span class="card-agent" style="margin-left:auto">'+(t.agent||'?')+'</span>'
2871
+ +'<div class="detail-title-wrap">'
2872
+ +'<div class="detail-kicker">'+kicker+'</div>'
2873
+ +'<h1 class="detail-title">'+title(t.prompt)+'</h1>'
2874
+ +'</div>'
2875
+ +'<span class="card-agent" style="margin-top:14px">'+ab(t.agent,'sm')+agentLabel(t.agent)+'</span>'
354
2876
  +'</div>'
355
- +'<div class="detail-meta"><span class="status-icon '+s+'"></span> '+s+' \\u00B7 '+d+(co!==null?' \\u00B7 $'+(co/100).toFixed(2):'')+'</div>'
356
- +oHtml+convHtml+traceHtml;
2877
+ +'<div class="detail-meta" data-verify-unit="detail" data-verify-status="'+s+'" data-verify-terminal-fail="'+(isFailed?'true':'false')+'" data-verify-has-explanation="'+(!!(blockHtml||crashHtml||recoveryHtml||pushHtml||t.stalledReason)?'true':'false')+'"><span class="status-icon '+s+'"></span> '+s+' \\u00B7 '+d+(co!==null?' \\u00B7 '+costPill(co):'')+(steps.length?' \\u00B7 '+steps.length+' steps':'')+_artTrig+'</div>'
2878
+ // Demo v12 hierarchy: state alerts → outcome → the multi-speaker
2879
+ // CONVERSATION (the thread spine · me + team + agents · all personas) →
2880
+ // the phased story → artifacts → synthesis → verifier → eng detail.
2881
+ // Persona visibility: pm/designer see outcome + conversation + artifacts +
2882
+ // summary + verified; the eng-detail panels (timeline, tokens, trace) are
2883
+ // p-eng.
2884
+ // Lead with delivery (step 2a of the timeline restructure · task #51):
2885
+ // the Final Report is the headline deliverable, so it sits right after the
2886
+ // outcome — not buried below the conversation, tool log, and artifacts as
2887
+ // it was. The full timeline merge (folding tool activity into convHtml +
2888
+ // artifacts drawer) follows; this reorder is the safe, isolated first win.
2889
+ +blockHtml+pushHtml+crashHtml+recoveryHtml+oHtml
2890
+ +summHtml
2891
+ // Unified timeline (task #51 · step 2b): the conversation IS the spine and
2892
+ // the tool activity is woven into it as inline collapsed events grouped by
2893
+ // emergent sections — replacing the old separate Conversation panel + the
2894
+ // "Activity · phased" tool-card panel.
2895
+ +renderTimeline(t, wrapCtxUrls)
2896
+ +chipsHtml+gitNoteHtml+verifyHtml
2897
+ +'<div class="p-eng">'+tokenHtml+traceHtml+'</div>'
2898
+ +_artDrawer;
2899
+ }
2900
+
2901
+ // E-03 · user override · reveal a reason input, then POST a real audit entry.
2902
+ // Replaces the old al('Override flow goes to v0.8') lie with a real hash-chain
2903
+ // append. The returned shortHash is the REAL chain tip, not a mockup.
2904
+ function overrideBlock(sessionId, toolName){
2905
+ const actions=document.getElementById('hook-block-actions');
2906
+ if(!actions||actions.dataset.open==='1')return;
2907
+ actions.dataset.open='1';
2908
+ actions.innerHTML='<input id="override-reason" placeholder="Why are you overriding this block?" style="flex:1;background:var(--bg-2,#1a1a1f);border:1px solid var(--border);border-radius:5px;color:var(--fg);padding:6px 8px;font-size:11.5px"/>'
2909
+ +'<button id="override-submit">Log + proceed</button>'
2910
+ +'<button id="override-cancel">Cancel</button>';
2911
+ const inp=document.getElementById('override-reason'); if(inp)inp.focus();
2912
+ document.getElementById('override-cancel').onclick=()=>{ if(_detail)showD(_detail); };
2913
+ document.getElementById('override-submit').onclick=async()=>{
2914
+ const reason=(document.getElementById('override-reason')||{}).value||'';
2915
+ if(!reason.trim()){ al('A reason is required to log an override'); return; }
2916
+ try{
2917
+ const r=await fetch('/api/audit/override',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId,toolName,reason})}).then(x=>x.json());
2918
+ const res=document.getElementById('override-result');
2919
+ if(r&&r.ok){
2920
+ actions.innerHTML='';
2921
+ if(res)res.innerHTML='<span class="ok">\\u2713</span> override logged · chain continues <code>'+(r.shortHash||('#'+r.seq))+'</code>';
2922
+ } else { al('Override failed: '+((r&&r.error)||'unknown')); }
2923
+ }catch(e){ al('Network error: '+e.message); }
2924
+ };
357
2925
  }
358
2926
 
359
2927
  async function a(action, id) {
2928
+ // Only 'kill' is a real, wired action today. Pause/resume/restart were
2929
+ // unwired stubs with no callers — removed (production-grade · no dead
2930
+ // "not yet wired" affordances). Re-add when real agent-process control lands.
360
2931
  if(action==='kill'){ try{await fetch('/api/task/'+id+'/kill',{method:'POST'});fetchAll();}catch(e){al('Failed: '+e.message);} return; }
361
- if(action==='pause'){ al('Pause not yet wired to agent API'); return; }
362
- if(action==='resume'){ al('Resume not yet wired to agent API'); return; }
363
- if(action==='restart'){ al('Restart not yet wired'); return; }
364
2932
  }
365
2933
 
366
- function vfy(id) {
367
- const s=allS(_cached.tasks||{}), t=s.find(x=>x.sessionId===id);
368
- if(!t)return;
369
- t.verification='verified';
370
- render();
2934
+ // v0.8 S2.5 · Context Sources panel · lives inside the settings drawer
2935
+ function csIconHtml(id){
2936
+ const initials=id==='linear'?'L':id==='figma'?'F':id.slice(0,1).toUpperCase();
2937
+ return '<span class="cs-icon '+id+'">'+initials+'</span>';
2938
+ }
2939
+ function csRelativeTime(ts){
2940
+ if(!ts) return '';
2941
+ const delta=Date.now()-ts;
2942
+ const m=Math.floor(delta/60000); if(m<60)return m+'m ago';
2943
+ const h=Math.floor(m/60); if(h<24)return h+'h ago';
2944
+ return Math.floor(h/24)+'d ago';
2945
+ }
2946
+ function csDescription(id){
2947
+ // Fix D6 (2026-05-23) · {placeholder} syntax avoids browser eating
2948
+ // <angle-bracket-tags> · the description renders correctly + agents
2949
+ // recognize the convention from man-page conventions.
2950
+ if(id==='linear') return 'Read Linear issues by ID. Agent can pull title, status, assignee, description, comments.';
2951
+ if(id==='figma') return 'Read Figma files or single nodes. Agent gets the tree, dimensions, and text content.';
2952
+ if(id==='drawio') return 'Read draw.io diagrams (XML). Drop drawio://gh/{owner}/{repo}/{path} into a prompt.';
2953
+ if(id==='github') return 'Read public GitHub issues + PRs. Drop github://{owner}/{repo}/issues/{n} into a prompt.';
2954
+ return '';
2955
+ }
2956
+ function csExampleUrl(id){
2957
+ if(id==='linear') return 'linear://INFRA-247';
2958
+ if(id==='figma') return 'figma://file/abc123';
2959
+ if(id==='drawio') return 'drawio://gh/owner/repo/docs/arch.drawio';
2960
+ if(id==='github') return 'github://owner/repo/issues/42';
2961
+ return '';
2962
+ }
2963
+ async function loadContextSources(){
2964
+ const list=document.getElementById('cs-list');
2965
+ if(!list)return;
2966
+ try{
2967
+ const r=await fetch('/api/context-sources').then(x=>x.json());
2968
+ const items=(r&&r.providers)||[];
2969
+ if(!items.length){list.innerHTML='<div style="color:var(--fg-40); font-size:11px; padding: 8px 0">no providers registered</div>'; return;}
2970
+ list.innerHTML=items.map(function(p){
2971
+ const statusChip='<span class="cs-status '+p.status+'">'+p.status+'</span>';
2972
+ let meta='';
2973
+ if((p.status==='connected'||p.status==='expired') && p.userLabel){
2974
+ meta='<span class="cs-meta">· '+String(p.userLabel).replace(/</g,'&lt;');
2975
+ if(p.connectedAt) meta+=' · '+csRelativeTime(p.connectedAt);
2976
+ meta+='</span>';
2977
+ } else if(p.status==='manual'){
2978
+ meta='<span class="cs-meta">· apiKey set</span>';
2979
+ }
2980
+ let actions='';
2981
+ let belowDesc='';
2982
+ if(p.authType==='public'){
2983
+ belowDesc='<div style="margin-top:6px;"><span class="ctx-url-chip" data-url="'+csExampleUrl(p.id)+'" title="click to preview · or copy + paste into a prompt">'+csExampleUrl(p.id)+'</span></div>';
2984
+ // Fix D5/D6 (2026-05-23) · for public adapters, render the example
2985
+ // URL on its OWN line BELOW the description (via belowDesc) so the
2986
+ // description column doesn't collapse to a narrow strip when the
2987
+ // URL chip takes a wide actions slot. No actions slot needed —
2988
+ // Connect doesn't apply for public adapters.
2989
+ actions='';
2990
+ } else if(p.status==='connected'){
2991
+ actions='<button class="cs-btn" onclick="testCS(\\''+p.id+'\\')">Test</button>'
2992
+ +'<button class="cs-btn danger" onclick="disconnectCS(\\''+p.id+'\\', this)">Disconnect</button>';
2993
+ } else if(p.status==='expired'){
2994
+ actions=p.clientIdConfigured
2995
+ ? '<button class="cs-btn primary" onclick="connectCS(\\''+p.id+'\\')">Reconnect</button>'
2996
+ : '<button class="cs-btn" disabled title="set CODEHERE_'+p.id.toUpperCase()+'_CLIENT_ID">Reconnect</button>';
2997
+ } else if(p.status==='manual'){
2998
+ actions=p.clientIdConfigured
2999
+ ? '<button class="cs-btn primary" onclick="connectCS(\\''+p.id+'\\')">Switch to OAuth</button>'
3000
+ : '<button class="cs-btn" disabled>Connect</button>';
3001
+ } else {
3002
+ actions=p.clientIdConfigured
3003
+ ? '<button class="cs-btn primary" onclick="connectCS(\\''+p.id+'\\')">Connect</button>'
3004
+ : '<button class="cs-btn" disabled title="set CODEHERE_'+p.id.toUpperCase()+'_CLIENT_ID">Connect</button>';
3005
+ }
3006
+ const hint=!p.clientIdConfigured && p.status==='disconnected'
3007
+ ? '<div class="cs-empty-hint">Set <code style="font-family:var(--mono); color:var(--warn)">CODEHERE_'+p.id.toUpperCase()+'_CLIENT_ID</code> in the daemon\\'s env to enable click-to-connect. Or paste a manual key into <code style="font-family:var(--mono)">~/.codehere/context-sources.json</code>.</div>'
3008
+ : '';
3009
+ return '<div class="cs-row" id="cs-row-'+p.id+'">'
3010
+ + csIconHtml(p.id)
3011
+ + '<div style="display:flex; flex-direction:column; min-width:0; flex:1;">'
3012
+ + '<div style="display:flex; align-items:center; gap:6px;"><span class="cs-label">'+p.label+'</span>'+statusChip+meta+'</div>'
3013
+ + '<div style="font-size:11px; color:var(--fg-50); margin-top:3px; line-height:1.5;">'+csDescription(p.id)+'</div>'
3014
+ + belowDesc
3015
+ + '</div>'
3016
+ + (actions ? '<div class="cs-actions">'+actions+'</div>' : '')
3017
+ + '</div>'
3018
+ + hint
3019
+ + '<div id="cs-test-'+p.id+'"></div>';
3020
+ }).join('');
3021
+ }catch(e){
3022
+ list.innerHTML='<div style="color:var(--block); font-size:11px; padding:8px 0">failed to load: '+(e&&e.message?e.message:String(e))+'</div>';
3023
+ }
3024
+ }
3025
+ async function connectCS(provider){
3026
+ try{
3027
+ const r=await fetch('/api/context-sources/'+encodeURIComponent(provider)+'/connect',{method:'POST'});
3028
+ const data=await r.json();
3029
+ if(!r.ok){
3030
+ if(data.error==='provider_not_registered'){
3031
+ al(provider+' OAuth app not registered. Set '+data.envVar+' and restart the daemon.');
3032
+ } else {
3033
+ al('connect failed: '+(data.error||r.status));
3034
+ }
3035
+ return;
3036
+ }
3037
+ if(data.authorizeUrl){
3038
+ window.open(data.authorizeUrl,'_blank');
3039
+ let poll=setInterval(loadContextSources, 2000);
3040
+ setTimeout(()=>clearInterval(poll), 120000);
3041
+ }
3042
+ }catch(e){
3043
+ al('connect failed: '+(e&&e.message?e.message:String(e)));
3044
+ }
3045
+ }
3046
+ async function disconnectCS(provider, btnEl){
3047
+ // Inline confirmation · replace the button with a confirm/cancel pair
3048
+ if(btnEl && !btnEl.dataset.confirming){
3049
+ const orig=btnEl.outerHTML;
3050
+ btnEl.outerHTML='<span style="font-size:10px; color:var(--fg-50); margin-right:6px;">disconnect?</span><button class="cs-btn danger" id="cs-confirm-'+provider+'">yes</button><button class="cs-btn" id="cs-cancel-'+provider+'">cancel</button>';
3051
+ const yes=document.getElementById('cs-confirm-'+provider);
3052
+ const no=document.getElementById('cs-cancel-'+provider);
3053
+ if(yes) yes.onclick=async()=>{
3054
+ await fetch('/api/context-sources/'+encodeURIComponent(provider)+'/disconnect',{method:'POST'});
3055
+ loadContextSources();
3056
+ };
3057
+ if(no) no.onclick=()=>{loadContextSources();};
3058
+ return;
3059
+ }
3060
+ }
3061
+ async function testCS(provider){
3062
+ const slot=document.getElementById('cs-test-'+provider);
3063
+ if(slot) slot.innerHTML='<div class="cs-test-result" style="color:var(--fg-50); background: rgba(255,255,255,0.02)">testing...</div>';
3064
+ try{
3065
+ const r=await fetch('/api/context-sources/'+encodeURIComponent(provider)+'/test',{method:'POST'});
3066
+ const data=await r.json();
3067
+ if(slot){
3068
+ if(data.ok){
3069
+ slot.innerHTML='<div class="cs-test-result ok">&check; reachable · '+(data.who||'authenticated')+'</div>';
3070
+ } else {
3071
+ slot.innerHTML='<div class="cs-test-result fail">&times; failed: '+(data.error||'unknown')+'</div>';
3072
+ }
3073
+ setTimeout(()=>{ if(slot) slot.innerHTML=''; }, 6000);
3074
+ }
3075
+ }catch(e){
3076
+ if(slot) slot.innerHTML='<div class="cs-test-result fail">&times; '+(e&&e.message?e.message:String(e))+'</div>';
3077
+ }
3078
+ }
3079
+ function openDrawer(){
3080
+ document.getElementById('settings-drawer')?.classList.add('open');
3081
+ document.getElementById('drawer-overlay')?.classList.add('open');
3082
+ loadContextSources();
3083
+ }
3084
+ function closeDrawer(){
3085
+ document.getElementById('settings-drawer')?.classList.remove('open');
3086
+ document.getElementById('drawer-overlay')?.classList.remove('open');
3087
+ }
3088
+
3089
+ // v0.8 S3 · Reshape view · port of demo v12 lines 2486-2534.
3090
+ // Body-class-driven view modifier. Three preset shapes + a free-text
3091
+ // classifier that maps to the closest preset. Persists the active shape
3092
+ // to localStorage so the user's choice survives a reload.
3093
+ const SHAPE_CLASSES=['shape-ship-only','shape-velocity','shape-security'];
3094
+ const SHAPE_CONFIGS={
3095
+ 'ship-only': 'density: standard · hide: tc, audit, code · expand: final-report, file-chips',
3096
+ 'velocity': 'density: compact · hide: tc-outputs, audit, code · expand: outcome, duration',
3097
+ 'security': 'density: standard · amplify: audit, hook-block, trust · hide: prose, repl-beats',
3098
+ };
3099
+ function openReshape(){
3100
+ const m=document.getElementById('reshape-modal');
3101
+ const ac=document.getElementById('agent-compose');
3102
+ const inp=document.getElementById('reshape-input');
3103
+ m?.classList.add('open');
3104
+ ac?.classList.remove('show');
3105
+ if(inp){ inp.value=''; setTimeout(()=>inp.focus(), 100); }
3106
+ }
3107
+ function closeReshape(){
3108
+ document.getElementById('reshape-modal')?.classList.remove('open');
3109
+ }
3110
+ function applyShape(shape, promptText, configText){
3111
+ // Clear existing
3112
+ SHAPE_CLASSES.forEach(s=>document.body.classList.remove(s));
3113
+ document.body.classList.add('shape-'+shape);
3114
+ document.body.classList.add('custom-view');
3115
+ // Update banner with prompt
3116
+ const bp=document.getElementById('custom-view-prompt');
3117
+ if(bp) bp.textContent='"'+promptText+'"';
3118
+ // Animate the compose preview
3119
+ const echo=document.getElementById('compose-prompt-echo');
3120
+ const cfg=document.getElementById('compose-config');
3121
+ if(echo) echo.textContent=promptText;
3122
+ if(cfg) cfg.textContent=configText;
3123
+ document.getElementById('agent-compose')?.classList.add('show');
3124
+ // Persist
3125
+ try{ localStorage.setItem('codehere_shape', JSON.stringify({shape, promptText, configText})); }catch{}
3126
+ // Close modal after compose animation finishes
3127
+ setTimeout(()=>closeReshape(), 2600);
3128
+ }
3129
+ function resetShape(){
3130
+ SHAPE_CLASSES.forEach(s=>document.body.classList.remove(s));
3131
+ document.body.classList.remove('custom-view');
3132
+ try{ localStorage.removeItem('codehere_shape'); }catch{}
3133
+ }
3134
+ function classifyShape(text){
3135
+ const lower=text.toLowerCase();
3136
+ if(/shipped|final|cost|outcome/.test(lower)) return 'ship-only';
3137
+ if(/security|audit|trust|block|hook/.test(lower)) return 'security';
3138
+ if(/velocity|duration|fast|quick|time/.test(lower)) return 'velocity';
3139
+ return 'velocity';
3140
+ }
3141
+ function restoreShape(){
3142
+ try{
3143
+ const raw=localStorage.getItem('codehere_shape');
3144
+ if(!raw)return;
3145
+ const {shape, promptText, configText}=JSON.parse(raw);
3146
+ if(shape && promptText){
3147
+ SHAPE_CLASSES.forEach(s=>document.body.classList.remove(s));
3148
+ document.body.classList.add('shape-'+shape);
3149
+ document.body.classList.add('custom-view');
3150
+ const bp=document.getElementById('custom-view-prompt');
3151
+ if(bp) bp.textContent='"'+promptText+'"';
3152
+ }
3153
+ }catch{}
3154
+ }
3155
+
3156
+ // F-06 · audit chain verification · click handler for the dashboard panel.
3157
+ async function verifyAuditChain(){
3158
+ const btn=document.getElementById('audit-verify-btn');
3159
+ const res=document.getElementById('audit-result');
3160
+ if(!btn||!res)return;
3161
+ btn.textContent='Verifying...';
3162
+ btn.className='audit-chain-btn';
3163
+ try{
3164
+ const r=await fetch('/api/audit/verify').then(x=>x.json());
3165
+ if(r.error){
3166
+ res.innerHTML='<span class="fail">Failed: '+String(r.error).replace(/</g,'&lt;')+'</span>';
3167
+ btn.textContent='Verify chain'; return;
3168
+ }
3169
+ const counts='<div class="audit-chain-counts">'+r.totalEntries+' entries &middot; '+r.chainedEntries+' hash-chained &middot; '+r.legacyEntries+' legacy</div>';
3170
+ if(r.valid){
3171
+ res.innerHTML='<span class="ok">&check; Chain intact &middot; every entry verified against its predecessor hash.</span>'+counts;
3172
+ btn.textContent='Verified';
3173
+ btn.className='audit-chain-btn verified';
3174
+ } else {
3175
+ const errs=(r.errors||[]).slice(0,5).map(function(e){
3176
+ return '<div class="audit-chain-error">line '+e.lineNumber+' &middot; '+e.reason+' &middot; '+String(e.detail||'').replace(/</g,'&lt;')+'</div>';
3177
+ }).join('');
3178
+ res.innerHTML='<span class="fail">&times; Chain BROKEN &middot; '+(r.errors||[]).length+' break point(s) detected.</span>'+counts+errs;
3179
+ btn.textContent='Re-verify';
3180
+ btn.className='audit-chain-btn broken';
3181
+ }
3182
+ }catch(e){
3183
+ res.innerHTML='<span class="fail">Failed to reach /api/audit/verify: '+(e&&e.message?e.message:String(e))+'</span>';
3184
+ btn.textContent='Verify chain';
3185
+ }
371
3186
  }
372
3187
 
373
3188
  function al(m){const e=document.getElementById('toast');if(e){e.textContent=m;e.style.display='block';setTimeout(()=>{e.style.display='none';},3000);}}
@@ -380,6 +3195,10 @@ async function fetchAll() {
380
3195
  if(t.pricing)_cached.pricing=t.pricing;
381
3196
  render();
382
3197
  }catch(e){console.error('fetch error',e);}
3198
+ try {
3199
+ const h=await fetch('/api/host-sessions').then(x=>x.json());
3200
+ renderHostSessions(h);
3201
+ }catch{}
383
3202
  try {
384
3203
  if(!_cached.pricing){
385
3204
  const p=await fetch('/api/spend/today').then(x=>x.json());
@@ -388,17 +3207,27 @@ async function fetchAll() {
388
3207
  }catch{}
389
3208
  }
390
3209
 
391
- async function submit(ta,btn){
3210
+ let _submitting=false;
3211
+ async function submit(ta,btn,taskId){
3212
+ if(_submitting)return;
392
3213
  const p=ta.value.trim();
393
3214
  if(!p)return;
3215
+ _submitting=true;
394
3216
  btn.disabled=true;ta.disabled=true;
395
3217
  try {
396
- const r=await fetch('/api/task',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt:p})});
397
- if(!r.ok){const e=await r.json();al('Failed: '+(e.error||'unknown'));}else{
398
- ta.value='';btn.textContent='\\u2713';setTimeout(()=>{btn.textContent='\\u2191';btn.disabled=false;ta.disabled=false;ta.focus();},1200);
3218
+ // When called from detail view (taskId set), route to follow-up endpoint
3219
+ // so the message goes to the SAME session that ran the original work.
3220
+ // Without this, every "ask follow-up" silently spawned a brand-new task,
3221
+ // which is the disabled-composer bug's mirror image: composer enabled but
3222
+ // routed wrong.
3223
+ const url = taskId ? '/api/task/'+taskId+'/message' : '/api/task';
3224
+ const body = taskId ? JSON.stringify({text:p}) : JSON.stringify({prompt:p});
3225
+ const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body});
3226
+ if(!r.ok){const e=await r.json();al('Failed: '+(e.error||'unknown'));btn.disabled=false;ta.disabled=false;_submitting=false;}else{
3227
+ ta.value='';btn.textContent='\\u2713';setTimeout(()=>{btn.textContent='\\u2191';btn.disabled=false;ta.disabled=false;_submitting=false;ta.focus();},1200);
399
3228
  fetchAll();
400
3229
  }
401
- }catch(e){al('Network error: '+e.message);btn.disabled=false;ta.disabled=false;}
3230
+ }catch(e){al('Network error: '+e.message);btn.disabled=false;ta.disabled=false;_submitting=false;}
402
3231
  }
403
3232
 
404
3233
  function offlineMode(v){
@@ -407,7 +3236,560 @@ function offlineMode(v){
407
3236
  if(b)b.style.display=v?'block':'none';
408
3237
  }
409
3238
 
3239
+ let _healthFails=0;
3240
+ async function pollHealth(){
3241
+ try {
3242
+ const r=await fetch('/health',{cache:'no-store'});
3243
+ if(r.ok){if(_healthFails>0){offlineMode(false);fetchAll();}_healthFails=0;}
3244
+ else {_healthFails++;if(_healthFails>=2)offlineMode(true);}
3245
+ } catch {_healthFails++;if(_healthFails>=2)offlineMode(true);}
3246
+ }
3247
+
410
3248
  document.addEventListener('DOMContentLoaded',()=>{
3249
+ // A-10 · populate the OSS footer · version from /api/version
3250
+ fetch('/api/version').then(r=>r.json()).then(v=>{
3251
+ const ve=document.getElementById('oss-version');
3252
+ if(ve && v && v.version) ve.textContent='v'+v.version;
3253
+ }).catch(()=>{});
3254
+ // v0.8 S2.5 · settings drawer wire-up · cog opens, X / overlay closes, Esc closes
3255
+ document.getElementById('settings-open')?.addEventListener('click', openDrawer);
3256
+ document.getElementById('settings-close')?.addEventListener('click', closeDrawer);
3257
+ document.getElementById('drawer-overlay')?.addEventListener('click', closeDrawer);
3258
+
3259
+ // Persona view · "show only what matters to your role" (demo v12 header).
3260
+ // Sets body[data-persona] which CSS uses to hide eng-detail panels for
3261
+ // pm/designer. Persisted so the role sticks across reloads.
3262
+ const _pSel=document.getElementById('persona-select');
3263
+ if(_pSel){
3264
+ let saved='all'; try{ saved=localStorage.getItem('codehere-persona')||'all'; }catch{}
3265
+ _pSel.value=saved; document.body.setAttribute('data-persona',saved);
3266
+ _pSel.addEventListener('change',()=>{
3267
+ const v=_pSel.value||'all';
3268
+ document.body.setAttribute('data-persona',v);
3269
+ try{ localStorage.setItem('codehere-persona',v); }catch{}
3270
+ });
3271
+ }
3272
+
3273
+ // Density toggle · compact / standard · persisted (demo W8)
3274
+ const _dT=document.getElementById('density-toggle');
3275
+ if(_dT){
3276
+ let dc='0'; try{ dc=localStorage.getItem('codehere-density-compact')||'0'; }catch{}
3277
+ const applyD=(on)=>{ document.body.classList.toggle('density-compact',on); _dT.classList.toggle('on',on); _dT.innerHTML=on?'\\u229E Standard':'\\u229E Compact'; };
3278
+ applyD(dc==='1');
3279
+ _dT.addEventListener('click',()=>{ const on=!document.body.classList.contains('density-compact'); applyD(on); try{ localStorage.setItem('codehere-density-compact',on?'1':'0'); }catch{} });
3280
+ }
3281
+
3282
+ // v0.8 S3 · Reshape modal wire-up · button opens, X / outside-click / Esc closes
3283
+ document.getElementById('reshape-open')?.addEventListener('click', openReshape);
3284
+ document.getElementById('reshape-close')?.addEventListener('click', closeReshape);
3285
+ document.getElementById('reshape-modal')?.addEventListener('click', e=>{
3286
+ if(e.target===document.getElementById('reshape-modal')) closeReshape();
3287
+ });
3288
+ document.querySelectorAll('.reshape-suggestions .pill').forEach(b=>{
3289
+ b.addEventListener('click', ()=>{
3290
+ const shape=b.dataset.shape;
3291
+ const prompt=b.dataset.prompt;
3292
+ const cfg=SHAPE_CONFIGS[shape] || 'density: standard';
3293
+ applyShape(shape, prompt, cfg);
3294
+ });
3295
+ });
3296
+ document.getElementById('reshape-submit')?.addEventListener('click', ()=>{
3297
+ const inp=document.getElementById('reshape-input');
3298
+ const txt=(inp?.value||'').trim();
3299
+ if(!txt) return;
3300
+ const shape=classifyShape(txt);
3301
+ applyShape(shape, txt, 'density: standard · agent-composed from prompt');
3302
+ });
3303
+ document.getElementById('reshape-input')?.addEventListener('keydown', e=>{
3304
+ if(e.key==='Enter') document.getElementById('reshape-submit')?.click();
3305
+ });
3306
+ // Restore last shape on load
3307
+ restoreShape();
3308
+
3309
+ /* v0.8 S4 · share-thread modal wire-up */
3310
+ let _shareBundle = null;
3311
+ let _shareLink = '';
3312
+ const shareModal = document.getElementById('share-modal');
3313
+ const openShare = async ()=>{
3314
+ // Fix D10 (2026-05-23) · Two bugs in one:
3315
+ // (a) _detail is a STRING sessionId, not an object · _detail.sessionId
3316
+ // would have been undefined even when a session WAS selected.
3317
+ // (b) No session selected previously threw a useless toast · now we
3318
+ // auto-pick the most-recent completed session. The user can still
3319
+ // open the modal from any task by clicking it first.
3320
+ let sessionId = _detail;
3321
+ if(!sessionId){
3322
+ const all = allS(_cached.tasks||{});
3323
+ const done = all.filter(x=>x.status==='completed'||x.status==='done');
3324
+ if(!done.length){
3325
+ flashToast('No completed sessions yet · run a task first');
3326
+ return;
3327
+ }
3328
+ // Most recent done (sort by completedAt desc)
3329
+ done.sort((a,b)=>(b.completedAt||0)-(a.completedAt||0));
3330
+ sessionId = done[0].sessionId;
3331
+ }
3332
+ shareModal.classList.add('open');
3333
+ document.getElementById('share-link-id').textContent = '…fetching…';
3334
+ document.getElementById('share-link-fp').textContent = '…';
3335
+ try{
3336
+ const r = await fetch('/api/thread/' + encodeURIComponent(sessionId) + '/export', { method:'POST' });
3337
+ if(!r.ok) throw new Error('export failed · HTTP ' + r.status);
3338
+ const j = await r.json();
3339
+ _shareBundle = j.bundle;
3340
+ _shareLink = j.link;
3341
+ document.getElementById('share-link-id').textContent = (j.bundle?.body?.sessionId || sessionId || '').slice(0,12);
3342
+ document.getElementById('share-link-fp').textContent = (j.fingerprint || '').replace('...', '…');
3343
+ }catch(err){
3344
+ document.getElementById('share-link-id').textContent = 'error';
3345
+ document.getElementById('share-link-fp').textContent = (err && err.message) || 'see console';
3346
+ console.error('share export failed', err);
3347
+ }
3348
+ };
3349
+ const closeShare = ()=>shareModal.classList.remove('open');
3350
+ document.getElementById('share-open')?.addEventListener('click', openShare);
3351
+ document.getElementById('share-close')?.addEventListener('click', closeShare);
3352
+ shareModal?.addEventListener('click', e=>{ if(e.target===shareModal) closeShare(); });
3353
+ document.getElementById('share-copy-link')?.addEventListener('click', async ()=>{
3354
+ if(!_shareLink) return;
3355
+ try{ await navigator.clipboard.writeText(_shareLink); flashToast('Link copied'); }
3356
+ catch{ flashToast('Could not copy · try the .json download'); }
3357
+ });
3358
+ document.getElementById('share-download')?.addEventListener('click', ()=>{
3359
+ if(!_shareBundle) return;
3360
+ const blob = new Blob([JSON.stringify(_shareBundle, null, 2)], { type:'application/json' });
3361
+ const url = URL.createObjectURL(blob);
3362
+ const a = document.createElement('a');
3363
+ a.href = url;
3364
+ a.download = (_shareBundle.body?.sessionId?.slice(0,12) || 'thread') + '.codehere.json';
3365
+ document.body.appendChild(a); a.click(); a.remove();
3366
+ setTimeout(()=>URL.revokeObjectURL(url), 1000);
3367
+ });
3368
+ function flashToast(msg){
3369
+ const t = document.getElementById('share-toast');
3370
+ if(!t) return;
3371
+ t.textContent = msg; t.classList.add('show');
3372
+ setTimeout(()=>t.classList.remove('show'), 1600);
3373
+ }
3374
+
3375
+ /* v0.9 S7 · overlay-renderer · inspect-and-act */
3376
+ const orOverlay = document.getElementById('or-overlay');
3377
+ const orBody = document.getElementById('or-body');
3378
+ function orClose(){ orOverlay.classList.remove('open'); }
3379
+ function orSchemeOf(url){ const m = String(url||'').match(/^([a-z][a-z0-9+.-]*):/); return m ? m[1] : '?'; }
3380
+ function orEscape(s){
3381
+ return String(s||'').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
3382
+ }
3383
+ function orRenderLoading(scheme, url){
3384
+ document.getElementById('or-src').textContent = scheme;
3385
+ document.getElementById('or-meta').textContent = '· fetching ' + url;
3386
+ document.getElementById('or-hint').textContent = orComposerHintFor(scheme);
3387
+ document.getElementById('or-input').placeholder = orPlaceholderFor(scheme);
3388
+ orBody.innerHTML = '<div class="preview-loading">fetching from ' + orEscape(scheme) + '…</div>';
3389
+ }
3390
+ function orComposerHintFor(scheme){
3391
+ if(scheme==='linear') return 'Ask the agent about this Linear ticket · all interaction captured in audit chain';
3392
+ if(scheme==='github') return 'Ask the agent about this GitHub issue or PR · all interaction captured';
3393
+ if(scheme==='figma') return 'Ask the agent about this Figma frame · all interaction captured';
3394
+ if(scheme==='drawio') return 'Ask the agent about this diagram · all interaction captured';
3395
+ return 'Ask the agent about this · all interaction captured';
3396
+ }
3397
+ function orPlaceholderFor(scheme){
3398
+ if(scheme==='linear') return "e.g. summarize the blocker · or what's the priority?";
3399
+ if(scheme==='github') return "e.g. what's the discussion about · what changed last comment?";
3400
+ if(scheme==='figma') return "e.g. extract the design tokens · what fields are in the modal?";
3401
+ if(scheme==='drawio') return "e.g. what's the request flow · where does the queue sit?";
3402
+ return 'Ask about this resource…';
3403
+ }
3404
+ function orRenderError(scheme, url, kind, message){
3405
+ let hint = '';
3406
+ if(kind==='auth') hint = 'Open Settings → Integrations to connect '+scheme+'.';
3407
+ else if(kind==='not_found') hint = 'The resource at '+url+' was not reachable.';
3408
+ else if(kind==='rate_limit') hint = 'The '+scheme+' API is rate-limited · try again in a minute.';
3409
+ else if(kind==='invalid_url') hint = 'codehere has no adapter for that URL scheme.';
3410
+ orBody.innerHTML =
3411
+ '<div class="preview-error">' +
3412
+ '<div>' + orEscape(message) + '</div>' +
3413
+ (hint ? '<div class="hint">' + orEscape(hint) + '</div>' : '') +
3414
+ '</div>';
3415
+ }
3416
+ function orRenderPayload(payload){
3417
+ const meta = payload.metadata || {};
3418
+ document.getElementById('or-meta').textContent =
3419
+ '· ' + (meta.bytes ? meta.bytes + ' bytes' : (payload.estimatedTokens ? '~' + payload.estimatedTokens + ' tokens' : '')) +
3420
+ ' · fetched ' + (payload.fetchedAt || '').slice(11,19);
3421
+ // Per-source structured render · Linear gets the demo v12 authentic
3422
+ // ticket card · everything else falls back to title + meta-pills + plain text.
3423
+ if(payload.source === 'linear'){
3424
+ orBody.innerHTML = orRenderLinear(payload);
3425
+ return;
3426
+ }
3427
+ const titleCandidate =
3428
+ meta.title || meta.label ||
3429
+ (meta.identifier ? meta.identifier + ' · ' + (meta.title||'') : null) ||
3430
+ payload.url;
3431
+ let pillsHtml = '';
3432
+ if(meta.state){
3433
+ const cls = meta.state === 'open' ? 'state-open' : meta.state === 'closed' ? 'state-closed' : '';
3434
+ pillsHtml += '<span class="pill ' + cls + '">' + orEscape(meta.state) + '</span>';
3435
+ }
3436
+ if(meta.author) pillsHtml += '<span class="pill">@' + orEscape(meta.author) + '</span>';
3437
+ if(meta.assignee) pillsHtml += '<span class="pill">→ ' + orEscape(meta.assignee) + '</span>';
3438
+ if(meta.priorityLabel) pillsHtml += '<span class="pill">' + orEscape(meta.priorityLabel) + '</span>';
3439
+ if(typeof meta.commentCount === 'number') pillsHtml += '<span class="pill">' + meta.commentCount + ' comments</span>';
3440
+ if(typeof meta.cellCount === 'number') pillsHtml += '<span class="pill">' + meta.cellCount + ' cells</span>';
3441
+ if(typeof meta.edgeCount === 'number') pillsHtml += '<span class="pill">' + meta.edgeCount + ' edges</span>';
3442
+ orBody.innerHTML =
3443
+ '<h3>' + orEscape(titleCandidate) + '</h3>' +
3444
+ '<div class="url-row">' + orEscape(payload.url) + '</div>' +
3445
+ (pillsHtml ? '<div class="meta-row">' + pillsHtml + '</div>' : '') +
3446
+ '<div class="preview-text">' + orEscape(payload.text) + '</div>';
3447
+ }
3448
+ function orRenderLinear(payload){
3449
+ const m = payload.metadata || {};
3450
+ const initials = (m.assignee||'').split(/\\s+/).map(w=>w[0]||'').join('').slice(0,2).toUpperCase() || '??';
3451
+ const priCls = (m.priorityLabel||'').toLowerCase().includes('urgent') ? 'linear-pri-urgent' : '';
3452
+ const stateCls = String(m.stateType||m.state||'').toLowerCase().includes('progress') ? 'linear-status-progress' : '';
3453
+ const props = [];
3454
+ if(m.priorityLabel) {
3455
+ props.push('<span class="linear-pill '+priCls+'"><span class="lin-icn linear-pri-bars"><div></div><div></div><div></div></span>'+orEscape(m.priorityLabel)+'</span>');
3456
+ }
3457
+ if(m.state) {
3458
+ props.push('<span class="linear-pill '+stateCls+'"><span class="lin-icn linear-status-circle"></span>'+orEscape(m.state)+'</span>');
3459
+ }
3460
+ if(m.assignee) {
3461
+ props.push('<span class="linear-pill linear-assignee"><span class="av" style="background:#76b7ff">'+orEscape(initials)+'</span>'+orEscape(m.assignee)+'</span>');
3462
+ }
3463
+ const commentsHtml = (m.comments||[]).map(c=>{
3464
+ const ini = (c.author||'').split(/\\s+/).map(w=>w[0]||'').join('').slice(0,2).toUpperCase() || '??';
3465
+ return '<div class="linear-comment">'+
3466
+ '<div class="av">'+orEscape(ini)+'</div>'+
3467
+ '<div class="col">'+
3468
+ '<div class="who">'+orEscape(c.author)+' <span class="ts">'+orEscape((c.createdAt||'').slice(0,10))+'</span></div>'+
3469
+ '<div class="body">'+orEscape(c.body||'')+'</div>'+
3470
+ '</div></div>';
3471
+ }).join('');
3472
+ return '<div class="linear-render">'+
3473
+ '<div class="linear-id">'+orEscape(m.identifier||payload.url)+'</div>'+
3474
+ '<div class="linear-title">'+orEscape(m.title||'(no title)')+'</div>'+
3475
+ (props.length ? '<div class="linear-props">'+props.join('')+'</div>' : '')+
3476
+ (m.description ? '<div class="linear-desc">'+orEscape(m.description.slice(0,800))+'</div>' : '')+
3477
+ (commentsHtml ? '<div class="linear-sep"><span class="lbl">Activity</span><div class="line"></div></div>'+commentsHtml : '')+
3478
+ '</div>';
3479
+ }
3480
+ async function openOverlayFor(url){
3481
+ const scheme = orSchemeOf(url);
3482
+ orRenderLoading(scheme, url);
3483
+ orOverlay.classList.add('open');
3484
+ document.getElementById('or-input').value = '';
3485
+ try{
3486
+ const r = await fetch('/api/context-source/preview?url=' + encodeURIComponent(url));
3487
+ const j = await r.json();
3488
+ if(!r.ok || !j.ok){
3489
+ orRenderError(scheme, url, (j && j.kind) || 'unknown', (j && j.error) || ('HTTP ' + r.status));
3490
+ return;
3491
+ }
3492
+ orRenderPayload(j.payload);
3493
+ }catch(err){
3494
+ orRenderError(scheme, url, 'network', (err && err.message) || String(err));
3495
+ }
3496
+ }
3497
+ // Click delegation · any element with .ctx-url-chip + data-url opens the overlay.
3498
+ document.addEventListener('click', (e)=>{
3499
+ const el = (e.target instanceof Element) ? e.target.closest('.ctx-url-chip') : null;
3500
+ if(!el) return;
3501
+ const url = el.getAttribute('data-url') || el.textContent.trim();
3502
+ if(url) openOverlayFor(url);
3503
+ });
3504
+ document.getElementById('or-close')?.addEventListener('click', orClose);
3505
+ orOverlay?.addEventListener('click', e=>{ if(e.target === orOverlay) orClose(); });
3506
+ // Make openOverlayFor reachable by other scripts that want to attach explicit
3507
+ // open buttons (e.g. settings drawer · history rows · adapter examples).
3508
+ window.codehereOpenOverlay = openOverlayFor;
3509
+
3510
+ /* v0.9 S6 part C · W-PASS approval modal · poll → render → decide */
3511
+ let _wpCurrent = null; // current pending pass shown in modal
3512
+ let _wpEditing = false;
3513
+ const wpModal = document.getElementById('wp-modal');
3514
+ const wpBadge = document.getElementById('wp-pending-badge');
3515
+
3516
+ function wpPretty(v){
3517
+ try { return JSON.stringify(v, null, 2); }
3518
+ catch { return String(v); }
3519
+ }
3520
+ // Demo v12 diff-style payload coloring · render JSON with keys grey,
3521
+ // string vals green, "from→to" patterns colored. When the value is a
3522
+ // string starting with "FROM:VAL → TO:VAL" or {from, to} pair, render
3523
+ // the FROM with strike-through red and the TO with green.
3524
+ function wpPayloadHtml(v){
3525
+ function esc(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
3526
+ function renderVal(val, indent){
3527
+ const pad = ' '.repeat(indent);
3528
+ if(val === null) return '<span class="val">null</span>';
3529
+ if(typeof val === 'number' || typeof val === 'boolean') return '<span class="val">'+esc(String(val))+'</span>';
3530
+ if(typeof val === 'string'){
3531
+ const m = val.match(/^(.+?)\\s*→\\s*(.+)$/);
3532
+ if(m) return '<span class="from">"'+esc(m[1])+'"</span><span class="arrow">→</span><span class="val">"'+esc(m[2])+'"</span>';
3533
+ return '<span class="val">"'+esc(val)+'"</span>';
3534
+ }
3535
+ if(Array.isArray(val)){
3536
+ if(val.length === 0) return '[]';
3537
+ const parts = val.map(x => pad + ' ' + renderVal(x, indent+1));
3538
+ return '[\\n' + parts.join(',\\n') + '\\n' + pad + ']';
3539
+ }
3540
+ if(typeof val === 'object'){
3541
+ // Special-case {from, to} → render as colored diff
3542
+ if('from' in val && 'to' in val){
3543
+ return '<span class="from">"'+esc(String(val.from))+'"</span><span class="arrow">→</span><span class="val">"'+esc(String(val.to))+'"</span>';
3544
+ }
3545
+ const keys = Object.keys(val);
3546
+ if(keys.length === 0) return '{}';
3547
+ const parts = keys.map(k => pad + ' <span class="key">"'+esc(k)+'"</span>: ' + renderVal(val[k], indent+1));
3548
+ return '{\\n' + parts.join(',\\n') + '\\n' + pad + '}';
3549
+ }
3550
+ return esc(String(val));
3551
+ }
3552
+ try { return renderVal(v, 0); }
3553
+ catch { return esc(wpPretty(v)); }
3554
+ }
3555
+ function wpHumanTitle(intent){
3556
+ const target = String(intent.target||'');
3557
+ const method = String(intent.method||'POST');
3558
+ const tail = target.replace(/^[a-z]+:\\/\\//,'');
3559
+ return method + ' · ' + tail;
3560
+ }
3561
+ function wpHumanTarget(intent){
3562
+ return String(intent.target||'');
3563
+ }
3564
+ let _wpTrustGrade = null; // last-known trust grade for current pass
3565
+ function wpRender(status){
3566
+ if(!status || status.kind !== 'pending') return Promise.resolve();
3567
+ const it = status.intent;
3568
+ _wpCurrent = status;
3569
+ _wpEditing = false;
3570
+ document.getElementById('wp-title').textContent = wpHumanTitle(it);
3571
+ document.getElementById('wp-reason').textContent = it.reason || '(no reason given)';
3572
+ document.getElementById('wp-target').textContent = wpHumanTarget(it);
3573
+ document.getElementById('wp-emitter').textContent = it.emittedBy || 'agent';
3574
+ document.getElementById('wp-session').textContent = (it.sessionId||'').slice(0,12);
3575
+ document.getElementById('wp-method').textContent = it.method || 'POST';
3576
+ // v0.9 · fetch + render per-domain trust grade · returned promise lets
3577
+ // callers (e.g., openWpModal demo synth) chain reliably instead of
3578
+ // racing setTimeout. Resolves whether the fetch succeeds, 404s, or
3579
+ // network-fails.
3580
+ const trustPromise = fetch('/api/writepass/'+encodeURIComponent(it.id)+'/trust')
3581
+ .then(r=>r.json()).then(t=>{
3582
+ _wpTrustGrade = t;
3583
+ const row = document.getElementById('wp-trust-row');
3584
+ if(!row || !t || !t.host) { if(row) row.style.display='none'; return; }
3585
+ row.style.display = '';
3586
+ document.getElementById('wp-trust-host').textContent = t.host;
3587
+ const ge = document.getElementById('wp-trust-grade');
3588
+ ge.textContent = t.grade;
3589
+ ge.className = 'wp-trust-grade ' + t.grade;
3590
+ document.getElementById('wp-trust-reason').textContent = ' · ' + (t.reason||'');
3591
+ })
3592
+ .catch(()=>{ /* no daemon trust info · ignore */ });
3593
+ document.getElementById('wp-targetline').textContent = wpHumanTarget(it);
3594
+ document.getElementById('wp-payload').innerHTML = wpPayloadHtml(it.payload);
3595
+ document.getElementById('wp-payload-wrap').style.display = '';
3596
+ document.getElementById('wp-payload-edit').style.display = 'none';
3597
+ document.getElementById('wp-actions').style.display = '';
3598
+ document.getElementById('wp-actions-editing').style.display = 'none';
3599
+ const result = document.getElementById('wp-result');
3600
+ result.style.display = 'none';
3601
+ result.className = 'wp-result';
3602
+ result.textContent = '';
3603
+ wpModal.classList.add('open');
3604
+ wpBadge.classList.remove('show');
3605
+ return trustPromise;
3606
+ }
3607
+ function wpCloseModal(){
3608
+ wpModal.classList.remove('open');
3609
+ _wpCurrent = null;
3610
+ _wpEditing = false;
3611
+ // Fix D9 (2026-05-23) · re-poll immediately so the badge re-appears
3612
+ // for any still-pending pass without waiting up to 4s. Without this,
3613
+ // closing the modal without deciding left the badge hidden for one
3614
+ // poll cycle even though the pass was still pending.
3615
+ wpPoll();
3616
+ }
3617
+ async function wpPoll(){
3618
+ try{
3619
+ const r = await fetch('/api/writepass/pending');
3620
+ if(!r.ok) return;
3621
+ const j = await r.json();
3622
+ const passes = (j && j.passes) || [];
3623
+ if(passes.length === 0){
3624
+ if(!wpModal.classList.contains('open')) wpBadge.classList.remove('show');
3625
+ return;
3626
+ }
3627
+ // If modal already open with this pass, skip · otherwise badge it.
3628
+ const top = passes[0];
3629
+ if(_wpCurrent && _wpCurrent.intent.id === top.intent.id) return;
3630
+ if(wpModal.classList.contains('open')) return;
3631
+ _wpCurrent = top;
3632
+ wpBadge.classList.add('show');
3633
+ }catch{
3634
+ // Daemon hiccup · ignore · next poll catches up.
3635
+ }
3636
+ }
3637
+ async function wpDecide(kind, editedPayload, trustThisDomain){
3638
+ if(!_wpCurrent) return;
3639
+ const passId = _wpCurrent.intent.id;
3640
+ const body = { kind };
3641
+ if(kind === 'edit') body.editedPayload = editedPayload;
3642
+ if(trustThisDomain) body.trustThisDomain = true;
3643
+ const result = document.getElementById('wp-result');
3644
+ result.style.display = 'block';
3645
+ result.className = 'wp-result';
3646
+ result.textContent = kind === 'cancel' ? 'cancelling…' : 'sending…';
3647
+ try{
3648
+ const r = await fetch('/api/writepass/'+encodeURIComponent(passId)+'/decide', {
3649
+ method: 'POST',
3650
+ headers: { 'Content-Type': 'application/json' },
3651
+ body: JSON.stringify(body),
3652
+ });
3653
+ const j = await r.json();
3654
+ // v0.9 · first-write domain trust gate
3655
+ if(j && j.status === 'requires_domain_trust'){
3656
+ result.className = 'wp-result fail';
3657
+ result.innerHTML = 'first-write to <b>'+orEscape(j.host)+'</b> (grade '+orEscape(j.grade)+') requires trust · '+orEscape(j.reason||'')+
3658
+ '<div class="wp-trust-prompt"><label><input type="checkbox" id="wp-trust-confirm"/> Trust <b>'+orEscape(j.host)+'</b> for future writes · then approve + send</label>'+
3659
+ ' <button class="wp-btn-approve" id="wp-trust-approve" style="margin-left:8px;padding:5px 10px;font-size:11.5px">Approve + trust</button></div>';
3660
+ document.getElementById('wp-trust-approve').onclick = ()=>{
3661
+ if(!document.getElementById('wp-trust-confirm').checked){ result.textContent='Tick the checkbox to confirm trust first.'; return; }
3662
+ wpDecide('approve', undefined, true);
3663
+ };
3664
+ return;
3665
+ }
3666
+ const finalStatus = j && j.kind ? j : null;
3667
+ if(!finalStatus){
3668
+ result.className = 'wp-result fail';
3669
+ result.textContent = 'unexpected response · check daemon logs';
3670
+ return;
3671
+ }
3672
+ if(finalStatus.kind === 'cancelled'){
3673
+ result.className = 'wp-result cancelled';
3674
+ result.textContent = 'cancelled · audit chain captured the refusal';
3675
+ } else if(finalStatus.kind === 'sent'){
3676
+ const code = finalStatus.response && finalStatus.response.status;
3677
+ result.className = 'wp-result ok';
3678
+ result.textContent = '✓ sent · HTTP '+code+' · audit chain captured payload + response';
3679
+ } else if(finalStatus.kind === 'failed'){
3680
+ const code = finalStatus.response && finalStatus.response.status;
3681
+ const err = finalStatus.response && finalStatus.response.error;
3682
+ result.className = 'wp-result fail';
3683
+ result.textContent = '✗ failed · HTTP '+code+(err?' · '+err:'')+' · audit captured';
3684
+ }
3685
+ _wpCurrent = null;
3686
+ setTimeout(wpCloseModal, 2400);
3687
+ }catch(err){
3688
+ result.className = 'wp-result fail';
3689
+ result.textContent = 'request failed · '+((err&&err.message)||String(err));
3690
+ }
3691
+ }
3692
+
3693
+ wpBadge?.addEventListener('click', ()=>{
3694
+ if(_wpCurrent && _wpCurrent.kind === 'pending') wpRender(_wpCurrent);
3695
+ else wpPoll().then(()=>{ if(_wpCurrent) wpRender(_wpCurrent); });
3696
+ });
3697
+ // Expose an opener so the inline .write-pass primitive's "Approve + send"
3698
+ // button (rendered inside the step timeline · see toolCardsHtml · gap 8)
3699
+ // can open the modal. If a real pending pass is already polled, use it.
3700
+ // Artifacts drawer open/close (task #51). Global so inline onclick resolves.
3701
+ window.openArtD = function(){ var d=document.getElementById('artDrawer'),s=document.getElementById('artScrim'); if(d)d.classList.add('open'); if(s)s.classList.add('open'); };
3702
+ window.closeArtD = function(){ var d=document.getElementById('artDrawer'),s=document.getElementById('artScrim'); if(d)d.classList.remove('open'); if(s)s.classList.remove('open'); };
3703
+
3704
+ // Otherwise synthesize a demo-shape pass from the open detail view so the
3705
+ // modal isn't a dead button on the seed session.
3706
+ window.openWpModal = function(){
3707
+ if(_wpCurrent && _wpCurrent.kind === 'pending') {
3708
+ wpRender(_wpCurrent);
3709
+ return;
3710
+ }
3711
+ // Synthesize from the current detail view if one is open · the demo
3712
+ // seed's W-PASS step has the payload encoded in step.output.
3713
+ const id = (document.getElementById('detail-view') && window._detailId) || 'demo-pending';
3714
+ const synth = {
3715
+ kind: 'pending',
3716
+ intent: {
3717
+ id: 'demo-wp-' + Date.now(),
3718
+ sessionId: id,
3719
+ emittedBy: 'conductor',
3720
+ method: 'PATCH',
3721
+ target: 'linear://INFRA-247',
3722
+ reason: 'Close ticket once tests pass + benchmark holds · human approval required for third-party writes.',
3723
+ payload: { status: { from: 'in-progress', to: 'done' } },
3724
+ },
3725
+ };
3726
+ // Synthesize the trust-grade row too so the demo modal shows the full
3727
+ // affordance. wpRender now returns the fetch promise · chain on .then()
3728
+ // so the override fires AFTER the async fetch settles (no setTimeout
3729
+ // race · was 300ms guess, now deterministic). The trust-reason includes
3730
+ // a "(demo data)" suffix per the 2026-05-24 retro: synth data must be
3731
+ // visibly honest about being synth, per [[no-screen-lies]] +
3732
+ // [[demo-synth-modal-pattern]] rules.
3733
+ wpRender(synth).then(() => {
3734
+ const row = document.getElementById('wp-trust-row');
3735
+ if(!row) return;
3736
+ row.style.display = '';
3737
+ document.getElementById('wp-trust-host').textContent = 'linear.app';
3738
+ const ge = document.getElementById('wp-trust-grade');
3739
+ ge.textContent = 'A';
3740
+ ge.className = 'wp-trust-grade A';
3741
+ document.getElementById('wp-trust-reason').textContent =
3742
+ ' \\u00B7 18mo on this domain \\u00B7 no overrides \\u00B7 (demo data \\u00B7 linear adapter not configured)';
3743
+ });
3744
+ };
3745
+ document.getElementById('wp-approve')?.addEventListener('click', ()=>wpDecide('approve'));
3746
+ document.getElementById('wp-cancel')?.addEventListener('click', ()=>wpDecide('cancel'));
3747
+ document.getElementById('wp-edit')?.addEventListener('click', ()=>{
3748
+ if(!_wpCurrent) return;
3749
+ _wpEditing = true;
3750
+ document.getElementById('wp-payload-wrap').style.display = 'none';
3751
+ const ta = document.getElementById('wp-payload-edit');
3752
+ ta.style.display = 'block';
3753
+ ta.value = wpPretty(_wpCurrent.intent.payload);
3754
+ document.getElementById('wp-actions').style.display = 'none';
3755
+ document.getElementById('wp-actions-editing').style.display = '';
3756
+ });
3757
+ document.getElementById('wp-edit-cancel')?.addEventListener('click', ()=>{
3758
+ if(!_wpCurrent) return;
3759
+ wpRender(_wpCurrent); // re-render the read-only view
3760
+ });
3761
+ document.getElementById('wp-save')?.addEventListener('click', ()=>{
3762
+ const ta = document.getElementById('wp-payload-edit');
3763
+ let parsed;
3764
+ try{ parsed = JSON.parse(ta.value); }
3765
+ catch(err){
3766
+ const result = document.getElementById('wp-result');
3767
+ result.style.display = 'block';
3768
+ result.className = 'wp-result fail';
3769
+ result.textContent = 'edited payload is not valid JSON · '+((err&&err.message)||'parse error');
3770
+ return;
3771
+ }
3772
+ wpDecide('edit', parsed);
3773
+ });
3774
+ // Poll on load + every 4s. Cheap.
3775
+ wpPoll();
3776
+ setInterval(wpPoll, 4000);
3777
+
3778
+ // Fix · local pill should reflect the ACTUAL host:port, not the hardcoded
3779
+ // 127.0.0.1:3773 (a user on --port 3899 would see the wrong pill).
3780
+ const localHostEl = document.getElementById('local-host');
3781
+ if(localHostEl) localHostEl.textContent = window.location.host;
3782
+
3783
+ document.addEventListener('keydown', (e)=>{
3784
+ if(e.key==='Escape'){
3785
+ if(document.getElementById('settings-drawer')?.classList.contains('open')) closeDrawer();
3786
+ else if(document.getElementById('share-modal')?.classList.contains('open')) closeShare();
3787
+ else if(document.getElementById('wp-modal')?.classList.contains('open')) wpCloseModal();
3788
+ // Fix D-ESC (2026-05-23) · overlay-renderer Esc handler · was missing
3789
+ else if(document.getElementById('or-overlay')?.classList.contains('open')) orClose();
3790
+ else if(document.getElementById('reshape-modal')?.classList.contains('open')) closeReshape();
3791
+ }
3792
+ });
411
3793
  fetchAll();
412
3794
 
413
3795
  const es=new EventSource('/api/events/sessions');
@@ -420,21 +3802,126 @@ document.addEventListener('DOMContentLoaded',()=>{
420
3802
  });
421
3803
  es.addEventListener('open',()=>{if(sseTimer){clearInterval(pollTimer);pollTimer=setInterval(fetchAll,8000);setTimeout(()=>{clearInterval(pollTimer);pollTimer=setInterval(fetchAll,30000);},10000);}});
422
3804
 
423
- window.addEventListener('offline',()=>offlineMode(true));
424
- window.addEventListener('online',()=>{offlineMode(false);fetchAll();});
3805
+ setInterval(pollHealth, 8000);
3806
+ pollHealth();
3807
+
3808
+ /* Audit-chain footer stats + activity dots · enriches the footer
3809
+ from a one-liner to a real status panel. Polls /api/audit/summary
3810
+ every 8s. */
3811
+ async function pollAuditStats(){
3812
+ try{
3813
+ const r = await fetch('/api/audit/summary');
3814
+ if(!r.ok) return;
3815
+ const d = await r.json();
3816
+ const statsEl = document.getElementById('audit-chain-stats');
3817
+ const dotsEl = document.getElementById('audit-chain-dots');
3818
+ if(statsEl){
3819
+ const actions = d.actions || 0;
3820
+ const blocked = d.blocked || 0;
3821
+ const agents = d.agents || 0;
3822
+ statsEl.textContent = actions+' entries last 24h · '+agents+' agent'+(agents===1?'':'s')+' · '+blocked+' blocked';
3823
+ }
3824
+ if(dotsEl){
3825
+ // Without per-entry recent[], approximate by drawing N=min(actions,12)
3826
+ // green dots and B=min(blocked, 12) red dots. Honest enough · still
3827
+ // gives the user visual "audit chain is alive" signal.
3828
+ const a = Math.min(d.actions||0, 12);
3829
+ const b = Math.min(d.blocked||0, 12);
3830
+ const greens = Math.max(0, a - b);
3831
+ let h = '';
3832
+ for(let i=0;i<b;i++) h += '<span class="dot block" title="blocked entry"></span>';
3833
+ for(let i=0;i<greens;i++) h += '<span class="dot allow" title="allowed entry"></span>';
3834
+ if(!a) for(let i=0;i<3;i++) h += '<span class="dot muted" title="no entries yet"></span>';
3835
+ dotsEl.innerHTML = h;
3836
+ }
3837
+ }catch{ /* ignore */ }
3838
+ }
3839
+ setInterval(pollAuditStats, 8000);
3840
+ pollAuditStats();
3841
+
3842
+ /* Failover banner · ported from v1 on 2026-05-23 /v1 kill.
3843
+ Polls /api/failover-offers · when a rate-limited agent has a viable
3844
+ fallback, an amber strip appears with Switch + Dismiss. */
3845
+ (function wireFailover(){
3846
+ const fbEl = document.getElementById('failover-banner');
3847
+ if(!fbEl) return;
3848
+ let fbDismissed = false;
3849
+ function escFb(s){ return String(s||'').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
3850
+ function render(offers){
3851
+ if(!offers || offers.length === 0 || fbDismissed){ fbEl.classList.remove('show'); return; }
3852
+ fbEl.classList.add('show');
3853
+ fbEl.innerHTML = offers.map(o => {
3854
+ const msg = '<b>'+escFb(o.signal && o.signal.fromAgent)+'</b> hit '+escFb(o.signal && o.signal.errorCode)+
3855
+ ' — switch to <b>'+escFb(o.target)+'</b>?';
3856
+ const actions = '<div class="fb-actions">'+
3857
+ '<button class="fb-accept" data-session="'+escFb(o.sessionId)+'" data-target="'+escFb(o.target)+'">Switch</button>'+
3858
+ '<button class="fb-dismiss" data-session="'+escFb(o.sessionId)+'">Dismiss</button>'+
3859
+ '</div>';
3860
+ return '<div class="fb-msg">'+msg+'</div>'+actions;
3861
+ }).join('');
3862
+ fbEl.querySelectorAll('.fb-accept').forEach(btn => {
3863
+ btn.addEventListener('click', async ()=>{
3864
+ btn.disabled = true; btn.textContent = 'Switching…';
3865
+ try{
3866
+ const r = await fetch('/api/failover-accept', {
3867
+ method: 'POST',
3868
+ headers: { 'Content-Type': 'application/json' },
3869
+ body: JSON.stringify({ sessionId: btn.dataset.session, targetAgent: btn.dataset.target }),
3870
+ });
3871
+ const d = await r.json();
3872
+ if(d.error || !d.ok || !d.newSessionId){
3873
+ fbEl.innerHTML = '<div class="fb-msg"><b>Failover failed:</b> '+escFb(d.error || d.detail || 'Target agent not ready')+'</div>';
3874
+ return;
3875
+ }
3876
+ fbEl.innerHTML = '<div class="fb-msg">↻ Switched to <b>'+escFb(btn.dataset.target)+'</b> · new session <b>'+escFb(d.newSessionId.slice(0,8))+'</b></div>'+
3877
+ '<div class="fb-actions"><button class="fb-dismiss" id="fb-ok">OK</button></div>';
3878
+ document.getElementById('fb-ok').addEventListener('click', ()=>{ fbDismissed = true; fbEl.classList.remove('show'); });
3879
+ }catch(err){
3880
+ fbEl.innerHTML = '<div class="fb-msg"><b>Failover failed:</b> network error · check daemon</div>';
3881
+ }
3882
+ });
3883
+ });
3884
+ fbEl.querySelectorAll('.fb-dismiss').forEach(btn => {
3885
+ btn.addEventListener('click', ()=>{ fbDismissed = true; fbEl.classList.remove('show'); });
3886
+ });
3887
+ }
3888
+ setInterval(async ()=>{
3889
+ try{
3890
+ const r = await fetch('/api/failover-offers');
3891
+ const d = await r.json();
3892
+ render(d.offers);
3893
+ }catch{ /* fine */ }
3894
+ }, 5000);
3895
+ })();
425
3896
 
426
3897
  document.addEventListener('keydown',e=>{
427
3898
  if(e.key==='Escape'&&_detail)hideD();
428
3899
  if((e.metaKey||e.ctrlKey)&&e.key==='k'){e.preventDefault();const ta=document.getElementById('main-ta')||document.getElementById('detail-ta');if(ta)ta.focus();}
429
3900
  });
430
3901
 
3902
+ // Composer .typing toggle · brand border the moment the input has chars.
3903
+ // The focus-within border handles "input is focused"; the .typing class
3904
+ // handles "input has content" so the affordance stays even if the user
3905
+ // tabs away mid-prompt.
3906
+ function bindTypingState(taEl){
3907
+ if(!taEl) return;
3908
+ const bar = taEl.closest('.input-bar');
3909
+ if(!bar) return;
3910
+ taEl.addEventListener('input', ()=>{
3911
+ if(taEl.value.length > 0) bar.classList.add('typing');
3912
+ else bar.classList.remove('typing');
3913
+ });
3914
+ }
3915
+
431
3916
  const ta=document.getElementById('main-ta'), btn=document.getElementById('main-btn');
432
3917
  if(btn)btn.addEventListener('click',()=>submit(ta,btn));
433
3918
  if(ta)ta.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey&&!ta.disabled&&btn&&!btn.disabled){e.preventDefault();submit(ta,btn);}});
3919
+ bindTypingState(ta);
434
3920
 
435
3921
  const dta=document.getElementById('detail-ta'), dbtn=document.getElementById('detail-btn');
436
- if(dbtn)dbtn.addEventListener('click',()=>submit(dta,dbtn));
437
- if(dta)dta.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey&&!dta.disabled&&dbtn&&!dbtn.disabled){e.preventDefault();submit(dta,dbtn);}});
3922
+ if(dbtn)dbtn.addEventListener('click',()=>submit(dta,dbtn,_detail));
3923
+ if(dta)dta.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey&&!dta.disabled&&dbtn&&!dbtn.disabled){e.preventDefault();submit(dta,dbtn,_detail);}});
3924
+ bindTypingState(dta);
438
3925
  });
439
3926
  `;
440
3927
  export const TASK_HTML_V2 = `<!doctype html>
@@ -448,14 +3935,56 @@ export const TASK_HTML_V2 = `<!doctype html>
448
3935
  <body>
449
3936
  <div class="app">
450
3937
  <div class="header">
451
- <span class="logo">codehere</span>
3938
+ <span class="logo"><span class="dot"></span><span class="name">codehere</span></span>
3939
+ <span class="beta-badge" title="codehere is in beta · heading to invite-only early access">beta</span>
3940
+ <span class="local-pill" title="zero telemetry · all data on this machine">
3941
+ <span class="dot"></span>local &middot; <span id="local-host">127.0.0.1:3773</span>
3942
+ </span>
452
3943
  <span class="budget safe" id="budget"></span>
453
- <a href="/" class="v1-link">v1 Dashboard</a>
3944
+ <select class="persona-select" id="persona-select" title="Show only what matters to your role" aria-label="View as role">
3945
+ <option value="all">Everyone</option>
3946
+ <option value="pm">PM view</option>
3947
+ <option value="designer">Designer view</option>
3948
+ <option value="eng">Engineer view</option>
3949
+ </select>
3950
+ <button class="density-toggle" id="density-toggle" title="Toggle compact / standard density">&#8862; Compact</button>
3951
+ <button class="reshape-btn" id="reshape-open" title="Describe the view you want · agent composes from primitives">⌘ Reshape view</button>
3952
+ <button class="share-btn-hdr" id="share-open" title="Share this thread · still local-first · link contains a signed payload, not your data" aria-label="Share thread">↗ Share</button>
3953
+ <button class="settings-btn" id="settings-open" title="Settings · integrations" aria-label="Open settings">⚙</button>
3954
+ </div>
3955
+
3956
+ <!-- v0.8 S3 · Custom-view banner · appears when reshape is active -->
3957
+ <div class="custom-view-banner" id="custom-view-banner">
3958
+ <span class="lbl">custom view</span>
3959
+ <span class="prompt" id="custom-view-prompt"></span>
3960
+ <span class="reset" onclick="resetShape()">reset</span>
454
3961
  </div>
455
3962
 
456
- <div class="squad-row" id="squad">
3963
+ <!-- Failover banner · ported from v1 on /v1 kill 2026-05-23 -->
3964
+ <div class="failover-banner" id="failover-banner"></div>
3965
+
3966
+ <!-- C-01 · first-run indexing banner · auto-hides once you have ≥ 3 sessions -->
3967
+ <div class="indexing-banner" id="indexing-banner">
3968
+ <span class="what">first run</span>
3969
+ <span>codehere indexed your repo locally · zero telemetry · all state at <code>~/.codehere/</code></span>
3970
+ </div>
3971
+
3972
+ <!-- F-01 · budget threshold warning · only renders when daily spend &gt;= 80% of cap -->
3973
+ <div class="budget-banner" id="budget-banner">
3974
+ <span class="what" id="budget-banner-what"></span>
3975
+ <span id="budget-banner-msg"></span>
3976
+ <span class="hint">free-tier providers (Aider, Gemini free) unaffected</span>
3977
+ </div>
3978
+
3979
+ <div class="squad-row">
457
3980
  <span style="color:var(--fg-30)">Squad:</span>
458
- <span class="squad-agent"><span class="squad-dot offline"></span>loading...</span>
3981
+ <span id="squad" style="display:contents">
3982
+ <span class="squad-agent"><span class="squad-dot offline"></span>loading...</span>
3983
+ </span>
3984
+ <!-- F-02 · cross-worktree alert · only visible when any out-of-cwd session has run > 5m -->
3985
+ <span class="worktree-alert" id="worktree-alert" onclick="if(window._wkTarget)showD(window._wkTarget)">
3986
+ <span class="pulse-dot"></span><span id="worktree-alert-text">other worktree</span>
3987
+ </span>
459
3988
  </div>
460
3989
 
461
3990
  <div class="bar">
@@ -468,6 +3997,7 @@ export const TASK_HTML_V2 = `<!doctype html>
468
3997
 
469
3998
  <div class="content">
470
3999
  <div id="task-list-view" class="content-inner">
4000
+ <div id="host-sessions-strip" style="display:none"></div>
471
4001
  <div id="task-list">
472
4002
  <div class="skeleton" id="skeleton">
473
4003
  <div class="skeleton-card"></div>
@@ -480,9 +4010,9 @@ export const TASK_HTML_V2 = `<!doctype html>
480
4010
  <div id="detail-view"></div>
481
4011
  <div class="input-area" style="border-top:none;padding:12px 0">
482
4012
  <div class="input-bar" style="max-width:100%">
483
- <textarea class="input-ta" id="detail-ta" placeholder="Request changes or ask a question..." rows="1" disabled></textarea>
484
- <div class="input-meta"><span>Auto</span></div>
485
- <button class="send-btn" id="detail-btn" disabled>&#8593;</button>
4013
+ <textarea class="input-ta composer-bar" id="detail-ta" placeholder="Request changes or ask a question..." rows="1" disabled></textarea>
4014
+ <div class="input-meta composer-meta"><span class="pick-agent" title="Auto-pick · best agent for this task">↳ Auto</span><span class="cost-est" title="Estimated cost · based on prompt length + recent agent pricing">~$0.02</span></div>
4015
+ <button class="send-btn composer-send" id="detail-btn" disabled>&#8593;</button>
486
4016
  </div>
487
4017
  </div>
488
4018
  </div>
@@ -490,14 +4020,177 @@ export const TASK_HTML_V2 = `<!doctype html>
490
4020
 
491
4021
  <div class="input-area">
492
4022
  <div class="input-bar">
493
- <textarea class="input-ta" id="main-ta" placeholder="Describe a task or ask a question..." rows="1"></textarea>
494
- <div class="input-meta"><span>Auto</span></div>
495
- <button class="send-btn" id="main-btn">&#8593;</button>
4023
+ <textarea class="input-ta composer-bar" id="main-ta" placeholder="Describe a task or ask a question..." rows="1"></textarea>
4024
+ <div class="input-meta composer-meta"><span class="pick-agent" title="Auto-pick · best agent for this task">↳ Auto</span><span class="cost-est" title="Estimated cost · based on prompt length + recent agent pricing">~$0.02</span></div>
4025
+ <button class="send-btn composer-send" id="main-btn">&#8593;</button>
4026
+ </div>
4027
+ <div class="kbd-hint">
4028
+ <span class="item"><span class="kbd">&#8629;</span> send</span>
4029
+ <span class="item"><span class="kbd">&#8679;&#8629;</span> newline</span>
4030
+ <span class="item"><span class="kbd">&#8984;K</span> focus composer</span>
4031
+ <span class="item"><span class="kbd">Esc</span> close modal</span>
496
4032
  </div>
497
4033
  </div>
498
4034
 
4035
+ <!-- v0.8 S3 · Reshape modal · ported from demo v12 lines 1142-1176 -->
4036
+ <div class="reshape-modal" id="reshape-modal">
4037
+ <div class="reshape-card">
4038
+ <button class="close-x" id="reshape-close" aria-label="Close">×</button>
4039
+ <div class="k">reshape this view</div>
4040
+ <h2>Describe the view you want.</h2>
4041
+ <p class="sub">Your agent composes the layout from codehere's shared primitives. Software ships building blocks · the final interface is yours to author.</p>
4042
+ <div class="reshape-input">
4043
+ <input id="reshape-input" placeholder="e.g. just show me what shipped + cost · no code"/>
4044
+ <button id="reshape-submit">Compose</button>
4045
+ </div>
4046
+ <div class="reshape-suggestions persona prims">
4047
+ <div class="lbl">or pick a quick start</div>
4048
+ <button class="pill" data-shape="ship-only" data-prompt="just show me what shipped and the cost · hide everything else">just show me what shipped + cost <span class="what">— Final Report + file chips only</span></button>
4049
+ <button class="pill" data-shape="velocity" data-prompt="velocity view · outcomes + duration + agent allocation · hide audit and code">velocity view <span class="what">— outcomes + time + cost · for daily check-in</span></button>
4050
+ <button class="pill" data-shape="security" data-prompt="security view · highlight hook blocks, overrides, trust grades · hide narrative">security view <span class="what">— blocks / overrides / trust amplified</span></button>
4051
+ </div>
4052
+ <div class="agent-compose recomposing" id="agent-compose">
4053
+ <div class="agent-line"><span style="font-family:var(--mono); color:var(--accent); font-weight:600">claude-code</span><span style="color:var(--fg-40)">· composing your view from shared primitives</span></div>
4054
+ <div style="margin-top: 8px;">
4055
+ <div class="line">read <span class="info">user prompt</span> · "<span id="compose-prompt-echo"></span>"</div>
4056
+ <div class="line">analyze · <span class="kw">5 primitives in scope</span>: <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">turn</code> · <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">panel</code> · <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">artifact</code> · <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">audit-line</code> · <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">tool-card</code></div>
4057
+ <div class="line">compose <span class="kw">view-config</span> · <span id="compose-config" style="color:var(--fg-90)"></span></div>
4058
+ <div class="line">apply · <span class="ok">view ready · 0.4s</span></div>
4059
+ <div class="line"><span class="ok">&check;</span> custom view active · <span style="color:var(--fg-50)">re-shape any time by clicking the button</span></div>
4060
+ </div>
4061
+ </div>
4062
+ <div class="primitives-note">
4063
+ <b>codehere ships primitives, not screens.</b> Your agent composes the layout from these building blocks · you author the final view in plain English.
4064
+ <div class="prims">
4065
+ <span class="p">turn</span>
4066
+ <span class="p">panel</span>
4067
+ <span class="p">artifact</span>
4068
+ <span class="p">audit-line</span>
4069
+ <span class="p">tool-card</span>
4070
+ <span class="p">verified-block</span>
4071
+ <span class="p">file-chip</span>
4072
+ <span class="p">write-pass</span>
4073
+ </div>
4074
+ </div>
4075
+ </div>
4076
+ </div>
4077
+
4078
+ <!-- v0.8 S4 · share-thread modal · ported from demo v12 lines 1126-1139 -->
4079
+ <div class="share-modal" id="share-modal" role="dialog" aria-modal="true" aria-labelledby="share-title">
4080
+ <div class="share-card">
4081
+ <div class="head" id="share-title">↗ Share this thread <span class="tag">· still local-first</span></div>
4082
+ <p class="blurb">Codehere generates a signed payload (steps · artifacts · Final Report · audit chain) you can paste into Slack, an email, a PR description. Your code never leaves your machine — only the synthesized record.</p>
4083
+ <div class="share-link-box" id="share-link-box">codehere://thread/<span id="share-link-id">…</span>?signed=ed25519:<span id="share-link-fp">…</span></div>
4084
+ <div class="share-actions">
4085
+ <button class="share-btn-primary" id="share-copy-link">Copy link</button>
4086
+ <button class="share-btn-secondary" id="share-download">Download bundle (.json)</button>
4087
+ <button class="share-btn-secondary" id="share-close">Close</button>
4088
+ </div>
4089
+ <p class="share-foot">Link contains a signed payload · not source code · recipient verifies chain integrity locally before trusting.</p>
4090
+ </div>
4091
+ </div>
4092
+ <div class="share-toast" id="share-toast">Link copied</div>
4093
+
4094
+ <!-- v0.9 S7 · overlay-renderer · inspect-and-act for ctx-source URLs -->
4095
+ <div class="or-overlay overlay-render" id="or-overlay" role="dialog" aria-modal="true">
4096
+ <div class="or-card or-content">
4097
+ <div class="or-head">
4098
+ <span class="src" id="or-src">linear</span>
4099
+ <span class="meta" id="or-meta">…</span>
4100
+ <span class="audit"><b>● audit</b> · captured locally</span>
4101
+ <button class="or-close" id="or-close" aria-label="Close">×</button>
4102
+ </div>
4103
+ <div class="or-body" id="or-body">
4104
+ <div class="preview-loading">fetching…</div>
4105
+ </div>
4106
+ <div class="or-composer or-agent-reply">
4107
+ <span class="hint" id="or-hint">Ask the agent about this · all interaction captured in audit chain</span>
4108
+ <div class="row">
4109
+ <input id="or-input" placeholder="e.g. summarize the blocker · or what's the priority?"/>
4110
+ <button id="or-ask" disabled title="Composer ships in S7 Part D">Ask agent</button>
4111
+ </div>
4112
+ </div>
4113
+ </div>
4114
+ </div>
4115
+
4116
+ <!-- v0.9 S6 · W-PASS approval modal · demo v12 lines 2030-2050 -->
4117
+ <button class="wp-pending-badge" id="wp-pending-badge" title="An agent wants to make a write · click to review">↗ write awaits</button>
4118
+ <div class="wp-modal" id="wp-modal" role="dialog" aria-modal="true" aria-labelledby="wp-title">
4119
+ <div class="wp-card write-pass">
4120
+ <div class="wp-banner"><span class="dot"></span><span>write-pass · agent action · awaits your approval</span></div>
4121
+ <h3 class="wp-title" id="wp-title">…</h3>
4122
+ <p class="wp-reason" id="wp-reason">…</p>
4123
+ <div class="wp-meta">
4124
+ tool · <b id="wp-target">…</b> · third-party API · <span style="color:var(--accent)">mediated by codehere</span>
4125
+ </div>
4126
+ <div class="wp-meta">
4127
+ agent · <b id="wp-emitter">…</b> · session <span id="wp-session">…</span>
4128
+ </div>
4129
+ <div class="wp-meta" id="wp-trust-row" style="display:none">
4130
+ domain · <b id="wp-trust-host">…</b> · trust grade
4131
+ <span class="wp-trust-grade" id="wp-trust-grade">?</span>
4132
+ <span id="wp-trust-reason" style="color:var(--fg-50)"></span>
4133
+ </div>
4134
+ <div class="wp-payload-wrap" id="wp-payload-wrap">
4135
+ <div class="wp-payload-head">
4136
+ <span class="method" id="wp-method">PATCH</span>
4137
+ <span id="wp-targetline">…</span>
4138
+ </div>
4139
+ <div class="wp-payload" id="wp-payload">…</div>
4140
+ </div>
4141
+ <textarea class="wp-payload-edit" id="wp-payload-edit" style="display:none" spellcheck="false"></textarea>
4142
+ <div class="wp-actions" id="wp-actions">
4143
+ <button class="wp-btn-approve" id="wp-approve">Approve + send</button>
4144
+ <button class="wp-btn-edit" id="wp-edit">Edit payload</button>
4145
+ <button class="wp-btn-cancel" id="wp-cancel">Cancel</button>
4146
+ </div>
4147
+ <div class="wp-actions" id="wp-actions-editing" style="display:none">
4148
+ <button class="wp-btn-save" id="wp-save">Save + send</button>
4149
+ <button class="wp-btn-cancel" id="wp-edit-cancel">Discard edit</button>
4150
+ </div>
4151
+ <p class="wp-foot">codehere mediates every agent write to a third-party tool · audit chain captures URL · payload · approval · response · signed timestamp · one verifiable thread instead of scattered tool logs</p>
4152
+ <div class="wp-result" id="wp-result" style="display:none"></div>
4153
+ </div>
4154
+ </div>
4155
+
4156
+ <!-- Settings drawer (A-09) · holds Context Sources + future toggles -->
4157
+ <div class="drawer-overlay" id="drawer-overlay"></div>
4158
+ <aside class="drawer" id="settings-drawer" aria-hidden="true">
4159
+ <button class="drawer-close" id="settings-close" aria-label="Close settings">×</button>
4160
+ <div class="drawer-title">Settings</div>
4161
+
4162
+ <h3>Integrations</h3>
4163
+ <p class="sub">Connect external tools so agents can read <code style="background:var(--bg-2);padding:1px 4px;border-radius:3px;font-family:var(--mono);font-size:10px">linear://</code> · <code style="background:var(--bg-2);padding:1px 4px;border-radius:3px;font-family:var(--mono);font-size:10px">figma://</code> · <code style="background:var(--bg-2);padding:1px 4px;border-radius:3px;font-family:var(--mono);font-size:10px">github://</code> · <code style="background:var(--bg-2);padding:1px 4px;border-radius:3px;font-family:var(--mono);font-size:10px">drawio://</code> URLs in prompts.</p>
4164
+ <div id="cs-list">
4165
+ <div style="color:var(--fg-40); font-size:11px">loading...</div>
4166
+ </div>
4167
+ </aside>
4168
+
499
4169
  <div id="offline-banner" class="offline-banner" style="display:none">You're offline. Actions will resume when connected.</div>
500
4170
  <div id="toast" style="display:none;position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:var(--bg-2);border:1px solid var(--border);border-radius:8px;padding:8px 16px;font-size:12px;color:var(--fg-70);z-index:100"></div>
4171
+
4172
+ <!-- RADICAL bottom-chrome collapse (2026-05-24) · audit-chain panel
4173
+ inlined into the OSS-footer status bar · ~115px → ~28px ·
4174
+ all load-bearing info preserved (dots · stats · Verify button) ·
4175
+ only the redundant subtitle + panel-card chrome removed. -->
4176
+ <div class="oss-footer">
4177
+ <span class="brand">codehere</span>
4178
+ <span class="sep">&middot;</span>
4179
+ <span id="oss-version">v0.6.x</span>
4180
+ <span class="sep">&middot;</span>
4181
+ <a href="https://github.com/muhammadegaa/codehere" target="_blank" rel="noopener">github</a>
4182
+ <span class="sep">&middot;</span>
4183
+ <span>MIT</span>
4184
+ <span class="sep">&middot;</span>
4185
+ <span class="audit-chain-panel">
4186
+ <span class="audit-chain-title">audit</span>
4187
+ <span id="audit-chain-stats" style="color:var(--fg-50);font-family:var(--mono);font-size:10.5px">— entries · —</span>
4188
+ <span id="audit-chain-dots" class="audit-chain-dots" style="display:inline-flex;gap:3px"></span>
4189
+ <button id="audit-verify-btn" class="audit-chain-btn" onclick="verifyAuditChain()" title="Walk the chain · verify SHA-256 integrity without trusting the daemon">Verify</button>
4190
+ <span id="audit-result" class="audit-chain-result" style="color:var(--fg-50)"></span>
4191
+ </span>
4192
+ <span class="right">built locally &middot; zero telemetry</span>
4193
+ </div>
501
4194
  </div>
502
4195
  <script>${APP_SCRIPT}</script>
503
4196
  </body>