codehere 0.8.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +22 -13
  2. package/dist/application/services/verdict-service.js +154 -0
  3. package/dist/application/use-cases/assess-cross-vendor.js +126 -0
  4. package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
  5. package/dist/application/use-cases/export-aat.js +107 -0
  6. package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
  7. package/dist/application/use-cases/generate-final-report.js +140 -15
  8. package/dist/application/use-cases/generate-narration.js +5 -3
  9. package/dist/application/use-cases/generate-thread-title.js +5 -2
  10. package/dist/application/use-cases/plan-auto-review.js +57 -0
  11. package/dist/application/use-cases/reconcile-audit.js +100 -0
  12. package/dist/application/use-cases/rerun-tests.js +226 -0
  13. package/dist/application/use-cases/search-sessions.js +70 -0
  14. package/dist/application/use-cases/should-auto-review.js +71 -0
  15. package/dist/application/use-cases/thread-export.js +90 -0
  16. package/dist/application/use-cases/verify-claim.js +187 -5
  17. package/dist/domain/audit.js +11 -0
  18. package/dist/domain/services/codebase-detector.js +0 -6
  19. package/dist/domain/task.js +91 -0
  20. package/dist/error-handler.js +3 -115
  21. package/dist/index.js +269 -25
  22. package/dist/infrastructure/completion/bash-completion.js +7 -3
  23. package/dist/infrastructure/completion/fish-completion.js +7 -12
  24. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  25. package/dist/infrastructure/config/config-manager.js +4 -3
  26. package/dist/infrastructure/config/product-context.js +1 -19
  27. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  28. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  29. package/dist/infrastructure/context/github-api.js +110 -0
  30. package/dist/infrastructure/context/github-device-flow.js +65 -0
  31. package/dist/infrastructure/context/oauth-store.js +5 -2
  32. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  33. package/dist/infrastructure/cost/budget-config.js +3 -2
  34. package/dist/infrastructure/cost/cost-router.js +16 -23
  35. package/dist/infrastructure/cost/spend-aggregator.js +90 -15
  36. package/dist/infrastructure/cost/team-ledger.js +355 -0
  37. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  38. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  39. package/dist/infrastructure/hooks/claude-sdk-hook.js +133 -3
  40. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  41. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  42. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  43. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  44. package/dist/infrastructure/orchestration/agents-config-seed.js +22 -10
  45. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  46. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  47. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  48. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  49. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  50. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  51. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  52. package/dist/infrastructure/orchestration/failover-config.js +0 -10
  53. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  54. package/dist/infrastructure/orchestration/generic-cli-adapter.js +132 -1
  55. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  56. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +164 -9
  57. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  58. package/dist/infrastructure/orchestration/progress-aggregator.js +238 -21
  59. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  60. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  61. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  62. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  63. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  64. package/dist/infrastructure/protocols/mcp-server-impl.js +1462 -380
  65. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  66. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  67. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  68. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  69. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  70. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  71. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  72. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  73. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  74. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  75. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  76. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  77. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  78. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  79. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  80. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  81. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  82. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  83. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  84. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  85. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  86. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  87. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  88. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4214
  89. package/dist/infrastructure/protocols/verify-page.js +849 -155
  90. package/dist/infrastructure/security/fix-code.js +144 -0
  91. package/dist/infrastructure/security/fix-secret.js +89 -0
  92. package/dist/infrastructure/security/permissions-config.js +3 -2
  93. package/dist/infrastructure/security/scan-types.js +10 -0
  94. package/dist/infrastructure/security/ship-scan.js +195 -0
  95. package/dist/infrastructure/security/trust-scorer.js +70 -3
  96. package/dist/infrastructure/security/vibe-checks.js +99 -0
  97. package/dist/infrastructure/storage/atomic-write.js +86 -0
  98. package/dist/infrastructure/storage/project-thread-mirror.js +150 -3
  99. package/dist/infrastructure/storage/project-thread-reader.js +261 -24
  100. package/dist/monitoring.js +2 -2
  101. package/dist/presentation/cli/commands/audit-command.js +79 -4
  102. package/dist/presentation/cli/commands/budget-command.js +2 -2
  103. package/dist/presentation/cli/commands/check-command.js +96 -0
  104. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  105. package/dist/presentation/cli/commands/help-command.js +5 -5
  106. package/dist/presentation/cli/commands/hook-command.js +5 -5
  107. package/dist/presentation/cli/commands/init-command.js +26 -14
  108. package/dist/presentation/cli/commands/memory-command.js +28 -28
  109. package/dist/presentation/cli/commands/permission-command.js +1 -12
  110. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  111. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  112. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  113. package/dist/presentation/cli/commands/report-command.js +48 -32
  114. package/dist/presentation/cli/commands/scan-command.js +52 -0
  115. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  116. package/dist/presentation/cli/commands/search-command.js +51 -0
  117. package/dist/presentation/cli/commands/setup-command.js +31 -8
  118. package/dist/presentation/cli/commands/status-command.js +39 -86
  119. package/dist/presentation/cli/commands/team-command.js +256 -0
  120. package/dist/presentation/cli/commands/thread-command.js +87 -5
  121. package/dist/presentation/cli/commands/trust-command.js +53 -31
  122. package/dist/presentation/cli/commands/verify-command.js +74 -9
  123. package/dist/presentation/cli/error-display.js +0 -8
  124. package/dist/presentation/cli/repl.js +15 -7
  125. package/dist/presentation/cli/task-deliverable.js +36 -10
  126. package/dist/testing/replay-adapter.js +0 -19
  127. package/dist/ui.js +51 -239
  128. package/dist/utils/version.js +0 -6
  129. package/package.json +30 -10
  130. package/dist/domain/entities/task.js +0 -61
  131. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  132. package/dist/infrastructure/security/input-validator.js +0 -152
  133. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  134. package/dist/infrastructure/storage/task-repository.js +0 -206
@@ -1,4173 +1,31 @@
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
- `;
1
+ import { SHARED_STYLES } from './ui/styles.js';
2
+ import { FONT_FACES } from './ui/font-faces.js';
3
+ import { APP_SCRIPT } from './ui/app-script.js';
4143
4
  export const TASK_HTML_V2 = `<!doctype html>
4144
5
  <html lang="en">
4145
6
  <head>
4146
7
  <meta charset="utf-8" />
4147
8
  <meta name="viewport" content="width=device-width,initial-scale=1" />
4148
- <title>codehere</title>
4149
- <style>${SHARED_STYLES}</style>
9
+ <!-- S16 · one brand everywhere · favicon = the landing's gradient-C (served
10
+ locally from /icon.svg, never hotlinked) · title follows the landing's
11
+ 'codehere · <context>' pattern with an honest local context. -->
12
+ <link rel="icon" type="image/svg+xml" href="/icon.svg" />
13
+ <title>codehere · alpha · local</title>
14
+ <style>${FONT_FACES}${SHARED_STYLES}</style>
4150
15
  </head>
4151
16
  <body>
4152
17
  <div class="app">
18
+ <!-- S13 · header 9->3: wordmark · ONE live context pill (local + spend) ·
19
+ ⌘K hint. Beta chip, budget chip, lenses, density, reshape,
20
+ share, gear all live INSIDE the ⌘K palette now — same functions,
21
+ one keystroke away (rule 1 + rule 9). -->
4153
22
  <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>
23
+ <span class="logo" onclick="goHome()" style="cursor:pointer" title="Home"><span class="name">codehere</span><span class="prerelease" title="pre-release · v0.9 · not yet validated with real users">alpha</span></span>
24
+ <span class="local-pill" title="Running locally · no telemetry · state on disk at ~/.codehere">
25
+ <span class="dot"></span>Local<span class="budget safe" id="budget"></span>
4158
26
  </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>
27
+ <!-- Spend & Audit moved into the sidebar rail (nav option B, 2026-07-07) — the rail is THE navigator. -->
28
+ <button class="cmdk-hint" id="cmdk-open" title="Commands &amp; search · ⌘K" aria-label="Open the command palette"><span class="kbd">&#8984;K</span></button>
4171
29
  </div>
4172
30
 
4173
31
  <!-- v0.8 S3 · Custom-view banner · appears when reshape is active -->
@@ -4190,64 +48,82 @@ export const TASK_HTML_V2 = `<!doctype html>
4190
48
  <div class="budget-banner" id="budget-banner">
4191
49
  <span class="what" id="budget-banner-what"></span>
4192
50
  <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>
51
+ <span class="hint">subscription &amp; free-tier agents (e.g. Cursor) still run</span>
4213
52
  </div>
4214
53
 
54
+ <!-- Cockpit shell (Phase-1 S1): a persistent Projects sidebar beside the main
55
+ pane. The main pane reuses the existing list/room/detail views unchanged. -->
56
+ <div class="shell">
57
+ <aside class="sidebar" id="sidebar" aria-label="Projects"></aside>
4215
58
  <div class="content">
4216
59
  <div id="task-list-view" class="content-inner">
60
+ <!-- First-run: a non-blocking, dismissible value-first welcome card (renderWelcomeCard),
61
+ NOT a full-screen overlay. Sits above the workspace, which stays usable underneath. -->
62
+ <div id="home-welcome" style="display:none"></div>
63
+ <!-- S13 · "NEEDS YOUR DECISION" — the FIRST thing on the page and the
64
+ ONLY orange. Rendered by renderDecisions() from REAL state only
65
+ (pending write-passes + verifier-broken verdicts today + failed
66
+ runs with retry available today). Empty -> does not render AT ALL
67
+ (data-verify-unit removed so H14 can pin absent <=> N===0). -->
68
+ <div id="decision-queue" style="display:none"></div>
69
+ <!-- F-02 · cross-worktree alert · only visible when any out-of-cwd session has run > 5m -->
70
+ <span class="worktree-alert" id="worktree-alert" onclick="if(window._wkTarget)showD(window._wkTarget)">
71
+ <span class="pulse-dot"></span><span id="worktree-alert-text" data-user-content>other worktree</span>
72
+ </span>
73
+ <!-- Observer (Thariq loop-2): at rest a calm ONE-LINE row above TODAY
74
+ ("● watching N terminal sessions · M live"), expandable to the
75
+ full rail; first-run keeps the teach state. -->
76
+ <div id="observer-rail" style="display:none"></div>
77
+ <!-- S12->S13 · the hero element. With tasks today it renders the TODAY
78
+ day-line ("TODAY · N AGENTS · N CLAIMS CHECKED · $X.XX") and keeps
79
+ carrying the data-verify-hero-* stamps (H13, derivations unchanged
80
+ via heroCounts). Zero tasks today -> the first-run hero with its
81
+ teach copy, ordered to the top via body.first-run. -->
82
+ <div class="home-hero" id="home-hero" style="display:none" data-verify-unit="home-hero" data-verify-hero-mode="first-run"></div>
83
+ <!-- S13 · active-filter token · filters live in ⌘K; the applied filter
84
+ shows as ONE removable token, not a tab bar. -->
85
+ <div id="filter-token" style="display:none"></div>
4217
86
  <div id="task-list">
4218
87
  <div class="skeleton" id="skeleton">
4219
- <div class="skeleton-card"></div>
4220
- <div class="skeleton-card"></div>
4221
- <div class="skeleton-card"></div>
88
+ <div class="skeleton-card"><span class="sk-sweep"></span></div>
89
+ <div class="skeleton-card"><span class="sk-sweep"></span></div>
90
+ <div class="skeleton-card"><span class="sk-sweep"></span></div>
91
+ <div class="skeleton-card"><span class="sk-sweep"></span></div>
4222
92
  </div>
4223
93
  </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
94
  </div>
4229
95
  <div id="task-detail-view" style="display:none" class="content-inner">
4230
96
  <div id="detail-view"></div>
4231
97
  <div class="input-area" style="border-top:none;padding:12px 0">
4232
98
  <div class="input-bar" style="max-width:100%">
4233
99
  <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>
100
+ <div class="input-meta composer-meta"><span class="pick-agent" title="Auto-pick · best agent for this task">› Auto</span><span class="cost-est" id="cost-est-detail" data-verify-unit="cost-est" title="Estimated cost">&#8230;</span></div>
101
+ <button class="send-btn composer-send" id="detail-btn" disabled aria-label="Send follow-up">&#8593;</button>
4236
102
  </div>
103
+ <div class="send-fail" id="detail-send-fail" style="display:none"></div>
4237
104
  </div>
4238
105
  </div>
106
+ <!-- Spend & Audit: the team cost + audit ledger as its own page (nav tab), not a banner on the workspace. -->
107
+ <div id="spend-view" style="display:none" class="content-inner">
108
+ <div id="team-cost-hero"></div>
109
+ </div>
110
+ </div>
4239
111
  </div>
4240
112
 
4241
113
  <div class="input-area">
114
+ <!-- GitHub import row · populated by initGhImport(): a Connect button when
115
+ disconnected, or a repo field + issue PICKER when connected. -->
116
+ <div id="gh-import" class="gh-import"></div>
4242
117
  <div class="input-bar">
4243
118
  <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>
119
+ <div class="input-meta composer-meta"><span class="agentpick-wrap"><input type="hidden" id="agent-pick" value="auto"><button type="button" class="agentpick" id="agentpick-btn" aria-haspopup="listbox" aria-expanded="false" title="Who runs this task · Auto picks the best fit" onclick="toggleAgentMenu(event)"><span id="agentpick-cur" class="agentpick-cur"></span><svg class="agentpick-car" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg></button><div class="agentpick-menu" id="agentpick-menu" role="listbox" aria-label="Pick the agent for this task" hidden></div></span><span class="cost-est" id="cost-est-main" data-verify-unit="cost-est" title="Estimated cost">&#8230;</span></div>
120
+ <button class="send-btn composer-send" id="main-btn" aria-label="Send task">&#8593;</button>
4246
121
  </div>
122
+ <div class="send-fail" id="main-send-fail" style="display:none"></div>
4247
123
  <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>
124
+ <span class="item"><span class="kbd">&#9166;</span> send</span>
125
+ <span class="item"><span class="kbd">shift &#9166;</span> newline</span>
126
+ <span class="item"><span class="kbd">&#8984;K</span> commands</span>
4251
127
  <span class="item"><span class="kbd">Esc</span> close modal</span>
4252
128
  </div>
4253
129
  </div>
@@ -4263,14 +139,14 @@ export const TASK_HTML_V2 = `<!doctype html>
4263
139
  <input id="reshape-input" placeholder="e.g. just show me what shipped + cost · no code"/>
4264
140
  <button id="reshape-submit">Compose</button>
4265
141
  </div>
4266
- <div class="reshape-suggestions persona prims">
142
+ <div class="reshape-suggestions prims">
4267
143
  <div class="lbl">or pick a quick start</div>
4268
144
  <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
145
  <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
146
  <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
147
  </div>
4272
148
  <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>
149
+ <div class="agent-line"><span style="font-family:var(--mono); color:var(--fg-90); font-weight:600">claude-code</span><span style="color:var(--fg-40)">· composing your view from shared primitives</span></div>
4274
150
  <div style="margin-top: 8px;">
4275
151
  <div class="line">read <span class="info">user prompt</span> · "<span id="compose-prompt-echo"></span>"</div>
4276
152
  <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>
@@ -4295,11 +171,25 @@ export const TASK_HTML_V2 = `<!doctype html>
4295
171
  </div>
4296
172
  </div>
4297
173
 
174
+ <!-- Connect a project from a GitHub URL · clone locally, open as a project -->
175
+ <div class="clone-modal" id="clone-modal" role="dialog" aria-modal="true" aria-labelledby="clone-title">
176
+ <div class="clone-card">
177
+ <button class="close-x" id="clone-close" aria-label="Close" onclick="closeClone()">&#215;</button>
178
+ <div class="head" id="clone-title">Open a project from GitHub</div>
179
+ <p class="blurb">Paste a repo URL. codehere clones it to <code>~/codehere-projects/</code> on this machine and opens it &#8212; using your existing git access. Private repos work if your git is already set up.</p>
180
+ <div class="clone-input">
181
+ <input id="clone-url" placeholder="github.com/org/repo &#183; or org/repo" autocomplete="off" spellcheck="false" onkeydown="if(event.key==='Enter')doClone()"/>
182
+ <button id="clone-submit" onclick="doClone()">Clone</button>
183
+ </div>
184
+ <div class="clone-status" id="clone-status" style="display:none"></div>
185
+ </div>
186
+ </div>
187
+
4298
188
  <!-- v0.8 S4 · share-thread modal · ported from demo v12 lines 1126-1139 -->
4299
189
  <div class="share-modal" id="share-modal" role="dialog" aria-modal="true" aria-labelledby="share-title">
4300
190
  <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>
191
+ <div class="head" id="share-title">Share this thread <span class="tag">· still local-first</span></div>
192
+ <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 does.</p>
4303
193
  <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
194
  <div class="share-actions">
4305
195
  <button class="share-btn-primary" id="share-copy-link">Copy link</button>
@@ -4334,14 +224,14 @@ export const TASK_HTML_V2 = `<!doctype html>
4334
224
  </div>
4335
225
 
4336
226
  <!-- 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>
227
+ <button class="wp-pending-badge" id="wp-pending-badge" title="An agent wants to make a write · click to review">write awaits ›</button>
4338
228
  <div class="wp-modal" id="wp-modal" role="dialog" aria-modal="true" aria-labelledby="wp-title">
4339
- <div class="wp-card write-pass">
229
+ <div class="wp-card">
4340
230
  <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>
231
+ <h3 class="wp-title" id="wp-title" data-user-content>…</h3>
232
+ <p class="wp-reason" id="wp-reason" data-user-content>…</p>
4343
233
  <div class="wp-meta">
4344
- tool · <b id="wp-target">…</b> · third-party API · <span style="color:var(--accent)">mediated by codehere</span>
234
+ tool · <b id="wp-target" data-user-content>…</b> · third-party API · <span style="color:var(--accent)">gated by codehere</span>
4345
235
  </div>
4346
236
  <div class="wp-meta">
4347
237
  agent · <b id="wp-emitter">…</b> · session <span id="wp-session">…</span>
@@ -4354,9 +244,9 @@ export const TASK_HTML_V2 = `<!doctype html>
4354
244
  <div class="wp-payload-wrap" id="wp-payload-wrap">
4355
245
  <div class="wp-payload-head">
4356
246
  <span class="method" id="wp-method">PATCH</span>
4357
- <span id="wp-targetline">…</span>
247
+ <span id="wp-targetline" data-user-content>…</span>
4358
248
  </div>
4359
- <div class="wp-payload" id="wp-payload">…</div>
249
+ <div class="wp-payload" id="wp-payload" data-user-content>…</div>
4360
250
  </div>
4361
251
  <textarea class="wp-payload-edit" id="wp-payload-edit" style="display:none" spellcheck="false"></textarea>
4362
252
  <div class="wp-actions" id="wp-actions">
@@ -4368,11 +258,22 @@ export const TASK_HTML_V2 = `<!doctype html>
4368
258
  <button class="wp-btn-save" id="wp-save">Save + send</button>
4369
259
  <button class="wp-btn-cancel" id="wp-edit-cancel">Discard edit</button>
4370
260
  </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>
261
+ <p class="wp-foot">codehere gates this 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
262
  <div class="wp-result" id="wp-result" style="display:none"></div>
4373
263
  </div>
4374
264
  </div>
4375
265
 
266
+ <!-- S13 · the ⌘K command palette — THE control surface. Every control that
267
+ left the header is a command here; each label names its visible
268
+ consequence and Enter actually runs the existing function. -->
269
+ <div class="palette-scrim" id="palette-scrim" data-verify-unit="palette">
270
+ <div class="palette" role="dialog" aria-modal="true" aria-label="Command palette">
271
+ <div class="pin"><input id="palette-input" placeholder="Type a command · every control lives here…" autocomplete="off" spellcheck="false" aria-label="Filter commands"/></div>
272
+ <div class="plist" id="palette-list"></div>
273
+ <div class="pfoot"><span>arrow keys navigate</span><span>&#9166; run</span><span>esc close</span></div>
274
+ </div>
275
+ </div>
276
+
4376
277
  <!-- Settings drawer (A-09) · holds Context Sources + future toggles -->
4377
278
  <div class="drawer-overlay" id="drawer-overlay"></div>
4378
279
  <aside class="drawer" id="settings-drawer" aria-hidden="true">
@@ -4380,9 +281,9 @@ export const TASK_HTML_V2 = `<!doctype html>
4380
281
  <div class="drawer-title">Settings</div>
4381
282
 
4382
283
  <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>
284
+ <p class="sub">Working context sources appear here. Use their URL chips in prompts.</p>
4384
285
  <div id="cs-list">
4385
- <div style="color:var(--fg-40); font-size:11px">loading...</div>
286
+ <div style="color:var(--fg-40); font-size:12px">loading...</div>
4386
287
  </div>
4387
288
  </aside>
4388
289
 
@@ -4390,7 +291,7 @@ export const TASK_HTML_V2 = `<!doctype html>
4390
291
  <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
292
 
4392
293
  <!-- RADICAL bottom-chrome collapse (2026-05-24) · audit-chain panel
4393
- inlined into the OSS-footer status bar · ~115px → ~28px ·
294
+ inlined into the OSS-footer status bar · ~115px -> ~28px ·
4394
295
  all load-bearing info preserved (dots · stats · Verify button) ·
4395
296
  only the redundant subtitle + panel-card chrome removed. -->
4396
297
  <div class="oss-footer">
@@ -4403,15 +304,16 @@ export const TASK_HTML_V2 = `<!doctype html>
4403
304
  <span>MIT</span>
4404
305
  <span class="sep">&middot;</span>
4405
306
  <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>
307
+ <button id="audit-verify-btn" class="audit-chain-btn" onclick="verifyAuditChain()" title="Check your local activity log hasn't been edited">verify log</button>
4410
308
  <span id="audit-result" class="audit-chain-result" style="color:var(--fg-50)"></span>
4411
309
  </span>
4412
- <span class="right">built locally &middot; zero telemetry</span>
310
+ <span class="right">built locally</span>
4413
311
  </div>
4414
312
  </div>
313
+ <!-- First-run: welcome splash + first-time setup (JS-rendered), then the tour. -->
314
+ <div id="ob-welcome" class="ob-welcome" hidden></div>
315
+ <!-- Interactive onboarding walkthrough — spotlight coachmarks over the real UI. -->
316
+ <div id="ob-tour" class="ob-tour" hidden><div class="ob-spot" id="ob-spot"></div><div class="ob-pop" id="ob-pop"></div></div>
4415
317
  <script>${APP_SCRIPT}</script>
4416
318
  </body>
4417
319
  </html>`;