codehere 0.8.0 → 1.0.2

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