codehere 0.8.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -184
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +329 -0
- package/dist/index.js +74 -1004
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
- package/dist/presentation/cli/commands/check-command.js +251 -0
- package/dist/presentation/cli/commands/help-command.js +22 -286
- package/dist/ui.js +19 -438
- package/dist/utils/version.js +0 -12
- package/package.json +18 -27
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/generate-final-report.js +0 -134
- package/dist/application/use-cases/generate-narration.js +0 -128
- package/dist/application/use-cases/generate-thread-title.js +0 -178
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/thread-export.js +0 -155
- package/dist/application/use-cases/verify-claim.js +0 -274
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/entities/task.js +0 -61
- package/dist/domain/services/codebase-detector.js +0 -91
- package/dist/error-handler.js +0 -251
- package/dist/infrastructure/completion/bash-completion.js +0 -25
- package/dist/infrastructure/completion/fish-completion.js +0 -22
- package/dist/infrastructure/completion/zsh-completion.js +0 -30
- package/dist/infrastructure/config/config-manager.js +0 -173
- package/dist/infrastructure/config/product-context.js +0 -67
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -163
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -119
- package/dist/infrastructure/context/registry-bootstrap.js +0 -99
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -113
- package/dist/infrastructure/cost/cost-router.js +0 -104
- package/dist/infrastructure/cost/spend-aggregator.js +0 -147
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
- package/dist/infrastructure/hooks/hook-mode.js +0 -71
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
- package/dist/infrastructure/orchestration/context-handoff.js +0 -144
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -77
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
- package/dist/infrastructure/orchestration/graphify-context.js +0 -254
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1091
- package/dist/infrastructure/orchestration/session-persistence.js +0 -258
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/protocols/verify-page.js +0 -983
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/security/permissions-config.js +0 -234
- package/dist/infrastructure/security/trust-scorer.js +0 -657
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
- package/dist/infrastructure/storage/project-thread-reader.js +0 -172
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/infrastructure/storage/task-repository.js +0 -206
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -126
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -198
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -578
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -116
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -387
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
- package/dist/presentation/cli/commands/setup-command.js +0 -307
- package/dist/presentation/cli/commands/status-command.js +0 -661
- package/dist/presentation/cli/commands/thread-command.js +0 -288
- package/dist/presentation/cli/commands/trust-command.js +0 -306
- package/dist/presentation/cli/commands/verify-command.js +0 -233
- package/dist/presentation/cli/error-display.js +0 -336
- package/dist/presentation/cli/repl.js +0 -797
- package/dist/presentation/cli/task-deliverable.js +0 -127
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -166
|
@@ -1,4418 +0,0 @@
|
|
|
1
|
-
import { PHASE_CLASSIFIER_SRC } from './thread-phase.js';
|
|
2
|
-
const SHARED_STYLES = `
|
|
3
|
-
:root {
|
|
4
|
-
--bg: hsl(0 0% 3.9%); --bg-1: hsl(0 0% 6%); --bg-2: hsl(0 0% 9%);
|
|
5
|
-
--fg: hsl(0 0% 98%); --fg-70: rgba(255,255,255,0.70);
|
|
6
|
-
--fg-50: rgba(255,255,255,0.50); --fg-40: rgba(255,255,255,0.40);
|
|
7
|
-
--fg-30: rgba(255,255,255,0.30); --fg-20: rgba(255,255,255,0.20);
|
|
8
|
-
--border: rgba(255,255,255,0.10); --border-soft: rgba(255,255,255,0.06);
|
|
9
|
-
--accent: #ff9a76; --accent-wash: rgba(255,154,118,0.10);
|
|
10
|
-
--allow: #28c840; --block: #ff5f57; --warn: #febc2e;
|
|
11
|
-
--info: #76b7ff;
|
|
12
|
-
/* Per-agent identity colors — AGENTIC_UX_FRAMEWORK primitive #1 */
|
|
13
|
-
--agent-claude: #d97757;
|
|
14
|
-
--agent-codex: #10a37f;
|
|
15
|
-
--agent-aider: #4a90e2;
|
|
16
|
-
--agent-gemini: #fbbc05;
|
|
17
|
-
--agent-opencode: #06b6d4;
|
|
18
|
-
--radius: 8px;
|
|
19
|
-
--sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
20
|
-
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
21
|
-
}
|
|
22
|
-
* { box-sizing: border-box; margin: 0; }
|
|
23
|
-
body {
|
|
24
|
-
margin: 0; background: var(--bg); color: var(--fg);
|
|
25
|
-
font-family: var(--sans); font-size: 13px; line-height: 1.55;
|
|
26
|
-
-webkit-font-smoothing: antialiased; height: 100vh; overflow: hidden;
|
|
27
|
-
position: relative;
|
|
28
|
-
}
|
|
29
|
-
/* Ambient dot-grid backdrop · matches demo v12 + landing globals.css.
|
|
30
|
-
* Very faint blue dots on 24px grid · adds depth without distraction so
|
|
31
|
-
* the cards above visibly "lift" off a textured surface. Fixed position
|
|
32
|
-
* so it stays put while content scrolls. */
|
|
33
|
-
body::before {
|
|
34
|
-
content: ''; position: fixed; inset: 0;
|
|
35
|
-
background-image: radial-gradient(circle, rgba(74,144,226,0.045) 1px, transparent 1px);
|
|
36
|
-
background-size: 24px 24px;
|
|
37
|
-
pointer-events: none; z-index: 0; opacity: 0.7;
|
|
38
|
-
}
|
|
39
|
-
.app { height: 100vh; display: flex; flex-direction: column; position: relative; z-index: 1; }
|
|
40
|
-
|
|
41
|
-
/* Header · glass · matches demo v12 + landing globals · darker tint
|
|
42
|
-
* with backdrop blur + saturate so the header sits visibly above the
|
|
43
|
-
* scroll content beneath it. */
|
|
44
|
-
.header {
|
|
45
|
-
display: flex; align-items: center; gap: 14px;
|
|
46
|
-
padding: 10px 20px; border-bottom: 1px solid var(--border-soft);
|
|
47
|
-
background: rgba(0,0,0,0.55);
|
|
48
|
-
backdrop-filter: blur(16px) saturate(140%);
|
|
49
|
-
-webkit-backdrop-filter: blur(16px) saturate(140%);
|
|
50
|
-
flex-shrink: 0;
|
|
51
|
-
}
|
|
52
|
-
/* Brand wordmark · matches landing + demo v12 spec · gradient name + pulsing
|
|
53
|
-
* brand-color dot. First thing every user sees on every screen. */
|
|
54
|
-
.logo { font-size: 15px; font-weight: 700; display: inline-flex; align-items: center; gap: 7px; letter-spacing: -0.01em; }
|
|
55
|
-
.logo .name {
|
|
56
|
-
background: linear-gradient(135deg, #ff9a76 0%, #4a90e2 100%);
|
|
57
|
-
-webkit-background-clip: text; background-clip: text;
|
|
58
|
-
-webkit-text-fill-color: transparent; color: transparent;
|
|
59
|
-
letter-spacing: -0.01em;
|
|
60
|
-
}
|
|
61
|
-
.logo .dot {
|
|
62
|
-
width: 6px; height: 6px; border-radius: 50%;
|
|
63
|
-
background: var(--accent); animation: pulse 1.6s ease infinite;
|
|
64
|
-
box-shadow: 0 0 0 3px rgba(255,154,118,0.18);
|
|
65
|
-
flex-shrink: 0;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* A-04 · local-first status pill · pinned next to the logo so the privacy
|
|
69
|
-
* promise is the SECOND thing every user sees · zero telemetry, all data
|
|
70
|
-
* on this machine. The pulsing dot says "still alive" without occupying
|
|
71
|
-
* focus. */
|
|
72
|
-
.local-pill {
|
|
73
|
-
display: inline-flex; align-items: center; gap: 6px;
|
|
74
|
-
font-family: var(--mono); font-size: 10px; color: var(--fg-50);
|
|
75
|
-
padding: 3px 8px; border: 1px solid var(--border); border-radius: 999px;
|
|
76
|
-
background: rgba(40,200,64,0.04);
|
|
77
|
-
}
|
|
78
|
-
.local-pill .dot {
|
|
79
|
-
width: 6px; height: 6px; border-radius: 50%;
|
|
80
|
-
background: var(--allow); animation: pulse 2.4s ease infinite;
|
|
81
|
-
}
|
|
82
|
-
.local-pill:hover { color: var(--fg-70); border-color: var(--fg-30); }
|
|
83
|
-
|
|
84
|
-
/* Beta badge · honest product-stage signal pinned to the wordmark. codehere
|
|
85
|
-
* is pre-1.0 and heading to invite-only validation; the badge sets that
|
|
86
|
-
* expectation up front rather than implying a finished GA product. Brand
|
|
87
|
-
* tint (the only colored chip besides the green local-pill) so it reads as
|
|
88
|
-
* intentional, not a warning. */
|
|
89
|
-
.beta-badge {
|
|
90
|
-
font-family: var(--mono); font-size: 9px; font-weight: 700;
|
|
91
|
-
letter-spacing: 0.06em; text-transform: uppercase; line-height: 1;
|
|
92
|
-
color: var(--accent, #ff9a76); padding: 3px 7px; border-radius: 999px;
|
|
93
|
-
border: 1px solid rgba(255, 154, 118, 0.45);
|
|
94
|
-
background: rgba(255, 154, 118, 0.12);
|
|
95
|
-
align-self: center; cursor: default;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/* Header budget chip · matches demo v12 spec · bordered chip with padding
|
|
99
|
-
* so the cost signal looks like a deliberate object, not a stray number. */
|
|
100
|
-
.budget {
|
|
101
|
-
margin-left: auto; font-family: var(--mono); font-size: 11px;
|
|
102
|
-
color: var(--fg-70); padding: 3px 9px;
|
|
103
|
-
border: 1px solid var(--border); border-radius: 4px;
|
|
104
|
-
letter-spacing: 0.02em;
|
|
105
|
-
}
|
|
106
|
-
.budget.safe { color: var(--fg-70); border-color: var(--border); }
|
|
107
|
-
.budget.warn { color: var(--warn); border-color: rgba(254,188,46,0.4); background: rgba(254,188,46,0.04); }
|
|
108
|
-
.budget.block { color: var(--block); border-color: rgba(255,95,87,0.4); background: rgba(255,95,87,0.05); font-weight: 600; }
|
|
109
|
-
|
|
110
|
-
/* D-01 · file tree with trust grades · in the task detail · render each
|
|
111
|
-
* file as a chip with +N/-M diff counts and a trust grade pill. */
|
|
112
|
-
.file-chip {
|
|
113
|
-
display: inline-flex; align-items: center; gap: 8px;
|
|
114
|
-
padding: 5px 10px; margin: 4px 6px 4px 0;
|
|
115
|
-
border: 1px solid var(--border-soft); border-radius: 6px;
|
|
116
|
-
background: rgba(255,255,255,0.02);
|
|
117
|
-
font-family: var(--mono); font-size: 11px;
|
|
118
|
-
}
|
|
119
|
-
.file-chip .path { color: var(--fg-90); }
|
|
120
|
-
.file-chip .added { color: var(--allow); font-size: 10px; }
|
|
121
|
-
.file-chip .removed { color: var(--block); font-size: 10px; }
|
|
122
|
-
.file-chip .trust { font-size: 9px; padding: 1px 5px; border-radius: 3px; font-weight: 600; }
|
|
123
|
-
.file-chip .trust.A { color: var(--allow); background: rgba(40,200,64,0.10); }
|
|
124
|
-
.file-chip .trust.B { color: #8be88b; background: rgba(40,200,64,0.06); }
|
|
125
|
-
.file-chip .trust.C { color: var(--warn); background: rgba(254,188,46,0.10); }
|
|
126
|
-
.file-chip .trust.F { color: var(--block); background: rgba(255,95,87,0.10); }
|
|
127
|
-
/* File tree · dir-grouped with connectors + per-row trust (demo 2.5) */
|
|
128
|
-
.file-tree { font-family: var(--mono); font-size: 11px; margin-bottom: 8px; }
|
|
129
|
-
.ft-dir { color: var(--fg-50); font-size: 10.5px; margin: 6px 0 2px; }
|
|
130
|
-
.ft-row { display: flex; align-items: center; gap: 6px; padding: 2px 0 2px 8px; }
|
|
131
|
-
.ft-branch { color: var(--fg-30); }
|
|
132
|
-
.ft-name { color: var(--fg-80); }
|
|
133
|
-
.ft-row .added { color: var(--allow); font-size: 10px; }
|
|
134
|
-
.ft-row .removed { color: var(--block); font-size: 10px; }
|
|
135
|
-
.ft-row .trust { font-size: 9px; padding: 1px 5px; border-radius: 3px; font-weight: 600; margin-left: auto; }
|
|
136
|
-
.ft-row .trust.A { color: var(--allow); background: rgba(40,200,64,0.10); }
|
|
137
|
-
.ft-row .trust.B { color: #8be88b; background: rgba(40,200,64,0.06); }
|
|
138
|
-
.ft-row .trust.C { color: var(--warn); background: rgba(254,188,46,0.10); }
|
|
139
|
-
.ft-row .trust.F { color: var(--block); background: rgba(255,95,87,0.10); }
|
|
140
|
-
|
|
141
|
-
/* Inline code-card · matches demo v12 (lines 441-454) · renders when an
|
|
142
|
-
* artifact file carries a .snippet. Dark VS-Code-ish surface with mono
|
|
143
|
-
* font, line numbers, lightweight syntax tinting via .kw/.fn/.str/.num/.cm.
|
|
144
|
-
* Lives directly under the file-chip strip so the reviewer can SEE what
|
|
145
|
-
* the agent wrote, not just "+118 lines". */
|
|
146
|
-
.code-card {
|
|
147
|
-
background: hsl(0 0% 12%); border: 1px solid var(--border);
|
|
148
|
-
border-radius: 6px; overflow: hidden; margin: 6px 0 10px;
|
|
149
|
-
}
|
|
150
|
-
.code-card .head {
|
|
151
|
-
padding: 7px 12px; background: rgba(255,255,255,0.03);
|
|
152
|
-
border-bottom: 1px solid var(--border-soft);
|
|
153
|
-
display: flex; align-items: center; gap: 8px;
|
|
154
|
-
font-family: var(--mono); font-size: 10.5px; color: var(--fg-50);
|
|
155
|
-
}
|
|
156
|
-
.code-card .head .lang { color: var(--fg-30); }
|
|
157
|
-
.code-card .head .path { color: var(--fg-70); }
|
|
158
|
-
.code-card .head .meta { margin-left: auto; color: var(--fg-30); font-size: 10px; }
|
|
159
|
-
.code-card pre {
|
|
160
|
-
margin: 0; padding: 11px 14px;
|
|
161
|
-
font-family: var(--mono); font-size: 11.5px; line-height: 1.6;
|
|
162
|
-
color: var(--fg-70); overflow-x: auto; white-space: pre;
|
|
163
|
-
}
|
|
164
|
-
.code-card .ln {
|
|
165
|
-
display: inline-block; width: 26px; text-align: right; margin-right: 12px;
|
|
166
|
-
color: var(--fg-20); user-select: none;
|
|
167
|
-
}
|
|
168
|
-
.code-card .kw { color: #c586c0; }
|
|
169
|
-
.code-card .fn { color: #dcdcaa; }
|
|
170
|
-
.code-card .str { color: #ce9178; }
|
|
171
|
-
.code-card .num { color: #b5cea8; }
|
|
172
|
-
.code-card .cm { color: #6a9955; font-style: italic; }
|
|
173
|
-
.code-card .tp { color: #4ec9b0; }
|
|
174
|
-
|
|
175
|
-
/* D-04 · test result chip · single line with pass/fail counts + duration */
|
|
176
|
-
.test-chip {
|
|
177
|
-
display: inline-flex; align-items: center; gap: 8px;
|
|
178
|
-
padding: 5px 10px; margin: 4px 0;
|
|
179
|
-
border: 1px solid var(--border-soft); border-radius: 6px;
|
|
180
|
-
font-family: var(--mono); font-size: 11px;
|
|
181
|
-
background: rgba(40,200,64,0.04);
|
|
182
|
-
}
|
|
183
|
-
.test-chip.has-failures { background: rgba(255,95,87,0.05); }
|
|
184
|
-
.test-chip .passed { color: var(--allow); }
|
|
185
|
-
.test-chip .failed { color: var(--block); }
|
|
186
|
-
.test-chip .dur { color: var(--fg-40); }
|
|
187
|
-
|
|
188
|
-
/* D-05 · diagnose+fix loop · groups consecutive test-failure → edit → re-test
|
|
189
|
-
* sequences visually so the reviewer can see "tried, failed, fixed, re-ran" */
|
|
190
|
-
.fix-loop {
|
|
191
|
-
border-left: 2px solid var(--warn); padding-left: 10px;
|
|
192
|
-
margin: 8px 0;
|
|
193
|
-
}
|
|
194
|
-
.fix-loop-label {
|
|
195
|
-
font-family: var(--mono); font-size: 9px; color: var(--warn);
|
|
196
|
-
text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/* C-01 · first-run indexing banner · shows once per first-task, then fades */
|
|
200
|
-
.indexing-banner {
|
|
201
|
-
display: none;
|
|
202
|
-
padding: 6px 14px; margin-bottom: 10px;
|
|
203
|
-
background: rgba(74,144,226,0.06); border: 1px solid rgba(74,144,226,0.18);
|
|
204
|
-
border-radius: 6px;
|
|
205
|
-
font-family: var(--mono); font-size: 11px; color: var(--info, #4a90e2);
|
|
206
|
-
}
|
|
207
|
-
.indexing-banner .what { color: var(--fg-70); font-weight: 600; margin-right: 6px; }
|
|
208
|
-
|
|
209
|
-
/* C-02 · tool-call card · matches demo v12 .tc + .tc-out (lines 312-318).
|
|
210
|
-
* Each tool call gets a darker card surface with a bolded tool name, dim
|
|
211
|
-
* target path, right-aligned duration · output block below uses brighter
|
|
212
|
-
* fg so the agent's report is readable rather than disappearing into the
|
|
213
|
-
* card body. */
|
|
214
|
-
.tc-card {
|
|
215
|
-
border: 1px solid var(--border); border-radius: 5px;
|
|
216
|
-
padding: 8px 11px; margin: 7px 0;
|
|
217
|
-
background: hsl(0 0% 9%);
|
|
218
|
-
}
|
|
219
|
-
.tc-card .head {
|
|
220
|
-
display: flex; align-items: baseline; gap: 7px;
|
|
221
|
-
font-family: var(--mono); font-size: 11px; flex-wrap: wrap;
|
|
222
|
-
}
|
|
223
|
-
.tc-card .tool { color: var(--fg); font-weight: 600; }
|
|
224
|
-
.tc-card .target { color: var(--fg-70); }
|
|
225
|
-
.tc-card .hook-badge {
|
|
226
|
-
font-family: var(--mono); font-size: 9px; padding: 1px 5px; border-radius: 3px;
|
|
227
|
-
text-transform: uppercase; letter-spacing: 0.04em;
|
|
228
|
-
}
|
|
229
|
-
.tc-card .hook-badge.allow { color: var(--allow); background: rgba(40,200,64,0.10); }
|
|
230
|
-
.tc-card .hook-badge.clean { color: #8be88b; background: rgba(40,200,64,0.06); }
|
|
231
|
-
.tc-card .hook-badge.block { color: var(--block); background: rgba(255,95,87,0.10); }
|
|
232
|
-
.tc-card .hook-badge.override { color: var(--warn); background: rgba(254,188,46,0.10); }
|
|
233
|
-
.tc-card .dur {
|
|
234
|
-
color: var(--fg-30); margin-left: auto; font-family: var(--mono); font-size: 10px;
|
|
235
|
-
}
|
|
236
|
-
.tc-card .output {
|
|
237
|
-
margin: 5px 0 0; padding: 6px 9px;
|
|
238
|
-
font-family: var(--mono); font-size: 10.5px;
|
|
239
|
-
color: var(--fg-70); line-height: 1.55;
|
|
240
|
-
background: rgba(0,0,0,0.25); border-radius: 4px;
|
|
241
|
-
white-space: pre-wrap; max-height: 120px; overflow: auto;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/* E-03 · hook BLOCK affordance · override-with-reason · red-bordered card
|
|
245
|
-
* with 3 action buttons. */
|
|
246
|
-
.hook-block-card {
|
|
247
|
-
border: 1px solid rgba(255,95,87,0.35); border-radius: 8px;
|
|
248
|
-
padding: 12px 14px; margin: 10px 0;
|
|
249
|
-
background: rgba(255,95,87,0.04);
|
|
250
|
-
}
|
|
251
|
-
.hook-block-title { font-family: var(--mono); font-size: 10px; color: var(--block); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
|
|
252
|
-
.hook-block-msg { color: var(--fg-90); font-size: 13px; margin-bottom: 10px; }
|
|
253
|
-
.hook-block-actions { display: flex; gap: 8px; }
|
|
254
|
-
.hook-block-actions button {
|
|
255
|
-
font-family: var(--mono); font-size: 11px;
|
|
256
|
-
padding: 5px 12px; border-radius: 5px; cursor: pointer;
|
|
257
|
-
border: 1px solid var(--border); background: transparent; color: var(--fg-70);
|
|
258
|
-
}
|
|
259
|
-
.hook-block-actions button:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
260
|
-
|
|
261
|
-
/* E-07 · push gate · same hook-block pattern but warn-tinted for git push */
|
|
262
|
-
.push-gate-card {
|
|
263
|
-
border: 1px solid rgba(254,188,46,0.35); border-radius: 8px;
|
|
264
|
-
padding: 12px 14px; margin: 10px 0;
|
|
265
|
-
background: rgba(254,188,46,0.04);
|
|
266
|
-
}
|
|
267
|
-
.push-gate-title { font-family: var(--mono); font-size: 10px; color: var(--warn); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
|
|
268
|
-
|
|
269
|
-
/* P3 · Recovery affordance · session-level failure with explanatory copy
|
|
270
|
-
+ user options (retry · diagnose · mark resolved). Demo v12 U10 spec.
|
|
271
|
-
Distinct from hook-block (agent BLOCKED by safety) — recovery covers
|
|
272
|
-
agent CRASHED / errored without a deliberate block. */
|
|
273
|
-
.recovery-card {
|
|
274
|
-
border: 1px solid rgba(255,95,87,0.35); border-radius: 8px;
|
|
275
|
-
padding: 14px 16px; margin: 10px 0;
|
|
276
|
-
background: rgba(255,95,87,0.04);
|
|
277
|
-
border-left: 3px solid var(--block);
|
|
278
|
-
}
|
|
279
|
-
.recovery-title { font-family: var(--mono); font-size: 10px; color: var(--block); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
|
280
|
-
.recovery-title .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--block); }
|
|
281
|
-
.recovery-cause { color: var(--fg-90); font-size: 13px; line-height: 1.5; margin-bottom: 6px; }
|
|
282
|
-
.recovery-cause b { color: var(--fg); font-weight: 600; }
|
|
283
|
-
.git-note .git-note-body { color: var(--fg-70); font-size: 12.5px; line-height: 1.55; }
|
|
284
|
-
.git-note .git-note-body code { font-family: var(--mono); font-size: 11.5px; background: var(--bg-2); padding: 1px 5px; border-radius: 3px; }
|
|
285
|
-
.recovery-resolve { color: var(--fg-70); font-size: 12.5px; line-height: 1.55; margin: 6px 0 10px; padding: 8px 11px; background: rgba(255,255,255,0.025); border-radius: 5px; border-left: 2px solid var(--fg-30); }
|
|
286
|
-
.recovery-resolve b { color: var(--fg); font-weight: 500; }
|
|
287
|
-
.recovery-actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
|
|
288
|
-
.recovery-actions button {
|
|
289
|
-
font-family: var(--sans); font-size: 12px; padding: 7px 14px; border-radius: 5px; cursor: pointer;
|
|
290
|
-
border: 1px solid var(--border); background: transparent; color: var(--fg-70); transition: all 160ms ease;
|
|
291
|
-
}
|
|
292
|
-
.recovery-actions button:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
293
|
-
.recovery-actions button.primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 600; }
|
|
294
|
-
.recovery-actions button.primary:hover { box-shadow: 0 0 0 3px rgba(255,154,118,0.20); }
|
|
295
|
-
|
|
296
|
-
/* G-U10 · crash recovery surface · shown when session was orphaned · "the
|
|
297
|
-
* daemon was killed mid-task · you can resume or discard" */
|
|
298
|
-
.crash-recovery {
|
|
299
|
-
border: 1px solid var(--border); border-radius: 8px;
|
|
300
|
-
padding: 10px 14px; margin: 10px 0;
|
|
301
|
-
background: rgba(255,255,255,0.02);
|
|
302
|
-
}
|
|
303
|
-
.crash-recovery-title { font-family: var(--mono); font-size: 10px; color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px; }
|
|
304
|
-
|
|
305
|
-
/* v0.8 S3 · Reshape view · ported from demo v12 lines 73-132 + 2486-2534.
|
|
306
|
-
* Demo v12's W-W4 differentiator · user describes the view they want,
|
|
307
|
-
* agent "composes" it from shared primitives. Backed by body.shape-*
|
|
308
|
-
* classes + a custom-view banner. Three preset shapes (ship-only,
|
|
309
|
-
* velocity, security) plus a free-text input that naive-classifies to
|
|
310
|
-
* the closest preset. */
|
|
311
|
-
.reshape-btn {
|
|
312
|
-
background: linear-gradient(135deg, rgba(255,154,118,0.14), rgba(74,144,226,0.10));
|
|
313
|
-
border: 1px solid rgba(255,154,118,0.4); border-radius: 5px;
|
|
314
|
-
padding: 5px 12px; font-family: var(--mono); font-size: 10.5px;
|
|
315
|
-
color: var(--fg); cursor: pointer; letter-spacing: 0.04em;
|
|
316
|
-
transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
317
|
-
display: inline-flex; align-items: center; gap: 6px; font-weight: 500;
|
|
318
|
-
}
|
|
319
|
-
.reshape-btn:hover {
|
|
320
|
-
background: linear-gradient(135deg, rgba(255,154,118,0.22), rgba(74,144,226,0.16));
|
|
321
|
-
border-color: var(--accent);
|
|
322
|
-
box-shadow: 0 0 0 3px rgba(255,154,118,0.15);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/* v0.8 S4 · share-thread button + popover · per demo v12 W-W2 */
|
|
326
|
-
.share-btn-hdr {
|
|
327
|
-
background: transparent; border: 1px solid var(--border); border-radius: 5px;
|
|
328
|
-
padding: 5px 11px; font-family: var(--mono); font-size: 10.5px;
|
|
329
|
-
color: var(--fg-50); cursor: pointer; letter-spacing: 0.04em;
|
|
330
|
-
transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
331
|
-
display: inline-flex; align-items: center; gap: 6px;
|
|
332
|
-
}
|
|
333
|
-
.share-btn-hdr:hover { color: var(--fg); border-color: var(--fg-30); background: rgba(255,255,255,0.03); }
|
|
334
|
-
.share-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(6px); display: none; align-items: center; justify-content: center; z-index: 310; padding: 24px; }
|
|
335
|
-
.share-modal.open { display: flex; animation: rs-fade 240ms ease; }
|
|
336
|
-
.share-modal.open .share-card { animation: wp-card-in 380ms cubic-bezier(0.16, 1, 0.3, 1); }
|
|
337
|
-
/* Share card · gradient border via mask trick (same pattern as
|
|
338
|
-
* reshape-card · same brand alpha 0.45 demo uses for recomposing card
|
|
339
|
-
* lines 89-90). Consistency · both "agent acts on your behalf" modals
|
|
340
|
-
* read as the same family. */
|
|
341
|
-
.share-card { width: 100%; max-width: 540px; background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; padding: 20px 22px; position: relative; overflow: hidden; }
|
|
342
|
-
.share-card::before {
|
|
343
|
-
content: ''; position: absolute; inset: -1px;
|
|
344
|
-
border-radius: inherit; padding: 1px;
|
|
345
|
-
background: linear-gradient(135deg, rgba(255,154,118,0.45), rgba(74,144,226,0.45));
|
|
346
|
-
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
347
|
-
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
348
|
-
-webkit-mask-composite: xor; mask-composite: exclude;
|
|
349
|
-
pointer-events: none;
|
|
350
|
-
}
|
|
351
|
-
.share-card .head { display: flex; align-items: center; gap: 8px; color: var(--fg); font-weight: 600; font-size: 13px; }
|
|
352
|
-
.share-card .head .tag { color: var(--fg-40); font-weight: 400; font-size: 11px; font-family: var(--mono); }
|
|
353
|
-
.share-card .blurb { color: var(--fg-70); font-size: 12.5px; margin: 10px 0; line-height: 1.55; }
|
|
354
|
-
/* Signed-payload link · brand-tinted so the value reads as "this is the
|
|
355
|
-
* codehere artifact you're about to share" · not a generic URL. The
|
|
356
|
-
* canonical brand var is --accent in product, --brand in demo — same
|
|
357
|
-
* #ff9a76. The original css here referenced --brand (undefined in
|
|
358
|
-
* product) and silently fell back to default fg-light. */
|
|
359
|
-
.share-link-box { background: hsl(0 0% 9%); border: 1px solid var(--border); border-radius: 6px; padding: 9px 12px; font-family: var(--mono); font-size: 11px; color: var(--accent); margin: 10px 0; word-break: break-all; max-height: 96px; overflow: auto; }
|
|
360
|
-
.share-actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
|
|
361
|
-
.share-actions button { font-family: var(--sans); font-size: 12px; padding: 7px 14px; border-radius: 5px; cursor: pointer; }
|
|
362
|
-
.share-btn-primary { background: var(--accent); color: #000; border: 0; font-weight: 600; }
|
|
363
|
-
.share-btn-primary:hover { filter: brightness(1.08); }
|
|
364
|
-
.share-btn-secondary { background: transparent; color: var(--fg-50); border: 1px solid var(--border); }
|
|
365
|
-
.share-btn-secondary:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
366
|
-
.share-foot { color: var(--fg-40); font-size: 11px; margin-top: 12px; font-family: var(--mono); line-height: 1.5; }
|
|
367
|
-
.share-toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--bg-1); border: 1px solid var(--border); color: var(--fg); padding: 10px 18px; border-radius: 6px; font-size: 12px; box-shadow: 0 6px 24px rgba(0,0,0,0.4); z-index: 320; opacity: 0; pointer-events: none; transition: opacity 240ms ease; }
|
|
368
|
-
.share-toast.show { opacity: 1; }
|
|
369
|
-
|
|
370
|
-
/* v0.9 S6 · W-PASS approval modal · demo v12 lines 2030-2050 */
|
|
371
|
-
.wp-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; z-index: 330; padding: 24px; }
|
|
372
|
-
.wp-modal.open { display: flex; animation: rs-fade 280ms ease; }
|
|
373
|
-
.wp-modal.open .wp-card { animation: wp-card-in 380ms cubic-bezier(0.16, 1, 0.3, 1); }
|
|
374
|
-
@keyframes wp-card-in {
|
|
375
|
-
from { opacity: 0; transform: translateY(12px) scale(0.985); }
|
|
376
|
-
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
377
|
-
}
|
|
378
|
-
.wp-card { width: 100%; max-width: 620px; background: var(--bg-1); border: 1px solid rgba(255,154,118,0.40); border-radius: 12px; padding: 22px 24px; position: relative; box-shadow: 0 12px 48px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,154,118,0.06); }
|
|
379
|
-
.wp-banner { display: flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 10.5px; color: var(--accent); letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 4px; }
|
|
380
|
-
.wp-banner .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px rgba(255,154,118,0.18); animation: wp-pulse 1.6s ease-in-out infinite; }
|
|
381
|
-
@keyframes wp-pulse { 0%,100% { box-shadow: 0 0 0 3px rgba(255,154,118,0.18); } 50% { box-shadow: 0 0 0 7px rgba(255,154,118,0.04); } }
|
|
382
|
-
.wp-title { font-size: 16px; font-weight: 600; color: var(--fg); margin: 4px 0 6px; letter-spacing: -0.01em; }
|
|
383
|
-
.wp-reason { font-size: 12.5px; color: var(--fg-70); line-height: 1.5; margin: 0 0 14px; padding: 8px 11px; background: rgba(255,255,255,0.025); border-left: 2px solid var(--accent); border-radius: 0 4px 4px 0; }
|
|
384
|
-
.wp-meta { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); margin: 8px 0; line-height: 1.6; }
|
|
385
|
-
.wp-meta b { color: var(--fg-90); }
|
|
386
|
-
.wp-payload-wrap { background: var(--bg-2); border: 1px solid var(--border); border-radius: 6px; margin: 12px 0; max-height: 240px; overflow: auto; }
|
|
387
|
-
.wp-payload-head { display: flex; align-items: center; gap: 8px; padding: 6px 11px; background: rgba(255,255,255,0.025); border-bottom: 1px solid var(--border); font-family: var(--mono); font-size: 10px; color: var(--fg-50); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
388
|
-
.wp-payload-head .method { color: var(--accent); font-weight: 600; }
|
|
389
|
-
.wp-payload { padding: 10px 12px; font-family: var(--mono); font-size: 11px; color: var(--fg-90); white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
|
|
390
|
-
.wp-payload-edit { width: 100%; min-height: 140px; background: var(--bg-2); border: 1px solid var(--accent); border-radius: 6px; padding: 10px 12px; font-family: var(--mono); font-size: 11px; color: var(--fg-90); resize: vertical; outline: none; }
|
|
391
|
-
.wp-actions { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
|
|
392
|
-
.wp-actions button { font-family: var(--sans); font-size: 12px; padding: 8px 16px; border-radius: 5px; cursor: pointer; font-weight: 500; transition: all 160ms ease; }
|
|
393
|
-
.wp-btn-approve { background: var(--accent); color: #000; border: 0; }
|
|
394
|
-
.wp-btn-approve:hover { box-shadow: 0 0 0 3px rgba(255,154,118,0.20); }
|
|
395
|
-
.wp-btn-edit { background: transparent; color: var(--fg); border: 1px solid var(--border); }
|
|
396
|
-
.wp-btn-edit:hover { border-color: var(--accent); color: var(--accent); }
|
|
397
|
-
.wp-btn-cancel { background: transparent; color: var(--fg-50); border: 1px solid var(--border); }
|
|
398
|
-
.wp-btn-cancel:hover { color: var(--block); border-color: var(--block); }
|
|
399
|
-
.wp-btn-save { background: var(--accent); color: #000; border: 0; }
|
|
400
|
-
.wp-foot { font-family: var(--mono); font-size: 10.5px; color: var(--fg-40); margin-top: 12px; line-height: 1.55; }
|
|
401
|
-
.wp-result { margin-top: 12px; padding: 8px 11px; border-radius: 4px; font-size: 11.5px; line-height: 1.5; }
|
|
402
|
-
.wp-result.ok { background: rgba(40,200,64,0.08); border: 1px solid rgba(40,200,64,0.30); color: var(--allow); }
|
|
403
|
-
.wp-result.fail { background: rgba(254,80,80,0.08); border: 1px solid rgba(254,80,80,0.30); color: var(--block); }
|
|
404
|
-
.wp-result.cancelled { background: rgba(255,255,255,0.04); border: 1px solid var(--border); color: var(--fg-50); }
|
|
405
|
-
.wp-pending-badge { position: fixed; top: 14px; right: 220px; background: var(--accent); color: #000; padding: 4px 10px; border-radius: 5px; font-family: var(--mono); font-size: 10.5px; font-weight: 600; cursor: pointer; display: none; z-index: 305; box-shadow: 0 4px 16px rgba(255,154,118,0.30); animation: wp-pulse 1.6s ease-in-out infinite; letter-spacing: 0.03em; }
|
|
406
|
-
|
|
407
|
-
/* Per-domain mediated-write trust grade chip · v0.9 must-ship */
|
|
408
|
-
.wp-trust-grade { display: inline-block; padding: 2px 7px; border-radius: 3px; font-family: var(--mono); font-size: 10.5px; font-weight: 700; letter-spacing: 0.03em; margin: 0 4px; }
|
|
409
|
-
.wp-trust-grade.A { color: var(--allow); background: rgba(40,200,64,0.12); }
|
|
410
|
-
.wp-trust-grade.B { color: #8be88b; background: rgba(40,200,64,0.08); }
|
|
411
|
-
.wp-trust-grade.C { color: var(--warn); background: rgba(254,188,46,0.10); }
|
|
412
|
-
.wp-trust-grade.D { color: var(--warn); background: rgba(254,188,46,0.15); }
|
|
413
|
-
.wp-trust-grade.F { color: var(--block); background: rgba(255,95,87,0.12); }
|
|
414
|
-
.wp-trust-prompt { margin-top: 10px; padding: 9px 11px; background: rgba(255,95,87,0.06); border: 1px solid rgba(255,95,87,0.30); border-radius: 5px; font-size: 12px; color: var(--fg-90); }
|
|
415
|
-
.wp-trust-prompt input { margin-right: 6px; }
|
|
416
|
-
.wp-pending-badge.show { display: inline-block; }
|
|
417
|
-
|
|
418
|
-
/* v0.9 S7 · overlay-renderer · inspect-and-act for ctx-source URLs */
|
|
419
|
-
.or-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.72); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; z-index: 295; padding: 24px; }
|
|
420
|
-
.or-overlay.open { display: flex; animation: rs-fade 280ms ease; }
|
|
421
|
-
.or-overlay.open .or-card { animation: wp-card-in 380ms cubic-bezier(0.16, 1, 0.3, 1); }
|
|
422
|
-
.or-card { width: 100%; max-width: 720px; max-height: calc(100vh - 48px); background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
|
|
423
|
-
.or-card::before { content: ''; position: absolute; inset: -1px; border-radius: inherit; padding: 1px; background: linear-gradient(135deg, rgba(74,144,226,0.4), rgba(255,154,118,0.4)); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; }
|
|
424
|
-
.or-head { display: flex; align-items: center; gap: 10px; padding: 11px 15px; border-bottom: 1px solid var(--border); font-family: var(--mono); font-size: 11px; color: var(--fg-70); flex-shrink: 0; }
|
|
425
|
-
.or-head .src { color: var(--brand-blue, #4a90e2); font-weight: 600; }
|
|
426
|
-
.or-head .meta { color: var(--fg-50); }
|
|
427
|
-
.or-head .audit { margin-left: auto; padding: 2px 8px; background: rgba(40,200,64,0.06); border: 1px solid rgba(40,200,64,0.25); border-radius: 3px; font-size: 10px; color: var(--fg-50); }
|
|
428
|
-
.or-head .audit b { color: var(--allow); }
|
|
429
|
-
.or-head .or-close { background: transparent; border: 0; color: var(--fg-50); cursor: pointer; padding: 2px 8px; border-radius: 4px; font-size: 16px; line-height: 1; }
|
|
430
|
-
.or-head .or-close:hover { color: var(--fg); background: var(--bg-2); }
|
|
431
|
-
.or-body { padding: 16px 20px; overflow-y: auto; flex: 1; }
|
|
432
|
-
.or-body h3 { font-size: 17px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; margin: 0 0 4px; }
|
|
433
|
-
.or-body .url-row { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); margin-bottom: 12px; word-break: break-all; }
|
|
434
|
-
.or-body .meta-row { display: flex; gap: 6px; flex-wrap: wrap; margin: 6px 0 14px; font-family: var(--mono); font-size: 10.5px; }
|
|
435
|
-
.or-body .meta-row .pill { padding: 2px 7px; border: 1px solid var(--border); border-radius: 3px; color: var(--fg-70); background: var(--bg-2); }
|
|
436
|
-
.or-body .meta-row .pill.state-open { color: var(--allow); border-color: rgba(40,200,64,0.30); background: rgba(40,200,64,0.06); }
|
|
437
|
-
.or-body .meta-row .pill.state-closed { color: var(--block); border-color: rgba(254,80,80,0.30); background: rgba(254,80,80,0.06); }
|
|
438
|
-
.or-body .preview-text { padding: 12px 14px; background: rgba(255,255,255,0.025); border: 1px solid var(--border); border-radius: 6px; font-family: var(--mono); font-size: 11.5px; color: var(--fg-90); line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 360px; overflow-y: auto; }
|
|
439
|
-
.or-body .preview-empty { padding: 18px; text-align: center; color: var(--fg-50); font-size: 12px; }
|
|
440
|
-
.or-body .preview-error { padding: 14px 16px; background: rgba(254,80,80,0.06); border: 1px solid rgba(254,80,80,0.30); border-radius: 6px; color: var(--block); font-size: 12.5px; line-height: 1.5; }
|
|
441
|
-
.or-body .preview-error .hint { color: var(--fg-50); font-family: var(--mono); font-size: 10.5px; margin-top: 6px; }
|
|
442
|
-
.or-body .preview-loading { display: flex; align-items: center; gap: 8px; padding: 16px; color: var(--fg-50); font-family: var(--mono); font-size: 11.5px; }
|
|
443
|
-
.or-body .preview-loading::before { content: ''; width: 10px; height: 10px; border: 2px solid var(--fg-30); border-top-color: var(--accent); border-radius: 50%; animation: or-spin 700ms linear infinite; }
|
|
444
|
-
@keyframes or-spin { to { transform: rotate(360deg); } }
|
|
445
|
-
.or-composer { padding: 12px 14px; background: var(--bg-2); border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; }
|
|
446
|
-
.or-composer .hint { font-family: var(--mono); font-size: 10px; color: var(--fg-50); letter-spacing: 0.06em; }
|
|
447
|
-
.or-composer .row { display: flex; gap: 8px; align-items: center; }
|
|
448
|
-
.or-composer input { flex: 1; background: var(--bg-1); border: 1px solid var(--border); border-radius: 6px; padding: 8px 11px; color: var(--fg); font-family: var(--sans); font-size: 13px; outline: none; transition: border-color 160ms ease; }
|
|
449
|
-
.or-composer input:focus { border-color: var(--accent); }
|
|
450
|
-
.or-composer button { background: var(--accent); color: #000; border: 0; border-radius: 6px; padding: 8px 14px; font-family: var(--sans); font-size: 12.5px; font-weight: 600; cursor: pointer; }
|
|
451
|
-
.or-composer button:hover { box-shadow: 0 0 0 3px rgba(255,154,118,0.18); }
|
|
452
|
-
.or-composer button:disabled { opacity: 0.40; cursor: default; box-shadow: none; }
|
|
453
|
-
|
|
454
|
-
/* Linear inline render · per demo v12 lines 569-600. Authentic Linear
|
|
455
|
-
ticket card displayed inside the overlay when previewing linear://. */
|
|
456
|
-
.linear-render { background: #1c1c1e; color: #ebeaef; padding: 24px 28px; border-radius: 6px; font-family: 'Inter', -apple-system, sans-serif; }
|
|
457
|
-
.linear-id { font-family: var(--mono); font-size: 11px; color: #8a8a8e; letter-spacing: 0.04em; margin-bottom: 8px; }
|
|
458
|
-
.linear-title { font-size: 19px; font-weight: 600; color: #ebeaef; line-height: 1.35; margin-bottom: 14px; letter-spacing: -0.01em; }
|
|
459
|
-
.linear-props { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 16px; }
|
|
460
|
-
.linear-pill { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 4px; font-size: 11.5px; background: rgba(255,255,255,0.04); color: #b8b7bc; }
|
|
461
|
-
.linear-pri-urgent { color: #f87171; }
|
|
462
|
-
.linear-pri-bars { display: inline-flex; align-items: flex-end; gap: 1.5px; width: 12px; height: 12px; }
|
|
463
|
-
.linear-pri-bars div { width: 2px; background: currentColor; border-radius: 0.5px; }
|
|
464
|
-
.linear-pri-bars div:nth-child(1) { height: 4px; }
|
|
465
|
-
.linear-pri-bars div:nth-child(2) { height: 7px; }
|
|
466
|
-
.linear-pri-bars div:nth-child(3) { height: 10px; }
|
|
467
|
-
.linear-status-progress { color: #fbbf24; }
|
|
468
|
-
.linear-status-circle { width: 12px; height: 12px; border-radius: 50%; border: 1.5px solid currentColor; position: relative; box-sizing: border-box; }
|
|
469
|
-
.linear-status-circle::after { content: ''; position: absolute; inset: 0; background: currentColor; border-radius: 50%; clip-path: polygon(0 0, 50% 0, 50% 100%, 0% 100%); }
|
|
470
|
-
.linear-assignee { background: rgba(255,255,255,0.06); }
|
|
471
|
-
.linear-assignee .av { width: 16px; height: 16px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 9px; font-weight: 700; color: #fff; font-family: var(--mono); }
|
|
472
|
-
.linear-label { font-weight: 500; }
|
|
473
|
-
.linear-desc { font-size: 13.5px; color: #b8b7bc; line-height: 1.65; padding: 14px 16px; background: rgba(255,255,255,0.025); border-radius: 6px; margin-bottom: 18px; }
|
|
474
|
-
.linear-desc code { background: rgba(255,255,255,0.07); padding: 1px 5px; border-radius: 3px; font-family: var(--mono); font-size: 12px; color: #ebeaef; }
|
|
475
|
-
.linear-sep { display: flex; align-items: center; gap: 10px; margin: 14px 0 12px; }
|
|
476
|
-
.linear-sep .line { flex: 1; height: 1px; background: rgba(255,255,255,0.08); }
|
|
477
|
-
.linear-sep .lbl { font-family: var(--mono); font-size: 10px; color: #6f6e74; letter-spacing: 0.1em; text-transform: uppercase; }
|
|
478
|
-
.linear-comment { display: flex; gap: 11px; padding: 10px 0; }
|
|
479
|
-
.linear-comment .av { width: 24px; height: 24px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; color: #fff; font-family: var(--mono); flex-shrink: 0; background: #5e6ad2; }
|
|
480
|
-
.linear-comment .col { flex: 1; min-width: 0; }
|
|
481
|
-
.linear-comment .who { display: flex; align-items: baseline; gap: 7px; font-size: 12.5px; font-weight: 600; color: #ebeaef; margin-bottom: 2px; }
|
|
482
|
-
.linear-comment .ts { color: #6f6e74; font-size: 11px; font-weight: 400; }
|
|
483
|
-
.linear-comment .body { font-size: 13px; color: #b8b7bc; line-height: 1.55; }
|
|
484
|
-
|
|
485
|
-
/* Make ctx-source URL chips throughout the UI clickable + discoverable.
|
|
486
|
-
Matches demo v12 lines 690-694. */
|
|
487
|
-
.ctx-url-chip { cursor: pointer; transition: all 180ms ease; padding: 2px 7px 2px 22px; border-radius: 4px; background: rgba(74,144,226,0.08); border: 1px solid rgba(74,144,226,0.30); color: var(--brand-blue, #4a90e2); position: relative; font-family: var(--mono); font-size: 10.5px; display: inline-block; }
|
|
488
|
-
.ctx-url-chip::before { content: '\\1F441'; position: absolute; left: 6px; top: 50%; transform: translateY(-50%); font-size: 10px; opacity: 0.85; }
|
|
489
|
-
.ctx-url-chip:hover { background: rgba(74,144,226,0.18); border-color: rgba(74,144,226,0.55); }
|
|
490
|
-
|
|
491
|
-
.reshape-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(6px); display: none; align-items: center; justify-content: center; z-index: 300; padding: 24px; }
|
|
492
|
-
.reshape-modal.open { display: flex; animation: rs-fade 280ms ease; }
|
|
493
|
-
.reshape-modal.open .reshape-card { animation: wp-card-in 380ms cubic-bezier(0.16, 1, 0.3, 1); }
|
|
494
|
-
@keyframes rs-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
495
|
-
/* Reshape card · matches demo v12 (lines 103-138) · gradient border via
|
|
496
|
-
* mask trick so the card looks like the agent itself composed it (orange
|
|
497
|
-
* for codehere brand, blue for primitives). Without this the modal feels
|
|
498
|
-
* like a generic dialog · with it, the modal reads as the moment the
|
|
499
|
-
* agent gets to work. */
|
|
500
|
-
.reshape-card { width: 100%; max-width: 580px; background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; padding: 22px 24px; position: relative; overflow: hidden; }
|
|
501
|
-
.reshape-card::before {
|
|
502
|
-
content: ''; position: absolute; inset: -1px;
|
|
503
|
-
border-radius: inherit; padding: 1px;
|
|
504
|
-
background: linear-gradient(135deg, rgba(255,154,118,0.5) 0%, rgba(74,144,226,0.5) 100%);
|
|
505
|
-
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
506
|
-
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
507
|
-
-webkit-mask-composite: xor; mask-composite: exclude;
|
|
508
|
-
pointer-events: none;
|
|
509
|
-
}
|
|
510
|
-
/* Primitives note · blue info box at the modal bottom explaining what
|
|
511
|
-
* "primitives" the agent composes from · demo v12 lines 134-137. */
|
|
512
|
-
.primitives-note {
|
|
513
|
-
margin-top: 14px; padding: 8px 12px;
|
|
514
|
-
background: rgba(74,144,226,0.05); border: 1px solid rgba(74,144,226,0.2);
|
|
515
|
-
border-radius: 5px;
|
|
516
|
-
font-size: 11.5px; color: var(--fg-70); line-height: 1.55;
|
|
517
|
-
}
|
|
518
|
-
.primitives-note b { color: #4a90e2; font-weight: 500; }
|
|
519
|
-
.primitives-note .prims { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); margin-top: 5px; }
|
|
520
|
-
.primitives-note .prims .p {
|
|
521
|
-
display: inline-block; padding: 1px 6px; margin: 2px 3px 0 0;
|
|
522
|
-
background: hsl(0 0% 9%); border: 1px solid var(--border);
|
|
523
|
-
border-radius: 3px; color: var(--fg-70);
|
|
524
|
-
}
|
|
525
|
-
.reshape-card .k { font-family: var(--mono); font-size: 10px; color: var(--accent); letter-spacing: 0.14em; text-transform: uppercase; }
|
|
526
|
-
.reshape-card h2 { font-size: 18px; font-weight: 600; margin: 4px 0 6px; color: var(--fg); letter-spacing: -0.01em; }
|
|
527
|
-
.reshape-card p.sub { color: var(--fg-50); font-size: 12.5px; margin: 0 0 14px; line-height: 1.55; }
|
|
528
|
-
.reshape-input { display: flex; gap: 8px; margin: 8px 0 16px; }
|
|
529
|
-
.reshape-input input { flex: 1; background: var(--bg-2); border: 1px solid var(--border); border-radius: 6px; padding: 9px 12px; color: var(--fg); font-size: 13px; outline: none; }
|
|
530
|
-
.reshape-input input:focus { border-color: var(--accent); }
|
|
531
|
-
.reshape-input button { background: var(--accent); color: #000; border: 0; border-radius: 6px; padding: 0 16px; font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
532
|
-
.reshape-input button:hover { filter: brightness(1.08); }
|
|
533
|
-
.reshape-suggestions { display: flex; flex-direction: column; gap: 5px; margin: 6px 0 14px; }
|
|
534
|
-
.reshape-suggestions .lbl { font-family: var(--mono); font-size: 9.5px; color: var(--fg-40); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 3px; }
|
|
535
|
-
.reshape-suggestions .pill { padding: 6px 11px; background: rgba(255,255,255,0.025); border: 1px solid var(--border); border-radius: 5px; font-size: 12px; color: var(--fg-90); cursor: pointer; text-align: left; }
|
|
536
|
-
.reshape-suggestions .pill:hover { border-color: var(--accent); background: rgba(255,154,118,0.05); }
|
|
537
|
-
.reshape-suggestions .pill .what { color: var(--fg-50); margin-left: 8px; font-size: 11px; }
|
|
538
|
-
.reshape-card .close-x { position: absolute; top: 14px; right: 14px; background: transparent; border: 0; color: var(--fg-50); cursor: pointer; font-size: 18px; padding: 4px 6px; border-radius: 4px; }
|
|
539
|
-
.reshape-card .close-x:hover { color: var(--fg); background: var(--bg-2); }
|
|
540
|
-
.agent-compose { display: none; padding: 12px 14px; background: var(--bg-2); border: 1px solid var(--border); border-radius: 5px; font-family: var(--mono); font-size: 11.5px; color: var(--fg-70); margin-top: 12px; }
|
|
541
|
-
.agent-compose.show { display: block; animation: rs-fade 200ms ease; }
|
|
542
|
-
.agent-compose .agent-line { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
|
543
|
-
.agent-compose .line { opacity: 0; animation: rs-slide 240ms ease forwards; }
|
|
544
|
-
@keyframes rs-slide { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } }
|
|
545
|
-
.agent-compose .line:nth-child(1) { animation-delay: 200ms; }
|
|
546
|
-
.agent-compose .line:nth-child(2) { animation-delay: 600ms; }
|
|
547
|
-
.agent-compose .line:nth-child(3) { animation-delay: 1000ms; }
|
|
548
|
-
.agent-compose .line:nth-child(4) { animation-delay: 1400ms; }
|
|
549
|
-
.agent-compose .line:nth-child(5) { animation-delay: 1800ms; }
|
|
550
|
-
.agent-compose .ok { color: var(--allow); }
|
|
551
|
-
.agent-compose .info { color: #4a90e2; }
|
|
552
|
-
.agent-compose .kw { color: var(--accent); }
|
|
553
|
-
|
|
554
|
-
/* Custom-view banner · appears when reshape is active */
|
|
555
|
-
.custom-view-banner {
|
|
556
|
-
display: none;
|
|
557
|
-
padding: 8px 20px;
|
|
558
|
-
background: linear-gradient(90deg, rgba(255,154,118,0.06), rgba(74,144,226,0.04));
|
|
559
|
-
border-bottom: 1px solid rgba(255,154,118,0.2);
|
|
560
|
-
font-family: var(--mono); font-size: 11px; color: var(--fg-70);
|
|
561
|
-
display: none;
|
|
562
|
-
}
|
|
563
|
-
body.custom-view .custom-view-banner { display: flex; align-items: center; gap: 10px; }
|
|
564
|
-
.custom-view-banner .lbl { color: var(--accent); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; font-size: 9.5px; }
|
|
565
|
-
.custom-view-banner .prompt { color: var(--fg-90); font-style: italic; }
|
|
566
|
-
.custom-view-banner .reset { margin-left: auto; color: var(--fg-50); cursor: pointer; padding: 2px 8px; border: 1px solid var(--border); border-radius: 3px; font-size: 10px; }
|
|
567
|
-
.custom-view-banner .reset:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
568
|
-
|
|
569
|
-
/* Failover banner · ported from v1 ui-page.ts on 2026-05-23 as part of /v1 kill.
|
|
570
|
-
Polls /api/failover-offers · when a rate-limited agent has a viable fallback,
|
|
571
|
-
amber strip appears between header and task list with "Switch / Dismiss". */
|
|
572
|
-
.failover-banner {
|
|
573
|
-
margin: 12px 24px 0; padding: 12px 18px;
|
|
574
|
-
border: 1px solid rgba(254,188,46,0.30);
|
|
575
|
-
background: rgba(254,188,46,0.06);
|
|
576
|
-
border-radius: 8px;
|
|
577
|
-
font-size: 13px; line-height: 1.5;
|
|
578
|
-
display: none; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
579
|
-
}
|
|
580
|
-
.failover-banner.show { display: flex; }
|
|
581
|
-
.failover-banner .fb-msg { flex: 1; color: var(--fg-70); min-width: 200px; }
|
|
582
|
-
.failover-banner .fb-msg b { color: var(--fg); font-weight: 600; }
|
|
583
|
-
.failover-banner .fb-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
|
584
|
-
.failover-banner .fb-actions button {
|
|
585
|
-
font-family: var(--mono); font-size: 11px;
|
|
586
|
-
padding: 5px 12px; border-radius: 5px; cursor: pointer;
|
|
587
|
-
border: 1px solid var(--border); background: transparent;
|
|
588
|
-
color: var(--fg-70); transition: color 0.15s, border-color 0.15s;
|
|
589
|
-
}
|
|
590
|
-
.failover-banner .fb-actions button:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
591
|
-
.failover-banner .fb-actions button.fb-accept {
|
|
592
|
-
background: rgba(254,188,46,0.12); border-color: rgba(254,188,46,0.40);
|
|
593
|
-
color: var(--warn); font-weight: 600;
|
|
594
|
-
}
|
|
595
|
-
.failover-banner .fb-actions button.fb-accept:hover { background: rgba(254,188,46,0.20); }
|
|
596
|
-
|
|
597
|
-
/* SHAPE: ship-only · hide tool-call cards, steps blob, hook blocks, etc. */
|
|
598
|
-
body.shape-ship-only .tc-card,
|
|
599
|
-
body.shape-ship-only details.collapsible,
|
|
600
|
-
body.shape-ship-only .hook-block-card,
|
|
601
|
-
body.shape-ship-only .push-gate-card { display: none !important; }
|
|
602
|
-
|
|
603
|
-
/* REPL primitives · thinking dots + streaming caret + repl-beat narrative.
|
|
604
|
-
* Match demo v12 lines 299-309 + 455-457. The thinking pill appears
|
|
605
|
-
* while the agent is mid-reasoning; the streaming caret appends to text
|
|
606
|
-
* being streamed in; the repl-beat is an italic narrative paragraph
|
|
607
|
-
* between tool calls (italic = "agent's voice", not output). All three
|
|
608
|
-
* use the same brand-color blink so live state reads consistently. */
|
|
609
|
-
.thinking {
|
|
610
|
-
display: inline-flex; align-items: center; gap: 5px;
|
|
611
|
-
padding: 5px 10px; margin: 4px 0;
|
|
612
|
-
background: hsl(0 0% 9%); border: 1px solid var(--border);
|
|
613
|
-
border-radius: 12px;
|
|
614
|
-
font-family: var(--mono); font-size: 11px; color: var(--fg-50);
|
|
615
|
-
}
|
|
616
|
-
.thinking .dots { display: inline-flex; gap: 3px; }
|
|
617
|
-
.thinking .dots span {
|
|
618
|
-
width: 4px; height: 4px; border-radius: 50%; background: var(--fg-40);
|
|
619
|
-
animation: cb-blink 1.4s ease infinite;
|
|
620
|
-
}
|
|
621
|
-
.thinking .dots span:nth-child(2) { animation-delay: 0.2s; }
|
|
622
|
-
.thinking .dots span:nth-child(3) { animation-delay: 0.4s; }
|
|
623
|
-
@keyframes cb-blink { 0%, 80%, 100% { opacity: 0.3; } 40% { opacity: 1; } }
|
|
624
|
-
.streaming { display: inline; }
|
|
625
|
-
.streaming::after {
|
|
626
|
-
content: '\\258D'; color: var(--accent); margin-left: 2px;
|
|
627
|
-
animation: cb-cursor 1s steps(2) infinite;
|
|
628
|
-
}
|
|
629
|
-
@keyframes cb-cursor { 50% { opacity: 0; } }
|
|
630
|
-
.repl-beat {
|
|
631
|
-
padding: 7px 11px; margin: 8px 0;
|
|
632
|
-
background: rgba(0,0,0,0.18);
|
|
633
|
-
border-left: 2px solid var(--border); border-radius: 0 4px 4px 0;
|
|
634
|
-
font-style: italic; color: var(--fg-70); font-size: 12.5px;
|
|
635
|
-
line-height: 1.55;
|
|
636
|
-
}
|
|
637
|
-
.repl-beat .cursor {
|
|
638
|
-
display: inline-block; width: 7px; height: 12px;
|
|
639
|
-
background: var(--accent); margin-left: 3px; vertical-align: text-bottom;
|
|
640
|
-
animation: cb-cursor 1s steps(2) infinite;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/* Audit-line · narrative beat between tool cards · matches demo v12
|
|
644
|
-
* lines 405-407. Dim mono row with a #NNNN seq prefix. When the chain
|
|
645
|
-
* records a notable event (pre-prompt scan, override, budget warning,
|
|
646
|
-
* W-PASS approval) the audit chain becomes VISIBLE in the timeline ·
|
|
647
|
-
* not buried in the footer. The line is the proof; the footer is the
|
|
648
|
-
* summary. */
|
|
649
|
-
.audit-line {
|
|
650
|
-
font-family: var(--mono); font-size: 10.5px; color: var(--fg-40);
|
|
651
|
-
padding: 6px 12px; margin: 8px 0;
|
|
652
|
-
background: rgba(0,0,0,0.2);
|
|
653
|
-
border-left: 2px solid var(--border); border-radius: 3px;
|
|
654
|
-
line-height: 1.55;
|
|
655
|
-
}
|
|
656
|
-
.audit-line .seq { color: var(--fg-30); margin-right: 8px; }
|
|
657
|
-
.audit-line.warn {
|
|
658
|
-
color: var(--warn); border-left-color: var(--warn);
|
|
659
|
-
background: rgba(254,188,46,0.03);
|
|
660
|
-
}
|
|
661
|
-
.audit-line.allow { border-left-color: rgba(40,200,64,0.4); }
|
|
662
|
-
.audit-line b { color: var(--fg-90); font-weight: 600; }
|
|
663
|
-
.audit-line code { background: hsl(0 0% 9%); padding: 1px 5px; border-radius: 3px; color: var(--fg-70); }
|
|
664
|
-
|
|
665
|
-
/* project-memory nudge · same quiet family as .audit-line (trust present-
|
|
666
|
-
but-quiet); .on = the post-click confirmation state */
|
|
667
|
-
.mem-nudge {
|
|
668
|
-
font-family: var(--mono); font-size: 10.5px; color: var(--fg-40);
|
|
669
|
-
padding: 6px 12px; margin: 8px 0;
|
|
670
|
-
background: rgba(0,0,0,0.2);
|
|
671
|
-
border-left: 2px solid var(--border); border-radius: 3px;
|
|
672
|
-
line-height: 1.55;
|
|
673
|
-
}
|
|
674
|
-
.mem-nudge .mem-btn {
|
|
675
|
-
background: none; border: 1px solid var(--border); border-radius: 4px;
|
|
676
|
-
color: var(--fg-70); font-family: var(--mono); font-size: 10.5px;
|
|
677
|
-
padding: 2px 8px; cursor: pointer; margin-left: 4px;
|
|
678
|
-
}
|
|
679
|
-
.mem-nudge .mem-btn:hover { border-color: var(--accent); color: var(--fg); }
|
|
680
|
-
.mem-nudge.on { border-left-color: rgba(40,200,64,0.4); color: var(--fg-70); }
|
|
681
|
-
|
|
682
|
-
/* auditor persona · audit-chain + verifier + hook-badges amplified,
|
|
683
|
-
summary muted (per demo v12 line 2462: "amplify audit-lines +
|
|
684
|
-
hook-badges + trust-grades") */
|
|
685
|
-
body[data-persona="auditor"] .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; color: var(--fg-70); font-size: 11.5px; padding: 8px 14px; }
|
|
686
|
-
body[data-persona="auditor"] .verifier-panel { box-shadow: 0 0 0 2px rgba(40,200,64,0.18); }
|
|
687
|
-
body[data-persona="auditor"] .hook-badge { transform: scale(1.05); }
|
|
688
|
-
body[data-persona="auditor"] .file-chip .trust { font-weight: 700; padding: 2px 7px; }
|
|
689
|
-
body[data-persona="auditor"] .panel:has(p) { opacity: 0.7; }
|
|
690
|
-
|
|
691
|
-
/* SHAPE: velocity · hide tool-call cards, summary prose, tokens, verifier · keep outcome + files */
|
|
692
|
-
body.shape-velocity .tc-card,
|
|
693
|
-
body.shape-velocity .token-table,
|
|
694
|
-
body.shape-velocity details.collapsible { display: none !important; }
|
|
695
|
-
body.shape-velocity .panel:has(.verifier-panel) { display: none; }
|
|
696
|
-
|
|
697
|
-
/* SHAPE: security · amplify verifier + hook blocks; hide summary prose */
|
|
698
|
-
body.shape-security .panel:has(.token-table),
|
|
699
|
-
body.shape-security details.collapsible { display: none !important; }
|
|
700
|
-
body.shape-security .verifier-panel { box-shadow: 0 0 0 2px rgba(255,95,87,0.18); padding: 14px; }
|
|
701
|
-
body.shape-security .hook-block-card { box-shadow: 0 0 0 2px rgba(255,95,87,0.3); }
|
|
702
|
-
|
|
703
|
-
/* Reshape shapes recompose the goal room too (slice 4): "just what shipped"
|
|
704
|
-
* / velocity hide per-turn meta; the audit/security shape amplifies the
|
|
705
|
-
* room's audit line. Reuses the existing body.shape-* + .p-eng/.audit-line. */
|
|
706
|
-
body.shape-ship-only #task-list .card-row2 .p-eng,
|
|
707
|
-
body.shape-velocity #task-list .card-row2 .p-eng { display: none !important; }
|
|
708
|
-
body.shape-security #task-list .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; font-size: 11.5px; padding: 8px 14px; }
|
|
709
|
-
|
|
710
|
-
/* v0.8 S2.5 · Settings drawer · ported from demo v12 lines 178-192 + A-09
|
|
711
|
-
* in the backlog. Right-side off-canvas panel · holds Context Sources +
|
|
712
|
-
* future section-visibility toggles. */
|
|
713
|
-
.drawer { position: fixed; top: 0; right: 0; width: 360px; height: 100vh; background: var(--bg-1); border-left: 1px solid var(--border); transform: translateX(100%); transition: transform 360ms cubic-bezier(0.16, 1, 0.3, 1); z-index: 200; padding: 20px 18px; overflow-y: auto; }
|
|
714
|
-
.drawer.open { transform: translateX(0); }
|
|
715
|
-
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); opacity: 0; pointer-events: none; transition: opacity 300ms cubic-bezier(0.16, 1, 0.3, 1); z-index: 199; }
|
|
716
|
-
.drawer-overlay.open { opacity: 1; pointer-events: auto; }
|
|
717
|
-
.drawer h3 { font-family: var(--mono); font-size: 11px; color: var(--fg-50); letter-spacing: 0.12em; text-transform: uppercase; margin: 16px 0 8px; }
|
|
718
|
-
.drawer h3:first-child { margin-top: 0; }
|
|
719
|
-
.drawer p.sub { font-size: 11px; color: var(--fg-50); margin: 2px 0 12px; }
|
|
720
|
-
.drawer-close { position: absolute; top: 14px; right: 14px; background: transparent; border: 0; color: var(--fg-50); cursor: pointer; font-size: 18px; line-height: 1; padding: 4px 6px; border-radius: 4px; }
|
|
721
|
-
.drawer-close:hover { color: var(--fg); background: var(--bg-2); }
|
|
722
|
-
.drawer-title { font-size: 15px; font-weight: 600; color: var(--fg); margin-bottom: 12px; padding-right: 30px; }
|
|
723
|
-
|
|
724
|
-
/* Settings-button (cog) in the header · opens the drawer */
|
|
725
|
-
.settings-btn { background: transparent; border: 1px solid var(--border); color: var(--fg-50); cursor: pointer; padding: 4px 8px; border-radius: 6px; font-size: 12px; }
|
|
726
|
-
.settings-btn:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
727
|
-
|
|
728
|
-
/* Context-source rows inside the drawer */
|
|
729
|
-
.cs-row { display: flex; align-items: flex-start; gap: 10px; padding: 10px 0; font-size: 12px; border-bottom: 1px solid var(--border-soft); }
|
|
730
|
-
.cs-row .cs-icon { margin-top: 1px; }
|
|
731
|
-
.cs-row:last-child { border-bottom: none; }
|
|
732
|
-
.cs-row .cs-icon { width: 24px; height: 24px; border-radius: 5px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; font-family: var(--mono); }
|
|
733
|
-
.cs-row .cs-icon.linear { background: #5e6ad2; color: #fff; }
|
|
734
|
-
.cs-row .cs-icon.figma { background: #1e1e1e; color: #fff; border: 1px solid rgba(255,255,255,0.2); }
|
|
735
|
-
.cs-row .cs-label { font-weight: 500; color: var(--fg-90); }
|
|
736
|
-
.cs-row .cs-meta { font-size: 10px; color: var(--fg-50); font-family: var(--mono); }
|
|
737
|
-
.cs-row .cs-status { font-family: var(--mono); font-size: 9px; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
738
|
-
.cs-row .cs-status.connected { color: var(--allow); background: rgba(40,200,64,0.10); }
|
|
739
|
-
.cs-row .cs-status.expired { color: var(--warn); background: rgba(254,188,46,0.10); }
|
|
740
|
-
.cs-row .cs-status.manual { color: var(--fg-50); background: rgba(255,255,255,0.04); }
|
|
741
|
-
.cs-row .cs-status.disconnected { color: var(--fg-40); background: rgba(255,255,255,0.02); }
|
|
742
|
-
.cs-row .cs-status.available { color: var(--accent); background: rgba(255,154,118,0.10); }
|
|
743
|
-
.cs-row .cs-actions { margin-left: auto; display: flex; gap: 6px; }
|
|
744
|
-
.cs-btn { font-family: var(--mono); font-size: 11px; padding: 3px 9px; border-radius: 4px; cursor: pointer; background: transparent; color: var(--fg-70); border: 1px solid var(--border); }
|
|
745
|
-
.cs-btn:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
746
|
-
.cs-btn.primary { color: var(--accent); border-color: rgba(255,154,118,0.4); }
|
|
747
|
-
.cs-btn.primary:hover { background: rgba(255,154,118,0.08); }
|
|
748
|
-
.cs-btn.danger:hover { color: var(--block); border-color: var(--block); }
|
|
749
|
-
.cs-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
750
|
-
.cs-empty-hint { font-size: 11px; color: var(--fg-50); padding: 8px 10px; background: rgba(255,255,255,0.02); border-radius: 4px; margin-top: 4px; }
|
|
751
|
-
.cs-test-result { font-family: var(--mono); font-size: 10px; padding: 4px 8px; margin-top: 4px; border-radius: 3px; }
|
|
752
|
-
.cs-test-result.ok { color: var(--allow); background: rgba(40,200,64,0.06); }
|
|
753
|
-
.cs-test-result.fail { color: var(--block); background: rgba(255,95,87,0.06); }
|
|
754
|
-
|
|
755
|
-
/* Demo v12 line 322 + 518-521 · context-source rendering on tool-call cards */
|
|
756
|
-
.tc-card.ctx-source { border-left: 2px solid #4a90e2; }
|
|
757
|
-
.tc-card .hook-badge.ctx { color: #4a90e2; background: rgba(74,144,226,0.08); border: 1px solid rgba(74,144,226,0.3); }
|
|
758
|
-
|
|
759
|
-
/* P10 · per-step hook badges in the trace details (same styles, different parent). */
|
|
760
|
-
.tc .hook-badge { font-family: var(--mono); font-size: 9px; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; margin-right: 6px; vertical-align: middle; }
|
|
761
|
-
.tc .hook-badge.allow { color: var(--allow); background: rgba(40,200,64,0.10); }
|
|
762
|
-
.tc .hook-badge.block { color: var(--block); background: rgba(255,95,87,0.10); }
|
|
763
|
-
.tc .hook-badge.override { color: var(--warn); background: rgba(254,188,46,0.10); }
|
|
764
|
-
.tc .hook-badge.ctx { color: #4a90e2; background: rgba(74,144,226,0.08); border: 1px solid rgba(74,144,226,0.3); }
|
|
765
|
-
.ctx-source-banner { padding: 8px 12px; background: rgba(74,144,226,0.04); border: 1px solid rgba(74,144,226,0.25); border-left: 3px solid #4a90e2; border-radius: 5px; margin: 8px 0; font-size: 11.5px; color: var(--fg-70); }
|
|
766
|
-
.ctx-source-banner b { color: #4a90e2; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; margin-right: 6px; }
|
|
767
|
-
|
|
768
|
-
/* F-06 · interactive audit chain panel · dashboard footer · click "Verify"
|
|
769
|
-
* to call /api/audit/verify and render the verdict in plain English. The
|
|
770
|
-
* chain is the source of truth, NOT codehere itself · the button proves
|
|
771
|
-
* the daemon isn't lying to you. */
|
|
772
|
-
/* RADICAL bottom-chrome collapse (2026-05-24 · Ega flagged "audit
|
|
773
|
-
* section consuming space"). Pre-fix: audit-chain panel ate ~90px ·
|
|
774
|
-
* OSS footer ate ~25px · total bottom chrome ~115px on every screen.
|
|
775
|
-
* Post-fix: panel collapsed to inline + co-lives with OSS footer in
|
|
776
|
-
* the same 28px status-bar line. Audit info stays load-bearing
|
|
777
|
-
* (dots + stats + Verify button preserved · only the SHA-256
|
|
778
|
-
* subtitle and the panel-card chrome removed). Net ~85px reclaimed.
|
|
779
|
-
* No info-loss · just spatial economy. */
|
|
780
|
-
.audit-chain-panel {
|
|
781
|
-
display: inline-flex; align-items: center; gap: 12px;
|
|
782
|
-
padding: 0; margin: 0; background: none; border: none;
|
|
783
|
-
}
|
|
784
|
-
.audit-chain-head { display: inline-flex; align-items: center; gap: 10px; }
|
|
785
|
-
.audit-chain-title { font-family: var(--mono); font-size: 10px; color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
786
|
-
/* audit-chain stats line + activity dots · denser footer per demo v12 */
|
|
787
|
-
.audit-chain-dots .dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
|
|
788
|
-
.audit-chain-dots .dot.allow { background: var(--allow); }
|
|
789
|
-
.audit-chain-dots .dot.block { background: var(--block); }
|
|
790
|
-
.audit-chain-dots .dot.override { background: var(--warn); }
|
|
791
|
-
.audit-chain-dots .dot.muted { background: var(--fg-30); }
|
|
792
|
-
.audit-chain-btn {
|
|
793
|
-
font-family: var(--mono); font-size: 11px;
|
|
794
|
-
padding: 4px 12px; border-radius: 5px; cursor: pointer;
|
|
795
|
-
background: rgba(255,154,118,0.08); color: var(--accent);
|
|
796
|
-
border: 1px solid rgba(255,154,118,0.40); font-weight: 600;
|
|
797
|
-
transition: all 160ms ease;
|
|
798
|
-
}
|
|
799
|
-
.audit-chain-btn:hover { background: rgba(255,154,118,0.20); box-shadow: 0 0 0 3px rgba(255,154,118,0.10); }
|
|
800
|
-
.audit-chain-btn.verified { color: var(--allow); border-color: rgba(40,200,64,0.3); }
|
|
801
|
-
.audit-chain-btn.broken { color: var(--block); border-color: rgba(255,95,87,0.3); }
|
|
802
|
-
/* audit-chain-result starts collapsed (the subtitle "SHA-256
|
|
803
|
-
* hash-chain · run verify…" ate 25px on every screen). Only renders
|
|
804
|
-
* when verifyAuditChain() runs and writes a verdict · gets shown
|
|
805
|
-
* inline temporarily after click then fades back. */
|
|
806
|
-
.audit-chain-result { display: none; margin-top: 0; font-family: var(--mono); font-size: 11px; }
|
|
807
|
-
.audit-chain-result.show { display: inline-block; margin-left: 12px; }
|
|
808
|
-
.audit-chain-result .ok { color: var(--allow); }
|
|
809
|
-
.audit-chain-result .fail { color: var(--block); }
|
|
810
|
-
.audit-chain-counts { color: var(--fg-50); margin-top: 4px; }
|
|
811
|
-
.audit-chain-error { color: var(--block); margin-top: 4px; padding-left: 12px; }
|
|
812
|
-
|
|
813
|
-
/* F-04 · time-gap separator · slides between two consecutive items in the
|
|
814
|
-
* task list when their times differ by more than 2 hours. Says "Nh later"
|
|
815
|
-
* with an optional "· daemon restart" tail when there's a known restart in
|
|
816
|
-
* the gap. Helps the reviewer parse 17h+ histories without scrolling
|
|
817
|
-
* forever. */
|
|
818
|
-
.time-gap {
|
|
819
|
-
display: flex; align-items: center; gap: 16px;
|
|
820
|
-
padding: 14px 0; margin: 16px 0;
|
|
821
|
-
font-family: var(--mono); font-size: 11px;
|
|
822
|
-
color: var(--accent); text-transform: uppercase; letter-spacing: 0.18em;
|
|
823
|
-
}
|
|
824
|
-
.time-gap .line {
|
|
825
|
-
flex: 1; height: 1px;
|
|
826
|
-
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
|
827
|
-
opacity: 0.45;
|
|
828
|
-
}
|
|
829
|
-
.time-gap .label {
|
|
830
|
-
padding: 7px 16px; border: 1px solid rgba(255,154,118,0.3);
|
|
831
|
-
border-radius: 100px; background: rgba(255,154,118,0.04);
|
|
832
|
-
text-align: center; line-height: 1.3;
|
|
833
|
-
}
|
|
834
|
-
.time-gap .label b { color: var(--fg); font-weight: 600; }
|
|
835
|
-
.time-gap .label .sub { display: block; margin-top: 3px; font-size: 9px; color: var(--fg-50); letter-spacing: 0.12em; }
|
|
836
|
-
|
|
837
|
-
/* F-08 · token table · per-agent breakdown of inputs/outputs/cache/cost in
|
|
838
|
-
* task detail · only renders when artifacts.usage has tokens. Mono numbers
|
|
839
|
-
* column-aligned, total row bold-line-above. */
|
|
840
|
-
.token-table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 11px; }
|
|
841
|
-
.token-table th { text-align: right; color: var(--fg-40); font-size: 9px; text-transform: uppercase; padding: 4px 8px; font-weight: 500; }
|
|
842
|
-
.token-table th:first-child { text-align: left; }
|
|
843
|
-
.token-table td { padding: 4px 8px; text-align: right; color: var(--fg-90); }
|
|
844
|
-
.token-table td:first-child { text-align: left; color: var(--fg-70); }
|
|
845
|
-
.token-table tr.total td { border-top: 1px solid var(--border); color: var(--fg); font-weight: 600; padding-top: 6px; }
|
|
846
|
-
.token-table td.dim { color: var(--fg-40); }
|
|
847
|
-
|
|
848
|
-
/* F-02 · cross-worktree alert · pulsing dot + label in the squad-row when
|
|
849
|
-
* any session in a different cwd has been running > 5 minutes. Click to
|
|
850
|
-
* jump to that detail. */
|
|
851
|
-
.worktree-alert {
|
|
852
|
-
display: none;
|
|
853
|
-
font-family: var(--mono); font-size: 10px; color: var(--warn);
|
|
854
|
-
background: rgba(254,188,46,0.06); border: 1px solid rgba(254,188,46,0.18);
|
|
855
|
-
padding: 2px 8px; border-radius: 4px; cursor: pointer;
|
|
856
|
-
margin-left: 8px;
|
|
857
|
-
}
|
|
858
|
-
.worktree-alert:hover { background: rgba(254,188,46,0.12); }
|
|
859
|
-
.worktree-alert .pulse-dot {
|
|
860
|
-
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
|
861
|
-
background: var(--warn); margin-right: 6px; animation: pulse 1.6s ease infinite;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/* F-01 · budget threshold warning · drops down under the header when daily
|
|
865
|
-
* spend crosses 80% of the cap. Color tracks safe/warn/block. The audit-
|
|
866
|
-
* line copy is intentionally plain English: who, how much, what to do. */
|
|
867
|
-
.budget-banner {
|
|
868
|
-
display: none;
|
|
869
|
-
padding: 8px 20px; border-bottom: 1px solid var(--border-soft);
|
|
870
|
-
font-family: var(--mono); font-size: 11px;
|
|
871
|
-
background: rgba(254,188,46,0.04); color: var(--warn);
|
|
872
|
-
}
|
|
873
|
-
.budget-banner.block {
|
|
874
|
-
background: rgba(255,95,87,0.06); color: var(--block);
|
|
875
|
-
}
|
|
876
|
-
.budget-banner .what { color: var(--fg-90); margin-right: 8px; font-weight: 600; }
|
|
877
|
-
.budget-banner .hint { color: var(--fg-50); margin-left: 8px; }
|
|
878
|
-
.v1-link {
|
|
879
|
-
font-family: var(--mono); font-size: 11px; color: var(--fg-40);
|
|
880
|
-
text-decoration: none; padding: 4px 10px; border: 1px solid var(--border);
|
|
881
|
-
border-radius: 6px;
|
|
882
|
-
}
|
|
883
|
-
.v1-link:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
884
|
-
|
|
885
|
-
/* A-10 · OSS · MIT footer · proof-of-real strip at the very bottom of the
|
|
886
|
-
* dashboard. The marketing line is "codehere is OSS, audit it yourself" ·
|
|
887
|
-
* if the running daemon doesn't say so somewhere visible, the line is
|
|
888
|
-
* empty marketing. Test count + version come from the server. */
|
|
889
|
-
/* OSS footer · proof-of-real strip · matches demo .proof-footer (lines
|
|
890
|
-
* 497-505). Darker bg so it reads as ground floor. Codehere wordmark gets
|
|
891
|
-
* the brand tint · the only colored item · everything else dim. Hover on
|
|
892
|
-
* any link goes to full fg, not 70 · the click target deserves full
|
|
893
|
-
* contrast when the cursor's actually there. */
|
|
894
|
-
.oss-footer {
|
|
895
|
-
flex-shrink: 0; padding: 9px 20px;
|
|
896
|
-
border-top: 1px solid var(--border-soft);
|
|
897
|
-
background: rgba(0,0,0,0.35);
|
|
898
|
-
font-family: var(--mono); font-size: 10.5px; color: var(--fg-40);
|
|
899
|
-
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
900
|
-
}
|
|
901
|
-
.oss-footer .brand { color: var(--accent); }
|
|
902
|
-
.oss-footer .right { margin-left: auto; }
|
|
903
|
-
.oss-footer a { color: var(--fg-50); text-decoration: none; transition: color 180ms cubic-bezier(0.16,1,0.3,1); }
|
|
904
|
-
.oss-footer a:hover { color: var(--fg); }
|
|
905
|
-
.oss-footer .sep { color: var(--fg-30); }
|
|
906
|
-
|
|
907
|
-
.squad-row {
|
|
908
|
-
display: flex; align-items: center; gap: 12px;
|
|
909
|
-
padding: 6px 20px; border-bottom: 1px solid var(--border-soft);
|
|
910
|
-
font-size: 11px; color: var(--fg-50); flex-shrink: 0;
|
|
911
|
-
}
|
|
912
|
-
.squad-agent { display: flex; align-items: center; gap: 4px; }
|
|
913
|
-
.squad-dot { width: 6px; height: 6px; border-radius: 50%; }
|
|
914
|
-
.squad-dot.ready { background: var(--allow); }
|
|
915
|
-
.squad-dot.busy { background: var(--warn); }
|
|
916
|
-
.squad-dot.offline { background: var(--fg-30); }
|
|
917
|
-
/* setup = installed/known but can't run yet · hollow ring, not a filled dot,
|
|
918
|
-
so it reads "not ready" at a glance and never like an active green/amber. */
|
|
919
|
-
.squad-dot.setup { background: transparent; border: 1.5px solid var(--fg-40); box-sizing: border-box; }
|
|
920
|
-
.squad-setup { font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--fg-40); border: 1px solid var(--fg-20); border-radius: 3px; padding: 0 4px; margin-left: 2px; }
|
|
921
|
-
|
|
922
|
-
.bar {
|
|
923
|
-
display: flex; align-items: center; gap: 8px;
|
|
924
|
-
padding: 10px 20px; border-bottom: 1px solid var(--border-soft);
|
|
925
|
-
flex-shrink: 0;
|
|
926
|
-
}
|
|
927
|
-
.new-btn {
|
|
928
|
-
background: var(--accent); color: #000; font-weight: 600;
|
|
929
|
-
border: none; border-radius: 6px; padding: 6px 14px;
|
|
930
|
-
font-family: var(--sans); font-size: 12px; cursor: pointer; white-space: nowrap;
|
|
931
|
-
}
|
|
932
|
-
.new-btn:hover { filter: brightness(1.05); }
|
|
933
|
-
|
|
934
|
-
.filter-tab {
|
|
935
|
-
font-family: var(--mono); font-size: 11px; color: var(--fg-50);
|
|
936
|
-
cursor: pointer; padding: 4px 10px; border-radius: 6px;
|
|
937
|
-
border: 1px solid transparent; white-space: nowrap;
|
|
938
|
-
}
|
|
939
|
-
.filter-tab:hover { color: var(--fg); background: rgba(255,255,255,0.04); }
|
|
940
|
-
.filter-tab.active { color: var(--fg); background: rgba(255,154,118,0.08); border-color: rgba(255,154,118,0.25); }
|
|
941
|
-
.filter-tab .cnt { color: var(--fg-40); margin-left: 2px; }
|
|
942
|
-
|
|
943
|
-
.content { flex: 1; overflow-y: auto; }
|
|
944
|
-
.content-inner { padding: 16px 20px; max-width: 900px; margin: 0 auto; }
|
|
945
|
-
.section-label {
|
|
946
|
-
font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
|
|
947
|
-
color: var(--fg-40); text-transform: uppercase; margin: 16px 0 8px;
|
|
948
|
-
display: flex; align-items: center; gap: 8px;
|
|
949
|
-
}
|
|
950
|
-
.section-label:first-child { margin-top: 0; }
|
|
951
|
-
/* Featured section gets the brand tint · pinned demo + recent-work
|
|
952
|
-
* sections deserve the visual signal that says "this is what to look
|
|
953
|
-
* at first". Sub-dot precedes the label so the eye lands there before
|
|
954
|
-
* reading the text. */
|
|
955
|
-
.section-label.featured { color: var(--accent); }
|
|
956
|
-
.section-label.featured::before {
|
|
957
|
-
content: ''; display: inline-block;
|
|
958
|
-
width: 5px; height: 5px; border-radius: 50%;
|
|
959
|
-
background: var(--accent);
|
|
960
|
-
box-shadow: 0 0 0 3px rgba(255,154,118,0.18);
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
/* Goal-first home · each project (goal) is a group; its tasks live inside.
|
|
964
|
-
* The home is organized by what you're working toward, not by status. */
|
|
965
|
-
.goal-group { margin: 0 0 22px; }
|
|
966
|
-
.goal-head {
|
|
967
|
-
display: flex; align-items: baseline; gap: 10px;
|
|
968
|
-
padding: 0 0 8px; margin: 20px 0 10px;
|
|
969
|
-
border-bottom: 1px solid var(--border);
|
|
970
|
-
}
|
|
971
|
-
.goal-group:first-child .goal-head { margin-top: 0; }
|
|
972
|
-
.goal-title { font-size: 15px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
|
|
973
|
-
.goal-meta { font-family: var(--mono); font-size: 10.5px; color: var(--fg-40); margin-left: auto; }
|
|
974
|
-
.goal-head { cursor: pointer; }
|
|
975
|
-
.goal-head:hover .goal-title { color: var(--accent); }
|
|
976
|
-
.goal-head:hover .goal-meta { color: var(--fg-70); }
|
|
977
|
-
|
|
978
|
-
/* The goal room · a single goal opened to its multi-agent turns, in order. */
|
|
979
|
-
.room-head { margin: 4px 0 14px; }
|
|
980
|
-
.room-back { background: transparent; border: 0; color: var(--fg-50); font-family: var(--mono); font-size: 11px; cursor: pointer; padding: 0; margin-bottom: 12px; }
|
|
981
|
-
.room-back:hover { color: var(--fg); }
|
|
982
|
-
.room-title { font-size: 20px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
|
|
983
|
-
.room-sub { font-family: var(--mono); font-size: 11px; color: var(--fg-40); margin-top: 5px; }
|
|
984
|
-
.turns-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; color: var(--fg-40); text-transform: uppercase; margin: 18px 0 12px; }
|
|
985
|
-
|
|
986
|
-
/* Task card · glass surface · matches demo v12 .glass-card pattern.
|
|
987
|
-
* Faint white-on-dark fill + 1px low-contrast border · sits on top of
|
|
988
|
-
* the ambient dot-grid. Hover lifts 2px with a bigger shadow + inner
|
|
989
|
-
* ring · the card visibly comes forward when the cursor's on it. */
|
|
990
|
-
.task-card {
|
|
991
|
-
background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);
|
|
992
|
-
border-left: 2px solid rgba(255,255,255,0.06);
|
|
993
|
-
border-radius: 8px; padding: 13px 15px; margin-bottom: 8px;
|
|
994
|
-
cursor: pointer;
|
|
995
|
-
transition:
|
|
996
|
-
transform 300ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
997
|
-
border-color 300ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
998
|
-
background 300ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
999
|
-
box-shadow 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
1000
|
-
}
|
|
1001
|
-
.task-card:hover {
|
|
1002
|
-
border-color: rgba(255,255,255,0.12); background: rgba(255,255,255,0.05);
|
|
1003
|
-
transform: translateY(-2px);
|
|
1004
|
-
box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.05);
|
|
1005
|
-
}
|
|
1006
|
-
.task-card.tc-pinned { border-left-color: var(--accent); background: linear-gradient(90deg, rgba(255,154,118,0.04), rgba(255,255,255,0.02) 30%); }
|
|
1007
|
-
.task-card.agent-claude-code { border-left-color: #76b7ff; }
|
|
1008
|
-
.task-card.agent-codex { border-left-color: #28c840; }
|
|
1009
|
-
.task-card.agent-aider { border-left-color: #ff9a76; }
|
|
1010
|
-
.task-card.agent-opencode { border-left-color: #c4b5fd; }
|
|
1011
|
-
.task-card.agent-gemini { border-left-color: #febc2e; }
|
|
1012
|
-
.task-card.agent-conductor { border-left-color: var(--fg-30); }
|
|
1013
|
-
/* Outcome chip on the right of card-row2 · ✓ / ⚠ / ↻ */
|
|
1014
|
-
.card-outcome { margin-left: auto; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em; padding: 2px 8px; border-radius: 3px; }
|
|
1015
|
-
.card-outcome.ok { color: var(--allow); background: rgba(40,200,64,0.10); }
|
|
1016
|
-
.card-outcome.warn { color: var(--warn); background: rgba(254,188,46,0.10); }
|
|
1017
|
-
.card-outcome.fail { color: var(--block); background: rgba(255,95,87,0.10); }
|
|
1018
|
-
.card-outcome.queued { color: var(--fg-50); background: rgba(255,255,255,0.03); }
|
|
1019
|
-
.card-stalled { margin-top: 6px; font-size: 11px; line-height: 1.4; color: var(--block); background: rgba(255,95,87,0.07); border-left: 2px solid var(--block); padding: 5px 8px; border-radius: 3px; }
|
|
1020
|
-
.card-row1 { display: flex; align-items: center; gap: 8px; }
|
|
1021
|
-
.status-icon { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
1022
|
-
.status-icon.running { background: var(--accent); animation: pulse 1.5s ease infinite; }
|
|
1023
|
-
.status-icon.paused { background: var(--warn); }
|
|
1024
|
-
.status-icon.done { background: var(--allow); }
|
|
1025
|
-
.status-icon.failed { background: var(--block); }
|
|
1026
|
-
.status-icon.crashed { background: var(--block); }
|
|
1027
|
-
.status-icon.killed { background: var(--fg-30); }
|
|
1028
|
-
.status-icon.queued { background: var(--fg-30); }
|
|
1029
|
-
.status-icon.blocked { background: var(--warn); }
|
|
1030
|
-
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
1031
|
-
|
|
1032
|
-
.card-title { font-weight: 600; color: var(--fg); font-size: 13px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1033
|
-
.card-agent { font-family: var(--mono); font-size: 10px; color: var(--fg-50); background: rgba(255,255,255,0.04); padding: 2px 6px; border-radius: 4px; display: inline-flex; align-items: center; gap: 5px; }
|
|
1034
|
-
/* Agent identity badge (squircle with initials) — used in cards, squad row, detail header */
|
|
1035
|
-
.ab { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 5px; font-family: var(--mono); font-size: 9px; font-weight: 700; flex-shrink: 0; letter-spacing: -0.04em; }
|
|
1036
|
-
.ab.claude { background: var(--agent-claude); color: #000; }
|
|
1037
|
-
.ab.codex { background: var(--agent-codex); color: #fff; }
|
|
1038
|
-
.ab.aider { background: var(--agent-aider); color: #fff; }
|
|
1039
|
-
.ab.gemini { background: var(--agent-gemini); color: #000; }
|
|
1040
|
-
.ab.opencode { background: var(--agent-opencode); color: #000; }
|
|
1041
|
-
.ab.conductor { background: var(--accent); color: #000; }
|
|
1042
|
-
.ab.auto { background: var(--fg-30); color: #000; }
|
|
1043
|
-
.ab.user { background: #5a5a62; color: #fff; }
|
|
1044
|
-
.ab.lg { width: 22px; height: 22px; font-size: 10px; border-radius: 6px; }
|
|
1045
|
-
.ab.sm { width: 14px; height: 14px; font-size: 8px; border-radius: 3px; }
|
|
1046
|
-
/* Cost signal pill — primitive #2 */
|
|
1047
|
-
.cost-signal { display: inline-flex; align-items: center; gap: 4px; font-family: var(--mono); font-size: 9px; color: var(--fg-40); padding: 1px 6px; border: 1px solid var(--border); border-radius: 3px; }
|
|
1048
|
-
.cost-signal.free { color: var(--allow); border-color: rgba(40,200,64,0.25); background: rgba(40,200,64,0.04); }
|
|
1049
|
-
.cost-signal.paid { color: var(--fg-50); }
|
|
1050
|
-
.card-row2 { display: flex; align-items: center; gap: 10px; margin-top: 6px; font-family: var(--mono); font-size: 10px; color: var(--fg-40); }
|
|
1051
|
-
.card-row3 { margin-top: 6px; display: flex; gap: 6px; }
|
|
1052
|
-
.card-btn {
|
|
1053
|
-
font-family: var(--mono); font-size: 10px; padding: 3px 8px;
|
|
1054
|
-
border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
|
|
1055
|
-
background: transparent; color: var(--fg-50);
|
|
1056
|
-
}
|
|
1057
|
-
.card-btn:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
1058
|
-
.card-btn.kill:hover { color: var(--block); border-color: var(--block); }
|
|
1059
|
-
.card-btn.pause:hover { color: var(--warn); border-color: var(--warn); }
|
|
1060
|
-
.card-btn.verify:hover { color: var(--allow); border-color: var(--allow); }
|
|
1061
|
-
.card-btn.restart:hover { color: var(--accent); border-color: var(--accent); }
|
|
1062
|
-
.verify-badge { font-family: var(--mono); font-size: 9px; padding: 1px 5px; border-radius: 4px; }
|
|
1063
|
-
.verify-badge.verified { color: var(--allow); background: rgba(40,200,64,0.10); }
|
|
1064
|
-
.verify-badge.unverified { color: var(--fg-30); background: rgba(255,255,255,0.04); }
|
|
1065
|
-
.verify-badge.failed { color: var(--block); background: rgba(255,95,87,0.10); }
|
|
1066
|
-
|
|
1067
|
-
.empty-state { text-align: center; padding: 60px 20px; color: var(--fg-40); }
|
|
1068
|
-
.empty-state h2 { color: var(--fg-70); margin-bottom: 8px; font-weight: 500; font-size: 15px; }
|
|
1069
|
-
|
|
1070
|
-
.skeleton { padding: 16px 20px; max-width: 900px; margin: 0 auto; }
|
|
1071
|
-
.skeleton-card {
|
|
1072
|
-
height: 60px; border-radius: 8px; margin-bottom: 8px;
|
|
1073
|
-
background: linear-gradient(90deg, rgba(255,255,255,0.02) 25%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0.02) 75%);
|
|
1074
|
-
background-size: 200% 100%; animation: shimmer 1.5s ease infinite;
|
|
1075
|
-
}
|
|
1076
|
-
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
1077
|
-
|
|
1078
|
-
/* Detail header · matches demo v12 .phase-head pattern · uppercase brand
|
|
1079
|
-
* kicker label · 22px shimmering gradient title · meta line beneath.
|
|
1080
|
-
* The shimmer is the same 6-stop gradient as demo h1, scaled down for
|
|
1081
|
-
* inside-a-page use (vs full-phase headline). */
|
|
1082
|
-
.detail-header {
|
|
1083
|
-
display: flex; align-items: flex-start; gap: 12px; margin-bottom: 6px;
|
|
1084
|
-
}
|
|
1085
|
-
.back-btn {
|
|
1086
|
-
font-family: var(--mono); font-size: 12px; color: var(--accent);
|
|
1087
|
-
cursor: pointer; padding: 4px 8px; border-radius: 4px;
|
|
1088
|
-
border: 1px solid rgba(255,154,118,0.2);
|
|
1089
|
-
flex-shrink: 0; margin-top: 14px;
|
|
1090
|
-
}
|
|
1091
|
-
.back-btn:hover { background: var(--accent-wash); }
|
|
1092
|
-
.detail-title-wrap { flex: 1; min-width: 0; }
|
|
1093
|
-
.detail-kicker {
|
|
1094
|
-
font-family: var(--mono); font-size: 10.5px; color: var(--accent);
|
|
1095
|
-
letter-spacing: 0.18em; text-transform: uppercase; line-height: 1;
|
|
1096
|
-
margin-bottom: 4px;
|
|
1097
|
-
}
|
|
1098
|
-
.detail-title {
|
|
1099
|
-
font-weight: 600; font-size: 22px; line-height: 1.25;
|
|
1100
|
-
letter-spacing: -0.015em;
|
|
1101
|
-
background: linear-gradient(90deg, #ff9a76 0%, #f5c6b0 22%, #ff9a76 38%, #4a90e2 55%, #8ab8ee 70%, #4a90e2 85%, #ff9a76 100%);
|
|
1102
|
-
background-size: 200% auto;
|
|
1103
|
-
-webkit-background-clip: text; background-clip: text;
|
|
1104
|
-
-webkit-text-fill-color: transparent; color: transparent;
|
|
1105
|
-
animation: titleShimmer 6s linear infinite;
|
|
1106
|
-
}
|
|
1107
|
-
@keyframes titleShimmer {
|
|
1108
|
-
0% { background-position: 0% center; }
|
|
1109
|
-
100% { background-position: 200% center; }
|
|
1110
|
-
}
|
|
1111
|
-
.detail-meta { font-family: var(--mono); font-size: 11px; color: var(--fg-50); margin-bottom: 16px; }
|
|
1112
|
-
.detail-meta .phase-pos { color: var(--accent); }
|
|
1113
|
-
/* Artifacts drawer (task #51) · header trigger + slide-in side panel */
|
|
1114
|
-
.art-trig { color: #4a90e2; cursor: pointer; }
|
|
1115
|
-
.art-trig:hover { text-decoration: underline; }
|
|
1116
|
-
.art-scrim { position: fixed; inset: 0; background: rgba(0,0,0,.62); opacity: 0; pointer-events: none; transition: opacity .18s; z-index: 90; }
|
|
1117
|
-
.art-scrim.open { opacity: 1; pointer-events: auto; }
|
|
1118
|
-
/* Centered preview popup · scales up on large screens so diagrams/tables get
|
|
1119
|
-
room (vs a cramped right drawer). */
|
|
1120
|
-
.art-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-48%) scale(.98); opacity: 0; pointer-events: none;
|
|
1121
|
-
width: min(880px, 92vw); max-height: 86vh; background: #16171d; border: 1px solid var(--border); border-radius: 12px;
|
|
1122
|
-
box-shadow: 0 24px 80px rgba(0,0,0,.6); transition: opacity .18s ease, transform .18s ease; z-index: 100; display: flex; flex-direction: column; }
|
|
1123
|
-
.art-modal.open { opacity: 1; pointer-events: auto; transform: translate(-50%,-50%) scale(1); }
|
|
1124
|
-
.art-modal-h { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; font-family: var(--mono); font-size: 12px; font-weight: 600; color: var(--fg-80); }
|
|
1125
|
-
.art-modal-h .x { margin-left: auto; cursor: pointer; color: var(--fg-50); font-size: 16px; }
|
|
1126
|
-
.art-modal-h .x:hover { color: var(--fg-80); }
|
|
1127
|
-
.art-modal-body { overflow: auto; padding: 16px 18px; }
|
|
1128
|
-
.art-modal-body .panel { border: none; padding: 0; margin: 0; background: transparent; }
|
|
1129
|
-
.art-modal-body .panel-title { display: none; }
|
|
1130
|
-
/* Legible canvas: each artifact lifts onto an elevated surface so it's not
|
|
1131
|
-
lost on near-black, and diagram/chart strokes + table text get contrast. */
|
|
1132
|
-
.art-modal .artifact-card, .art-modal .art-mock, .art-modal .art-pick { background: #1c1e26; border: 1px solid #2c2f3a; border-radius: 10px; padding: 14px; margin-bottom: 14px; }
|
|
1133
|
-
.art-modal .art-pick { border-left: 3px solid var(--accent); }
|
|
1134
|
-
.art-modal .artifact-h { color: var(--fg-70); font-size: 11px; margin-bottom: 10px; }
|
|
1135
|
-
.art-modal .art-seq { background: #21242e; border: 1px solid #333744; border-radius: 8px; padding: 12px; }
|
|
1136
|
-
.art-modal .art-cmp .hd { background: #2a2d38; color: var(--fg-80); }
|
|
1137
|
-
.art-modal .art-cmp .rl { background: #23262f; color: var(--fg-70); }
|
|
1138
|
-
.art-modal .art-cmp .cell { background: #1a1c23; color: #d2d4de; }
|
|
1139
|
-
.art-modal .art-mock-body { color: #cdd0da; }
|
|
1140
|
-
/* brighter diagram strokes/text so the graph actually reads */
|
|
1141
|
-
.art-modal .seq-svg .ll { stroke: #4a4d5a; }
|
|
1142
|
-
.art-modal .seq-svg .box { fill: #2a2e3a; stroke: #444858; }
|
|
1143
|
-
.art-modal .seq-svg .msg { stroke: #c2c4d0; }
|
|
1144
|
-
.art-modal .seq-svg .msg.ret { stroke: #8a8d9c; }
|
|
1145
|
-
.art-modal .seq-svg .mt { fill: #d2d4de; }
|
|
1146
|
-
.art-modal .seq-svg .ah { fill: #c2c4d0; }
|
|
1147
|
-
.art-modal .seq-svg .ah-ret { fill: #8a8d9c; }
|
|
1148
|
-
@media (max-width: 560px){ .art-modal { width: 94vw; max-height: 90vh; } }
|
|
1149
|
-
.detail-actions { display: flex; gap: 6px; }
|
|
1150
|
-
/* Chapter divider · phase transition in the thread narrative (demo v12) */
|
|
1151
|
-
.chapter-divider { display: flex; align-items: center; gap: 10px; margin: 14px 0 10px; font-family: var(--mono); font-size: 10.5px; }
|
|
1152
|
-
.chapter-divider .ch-prev { color: var(--allow); white-space: nowrap; }
|
|
1153
|
-
.chapter-divider .ch-prev:empty { display: none; }
|
|
1154
|
-
.chapter-divider .ch-line { flex: 1; height: 1px; background: var(--border); }
|
|
1155
|
-
.chapter-divider .ch-next { color: var(--fg-50); white-space: nowrap; text-transform: uppercase; letter-spacing: 0.4px; }
|
|
1156
|
-
.chapter-divider .ch-next b { color: var(--accent); }
|
|
1157
|
-
/* Persona selector · "show only what matters to your role" */
|
|
1158
|
-
.persona-select { background: var(--bg-2,#1a1a1f); color: var(--fg-70); border: 1px solid var(--border); border-radius: 6px; font-size: 11px; padding: 4px 8px; font-family: inherit; cursor: pointer; }
|
|
1159
|
-
.persona-select:hover { border-color: var(--fg-30); }
|
|
1160
|
-
.density-toggle { background: var(--bg-2,#1a1a1f); color: var(--fg-70); border: 1px solid var(--border); border-radius: 6px; font-size: 11px; padding: 4px 9px; font-family: inherit; cursor: pointer; }
|
|
1161
|
-
.density-toggle:hover { border-color: var(--fg-30); }
|
|
1162
|
-
.density-toggle.on { border-color: var(--accent); color: var(--fg); background: rgba(255,154,118,.08); }
|
|
1163
|
-
/* Compact density · tightens the thread for daily-driver scanning (demo W8) */
|
|
1164
|
-
body.density-compact .panel { padding: 8px 11px; margin-bottom: 8px; }
|
|
1165
|
-
body.density-compact .tc-card { padding: 5px 9px; }
|
|
1166
|
-
body.density-compact .tc-out, body.density-compact .output { padding: 4px 8px; font-size: 10px; }
|
|
1167
|
-
body.density-compact .conv-turn { padding: 6px 0; }
|
|
1168
|
-
body.density-compact .conv-body { font-size: 12px; }
|
|
1169
|
-
body.density-compact .artifact-card { margin-bottom: 7px; }
|
|
1170
|
-
body[data-persona="pm"] .p-eng, body[data-persona="designer"] .p-eng { display: none; }
|
|
1171
|
-
/* Per-role granularity · PM wants decisions/outcomes (hide the technical
|
|
1172
|
-
request-flow); Designer wants the visual flow (hide the data tables). */
|
|
1173
|
-
body[data-persona="pm"] .p-flow { display: none; }
|
|
1174
|
-
body[data-persona="designer"] .p-data { display: none; }
|
|
1175
|
-
/* User-override result line (E-03 real flow) */
|
|
1176
|
-
.override-result { margin-top: 8px; font-size: 11.5px; color: var(--fg-60); }
|
|
1177
|
-
.override-result .ok { color: var(--allow); margin-right: 4px; }
|
|
1178
|
-
.override-result code { background: var(--bg-2,#1a1a1f); padding: 1px 5px; border-radius: 3px; color: var(--accent); font-size: 10.5px; }
|
|
1179
|
-
/* Structured artifacts · comparison / mock / pick (demo v12 parity) */
|
|
1180
|
-
.artifact-card { margin-bottom: 12px; }
|
|
1181
|
-
.artifact-h { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); margin-bottom: 6px; }
|
|
1182
|
-
.art-cmp { display: grid; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; font-size: 11px; }
|
|
1183
|
-
.art-cmp .hd { background: var(--bg-2,#1a1a1f); color: var(--fg-60); font-family: var(--mono); font-size: 10px; padding: 6px 8px; font-weight: 600; }
|
|
1184
|
-
.art-cmp .rl { background: var(--bg-1,#141418); color: var(--fg-50); font-family: var(--mono); font-size: 10px; padding: 6px 8px; }
|
|
1185
|
-
.art-cmp .cell { background: var(--bg,#0f0f12); color: var(--fg-70); padding: 6px 8px; }
|
|
1186
|
-
.art-cmp .cell .pick { color: var(--allow); }
|
|
1187
|
-
.art-cmp .cell .meh { color: var(--warn,#e0b341); }
|
|
1188
|
-
.art-mock { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; margin-bottom: 12px; }
|
|
1189
|
-
.art-mock-head { display: flex; align-items: center; background: var(--bg-2,#1a1a1f); padding: 6px 10px; font-family: var(--mono); font-size: 10.5px; }
|
|
1190
|
-
.art-mock-head .status { color: var(--warn,#e0b341); }
|
|
1191
|
-
.art-mock-head .ct { margin-left: auto; color: var(--fg-40); font-size: 10px; }
|
|
1192
|
-
.art-mock-body { padding: 10px 12px; font-family: var(--mono); font-size: 11px; color: var(--fg-70); white-space: pre; overflow-x: auto; }
|
|
1193
|
-
.art-mock-body .jk { color: var(--fg-50); }
|
|
1194
|
-
.art-mock-body .js { color: var(--allow); }
|
|
1195
|
-
.art-mock-body .jn { color: var(--warn,#e0b341); }
|
|
1196
|
-
.art-pick { border-left: 2px solid var(--accent); background: linear-gradient(90deg, rgba(255,154,118,0.05), transparent); border-radius: 0 6px 6px 0; padding: 10px 12px; margin-bottom: 12px; }
|
|
1197
|
-
.art-pick .k { color: var(--accent); font-family: var(--mono); font-size: 11.5px; font-weight: 600; margin-bottom: 5px; }
|
|
1198
|
-
.art-pick .what { color: var(--fg-80); font-size: 12px; line-height: 1.5; margin-bottom: 5px; }
|
|
1199
|
-
.art-pick .why { color: var(--fg-50); font-size: 11.5px; line-height: 1.5; }
|
|
1200
|
-
/* Benchmark chart · horizontal bars (demo v12 .bench) */
|
|
1201
|
-
.art-bench { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; font-size: 11px; }
|
|
1202
|
-
.art-bench .bl { font-family: var(--mono); color: var(--fg-50); min-width: 96px; font-size: 10.5px; }
|
|
1203
|
-
.art-bench .bb { flex: 1; height: 8px; background: var(--bg-2,#1a1a1f); border-radius: 4px; overflow: hidden; }
|
|
1204
|
-
.art-bench .bf { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
|
|
1205
|
-
.art-bench .bf.allow { background: var(--allow); }
|
|
1206
|
-
.art-bench .bf.warn { background: var(--warn,#e0b341); }
|
|
1207
|
-
.art-bench .bv { font-family: var(--mono); color: var(--fg-70); min-width: 56px; text-align: right; font-size: 10.5px; }
|
|
1208
|
-
/* Verifier severity checklist · ✓/⚠ per check */
|
|
1209
|
-
.art-checklist { display: flex; flex-direction: column; gap: 1px; }
|
|
1210
|
-
.art-check { display: flex; align-items: baseline; gap: 9px; padding: 5px 0; border-top: 1px solid rgba(42,42,50,.5); font-size: 12px; }
|
|
1211
|
-
.art-check:first-child { border-top: none; }
|
|
1212
|
-
.art-check .ci { color: var(--allow); flex-shrink: 0; font-size: 12px; }
|
|
1213
|
-
.art-check.warn .ci { color: var(--warn,#e0b341); }
|
|
1214
|
-
.art-check .cl { color: var(--fg-80); }
|
|
1215
|
-
.art-check .cn { color: var(--fg-40); font-family: var(--mono); font-size: 10.5px; margin-left: auto; text-align: right; }
|
|
1216
|
-
/* Sequence diagram · inline SVG · var() resolves in CSS rules, never in SVG
|
|
1217
|
-
presentation attributes — so every fill/stroke is classed, not inlined. */
|
|
1218
|
-
.art-seq { overflow-x: auto; padding: 2px 0; }
|
|
1219
|
-
.seq-svg { display: block; max-width: 100%; }
|
|
1220
|
-
.seq-svg .ll { stroke: var(--border); stroke-dasharray: 3 3; }
|
|
1221
|
-
.seq-svg .box { fill: var(--bg-2,#1a1a1f); stroke: var(--border); }
|
|
1222
|
-
.seq-svg .at { fill: var(--fg-80); }
|
|
1223
|
-
.seq-svg .msg { stroke: var(--fg-50); fill: none; }
|
|
1224
|
-
.seq-svg .msg.ret { stroke: var(--fg-40); stroke-dasharray: 4 3; }
|
|
1225
|
-
.seq-svg .msg.allow { stroke: var(--allow); }
|
|
1226
|
-
.seq-svg .msg.warn { stroke: var(--warn,#e0b341); }
|
|
1227
|
-
.seq-svg .mt { fill: var(--fg-60); }
|
|
1228
|
-
.seq-svg .mt.allow { fill: var(--allow); }
|
|
1229
|
-
.seq-svg .mt.warn { fill: var(--warn,#e0b341); }
|
|
1230
|
-
.seq-svg .ah { fill: var(--fg-50); }
|
|
1231
|
-
.seq-svg .ah-allow { fill: var(--allow); }
|
|
1232
|
-
.seq-svg .ah-warn { fill: var(--warn,#e0b341); }
|
|
1233
|
-
.seq-svg .ah-ret { fill: var(--fg-40); }
|
|
1234
|
-
/* Multi-speaker conversation · me + team + agents (demo v12 .turn/.speaker) */
|
|
1235
|
-
.conv-turn { padding: 9px 0; border-top: 1px solid rgba(42,42,50,.5); }
|
|
1236
|
-
.conv-turn:first-child { border-top: none; }
|
|
1237
|
-
.conv-head { display: flex; align-items: center; gap: 7px; margin-bottom: 5px; }
|
|
1238
|
-
.conv-name { font-size: 12px; font-weight: 600; color: var(--fg-80); }
|
|
1239
|
-
.conv-ts { font-family: var(--mono); font-size: 10px; color: var(--fg-30); margin-left: auto; }
|
|
1240
|
-
.conv-body { font-size: 13px; color: var(--fg-80); line-height: 1.55; padding-left: 25px; }
|
|
1241
|
-
.conv-body .at-route { color: var(--accent); font-family: var(--mono); font-size: 12px; }
|
|
1242
|
-
.conv-body .route-line { color: var(--fg-50); font-size: 12px; }
|
|
1243
|
-
.conv-body .route-line .ab { vertical-align: middle; margin: 0 3px; }
|
|
1244
|
-
|
|
1245
|
-
/* Unified timeline (task #51) · human/agent tier chips + inline collapsed
|
|
1246
|
-
tool events + emergent section dividers. Reuses .conv-* + .ab. */
|
|
1247
|
-
.conv-tier { font-family: var(--mono); font-size: 8.5px; letter-spacing: .06em; text-transform: uppercase; padding: 1px 6px; border-radius: 4px; border: 1px solid var(--border); color: var(--fg-50); }
|
|
1248
|
-
.conv-turn.is-human .conv-tier { color: var(--fg-80); border-color: rgba(255,255,255,.18); }
|
|
1249
|
-
.conv-turn.is-human { background: rgba(255,255,255,.012); border-radius: 8px; }
|
|
1250
|
-
.tl-div { display: flex; align-items: center; gap: 9px; margin: 15px 0 8px; }
|
|
1251
|
-
.tl-verb { font-family: var(--mono); font-size: 10px; letter-spacing: .03em; color: var(--fg-70); background: rgba(255,255,255,.03); border: 1px solid var(--border); border-radius: 999px; padding: 2px 10px; white-space: nowrap; }
|
|
1252
|
-
.tl-cnt { font-family: var(--mono); font-size: 9.5px; color: var(--fg-30); white-space: nowrap; }
|
|
1253
|
-
.tl-ln { flex: 1; height: 1px; background: var(--border); }
|
|
1254
|
-
.tl-acts { padding-left: 25px; display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }
|
|
1255
|
-
.tl-evt { font-family: var(--mono); font-size: 11px; }
|
|
1256
|
-
.tl-evt > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 7px; padding: 4px 9px; border: 1px solid var(--border); border-radius: 6px; color: var(--fg-50); }
|
|
1257
|
-
.tl-evt > summary::-webkit-details-marker { display: none; }
|
|
1258
|
-
.tl-evt > summary:hover { color: var(--fg-70); border-color: var(--fg-30); }
|
|
1259
|
-
.tl-evt .tl-caret { color: var(--fg-30); transition: transform .12s; font-size: 9px; }
|
|
1260
|
-
.tl-evt[open] .tl-caret { transform: rotate(90deg); }
|
|
1261
|
-
.tl-evt .tl-lbl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1262
|
-
.tl-evt .tl-dur { color: var(--fg-30); }
|
|
1263
|
-
.tl-evt .tl-badge { font-size: 8.5px; padding: 1px 6px; border-radius: 4px; margin-left: auto; }
|
|
1264
|
-
.tl-evt .tl-lbl + .tl-badge { margin-left: auto; }
|
|
1265
|
-
.tl-evt .tl-badge.allow { color: var(--allow); background: rgba(52,208,88,.12); }
|
|
1266
|
-
.tl-evt .tl-badge.block { color: #e5534b; background: rgba(229,83,75,.14); }
|
|
1267
|
-
.tl-evt .tl-badge.clean { color: var(--accent); background: rgba(255,154,118,.14); }
|
|
1268
|
-
.tl-evt.block > summary { border-color: rgba(229,83,75,.35); }
|
|
1269
|
-
.tl-out { font-family: var(--mono); font-size: 10.5px; color: var(--fg-50); white-space: pre-wrap; padding: 7px 10px 2px 20px; }
|
|
1270
|
-
/* Agent's own words (kind:'say') · the "watch it work" reasoning, in the agent's voice */
|
|
1271
|
-
.tl-say { display: flex; gap: 9px; align-items: flex-start; margin: 9px 0; }
|
|
1272
|
-
.tl-say .ab { margin-top: 2px; }
|
|
1273
|
-
.tl-say-text { flex: 1; min-width: 0; font-size: 13px; color: var(--fg-80); line-height: 1.5; background: rgba(255,255,255,.025); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; }
|
|
1274
|
-
|
|
1275
|
-
.panel {
|
|
1276
|
-
border: 1px solid var(--border); border-radius: 8px;
|
|
1277
|
-
padding: 12px 14px; margin-bottom: 12px;
|
|
1278
|
-
}
|
|
1279
|
-
.panel-title { font-family: var(--mono); font-size: 10px; color: var(--fg-40); text-transform: uppercase; margin-bottom: 8px; }
|
|
1280
|
-
/* Observed (host) sessions — the coding agents codehere watches in your own
|
|
1281
|
-
terminal · distinct from spawned tasks below. Proves the INWARD axis:
|
|
1282
|
-
every agent, even the one you're talking to right now, is a member. */
|
|
1283
|
-
.obs-strip { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 14px; background: linear-gradient(180deg, rgba(255,154,118,0.04), transparent); }
|
|
1284
|
-
.obs-head { display: flex; align-items: center; gap: 7px; font-size: 11.5px; color: var(--fg-60); margin-bottom: 2px; }
|
|
1285
|
-
.obs-head .eye { opacity: 0.7; }
|
|
1286
|
-
.obs-head b { color: var(--fg-80); font-weight: 600; }
|
|
1287
|
-
.obs-sub { font-size: 10.5px; color: var(--fg-40); margin-bottom: 9px; }
|
|
1288
|
-
.obs-row { display: flex; align-items: center; gap: 10px; padding: 6px 8px; margin: 0 -8px; border-top: 1px solid var(--border); font-size: 11.5px; cursor: pointer; border-radius: 5px; transition: background 0.12s; }
|
|
1289
|
-
.obs-row:first-of-type { border-top: none; }
|
|
1290
|
-
.obs-row:hover { background: rgba(255,154,118,0.06); }
|
|
1291
|
-
.obs-id { font-family: var(--mono); color: var(--accent); font-size: 11px; min-width: 66px; }
|
|
1292
|
-
.obs-mix { color: var(--fg-50); flex: 1; font-family: var(--mono); font-size: 10.5px; }
|
|
1293
|
-
.obs-dur { color: var(--fg-40); font-size: 10.5px; }
|
|
1294
|
-
.obs-live { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
|
1295
|
-
.obs-live.live { background: var(--allow); animation: cb-blink 1.4s ease infinite; }
|
|
1296
|
-
.obs-live.recent { background: var(--warn,#e0b341); }
|
|
1297
|
-
.obs-live.idle { background: var(--fg-30); }
|
|
1298
|
-
.obs-badge { font-size: 9.5px; text-transform: uppercase; font-family: var(--mono); padding: 1px 6px; border-radius: 4px; letter-spacing: 0.3px; }
|
|
1299
|
-
.obs-badge.low { color: var(--allow); border: 1px solid color-mix(in srgb, var(--allow) 40%, transparent); }
|
|
1300
|
-
.obs-badge.medium { color: var(--warn, #e0b341); border: 1px solid color-mix(in srgb, var(--warn, #e0b341) 40%, transparent); }
|
|
1301
|
-
.obs-badge.high { color: var(--block); border: 1px solid color-mix(in srgb, var(--block) 40%, transparent); }
|
|
1302
|
-
.obs-blocked { color: var(--block); font-size: 10.5px; font-family: var(--mono); }
|
|
1303
|
-
/* Host-session thread · tool-call timeline */
|
|
1304
|
-
.obs-timeline { display: flex; flex-direction: column; }
|
|
1305
|
-
.obs-call { display: flex; align-items: baseline; gap: 10px; padding: 5px 0; border-top: 1px solid var(--border); font-size: 11.5px; }
|
|
1306
|
-
.obs-call:first-child { border-top: none; }
|
|
1307
|
-
.obs-call.is-block { background: color-mix(in srgb, var(--block) 8%, transparent); margin: 0 -8px; padding: 5px 8px; border-radius: 4px; border-top-color: transparent; }
|
|
1308
|
-
.obs-call-time { font-family: var(--mono); color: var(--fg-30); font-size: 10px; min-width: 64px; flex-shrink: 0; }
|
|
1309
|
-
.obs-call-tool { font-family: var(--mono); color: var(--accent); font-size: 10.5px; min-width: 56px; flex-shrink: 0; }
|
|
1310
|
-
.obs-call-prev { color: var(--fg-60); font-family: var(--mono); font-size: 10.5px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1311
|
-
.obs-call-tag { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: 0.3px; padding: 1px 5px; border-radius: 3px; flex-shrink: 0; }
|
|
1312
|
-
.obs-call-tag.ok { color: var(--fg-40); }
|
|
1313
|
-
.obs-call-tag.warn { color: var(--warn, #e0b341); }
|
|
1314
|
-
.obs-call-tag.block { color: var(--block); border: 1px solid color-mix(in srgb, var(--block) 40%, transparent); }
|
|
1315
|
-
.obs-call-tag.crit { color: #fff; background: var(--block); }
|
|
1316
|
-
/* Chain-flow · audit chain as connected nodes (demo v12 .chain-flow) */
|
|
1317
|
-
.chain-flow-wrap { padding: 4px 0 14px; }
|
|
1318
|
-
.chain-flow { display: flex; align-items: center; gap: 0; overflow-x: auto; padding: 4px 2px 8px; }
|
|
1319
|
-
.cf-node { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; min-width: 34px; height: 34px; border-radius: 7px; border: 1px solid var(--border); background: var(--bg-2,#1a1a1f); flex-shrink: 0; cursor: default; }
|
|
1320
|
-
.cf-node .cf-seq { font-family: var(--mono); font-size: 7.5px; color: var(--fg-40); line-height: 1; }
|
|
1321
|
-
.cf-node .cf-tool { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--fg-70); line-height: 1.2; }
|
|
1322
|
-
.cf-node.ok { border-color: color-mix(in srgb, var(--allow) 35%, var(--border)); }
|
|
1323
|
-
.cf-node.ok .cf-tool { color: var(--allow); }
|
|
1324
|
-
.cf-node.block { border-color: var(--block); background: color-mix(in srgb, var(--block) 12%, var(--bg-2,#1a1a1f)); }
|
|
1325
|
-
.cf-node.block .cf-tool { color: var(--block); }
|
|
1326
|
-
.cf-node.crit { border-color: var(--block); background: var(--block); }
|
|
1327
|
-
.cf-node.crit .cf-tool, .cf-node.crit .cf-seq { color: #fff; }
|
|
1328
|
-
.cf-link { width: 14px; height: 2px; background: linear-gradient(90deg, var(--allow), color-mix(in srgb, var(--allow) 30%, transparent)); flex-shrink: 0; opacity: 0.6; }
|
|
1329
|
-
.cf-gap { font-family: var(--mono); font-size: 11px; color: var(--fg-30); padding: 0 8px; flex-shrink: 0; letter-spacing: 1px; }
|
|
1330
|
-
.cf-legend { font-family: var(--mono); font-size: 10px; color: var(--fg-40); }
|
|
1331
|
-
/* Observed session · honest read-only affordance (no fake composer) */
|
|
1332
|
-
.obs-readonly-note { margin-top: 14px; padding: 12px 14px; border: 1px dashed var(--border); border-radius: 8px; background: rgba(255,255,255,0.015); }
|
|
1333
|
-
.obs-readonly-note .t { font-size: 12px; color: var(--fg-50); line-height: 1.5; margin-bottom: 10px; }
|
|
1334
|
-
.obs-fork-btn { background: var(--accent); color: #1a1a1f; border: none; border-radius: 6px; font-size: 12px; font-weight: 600; padding: 7px 12px; cursor: pointer; }
|
|
1335
|
-
.obs-fork-btn:hover { filter: brightness(1.08); }
|
|
1336
|
-
.obs-fork-btn:disabled { opacity: 0.6; cursor: default; }
|
|
1337
|
-
/* Remote-control panel · drive a managed agent (inject into running turn) */
|
|
1338
|
-
#control-panel:empty { display: none; }
|
|
1339
|
-
.ctl-head { font-size: 11px; color: var(--fg-50); font-family: var(--mono); margin-bottom: 8px; }
|
|
1340
|
-
.ctl-head .ag { color: var(--agent-codex); font-weight: 600; }
|
|
1341
|
-
.ctl-head code { background: var(--bg-2,#1a1a1f); padding: 1px 5px; border-radius: 3px; color: var(--fg-70); }
|
|
1342
|
-
.ctl-feed { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; max-height: 240px; overflow-y: auto; }
|
|
1343
|
-
.ctl-evt { font-size: 12px; color: var(--fg-80); padding: 5px 9px; background: var(--bg-2,#1a1a1f); border-radius: 6px; border-left: 2px solid var(--agent-codex); }
|
|
1344
|
-
.ctl-evt.dim { color: var(--fg-40); border-left-color: var(--border); font-style: italic; }
|
|
1345
|
-
.ctl-composer { display: flex; gap: 7px; }
|
|
1346
|
-
.ctl-composer input { flex: 1; background: var(--bg-2,#1a1a1f); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); padding: 7px 10px; font-size: 12px; }
|
|
1347
|
-
.ctl-composer input:focus { outline: none; border-color: var(--accent); }
|
|
1348
|
-
.ctl-composer button { background: var(--accent); color: #1a1a1f; border: none; border-radius: 6px; font-size: 12px; font-weight: 600; padding: 7px 12px; cursor: pointer; }
|
|
1349
|
-
/* Remote Control · honest live badge + steer chips + channel line. The badge
|
|
1350
|
-
renders ONLY inside the real-channel branch (controlManagedAgent success) —
|
|
1351
|
-
no live channel, no badge. Anti-screen-lie (the F-AGENT-SWITCHER class). */
|
|
1352
|
-
.ctl-badge { display: inline-flex; align-items: center; gap: 7px; background: rgba(40,200,64,0.08); border: 1px solid rgba(40,200,64,0.35); border-radius: 999px; padding: 5px 12px; font-size: 12px; color: var(--allow); font-weight: 600; margin-bottom: 10px; }
|
|
1353
|
-
.ctl-badge .lv { width: 8px; height: 8px; border-radius: 50%; background: var(--allow); box-shadow: 0 0 7px var(--allow); animation: pulse 1.6s infinite; }
|
|
1354
|
-
.ctl-steer { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 9px; }
|
|
1355
|
-
.ctl-steer .chip { font-size: 11.5px; color: var(--fg-70); border: 1px solid var(--border); border-radius: 999px; padding: 4px 11px; cursor: pointer; }
|
|
1356
|
-
.ctl-steer .chip:hover { border-color: var(--accent); color: var(--fg); }
|
|
1357
|
-
.ctl-steer .chip.stop { border-color: rgba(255,95,87,0.4); color: var(--block); }
|
|
1358
|
-
.ctl-channel { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--fg-40); margin-top: 8px; }
|
|
1359
|
-
.ctl-channel .lv { width: 7px; height: 7px; border-radius: 50%; background: var(--allow); box-shadow: 0 0 7px var(--allow); }
|
|
1360
|
-
.ctl-channel b { color: var(--allow); }
|
|
1361
|
-
.outcome-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--fg-70); margin-bottom: 4px; }
|
|
1362
|
-
.outcome-item .check { color: var(--allow); }
|
|
1363
|
-
.outcome-item .cross { color: var(--block); }
|
|
1364
|
-
.outcome-item .pending { color: var(--fg-40); }
|
|
1365
|
-
.progress-dots { display: flex; gap: 3px; margin: 8px 0; }
|
|
1366
|
-
.pdot { width: 10px; height: 10px; border-radius: 50%; border: 1px solid var(--border); }
|
|
1367
|
-
.pdot.done { background: var(--allow); border-color: var(--allow); }
|
|
1368
|
-
.pdot.current { background: var(--accent); border-color: var(--accent); animation: pulse 1.5s ease infinite; }
|
|
1369
|
-
.pdot.pending { background: transparent; }
|
|
1370
|
-
|
|
1371
|
-
.verdict { font-family: var(--mono); font-size: 11px; margin-top: 8px; }
|
|
1372
|
-
.verdict.unverified { color: var(--fg-40); }
|
|
1373
|
-
.verdict.verified { color: var(--allow); }
|
|
1374
|
-
.verdict.failed { color: var(--block); }
|
|
1375
|
-
|
|
1376
|
-
/* F-07 · verifier-agent panel (verified celebration / broken alert / partial warn) */
|
|
1377
|
-
/* Verifier-agent as killer-moment step · accent border, bigger type,
|
|
1378
|
-
soft glow · this is THE differentiator per claim_market_position. */
|
|
1379
|
-
.verifier-killer-step {
|
|
1380
|
-
border: 1px solid rgba(40,200,64,0.30) !important;
|
|
1381
|
-
border-left-width: 3px !important;
|
|
1382
|
-
background: linear-gradient(135deg, rgba(40,200,64,0.04), rgba(255,154,118,0.02)) !important;
|
|
1383
|
-
box-shadow: 0 0 0 3px rgba(40,200,64,0.04);
|
|
1384
|
-
padding: 12px 14px !important;
|
|
1385
|
-
}
|
|
1386
|
-
.verifier-killer-title { color: var(--allow); font-weight: 700; font-size: 12.5px; letter-spacing: 0.04em; font-family: var(--mono); }
|
|
1387
|
-
.verifier-killer-body { color: var(--fg-90); font-size: 13px; line-height: 1.55; margin-top: 6px; padding-left: 0; }
|
|
1388
|
-
.verifier-killer-body code { background: var(--bg-2); padding: 1px 5px; border-radius: 3px; font-family: var(--mono); font-size: 11.5px; color: var(--accent); }
|
|
1389
|
-
|
|
1390
|
-
/* W-PASS payload diff coloring · old values red strike-through, new values green */
|
|
1391
|
-
.wp-payload .key { color: var(--fg-50); }
|
|
1392
|
-
.wp-payload .val { color: var(--allow); }
|
|
1393
|
-
.wp-payload .from { color: var(--block); text-decoration: line-through; }
|
|
1394
|
-
.wp-payload .arrow { color: var(--fg-40); margin: 0 4px; }
|
|
1395
|
-
.wp-payload .added { color: var(--allow); }
|
|
1396
|
-
|
|
1397
|
-
/* W-PASS inline write-pass card · matches demo v12 lines 691-704 · blue
|
|
1398
|
-
* accent (not orange) because writes to third-party systems are the rare
|
|
1399
|
-
* moment when codehere mediates instead of just observes. Lives inline in
|
|
1400
|
-
* the step timeline · primary button opens the modal for actual approval. */
|
|
1401
|
-
.write-pass {
|
|
1402
|
-
padding: 12px 14px; margin: 10px 0;
|
|
1403
|
-
background: rgba(74,144,226,0.05);
|
|
1404
|
-
border: 1px solid rgba(74,144,226,0.3);
|
|
1405
|
-
border-left: 3px solid #4a90e2;
|
|
1406
|
-
border-radius: 6px;
|
|
1407
|
-
}
|
|
1408
|
-
.write-pass .head {
|
|
1409
|
-
display: flex; align-items: baseline; gap: 8px;
|
|
1410
|
-
margin-bottom: 6px;
|
|
1411
|
-
font-family: var(--mono); font-size: 10.5px;
|
|
1412
|
-
color: #4a90e2; letter-spacing: 0.12em; text-transform: uppercase;
|
|
1413
|
-
}
|
|
1414
|
-
.write-pass h4 {
|
|
1415
|
-
font-size: 13.5px; font-weight: 600; color: var(--fg);
|
|
1416
|
-
margin: 2px 0 6px;
|
|
1417
|
-
}
|
|
1418
|
-
.write-pass .target { font-family: var(--mono); font-size: 11.5px; color: var(--fg-70); margin-bottom: 6px; }
|
|
1419
|
-
.write-pass .target b { color: var(--fg); }
|
|
1420
|
-
.write-pass .payload {
|
|
1421
|
-
padding: 8px 11px; margin: 6px 0;
|
|
1422
|
-
background: hsl(0 0% 9%); border: 1px solid var(--border);
|
|
1423
|
-
border-radius: 4px;
|
|
1424
|
-
font-family: var(--mono); font-size: 11px;
|
|
1425
|
-
color: var(--fg-70); white-space: pre-wrap; line-height: 1.55;
|
|
1426
|
-
}
|
|
1427
|
-
.write-pass .payload .key { color: var(--fg-50); }
|
|
1428
|
-
.write-pass .payload .val { color: var(--allow); font-weight: 600; }
|
|
1429
|
-
.write-pass .payload .from { color: var(--block); text-decoration: line-through; }
|
|
1430
|
-
.write-pass .payload .arrow { color: var(--fg-40); margin: 0 4px; }
|
|
1431
|
-
.write-pass .actions {
|
|
1432
|
-
display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap;
|
|
1433
|
-
}
|
|
1434
|
-
.write-pass button {
|
|
1435
|
-
font-family: var(--mono); font-size: 11px; padding: 5px 11px;
|
|
1436
|
-
border-radius: 4px; cursor: pointer;
|
|
1437
|
-
border: 1px solid var(--border); background: transparent; color: var(--fg-70);
|
|
1438
|
-
transition: filter 150ms, border-color 150ms, color 150ms;
|
|
1439
|
-
}
|
|
1440
|
-
.write-pass button.primary {
|
|
1441
|
-
background: #4a90e2; color: #000; border-color: #4a90e2; font-weight: 600;
|
|
1442
|
-
}
|
|
1443
|
-
.write-pass button.primary:hover { filter: brightness(1.08); }
|
|
1444
|
-
.write-pass button.secondary:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
1445
|
-
|
|
1446
|
-
.verifier-panel { border-left: 2px solid var(--border); padding-left: 12px; }
|
|
1447
|
-
.gotcha { border-left-color: var(--block); background: rgba(255,95,87,0.04); padding: 6px 10px; border-radius: 4px; margin-top: 4px; font-size: 12px; }
|
|
1448
|
-
.verifier-panel.broken { border-left-color: var(--block); }
|
|
1449
|
-
.verifier-panel.partial { border-left-color: var(--warn); }
|
|
1450
|
-
|
|
1451
|
-
/* Verified celebration · matches demo v12 .verified-block (lines 429-438).
|
|
1452
|
-
* Centered card with gradient green bg, 38px ✓ badge, uppercase VERIFIED
|
|
1453
|
-
* kicker, larger headline, mono stats line. Arrival animation slides up
|
|
1454
|
-
* + shimmer sweep so the moment lands. The single most important moment
|
|
1455
|
-
* in the detail view — the agent's claim survived verification. */
|
|
1456
|
-
.verified-block {
|
|
1457
|
-
padding: 18px 22px; text-align: center;
|
|
1458
|
-
background: linear-gradient(135deg, rgba(40,200,64,0.08), rgba(40,200,64,0.02));
|
|
1459
|
-
border: 1px solid rgba(40,200,64,0.35);
|
|
1460
|
-
border-left: 3px solid var(--allow); border-radius: 8px;
|
|
1461
|
-
margin: 8px 0; position: relative; overflow: hidden;
|
|
1462
|
-
opacity: 0; transform: translateY(12px);
|
|
1463
|
-
animation: verifiedIn 600ms cubic-bezier(0.16,1,0.3,1) 300ms forwards,
|
|
1464
|
-
verifiedShimmer 1.2s ease 900ms;
|
|
1465
|
-
}
|
|
1466
|
-
@keyframes verifiedIn { to { opacity: 1; transform: translateY(0); } }
|
|
1467
|
-
@keyframes verifiedShimmer {
|
|
1468
|
-
0% { background-image: linear-gradient(110deg, transparent 30%, rgba(40,200,64,0.18) 50%, transparent 70%); background-size: 250% 100%; background-position: -50% 0; background-repeat: no-repeat; }
|
|
1469
|
-
100% { background-image: linear-gradient(110deg, transparent 30%, rgba(40,200,64,0.18) 50%, transparent 70%); background-size: 250% 100%; background-position: 150% 0; background-repeat: no-repeat; }
|
|
1470
|
-
}
|
|
1471
|
-
.verified-block .badge {
|
|
1472
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
1473
|
-
width: 38px; height: 38px; border-radius: 50%;
|
|
1474
|
-
background: var(--allow); color: #000;
|
|
1475
|
-
font-size: 18px; font-weight: 700;
|
|
1476
|
-
margin-bottom: 8px; box-shadow: 0 0 0 4px rgba(40,200,64,0.15);
|
|
1477
|
-
}
|
|
1478
|
-
.verified-block .k {
|
|
1479
|
-
font-family: var(--mono); font-size: 10.5px; color: var(--allow);
|
|
1480
|
-
letter-spacing: 0.18em; text-transform: uppercase; margin-bottom: 5px;
|
|
1481
|
-
}
|
|
1482
|
-
.verified-block .what {
|
|
1483
|
-
font-size: 18px; font-weight: 600; color: var(--fg);
|
|
1484
|
-
margin-bottom: 6px; letter-spacing: -0.01em; line-height: 1.35;
|
|
1485
|
-
}
|
|
1486
|
-
.verified-block .stats {
|
|
1487
|
-
color: var(--fg-50); font-size: 11.5px;
|
|
1488
|
-
font-family: var(--mono); letter-spacing: 0.04em;
|
|
1489
|
-
}
|
|
1490
|
-
.verifier-summary { font-size: 13px; color: var(--fg-90); margin-bottom: 6px; }
|
|
1491
|
-
.verifier-summary .v-icon { display: inline-block; margin-right: 6px; font-weight: bold; }
|
|
1492
|
-
.verifier-panel.broken .v-icon { color: var(--block); }
|
|
1493
|
-
.verifier-panel.verified .v-icon { color: var(--allow); }
|
|
1494
|
-
.verifier-panel.partial .v-icon { color: var(--warn); }
|
|
1495
|
-
.finding { padding: 8px 10px; margin-top: 6px; border-radius: 4px; background: rgba(255,255,255,0.02); font-size: 12px; }
|
|
1496
|
-
.finding-sev { font-family: var(--mono); font-size: 9px; padding: 1px 5px; border-radius: 3px; margin-right: 6px; text-transform: uppercase; }
|
|
1497
|
-
.finding-sev.fail { color: var(--block); background: rgba(255,95,87,0.10); }
|
|
1498
|
-
.finding-sev.warn { color: var(--warn); background: rgba(254,188,46,0.10); }
|
|
1499
|
-
.finding-sev.info { color: var(--fg-50); background: rgba(255,255,255,0.04); }
|
|
1500
|
-
.finding-kind { font-family: var(--mono); font-size: 10px; color: var(--fg-40); margin-right: 6px; }
|
|
1501
|
-
.finding-msg { color: var(--fg-90); }
|
|
1502
|
-
.finding-detail { font-family: var(--mono); font-size: 11px; color: var(--fg-50); margin-top: 4px; padding-left: 18px; }
|
|
1503
|
-
.finding-detail .label { color: var(--fg-30); margin-right: 4px; }
|
|
1504
|
-
|
|
1505
|
-
details.collapsible { margin-bottom: 12px; }
|
|
1506
|
-
details.collapsible summary {
|
|
1507
|
-
font-family: var(--mono); font-size: 11px; color: var(--fg-50);
|
|
1508
|
-
cursor: pointer; padding: 8px 0; user-select: none;
|
|
1509
|
-
}
|
|
1510
|
-
details.collapsible summary:hover { color: var(--fg); }
|
|
1511
|
-
.turn { padding: 6px 0; border-bottom: 1px solid var(--border-soft); font-size: 12px; }
|
|
1512
|
-
.turn.user { color: var(--accent); }
|
|
1513
|
-
.turn.assistant { color: var(--fg-70); }
|
|
1514
|
-
.tc { font-size: 12px; padding: 8px 0; border-bottom: 1px solid var(--border-soft); }
|
|
1515
|
-
.tc-label { color: var(--fg); font-weight: 500; }
|
|
1516
|
-
.tc-output { margin: 4px 0 0; padding: 6px 10px; background: var(--bg-2); border-radius: 4px; font-family: var(--mono); font-size: 11px; color: var(--fg-50); white-space: pre-wrap; word-break: break-all; max-height: 100px; overflow: auto; }
|
|
1517
|
-
.tc-dur { font-size: 10px; color: var(--fg-30); margin-left: 8px; }
|
|
1518
|
-
|
|
1519
|
-
.input-area {
|
|
1520
|
-
border-top: 1px solid var(--border-soft); padding: 12px 20px;
|
|
1521
|
-
background: var(--bg-1); flex-shrink: 0;
|
|
1522
|
-
}
|
|
1523
|
-
.input-bar {
|
|
1524
|
-
display: flex; align-items: center; gap: 10px;
|
|
1525
|
-
background: var(--bg-2); border: 1px solid var(--border);
|
|
1526
|
-
border-radius: 10px; padding: 10px 14px; max-width: 900px; margin: 0 auto;
|
|
1527
|
-
}
|
|
1528
|
-
/* Composer · typing affordance · matches demo v12 .composer-bar.typing
|
|
1529
|
-
* (line 329). The border goes brand-orange the moment the user starts
|
|
1530
|
-
* typing · the input visibly "wakes up." :focus-within handles the
|
|
1531
|
-
* focus state, .typing handles the value-has-chars state. */
|
|
1532
|
-
.input-bar:focus-within { border-color: var(--accent); }
|
|
1533
|
-
.input-bar.typing { border-color: var(--accent); }
|
|
1534
|
-
.input-bar.warn { border-color: var(--warn); }
|
|
1535
|
-
.input-bar.block { border-color: var(--block); }
|
|
1536
|
-
/* R10 · one canonical composer at a time. When detail-view is open the
|
|
1537
|
-
* detail follow-up composer ("Ask a follow-up about this task...") is
|
|
1538
|
-
* the only input that should accept text · hide the global main
|
|
1539
|
-
* composer + kbd-hint. Pre-fix bug: BOTH composers stacked, both with
|
|
1540
|
-
* "↳ Auto · ~$0.02" chips, user couldn't tell which one typed to which.
|
|
1541
|
-
* Surfaced 2026-05-24 by Ega. */
|
|
1542
|
-
body.detail-open > .app > .input-area { display: none; }
|
|
1543
|
-
/* Keyboard hint · matches demo v12 lines 418-419. Subtle row of mono
|
|
1544
|
-
* shortcuts under the composer · ↵ send · ⇧↵ newline · ⌘K focus.
|
|
1545
|
-
* Discoverable without being noise. */
|
|
1546
|
-
.kbd-hint {
|
|
1547
|
-
font-family: var(--mono); font-size: 10px; color: var(--fg-30);
|
|
1548
|
-
padding: 4px 0 0; max-width: 900px; margin: 0 auto;
|
|
1549
|
-
display: flex; gap: 14px; align-items: center; flex-wrap: wrap;
|
|
1550
|
-
}
|
|
1551
|
-
.kbd-hint .item { display: inline-flex; gap: 5px; align-items: center; }
|
|
1552
|
-
.kbd {
|
|
1553
|
-
padding: 1px 5px; background: hsl(0 0% 12%);
|
|
1554
|
-
border: 1px solid var(--border); border-radius: 3px;
|
|
1555
|
-
color: var(--fg-50); font-size: 9px; line-height: 1.4;
|
|
1556
|
-
}
|
|
1557
|
-
.input-ta {
|
|
1558
|
-
flex: 1; background: transparent; border: none;
|
|
1559
|
-
color: var(--fg); font-family: var(--sans); font-size: 13px;
|
|
1560
|
-
resize: none; outline: none; height: 22px; line-height: 22px;
|
|
1561
|
-
}
|
|
1562
|
-
.input-ta::placeholder { color: var(--fg-40); }
|
|
1563
|
-
.input-ta:disabled { color: var(--fg-30); }
|
|
1564
|
-
.input-meta { font-family: var(--mono); font-size: 10px; color: var(--fg-40); white-space: nowrap; display: inline-flex; gap: 8px; align-items: center; }
|
|
1565
|
-
.input-meta .pick-agent { color: var(--fg-70); cursor: pointer; padding: 2px 6px; border-radius: 3px; transition: background 0.15s; background: transparent; border: none; font: inherit; -webkit-appearance: none; appearance: none; }
|
|
1566
|
-
.input-meta .pick-agent:hover { background: rgba(255,255,255,0.05); color: var(--fg); }
|
|
1567
|
-
.input-meta select.pick-agent option { background: var(--bg-2, #1a1a1f); color: var(--fg); }
|
|
1568
|
-
.input-meta .cost-est { color: var(--allow); padding: 2px 6px; border-radius: 3px; background: rgba(40,200,64,0.06); }
|
|
1569
|
-
.send-btn {
|
|
1570
|
-
background: var(--accent); color: #000; border: none; border-radius: 6px;
|
|
1571
|
-
width: 28px; height: 28px; display: flex; align-items: center;
|
|
1572
|
-
justify-content: center; cursor: pointer; flex-shrink: 0;
|
|
1573
|
-
transition: transform 0.2s;
|
|
1574
|
-
}
|
|
1575
|
-
.send-btn:hover { filter: brightness(1.05); }
|
|
1576
|
-
.send-btn:disabled { opacity: 0.35; cursor: default; }
|
|
1577
|
-
.offline-banner {
|
|
1578
|
-
text-align: center; padding: 4px; font-size: 10px; font-family: var(--mono);
|
|
1579
|
-
color: var(--warn); background: rgba(254,188,46,0.06);
|
|
1580
|
-
border-bottom: 1px solid rgba(254,188,46,0.15);
|
|
1581
|
-
}
|
|
1582
|
-
::-webkit-scrollbar { width: 6px; }
|
|
1583
|
-
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
1584
|
-
`;
|
|
1585
|
-
const APP_SCRIPT = `
|
|
1586
|
-
const _cached = {};
|
|
1587
|
-
let _filter = 'all', _detail = null, _room = null, _offline = false, _autoOpened = false;
|
|
1588
|
-
// Project-memory state for the open room · null = unknown (fetch in flight),
|
|
1589
|
-
// false = agents forget this project (show the nudge), true = memory already
|
|
1590
|
-
// on (stay quiet), 'just-on' = turned on from the nudge this visit (confirm).
|
|
1591
|
-
let _roomMemory = null;
|
|
1592
|
-
const ICONS = { queued:'\\u23F3', running:'\\u25CF', paused:'\\u23F8', blocked:'\\uD83D\\uDD12', done:'\\u2713', failed:'\\u2715', crashed:'!', killed:'\\u2298' };
|
|
1593
|
-
const STAT_ORDER = ['running','queued','paused','blocked','done','failed','crashed','killed'];
|
|
1594
|
-
const FILTERS = { all: s=>true, running: s=>['running','queued','paused','blocked'].includes(s.status), done: s=>['done','completed'].includes(s.status), failed: s=>['failed','crashed','killed','error'].includes(s.status) };
|
|
1595
|
-
|
|
1596
|
-
function ms(t) { return t?'\\u2713':'\\u2715'; }
|
|
1597
|
-
function dur(ms) { if(!ms&&ms!==0)return''; const s=Math.floor(ms/1000); if(s<60)return s+'s'; return Math.floor(s/60)+'m '+(s%60)+'s'; }
|
|
1598
|
-
// Strip internal context-injection prefixes from a prompt before showing it to
|
|
1599
|
-
// the user. Real spawns prepend "[graphify context: ...]\\n\\n" (and failover
|
|
1600
|
-
// adds "[Previous session context — ...]") — functional for the agent, noise
|
|
1601
|
-
// for the human. Found via real-execution dogfood #45: the prefix was becoming
|
|
1602
|
-
// the thread title. Display-only; the actual spawn prompt keeps the prefix.
|
|
1603
|
-
function displayPrompt(p) {
|
|
1604
|
-
return String(p == null ? '' : p)
|
|
1605
|
-
.replace(/^\\[graphify context:[\\s\\S]*?\\]\\s*\\n+/, '')
|
|
1606
|
-
.replace(/^\\[Previous session context[\\s\\S]*?\\]\\s*\\n+/, '')
|
|
1607
|
-
.replace(/^\\s+/, '');
|
|
1608
|
-
}
|
|
1609
|
-
function title(p) {
|
|
1610
|
-
// Build a headline from whole CLAUSES (split on · — and sentence-enders),
|
|
1611
|
-
// accumulating up to ~72 chars — so we never chop mid-phrase like the old
|
|
1612
|
-
// "...200 rps · Lua INCR in". Trailing punctuation stripped per clause.
|
|
1613
|
-
var s=(displayPrompt(p)||'Untitled').split('\\n')[0].trim();
|
|
1614
|
-
var parts=s.split(/\\s*[\\u00B7\\u2014]\\s*|(?<=[.!?])\\s+/)
|
|
1615
|
-
.map(function(x){return x.replace(/[.!?,;:\\s]+$/,'').trim();}).filter(Boolean);
|
|
1616
|
-
var out='';
|
|
1617
|
-
for(var i=0;i<parts.length;i++){
|
|
1618
|
-
var nx=out?out+' \\u00B7 '+parts[i]:parts[i];
|
|
1619
|
-
if(out && nx.length>72) break;
|
|
1620
|
-
out=nx; if(out.length>=72) break;
|
|
1621
|
-
}
|
|
1622
|
-
out=(out||s).trim();
|
|
1623
|
-
if(out.length>80) out=out.slice(0,80).replace(/\\s+\\S*$/,'')+'\\u2026';
|
|
1624
|
-
return out||'Untitled';
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
// Per-agent identity badge — primitive #1 from AGENTIC_UX_FRAMEWORK.
|
|
1628
|
-
// Returns a squircle with the agent's initials in its identity color.
|
|
1629
|
-
// Recognized: claude-code, codex, aider, gemini, opencode, conductor, auto.
|
|
1630
|
-
function agentKey(id) {
|
|
1631
|
-
const s = String(id||'').toLowerCase();
|
|
1632
|
-
if (s === 'claude-code' || s.startsWith('claude')) return 'claude';
|
|
1633
|
-
if (s === 'codex' || s.startsWith('codex')) return 'codex';
|
|
1634
|
-
if (s === 'aider' || s.startsWith('aider')) return 'aider';
|
|
1635
|
-
if (s === 'gemini' || s.startsWith('gemini')) return 'gemini';
|
|
1636
|
-
if (s === 'opencode' || s.startsWith('opencode')) return 'opencode';
|
|
1637
|
-
if (s === 'conductor') return 'conductor';
|
|
1638
|
-
if (s === 'auto') return 'auto';
|
|
1639
|
-
return 'auto';
|
|
1640
|
-
}
|
|
1641
|
-
function agentInitials(id) {
|
|
1642
|
-
const k = agentKey(id);
|
|
1643
|
-
if (k === 'claude') return 'CC';
|
|
1644
|
-
if (k === 'codex') return 'CD';
|
|
1645
|
-
if (k === 'aider') return 'AI';
|
|
1646
|
-
if (k === 'gemini') return 'GM';
|
|
1647
|
-
if (k === 'opencode') return 'OC';
|
|
1648
|
-
if (k === 'conductor') return 'CH';
|
|
1649
|
-
return '?';
|
|
1650
|
-
}
|
|
1651
|
-
function ab(id, size) {
|
|
1652
|
-
const k = agentKey(id);
|
|
1653
|
-
const sz = size === 'sm' ? ' sm' : (size === 'lg' ? ' lg' : '');
|
|
1654
|
-
return '<span class="ab ' + k + sz + '">' + agentInitials(id) + '</span>';
|
|
1655
|
-
}
|
|
1656
|
-
function agentLabel(id) {
|
|
1657
|
-
const k = agentKey(id);
|
|
1658
|
-
if (k === 'claude') return 'Claude Code';
|
|
1659
|
-
if (k === 'codex') return 'Codex';
|
|
1660
|
-
if (k === 'aider') return 'Aider';
|
|
1661
|
-
if (k === 'gemini') return 'Gemini';
|
|
1662
|
-
if (k === 'opencode') return 'OpenCode';
|
|
1663
|
-
if (k === 'conductor') return 'Conductor';
|
|
1664
|
-
if (k === 'auto') return 'Auto';
|
|
1665
|
-
return id || '?';
|
|
1666
|
-
}
|
|
1667
|
-
// Cost signal pill — primitive #2.
|
|
1668
|
-
function costPill(cents, paid) {
|
|
1669
|
-
if (cents === 0 || cents === null || cents === undefined) {
|
|
1670
|
-
return '<span class="cost-signal free">$0.00</span>';
|
|
1671
|
-
}
|
|
1672
|
-
const dollars = '$' + (cents/100).toFixed(cents < 10 ? 4 : 2);
|
|
1673
|
-
return '<span class="cost-signal ' + (paid === false ? 'free' : 'paid') + '">' + dollars + '</span>';
|
|
1674
|
-
}
|
|
1675
|
-
// Inline code-card renderer · single-pass tokenizer · processes one token at
|
|
1676
|
-
// a time so substitution markup never gets re-matched on the next replace.
|
|
1677
|
-
// Lightweight intentionally · "looks like code", not a real lexer.
|
|
1678
|
-
function codeCard(path, snippet, lang) {
|
|
1679
|
-
const ext = (lang || path.split('.').pop() || '').toLowerCase();
|
|
1680
|
-
const langLabel = ext === 'ts' || ext === 'tsx' ? 'typescript'
|
|
1681
|
-
: ext === 'js' || ext === 'jsx' ? 'javascript'
|
|
1682
|
-
: ext === 'py' ? 'python'
|
|
1683
|
-
: ext === 'rs' ? 'rust'
|
|
1684
|
-
: ext === 'go' ? 'go'
|
|
1685
|
-
: ext === 'lua' ? 'lua'
|
|
1686
|
-
: (ext || 'text');
|
|
1687
|
-
function esc(s){ return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
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=>({'&':'&','<':'<','>':'>','"':'"'}[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">🔒 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">🧠 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=>({'&':'&','<':'<','>':'>','"':'"'}[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=>({'&':'&','<':'<','>':'>','"':'"'}[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">👁</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=>({'&':'&','<':'<','>':'>','"':'"'}[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,'"').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,'"')+'">'+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=>({'&':'&','<':'<','>':'>','"':'"'}[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=>({'&':'&','<':'<','>':'>','"':'"'}[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=>({'&':'&','<':'<','>':'>','"':'"'}[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=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
2352
|
-
function tintJson(raw){
|
|
2353
|
-
return he(raw)
|
|
2354
|
-
.replace(/"([^&]*?)"(\\s*:)/g, '<span class="jk">"$1"</span>$2')
|
|
2355
|
-
.replace(/:\\s*"([^&]*?)"/g, ': <span class="js">"$1"</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=>({'&':'&','<':'<','>':'>','"':'"'}[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,'<');
|
|
2510
|
-
var wtail=(parts.slice(2).join(' · ')||(z.state==='active'?'awaiting approval':'')).replace(/</g,'<');
|
|
2511
|
-
var pHtml=(z.output?String(z.output):'').replace(/</g,'<')
|
|
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,'<'));
|
|
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,'<'))+'</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,'<')+'</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,'<')+'</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,'<')+'</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=>({'&':'&','<':'<','>':'>','"':'"'}[c]))
|
|
2683
|
-
: lastErr
|
|
2684
|
-
? '<b>What failed:</b> '+String(lastErr).slice(0,200).replace(/</g,'<')
|
|
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 & 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,'<');
|
|
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">· '+Math.round(testsObj.durationMs/100)/10+'s</span>' : '';
|
|
2744
|
-
chipsHtml += '<div class="test-chip'+failCls+'">'
|
|
2745
|
-
+ '<span class="passed">✓ '+passed+' passed</span>'
|
|
2746
|
-
+ (failed>0 ? '<span class="failed">× '+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,'<'));
|
|
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,'<'))+'</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,'<')
|
|
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,'<')+'</span></div>'
|
|
2842
|
-
+ '<h4>'+title.replace(/</g,'<')+'</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,'<')) : 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,'<'):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,'<')+'</span>'
|
|
2939
|
-
+ '<div class="finding-detail"><span class="label">claim:</span>'+String(f.claim||'').replace(/</g,'<')+'</div>'
|
|
2940
|
-
+ '<div class="finding-detail"><span class="label">reality:</span>'+String(f.reality||'').replace(/</g,'<')+'</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,'<')+'</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,'<')
|
|
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,'<');
|
|
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">✓ reachable · '+(data.who||'authenticated')+'</div>';
|
|
3254
|
-
} else {
|
|
3255
|
-
slot.innerHTML='<div class="cs-test-result fail">× 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">× '+(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,'<')+'</span>';
|
|
3351
|
-
btn.textContent='Verify chain'; return;
|
|
3352
|
-
}
|
|
3353
|
-
const counts='<div class="audit-chain-counts">'+r.totalEntries+' entries · '+r.chainedEntries+' hash-chained · '+r.legacyEntries+' legacy</div>';
|
|
3354
|
-
if(r.valid){
|
|
3355
|
-
res.innerHTML='<span class="ok">✓ Chain intact · 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+' · '+e.reason+' · '+String(e.detail||'').replace(/</g,'<')+'</div>';
|
|
3361
|
-
}).join('');
|
|
3362
|
-
res.innerHTML='<span class="fail">× Chain BROKEN · '+(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 => ({'&':'&','<':'<','>':'>','"':'"'}[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=>({'&':'&','<':'<','>':'>','"':'"'}[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 => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
4066
|
-
function render(offers){
|
|
4067
|
-
if(!offers || offers.length === 0 || fbDismissed){ fbEl.classList.remove('show'); return; }
|
|
4068
|
-
fbEl.classList.add('show');
|
|
4069
|
-
fbEl.innerHTML = offers.map(o => {
|
|
4070
|
-
const msg = '<b>'+escFb(o.signal && o.signal.fromAgent)+'</b> hit '+escFb(o.signal && o.signal.errorCode)+
|
|
4071
|
-
' — switch to <b>'+escFb(o.target)+'</b>?';
|
|
4072
|
-
const actions = '<div class="fb-actions">'+
|
|
4073
|
-
'<button class="fb-accept" data-session="'+escFb(o.sessionId)+'" data-target="'+escFb(o.target)+'">Switch</button>'+
|
|
4074
|
-
'<button class="fb-dismiss" data-session="'+escFb(o.sessionId)+'">Dismiss</button>'+
|
|
4075
|
-
'</div>';
|
|
4076
|
-
return '<div class="fb-msg">'+msg+'</div>'+actions;
|
|
4077
|
-
}).join('');
|
|
4078
|
-
fbEl.querySelectorAll('.fb-accept').forEach(btn => {
|
|
4079
|
-
btn.addEventListener('click', async ()=>{
|
|
4080
|
-
btn.disabled = true; btn.textContent = 'Switching…';
|
|
4081
|
-
try{
|
|
4082
|
-
const r = await fetch('/api/failover-accept', {
|
|
4083
|
-
method: 'POST',
|
|
4084
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4085
|
-
body: JSON.stringify({ sessionId: btn.dataset.session, targetAgent: btn.dataset.target }),
|
|
4086
|
-
});
|
|
4087
|
-
const d = await r.json();
|
|
4088
|
-
if(d.error || !d.ok || !d.newSessionId){
|
|
4089
|
-
fbEl.innerHTML = '<div class="fb-msg"><b>Failover failed:</b> '+escFb(d.error || d.detail || 'Target agent not ready')+'</div>';
|
|
4090
|
-
return;
|
|
4091
|
-
}
|
|
4092
|
-
fbEl.innerHTML = '<div class="fb-msg">↻ Switched to <b>'+escFb(btn.dataset.target)+'</b> · new session <b>'+escFb(d.newSessionId.slice(0,8))+'</b></div>'+
|
|
4093
|
-
'<div class="fb-actions"><button class="fb-dismiss" id="fb-ok">OK</button></div>';
|
|
4094
|
-
document.getElementById('fb-ok').addEventListener('click', ()=>{ fbDismissed = true; fbEl.classList.remove('show'); });
|
|
4095
|
-
}catch(err){
|
|
4096
|
-
fbEl.innerHTML = '<div class="fb-msg"><b>Failover failed:</b> network error · check daemon</div>';
|
|
4097
|
-
}
|
|
4098
|
-
});
|
|
4099
|
-
});
|
|
4100
|
-
fbEl.querySelectorAll('.fb-dismiss').forEach(btn => {
|
|
4101
|
-
btn.addEventListener('click', ()=>{ fbDismissed = true; fbEl.classList.remove('show'); });
|
|
4102
|
-
});
|
|
4103
|
-
}
|
|
4104
|
-
setInterval(async ()=>{
|
|
4105
|
-
try{
|
|
4106
|
-
const r = await fetch('/api/failover-offers');
|
|
4107
|
-
const d = await r.json();
|
|
4108
|
-
render(d.offers);
|
|
4109
|
-
}catch{ /* fine */ }
|
|
4110
|
-
}, 5000);
|
|
4111
|
-
})();
|
|
4112
|
-
|
|
4113
|
-
document.addEventListener('keydown',e=>{
|
|
4114
|
-
if(e.key==='Escape'&&_detail)hideD();
|
|
4115
|
-
if((e.metaKey||e.ctrlKey)&&e.key==='k'){e.preventDefault();const ta=document.getElementById('main-ta')||document.getElementById('detail-ta');if(ta)ta.focus();}
|
|
4116
|
-
});
|
|
4117
|
-
|
|
4118
|
-
// Composer .typing toggle · brand border the moment the input has chars.
|
|
4119
|
-
// The focus-within border handles "input is focused"; the .typing class
|
|
4120
|
-
// handles "input has content" so the affordance stays even if the user
|
|
4121
|
-
// tabs away mid-prompt.
|
|
4122
|
-
function bindTypingState(taEl){
|
|
4123
|
-
if(!taEl) return;
|
|
4124
|
-
const bar = taEl.closest('.input-bar');
|
|
4125
|
-
if(!bar) return;
|
|
4126
|
-
taEl.addEventListener('input', ()=>{
|
|
4127
|
-
if(taEl.value.length > 0) bar.classList.add('typing');
|
|
4128
|
-
else bar.classList.remove('typing');
|
|
4129
|
-
});
|
|
4130
|
-
}
|
|
4131
|
-
|
|
4132
|
-
const ta=document.getElementById('main-ta'), btn=document.getElementById('main-btn');
|
|
4133
|
-
if(btn)btn.addEventListener('click',()=>submit(ta,btn));
|
|
4134
|
-
if(ta)ta.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey&&!ta.disabled&&btn&&!btn.disabled){e.preventDefault();submit(ta,btn);}});
|
|
4135
|
-
bindTypingState(ta);
|
|
4136
|
-
|
|
4137
|
-
const dta=document.getElementById('detail-ta'), dbtn=document.getElementById('detail-btn');
|
|
4138
|
-
if(dbtn)dbtn.addEventListener('click',()=>submit(dta,dbtn,_detail));
|
|
4139
|
-
if(dta)dta.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey&&!dta.disabled&&dbtn&&!dbtn.disabled){e.preventDefault();submit(dta,dbtn,_detail);}});
|
|
4140
|
-
bindTypingState(dta);
|
|
4141
|
-
});
|
|
4142
|
-
`;
|
|
4143
|
-
export const TASK_HTML_V2 = `<!doctype html>
|
|
4144
|
-
<html lang="en">
|
|
4145
|
-
<head>
|
|
4146
|
-
<meta charset="utf-8" />
|
|
4147
|
-
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
4148
|
-
<title>codehere</title>
|
|
4149
|
-
<style>${SHARED_STYLES}</style>
|
|
4150
|
-
</head>
|
|
4151
|
-
<body>
|
|
4152
|
-
<div class="app">
|
|
4153
|
-
<div class="header">
|
|
4154
|
-
<span class="logo"><span class="dot"></span><span class="name">codehere</span></span>
|
|
4155
|
-
<span class="beta-badge" title="codehere is in beta · heading to invite-only early access">beta</span>
|
|
4156
|
-
<span class="local-pill" title="zero telemetry · all data on this machine">
|
|
4157
|
-
<span class="dot"></span>local · <span id="local-host">127.0.0.1:3773</span>
|
|
4158
|
-
</span>
|
|
4159
|
-
<span class="budget safe" id="budget"></span>
|
|
4160
|
-
<select class="persona-select" id="persona-select" title="Show only what matters to your role" aria-label="View as role">
|
|
4161
|
-
<option value="all">Everyone</option>
|
|
4162
|
-
<option value="pm">PM view</option>
|
|
4163
|
-
<option value="designer">Designer view</option>
|
|
4164
|
-
<option value="eng">Engineer view</option>
|
|
4165
|
-
<option value="auditor">Auditor view</option>
|
|
4166
|
-
</select>
|
|
4167
|
-
<button class="density-toggle" id="density-toggle" title="Toggle compact / standard density">⊞ Compact</button>
|
|
4168
|
-
<button class="reshape-btn" id="reshape-open" title="Describe the view you want · agent composes from primitives">⌘ Reshape view</button>
|
|
4169
|
-
<button class="share-btn-hdr" id="share-open" title="Share this thread · still local-first · link contains a signed payload, not your data" aria-label="Share thread">↗ Share</button>
|
|
4170
|
-
<button class="settings-btn" id="settings-open" title="Settings · integrations" aria-label="Open settings">⚙</button>
|
|
4171
|
-
</div>
|
|
4172
|
-
|
|
4173
|
-
<!-- v0.8 S3 · Custom-view banner · appears when reshape is active -->
|
|
4174
|
-
<div class="custom-view-banner" id="custom-view-banner">
|
|
4175
|
-
<span class="lbl">custom view</span>
|
|
4176
|
-
<span class="prompt" id="custom-view-prompt"></span>
|
|
4177
|
-
<span class="reset" onclick="resetShape()">reset</span>
|
|
4178
|
-
</div>
|
|
4179
|
-
|
|
4180
|
-
<!-- Failover banner · ported from v1 on /v1 kill 2026-05-23 -->
|
|
4181
|
-
<div class="failover-banner" id="failover-banner"></div>
|
|
4182
|
-
|
|
4183
|
-
<!-- C-01 · first-run indexing banner · auto-hides once you have ≥ 3 sessions -->
|
|
4184
|
-
<div class="indexing-banner" id="indexing-banner">
|
|
4185
|
-
<span class="what">first run</span>
|
|
4186
|
-
<span>codehere indexed your repo locally · zero telemetry · all state at <code>~/.codehere/</code></span>
|
|
4187
|
-
</div>
|
|
4188
|
-
|
|
4189
|
-
<!-- F-01 · budget threshold warning · only renders when daily spend >= 80% of cap -->
|
|
4190
|
-
<div class="budget-banner" id="budget-banner">
|
|
4191
|
-
<span class="what" id="budget-banner-what"></span>
|
|
4192
|
-
<span id="budget-banner-msg"></span>
|
|
4193
|
-
<span class="hint">free-tier providers (Aider, Gemini free) unaffected</span>
|
|
4194
|
-
</div>
|
|
4195
|
-
|
|
4196
|
-
<div class="squad-row">
|
|
4197
|
-
<span style="color:var(--fg-30)">Squad:</span>
|
|
4198
|
-
<span id="squad" style="display:contents">
|
|
4199
|
-
<span class="squad-agent"><span class="squad-dot offline"></span>loading...</span>
|
|
4200
|
-
</span>
|
|
4201
|
-
<!-- F-02 · cross-worktree alert · only visible when any out-of-cwd session has run > 5m -->
|
|
4202
|
-
<span class="worktree-alert" id="worktree-alert" onclick="if(window._wkTarget)showD(window._wkTarget)">
|
|
4203
|
-
<span class="pulse-dot"></span><span id="worktree-alert-text">other worktree</span>
|
|
4204
|
-
</span>
|
|
4205
|
-
</div>
|
|
4206
|
-
|
|
4207
|
-
<div class="bar">
|
|
4208
|
-
<button class="new-btn" onclick="document.getElementById('main-ta')?.focus()">+ New Task</button>
|
|
4209
|
-
<span class="filter-tab active" data-filter="all" onclick="setFilter('all')">All <span class="cnt" id="c-all">0</span></span>
|
|
4210
|
-
<span class="filter-tab" data-filter="running" onclick="setFilter('running')">• Running <span class="cnt" id="c-running">0</span></span>
|
|
4211
|
-
<span class="filter-tab" data-filter="done" onclick="setFilter('done')">✓ Done <span class="cnt" id="c-done">0</span></span>
|
|
4212
|
-
<span class="filter-tab" data-filter="failed" onclick="setFilter('failed')">× Failed <span class="cnt" id="c-failed">0</span></span>
|
|
4213
|
-
</div>
|
|
4214
|
-
|
|
4215
|
-
<div class="content">
|
|
4216
|
-
<div id="task-list-view" class="content-inner">
|
|
4217
|
-
<div id="task-list">
|
|
4218
|
-
<div class="skeleton" id="skeleton">
|
|
4219
|
-
<div class="skeleton-card"></div>
|
|
4220
|
-
<div class="skeleton-card"></div>
|
|
4221
|
-
<div class="skeleton-card"></div>
|
|
4222
|
-
</div>
|
|
4223
|
-
</div>
|
|
4224
|
-
<!-- Observed host sessions live BELOW the goals: the spec's home is
|
|
4225
|
-
"a list of your goals" — ambient observation must not bury the
|
|
4226
|
-
thesis surface (found burying it during the 2026-06-06 walk). -->
|
|
4227
|
-
<div id="host-sessions-strip" style="display:none"></div>
|
|
4228
|
-
</div>
|
|
4229
|
-
<div id="task-detail-view" style="display:none" class="content-inner">
|
|
4230
|
-
<div id="detail-view"></div>
|
|
4231
|
-
<div class="input-area" style="border-top:none;padding:12px 0">
|
|
4232
|
-
<div class="input-bar" style="max-width:100%">
|
|
4233
|
-
<textarea class="input-ta composer-bar" id="detail-ta" placeholder="Request changes or ask a question..." rows="1" disabled></textarea>
|
|
4234
|
-
<div class="input-meta composer-meta"><span class="pick-agent" title="Auto-pick · best agent for this task">↳ Auto</span><span class="cost-est" title="Estimated cost · based on prompt length + recent agent pricing">~$0.02</span></div>
|
|
4235
|
-
<button class="send-btn composer-send" id="detail-btn" disabled>↑</button>
|
|
4236
|
-
</div>
|
|
4237
|
-
</div>
|
|
4238
|
-
</div>
|
|
4239
|
-
</div>
|
|
4240
|
-
|
|
4241
|
-
<div class="input-area">
|
|
4242
|
-
<div class="input-bar">
|
|
4243
|
-
<textarea class="input-ta composer-bar" id="main-ta" placeholder="Describe a task or ask a question..." rows="1"></textarea>
|
|
4244
|
-
<div class="input-meta composer-meta"><select class="pick-agent" id="agent-pick" title="Who does this task — Auto picks the best fit" aria-label="Pick the agent for this task"><option value="auto">↳ Auto</option><option value="claude-code">claude-code</option><option value="codex">codex</option><option value="aider">aider</option><option value="gemini">gemini</option><option value="opencode">opencode</option></select><span class="cost-est" title="Estimated cost · based on prompt length + recent agent pricing">~$0.02</span></div>
|
|
4245
|
-
<button class="send-btn composer-send" id="main-btn">↑</button>
|
|
4246
|
-
</div>
|
|
4247
|
-
<div class="kbd-hint">
|
|
4248
|
-
<span class="item"><span class="kbd">↵</span> send</span>
|
|
4249
|
-
<span class="item"><span class="kbd">⇧↵</span> newline</span>
|
|
4250
|
-
<span class="item"><span class="kbd">⌘K</span> focus composer</span>
|
|
4251
|
-
<span class="item"><span class="kbd">Esc</span> close modal</span>
|
|
4252
|
-
</div>
|
|
4253
|
-
</div>
|
|
4254
|
-
|
|
4255
|
-
<!-- v0.8 S3 · Reshape modal · ported from demo v12 lines 1142-1176 -->
|
|
4256
|
-
<div class="reshape-modal" id="reshape-modal">
|
|
4257
|
-
<div class="reshape-card">
|
|
4258
|
-
<button class="close-x" id="reshape-close" aria-label="Close">×</button>
|
|
4259
|
-
<div class="k">reshape this view</div>
|
|
4260
|
-
<h2>Describe the view you want.</h2>
|
|
4261
|
-
<p class="sub">Your agent composes the layout from codehere's shared primitives. Software ships building blocks · the final interface is yours to author.</p>
|
|
4262
|
-
<div class="reshape-input">
|
|
4263
|
-
<input id="reshape-input" placeholder="e.g. just show me what shipped + cost · no code"/>
|
|
4264
|
-
<button id="reshape-submit">Compose</button>
|
|
4265
|
-
</div>
|
|
4266
|
-
<div class="reshape-suggestions persona prims">
|
|
4267
|
-
<div class="lbl">or pick a quick start</div>
|
|
4268
|
-
<button class="pill" data-shape="ship-only" data-prompt="just show me what shipped and the cost · hide everything else">just show me what shipped + cost <span class="what">— Final Report + file chips only</span></button>
|
|
4269
|
-
<button class="pill" data-shape="velocity" data-prompt="velocity view · outcomes + duration + agent allocation · hide audit and code">velocity view <span class="what">— outcomes + time + cost · for daily check-in</span></button>
|
|
4270
|
-
<button class="pill" data-shape="security" data-prompt="security view · highlight hook blocks, overrides, trust grades · hide narrative">security view <span class="what">— blocks / overrides / trust amplified</span></button>
|
|
4271
|
-
</div>
|
|
4272
|
-
<div class="agent-compose recomposing" id="agent-compose">
|
|
4273
|
-
<div class="agent-line"><span style="font-family:var(--mono); color:var(--accent); font-weight:600">claude-code</span><span style="color:var(--fg-40)">· composing your view from shared primitives</span></div>
|
|
4274
|
-
<div style="margin-top: 8px;">
|
|
4275
|
-
<div class="line">read <span class="info">user prompt</span> · "<span id="compose-prompt-echo"></span>"</div>
|
|
4276
|
-
<div class="line">analyze · <span class="kw">5 primitives in scope</span>: <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">turn</code> · <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">panel</code> · <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">artifact</code> · <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">audit-line</code> · <code style="background:var(--bg-2);padding:0 4px;border-radius:2px">tool-card</code></div>
|
|
4277
|
-
<div class="line">compose <span class="kw">view-config</span> · <span id="compose-config" style="color:var(--fg-90)"></span></div>
|
|
4278
|
-
<div class="line">apply · <span class="ok">view ready · 0.4s</span></div>
|
|
4279
|
-
<div class="line"><span class="ok">✓</span> custom view active · <span style="color:var(--fg-50)">re-shape any time by clicking the button</span></div>
|
|
4280
|
-
</div>
|
|
4281
|
-
</div>
|
|
4282
|
-
<div class="primitives-note">
|
|
4283
|
-
<b>codehere ships primitives, not screens.</b> Your agent composes the layout from these building blocks · you author the final view in plain English.
|
|
4284
|
-
<div class="prims">
|
|
4285
|
-
<span class="p">turn</span>
|
|
4286
|
-
<span class="p">panel</span>
|
|
4287
|
-
<span class="p">artifact</span>
|
|
4288
|
-
<span class="p">audit-line</span>
|
|
4289
|
-
<span class="p">tool-card</span>
|
|
4290
|
-
<span class="p">verified-block</span>
|
|
4291
|
-
<span class="p">file-chip</span>
|
|
4292
|
-
<span class="p">write-pass</span>
|
|
4293
|
-
</div>
|
|
4294
|
-
</div>
|
|
4295
|
-
</div>
|
|
4296
|
-
</div>
|
|
4297
|
-
|
|
4298
|
-
<!-- v0.8 S4 · share-thread modal · ported from demo v12 lines 1126-1139 -->
|
|
4299
|
-
<div class="share-modal" id="share-modal" role="dialog" aria-modal="true" aria-labelledby="share-title">
|
|
4300
|
-
<div class="share-card">
|
|
4301
|
-
<div class="head" id="share-title">↗ Share this thread <span class="tag">· still local-first</span></div>
|
|
4302
|
-
<p class="blurb">Codehere generates a signed payload (steps · artifacts · Final Report · audit chain) you can paste into Slack, an email, a PR description. Your code never leaves your machine — only the synthesized record.</p>
|
|
4303
|
-
<div class="share-link-box" id="share-link-box">codehere://thread/<span id="share-link-id">…</span>?signed=ed25519:<span id="share-link-fp">…</span></div>
|
|
4304
|
-
<div class="share-actions">
|
|
4305
|
-
<button class="share-btn-primary" id="share-copy-link">Copy link</button>
|
|
4306
|
-
<button class="share-btn-secondary" id="share-download">Download bundle (.json)</button>
|
|
4307
|
-
<button class="share-btn-secondary" id="share-close">Close</button>
|
|
4308
|
-
</div>
|
|
4309
|
-
<p class="share-foot">Link contains a signed payload · not source code · recipient verifies chain integrity locally before trusting.</p>
|
|
4310
|
-
</div>
|
|
4311
|
-
</div>
|
|
4312
|
-
<div class="share-toast" id="share-toast">Link copied</div>
|
|
4313
|
-
|
|
4314
|
-
<!-- v0.9 S7 · overlay-renderer · inspect-and-act for ctx-source URLs -->
|
|
4315
|
-
<div class="or-overlay overlay-render" id="or-overlay" role="dialog" aria-modal="true">
|
|
4316
|
-
<div class="or-card or-content">
|
|
4317
|
-
<div class="or-head">
|
|
4318
|
-
<span class="src" id="or-src">linear</span>
|
|
4319
|
-
<span class="meta" id="or-meta">…</span>
|
|
4320
|
-
<span class="audit"><b>● audit</b> · captured locally</span>
|
|
4321
|
-
<button class="or-close" id="or-close" aria-label="Close">×</button>
|
|
4322
|
-
</div>
|
|
4323
|
-
<div class="or-body" id="or-body">
|
|
4324
|
-
<div class="preview-loading">fetching…</div>
|
|
4325
|
-
</div>
|
|
4326
|
-
<div class="or-composer or-agent-reply">
|
|
4327
|
-
<span class="hint" id="or-hint">Ask the agent about this · all interaction captured in audit chain</span>
|
|
4328
|
-
<div class="row">
|
|
4329
|
-
<input id="or-input" placeholder="e.g. summarize the blocker · or what's the priority?"/>
|
|
4330
|
-
<button id="or-ask" disabled title="Composer ships in S7 Part D">Ask agent</button>
|
|
4331
|
-
</div>
|
|
4332
|
-
</div>
|
|
4333
|
-
</div>
|
|
4334
|
-
</div>
|
|
4335
|
-
|
|
4336
|
-
<!-- v0.9 S6 · W-PASS approval modal · demo v12 lines 2030-2050 -->
|
|
4337
|
-
<button class="wp-pending-badge" id="wp-pending-badge" title="An agent wants to make a write · click to review">↗ write awaits</button>
|
|
4338
|
-
<div class="wp-modal" id="wp-modal" role="dialog" aria-modal="true" aria-labelledby="wp-title">
|
|
4339
|
-
<div class="wp-card write-pass">
|
|
4340
|
-
<div class="wp-banner"><span class="dot"></span><span>write-pass · agent action · awaits your approval</span></div>
|
|
4341
|
-
<h3 class="wp-title" id="wp-title">…</h3>
|
|
4342
|
-
<p class="wp-reason" id="wp-reason">…</p>
|
|
4343
|
-
<div class="wp-meta">
|
|
4344
|
-
tool · <b id="wp-target">…</b> · third-party API · <span style="color:var(--accent)">mediated by codehere</span>
|
|
4345
|
-
</div>
|
|
4346
|
-
<div class="wp-meta">
|
|
4347
|
-
agent · <b id="wp-emitter">…</b> · session <span id="wp-session">…</span>
|
|
4348
|
-
</div>
|
|
4349
|
-
<div class="wp-meta" id="wp-trust-row" style="display:none">
|
|
4350
|
-
domain · <b id="wp-trust-host">…</b> · trust grade
|
|
4351
|
-
<span class="wp-trust-grade" id="wp-trust-grade">?</span>
|
|
4352
|
-
<span id="wp-trust-reason" style="color:var(--fg-50)"></span>
|
|
4353
|
-
</div>
|
|
4354
|
-
<div class="wp-payload-wrap" id="wp-payload-wrap">
|
|
4355
|
-
<div class="wp-payload-head">
|
|
4356
|
-
<span class="method" id="wp-method">PATCH</span>
|
|
4357
|
-
<span id="wp-targetline">…</span>
|
|
4358
|
-
</div>
|
|
4359
|
-
<div class="wp-payload" id="wp-payload">…</div>
|
|
4360
|
-
</div>
|
|
4361
|
-
<textarea class="wp-payload-edit" id="wp-payload-edit" style="display:none" spellcheck="false"></textarea>
|
|
4362
|
-
<div class="wp-actions" id="wp-actions">
|
|
4363
|
-
<button class="wp-btn-approve" id="wp-approve">Approve + send</button>
|
|
4364
|
-
<button class="wp-btn-edit" id="wp-edit">Edit payload</button>
|
|
4365
|
-
<button class="wp-btn-cancel" id="wp-cancel">Cancel</button>
|
|
4366
|
-
</div>
|
|
4367
|
-
<div class="wp-actions" id="wp-actions-editing" style="display:none">
|
|
4368
|
-
<button class="wp-btn-save" id="wp-save">Save + send</button>
|
|
4369
|
-
<button class="wp-btn-cancel" id="wp-edit-cancel">Discard edit</button>
|
|
4370
|
-
</div>
|
|
4371
|
-
<p class="wp-foot">codehere mediates every agent write to a third-party tool · audit chain captures URL · payload · approval · response · signed timestamp · one verifiable thread instead of scattered tool logs</p>
|
|
4372
|
-
<div class="wp-result" id="wp-result" style="display:none"></div>
|
|
4373
|
-
</div>
|
|
4374
|
-
</div>
|
|
4375
|
-
|
|
4376
|
-
<!-- Settings drawer (A-09) · holds Context Sources + future toggles -->
|
|
4377
|
-
<div class="drawer-overlay" id="drawer-overlay"></div>
|
|
4378
|
-
<aside class="drawer" id="settings-drawer" aria-hidden="true">
|
|
4379
|
-
<button class="drawer-close" id="settings-close" aria-label="Close settings">×</button>
|
|
4380
|
-
<div class="drawer-title">Settings</div>
|
|
4381
|
-
|
|
4382
|
-
<h3>Integrations</h3>
|
|
4383
|
-
<p class="sub">Connect external tools so agents can read <code style="background:var(--bg-2);padding:1px 4px;border-radius:3px;font-family:var(--mono);font-size:10px">linear://</code> · <code style="background:var(--bg-2);padding:1px 4px;border-radius:3px;font-family:var(--mono);font-size:10px">figma://</code> · <code style="background:var(--bg-2);padding:1px 4px;border-radius:3px;font-family:var(--mono);font-size:10px">github://</code> · <code style="background:var(--bg-2);padding:1px 4px;border-radius:3px;font-family:var(--mono);font-size:10px">drawio://</code> URLs in prompts.</p>
|
|
4384
|
-
<div id="cs-list">
|
|
4385
|
-
<div style="color:var(--fg-40); font-size:11px">loading...</div>
|
|
4386
|
-
</div>
|
|
4387
|
-
</aside>
|
|
4388
|
-
|
|
4389
|
-
<div id="offline-banner" class="offline-banner" style="display:none">You're offline. Actions will resume when connected.</div>
|
|
4390
|
-
<div id="toast" style="display:none;position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:var(--bg-2);border:1px solid var(--border);border-radius:8px;padding:8px 16px;font-size:12px;color:var(--fg-70);z-index:100"></div>
|
|
4391
|
-
|
|
4392
|
-
<!-- RADICAL bottom-chrome collapse (2026-05-24) · audit-chain panel
|
|
4393
|
-
inlined into the OSS-footer status bar · ~115px → ~28px ·
|
|
4394
|
-
all load-bearing info preserved (dots · stats · Verify button) ·
|
|
4395
|
-
only the redundant subtitle + panel-card chrome removed. -->
|
|
4396
|
-
<div class="oss-footer">
|
|
4397
|
-
<span class="brand">codehere</span>
|
|
4398
|
-
<span class="sep">·</span>
|
|
4399
|
-
<span id="oss-version">v0.6.x</span>
|
|
4400
|
-
<span class="sep">·</span>
|
|
4401
|
-
<a href="https://github.com/muhammadegaa/codehere" target="_blank" rel="noopener">github</a>
|
|
4402
|
-
<span class="sep">·</span>
|
|
4403
|
-
<span>MIT</span>
|
|
4404
|
-
<span class="sep">·</span>
|
|
4405
|
-
<span class="audit-chain-panel">
|
|
4406
|
-
<span class="audit-chain-title">audit</span>
|
|
4407
|
-
<span id="audit-chain-stats" style="color:var(--fg-50);font-family:var(--mono);font-size:10.5px">— entries · —</span>
|
|
4408
|
-
<span id="audit-chain-dots" class="audit-chain-dots" style="display:inline-flex;gap:3px"></span>
|
|
4409
|
-
<button id="audit-verify-btn" class="audit-chain-btn" onclick="verifyAuditChain()" title="Walk the chain · verify SHA-256 integrity without trusting the daemon">Verify</button>
|
|
4410
|
-
<span id="audit-result" class="audit-chain-result" style="color:var(--fg-50)"></span>
|
|
4411
|
-
</span>
|
|
4412
|
-
<span class="right">built locally · zero telemetry</span>
|
|
4413
|
-
</div>
|
|
4414
|
-
</div>
|
|
4415
|
-
<script>${APP_SCRIPT}</script>
|
|
4416
|
-
</body>
|
|
4417
|
-
</html>`;
|
|
4418
|
-
//# sourceMappingURL=ui-page-v2.js.map
|