crewforth 0.0.1 → 3.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3784 -0
- package/LICENSE +21 -0
- package/README.md +37 -7
- package/VERSION +1 -0
- package/adopt.sh +1670 -0
- package/bin/cli.js +497 -0
- package/kit/AGENT_TEMPLATE.md +92 -0
- package/kit/CLAUDE.md +228 -0
- package/kit/README.md +107 -0
- package/kit/agents/crew-backend-expert.md +95 -0
- package/kit/agents/crew-commit-agent.md +96 -0
- package/kit/agents/crew-database-expert.md +84 -0
- package/kit/agents/crew-devops-expert.md +82 -0
- package/kit/agents/crew-frontend-expert.md +85 -0
- package/kit/agents/crew-performance-expert.md +78 -0
- package/kit/agents/crew-planner.md +60 -0
- package/kit/agents/crew-privacy-agent.md +71 -0
- package/kit/agents/crew-review-agent.md +95 -0
- package/kit/agents/crew-security-expert.md +73 -0
- package/kit/agents/crew-session-manager.md +77 -0
- package/kit/agents/crew-test-expert.md +64 -0
- package/kit/eval/doctor.sh +465 -0
- package/kit/eval/gate-report.sh +137 -0
- package/kit/eval/golden-holdout.txt +53 -0
- package/kit/eval/golden-routing.txt +199 -0
- package/kit/eval/lib/crew-env.sh +14 -0
- package/kit/eval/lib/settings-json.awk +154 -0
- package/kit/eval/lib/star.sh +70 -0
- package/kit/eval/parser-conformance.sh +612 -0
- package/kit/eval/preflight.sh +186 -0
- package/kit/eval/routing-eval.sh +209 -0
- package/kit/eval/scan-skill.sh +162 -0
- package/kit/eval/smoke-test.sh +6693 -0
- package/kit/eval/update-guard.sh +81 -0
- package/kit/eval/utilization.sh +130 -0
- package/kit/hooks/board-sync.sh +106 -0
- package/kit/hooks/board.sh +962 -0
- package/kit/hooks/commit-msg +49 -0
- package/kit/hooks/context-usage.sh +353 -0
- package/kit/hooks/floor-blocklist.txt +124 -0
- package/kit/hooks/guard-bash.sh +1431 -0
- package/kit/hooks/guard-commit-scan.sh +511 -0
- package/kit/hooks/guard-write.sh +513 -0
- package/kit/hooks/pre-commit +577 -0
- package/kit/hooks/route-hint.sh +185 -0
- package/kit/hooks/secret-blocklist.txt +82 -0
- package/kit/hooks/session-guard.sh +149 -0
- package/kit/hooks/session-rehydrate.sh +41 -0
- package/kit/hooks/session-stats.sh +252 -0
- package/kit/hooks/session-update-check.sh +236 -0
- package/kit/hooks/skill-trust.sh +150 -0
- package/kit/hooks/trace-blocklist.txt +53 -0
- package/kit/settings.json +132 -0
- package/kit/skills/a11y/SKILL.md +44 -0
- package/kit/skills/adr/SKILL.md +74 -0
- package/kit/skills/api-design/SKILL.md +50 -0
- package/kit/skills/automode-policy/SKILL.md +91 -0
- package/kit/skills/automode-policy/references/policy.json +13 -0
- package/kit/skills/automode-policy/scripts/apply.sh +84 -0
- package/kit/skills/automode-policy/scripts/check.sh +102 -0
- package/kit/skills/backend-architecture/SKILL.md +103 -0
- package/kit/skills/brainstorm/SKILL.md +47 -0
- package/kit/skills/ci-pipeline/SKILL.md +38 -0
- package/kit/skills/commit-message/SKILL.md +82 -0
- package/kit/skills/commit-message/references/examples.md +8 -0
- package/kit/skills/confidence-check/SKILL.md +86 -0
- package/kit/skills/crew-board/SKILL.md +55 -0
- package/kit/skills/crew-brainstorm/SKILL.md +19 -0
- package/kit/skills/crew-code-review/SKILL.md +151 -0
- package/kit/skills/crew-code-review/references/history-search.md +19 -0
- package/kit/skills/crew-code-review/references/panel-mode.md +32 -0
- package/kit/skills/crew-code-review/references/receiving.md +23 -0
- package/kit/skills/crew-doctor/SKILL.md +36 -0
- package/kit/skills/crew-gates/SKILL.md +25 -0
- package/kit/skills/crew-handoff/SKILL.md +14 -0
- package/kit/skills/crew-plan/SKILL.md +18 -0
- package/kit/skills/crew-review/SKILL.md +23 -0
- package/kit/skills/crew-ship/SKILL.md +19 -0
- package/kit/skills/crew-skill/SKILL.md +61 -0
- package/kit/skills/crew-studio/SKILL.md +129 -0
- package/kit/skills/crew-update/SKILL.md +43 -0
- package/kit/skills/db-migration/SKILL.md +120 -0
- package/kit/skills/db-migration/references/backup-restore.md +33 -0
- package/kit/skills/db-migration/references/raw-sql.md +30 -0
- package/kit/skills/db-migration/references/tool-matrix.md +35 -0
- package/kit/skills/dependency-audit/SKILL.md +61 -0
- package/kit/skills/dependency-upgrade/SKILL.md +82 -0
- package/kit/skills/deploy/SKILL.md +100 -0
- package/kit/skills/deploy/references/proxy-ssl.md +56 -0
- package/kit/skills/deploy/references/rollback.md +18 -0
- package/kit/skills/deploy/references/scripts.md +9 -0
- package/kit/skills/deploy/references/topology-a.md +128 -0
- package/kit/skills/deploy/references/topology-b.md +44 -0
- package/kit/skills/docs-writer/SKILL.md +53 -0
- package/kit/skills/docs-writer/references/coauthoring.md +34 -0
- package/kit/skills/eval-grader/SKILL.md +75 -0
- package/kit/skills/eval-grader/references/method.md +51 -0
- package/kit/skills/frontend/SKILL.md +56 -0
- package/kit/skills/frontend/references/verify-contract.md +41 -0
- package/kit/skills/frontend-design/SKILL.md +71 -0
- package/kit/skills/frontend-design/references/design-review.md +34 -0
- package/kit/skills/frontend-rn-expo/SKILL.md +25 -0
- package/kit/skills/handoff/SKILL.md +71 -0
- package/kit/skills/handoff/references/cold-reader.md +51 -0
- package/kit/skills/i18n-integrity/SKILL.md +31 -0
- package/kit/skills/incident-runbook/SKILL.md +40 -0
- package/kit/skills/incident-runbook/references/postmortem.md +16 -0
- package/kit/skills/iterate/SKILL.md +40 -0
- package/kit/skills/mcp-builder/SKILL.md +54 -0
- package/kit/skills/mcp-builder/references/building.md +71 -0
- package/kit/skills/observability/SKILL.md +50 -0
- package/kit/skills/performance/SKILL.md +38 -0
- package/kit/skills/performance/references/bottlenecks.md +10 -0
- package/kit/skills/privacy-compliance/SKILL.md +75 -0
- package/kit/skills/red-team/SKILL.md +48 -0
- package/kit/skills/reflect/SKILL.md +57 -0
- package/kit/skills/release/SKILL.md +44 -0
- package/kit/skills/security-scan/SKILL.md +99 -0
- package/kit/skills/security-scan/references/ai-agents.md +107 -0
- package/kit/skills/security-scan/references/fronts.md +99 -0
- package/kit/skills/security-scan/references/prompting.md +23 -0
- package/kit/skills/security-scan/references/reporting.md +75 -0
- package/kit/skills/security-scan/references/verify.md +131 -0
- package/kit/skills/sonarqube-check/SKILL.md +108 -0
- package/kit/skills/sonarqube-check/references/local-engines.md +32 -0
- package/kit/skills/sonarqube-check/references/stand-up-server.md +25 -0
- package/kit/skills/spec-planning/SKILL.md +92 -0
- package/kit/skills/systematic-debugging/SKILL.md +57 -0
- package/kit/skills/systematic-debugging/references/techniques.md +44 -0
- package/kit/skills/teamboard/SKILL.md +88 -0
- package/kit/skills/teamboard/references/setup.md +37 -0
- package/kit/skills/testing/SKILL.md +49 -0
- package/kit/skills/testing/references/flaky-triage.md +51 -0
- package/kit/skills/threat-model/SKILL.md +57 -0
- package/kit/skills/threat-model/references/interview.md +51 -0
- package/kit/skills/threat-model/references/schema.md +42 -0
- package/kit/skills/token-budget/SKILL.md +51 -0
- package/kit/skills/trace-scan/SKILL.md +43 -0
- package/kit/skills/worktree/SKILL.md +40 -0
- package/kit/studio/README.md +266 -0
- package/kit/studio/ensure-node.sh +471 -0
- package/kit/studio/package.json +14 -0
- package/kit/studio/server/hooks/studio-gate.sh +169 -0
- package/kit/studio/server/index.js +902 -0
- package/kit/studio/server/lib/crew-env.js +11 -0
- package/kit/studio/server/lib/fleet.js +146 -0
- package/kit/studio/server/lib/graph.js +533 -0
- package/kit/studio/server/lib/instance.js +135 -0
- package/kit/studio/server/lib/kit-telemetry.js +182 -0
- package/kit/studio/server/lib/kit.js +158 -0
- package/kit/studio/server/lib/palette.js +118 -0
- package/kit/studio/server/lib/peers.js +72 -0
- package/kit/studio/server/lib/permissions.js +150 -0
- package/kit/studio/server/lib/projects.js +294 -0
- package/kit/studio/server/lib/roster.js +191 -0
- package/kit/studio/server/lib/session.js +309 -0
- package/kit/studio/server/lib/terminal.js +91 -0
- package/kit/studio/server/lib/transcript.js +100 -0
- package/kit/studio/web/app.js +1098 -0
- package/kit/studio/web/canvas.js +1179 -0
- package/kit/studio/web/chat.js +543 -0
- package/kit/studio/web/index.html +86 -0
- package/kit/studio/web/md.js +110 -0
- package/kit/studio/web/storage-migrate.js +23 -0
- package/kit/studio/web/style.css +1453 -0
- package/package.json +46 -8
- package/start.sh +748 -0
- package/bin/crewforth.js +0 -10
|
@@ -0,0 +1,1453 @@
|
|
|
1
|
+
/* The panel's palette, light and dark. */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #f7f8fa;
|
|
4
|
+
--surface: #ffffff;
|
|
5
|
+
--surface-2: #eef0f4;
|
|
6
|
+
--line: #d9dde5;
|
|
7
|
+
--ink: #1a1d24;
|
|
8
|
+
--ink-dim: #5c6472;
|
|
9
|
+
--ink-faint: #8d95a3;
|
|
10
|
+
|
|
11
|
+
--busy: #26c6e6;
|
|
12
|
+
--waiting: #f2a65a;
|
|
13
|
+
--idle: #9aa7cc;
|
|
14
|
+
--good: #35c874;
|
|
15
|
+
--bad: #ff8a4d;
|
|
16
|
+
--accent: #5b8cff;
|
|
17
|
+
|
|
18
|
+
/* Edge tones. A branch that did not finish on its own terms is the one
|
|
19
|
+
thing on this canvas that must not be read as "some other agent colour",
|
|
20
|
+
so it gets a hue of its own rather than the node's.
|
|
21
|
+
|
|
22
|
+
The four weights are tokens and not literals because the same stroke sits
|
|
23
|
+
on white here and on near-black in the dark theme: 0.3 that reads as
|
|
24
|
+
"finished, out of the way" on #0d1017 reads as "barely drawn" on #f7f8fa. */
|
|
25
|
+
--cv-fail: #d92d3f;
|
|
26
|
+
--cv-edge-live: 1;
|
|
27
|
+
--cv-edge-wake: 0.52;
|
|
28
|
+
--cv-edge-done: 0.42;
|
|
29
|
+
--cv-edge-quiet: 0.24;
|
|
30
|
+
|
|
31
|
+
--radius: 10px;
|
|
32
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
33
|
+
--sans: ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
:root:not([data-theme="light"]) {
|
|
37
|
+
--bg: #0d1017;
|
|
38
|
+
--surface: #141821;
|
|
39
|
+
--surface-2: #1b202b;
|
|
40
|
+
--line: #262c39;
|
|
41
|
+
--ink: #e6e9f0;
|
|
42
|
+
--ink-dim: #9aa7cc;
|
|
43
|
+
--ink-faint: #5c6472;
|
|
44
|
+
|
|
45
|
+
--cv-fail: #ff6b7a;
|
|
46
|
+
--cv-edge-live: 0.95;
|
|
47
|
+
--cv-edge-wake: 0.44;
|
|
48
|
+
--cv-edge-done: 0.3;
|
|
49
|
+
--cv-edge-quiet: 0.16;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@media (prefers-color-scheme: light) {
|
|
53
|
+
:root:not([data-theme="dark"]) {
|
|
54
|
+
--bg: #f7f8fa;
|
|
55
|
+
--surface: #ffffff;
|
|
56
|
+
--surface-2: #eef0f4;
|
|
57
|
+
--line: #d9dde5;
|
|
58
|
+
--ink: #1a1d24;
|
|
59
|
+
--ink-dim: #5c6472;
|
|
60
|
+
--ink-faint: #8d95a3;
|
|
61
|
+
|
|
62
|
+
--cv-fail: #d92d3f;
|
|
63
|
+
--cv-edge-live: 1;
|
|
64
|
+
--cv-edge-wake: 0.52;
|
|
65
|
+
--cv-edge-done: 0.42;
|
|
66
|
+
--cv-edge-quiet: 0.24;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
* { box-sizing: border-box; }
|
|
71
|
+
|
|
72
|
+
body {
|
|
73
|
+
margin: 0;
|
|
74
|
+
background: var(--bg);
|
|
75
|
+
color: var(--ink);
|
|
76
|
+
font-family: var(--sans);
|
|
77
|
+
font-size: 14px;
|
|
78
|
+
line-height: 1.5;
|
|
79
|
+
/* Fixed height, not min-height: the canvas sizes itself to its box, so a
|
|
80
|
+
body that can grow past the window puts the bottom of the graph below the
|
|
81
|
+
fold while the fit maths still believes everything is visible. */
|
|
82
|
+
height: 100vh;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ---- top bar ---- */
|
|
89
|
+
.bar {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: space-between;
|
|
93
|
+
gap: 16px;
|
|
94
|
+
padding: 12px 20px;
|
|
95
|
+
border-bottom: 1px solid var(--line);
|
|
96
|
+
background: var(--surface);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.brand { display: flex; align-items: center; gap: 10px; }
|
|
100
|
+
|
|
101
|
+
.brand h1 {
|
|
102
|
+
margin: 0;
|
|
103
|
+
font-size: 15px;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
letter-spacing: 0.01em;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Crewforth's own mark, from assets/mark.svg: three chevrons (›››), drawn as
|
|
109
|
+
strokes. Two palettes: the light ones from the logo read on the dark panel,
|
|
110
|
+
the darker ones from mark.svg read on the light. Same geometry either way. */
|
|
111
|
+
.brand-mark { width: 19px; height: 19px; flex: none; }
|
|
112
|
+
.brand-mark polyline { fill: none; stroke-linecap: round; stroke-linejoin: round; }
|
|
113
|
+
.brand-mark .bar-1 { stroke: #C7CBFB; }
|
|
114
|
+
.brand-mark .bar-2 { stroke: #9AA3F7; }
|
|
115
|
+
.brand-mark .bar-3 { stroke: #7C3AED; }
|
|
116
|
+
|
|
117
|
+
:root:not([data-theme="light"]) .brand-mark .bar-1 { stroke: #E5E7FB; }
|
|
118
|
+
:root:not([data-theme="light"]) .brand-mark .bar-2 { stroke: #B9BEF9; }
|
|
119
|
+
:root:not([data-theme="light"]) .brand-mark .bar-3 { stroke: #A78BFA; }
|
|
120
|
+
|
|
121
|
+
@media (prefers-color-scheme: dark) {
|
|
122
|
+
:root:not([data-theme="light"]) .brand-mark .bar-1 { stroke: #E5E7FB; }
|
|
123
|
+
:root:not([data-theme="light"]) .brand-mark .bar-2 { stroke: #B9BEF9; }
|
|
124
|
+
:root:not([data-theme="light"]) .brand-mark .bar-3 { stroke: #A78BFA; }
|
|
125
|
+
}
|
|
126
|
+
:root[data-theme="dark"] .brand-mark .bar-1 { stroke: #E5E7FB; }
|
|
127
|
+
:root[data-theme="dark"] .brand-mark .bar-2 { stroke: #B9BEF9; }
|
|
128
|
+
:root[data-theme="dark"] .brand-mark .bar-3 { stroke: #A78BFA; }
|
|
129
|
+
|
|
130
|
+
/* Two-tone, the way the wordmark does it. */
|
|
131
|
+
.brand-accent { color: #A78BFA; }
|
|
132
|
+
:root[data-theme="light"] .brand-accent { color: #7C3AED; }
|
|
133
|
+
|
|
134
|
+
.bar-right { display: flex; align-items: center; gap: 12px; }
|
|
135
|
+
|
|
136
|
+
.pulse {
|
|
137
|
+
font-family: var(--mono);
|
|
138
|
+
font-size: 11px;
|
|
139
|
+
color: var(--ink-faint);
|
|
140
|
+
letter-spacing: 0.06em;
|
|
141
|
+
text-transform: uppercase;
|
|
142
|
+
}
|
|
143
|
+
.pulse.on { color: var(--good); }
|
|
144
|
+
.pulse.off { color: var(--bad); }
|
|
145
|
+
|
|
146
|
+
.ghost {
|
|
147
|
+
background: transparent;
|
|
148
|
+
border: 1px solid var(--line);
|
|
149
|
+
color: var(--ink-dim);
|
|
150
|
+
border-radius: 8px;
|
|
151
|
+
padding: 4px 10px;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
}
|
|
155
|
+
.ghost:hover { color: var(--ink); border-color: var(--ink-faint); }
|
|
156
|
+
|
|
157
|
+
/* ---- layout ---- */
|
|
158
|
+
.meta { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
|
|
159
|
+
|
|
160
|
+
/* ---- fleet ---- */
|
|
161
|
+
.fleet { display: grid; gap: 1px; background: var(--line); }
|
|
162
|
+
|
|
163
|
+
.session {
|
|
164
|
+
display: grid;
|
|
165
|
+
grid-template-columns: auto 1fr auto;
|
|
166
|
+
gap: 14px;
|
|
167
|
+
align-items: center;
|
|
168
|
+
padding: 14px 16px;
|
|
169
|
+
background: var(--surface);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.ring {
|
|
173
|
+
width: 12px; height: 12px;
|
|
174
|
+
border-radius: 50%;
|
|
175
|
+
border: 2px solid var(--idle);
|
|
176
|
+
flex: none;
|
|
177
|
+
}
|
|
178
|
+
.ring[data-status="busy"] {
|
|
179
|
+
border-color: var(--busy);
|
|
180
|
+
background: color-mix(in srgb, var(--busy) 30%, transparent);
|
|
181
|
+
animation: breathe 1.6s ease-in-out infinite;
|
|
182
|
+
}
|
|
183
|
+
.ring[data-status="waiting"] {
|
|
184
|
+
border-color: var(--waiting);
|
|
185
|
+
border-style: dashed;
|
|
186
|
+
}
|
|
187
|
+
.ring[data-status="idle"] { border-color: var(--idle); }
|
|
188
|
+
|
|
189
|
+
@keyframes breathe {
|
|
190
|
+
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--busy) 45%, transparent); }
|
|
191
|
+
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--busy) 0%, transparent); }
|
|
192
|
+
}
|
|
193
|
+
@media (prefers-reduced-motion: reduce) {
|
|
194
|
+
.ring[data-status="busy"] { animation: none; }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.who { min-width: 0; }
|
|
198
|
+
.who .name { font-weight: 600; font-size: 14px; }
|
|
199
|
+
.who .path {
|
|
200
|
+
font-family: var(--mono);
|
|
201
|
+
font-size: 12px;
|
|
202
|
+
color: var(--ink-dim);
|
|
203
|
+
white-space: nowrap;
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
text-overflow: ellipsis;
|
|
206
|
+
}
|
|
207
|
+
.who .waiting-for {
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
color: var(--waiting);
|
|
210
|
+
margin-top: 3px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.stat { text-align: right; font-family: var(--mono); font-size: 11px; color: var(--ink-faint); white-space: nowrap; }
|
|
214
|
+
.stat .status { display: block; font-size: 12px; color: var(--ink-dim); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
215
|
+
|
|
216
|
+
/* ---- honest empty / unmeasured states ---- */
|
|
217
|
+
.note {
|
|
218
|
+
padding: 28px 20px;
|
|
219
|
+
text-align: center;
|
|
220
|
+
color: var(--ink-dim);
|
|
221
|
+
background: var(--surface);
|
|
222
|
+
}
|
|
223
|
+
.note strong { display: block; color: var(--ink); margin-bottom: 4px; font-weight: 600; }
|
|
224
|
+
.note .why { font-family: var(--mono); font-size: 12px; color: var(--ink-faint); margin-top: 6px; }
|
|
225
|
+
.note.unmeasured strong { color: var(--waiting); }
|
|
226
|
+
|
|
227
|
+
.foot {
|
|
228
|
+
padding: 10px 20px;
|
|
229
|
+
border-top: 1px solid var(--line);
|
|
230
|
+
font-family: var(--mono);
|
|
231
|
+
font-size: 11px;
|
|
232
|
+
color: var(--ink-faint);
|
|
233
|
+
background: var(--surface);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ============================================================== shell === */
|
|
237
|
+
|
|
238
|
+
.shell {
|
|
239
|
+
position: relative;
|
|
240
|
+
flex: 1; min-height: 0;
|
|
241
|
+
display: grid;
|
|
242
|
+
/* Five columns: the two dividers are real columns, so dragging one moves a
|
|
243
|
+
boundary rather than overlaying it. The conversation sits on the right,
|
|
244
|
+
beside the graph rather than under it — a reply is read next to the work
|
|
245
|
+
it is about, not below the fold. */
|
|
246
|
+
grid-template-columns: var(--side-w, 260px) 5px 1fr 5px var(--chat-w, 420px);
|
|
247
|
+
}
|
|
248
|
+
/* Placed explicitly, never by auto-flow. A `display: none` child occupies no
|
|
249
|
+
grid cell, so hiding the sidebar slid every remaining column one to the left:
|
|
250
|
+
the graph was handed the 22px rail slot and the conversation took the rest. */
|
|
251
|
+
.shell > .side { grid-column: 1; }
|
|
252
|
+
.shell > #resizer { grid-column: 2; }
|
|
253
|
+
.shell > .stage { grid-column: 3; }
|
|
254
|
+
.shell > #chat-split { grid-column: 4; }
|
|
255
|
+
.shell > .chat { grid-column: 5; }
|
|
256
|
+
|
|
257
|
+
.shell.no-chat { grid-template-columns: var(--side-w, 260px) 5px 1fr 0 0; }
|
|
258
|
+
.shell.no-side { grid-template-columns: 0 0 1fr 5px var(--chat-w, 420px); }
|
|
259
|
+
.shell.no-side.no-chat { grid-template-columns: 0 0 1fr 0 0; }
|
|
260
|
+
.shell.no-side > .side, .shell.no-side > #resizer { display: none; }
|
|
261
|
+
|
|
262
|
+
.resizer {
|
|
263
|
+
cursor: col-resize;
|
|
264
|
+
background: var(--line);
|
|
265
|
+
position: relative;
|
|
266
|
+
}
|
|
267
|
+
.resizer::after {
|
|
268
|
+
/* A 5px target is hard to hit; the hit area is wider than the line. */
|
|
269
|
+
content: ''; position: absolute; inset: 0 -4px; z-index: 5;
|
|
270
|
+
}
|
|
271
|
+
.resizer:hover, .resizer:focus-visible, .resizer.dragging { background: var(--accent); outline: none; }
|
|
272
|
+
body.resizing { cursor: col-resize; user-select: none; }
|
|
273
|
+
body.resizing .cv-root { pointer-events: none; }
|
|
274
|
+
|
|
275
|
+
.side {
|
|
276
|
+
border-right: 1px solid var(--line);
|
|
277
|
+
background: var(--surface);
|
|
278
|
+
display: flex;
|
|
279
|
+
flex-direction: column;
|
|
280
|
+
min-height: 0;
|
|
281
|
+
overflow: hidden;
|
|
282
|
+
}
|
|
283
|
+
.side-block { border-bottom: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; }
|
|
284
|
+
.side-block.grow { flex: 1; border-bottom: none; }
|
|
285
|
+
.side-head {
|
|
286
|
+
display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
|
|
287
|
+
padding: 10px 14px; flex: none;
|
|
288
|
+
}
|
|
289
|
+
.side-head h2 {
|
|
290
|
+
margin: 0; font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
|
|
291
|
+
text-transform: uppercase; color: var(--ink-faint);
|
|
292
|
+
}
|
|
293
|
+
.fleet, .sessions { overflow-y: auto; min-height: 0; }
|
|
294
|
+
.fleet { max-height: 30vh; }
|
|
295
|
+
|
|
296
|
+
.session, .srow {
|
|
297
|
+
display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center;
|
|
298
|
+
padding: 8px 14px; border-top: 1px solid var(--line); background: var(--surface);
|
|
299
|
+
}
|
|
300
|
+
.srow { cursor: pointer; }
|
|
301
|
+
.srow:hover { background: var(--surface-2); }
|
|
302
|
+
.srow[aria-current="true"] { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--accent); }
|
|
303
|
+
.srow .sid { font-family: var(--mono); font-size: 12px; }
|
|
304
|
+
.srow .sagents { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
|
|
305
|
+
.srow .sbadge {
|
|
306
|
+
font-family: var(--mono); font-size: 10px; color: var(--accent);
|
|
307
|
+
border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
|
|
308
|
+
border-radius: 4px; padding: 0 5px;
|
|
309
|
+
}
|
|
310
|
+
.who .name { font-size: 13px; }
|
|
311
|
+
.who .path { font-size: 11px; }
|
|
312
|
+
.stat { font-size: 10px; }
|
|
313
|
+
.stat .status { font-size: 10px; }
|
|
314
|
+
|
|
315
|
+
.bar-mid { flex: 1; text-align: center; }
|
|
316
|
+
|
|
317
|
+
/* ============================================================== stage === */
|
|
318
|
+
|
|
319
|
+
.stage { position: relative; min-width: 0; display: flex; }
|
|
320
|
+
.canvas { flex: 1; position: relative; overflow: hidden; background: var(--bg); }
|
|
321
|
+
|
|
322
|
+
/* A dot grid reads as a workspace rather than a void, and it makes panning
|
|
323
|
+
legible — without it the canvas looks frozen while it moves. */
|
|
324
|
+
.cv-root {
|
|
325
|
+
background-image: radial-gradient(var(--line) 1px, transparent 1px);
|
|
326
|
+
background-size: 22px 22px;
|
|
327
|
+
cursor: grab;
|
|
328
|
+
touch-action: none;
|
|
329
|
+
user-select: none;
|
|
330
|
+
}
|
|
331
|
+
.cv-root.cv-panning { cursor: grabbing; }
|
|
332
|
+
|
|
333
|
+
.cv-viewport { position: absolute; inset: 0; transform-origin: 0 0; }
|
|
334
|
+
.cv-edges { position: absolute; overflow: visible; pointer-events: none; }
|
|
335
|
+
.cv-nodes { position: absolute; inset: 0; }
|
|
336
|
+
|
|
337
|
+
/* ------------------------------------------------------------- edge ---
|
|
338
|
+
Delegation drawn as motion. The dash travels out of the parent's port and
|
|
339
|
+
into the child's, because the question a glance has to answer is "where did
|
|
340
|
+
the work go", not "these two are related".
|
|
341
|
+
|
|
342
|
+
Every state is carried by at least two channels — weight, dash pattern,
|
|
343
|
+
tone — and motion is only ever the third. That is what makes the
|
|
344
|
+
reduced-motion path below a removal of speed rather than of information. */
|
|
345
|
+
|
|
346
|
+
.cv-edge {
|
|
347
|
+
fill: none;
|
|
348
|
+
stroke-width: 1.6;
|
|
349
|
+
stroke-linecap: round;
|
|
350
|
+
stroke-dasharray: none;
|
|
351
|
+
stroke-dashoffset: 0;
|
|
352
|
+
opacity: var(--cv-edge-done);
|
|
353
|
+
/* One dash period. The flow animation travels exactly this far per
|
|
354
|
+
iteration, so a state can change its dash pattern without putting a seam
|
|
355
|
+
in the loop. */
|
|
356
|
+
--dash-period: 13px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* Working. The brightest, heaviest, fastest line on the canvas. */
|
|
360
|
+
.cv-edge[data-state="live"] {
|
|
361
|
+
opacity: var(--cv-edge-live);
|
|
362
|
+
stroke-width: 2.2;
|
|
363
|
+
stroke-dasharray: 6px 7px;
|
|
364
|
+
--dash-period: 13px;
|
|
365
|
+
animation: cv-flow 0.9s linear infinite;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Spawned, nothing back yet. Same direction, half lit, and moving slowly:
|
|
369
|
+
something is on this wire, and it has not produced anything. */
|
|
370
|
+
.cv-edge[data-state="waking"] {
|
|
371
|
+
opacity: var(--cv-edge-wake);
|
|
372
|
+
stroke-width: 1.6;
|
|
373
|
+
stroke-dasharray: 2px 7px;
|
|
374
|
+
--dash-period: 9px;
|
|
375
|
+
animation: cv-flow 1.9s linear infinite;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* Finished. Solid and pulled back, so live work stands out against it. */
|
|
379
|
+
.cv-edge[data-state="done"] {
|
|
380
|
+
opacity: var(--cv-edge-done);
|
|
381
|
+
stroke-width: 1.6;
|
|
382
|
+
stroke-dasharray: none;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* Ended, stale. Still there, saying nothing. */
|
|
386
|
+
.cv-edge[data-state="quiet"] {
|
|
387
|
+
opacity: var(--cv-edge-quiet);
|
|
388
|
+
stroke-width: 1.4;
|
|
389
|
+
stroke-dasharray: 2px 6px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* Wrong, and said in colour rather than in a label somewhere else. The stroke
|
|
393
|
+
itself is set to var(--cv-fail) rather than the child's palette colour. */
|
|
394
|
+
.cv-edge[data-state="failed"] {
|
|
395
|
+
opacity: 1;
|
|
396
|
+
stroke-width: 2.4;
|
|
397
|
+
stroke-dasharray: 1px 5px;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* A workflow's members are one dispatch. They share a weight here and a hue
|
|
401
|
+
family in canvas.js, so twelve of them read as a bundle rather than as
|
|
402
|
+
twelve independent decisions. */
|
|
403
|
+
.cv-edge[data-group="member"] { stroke-width: 1.4; }
|
|
404
|
+
.cv-edge[data-group="member"][data-state="live"] { stroke-width: 1.8; }
|
|
405
|
+
|
|
406
|
+
/* Arrival. The edge draws itself from the parent toward the node that just
|
|
407
|
+
appeared. Both the pattern and the offset live in the keyframes, so with
|
|
408
|
+
the animation switched off the edge simply renders in its final state
|
|
409
|
+
wearing its own status dash. */
|
|
410
|
+
.cv-edge.cv-drawing { animation: cv-draw 0.3s ease-out; }
|
|
411
|
+
|
|
412
|
+
@keyframes cv-flow { to { stroke-dashoffset: calc(-1 * var(--dash-period)); } }
|
|
413
|
+
@keyframes cv-draw {
|
|
414
|
+
from { stroke-dasharray: var(--edge-len) var(--edge-len); stroke-dashoffset: var(--edge-len); }
|
|
415
|
+
to { stroke-dasharray: var(--edge-len) var(--edge-len); stroke-dashoffset: 0; }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* ------------------------------------------------------------- node --- */
|
|
419
|
+
|
|
420
|
+
.cv-node {
|
|
421
|
+
position: absolute;
|
|
422
|
+
width: 236px;
|
|
423
|
+
min-height: 104px;
|
|
424
|
+
box-sizing: border-box;
|
|
425
|
+
background: var(--surface);
|
|
426
|
+
border: 1px solid var(--line);
|
|
427
|
+
border-radius: 12px;
|
|
428
|
+
padding: 12px 14px 12px 18px;
|
|
429
|
+
cursor: grab;
|
|
430
|
+
transition: box-shadow 0.15s, border-color 0.15s, opacity 0.25s, scale 0.25s;
|
|
431
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.18), 0 8px 20px -12px rgb(0 0 0 / 0.4);
|
|
432
|
+
}
|
|
433
|
+
.cv-node:hover { border-color: color-mix(in srgb, var(--node-color) 55%, var(--line)); }
|
|
434
|
+
.cv-node.cv-dragging { cursor: grabbing; z-index: 10; box-shadow: 0 12px 34px -10px rgb(0 0 0 / 0.55); }
|
|
435
|
+
.cv-node.cv-selected {
|
|
436
|
+
border-color: var(--node-color);
|
|
437
|
+
box-shadow: 0 0 0 1px var(--node-color), 0 12px 30px -14px var(--node-color);
|
|
438
|
+
}
|
|
439
|
+
.cv-node.cv-born { opacity: 0; scale: 0.9; }
|
|
440
|
+
|
|
441
|
+
.cv-accent {
|
|
442
|
+
position: absolute; left: 0; top: 12px; bottom: 12px; width: 3px;
|
|
443
|
+
border-radius: 0 3px 3px 0; background: var(--node-color);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/* A running card, legibly alive when nothing else on the canvas is moving.
|
|
447
|
+
One pulse per card and no more: the accent bar used to breathe as well, and
|
|
448
|
+
two rhythms on one card is the wall of throbbing this is trying to avoid.
|
|
449
|
+
|
|
450
|
+
The ring is drawn whether or not it breathes, so "running" survives with
|
|
451
|
+
motion off; breathing is the only part the animation adds. Only opacity is
|
|
452
|
+
animated — the shadow itself never changes, so nothing re-rasterises. */
|
|
453
|
+
.cv-node[data-state="live"]::after {
|
|
454
|
+
content: '';
|
|
455
|
+
position: absolute; inset: -2px;
|
|
456
|
+
border-radius: 14px;
|
|
457
|
+
pointer-events: none;
|
|
458
|
+
box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--node-color) 55%, transparent);
|
|
459
|
+
animation: cv-pulse 2.4s ease-in-out infinite;
|
|
460
|
+
}
|
|
461
|
+
@keyframes cv-pulse { 0%, 100% { opacity: 0.9; } 50% { opacity: 0.28; } }
|
|
462
|
+
|
|
463
|
+
/* Failed, killed, stopped — one look, and not the look of a colour some agent
|
|
464
|
+
might legitimately own. */
|
|
465
|
+
.cv-node[data-state="failed"] { border-color: var(--cv-fail); }
|
|
466
|
+
.cv-node[data-state="failed"] .cv-accent { background: var(--cv-fail); }
|
|
467
|
+
.cv-node[data-state="failed"] .cv-chip {
|
|
468
|
+
color: var(--cv-fail);
|
|
469
|
+
border-color: color-mix(in srgb, var(--cv-fail) 50%, transparent);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.cv-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
473
|
+
.cv-type {
|
|
474
|
+
font-family: var(--mono); font-size: 11px; font-weight: 600;
|
|
475
|
+
letter-spacing: 0.06em; text-transform: uppercase; color: var(--node-color);
|
|
476
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
477
|
+
}
|
|
478
|
+
.cv-type.cv-unknown::after {
|
|
479
|
+
content: '?'; margin-left: 5px; color: var(--ink-faint);
|
|
480
|
+
border: 1px solid var(--line); border-radius: 3px; padding: 0 3px; font-size: 9px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.cv-chip {
|
|
484
|
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.05em;
|
|
485
|
+
text-transform: uppercase; padding: 1px 6px; border-radius: 999px;
|
|
486
|
+
border: 1px solid var(--line); color: var(--ink-faint); white-space: nowrap; flex: none;
|
|
487
|
+
}
|
|
488
|
+
.cv-chip[data-status="running"] { color: var(--busy); border-color: color-mix(in srgb, var(--busy) 45%, transparent); }
|
|
489
|
+
.cv-chip[data-status="done"] { color: var(--good); border-color: color-mix(in srgb, var(--good) 40%, transparent); }
|
|
490
|
+
.cv-chip[data-status="stale"] { color: var(--waiting); border-color: color-mix(in srgb, var(--waiting) 40%, transparent); }
|
|
491
|
+
.cv-chip[data-status="starting"]{ color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
|
|
492
|
+
|
|
493
|
+
.cv-desc {
|
|
494
|
+
margin-top: 7px; font-size: 13px; line-height: 1.35; color: var(--ink);
|
|
495
|
+
display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
|
|
496
|
+
-webkit-box-orient: vertical; overflow: hidden;
|
|
497
|
+
}
|
|
498
|
+
.cv-node[data-kind="session"] .cv-desc { font-family: var(--mono); font-size: 12px; color: var(--ink-dim); }
|
|
499
|
+
|
|
500
|
+
.cv-foot { margin-top: 9px; display: flex; flex-wrap: wrap; gap: 5px; }
|
|
501
|
+
.cv-bit {
|
|
502
|
+
font-family: var(--mono); font-size: 10px; color: var(--ink-faint);
|
|
503
|
+
background: var(--surface-2); border-radius: 4px; padding: 1px 6px;
|
|
504
|
+
}
|
|
505
|
+
.cv-bit.cv-live { color: var(--busy); background: color-mix(in srgb, var(--busy) 14%, transparent); }
|
|
506
|
+
.cv-bit.cv-err { color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, transparent); }
|
|
507
|
+
.cv-bit.cv-branch { color: var(--ink-dim); }
|
|
508
|
+
|
|
509
|
+
/* Ports sit on whichever axis the flow runs along. */
|
|
510
|
+
.cv-port {
|
|
511
|
+
position: absolute; width: 9px; height: 9px; border-radius: 50%;
|
|
512
|
+
background: var(--surface); border: 2px solid var(--node-color);
|
|
513
|
+
}
|
|
514
|
+
[data-flow="down"] .cv-port { left: 50%; transform: translateX(-50%); }
|
|
515
|
+
[data-flow="down"] .cv-in { top: -5px; }
|
|
516
|
+
[data-flow="down"] .cv-out { bottom: -5px; }
|
|
517
|
+
|
|
518
|
+
[data-flow="right"] .cv-port { top: 50%; transform: translateY(-50%); }
|
|
519
|
+
[data-flow="right"] .cv-in { left: -5px; }
|
|
520
|
+
[data-flow="right"] .cv-out { right: -5px; }
|
|
521
|
+
.cv-node[data-kind="session"] .cv-in { display: none; }
|
|
522
|
+
|
|
523
|
+
/* --------------------------------------------------------------- hud --- */
|
|
524
|
+
|
|
525
|
+
.cv-hud {
|
|
526
|
+
position: absolute; right: 14px; bottom: 14px; z-index: 20;
|
|
527
|
+
display: flex; align-items: center; gap: 6px;
|
|
528
|
+
background: var(--surface); border: 1px solid var(--line);
|
|
529
|
+
border-radius: 9px; padding: 4px 8px;
|
|
530
|
+
}
|
|
531
|
+
.cv-btn {
|
|
532
|
+
background: transparent; border: none; color: var(--ink-dim);
|
|
533
|
+
cursor: pointer; font-size: 14px; line-height: 1; padding: 3px 5px; border-radius: 5px;
|
|
534
|
+
}
|
|
535
|
+
.cv-btn:hover { color: var(--ink); background: var(--surface-2); }
|
|
536
|
+
.cv-zoom {
|
|
537
|
+
font-family: var(--mono); font-size: 10px; color: var(--ink-faint);
|
|
538
|
+
min-width: 96px; text-align: right; white-space: nowrap;
|
|
539
|
+
}
|
|
540
|
+
/* Loud on purpose: it only appears when something went wrong. */
|
|
541
|
+
.cv-btn.cv-alarm { color: var(--cv-fail); font-family: var(--mono); font-size: 11px; }
|
|
542
|
+
.cv-btn.cv-alarm:hover { color: var(--cv-fail); background: color-mix(in srgb, var(--cv-fail) 16%, transparent); }
|
|
543
|
+
.cv-btn[hidden] { display: none; }
|
|
544
|
+
|
|
545
|
+
.cv-empty {
|
|
546
|
+
position: absolute; inset: 0; display: grid; place-content: center;
|
|
547
|
+
text-align: center; color: var(--ink-dim); padding: 24px; pointer-events: none;
|
|
548
|
+
}
|
|
549
|
+
.cv-empty strong { display: block; color: var(--ink); margin-bottom: 6px; font-size: 15px; }
|
|
550
|
+
|
|
551
|
+
/* --------------------------------------------------------- inspector --- */
|
|
552
|
+
|
|
553
|
+
.inspector {
|
|
554
|
+
width: 300px; flex: none; border-left: 1px solid var(--line);
|
|
555
|
+
background: var(--surface); overflow-y: auto; padding: 16px;
|
|
556
|
+
}
|
|
557
|
+
.inspector h3 {
|
|
558
|
+
margin: 0 0 2px; font-family: var(--mono); font-size: 12px;
|
|
559
|
+
letter-spacing: 0.06em; text-transform: uppercase;
|
|
560
|
+
}
|
|
561
|
+
.inspector .isub { color: var(--ink-dim); font-size: 13px; margin-bottom: 14px; }
|
|
562
|
+
.inspector dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; }
|
|
563
|
+
.inspector dt { font-size: 11px; color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
564
|
+
.inspector dd { margin: 0; font-family: var(--mono); font-size: 12px; word-break: break-word; }
|
|
565
|
+
.inspector .tools { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 5px; }
|
|
566
|
+
|
|
567
|
+
@media (max-width: 900px) {
|
|
568
|
+
.shell { grid-template-columns: 1fr; }
|
|
569
|
+
.side, .resizer { display: none; }
|
|
570
|
+
.inspector { display: none; }
|
|
571
|
+
}
|
|
572
|
+
/* =================================================== three readings ===
|
|
573
|
+
One picture, read three ways, chosen by how much of a card would actually
|
|
574
|
+
survive on screen. Nothing moves between them: the layout metrics are
|
|
575
|
+
identical in all three, so the shape of the graph is the constant and only
|
|
576
|
+
what is written inside a card changes.
|
|
577
|
+
|
|
578
|
+
The idea is not "hide things at low zoom" — hiding alone still leaves an
|
|
579
|
+
unreadable title. What survives is drawn at a constant SCREEN size: a world
|
|
580
|
+
font-size of 11px/k is 11px after the canvas transform, whatever k is.
|
|
581
|
+
canvas.js publishes k as --k and the reading as data-lod on the root, and
|
|
582
|
+
every card responds through the cascade, so a band change costs nothing per
|
|
583
|
+
node and cannot move the world that decided the band.
|
|
584
|
+
|
|
585
|
+
Registered so that a malformed write falls back to 1 rather than blanking
|
|
586
|
+
250 cards. k is clamped to MIN_K = 0.06 in canvas.js, so no divisor here is
|
|
587
|
+
ever zero. (The selfcheck's CSS parser drops at-rules, so this registration
|
|
588
|
+
is invisible to the gate — it is verified in a browser.) */
|
|
589
|
+
@property --k { syntax: '<number>'; initial-value: 1; inherits: true; }
|
|
590
|
+
|
|
591
|
+
/* NEAR — k >= 0.70. The card exactly as it has always been drawn, so it has
|
|
592
|
+
no rules of its own here. */
|
|
593
|
+
|
|
594
|
+
/* Blocks a smaller reading gives up fade rather than vanish, so the eye reads
|
|
595
|
+
a substitution instead of a loss. */
|
|
596
|
+
.cv-desc, .cv-foot, .cv-chip { transition: opacity 120ms; }
|
|
597
|
+
/* The same element is the 14px source glyph up close and the hue disc far
|
|
598
|
+
out; it travels there rather than being swapped. */
|
|
599
|
+
.cv-mark { transition: transform 160ms ease, width 160ms ease, height 160ms ease; }
|
|
600
|
+
|
|
601
|
+
/* MID — 0.20 <= k < 0.70, under the label budget. Short type, status chip,
|
|
602
|
+
mark, fold caret. The description and the foot bits are dropped not because
|
|
603
|
+
they are small but because they are DARK: the description fills the lower
|
|
604
|
+
half of the card in unreadable grey, and forty of those lower halves is the
|
|
605
|
+
noise floor that stops the type line reading. Removing them is what creates
|
|
606
|
+
the contrast the type needs. */
|
|
607
|
+
[data-lod="mid"] .cv-desc,
|
|
608
|
+
[data-lod="mid"] .cv-foot { opacity: 0; }
|
|
609
|
+
|
|
610
|
+
/* The chip keeps its channel and loses its words. Its text is a 10px absolute
|
|
611
|
+
glyph, so at the bottom of this band it is 2px on screen — unreadable, and
|
|
612
|
+
measured in a browser it was eating the width the type line needed: SESSION
|
|
613
|
+
was clipping to SESSI and REVIEWER to REVIEWE. As a dot it still says the
|
|
614
|
+
status in colour and gives 45 world px back to the name. */
|
|
615
|
+
[data-lod="mid"] .cv-chip {
|
|
616
|
+
font-size: 0;
|
|
617
|
+
padding: 0;
|
|
618
|
+
width: calc(7px / var(--k)); /* 7 SCREEN px at any k */
|
|
619
|
+
height: calc(7px / var(--k));
|
|
620
|
+
border-radius: 50%;
|
|
621
|
+
background: currentColor;
|
|
622
|
+
border-color: currentColor;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* The real type line is taken out of the flow and redrawn counter-scaled, as
|
|
626
|
+
the short name: the crew- prefix and the -expert / -agent suffix dropped. */
|
|
627
|
+
[data-lod="mid"] .cv-type,
|
|
628
|
+
[data-lod="far"] .cv-type { font-size: 0; letter-spacing: 0; }
|
|
629
|
+
[data-lod="mid"] .cv-type::after,
|
|
630
|
+
[data-lod="far"] .cv-type::after { content: none; }
|
|
631
|
+
[data-lod="mid"] .cv-type::before {
|
|
632
|
+
content: attr(data-short);
|
|
633
|
+
font-size: calc(11px / var(--k)); /* 11 SCREEN px at any k */
|
|
634
|
+
letter-spacing: 0.04em;
|
|
635
|
+
transition: font-size 120ms ease;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* MID, THE LIVE EXEMPTION. `far` below already establishes this principle —
|
|
639
|
+
"with 250 strokes on screen the only bright ones must be the branches that
|
|
640
|
+
are alive" — and gives the live card its name, its mark and a z-index. It
|
|
641
|
+
was never applied to THIS band, which is the one a viewer actually sits in:
|
|
642
|
+
measured at 60% with fourteen agents, and there the running card is
|
|
643
|
+
distinguishable from thirteen finished ones only by the hue of a 7px disc.
|
|
644
|
+
The panel's own README promises "which agent is running, what it is doing
|
|
645
|
+
right now"; at mid it was keeping neither.
|
|
646
|
+
The description is dropped at mid because forty dark lower halves are a
|
|
647
|
+
noise floor that stops the type line reading. ONE is not a floor — it is
|
|
648
|
+
the signal, and it is the only field that says WHICH agent. It is drawn
|
|
649
|
+
counter-scaled like every other survivor in this block, so it clears the
|
|
650
|
+
same 9px screen floor that set the band rather than undercutting it, and
|
|
651
|
+
clamped to one line so it cannot reclaim the whole lower half. */
|
|
652
|
+
[data-lod="mid"] .cv-node[data-state="live"] .cv-desc {
|
|
653
|
+
opacity: 1;
|
|
654
|
+
font-size: calc(11px / var(--k)); /* 11 SCREEN px at any k */
|
|
655
|
+
-webkit-line-clamp: 1; line-clamp: 1;
|
|
656
|
+
}
|
|
657
|
+
/* The chip keeps its WORD here instead of collapsing to a disc. The disc
|
|
658
|
+
exists because 10px of chip text is 2px on screen at the bottom of this
|
|
659
|
+
band and was clipping the type line; the running card is the one place that
|
|
660
|
+
trade is wrong, so its chip is counter-scaled instead of silenced. Every
|
|
661
|
+
property the disc rule set has to be undone explicitly, or the word renders
|
|
662
|
+
inside a 7px circle. */
|
|
663
|
+
[data-lod="mid"] .cv-node[data-state="live"] .cv-chip {
|
|
664
|
+
font-size: calc(9px / var(--k));
|
|
665
|
+
padding: 0 calc(5px / var(--k));
|
|
666
|
+
width: auto; height: auto;
|
|
667
|
+
border-radius: calc(999px / var(--k));
|
|
668
|
+
background: transparent;
|
|
669
|
+
border-color: color-mix(in srgb, var(--busy) 45%, transparent);
|
|
670
|
+
}
|
|
671
|
+
/* Rides above its neighbours for the same reason it does at `far`: the words
|
|
672
|
+
it just kept must not sit under the next card. */
|
|
673
|
+
[data-lod="mid"] .cv-node[data-state="live"] { z-index: 5; }
|
|
674
|
+
|
|
675
|
+
/* FAR — k < 0.20, or more drawn nodes than the label budget. No text is
|
|
676
|
+
attempted for an ordinary node, because none would survive. Three things
|
|
677
|
+
still resolve at this size and each gets a channel: POSITION (which type
|
|
678
|
+
group, which depth — the layout already encodes it), HUE (--node-color, the
|
|
679
|
+
one place colour is decided) and a RING (status). */
|
|
680
|
+
[data-lod="far"] {
|
|
681
|
+
/* Screen diameter of the disc: floored so it survives the tightest node
|
|
682
|
+
pitch, capped so it never outgrows its cell. Divided back out of k to get
|
|
683
|
+
the world size that draws it. */
|
|
684
|
+
--dot-screen: clamp(5px, calc(88 * var(--k) * 1px), 16px);
|
|
685
|
+
--dots: calc(var(--dot-screen) / var(--k));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* The card body stops being a rectangle, because the rectangle is the wall. */
|
|
689
|
+
[data-lod="far"] .cv-node {
|
|
690
|
+
background: transparent;
|
|
691
|
+
border-color: transparent;
|
|
692
|
+
box-shadow: none;
|
|
693
|
+
/* Pinned rather than left to min-height. A long description pushes the box
|
|
694
|
+
past 104px, and since the disc is centred on the box rather than on the
|
|
695
|
+
cell, cards with more text put their disc a few world px lower than their
|
|
696
|
+
neighbours — a lattice of dots that does not quite line up. Nothing
|
|
697
|
+
visible is clipped: what overflows here is already at opacity 0. */
|
|
698
|
+
height: 104px;
|
|
699
|
+
}
|
|
700
|
+
/* Except the one the user picked, which still has to be findable. */
|
|
701
|
+
[data-lod="far"] .cv-node.cv-selected {
|
|
702
|
+
box-shadow: 0 0 0 calc(1.5px / var(--k)) var(--node-color);
|
|
703
|
+
}
|
|
704
|
+
[data-lod="far"] .cv-desc,
|
|
705
|
+
[data-lod="far"] .cv-foot,
|
|
706
|
+
[data-lod="far"] .cv-chip { opacity: 0; }
|
|
707
|
+
[data-lod="far"] .cv-accent,
|
|
708
|
+
[data-lod="far"] .cv-port,
|
|
709
|
+
[data-lod="far"] .cv-fold { display: none; }
|
|
710
|
+
/* The card ring is a 236x104 rectangle; the disc carries "running" instead. */
|
|
711
|
+
[data-lod="far"] .cv-node[data-state="live"]::after { display: none; }
|
|
712
|
+
|
|
713
|
+
/* .cv-mark becomes the disc, in place. */
|
|
714
|
+
[data-lod="far"] .cv-mark {
|
|
715
|
+
position: absolute;
|
|
716
|
+
left: 18px; /* the padding it already sat behind */
|
|
717
|
+
top: 12px;
|
|
718
|
+
width: var(--dots);
|
|
719
|
+
height: var(--dots);
|
|
720
|
+
/* 93 and 33 carry a 14px mark from the head to the card's visual centre —
|
|
721
|
+
(236-14)/2 - 18 and (104-14)/2 - 12 — and the half-diameter term holds the
|
|
722
|
+
disc on that centre as it grows. */
|
|
723
|
+
transform: translate(calc(93px + 7px - var(--dots) / 2), calc(33px + 7px - var(--dots) / 2));
|
|
724
|
+
border-radius: 50%;
|
|
725
|
+
background: var(--node-color);
|
|
726
|
+
/* A screen-constant hairline around every agent circle. */
|
|
727
|
+
box-shadow: 0 0 0 calc(1.4px / var(--k)) color-mix(in srgb, var(--ink) 34%, transparent);
|
|
728
|
+
}
|
|
729
|
+
/* An 11-ray burst is mush at 7px. */
|
|
730
|
+
[data-lod="far"] .cv-mark > * { display: none; }
|
|
731
|
+
/* Source survives as SHAPE, which reads at 7px where a glyph does not. */
|
|
732
|
+
[data-lod="far"] .cv-node[data-source="kit"] .cv-mark { border-radius: 3px; }
|
|
733
|
+
/* Status survives as the ring — the chip's colour, moved. */
|
|
734
|
+
[data-lod="far"] .cv-node[data-state="live"] .cv-mark {
|
|
735
|
+
box-shadow: 0 0 0 calc(2px / var(--k)) var(--busy);
|
|
736
|
+
}
|
|
737
|
+
[data-lod="far"] .cv-node[data-state="failed"] .cv-mark {
|
|
738
|
+
box-shadow: 0 0 0 calc(2px / var(--k)) var(--cv-fail);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/* Who keeps a label out here. This is what makes the far band a picture
|
|
742
|
+
rather than a field of anonymous dots, and it answers the question the panel
|
|
743
|
+
exists for — which agent is running, and what is it doing — without a click.
|
|
744
|
+
Running and failed branches keep their words whatever the budget says; the
|
|
745
|
+
session and the workflows keep theirs because they are few and they anchor
|
|
746
|
+
the picture.
|
|
747
|
+
|
|
748
|
+
POSITIVE SELECTORS ONLY. The selfcheck's matcher treats a pseudo-class it
|
|
749
|
+
does not model as a non-match, so a :not() rule here would resolve to "no
|
|
750
|
+
match" in the gate and pass green for a rule the browser is running and the
|
|
751
|
+
test never saw. */
|
|
752
|
+
[data-lod="far"] .cv-node[data-state="live"] .cv-type::before,
|
|
753
|
+
[data-lod="far"] .cv-node[data-state="failed"] .cv-type::before,
|
|
754
|
+
[data-lod="far"] .cv-node[data-kind="session"] .cv-type::before,
|
|
755
|
+
[data-lod="far"] .cv-node[data-kind="workflow"] .cv-type::before {
|
|
756
|
+
content: attr(data-far);
|
|
757
|
+
font-size: calc(11px / var(--k));
|
|
758
|
+
letter-spacing: 0.04em;
|
|
759
|
+
transition: font-size 120ms ease;
|
|
760
|
+
}
|
|
761
|
+
/* Centred under its own disc and allowed out of the card, because at this size
|
|
762
|
+
the card is no longer a box — the disc is the node. */
|
|
763
|
+
[data-lod="far"] .cv-node[data-state="live"] .cv-type,
|
|
764
|
+
[data-lod="far"] .cv-node[data-state="failed"] .cv-type,
|
|
765
|
+
[data-lod="far"] .cv-node[data-kind="session"] .cv-type,
|
|
766
|
+
[data-lod="far"] .cv-node[data-kind="workflow"] .cv-type {
|
|
767
|
+
position: absolute;
|
|
768
|
+
left: 50%; /* = 118px, the disc's centre */
|
|
769
|
+
top: calc(52px + var(--dots) * 0.62);
|
|
770
|
+
transform: translateX(-50%);
|
|
771
|
+
overflow: visible;
|
|
772
|
+
text-overflow: clip;
|
|
773
|
+
}
|
|
774
|
+
/* Their words ride above the field rather than under the next card. */
|
|
775
|
+
[data-lod="far"] .cv-node[data-state="live"],
|
|
776
|
+
[data-lod="far"] .cv-node[data-state="failed"] { z-index: 5; }
|
|
777
|
+
|
|
778
|
+
/* With 250 strokes on screen the only bright ones must be the branches that
|
|
779
|
+
are alive and the branches that went wrong. */
|
|
780
|
+
[data-lod="far"] .cv-edge[data-state="done"],
|
|
781
|
+
[data-lod="far"] .cv-edge[data-state="quiet"] { opacity: var(--cv-edge-quiet); }
|
|
782
|
+
|
|
783
|
+
/* ------------------------------------------------------- stillness ---
|
|
784
|
+
Two ways the canvas stops moving, and one set of rules for both.
|
|
785
|
+
|
|
786
|
+
The first is the motion budget: canvas.js counts what would be in motion
|
|
787
|
+
and stamps data-motion="still" on the root past MOTION_BUDGET. A dash
|
|
788
|
+
travelling along a stroke is paint work, not compositor work, so its cost
|
|
789
|
+
scales with the number of moving strokes; 250 of them is a real session.
|
|
790
|
+
|
|
791
|
+
The second is the viewer asking for it.
|
|
792
|
+
|
|
793
|
+
Both land in the same place because every state above is already carried by
|
|
794
|
+
weight, dash and tone. Nothing here has a state to lose — only a speed. */
|
|
795
|
+
|
|
796
|
+
[data-motion="still"] .cv-edge,
|
|
797
|
+
[data-motion="still"] .cv-edge.cv-drawing,
|
|
798
|
+
[data-motion="still"] .cv-node::after { animation: none; }
|
|
799
|
+
[data-motion="still"] .cv-node.cv-born { opacity: 1; scale: 1; }
|
|
800
|
+
/* A reading change is motion too — the mark travels to the card's centre and
|
|
801
|
+
the label resizes. `.cv-node { transition: none }` does not reach a child;
|
|
802
|
+
that is specificity, not the media query, so each of these carries at least
|
|
803
|
+
the weight of the rule it cancels. */
|
|
804
|
+
[data-motion="still"] .cv-mark,
|
|
805
|
+
[data-motion="still"] .cv-type::before,
|
|
806
|
+
[data-motion="still"] .cv-desc,
|
|
807
|
+
[data-motion="still"] .cv-foot,
|
|
808
|
+
[data-motion="still"] .cv-chip { transition: none; }
|
|
809
|
+
|
|
810
|
+
/* A media query adds no specificity. `.cv-edge` alone loses to
|
|
811
|
+
`.cv-edge[data-state="live"]` and the flow keeps running, so each selector
|
|
812
|
+
here carries at least the weight of the rule it has to cancel. */
|
|
813
|
+
@media (prefers-reduced-motion: reduce) {
|
|
814
|
+
.cv-edge[data-state],
|
|
815
|
+
.cv-edge.cv-drawing,
|
|
816
|
+
.cv-node[data-state]::after { animation: none; }
|
|
817
|
+
.cv-node { transition: none; }
|
|
818
|
+
.cv-node .cv-mark,
|
|
819
|
+
.cv-node .cv-type::before,
|
|
820
|
+
.cv-node .cv-desc,
|
|
821
|
+
.cv-node .cv-foot,
|
|
822
|
+
.cv-node .cv-chip { transition: none; }
|
|
823
|
+
/* The birth class comes off a frame later, which with no transition left is
|
|
824
|
+
a blink from invisible to placed. Land it in its final state instead. */
|
|
825
|
+
.cv-node.cv-born { opacity: 1; scale: 1; }
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/* ==================================================== inspector detail === */
|
|
829
|
+
|
|
830
|
+
.inspector { width: 340px; transition: width 0.18s; display: flex; flex-direction: column; padding: 0; }
|
|
831
|
+
.inspector.wide { width: min(760px, 55vw); }
|
|
832
|
+
|
|
833
|
+
.ihead { display: flex; align-items: center; gap: 8px; padding: 14px 16px 0; }
|
|
834
|
+
.ihead h3 { flex: 1; margin: 0; }
|
|
835
|
+
.iwide { padding: 2px 8px; font-size: 12px; line-height: 1.2; }
|
|
836
|
+
.inspector .isub { padding: 4px 16px 12px; margin: 0; }
|
|
837
|
+
|
|
838
|
+
.itabs { display: flex; gap: 2px; padding: 0 12px; border-bottom: 1px solid var(--line); flex: none; }
|
|
839
|
+
.itab {
|
|
840
|
+
background: none; border: none; cursor: pointer;
|
|
841
|
+
font-family: var(--mono); font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase;
|
|
842
|
+
color: var(--ink-faint); padding: 8px 10px; border-bottom: 2px solid transparent; margin-bottom: -1px;
|
|
843
|
+
}
|
|
844
|
+
.itab:hover { color: var(--ink-dim); }
|
|
845
|
+
.itab.on { color: var(--ink); border-bottom-color: var(--accent); }
|
|
846
|
+
|
|
847
|
+
.ibody { flex: 1; overflow-y: auto; padding: 14px 16px 24px; min-height: 0; }
|
|
848
|
+
.ihint { color: var(--ink-faint); font-size: 13px; line-height: 1.5; padding: 6px 0; }
|
|
849
|
+
.ibar { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
|
|
850
|
+
.ibar .ghost { font-family: var(--mono); font-size: 11px; padding: 2px 8px; }
|
|
851
|
+
|
|
852
|
+
/* ------------------------------------------------------- activity list --- */
|
|
853
|
+
|
|
854
|
+
.itimeline { display: flex; flex-direction: column; gap: 1px; }
|
|
855
|
+
.istep {
|
|
856
|
+
display: grid; grid-template-columns: 22px 62px 1fr; gap: 8px; align-items: baseline;
|
|
857
|
+
padding: 5px 0; border-bottom: 1px solid var(--line);
|
|
858
|
+
}
|
|
859
|
+
.istep-n { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); text-align: right; }
|
|
860
|
+
.istep-tool { font-family: var(--mono); font-size: 11px; color: var(--accent); }
|
|
861
|
+
.istep-label { font-size: 12px; color: var(--ink-dim); overflow-wrap: anywhere; }
|
|
862
|
+
.md-narration { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--line); opacity: 0.85; }
|
|
863
|
+
|
|
864
|
+
/* ---------------------------------------------------------- markdown --- */
|
|
865
|
+
|
|
866
|
+
.md { font-size: 13px; line-height: 1.6; color: var(--ink); overflow-wrap: anywhere; }
|
|
867
|
+
.md-h { margin: 18px 0 6px; font-size: 13px; font-weight: 600; color: var(--ink); letter-spacing: 0.01em; }
|
|
868
|
+
.md-h:first-child { margin-top: 0; }
|
|
869
|
+
h4.md-h { font-size: 14px; }
|
|
870
|
+
h5.md-h, h6.md-h { font-size: 12px; color: var(--ink-dim); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
871
|
+
.md-p { margin: 0 0 9px; }
|
|
872
|
+
.md-list { margin: 0 0 9px; padding-left: 20px; }
|
|
873
|
+
.md-list li { margin-bottom: 3px; }
|
|
874
|
+
.md code {
|
|
875
|
+
font-family: var(--mono); font-size: 0.9em;
|
|
876
|
+
background: var(--surface-2); border-radius: 4px; padding: 1px 4px;
|
|
877
|
+
}
|
|
878
|
+
.md-code {
|
|
879
|
+
background: var(--surface-2); border: 1px solid var(--line); border-radius: 8px;
|
|
880
|
+
padding: 10px 12px; overflow-x: auto; margin: 0 0 10px;
|
|
881
|
+
font-family: var(--mono); font-size: 11.5px; line-height: 1.5;
|
|
882
|
+
}
|
|
883
|
+
.md-code code { background: none; padding: 0; }
|
|
884
|
+
.md-quote {
|
|
885
|
+
margin: 0 0 10px; padding: 6px 0 6px 12px;
|
|
886
|
+
border-left: 2px solid var(--accent); color: var(--ink-dim);
|
|
887
|
+
}
|
|
888
|
+
.md-hr { border: none; border-top: 1px solid var(--line); margin: 16px 0; }
|
|
889
|
+
.md-link { color: var(--accent); }
|
|
890
|
+
.md-tablewrap { overflow-x: auto; margin: 0 0 12px; }
|
|
891
|
+
.md-table { border-collapse: collapse; font-size: 12px; min-width: 100%; }
|
|
892
|
+
.md-table th, .md-table td {
|
|
893
|
+
border: 1px solid var(--line); padding: 5px 9px; text-align: left; vertical-align: top;
|
|
894
|
+
}
|
|
895
|
+
.md-table th { background: var(--surface-2); font-weight: 600; white-space: nowrap; }
|
|
896
|
+
|
|
897
|
+
/* `display: flex` on .inspector outranks the UA's `[hidden] { display: none }`,
|
|
898
|
+
so the hidden panel kept its width. Restate it explicitly. */
|
|
899
|
+
.inspector[hidden] { display: none; }
|
|
900
|
+
|
|
901
|
+
/* `overflow-wrap: anywhere` keeps long paths inside the panel, but inside a
|
|
902
|
+
table it shreds every cell into one letter per line. Tables get their real
|
|
903
|
+
width back and scroll sideways instead. */
|
|
904
|
+
.md-table th, .md-table td { overflow-wrap: normal; min-width: 76px; }
|
|
905
|
+
.md-table td { hyphens: none; }
|
|
906
|
+
|
|
907
|
+
/* ================================================= project sidebar === */
|
|
908
|
+
|
|
909
|
+
.side-filter { padding: 0 12px 8px; flex: none; }
|
|
910
|
+
.side-filter input {
|
|
911
|
+
width: 100%; box-sizing: border-box;
|
|
912
|
+
background: var(--bg); border: 1px solid var(--line); border-radius: 7px;
|
|
913
|
+
color: var(--ink); font-family: var(--mono); font-size: 11px; padding: 5px 8px;
|
|
914
|
+
}
|
|
915
|
+
.side-filter input:focus { outline: none; border-color: var(--accent); }
|
|
916
|
+
.side-filter input::placeholder { color: var(--ink-faint); }
|
|
917
|
+
|
|
918
|
+
.pgroup {
|
|
919
|
+
display: grid; grid-template-columns: 14px 1fr auto; gap: 7px; align-items: center;
|
|
920
|
+
padding: 7px 14px; cursor: pointer; border-top: 1px solid var(--line);
|
|
921
|
+
background: var(--surface-2);
|
|
922
|
+
}
|
|
923
|
+
.pgroup:hover { background: color-mix(in srgb, var(--accent) 8%, var(--surface-2)); }
|
|
924
|
+
.pgroup.current { box-shadow: inset 3px 0 0 var(--accent); }
|
|
925
|
+
.pcaret { font-size: 9px; color: var(--ink-faint); }
|
|
926
|
+
.plabel {
|
|
927
|
+
font-size: 12px; font-weight: 600;
|
|
928
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
929
|
+
}
|
|
930
|
+
.pcount { display: flex; gap: 5px; align-items: center; font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
|
|
931
|
+
|
|
932
|
+
.srow {
|
|
933
|
+
display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center;
|
|
934
|
+
padding: 6px 14px 6px 28px; border-top: 1px solid var(--line);
|
|
935
|
+
background: var(--surface); cursor: pointer;
|
|
936
|
+
}
|
|
937
|
+
.srow:hover { background: var(--surface-2); }
|
|
938
|
+
.srow[aria-current="true"] { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--good); }
|
|
939
|
+
.sname {
|
|
940
|
+
font-size: 12px; color: var(--ink);
|
|
941
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
942
|
+
}
|
|
943
|
+
.srow[aria-current="true"] .sname { font-weight: 600; }
|
|
944
|
+
.sright { display: flex; gap: 5px; align-items: center; }
|
|
945
|
+
.sagents { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
|
|
946
|
+
.sbadge {
|
|
947
|
+
font-family: var(--mono); font-size: 9px; color: var(--accent);
|
|
948
|
+
border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
|
|
949
|
+
border-radius: 4px; padding: 0 4px;
|
|
950
|
+
}
|
|
951
|
+
.ptrunc {
|
|
952
|
+
padding: 5px 14px 5px 28px; font-family: var(--mono); font-size: 10px;
|
|
953
|
+
color: var(--ink-faint); background: var(--surface); border-top: 1px solid var(--line);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/* ------------------------------------------------------- kit version --- */
|
|
957
|
+
|
|
958
|
+
.kitbadge {
|
|
959
|
+
font-family: var(--mono); font-size: 9px; padding: 0 4px;
|
|
960
|
+
border-radius: 4px; border: 1px solid var(--line); color: var(--ink-faint);
|
|
961
|
+
white-space: nowrap;
|
|
962
|
+
}
|
|
963
|
+
.kitbadge.ok { color: var(--good); border-color: color-mix(in srgb, var(--good) 35%, transparent); }
|
|
964
|
+
.kitbadge.old { color: var(--waiting); border-color: color-mix(in srgb, var(--waiting) 50%, transparent);
|
|
965
|
+
background: color-mix(in srgb, var(--waiting) 12%, transparent); }
|
|
966
|
+
.kitbadge.ahead { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent); }
|
|
967
|
+
.kitbadge.unknown { color: var(--ink-faint); }
|
|
968
|
+
|
|
969
|
+
.pgroup.gone .plabel { color: var(--ink-faint); text-decoration: line-through; }
|
|
970
|
+
|
|
971
|
+
.kitline {
|
|
972
|
+
display: block; font-family: var(--mono); font-size: 10px;
|
|
973
|
+
color: var(--ink-faint); letter-spacing: 0.03em; margin-top: 1px;
|
|
974
|
+
}
|
|
975
|
+
.kitline.warn { color: var(--waiting); }
|
|
976
|
+
|
|
977
|
+
.ptrunc.pmissing { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
978
|
+
.ptrunc.pmissing .ghost { font-family: var(--mono); font-size: 10px; padding: 1px 6px; }
|
|
979
|
+
|
|
980
|
+
/* ------------------------------------------------------- fold control --- */
|
|
981
|
+
|
|
982
|
+
.cv-fold {
|
|
983
|
+
background: transparent; border: 1px solid var(--line); border-radius: 5px;
|
|
984
|
+
color: var(--ink-dim); cursor: pointer; flex: none;
|
|
985
|
+
font-family: var(--mono); font-size: 9px; line-height: 1; padding: 2px 5px;
|
|
986
|
+
}
|
|
987
|
+
.cv-fold:hover { color: var(--ink); border-color: var(--node-color); }
|
|
988
|
+
.cv-fold[hidden] { display: none; }
|
|
989
|
+
|
|
990
|
+
/* A folded node is a container, so it reads as one: stacked edges behind the
|
|
991
|
+
card stand in for the children it is holding. */
|
|
992
|
+
.cv-node.cv-folded { box-shadow: 0 1px 2px rgb(0 0 0 / 0.18), 6px 6px 0 -2px var(--surface-2), 6px 6px 0 -1px var(--line); }
|
|
993
|
+
.cv-node[data-kind="workflow"] { border-style: dashed; }
|
|
994
|
+
.cv-node[data-kind="workflow"] .cv-desc { font-family: var(--mono); font-size: 11px; color: var(--ink-dim); }
|
|
995
|
+
|
|
996
|
+
/* The fold control is an affordance, not decoration: when a node is holding
|
|
997
|
+
children back it says how many, in the node's own colour. */
|
|
998
|
+
.cv-node.cv-folded .cv-fold {
|
|
999
|
+
color: var(--node-color);
|
|
1000
|
+
border-color: color-mix(in srgb, var(--node-color) 45%, transparent);
|
|
1001
|
+
background: color-mix(in srgb, var(--node-color) 12%, transparent);
|
|
1002
|
+
font-weight: 600;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/* ---------------------------------------------------------- origins --- */
|
|
1006
|
+
|
|
1007
|
+
/* Which machine a row came from, shown only once more than one is in view. */
|
|
1008
|
+
.origin {
|
|
1009
|
+
margin-left: 6px; padding: 0 4px; border-radius: 4px;
|
|
1010
|
+
font-family: var(--mono); font-size: 9px; font-weight: 400;
|
|
1011
|
+
color: var(--accent);
|
|
1012
|
+
border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
|
|
1013
|
+
vertical-align: 1px;
|
|
1014
|
+
}
|
|
1015
|
+
.meta.warn { color: var(--waiting); }
|
|
1016
|
+
.ptrunc.pdown { color: var(--bad); }
|
|
1017
|
+
|
|
1018
|
+
/* ================================================== conversation ====== */
|
|
1019
|
+
|
|
1020
|
+
.stage { position: relative; min-width: 0; display: block; }
|
|
1021
|
+
.canvas { position: absolute; inset: 0; }
|
|
1022
|
+
|
|
1023
|
+
/* The inspector floats over the canvas rather than taking a column of its own:
|
|
1024
|
+
with the conversation on the right there is no third column to spare, and a
|
|
1025
|
+
node's details are something you open and dismiss. */
|
|
1026
|
+
.inspector {
|
|
1027
|
+
position: absolute; top: 0; right: 0; bottom: 0; z-index: 30;
|
|
1028
|
+
border-left: 1px solid var(--line);
|
|
1029
|
+
box-shadow: -12px 0 24px -18px rgb(0 0 0 / 0.7);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.hsplit { height: 5px; background: var(--line); cursor: row-resize; position: relative; }
|
|
1033
|
+
.hsplit::after { content: ''; position: absolute; inset: -4px 0; }
|
|
1034
|
+
.hsplit:hover, .hsplit:focus-visible, .hsplit.dragging { background: var(--accent); outline: none; }
|
|
1035
|
+
.hsplit[hidden] { display: none; }
|
|
1036
|
+
body.vresizing { cursor: row-resize; user-select: none; }
|
|
1037
|
+
|
|
1038
|
+
.chat {
|
|
1039
|
+
min-width: 0; min-height: 0;
|
|
1040
|
+
display: flex;
|
|
1041
|
+
flex-direction: column;
|
|
1042
|
+
background: var(--surface);
|
|
1043
|
+
border-left: 1px solid var(--line);
|
|
1044
|
+
}
|
|
1045
|
+
.chat[hidden] { display: none; }
|
|
1046
|
+
|
|
1047
|
+
.chat-head {
|
|
1048
|
+
display: flex; align-items: center; gap: 10px;
|
|
1049
|
+
padding: 8px 14px; border-bottom: 1px solid var(--line); flex: none;
|
|
1050
|
+
}
|
|
1051
|
+
.chat-title {
|
|
1052
|
+
font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
|
|
1053
|
+
text-transform: uppercase; color: var(--ink-faint);
|
|
1054
|
+
}
|
|
1055
|
+
.chat-state {
|
|
1056
|
+
font-family: var(--mono); font-size: 10px; color: var(--ink-dim);
|
|
1057
|
+
border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px;
|
|
1058
|
+
}
|
|
1059
|
+
.chat-state[data-state="working"] { color: var(--busy); border-color: color-mix(in srgb, var(--busy) 45%, transparent); }
|
|
1060
|
+
.chat-state[data-state="idle"] { color: var(--good); border-color: color-mix(in srgb, var(--good) 35%, transparent); }
|
|
1061
|
+
.chat-state[data-state="exited"], .chat-state[data-state="failed"] { color: var(--bad); }
|
|
1062
|
+
.chat-cost { flex: 1; font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
|
|
1063
|
+
.chat-stop { font-family: var(--mono); font-size: 11px; padding: 2px 8px; }
|
|
1064
|
+
|
|
1065
|
+
.chat-log { flex: 1; overflow-y: auto; padding: 12px 14px; min-height: 0; }
|
|
1066
|
+
|
|
1067
|
+
/* Who said it, read from which side it sits on. The role label stays, small,
|
|
1068
|
+
because a bubble tells you the side but not the name — and once a session
|
|
1069
|
+
spawns agents there is more than one voice in here. */
|
|
1070
|
+
.msg { display: flex; flex-direction: column; gap: 3px; margin-bottom: 14px; }
|
|
1071
|
+
.msg-user { align-items: flex-end; }
|
|
1072
|
+
.msg-assistant { align-items: flex-start; }
|
|
1073
|
+
|
|
1074
|
+
.msg-role {
|
|
1075
|
+
font-family: var(--mono); font-size: 9px; text-transform: uppercase;
|
|
1076
|
+
letter-spacing: 0.08em; color: var(--ink-faint); padding: 0 4px;
|
|
1077
|
+
}
|
|
1078
|
+
.msg-user .msg-role { color: var(--accent); }
|
|
1079
|
+
|
|
1080
|
+
.msg-body {
|
|
1081
|
+
max-width: 88%;
|
|
1082
|
+
min-width: 0;
|
|
1083
|
+
font-size: 13px; line-height: 1.55;
|
|
1084
|
+
border-radius: 12px;
|
|
1085
|
+
padding: 8px 12px;
|
|
1086
|
+
overflow-wrap: anywhere;
|
|
1087
|
+
}
|
|
1088
|
+
.msg-user .msg-body {
|
|
1089
|
+
background: color-mix(in srgb, var(--accent) 18%, var(--surface-2));
|
|
1090
|
+
border-bottom-right-radius: 3px;
|
|
1091
|
+
}
|
|
1092
|
+
.msg-assistant .msg-body {
|
|
1093
|
+
background: var(--surface-2);
|
|
1094
|
+
border-bottom-left-radius: 3px;
|
|
1095
|
+
}
|
|
1096
|
+
.msg-text { white-space: pre-wrap; }
|
|
1097
|
+
|
|
1098
|
+
/* A reply carrying a table or a code block needs the room; a sentence does not. */
|
|
1099
|
+
.msg-assistant .msg-body:has(.md-table, .md-code) { max-width: 100%; }
|
|
1100
|
+
|
|
1101
|
+
.msg-streaming .msg-body { color: var(--ink-dim); }
|
|
1102
|
+
.thinking { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
|
|
1103
|
+
.thinking::after { content: ''; animation: dots 1.2s steps(4, end) infinite; }
|
|
1104
|
+
@keyframes dots { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75% { content: '...'; } }
|
|
1105
|
+
|
|
1106
|
+
/* Quick replies belong to the answer above them, so they sit on its side. */
|
|
1107
|
+
.msg-assistant .quick { justify-content: flex-start; }
|
|
1108
|
+
|
|
1109
|
+
.tool-card {
|
|
1110
|
+
display: flex; gap: 8px; align-items: baseline;
|
|
1111
|
+
background: var(--surface-2); border-radius: 6px; padding: 4px 8px; margin: 4px 0;
|
|
1112
|
+
}
|
|
1113
|
+
.tool-name { font-family: var(--mono); font-size: 10px; color: var(--accent); flex: none; }
|
|
1114
|
+
.tool-label { font-size: 12px; color: var(--ink-dim); overflow-wrap: anywhere; }
|
|
1115
|
+
|
|
1116
|
+
.chat-note { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); padding: 4px 0; }
|
|
1117
|
+
.chat-note.bad { color: var(--bad); }
|
|
1118
|
+
|
|
1119
|
+
.chat-form { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--line); flex: none; }
|
|
1120
|
+
.chat-form[hidden] { display: none; }
|
|
1121
|
+
.chat-input {
|
|
1122
|
+
flex: 1; resize: none; box-sizing: border-box;
|
|
1123
|
+
background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
|
|
1124
|
+
color: var(--ink); font-family: inherit; font-size: 13px; line-height: 1.45; padding: 7px 10px;
|
|
1125
|
+
}
|
|
1126
|
+
.chat-input:focus { outline: none; border-color: var(--accent); }
|
|
1127
|
+
.chat-input:disabled { opacity: 0.5; }
|
|
1128
|
+
.chat-send {
|
|
1129
|
+
background: var(--accent); border: none; border-radius: 8px; color: #fff;
|
|
1130
|
+
cursor: pointer; font-size: 12px; font-weight: 600; padding: 0 16px; align-self: stretch;
|
|
1131
|
+
}
|
|
1132
|
+
.chat-send:hover { filter: brightness(1.1); }
|
|
1133
|
+
|
|
1134
|
+
.ghost.primary {
|
|
1135
|
+
color: var(--accent);
|
|
1136
|
+
border-color: color-mix(in srgb, var(--accent) 45%, transparent);
|
|
1137
|
+
font-size: 12px;
|
|
1138
|
+
}
|
|
1139
|
+
.ghost.primary:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); }
|
|
1140
|
+
|
|
1141
|
+
.srow.owned .sname::after {
|
|
1142
|
+
content: 'owned'; margin-left: 6px; padding: 0 4px; border-radius: 4px;
|
|
1143
|
+
font-family: var(--mono); font-size: 9px; color: var(--good);
|
|
1144
|
+
border: 1px solid color-mix(in srgb, var(--good) 35%, transparent);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/* `display: grid` outranks the UA's `[hidden] { display: none }`, so the empty
|
|
1148
|
+
note kept painting over a graph that had nodes. Same trap as .inspector. */
|
|
1149
|
+
.cv-empty[hidden] { display: none; }
|
|
1150
|
+
|
|
1151
|
+
/* ------------------------------------------------------- permissions --- */
|
|
1152
|
+
|
|
1153
|
+
.perm-queue { flex: none; border-top: 1px solid var(--line); padding: 10px 14px; max-height: 46vh; overflow-y: auto; }
|
|
1154
|
+
.perm-queue[hidden] { display: none; }
|
|
1155
|
+
|
|
1156
|
+
.perm {
|
|
1157
|
+
border: 1px solid color-mix(in srgb, var(--waiting) 45%, var(--line));
|
|
1158
|
+
background: color-mix(in srgb, var(--waiting) 7%, transparent);
|
|
1159
|
+
border-radius: 9px; padding: 9px 11px; margin-bottom: 8px;
|
|
1160
|
+
}
|
|
1161
|
+
.perm-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
|
|
1162
|
+
.perm-tool {
|
|
1163
|
+
font-family: var(--mono); font-size: 11px; font-weight: 600;
|
|
1164
|
+
letter-spacing: 0.05em; text-transform: uppercase; color: var(--waiting);
|
|
1165
|
+
}
|
|
1166
|
+
.perm-clock { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
|
|
1167
|
+
.perm-detail {
|
|
1168
|
+
margin: 7px 0 0; padding: 7px 9px; border-radius: 6px;
|
|
1169
|
+
background: var(--surface-2); font-family: var(--mono); font-size: 11.5px;
|
|
1170
|
+
line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
1171
|
+
max-height: 140px; overflow-y: auto;
|
|
1172
|
+
}
|
|
1173
|
+
.perm-acts { display: flex; gap: 7px; margin-top: 9px; flex-wrap: wrap; }
|
|
1174
|
+
.perm-btn {
|
|
1175
|
+
background: transparent; border: 1px solid var(--line); border-radius: 7px;
|
|
1176
|
+
color: var(--ink-dim); cursor: pointer; font-size: 12px; padding: 4px 11px;
|
|
1177
|
+
}
|
|
1178
|
+
.perm-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--ink-faint); }
|
|
1179
|
+
.perm-btn:disabled { opacity: 0.4; cursor: default; }
|
|
1180
|
+
.perm-btn.ok { color: var(--good); border-color: color-mix(in srgb, var(--good) 40%, transparent); }
|
|
1181
|
+
.perm-btn.bad { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
|
|
1182
|
+
|
|
1183
|
+
.chat-state.ungated { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 50%, transparent); }
|
|
1184
|
+
|
|
1185
|
+
/* ------------------------------------------------------ quick replies --- */
|
|
1186
|
+
|
|
1187
|
+
.quick { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 8px; }
|
|
1188
|
+
.quick-hint {
|
|
1189
|
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.05em;
|
|
1190
|
+
text-transform: uppercase; color: var(--ink-faint);
|
|
1191
|
+
}
|
|
1192
|
+
.quick-btn {
|
|
1193
|
+
background: transparent; border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent);
|
|
1194
|
+
border-radius: 999px; color: var(--accent); cursor: pointer;
|
|
1195
|
+
font-size: 12px; padding: 3px 12px; max-width: 100%;
|
|
1196
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1197
|
+
}
|
|
1198
|
+
.quick-btn:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
|
|
1199
|
+
|
|
1200
|
+
/* --------------------------------------------------------- session tabs --- */
|
|
1201
|
+
|
|
1202
|
+
.panes { flex: 1; min-height: 0; display: flex; }
|
|
1203
|
+
.pane { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
1204
|
+
.pane[hidden] { display: none; }
|
|
1205
|
+
|
|
1206
|
+
.tabs { display: flex; gap: 3px; flex: 1; overflow-x: auto; align-items: center; }
|
|
1207
|
+
.tab {
|
|
1208
|
+
display: flex; align-items: center; gap: 6px;
|
|
1209
|
+
background: transparent; border: 1px solid var(--line); border-radius: 7px;
|
|
1210
|
+
color: var(--ink-dim); cursor: pointer; font-family: var(--mono); font-size: 11px;
|
|
1211
|
+
padding: 3px 7px; white-space: nowrap; flex: none;
|
|
1212
|
+
}
|
|
1213
|
+
.tab:hover { color: var(--ink); }
|
|
1214
|
+
.tab.on { color: var(--ink); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
1215
|
+
|
|
1216
|
+
.tab-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--idle); flex: none; }
|
|
1217
|
+
.tab-dot[data-state="working"] { background: var(--busy); animation: breathe 1.6s ease-in-out infinite; }
|
|
1218
|
+
.tab-dot[data-state="idle"] { background: var(--good); }
|
|
1219
|
+
.tab-dot[data-state="starting"]{ background: var(--accent); }
|
|
1220
|
+
.tab-dot[data-state="exited"], .tab-dot[data-state="failed"] { background: var(--bad); }
|
|
1221
|
+
|
|
1222
|
+
.tab-badge {
|
|
1223
|
+
background: var(--surface-2); border-radius: 999px; padding: 0 5px;
|
|
1224
|
+
font-size: 9px; color: var(--ink-faint);
|
|
1225
|
+
}
|
|
1226
|
+
.tab-badge.warn { background: color-mix(in srgb, var(--waiting) 22%, transparent); color: var(--waiting); }
|
|
1227
|
+
.tab-x { color: var(--ink-faint); font-size: 13px; line-height: 1; padding: 0 1px; }
|
|
1228
|
+
.tab-x:hover { color: var(--bad); }
|
|
1229
|
+
|
|
1230
|
+
@media (prefers-reduced-motion: reduce) { .tab-dot[data-state="working"] { animation: none; } }
|
|
1231
|
+
|
|
1232
|
+
/* -------------------------------------------------------- kit telemetry --- */
|
|
1233
|
+
|
|
1234
|
+
.kit-sum { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px; }
|
|
1235
|
+
.kit-sum .cv-bit[data-verdict="BLOCK"] { color: var(--bad); }
|
|
1236
|
+
.kit-sum .cv-bit[data-verdict="ASK"] { color: var(--waiting); }
|
|
1237
|
+
.kit-sum .cv-bit[data-verdict="ALLOW"] { color: var(--good); }
|
|
1238
|
+
|
|
1239
|
+
.gate-row {
|
|
1240
|
+
display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: baseline;
|
|
1241
|
+
padding: 4px 0; border-bottom: 1px solid var(--line); font-size: 12px;
|
|
1242
|
+
}
|
|
1243
|
+
.gate-when { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
|
|
1244
|
+
.gate-name { color: var(--ink-dim); overflow-wrap: anywhere; }
|
|
1245
|
+
.gate-sec { font-family: var(--mono); font-size: 10px; color: var(--ink-faint); }
|
|
1246
|
+
.gate-verdict {
|
|
1247
|
+
font-family: var(--mono); font-size: 9px; letter-spacing: 0.05em;
|
|
1248
|
+
border: 1px solid var(--line); border-radius: 4px; padding: 0 5px; white-space: nowrap;
|
|
1249
|
+
}
|
|
1250
|
+
.gate-verdict[data-verdict="BLOCK"] { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
|
|
1251
|
+
.gate-verdict[data-verdict="ASK"] { color: var(--waiting); border-color: color-mix(in srgb, var(--waiting) 40%, transparent); }
|
|
1252
|
+
.gate-verdict[data-verdict="ALLOW"] { color: var(--good); border-color: color-mix(in srgb, var(--good) 35%, transparent); }
|
|
1253
|
+
|
|
1254
|
+
.inspector dd.bad { color: var(--bad); }
|
|
1255
|
+
.ihint strong { display: block; color: var(--waiting); margin-bottom: 3px; }
|
|
1256
|
+
|
|
1257
|
+
/* A node that holds others opens on click, so it says so before you try. */
|
|
1258
|
+
.cv-node.cv-container { cursor: zoom-in; }
|
|
1259
|
+
.cv-node.cv-container:not(.cv-folded) { cursor: zoom-out; }
|
|
1260
|
+
.cv-node.cv-container.cv-folded .cv-desc::after {
|
|
1261
|
+
content: ' — click to open';
|
|
1262
|
+
color: var(--ink-faint);
|
|
1263
|
+
font-size: 11px;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/* --------------------------------------------------- reachable elsewhere --- */
|
|
1267
|
+
|
|
1268
|
+
.reach-head { border-top: 1px solid var(--line); padding-top: 10px; }
|
|
1269
|
+
.reach-head[hidden] { display: none; }
|
|
1270
|
+
#reach { max-height: 24vh; }
|
|
1271
|
+
/* Listed, not openable: the transcript is on the other machine. */
|
|
1272
|
+
.reach-row { cursor: default; opacity: 0.92; }
|
|
1273
|
+
.reach-row .name { font-size: 13px; }
|
|
1274
|
+
|
|
1275
|
+
/* ------------------------------------------------ history and read-only --- */
|
|
1276
|
+
|
|
1277
|
+
/* The seam between what was said before a fork and what is being said now. */
|
|
1278
|
+
.chat-seam {
|
|
1279
|
+
display: flex; align-items: center; gap: 10px;
|
|
1280
|
+
margin: 14px 0; font-family: var(--mono); font-size: 10px;
|
|
1281
|
+
letter-spacing: 0.05em; color: var(--ink-faint); text-transform: uppercase;
|
|
1282
|
+
}
|
|
1283
|
+
.chat-seam::before, .chat-seam::after {
|
|
1284
|
+
content: ''; flex: 1; height: 1px; background: var(--line);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
.chat-ro {
|
|
1288
|
+
flex: none; padding: 9px 14px; border-top: 1px solid var(--line);
|
|
1289
|
+
font-size: 12px; line-height: 1.45; color: var(--ink-dim);
|
|
1290
|
+
background: var(--surface-2);
|
|
1291
|
+
}
|
|
1292
|
+
.chat-ro[hidden] { display: none; }
|
|
1293
|
+
.chat-ro strong { color: var(--accent); font-weight: 600; }
|
|
1294
|
+
|
|
1295
|
+
.chat-state[data-state="observed"] { color: var(--ink-dim); }
|
|
1296
|
+
|
|
1297
|
+
/* --------------------------------------------------- panel controls --- */
|
|
1298
|
+
|
|
1299
|
+
.ghost.pin { padding: 1px 6px; font-size: 11px; line-height: 1.4; }
|
|
1300
|
+
.side-head { position: relative; }
|
|
1301
|
+
.side-head h2 { flex: 1; }
|
|
1302
|
+
|
|
1303
|
+
/* The chat divider only exists while there is a conversation beside it. */
|
|
1304
|
+
#chat-split[hidden] { display: none; }
|
|
1305
|
+
|
|
1306
|
+
.chat-wide { position: relative; }
|
|
1307
|
+
.chat-head .chat-grow { margin-left: 2px; }
|
|
1308
|
+
|
|
1309
|
+
@media (max-width: 1100px) {
|
|
1310
|
+
/* Two side panels and a graph do not fit; the conversation wins because it is
|
|
1311
|
+
the thing being read. */
|
|
1312
|
+
.shell { grid-template-columns: 0 0 1fr 5px var(--chat-w, 420px); }
|
|
1313
|
+
.shell > .side, .shell > #resizer { display: none; }
|
|
1314
|
+
}
|
|
1315
|
+
@media (max-width: 760px) {
|
|
1316
|
+
.shell, .shell.no-chat, .shell.no-side { grid-template-columns: 1fr; }
|
|
1317
|
+
.shell > .side, .shell > #resizer, .shell > #chat-split { display: none; }
|
|
1318
|
+
.inspector { display: none; }
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/* A live session is opened from the fleet the same way it is from the list. */
|
|
1322
|
+
.fleet-row.clickable { cursor: pointer; }
|
|
1323
|
+
.fleet-row.clickable:hover { background: var(--surface-2); }
|
|
1324
|
+
.fleet-row[aria-current="true"] { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--good); }
|
|
1325
|
+
|
|
1326
|
+
/* ============================================================ scrollbars === */
|
|
1327
|
+
|
|
1328
|
+
/* The browser default is a light, wide bar that sits on top of a dark panel
|
|
1329
|
+
like a seam. These are thin, dim until touched, and drawn from the theme so
|
|
1330
|
+
they belong to the panel rather than to the window. */
|
|
1331
|
+
* {
|
|
1332
|
+
scrollbar-width: thin; /* Firefox */
|
|
1333
|
+
scrollbar-color: var(--line) transparent;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
*::-webkit-scrollbar { width: 9px; height: 9px; }
|
|
1337
|
+
*::-webkit-scrollbar-track { background: transparent; }
|
|
1338
|
+
*::-webkit-scrollbar-thumb {
|
|
1339
|
+
background: var(--line);
|
|
1340
|
+
border-radius: 999px;
|
|
1341
|
+
/* A transparent border with background-clip leaves the thumb visually
|
|
1342
|
+
narrower than its hit area, so it is easy to grab and quiet to look at. */
|
|
1343
|
+
border: 2px solid transparent;
|
|
1344
|
+
background-clip: content-box;
|
|
1345
|
+
}
|
|
1346
|
+
*::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); background-clip: content-box; }
|
|
1347
|
+
*::-webkit-scrollbar-corner { background: transparent; }
|
|
1348
|
+
|
|
1349
|
+
/* Where two scrollable panes meet, the bar should not read as a third border. */
|
|
1350
|
+
.side *::-webkit-scrollbar, .chat *::-webkit-scrollbar, .inspector *::-webkit-scrollbar { width: 8px; }
|
|
1351
|
+
|
|
1352
|
+
/* ------------------------------------------------------- sidebar rail --- */
|
|
1353
|
+
|
|
1354
|
+
/* The control that brings the sidebar back sits where the sidebar was. It
|
|
1355
|
+
started in the header beside the theme toggle — the far corner from the edge
|
|
1356
|
+
it acts on — and was, fairly, hard to find. */
|
|
1357
|
+
.side-rail {
|
|
1358
|
+
position: absolute; left: 0; top: 0; bottom: 0; z-index: 25;
|
|
1359
|
+
width: 22px;
|
|
1360
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
1361
|
+
gap: 10px;
|
|
1362
|
+
background: var(--surface);
|
|
1363
|
+
border: none; border-right: 1px solid var(--line);
|
|
1364
|
+
color: var(--ink-faint);
|
|
1365
|
+
cursor: pointer;
|
|
1366
|
+
padding: 0;
|
|
1367
|
+
}
|
|
1368
|
+
.side-rail[hidden] { display: none; }
|
|
1369
|
+
.side-rail:hover { background: var(--surface-2); color: var(--ink); border-right-color: var(--accent); }
|
|
1370
|
+
.side-rail:focus-visible { outline: none; border-right-color: var(--accent); color: var(--ink); }
|
|
1371
|
+
|
|
1372
|
+
.side-rail-arrow { font-size: 15px; line-height: 1; }
|
|
1373
|
+
/* Turned along the rail, so the strip says what it opens rather than being an
|
|
1374
|
+
unlabelled sliver. */
|
|
1375
|
+
.side-rail-label {
|
|
1376
|
+
writing-mode: vertical-rl;
|
|
1377
|
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em;
|
|
1378
|
+
text-transform: uppercase;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/* The rail takes the space the sidebar left, so the graph does not sit under it. */
|
|
1382
|
+
.shell.no-side { grid-template-columns: 22px 0 1fr 5px var(--chat-w, 420px); }
|
|
1383
|
+
.shell.no-side.no-chat { grid-template-columns: 22px 0 1fr 0 0; }
|
|
1384
|
+
|
|
1385
|
+
@media (max-width: 760px) { .side-rail { display: none; } }
|
|
1386
|
+
|
|
1387
|
+
/* --------------------------------------------------------- full screen --- */
|
|
1388
|
+
|
|
1389
|
+
#fullscreen.on {
|
|
1390
|
+
color: var(--accent);
|
|
1391
|
+
border-color: color-mix(in srgb, var(--accent) 45%, transparent);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/* In full screen the window chrome is gone, so the panel's own top edge is the
|
|
1395
|
+
only frame left; it should not look like a floating bar. */
|
|
1396
|
+
:fullscreen .bar { border-top: none; }
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
/* ---------------------------------------------------------- node mark --- */
|
|
1400
|
+
|
|
1401
|
+
.cv-mark { width: 14px; height: 14px; flex: none; color: var(--node-color); }
|
|
1402
|
+
/* Both logos carry their own colours; only the generic glyphs take the node's. */
|
|
1403
|
+
.cv-node[data-source="kit"] .cv-mark,
|
|
1404
|
+
.cv-node[data-source="builtin"] .cv-mark { color: inherit; }
|
|
1405
|
+
/* A type the palette does not know keeps a neutral mark, like its colour. */
|
|
1406
|
+
.cv-mark.cv-mark-unknown { color: var(--ink-faint); opacity: 0.7; }
|
|
1407
|
+
.cv-node[data-source="kit"] .cv-mark { opacity: 1; }
|
|
1408
|
+
.cv-node[data-source="builtin"] .cv-mark { opacity: 0.85; }
|
|
1409
|
+
|
|
1410
|
+
/* ------------------------------------------------- tab bar that fits --- */
|
|
1411
|
+
|
|
1412
|
+
/* A tab has to give way before the close control does. Without min-width:0 a
|
|
1413
|
+
flex item refuses to shrink below its content, so a long session title pushed
|
|
1414
|
+
the × out of the panel and the only way to close a tab was to widen the
|
|
1415
|
+
panel first. */
|
|
1416
|
+
.chat-head { min-width: 0; }
|
|
1417
|
+
.tabs { min-width: 0; }
|
|
1418
|
+
.tab { min-width: 0; max-width: 190px; }
|
|
1419
|
+
.tab-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1420
|
+
.tab-dot, .tab-x, .tab-badge { flex: none; }
|
|
1421
|
+
|
|
1422
|
+
/* The status and the controls keep their size; the tabs are what scrolls. */
|
|
1423
|
+
.chat-state, .chat-cost, .chat-stop, .chat-grow { flex: none; }
|
|
1424
|
+
.chat-cost { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
1425
|
+
|
|
1426
|
+
/* Below this the status text is what has to go, not the way out of a tab. */
|
|
1427
|
+
@media (max-width: 1400px) {
|
|
1428
|
+
.chat-head .chat-cost { display: none; }
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/* ------------------------------------------------ foldable side sections */
|
|
1432
|
+
|
|
1433
|
+
.twisty {
|
|
1434
|
+
flex: none; width: 16px; height: 16px; padding: 0; margin-right: -2px;
|
|
1435
|
+
display: grid; place-items: center;
|
|
1436
|
+
background: none; border: 0; cursor: pointer; border-radius: 4px;
|
|
1437
|
+
font-size: 10px; line-height: 1; color: var(--ink-faint);
|
|
1438
|
+
transition: background 0.12s, color 0.12s;
|
|
1439
|
+
}
|
|
1440
|
+
.twisty:hover { background: var(--surface-2); color: var(--ink); }
|
|
1441
|
+
.twisty:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
1442
|
+
|
|
1443
|
+
.side-head [data-fold-for] { cursor: pointer; user-select: none; }
|
|
1444
|
+
.side-head [data-fold-for]:hover { color: var(--ink); }
|
|
1445
|
+
|
|
1446
|
+
/* Two selector classes, so this outranks the `display` these bodies get from
|
|
1447
|
+
.fleet / .sessions / .side-filter. A single .is-folded would tie and lose. */
|
|
1448
|
+
[data-fold-body].is-folded { display: none; }
|
|
1449
|
+
|
|
1450
|
+
/* A folded section stops competing for the leftover height: without this the
|
|
1451
|
+
sidebar keeps a tall blank gap where the project tree used to be. */
|
|
1452
|
+
.side-block.folded { flex: none; }
|
|
1453
|
+
.side-block.grow.folded { border-bottom: 1px solid var(--line); }
|