codehere 0.6.2 → 0.7.1

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