@vllm-sr/dsh 0.4.0 → 0.4.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/README.md +42 -7
- package/package.json +2 -2
- package/src/index.js +5 -4
- package/src/records.js +22 -0
- package/src/tool.js +21 -3
- package/web/client.js +86 -72
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# @vllm-sr/dsh
|
|
2
2
|
|
|
3
|
-
**System Duo** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): *Thinking, fast and slow
|
|
4
|
-
key.*
|
|
3
|
+
**System Duo** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): *Thinking, fast and slow.*
|
|
5
4
|
|
|
6
5
|
An agent's LLM thinks slowly, in tokens. System One, a decision model, answers the many small questions around it in
|
|
7
6
|
about half a second each, with calibrated probabilities: is this command dangerous, does this file try to give the
|
|
@@ -18,12 +17,14 @@ answers at `https://app.vllm-sr.ai/v1/systemone`, and `vllm-sr/auto` at `https:/
|
|
|
18
17
|
## Quick start
|
|
19
18
|
|
|
20
19
|
```bash
|
|
21
|
-
npm install -g @deepseek-ai/dsh
|
|
20
|
+
npm install -g @deepseek-ai/dsh pnpm
|
|
22
21
|
dsh plugin --profile web add @vllm-sr/dsh
|
|
23
22
|
dsh web
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
The
|
|
25
|
+
The first line installs DeepSeek Harness (0.2.1-rc.5 or a later 0.2 release, on Node 24.9 or later) and pnpm, which
|
|
26
|
+
`dsh plugin` uses to install plugins; without pnpm, `dsh plugin add` stops with "pnpm was not found". On a machine
|
|
27
|
+
with git, the package can also come straight from GitHub: `dsh plugin --profile web add github:Xunzhuo/dsh-system-one-plugin`.
|
|
27
28
|
|
|
28
29
|
On its first start the plugin gets one free System Duo key, with the same small proof of work the site's "Get your
|
|
29
30
|
API key" button runs in the browser, and saves it in DeepSeek Harness's credential store and in `~/.dsh/.env` (readable
|
|
@@ -75,9 +76,43 @@ the work folds, so a finished turn still shows what System One decided. Parallel
|
|
|
75
76
|
card, and every decision pops in with a short animation the first time it appears (none with reduced motion). Each
|
|
76
77
|
element shows the Decision API's own answer: the probabilities, the model and the measured latency.
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
Set `systemOneUi` to `compact` for one line per decision, or to `off` to hide them.
|
|
80
|
+
|
|
81
|
+
### Selectors
|
|
82
|
+
|
|
83
|
+
For tests and screen recordings, each System One element in Chat is one root marked `data-system-one`, with stable
|
|
84
|
+
attributes that say what it shows:
|
|
85
|
+
|
|
86
|
+
| Attribute | Values |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `data-system-one` | `card`, `compact` (one line) or `tool` (the `system_one` row) |
|
|
89
|
+
| `data-system-one-reflex` | `triage`, `guard`, `shield`, `done` or `tool` |
|
|
90
|
+
| `data-system-one-verdict` | triage `brief`; guard `allow`, `ask` or `deny`; shield `clean`, `removed` or `flagged`; done-check `stop` or `continue`; tool `running` or `done`; `error` when System One did not answer |
|
|
91
|
+
| `data-system-one-outcome` | guard, once you answer its approval: `allowed`, `rejected`, `cancelled` or `failed` |
|
|
92
|
+
| `data-system-one-why` | done-check: `complete`, `waiting`, `unfinished` or `unchecked` |
|
|
93
|
+
| `data-system-one-tone` | its colour: `ok` (green), `info` (ember), `warn` (amber), `bad` (red) or `muted` |
|
|
94
|
+
| `data-system-one-count` | how many decisions it holds (a batch: how many items) |
|
|
95
|
+
| `data-system-one-turn`, `data-system-one-step` | where it acted (the step for guard and shield) |
|
|
96
|
+
|
|
97
|
+
Inside a root, `data-system-one-part` marks `badge` (the ember **System One** pill), `latency`, `reflex`, `summary`,
|
|
98
|
+
`model`, `head`, `toggle` (the button of a one-line element), `rows`, `verdict`, `outcome`, `subject`, `facts`,
|
|
99
|
+
`note` and `more`, and in the batch row also `stack`, `legend`, `failed`, `results` and `raw`. Each number is a
|
|
100
|
+
`data-system-one-fact`: `kind`, `effort`, `unclear` and `risky` (triage), `risk`, `leaks` and `on-task` (guard),
|
|
101
|
+
`passages` and `injection` (shield), `complete`, `checked` and `waiting` (done-check). Decision rows carry
|
|
102
|
+
`data-system-one-decision` and `data-system-one-call`, batch questions `data-system-one-question`. For example:
|
|
103
|
+
|
|
104
|
+
| Element | Selector |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| Triage card | `[data-system-one-reflex="triage"]` |
|
|
107
|
+
| Guard card that asked you (amber) | `[data-system-one-reflex="guard"][data-system-one-verdict="ask"]` |
|
|
108
|
+
| The same card once you rejected the call (red) | `[data-system-one-reflex="guard"][data-system-one-outcome="rejected"]` |
|
|
109
|
+
| Injection removed (red) | `[data-system-one-reflex="shield"][data-system-one-verdict="removed"]` |
|
|
110
|
+
| A routine check (one line) | `[data-system-one="compact"][data-system-one-reflex="guard"]`, and `"shield"` |
|
|
111
|
+
| Done-check | `[data-system-one-reflex="done"]` |
|
|
112
|
+
| Batch row, finished | `[data-system-one="tool"][data-system-one-verdict="done"]` |
|
|
113
|
+
| The ember badge, and its latency | `[data-system-one-part="badge"]`, `[data-system-one-part="latency"]` |
|
|
114
|
+
| "Dangerous", and "You rejected it" | `[data-system-one-fact="risk"]`, `[data-system-one-part="outcome"]` |
|
|
115
|
+
| The reason | `[data-system-one-part="note"]` |
|
|
81
116
|
|
|
82
117
|
## Your data
|
|
83
118
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vllm-sr/dsh",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "System Duo for DeepSeek Harness. Thinking, fast and slow
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "System Duo for DeepSeek Harness. Thinking, fast and slow: System One decision models guard, shield, triage and check your agent around System Two (vllm-sr/auto), with one free app.vllm-sr.ai key.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import * as dshSession from '@deepseek-ai/dsh-session'
|
|
|
21
21
|
import { createDecisionClient, compactAnswer, yes } from './client.js'
|
|
22
22
|
import { SERVICES, appendDotenv, ensureKeys, readDotenv } from './keys.js'
|
|
23
23
|
import { DONE, EFFORT_LEVELS, GUARD, RISK_LEVELS, TRIAGE } from './questions.js'
|
|
24
|
-
import { UI_MODES, approvalOutcome, createRecorder, subject } from './records.js'
|
|
24
|
+
import { UI_MODES, approvalOutcome, createRecorder, relativeTo, subject } from './records.js'
|
|
25
25
|
import { screen } from './shield.js'
|
|
26
26
|
import { systemOneTool } from './tool.js'
|
|
27
27
|
import { clip, createTrace } from './trace.js'
|
|
@@ -335,12 +335,13 @@ export function apply(ctx, config) {
|
|
|
335
335
|
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
336
336
|
if (!gated.has(exec.name) || !exec.agent) return next()
|
|
337
337
|
const state = sessionState(exec.agent)
|
|
338
|
+
const workspace = await workspaceOf(exec)
|
|
338
339
|
const situation = [
|
|
339
340
|
`The user asked the agent:\n"""${clip(state?.user || '(unknown)', 1500)}"""`,
|
|
340
|
-
`Workspace: ${
|
|
341
|
+
`Workspace: ${workspace}`,
|
|
341
342
|
`The agent now wants to run:\n${describeCall(exec)}`,
|
|
342
343
|
].join('\n\n')
|
|
343
|
-
const base = { reflex: 'guard', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(briefCall(exec)) }
|
|
344
|
+
const base = { reflex: 'guard', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(relativeTo(briefCall(exec), workspace, homedir())) }
|
|
344
345
|
let response
|
|
345
346
|
try {
|
|
346
347
|
response = await client.ask({ model: config.model, questions: GUARD, inputs: [{ state: situation }], signal: exec.signal, timeoutMs: config.hookTimeoutMs, retries: config.hookRetries })
|
|
@@ -414,7 +415,7 @@ export function apply(ctx, config) {
|
|
|
414
415
|
const tail = text.slice(screened.length)
|
|
415
416
|
const where = exec.name === 'web_fetch' ? `a web page (${exec.arguments?.url})` : exec.name === 'read' ? `the file ${exec.arguments?.file_path}` : `the output of: ${clip(exec.arguments?.command, 200)}`
|
|
416
417
|
const prefix = `The user asked the agent: "${clip(state?.user || '(unknown)', 300)}"\nThe agent just read this passage from ${where}:`
|
|
417
|
-
const base = { reflex: 'shield', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(briefCall(exec)) }
|
|
418
|
+
const base = { reflex: 'shield', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(relativeTo(briefCall(exec), await workspaceOf(exec), homedir())) }
|
|
418
419
|
const started = performance.now()
|
|
419
420
|
let verdict
|
|
420
421
|
try {
|
package/src/records.js
CHANGED
|
@@ -20,6 +20,28 @@ export function subject(text, max = 200) {
|
|
|
20
20
|
return redact(clip(text, max))
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const escaped = text => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
24
|
+
// a path starts after a separator and ends at one, so /work does not match inside /work2 or /x/work
|
|
25
|
+
const START = String.raw`(?<=^|[\s'"=:(])`
|
|
26
|
+
const END = String.raw`(?=$|[\s'"/;&|)<>,])`
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A call as the cards show it: paths in the session's working directory relative to it, the home directory as `~`,
|
|
30
|
+
* and no leading `cd` into the working directory itself.
|
|
31
|
+
*/
|
|
32
|
+
export function relativeTo(text, workspace, home) {
|
|
33
|
+
let out = String(text ?? '')
|
|
34
|
+
const root = dir => (typeof dir === 'string' && dir.replace(/\/+$/, '').length > 1 ? escaped(dir.replace(/\/+$/, '')) : undefined)
|
|
35
|
+
const work = root(workspace)
|
|
36
|
+
if (work) {
|
|
37
|
+
out = out.replace(new RegExp(`${START}${work}/`, 'g'), '').replace(new RegExp(`${START}${work}${END}`, 'g'), '.')
|
|
38
|
+
out = out.replace(/^cd \.\s*&&\s*/, '')
|
|
39
|
+
}
|
|
40
|
+
const own = root(home)
|
|
41
|
+
if (own) out = out.replace(new RegExp(`${START}${own}${END}`, 'g'), '~')
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
/**
|
|
24
46
|
* @param {object} options
|
|
25
47
|
* @param {((session: object, type: string, data: object) => unknown) | undefined} options.append - DeepSeek
|
package/src/tool.js
CHANGED
|
@@ -12,20 +12,38 @@ You write the rubric as questions; System One answers every question for every i
|
|
|
12
12
|
|
|
13
13
|
Question types:
|
|
14
14
|
- "choice": pick one option. criteria: an object of option name -> short description (2 or more options).
|
|
15
|
-
- "noul": yes or no. The answer is the probability of yes.
|
|
15
|
+
- "noul": yes or no, with no criteria: say in the instructions what counts as yes. The answer is the probability of yes.
|
|
16
16
|
- "score": an ordered scale. criteria: a list of level descriptions, lowest first (2 to 10). The answer is the expected level (0 = first).
|
|
17
17
|
- "box": where something is in an image (images only); instructions name the target.
|
|
18
18
|
|
|
19
19
|
Example questions: {"team": {"type": "choice", "instructions": "Which team should handle this ticket?", "criteria": {"billing": "Payments, invoices, refunds", "engineering": "Bugs, errors, outages", "sales": "Plans and pricing"}}, "urgency": {"type": "score", "instructions": "How urgent is this ticket?", "criteria": ["Low", "Normal", "Urgent"]}, "angry": {"type": "noul", "instructions": "Is the customer angry?"}}`
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** The type a question's criteria imply: a list of levels is a score, named options a choice, anything else yes/no. */
|
|
22
|
+
function impliedType(criteria) {
|
|
23
|
+
if (Array.isArray(criteria)) return 'score'
|
|
24
|
+
if (!criteria || typeof criteria !== 'object') return 'noul'
|
|
25
|
+
return Object.keys(criteria).map(name => name.toLowerCase()).sort().join() === 'no,yes' ? 'noul' : 'choice'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseQuestions(value) {
|
|
22
29
|
const questions = typeof value === 'string' ? JSON.parse(value) : value
|
|
23
30
|
if (!questions || typeof questions !== 'object' || Array.isArray(questions) || Object.keys(questions).length === 0) {
|
|
24
31
|
throw new Error('questions must be an object of question id -> {type, instructions, criteria}')
|
|
25
32
|
}
|
|
26
|
-
for (const [id,
|
|
33
|
+
for (const [id, asked] of Object.entries(questions)) {
|
|
34
|
+
// System Two often leaves the type out of its first call
|
|
35
|
+
const question = asked && typeof asked === 'object' && asked.type === undefined ? { ...asked, type: impliedType(asked.criteria) } : asked
|
|
36
|
+
questions[id] = question
|
|
27
37
|
if (!['choice', 'noul', 'score', 'box'].includes(question?.type)) throw new Error(`question "${id}" needs type choice, noul, score or box`)
|
|
28
38
|
if (typeof question.instructions !== 'string' || !question.instructions.trim()) throw new Error(`question "${id}" needs instructions`)
|
|
39
|
+
// the Decision API does not answer a noul question that carries criteria
|
|
40
|
+
if (question.type === 'noul' && question.criteria !== undefined) {
|
|
41
|
+
const { criteria, ...rest } = question
|
|
42
|
+
const rules = typeof criteria === 'string' ? [criteria]
|
|
43
|
+
: Array.isArray(criteria) ? criteria.map(String)
|
|
44
|
+
: criteria && typeof criteria === 'object' ? Object.entries(criteria).map(([name, rule]) => `${name}: ${rule}`) : []
|
|
45
|
+
questions[id] = { ...rest, instructions: [rest.instructions.trim(), ...rules].join('\n') }
|
|
46
|
+
}
|
|
29
47
|
}
|
|
30
48
|
return questions
|
|
31
49
|
}
|
package/web/client.js
CHANGED
|
@@ -171,10 +171,10 @@ window.__ModuleLoader__.load({
|
|
|
171
171
|
verdict: 'brief', label: kindLabel, short: kindLabel.toLowerCase(), tone: 'info',
|
|
172
172
|
headline: [`${kindLabel} work`, `${effortLabel.toLowerCase()} effort`, ...flags].join(' · '),
|
|
173
173
|
facts: [
|
|
174
|
-
{ label: 'Kind', value: `${kindLabel} ${p2(confidence)}`, bar: confidence },
|
|
175
|
-
{ label: 'Effort', value: effort === undefined ? '?' : `${effortLabel} · ${effort.toFixed(1)} of 3`, bar: effort === undefined ? undefined : effort / 3 },
|
|
176
|
-
{ label: 'Unclear', value: p2(unclear), bar: unclear, tone: unclear >= 0.6 ? 'warn' : undefined },
|
|
177
|
-
{ label: 'Risky', value: p2(risky), bar: risky, tone: risky >= 0.5 ? 'bad' : undefined },
|
|
174
|
+
{ id: 'kind', label: 'Kind', value: `${kindLabel} ${p2(confidence)}`, bar: confidence },
|
|
175
|
+
{ id: 'effort', label: 'Effort', value: effort === undefined ? '?' : `${effortLabel} · ${effort.toFixed(1)} of 3`, bar: effort === undefined ? undefined : effort / 3 },
|
|
176
|
+
{ id: 'unclear', label: 'Unclear', value: p2(unclear), bar: unclear, tone: unclear >= 0.6 ? 'warn' : undefined },
|
|
177
|
+
{ id: 'risky', label: 'Risky', value: p2(risky), bar: risky, tone: risky >= 0.5 ? 'bad' : undefined },
|
|
178
178
|
],
|
|
179
179
|
note: hints.length ? `Brief for System Two: ${hints.join(' ')}` : undefined,
|
|
180
180
|
}
|
|
@@ -184,21 +184,21 @@ window.__ModuleLoader__.load({
|
|
|
184
184
|
const outcome = OUTCOME[row.outcome]
|
|
185
185
|
if (row.error) {
|
|
186
186
|
const fallback = { allow: { text: 'so the call ran', tone: 'muted' }, ask: { text: 'so you were asked', tone: 'warn' }, deny: { text: 'so the call was blocked', tone: 'bad' } }[row.verdict]
|
|
187
|
-
return { ...errorView(row, fallback), outcome }
|
|
187
|
+
return { ...errorView(row, fallback), outcome, outcomeKey: outcome ? row.outcome : undefined }
|
|
188
188
|
}
|
|
189
189
|
const subject = { tool: row.tool, text: row.call }
|
|
190
190
|
const verdict = GUARD[row.verdict]
|
|
191
|
-
if (!verdict) return { verdict: row.outcome ?? 'unknown', ...(outcome ?? { label: 'Reviewed', short: 'reviewed', tone: 'muted' }), subject, facts: [] }
|
|
191
|
+
if (!verdict) return { verdict: row.outcome ?? 'unknown', ...(outcome ?? { label: 'Reviewed', short: 'reviewed', tone: 'muted' }), outcomeKey: outcome ? row.outcome : undefined, subject, facts: [] }
|
|
192
192
|
const a = row.answers ?? {}
|
|
193
193
|
const risk = scoreOf(a.risk)
|
|
194
194
|
const leaks = yesOf(a.exfiltration)
|
|
195
195
|
const onTask = yesOf(a.on_task)
|
|
196
196
|
const facts = []
|
|
197
|
-
if (risk !== undefined) facts.push({ label: 'Risk', value: `${level(RISK, Math.min(3, risk))} · ${risk.toFixed(2)} of 3`, bar: risk / 3, tone: risk >= 2.5 ? 'bad' : risk >= 1.8 ? 'warn' : 'ok' })
|
|
198
|
-
if (leaks !== undefined) facts.push({ label: 'Leaks secrets', value: p2(leaks), bar: leaks, tone: leaks >= 0.7 ? 'bad' : undefined })
|
|
199
|
-
if (onTask !== undefined) facts.push({ label: 'On task', value: p2(onTask), bar: onTask, tone: onTask < 0.15 ? 'warn' : undefined })
|
|
197
|
+
if (risk !== undefined) facts.push({ id: 'risk', label: 'Risk', value: `${level(RISK, Math.min(3, risk))} · ${risk.toFixed(2)} of 3`, bar: risk / 3, tone: risk >= 2.5 ? 'bad' : risk >= 1.8 ? 'warn' : 'ok' })
|
|
198
|
+
if (leaks !== undefined) facts.push({ id: 'leaks', label: 'Leaks secrets', value: p2(leaks), bar: leaks, tone: leaks >= 0.7 ? 'bad' : undefined })
|
|
199
|
+
if (onTask !== undefined) facts.push({ id: 'on-task', label: 'On task', value: p2(onTask), bar: onTask, tone: onTask < 0.15 ? 'warn' : undefined })
|
|
200
200
|
return {
|
|
201
|
-
verdict: row.verdict, ...verdict, subject, facts, outcome, verdictTone: verdict.tone,
|
|
201
|
+
verdict: row.verdict, ...verdict, subject, facts, outcome, outcomeKey: outcome ? row.outcome : undefined, verdictTone: verdict.tone,
|
|
202
202
|
tone: outcome && SEVERITY[outcome.tone] > SEVERITY[verdict.tone] ? outcome.tone : verdict.tone,
|
|
203
203
|
note: row.verdict !== 'allow' && row.why ? `Because ${row.why}.` : undefined,
|
|
204
204
|
}
|
|
@@ -211,8 +211,8 @@ window.__ModuleLoader__.load({
|
|
|
211
211
|
return {
|
|
212
212
|
verdict: row.verdict, ...verdict, subject: { tool: row.tool, text: row.call },
|
|
213
213
|
facts: [
|
|
214
|
-
{ label: 'Passages', value: String(num(row.passages) ?? '?') },
|
|
215
|
-
{ label: 'Injection', value: p2(row.maxP), bar: num(row.maxP), tone: row.maxP >= 0.5 ? 'bad' : 'ok' },
|
|
214
|
+
{ id: 'passages', label: 'Passages', value: String(num(row.passages) ?? '?') },
|
|
215
|
+
{ id: 'injection', label: 'Injection', value: p2(row.maxP), bar: num(row.maxP), tone: row.maxP >= 0.5 ? 'bad' : 'ok' },
|
|
216
216
|
],
|
|
217
217
|
note: row.verdict === 'removed' ? `Removed ${plural(lines, 'line')} that gave orders to AI agents before System Two read the result.`
|
|
218
218
|
: row.verdict === 'flagged' ? `Found ${plural(lines, 'line')} that give orders to AI agents; the result was withheld.` : undefined,
|
|
@@ -226,12 +226,12 @@ window.__ModuleLoader__.load({
|
|
|
226
226
|
const verdict = DONE[why]
|
|
227
227
|
const round = `${num(row.steers) ?? '?'} of ${num(row.maxSteers) ?? '?'}`
|
|
228
228
|
return {
|
|
229
|
-
verdict: row.verdict === 'continue' ? 'continue' : 'stop', ...verdict, short: verdict.label.toLowerCase(),
|
|
229
|
+
verdict: row.verdict === 'continue' ? 'continue' : 'stop', ...verdict, why, short: verdict.label.toLowerCase(),
|
|
230
230
|
headline: row.verdict === 'continue' ? `${verdict.label} to System Two (${round})` : why === 'waiting' ? 'Waiting on you' : 'The work is done',
|
|
231
231
|
facts: [
|
|
232
|
-
{ label: 'Complete', value: p2(yesOf(a.complete)), bar: yesOf(a.complete), tone: yesOf(a.complete) < 0.5 ? 'warn' : 'ok' },
|
|
233
|
-
{ label: 'Checked', value: p2(yesOf(a.checked)), bar: yesOf(a.checked) },
|
|
234
|
-
{ label: 'Waiting on you', value: p2(yesOf(a.waiting)), bar: yesOf(a.waiting) },
|
|
232
|
+
{ id: 'complete', label: 'Complete', value: p2(yesOf(a.complete)), bar: yesOf(a.complete), tone: yesOf(a.complete) < 0.5 ? 'warn' : 'ok' },
|
|
233
|
+
{ id: 'checked', label: 'Checked', value: p2(yesOf(a.checked)), bar: yesOf(a.checked) },
|
|
234
|
+
{ id: 'waiting', label: 'Waiting on you', value: p2(yesOf(a.waiting)), bar: yesOf(a.waiting) },
|
|
235
235
|
],
|
|
236
236
|
note: why === 'unfinished' ? `Not finished yet, so System Two goes back to work (${round}).`
|
|
237
237
|
: why === 'unchecked' ? `The changes were not checked, so System Two goes back to verify them (${round}).` : undefined,
|
|
@@ -273,7 +273,7 @@ window.__ModuleLoader__.load({
|
|
|
273
273
|
const label = view.outcome ? `${view.label} → ${view.outcome.label}` : view.label
|
|
274
274
|
const rest = []
|
|
275
275
|
if (view.subject?.text) rest.push(`${view.subject.tool ?? ''} ${view.subject.text}`.trim())
|
|
276
|
-
const lead = view.facts.find(fact =>
|
|
276
|
+
const lead = view.facts.find(fact => ['risk', 'injection', 'complete', 'effort'].includes(fact.id))
|
|
277
277
|
if (lead) rest.push(`${lead.label.toLowerCase()} ${lead.value}`)
|
|
278
278
|
if (view.error) rest.push(view.error)
|
|
279
279
|
return { label, tone: view.tone, rest: rest.join(' · ') }
|
|
@@ -313,18 +313,18 @@ window.__ModuleLoader__.load({
|
|
|
313
313
|
|
|
314
314
|
function Badge({ id, ms, time }) {
|
|
315
315
|
const fresh = useFresh(`badge:${id}`, time)
|
|
316
|
-
return h('span', { className: cx('vsr-s1__badge', fresh && 'vsr-s1__badge--fresh') },
|
|
316
|
+
return h('span', { className: cx('vsr-s1__badge', fresh && 'vsr-s1__badge--fresh'), 'data-system-one-part': 'badge' },
|
|
317
317
|
h('i', { className: 'vsr-s1__dot', 'aria-hidden': true }),
|
|
318
318
|
h('span', { className: 'vsr-s1__name' }, 'System One'),
|
|
319
|
-
secs(ms) ? h('span', { className: 'vsr-s1__ms' }, secs(ms)) : null)
|
|
319
|
+
secs(ms) ? h('span', { className: 'vsr-s1__ms', 'data-system-one-part': 'latency' }, secs(ms)) : null)
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
function Pill({ tone, children, arrow }) {
|
|
323
|
-
return h('span', { className: cx('vsr-s1__pill', `vsr-s1__pill--${tone}`, arrow && 'vsr-s1__pill--then') }, children)
|
|
322
|
+
function Pill({ tone, children, arrow, part }) {
|
|
323
|
+
return h('span', { className: cx('vsr-s1__pill', `vsr-s1__pill--${tone}`, arrow && 'vsr-s1__pill--then'), 'data-system-one-part': part }, children)
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
function Fact({ label, value, bar, tone }) {
|
|
327
|
-
return h('span', { className: 'vsr-s1__fact' },
|
|
326
|
+
function Fact({ id, label, value, bar, tone }) {
|
|
327
|
+
return h('span', { className: 'vsr-s1__fact', 'data-system-one-fact': id },
|
|
328
328
|
h('span', { className: 'vsr-s1__fact-label' }, label),
|
|
329
329
|
num(bar) === undefined ? null : h('span', { className: cx('vsr-s1__bar', tone && `vsr-s1__bar--${tone}`), style: { '--p': clamp01(bar) }, 'aria-hidden': true }, h('i')),
|
|
330
330
|
h('b', null, value))
|
|
@@ -335,34 +335,43 @@ window.__ModuleLoader__.load({
|
|
|
335
335
|
return h('div', {
|
|
336
336
|
className: cx('vsr-s1__row', fresh && 'vsr-s1__row--fresh'),
|
|
337
337
|
'data-system-one-decision': view.verdict,
|
|
338
|
+
'data-system-one-outcome': view.outcomeKey,
|
|
338
339
|
'data-system-one-call': row.callId,
|
|
339
340
|
},
|
|
340
341
|
h('span', { className: 'vsr-s1__verdicts' },
|
|
341
|
-
h(Pill, { tone: view.verdictTone ?? view.tone }, view.label),
|
|
342
|
-
view.outcome ? h(Pill, { tone: view.outcome.tone, arrow: true }, view.outcome.label) : null),
|
|
342
|
+
h(Pill, { tone: view.verdictTone ?? view.tone, part: 'verdict' }, view.label),
|
|
343
|
+
view.outcome ? h(Pill, { tone: view.outcome.tone, arrow: true, part: 'outcome' }, view.outcome.label) : null),
|
|
343
344
|
h('div', { className: 'vsr-s1__main' },
|
|
344
|
-
view.subject?.text ? h('div', { className: 'vsr-s1__subject' },
|
|
345
|
+
view.subject?.text ? h('div', { className: 'vsr-s1__subject', 'data-system-one-part': 'subject' },
|
|
345
346
|
view.subject.tool ? h('span', { className: 'vsr-s1__tool' }, view.subject.tool) : null,
|
|
346
347
|
h('code', { className: 'vsr-s1__code', title: view.subject.text }, view.subject.text)) : null,
|
|
347
|
-
view.facts.length ? h('div', { className: 'vsr-s1__facts' }, view.facts.map(fact => h(Fact, { key: fact.
|
|
348
|
-
view.note ? h('div', { className: 'vsr-s1__note' }, view.note) : null),
|
|
349
|
-
!single && secs(row.ms) ? h('span', { className: 'vsr-s1__rowms' }, secs(row.ms)) : null)
|
|
348
|
+
view.facts.length ? h('div', { className: 'vsr-s1__facts', 'data-system-one-part': 'facts' }, view.facts.map(fact => h(Fact, { key: fact.id, ...fact }))) : null,
|
|
349
|
+
view.note ? h('div', { className: 'vsr-s1__note', 'data-system-one-part': 'note' }, view.note) : null),
|
|
350
|
+
!single && secs(row.ms) ? h('span', { className: 'vsr-s1__rowms', 'data-system-one-part': 'latency' }, secs(row.ms)) : null)
|
|
350
351
|
}
|
|
351
352
|
|
|
352
353
|
function Rows({ reflex, rows, views }) {
|
|
353
354
|
const [all, setAll] = React.useState(false)
|
|
354
355
|
const hidden = all ? 0 : Math.max(0, rows.length - ROWS_SHOWN)
|
|
355
|
-
return h('div', { className: 'vsr-s1__rows' },
|
|
356
|
-
hidden > 0 ? h('button', { type: 'button', className: 'vsr-s1__more', onClick: () => setAll(true) }, `Show ${plural(hidden, `earlier ${reflex === 'guard' ? 'call' : 'result'}`)}`) : null,
|
|
356
|
+
return h('div', { className: 'vsr-s1__rows', 'data-system-one-part': 'rows' },
|
|
357
|
+
hidden > 0 ? h('button', { type: 'button', className: 'vsr-s1__more', 'data-system-one-part': 'more', onClick: () => setAll(true) }, `Show ${plural(hidden, `earlier ${reflex === 'guard' ? 'call' : 'result'}`)}`) : null,
|
|
357
358
|
rows.slice(hidden).map((row, index) => h(Row, { key: idOf(row), row, view: views[hidden + index], single: rows.length === 1 })))
|
|
358
359
|
}
|
|
359
360
|
|
|
360
|
-
|
|
361
|
+
const whole = value => (Number.isSafeInteger(value) ? String(value) : undefined)
|
|
362
|
+
|
|
363
|
+
// a public contract (README.md, "Selectors"); React leaves out an attribute whose value is undefined
|
|
364
|
+
function attributes(kind, state, worst) {
|
|
361
365
|
return {
|
|
362
366
|
'data-system-one': kind,
|
|
363
367
|
'data-system-one-reflex': state.reflex,
|
|
364
368
|
'data-system-one-verdict': worst.verdict,
|
|
369
|
+
'data-system-one-outcome': worst.outcomeKey,
|
|
370
|
+
'data-system-one-why': worst.why,
|
|
371
|
+
'data-system-one-tone': worst.tone,
|
|
365
372
|
'data-system-one-count': String(state.rows.length),
|
|
373
|
+
'data-system-one-turn': whole(state.rows[0].turn),
|
|
374
|
+
'data-system-one-step': state.reflex === 'guard' || state.reflex === 'shield' ? whole(state.rows[0].step) : undefined,
|
|
366
375
|
}
|
|
367
376
|
}
|
|
368
377
|
|
|
@@ -377,14 +386,14 @@ window.__ModuleLoader__.load({
|
|
|
377
386
|
className: cx('vsr-s1', 'vsr-s1--card', `vsr-s1--${worst.tone}`, fresh && 'vsr-s1--fresh'),
|
|
378
387
|
role: 'group',
|
|
379
388
|
'aria-label': `System One ${reflex}: ${headline}`,
|
|
380
|
-
...attributes('card', state,
|
|
389
|
+
...attributes('card', state, worst),
|
|
381
390
|
},
|
|
382
391
|
h('span', { className: 'vsr-s1__sheen', 'aria-hidden': true }),
|
|
383
|
-
h('div', { className: 'vsr-s1__head' },
|
|
392
|
+
h('div', { className: 'vsr-s1__head', 'data-system-one-part': 'head' },
|
|
384
393
|
h(Badge, { key: idOf(last), id: idOf(last), ms: last.ms, time: last.time }),
|
|
385
|
-
h('span', { className: 'vsr-s1__reflex' }, reflex),
|
|
386
|
-
h('span', { className: 'vsr-s1__summary', title: headline }, headline),
|
|
387
|
-
last.model ? h('span', { className: 'vsr-s1__model', title: last.model }, shortModel(last.model)) : null),
|
|
394
|
+
h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, reflex),
|
|
395
|
+
h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
|
|
396
|
+
last.model ? h('span', { className: 'vsr-s1__model', 'data-system-one-part': 'model', title: last.model }, shortModel(last.model)) : null),
|
|
388
397
|
h(Rows, { reflex: state.reflex, rows: state.rows, views }))
|
|
389
398
|
}
|
|
390
399
|
|
|
@@ -406,13 +415,13 @@ window.__ModuleLoader__.load({
|
|
|
406
415
|
className: cx('vsr-s1', 'vsr-s1--compact', `vsr-s1--${worst.tone}`, open && 'vsr-s1--open', fresh && 'vsr-s1--fresh'),
|
|
407
416
|
role: 'group',
|
|
408
417
|
'aria-label': `System One ${reflex}: ${said}`,
|
|
409
|
-
...attributes('compact', state,
|
|
418
|
+
...attributes('compact', state, worst),
|
|
410
419
|
},
|
|
411
|
-
h('button', { type: 'button', className: 'vsr-s1__chip', 'aria-expanded': open, onClick: () => setOpen(!open) },
|
|
420
|
+
h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
|
|
412
421
|
h(Badge, { key: idOf(last), id: idOf(last), ms: last.ms, time: last.time }),
|
|
413
|
-
h('span', { className: 'vsr-s1__reflex' }, reflex),
|
|
414
|
-
h('span', { className: 'vsr-s1__summary', title: said },
|
|
415
|
-
h('span', { className: cx('vsr-s1__verdict', line.tone && `vsr-s1__verdict--${line.tone}`) }, line.label),
|
|
422
|
+
h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, reflex),
|
|
423
|
+
h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: said },
|
|
424
|
+
h('span', { className: cx('vsr-s1__verdict', line.tone && `vsr-s1__verdict--${line.tone}`), 'data-system-one-part': 'verdict' }, line.label),
|
|
416
425
|
line.rest ? ` · ${line.rest}` : null),
|
|
417
426
|
h(Chevron)),
|
|
418
427
|
open ? h(Rows, { reflex: state.reflex, rows: state.rows, views }) : null)
|
|
@@ -455,7 +464,7 @@ window.__ModuleLoader__.load({
|
|
|
455
464
|
|
|
456
465
|
function Stack({ parts, fresh }) {
|
|
457
466
|
const total = parts.reduce((sum, part) => sum + part.n, 0)
|
|
458
|
-
return h('span', { className: cx('vsr-s1__stack', fresh && 'vsr-s1__stack--fresh'), 'aria-hidden': true },
|
|
467
|
+
return h('span', { className: cx('vsr-s1__stack', fresh && 'vsr-s1__stack--fresh'), 'data-system-one-part': 'stack', 'aria-hidden': true },
|
|
459
468
|
total > 0 ? parts.filter(part => part.n > 0).map((part, index) => h('i', { key: part.label, style: { flexGrow: part.n, background: part.color, animationDelay: `${0.12 + index * 0.06}s` } })) : null)
|
|
460
469
|
}
|
|
461
470
|
|
|
@@ -463,12 +472,13 @@ window.__ModuleLoader__.load({
|
|
|
463
472
|
const LABEL_CHARS = 28
|
|
464
473
|
const shortLabel = label => (label.length > LABEL_CHARS ? `${label.slice(0, LABEL_CHARS - 1).trimEnd()}…` : label)
|
|
465
474
|
|
|
466
|
-
function Legend({ parts, extra }) {
|
|
467
|
-
return h('span', { className: 'vsr-s1__legend' },
|
|
475
|
+
function Legend({ parts, extra, failed }) {
|
|
476
|
+
return h('span', { className: 'vsr-s1__legend', 'data-system-one-part': 'legend' },
|
|
468
477
|
parts.map(part => h('span', { key: part.label, className: 'vsr-s1__key', title: `${part.label}: ${part.n}` },
|
|
469
478
|
h('i', { className: 'vsr-s1__swatch', style: { background: part.color } }),
|
|
470
479
|
`${shortLabel(part.label)} `, h('b', null, String(part.n)))),
|
|
471
|
-
extra ? h('span', { className: 'vsr-s1__key vsr-s1__key--note' }, extra) : null
|
|
480
|
+
extra ? h('span', { className: 'vsr-s1__key vsr-s1__key--note' }, extra) : null,
|
|
481
|
+
failed ? h('span', { className: 'vsr-s1__key vsr-s1__key--bad', 'data-system-one-part': 'failed' }, `${failed} failed`) : null)
|
|
472
482
|
}
|
|
473
483
|
|
|
474
484
|
function questionParts(summary) {
|
|
@@ -501,14 +511,14 @@ window.__ModuleLoader__.load({
|
|
|
501
511
|
h('code', { className: 'vsr-s1__qname', title: id }, id),
|
|
502
512
|
h('div', { className: 'vsr-s1__qbody' },
|
|
503
513
|
parts.length ? h(Stack, { parts, fresh }) : null,
|
|
504
|
-
h(Legend, { parts, extra
|
|
514
|
+
h(Legend, { parts, extra, failed: num(summary?.failed) || undefined })))
|
|
505
515
|
}
|
|
506
516
|
|
|
507
517
|
function ToolBody({ meta, fresh, cwd, openFile }) {
|
|
508
518
|
const entries = Object.entries(meta.summary ?? {})
|
|
509
|
-
return h('div', { className: 'vsr-s1__rows' },
|
|
519
|
+
return h('div', { className: 'vsr-s1__rows', 'data-system-one-part': 'rows' },
|
|
510
520
|
entries.map(([id, summary]) => h(Question, { key: id, id, summary, fresh })),
|
|
511
|
-
meta.output ? h('div', { className: 'vsr-s1__foot' },
|
|
521
|
+
meta.output ? h('div', { className: 'vsr-s1__foot', 'data-system-one-part': 'results' },
|
|
512
522
|
'Results ',
|
|
513
523
|
typeof openFile === 'function'
|
|
514
524
|
? h('button', { type: 'button', className: 'vsr-s1__link', onClick: () => openFile(meta.output) }, relative(meta.output, cwd))
|
|
@@ -524,36 +534,40 @@ window.__ModuleLoader__.load({
|
|
|
524
534
|
const [open, setOpen] = React.useState(false)
|
|
525
535
|
const source = args.file ? basename(args.file) : Array.isArray(args.images) ? plural(args.images.length, 'image') : Array.isArray(args.items) ? plural(args.items.length, 'item') : ''
|
|
526
536
|
const asked = questionCount(args.questions)
|
|
527
|
-
const common = { 'data-system-one
|
|
537
|
+
const common = { 'data-system-one-reflex': 'tool', 'data-system-one-call': callId }
|
|
528
538
|
|
|
529
539
|
if (phase !== 'result') {
|
|
530
540
|
const line = phase === 'preparing' ? 'Preparing a batch' : `Answering ${plural(asked, 'question')}${source ? ` over ${source}` : ''}`
|
|
531
|
-
return h('div', { className: 'vsr-s1 vsr-s1--compact vsr-s1--running', ...common, 'data-system-one-verdict': 'running' },
|
|
541
|
+
return h('div', { className: 'vsr-s1 vsr-s1--compact vsr-s1--running', 'data-system-one': 'tool', ...common, 'data-system-one-verdict': 'running', 'data-system-one-tone': 'info' },
|
|
532
542
|
h('div', { className: 'vsr-s1__chip vsr-s1__chip--static' },
|
|
533
543
|
h(Badge, { id: `${callId}:running`, time: Date.now() }),
|
|
534
|
-
h('span', { className: 'vsr-s1__reflex' }, REFLEX.tool),
|
|
535
|
-
h('span', { className: 'vsr-s1__summary vsr-s1__shimmer' }, line)))
|
|
544
|
+
h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
|
|
545
|
+
h('span', { className: 'vsr-s1__summary vsr-s1__shimmer', 'data-system-one-part': 'summary' }, line)))
|
|
536
546
|
}
|
|
537
547
|
|
|
538
548
|
if (block?.isError || !meta || ui === 'off') {
|
|
539
549
|
const body = resultText(block)
|
|
540
550
|
const first = body.split('\n')[0] || (block?.isError ? 'The batch failed' : 'Done')
|
|
541
|
-
return h('div', {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
551
|
+
return h('div', {
|
|
552
|
+
className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--plain', block?.isError && 'vsr-s1--bad', open && 'vsr-s1--open'),
|
|
553
|
+
'data-system-one': 'tool', ...common, 'data-system-one-verdict': block?.isError ? 'error' : 'done', 'data-system-one-tone': block?.isError ? 'bad' : 'muted',
|
|
554
|
+
},
|
|
555
|
+
h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
|
|
556
|
+
h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, 'System One'),
|
|
557
|
+
h('span', { className: cx('vsr-s1__summary', block?.isError && 'vsr-s1__summary--bad'), 'data-system-one-part': 'summary', title: first }, first),
|
|
558
|
+
h(Chevron)),
|
|
559
|
+
open ? h('pre', { className: 'vsr-s1__raw', 'data-system-one-part': 'raw' }, body) : null)
|
|
547
560
|
}
|
|
548
561
|
|
|
549
562
|
const ms = num(meta.seconds) === undefined ? undefined : meta.seconds * 1000
|
|
550
563
|
const headline = [plural(num(meta.items) ?? 0, 'item'), plural(num(meta.questions) ?? 0, 'question'), plural(num(meta.requests) ?? 0, 'request')].join(' · ')
|
|
564
|
+
const done = { ...common, 'data-system-one-verdict': 'done', 'data-system-one-tone': 'ok', 'data-system-one-count': String(meta.items ?? '') }
|
|
551
565
|
if (ui === 'compact') {
|
|
552
|
-
return h('div', { className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--ok', open && 'vsr-s1--open', fresh && 'vsr-s1--fresh'),
|
|
553
|
-
h('button', { type: 'button', className: 'vsr-s1__chip', 'aria-expanded': open, onClick: () => setOpen(!open) },
|
|
566
|
+
return h('div', { className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--ok', open && 'vsr-s1--open', fresh && 'vsr-s1--fresh'), 'data-system-one': 'tool', ...done },
|
|
567
|
+
h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
|
|
554
568
|
h(Badge, { id: `${callId}:done`, ms, time: block?.time }),
|
|
555
|
-
h('span', { className: 'vsr-s1__reflex' }, REFLEX.tool),
|
|
556
|
-
h('span', { className: 'vsr-s1__summary', title: headline }, headline),
|
|
569
|
+
h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
|
|
570
|
+
h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
|
|
557
571
|
h(Chevron)),
|
|
558
572
|
open ? h(ToolBody, { meta, fresh, cwd, openFile }) : null)
|
|
559
573
|
}
|
|
@@ -561,16 +575,15 @@ window.__ModuleLoader__.load({
|
|
|
561
575
|
className: cx('vsr-s1', 'vsr-s1--card', 'vsr-s1--ok', 'vsr-s1--tool', fresh && 'vsr-s1--fresh'),
|
|
562
576
|
role: 'group',
|
|
563
577
|
'aria-label': `System One batch: ${headline}`,
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
'data-system-one-count': String(meta.items ?? ''),
|
|
578
|
+
'data-system-one': 'tool',
|
|
579
|
+
...done,
|
|
567
580
|
},
|
|
568
581
|
h('span', { className: 'vsr-s1__sheen', 'aria-hidden': true }),
|
|
569
|
-
h('div', { className: 'vsr-s1__head' },
|
|
582
|
+
h('div', { className: 'vsr-s1__head', 'data-system-one-part': 'head' },
|
|
570
583
|
h(Badge, { id: `${callId}:done`, ms, time: block?.time }),
|
|
571
|
-
h('span', { className: 'vsr-s1__reflex' }, REFLEX.tool),
|
|
572
|
-
h('span', { className: 'vsr-s1__summary', title: headline }, headline),
|
|
573
|
-
meta.model ? h('span', { className: 'vsr-s1__model', title: meta.model }, shortModel(meta.model)) : null),
|
|
584
|
+
h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
|
|
585
|
+
h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
|
|
586
|
+
meta.model ? h('span', { className: 'vsr-s1__model', 'data-system-one-part': 'model', title: meta.model }, shortModel(meta.model)) : null),
|
|
574
587
|
h(ToolBody, { meta, fresh, cwd, openFile }))
|
|
575
588
|
}
|
|
576
589
|
|
|
@@ -631,14 +644,15 @@ window.__ModuleLoader__.load({
|
|
|
631
644
|
.vsr-s1--plain .vsr-s1__reflex{color:var(--vsr-s1-text-2)}
|
|
632
645
|
.vsr-s1__raw{margin:4px 0 8px;padding:8px 10px;max-height:320px;overflow:auto;border-radius:8px;background:color-mix(in srgb,var(--vsr-s1-text-3) 10%,transparent);font-family:var(--vsr-s1-mono);font-size:12px;white-space:pre-wrap}
|
|
633
646
|
.vsr-s1__q{display:grid;grid-template-columns:minmax(80px,140px) minmax(0,1fr);column-gap:14px;align-items:start}
|
|
634
|
-
.vsr-s1__qname{overflow:hidden;color:var(--vsr-s1-text);font-family:var(--vsr-s1-mono);font-size:12.5px;line-height:
|
|
647
|
+
.vsr-s1__qname{overflow:hidden;color:var(--vsr-s1-text);font-family:var(--vsr-s1-mono);font-size:12.5px;line-height:16px;white-space:nowrap;text-overflow:ellipsis;background:none;padding:0}
|
|
635
648
|
.vsr-s1__qbody{display:grid;gap:4px;min-width:0}
|
|
636
|
-
.vsr-s1__stack{display:flex;gap:2px;height:8px;border-radius:4px;overflow:hidden;background:var(--vsr-s1-track)}
|
|
649
|
+
.vsr-s1__stack{display:flex;gap:2px;height:8px;margin-top:4px;border-radius:4px;overflow:hidden;background:var(--vsr-s1-track)}
|
|
637
650
|
.vsr-s1__stack>i{display:block;min-width:3px;height:100%;transform-origin:left center}
|
|
638
651
|
.vsr-s1__legend{display:flex;flex-wrap:wrap;gap:1px 14px;color:var(--vsr-s1-text-2);font-size:12.5px}
|
|
639
652
|
.vsr-s1__key{display:inline-flex;align-items:center;white-space:nowrap}
|
|
640
653
|
.vsr-s1__key b{margin-left:4px;color:var(--vsr-s1-text);font-weight:600;font-variant-numeric:tabular-nums}
|
|
641
654
|
.vsr-s1__key--note{color:var(--vsr-s1-text-3)}
|
|
655
|
+
.vsr-s1__key--bad{color:var(--vsr-s1-bad);font-weight:600}
|
|
642
656
|
.vsr-s1__swatch{flex:none;width:8px;height:8px;margin-right:6px;border-radius:2px}
|
|
643
657
|
.vsr-s1__foot{color:var(--vsr-s1-text-3);font-size:12.5px}
|
|
644
658
|
.vsr-s1__foot code{font-family:var(--vsr-s1-mono)}
|