@vllm-sr/dsh 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,8 +11,8 @@ agent orders, is the work really done, which team should get each of these 230 t
11
11
  - **System One**, the decision model, decides: `vllm-sr/d3-ultra`. It answers typed questions (choice, yes/no, score,
12
12
  box) with probabilities, from one forward pass per question instead of generated text.
13
13
 
14
- Both come from [System Duo](https://app.vllm-sr.ai) with one free key, and the plugin gets that key for you: System One
15
- answers at `https://app.vllm-sr.ai/v1/systemone`, and `vllm-sr/auto` at `https://app.vllm-sr.ai/v1/chat/completions`.
14
+ Both take a free [System Duo](https://app.vllm-sr.ai) key, which the plugin gets for you: System One answers at `https://app.vllm-sr.ai/v1/systemone`,
15
+ and `vllm-sr/auto` at `https://app.vllm-sr.ai/v1/chat/completions`.
16
16
 
17
17
  ## Quick start
18
18
 
@@ -26,9 +26,10 @@ The first line installs DeepSeek Harness (0.2.1-rc.5 or a later 0.2 release, on
26
26
  `dsh plugin` uses to install plugins; without pnpm, `dsh plugin add` stops with "pnpm was not found". On a machine
27
27
  with git, the package can also come straight from GitHub: `dsh plugin --profile web add github:Xunzhuo/dsh-system-one-plugin`.
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
30
- API key" button runs in the browser, and saves it in DeepSeek Harness's credential store and in `~/.dsh/.env` (readable
31
- by you only). A notice in the session says so. `vllm-sr/auto` becomes the default model; choose another model
29
+ On its first start the plugin gets a free System Duo key, with the same small proof of work the site's "Get your API
30
+ key" button runs in the browser, and saves it in DeepSeek Harness's credential store and in `~/.dsh/.env` (readable
31
+ by you only). A line under your first request says so: "Free API key created for app.vllm-sr.ai, saved in
32
+ ~/.dsh/.env", or why the site refused one. `vllm-sr/auto` becomes the default model; choose another model
32
33
  in the app and System One keeps working beside it.
33
34
 
34
35
  Headless:
@@ -70,11 +71,13 @@ badge that carries the decision's latency:
70
71
  - **Done-check**: a card after the answer: done, waiting on you, or sent back to finish.
71
72
  - **`system_one` tool**: its row shows the batch: items, questions, each question's answer distribution and the
72
73
  results file.
74
+ - **Free key**: a **System Duo** line under your first request when the plugin created a key, or could not.
73
75
 
74
76
  Routine checks (an allowed call, a clean result) are one line and fold with the turn's work. Cards stay visible when
75
77
  the work folds, so a finished turn still shows what System One decided. Parallel calls in one step share one line or
76
78
  card, and every decision pops in with a short animation the first time it appears (none with reduced motion). Each
77
- element shows the Decision API's own answer: the probabilities, the model and the measured latency.
79
+ element shows the Decision API's own answer: the probabilities, the model (in bold, as `vllm-sr/d3-ultra`) and the
80
+ measured latency.
78
81
 
79
82
  Set `systemOneUi` to `compact` for one line per decision, or to `off` to hide them.
80
83
 
@@ -85,18 +88,20 @@ attributes that say what it shows:
85
88
 
86
89
  | Attribute | Values |
87
90
  | --- | --- |
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` | `card`, `compact` (one line), `tool` (the `system_one` row) or `notice` (the free-key line) |
92
+ | `data-system-one-reflex` | `triage`, `guard`, `shield`, `done`, `tool` or `key` |
93
+ | `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`; key `created` or `failed`; `error` when System One did not answer |
91
94
  | `data-system-one-outcome` | guard, once you answer its approval: `allowed`, `rejected`, `cancelled` or `failed` |
92
95
  | `data-system-one-why` | done-check: `complete`, `waiting`, `unfinished` or `unchecked` |
93
96
  | `data-system-one-tone` | its colour: `ok` (green), `info` (ember), `warn` (amber), `bad` (red) or `muted` |
94
97
  | `data-system-one-count` | how many decisions it holds (a batch: how many items) |
95
98
  | `data-system-one-turn`, `data-system-one-step` | where it acted (the step for guard and shield) |
99
+ | `data-system-one-model` | the model that answered, as `vllm-sr/d3-ultra` |
96
100
 
97
101
  Inside a root, `data-system-one-part` marks `badge` (the ember **System One** pill), `latency`, `reflex`, `summary`,
98
102
  `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
103
+ `note` and `more`, in the batch row also `stack`, `legend`, `failed`, `results` and `raw`, and in the free-key line
104
+ `badge` (the **System Duo** pill), `summary` and `links`. Each number is a
100
105
  `data-system-one-fact`: `kind`, `effort`, `unclear` and `risky` (triage), `risk`, `leaks` and `on-task` (guard),
101
106
  `passages` and `injection` (shield), `complete`, `checked` and `waiting` (done-check). Decision rows carry
102
107
  `data-system-one-decision` and `data-system-one-call`, batch questions `data-system-one-question`. For example:
@@ -113,6 +118,8 @@ Inside a root, `data-system-one-part` marks `badge` (the ember **System One** pi
113
118
  | The ember badge, and its latency | `[data-system-one-part="badge"]`, `[data-system-one-part="latency"]` |
114
119
  | "Dangerous", and "You rejected it" | `[data-system-one-fact="risk"]`, `[data-system-one-part="outcome"]` |
115
120
  | The reason | `[data-system-one-part="note"]` |
121
+ | The model's name (bold) | `[data-system-one-part="model"]` |
122
+ | Free key created | `[data-system-one="notice"][data-system-one-verdict="created"]` |
116
123
 
117
124
  ## Your data
118
125
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vllm-sr/dsh",
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.",
3
+ "version": "0.4.2",
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 a free app.vllm-sr.ai key.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
package/src/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * - done: agent/turn-stopping is the work really finished and checked?
10
10
  * - tool: system_one one rubric over many items, for the LLM to call
11
11
  *
12
- * On first use it gets one free System Duo key (app.vllm-sr.ai) for vllm-sr/auto and the decision models (keys.js).
12
+ * On first use it gets a free System Duo key (app.vllm-sr.ai) for vllm-sr/auto and the decision models (keys.js).
13
13
  * Each decision is also recorded in the session (records.js), and the browser half (web/client.js) draws it in Chat.
14
14
  */
15
15
  import { homedir } from 'node:os'
@@ -19,7 +19,7 @@ import z from '@deepseek-ai/schemastery'
19
19
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
20
20
  import * as dshSession from '@deepseek-ai/dsh-session'
21
21
  import { createDecisionClient, compactAnswer, yes } from './client.js'
22
- import { SERVICES, appendDotenv, ensureKeys, readDotenv } from './keys.js'
22
+ import { SERVICES, appendDotenv, describeKeys, ensureKeys, readDotenv } from './keys.js'
23
23
  import { DONE, EFFORT_LEVELS, GUARD, RISK_LEVELS, TRIAGE } from './questions.js'
24
24
  import { UI_MODES, approvalOutcome, createRecorder, relativeTo, subject } from './records.js'
25
25
  import { screen } from './shield.js'
@@ -125,7 +125,9 @@ function briefCall(exec) {
125
125
  export function apply(ctx, config) {
126
126
  let credentials
127
127
  let keysReady
128
+ let keysSettled = false
128
129
  let keysNotice
130
+ let keysRecord
129
131
  const keysAbort = new AbortController()
130
132
  // DeepSeek Harness's user dotenv: the credential store falls back to it, and a headless run reads it at launch
131
133
  const envFile = join(process.env.DSH_HOME || join(homedir(), '.dsh'), '.env')
@@ -161,7 +163,7 @@ export function apply(ctx, config) {
161
163
  if (!id) return undefined
162
164
  let state = sessions.get(id)
163
165
  if (!state) {
164
- state = { id, user: '', calls: [], assistant: '', steers: 0, triage: undefined, turn: 0, step: 0, asked: new Map(), pendingTriage: undefined }
166
+ state = { id, user: '', calls: [], assistant: '', steers: 0, triage: undefined, turn: 0, step: 0, asked: new Map(), pendingTriage: undefined, pendingKey: undefined }
165
167
  sessions.set(id, state)
166
168
  }
167
169
  return state
@@ -192,24 +194,15 @@ export function apply(ctx, config) {
192
194
  return ensureKeys({ resolve, store: keep, services, label: config.keyLabel, signal: keysAbort.signal })
193
195
  })().then(({ allocated, failed }) => {
194
196
  trace({ ev: 'keys', store: Boolean(store), allocated: allocated.map(s => s.site), failed: failed.map(f => ({ site: f.service.site, code: f.error.code, message: clip(String(f.error.message), 200) })) })
195
- const lines = []
196
- if (allocated.length) {
197
- const saved = store ? `DeepSeek Harness's credential store and ${where}` : `${where}`
198
- const later = !store ? ' A headless run reads keys when it starts, so vllm-sr/auto answers from the next run.' : ''
199
- const s = allocated[0]
200
- lines.push(`[System Duo] One free key for vllm-sr/auto and the decision models (${new URL(s.site).host}) is saved in ${saved}. Using it accepts the terms (${s.terms}); requests are logged to improve the services and to train models (${s.privacy}).${later}`)
201
- }
202
- for (const { service, error } of failed) {
203
- if (keysAbort.signal.aborted) continue
204
- // a DOMException's code is a legacy number (20 for an abort, 23 for a timeout); its name says what happened
205
- const why = typeof error.code === 'number' ? error.name : error.code ?? error.status ?? 'error'
206
- lines.push(`[System Duo] No free key from ${new URL(service.site).host} (${why}: ${clip(String(error.message), 160)}). Get one at ${service.site} and save it as ${service.ref}.`)
207
- }
208
- if (lines.length === 0) return
209
- if (!store) process.stderr.write(`${lines.join('\n')}\n`)
210
- keysNotice = notice(lines.join('\n'), allocated.length ? 'System Duo got a free key for vllm-sr/auto and the decision models' : 'System Duo could not get a free key')
197
+ const said = describeKeys({ allocated, failed: keysAbort.signal.aborted ? [] : failed }, { file: where, store: Boolean(store) })
198
+ if (!said) return
199
+ if (!store) process.stderr.write(`${said.text}\n`)
200
+ keysNotice = notice(said.text, said.summary)
201
+ keysRecord = said.record
211
202
  }, error => {
212
203
  trace({ ev: 'keys', error: clip(String(error?.message ?? error), 300) })
204
+ }).finally(() => {
205
+ keysSettled = true
213
206
  })
214
207
  }
215
208
 
@@ -222,10 +215,13 @@ export function apply(ctx, config) {
222
215
  startKeys(scoped.credentials)
223
216
  })
224
217
 
225
- function withNotices(decision, messages) {
218
+ // Chat hides notices, so the keys' one also goes to the session as a record that Chat draws under the request.
219
+ function withNotices(decision, messages, state, agent) {
226
220
  if (decision.kind !== 'enter') return decision
227
221
  const extra = [keysNotice, ...messages].filter(Boolean)
222
+ if (keysNotice && keysRecord && state && agent?.session) state.pendingKey = { session: agent.session, data: { ...keysRecord, turn: state.turn, step: state.step } }
228
223
  keysNotice = undefined
224
+ keysRecord = undefined
229
225
  return extra.length === 0 ? decision : { ...decision, messages: [...decision.messages, ...extra] }
230
226
  }
231
227
 
@@ -254,10 +250,11 @@ export function apply(ctx, config) {
254
250
  const AFTER_REQUEST = new Set(['request/header', 'request/context', 'assistant/message', 'step/end', 'turn/end'])
255
251
  ctx.on('session/event', (session, event) => {
256
252
  const state = sessions.get(session?.id)
257
- const pending = state?.pendingTriage
258
- if (pending && ((event?.type === 'user/message' && event.data?.source?.kind === 'user') || AFTER_REQUEST.has(event?.type))) {
253
+ const pending = [state?.pendingKey, state?.pendingTriage].filter(Boolean)
254
+ if (pending.length && ((event?.type === 'user/message' && event.data?.source?.kind === 'user') || AFTER_REQUEST.has(event?.type))) {
255
+ state.pendingKey = undefined
259
256
  state.pendingTriage = undefined
260
- queueMicrotask(() => record(pending.session, pending.data))
257
+ queueMicrotask(() => pending.forEach(entry => record(entry.session, entry.data)))
261
258
  }
262
259
  if (event?.type !== 'assistant/message') return
263
260
  const text = textOf(event.data?.message?.content)
@@ -289,7 +286,7 @@ export function apply(ctx, config) {
289
286
  // Triage: one look at each new user request before the model sees it.
290
287
  ctx.on('agent/pre-step', async (payload, next) => {
291
288
  if (!keysReady && !credentials) startKeys(undefined)
292
- if (keysReady) await Promise.race([keysReady, sleep(KEYS_WAIT_MS)])
289
+ if (keysReady && !keysSettled) await Promise.race([keysReady, sleep(KEYS_WAIT_MS, undefined, { ref: false })])
293
290
  const { agent, messages, signal, turn, step } = payload
294
291
  const userMessages = messages.filter(message => message.source?.kind === 'user')
295
292
  const state = sessionState(agent)
@@ -302,7 +299,7 @@ export function apply(ctx, config) {
302
299
  state.calls = []
303
300
  state.steers = 0
304
301
  trace({ ev: 'user', session: state.id, turn, text: clip(state.user, 4000) })
305
- if (!config.triage || !state.user) return withNotices(await next(), [])
302
+ if (!config.triage || !state.user) return withNotices(await next(), [], state, agent)
306
303
  let brief
307
304
  try {
308
305
  const response = await client.ask({ model: config.model, questions: TRIAGE, inputs: [{ state: clip(state.user, 12000) }], signal, timeoutMs: config.hookTimeoutMs, retries: config.hookRetries })
@@ -326,7 +323,7 @@ export function apply(ctx, config) {
326
323
  trace({ ev: 's1', reflex: 'triage', session: state.id, turn, error: String(error?.message ?? error) })
327
324
  if (!signal?.aborted) state.pendingTriage = { session: agent.session, data: { reflex: 'triage', turn, step, error: subject(error?.message ?? error) } }
328
325
  }
329
- return withNotices(await next(), [brief])
326
+ return withNotices(await next(), [brief], state, agent)
330
327
  })
331
328
 
332
329
  // Guard: every side-effecting call is reviewed before it runs.
package/src/keys.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The free System Duo key (app.vllm-sr.ai): one key for both vllm-sr/auto, the LLM (System Two), and the
3
- * decision models (System One). The site hands it out for a small proof of work, the same one its "Get your API
4
- * key" button runs in the browser; the key is kept in DeepSeek Harness's credential store.
2
+ * The free System Duo key (app.vllm-sr.ai), which both vllm-sr/auto, the LLM (System Two), and the decision models
3
+ * (System One) take. The site hands it out for a small proof of work, the same one its "Get your API key" button runs
4
+ * in the browser; the key is kept in DeepSeek Harness's credential store.
5
5
  */
6
6
  import { createHash, hash as oneShot } from 'node:crypto'
7
7
  import { closeSync, fchmodSync, mkdirSync, openSync, readFileSync, writeSync } from 'node:fs'
@@ -172,3 +172,40 @@ export async function ensureKeys({ resolve, store, services = SERVICES, label, f
172
172
  }))
173
173
  return { allocated, failed }
174
174
  }
175
+
176
+ /**
177
+ * What the session says about the keys a start fetched: the notice the model reads (and Trajectory shows), and the
178
+ * record Chat draws as an inline line.
179
+ * @param {object} outcome - ensureKeys' result
180
+ * @param {object} where
181
+ * @param {string} where.file - the dotenv file the key went to, as the user writes it (~/.dsh/.env)
182
+ * @param {boolean} where.store - whether DeepSeek Harness's credential store holds it too
183
+ * @returns {{text: string, summary: string, record: object} | undefined} nothing when no key was fetched or refused
184
+ */
185
+ export function describeKeys({ allocated, failed }, { file, store }) {
186
+ const host = site => new URL(site).host
187
+ const lines = []
188
+ if (allocated.length) {
189
+ const s = allocated[0]
190
+ const saved = store ? `DeepSeek Harness's credential store and ${file}` : file
191
+ const later = store ? '' : ' A headless run reads keys when it starts, so vllm-sr/auto answers from the next run.'
192
+ lines.push(`[System Duo] Free API key created for ${host(s.site)}, saved in ${saved}. Using it accepts the terms (${s.terms}); requests are logged to improve the services and to train models (${s.privacy}).${later}`)
193
+ }
194
+ const refused = failed.map(({ service, error }) => {
195
+ // a DOMException's code is a legacy number (20 for an abort, 23 for a timeout); its name says what happened
196
+ const why = typeof error.code === 'number' ? error.name : error.code ?? error.status ?? 'error'
197
+ const message = String(error.message).slice(0, 160)
198
+ lines.push(`[System Duo] No free API key from ${host(service.site)} (${why}: ${message}). Get one at ${service.site} and save it as ${service.ref}.`)
199
+ return { service, why, message }
200
+ })
201
+ if (lines.length === 0) return undefined
202
+ const s = allocated[0]
203
+ const record = s
204
+ ? { reflex: 'key', verdict: 'created', site: host(s.site), saved: file, store, terms: s.terms, privacy: s.privacy }
205
+ : { reflex: 'key', verdict: 'failed', site: host(refused[0].service.site), url: refused[0].service.site, ref: refused[0].service.ref, error: { code: String(refused[0].why), message: refused[0].message } }
206
+ return {
207
+ text: lines.join('\n'),
208
+ summary: s ? `Free API key created for ${host(s.site)}` : `No free API key from ${host(refused[0].service.site)}`,
209
+ record,
210
+ }
211
+ }
package/web/client.js CHANGED
@@ -64,7 +64,8 @@ window.__ModuleLoader__.load({
64
64
  const num = value => (typeof value === 'number' && Number.isFinite(value) ? value : undefined)
65
65
  const p2 = value => (num(value) === undefined ? '?' : value.toFixed(2))
66
66
  const secs = ms => (num(ms) === undefined ? '' : `${(ms / 1000).toFixed(2)} s`)
67
- const shortModel = model => (typeof model === 'string' ? model.replace(/^vllm-sr\//, '') : '')
67
+ // System One's models are vllm-sr's; the name always shows its owner, whatever form the answer gave it in
68
+ const fullModel = model => (typeof model === 'string' && model ? (model.includes('/') ? model : `vllm-sr/${model}`) : '')
68
69
  const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
69
70
  const clamp01 = value => Math.min(1, Math.max(0, value))
70
71
  const level = (labels, score) => labels[Math.min(labels.length - 1, Math.max(0, Math.round(score)))]
@@ -319,6 +320,11 @@ window.__ModuleLoader__.load({
319
320
  secs(ms) ? h('span', { className: 'vsr-s1__ms', 'data-system-one-part': 'latency' }, secs(ms)) : null)
320
321
  }
321
322
 
323
+ function Model({ model }) {
324
+ const name = fullModel(model)
325
+ return name ? h('b', { className: 'vsr-s1__model', 'data-system-one-part': 'model', title: name }, name) : null
326
+ }
327
+
322
328
  function Pill({ tone, children, arrow, part }) {
323
329
  return h('span', { className: cx('vsr-s1__pill', `vsr-s1__pill--${tone}`, arrow && 'vsr-s1__pill--then'), 'data-system-one-part': part }, children)
324
330
  }
@@ -372,6 +378,7 @@ window.__ModuleLoader__.load({
372
378
  'data-system-one-count': String(state.rows.length),
373
379
  'data-system-one-turn': whole(state.rows[0].turn),
374
380
  'data-system-one-step': state.reflex === 'guard' || state.reflex === 'shield' ? whole(state.rows[0].step) : undefined,
381
+ 'data-system-one-model': fullModel(state.rows[state.rows.length - 1].model) || undefined,
375
382
  }
376
383
  }
377
384
 
@@ -393,7 +400,7 @@ window.__ModuleLoader__.load({
393
400
  h(Badge, { key: idOf(last), id: idOf(last), ms: last.ms, time: last.time }),
394
401
  h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, reflex),
395
402
  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),
403
+ h(Model, { model: last.model })),
397
404
  h(Rows, { reflex: state.reflex, rows: state.rows, views }))
398
405
  }
399
406
 
@@ -423,15 +430,48 @@ window.__ModuleLoader__.load({
423
430
  h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: said },
424
431
  h('span', { className: cx('vsr-s1__verdict', line.tone && `vsr-s1__verdict--${line.tone}`), 'data-system-one-part': 'verdict' }, line.label),
425
432
  line.rest ? ` · ${line.rest}` : null),
433
+ open ? h(Model, { model: last.model }) : null,
426
434
  h(Chevron)),
427
435
  open ? h(Rows, { reflex: state.reflex, rows: state.rows, views }) : null)
428
436
  }
429
437
 
438
+ function KeyGlyph() {
439
+ return h('svg', { className: 'vsr-s1__glyph', viewBox: '0 0 16 16', width: 12, height: 12, 'aria-hidden': true },
440
+ h('circle', { cx: 5.5, cy: 8, r: 3, fill: 'none', stroke: 'currentColor', strokeWidth: 1.6 }),
441
+ h('path', { d: 'M8.5 8H14M12 8v2.5M14 8v2', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round' }))
442
+ }
443
+
444
+ // The free key a start fetched, or why it could not: one line under the request, in either mode.
445
+ function KeyNotice({ state }) {
446
+ const row = state.rows[state.rows.length - 1]
447
+ const created = row.verdict === 'created'
448
+ const fresh = useFresh(`key:${idOf(row)}`, row.time)
449
+ const site = text(row.site) || 'app.vllm-sr.ai'
450
+ const said = created
451
+ ? `Free API key created for ${site}, saved in ${text(row.saved) || '~/.dsh/.env'}`
452
+ : `No free API key from ${site}: ${text(row.error?.message) || 'the site refused'}. Get one at ${site} and save it as ${text(row.ref) || 'VLLM_SR_API_KEY'}.`
453
+ const link = (href, label) => (typeof href === 'string' && /^https:\/\//.test(href) ? h('a', { className: 'vsr-s1__link', href, target: '_blank', rel: 'noreferrer' }, label) : null)
454
+ return h('div', {
455
+ className: cx('vsr-s1', 'vsr-s1--notice', created ? 'vsr-s1--ok' : 'vsr-s1--bad', fresh && 'vsr-s1--fresh'),
456
+ role: 'note',
457
+ 'aria-label': `System Duo: ${said}`,
458
+ 'data-system-one': 'notice',
459
+ 'data-system-one-reflex': 'key',
460
+ 'data-system-one-verdict': created ? 'created' : 'failed',
461
+ 'data-system-one-tone': created ? 'ok' : 'bad',
462
+ 'data-system-one-turn': whole(row.turn),
463
+ },
464
+ h('span', { className: cx('vsr-s1__duo', fresh && 'vsr-s1__badge--fresh'), 'data-system-one-part': 'badge' }, h(KeyGlyph), 'System Duo'),
465
+ h('span', { className: cx('vsr-s1__summary', !created && 'vsr-s1__summary--bad'), 'data-system-one-part': 'summary', title: said }, said),
466
+ created && (row.terms || row.privacy) ? h('span', { className: 'vsr-s1__links', 'data-system-one-part': 'links' }, link(row.terms, 'Terms'), link(row.privacy, 'Privacy')) : null)
467
+ }
468
+
430
469
  // A routine check (an allowed call, a clean result) is one line in either mode; a decision that changed
431
470
  // something, and the triage and done-check that frame the turn, are cards unless the mode is compact.
432
471
  function SystemOneNode(props) {
433
472
  const state = props?.node?.data
434
473
  if (!state || !Array.isArray(state.rows) || state.rows.length === 0) return null
474
+ if (state.reflex === 'key') return h(Contained, null, h(KeyNotice, { state }))
435
475
  const slim = state.ui === 'compact' || !standsOut(state)
436
476
  return h(Contained, null, h(slim ? Compact : Card, { state }))
437
477
  }
@@ -561,13 +601,17 @@ window.__ModuleLoader__.load({
561
601
 
562
602
  const ms = num(meta.seconds) === undefined ? undefined : meta.seconds * 1000
563
603
  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 ?? '') }
604
+ const done = {
605
+ ...common, 'data-system-one-verdict': 'done', 'data-system-one-tone': 'ok', 'data-system-one-count': String(meta.items ?? ''),
606
+ 'data-system-one-model': fullModel(meta.model) || undefined,
607
+ }
565
608
  if (ui === 'compact') {
566
609
  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
610
  h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
568
611
  h(Badge, { id: `${callId}:done`, ms, time: block?.time }),
569
612
  h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
570
613
  h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
614
+ open ? h(Model, { model: meta.model }) : null,
571
615
  h(Chevron)),
572
616
  open ? h(ToolBody, { meta, fresh, cwd, openFile }) : null)
573
617
  }
@@ -583,7 +627,7 @@ window.__ModuleLoader__.load({
583
627
  h(Badge, { id: `${callId}:done`, ms, time: block?.time }),
584
628
  h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
585
629
  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),
630
+ h(Model, { model: meta.model })),
587
631
  h(ToolBody, { meta, fresh, cwd, openFile }))
588
632
  }
589
633
 
@@ -612,7 +656,13 @@ window.__ModuleLoader__.load({
612
656
  .vsr-s1__summary--bad{color:var(--vsr-s1-bad)}
613
657
  .vsr-s1__verdict{font-weight:600;color:var(--vsr-s1-text)}
614
658
  .vsr-s1__verdict--ok{color:var(--vsr-s1-ok)}.vsr-s1__verdict--warn{color:color-mix(in srgb,var(--vsr-s1-warn) 82%,var(--vsr-s1-text))}.vsr-s1__verdict--bad{color:var(--vsr-s1-bad)}.vsr-s1__verdict--info{color:var(--vsr-s1-ember-deep)}.vsr-s1__verdict--muted{color:var(--vsr-s1-text-3)}
615
- .vsr-s1__model{flex:none;color:var(--vsr-s1-text-3);font-size:12px;white-space:nowrap}
659
+ .vsr-s1__model{flex:none;color:var(--vsr-s1-text-2);font-size:12px;font-weight:700;letter-spacing:-.005em;white-space:nowrap}
660
+ .vsr-s1--notice{display:flex;align-items:center;gap:9px;min-width:0;margin:4px 0;padding:3px 0}
661
+ .vsr-s1--notice .vsr-s1__summary{white-space:normal}
662
+ .vsr-s1__duo{position:relative;display:inline-flex;flex:none;align-items:center;gap:5px;height:22px;padding:0 9px 0 8px;border-radius:999px;color:#fff;font-size:12px;font-weight:650;white-space:nowrap;background:linear-gradient(110deg,var(--vsr-s1-ember) 0%,#B04FD0 58%,#2F7BF6 100%);box-shadow:0 1px 2px rgba(90,60,170,.25),inset 0 1px 0 rgba(255,255,255,.32)}
663
+ .vsr-s1__glyph{flex:none}
664
+ .vsr-s1__links{display:inline-flex;flex:none;gap:10px}
665
+ a.vsr-s1__link{text-decoration:none}
616
666
  .vsr-s1__rows{display:grid;grid-template-columns:auto minmax(0,1fr) auto;column-gap:12px;row-gap:8px;margin-top:9px}
617
667
  .vsr-s1__rows>*{grid-column:1/-1}
618
668
  .vsr-s1__row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-columns:subgrid;column-gap:12px;align-items:start}