@phenx-inc/ctlsurf 0.17.4 → 0.17.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +1 -1
  2. package/out/headless/index.mjs +1 -1
  3. package/out/headless/index.mjs.map +1 -1
  4. package/out/renderer/assets/{cssMode-BJfoToLX.js → cssMode-LC2bCRa_.js} +3 -3
  5. package/out/renderer/assets/{freemarker2-T0eMtP1Z.js → freemarker2-Di7O120K.js} +1 -1
  6. package/out/renderer/assets/{handlebars-Dbbib8fi.js → handlebars-Dt8Fn4oY.js} +1 -1
  7. package/out/renderer/assets/{html-BRHT_oxc.js → html-wsYLMz8Z.js} +1 -1
  8. package/out/renderer/assets/{htmlMode-BRlZTxME.js → htmlMode-DAHN3Rfc.js} +3 -3
  9. package/out/renderer/assets/{index-RCAeuJTr.js → index-BSPboVCX.js} +114 -38
  10. package/out/renderer/assets/{index-CBWAeUYW.css → index-CmvYnexl.css} +15 -3
  11. package/out/renderer/assets/{javascript-BbOZOxq9.js → javascript-Ch481vNL.js} +2 -2
  12. package/out/renderer/assets/{jsonMode-CUvAeYcT.js → jsonMode-BmUlJiUR.js} +3 -3
  13. package/out/renderer/assets/{liquid-DRIACo_s.js → liquid-DAvcjBM1.js} +1 -1
  14. package/out/renderer/assets/{lspLanguageFeatures-Ds-3nH9p.js → lspLanguageFeatures-fZZkw0qY.js} +1 -1
  15. package/out/renderer/assets/{mdx-D76jlpLO.js → mdx-su1pUNh_.js} +1 -1
  16. package/out/renderer/assets/{python-CMuRPKJi.js → python-U5jDm-Cc.js} +1 -1
  17. package/out/renderer/assets/{razor-DRoH5mal.js → razor-CE13v8rw.js} +1 -1
  18. package/out/renderer/assets/{tsMode-dPJzgFD7.js → tsMode-Dtr-mLSh.js} +1 -1
  19. package/out/renderer/assets/{typescript-D5USKnLj.js → typescript-MRx8M-TQ.js} +1 -1
  20. package/out/renderer/assets/{xml-qYg1wt2P.js → xml-DYFD_EMc.js} +1 -1
  21. package/out/renderer/assets/{yaml-Uc-CvnvL.js → yaml-B_Zhj_tf.js} +1 -1
  22. package/out/renderer/index.html +2 -2
  23. package/package.json +1 -1
  24. package/scripts/test-work-map-ui.cjs +63 -1
  25. package/src/renderer/components/RollingCount.tsx +60 -0
  26. package/src/renderer/components/WorkMapPanel.tsx +41 -10
  27. package/src/renderer/styles.css +15 -3
@@ -55,8 +55,25 @@ async function main() {
55
55
  }
56
56
  win = new BrowserWindow({ show: false, width: 430, height: 860, webPreferences: { backgroundThrottling: false } })
57
57
  await win.loadFile(path.join(temp, 'index.html'))
58
+ win.webContents.debugger.attach('1.3')
59
+ await win.webContents.debugger.sendCommand('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'no-preference' }] })
58
60
  const run = code => win.webContents.executeJavaScript(code)
61
+ const finishRolls = () => run(`Promise.all(document.querySelector('.work-map-forest').getAnimations({subtree:true}).map(a=>a.finished.catch(()=>{})))`)
62
+ const investigationCount = `document.querySelector('.work-map-activity-count.investigating .work-map-count')`
63
+ const visibleInvestigationCount = `Array.from(${investigationCount}.querySelectorAll('.work-map-counter-reel'),el=>String(Math.round(-new DOMMatrixReadOnly(getComputedStyle(el).transform).m42/parseFloat(getComputedStyle(el).fontSize))%10)).join('')`
59
64
  await run(`new Promise(resolve=>setTimeout(resolve,150))`)
65
+ assert.equal(await run(`document.querySelector('.work-map-goal-heading').getAttribute('aria-expanded')`), 'true')
66
+ assert.equal(await run(`document.querySelector('.work-map-branch-heading').getAttribute('aria-expanded')`), 'false')
67
+ assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 0, 'event groups start collapsed')
68
+ assert.deepEqual(await run(`[...document.querySelectorAll('.work-map-activity-count')].map(el=>el.getAttribute('aria-label'))`), ['Thinking: 1', 'Investigations: 10, 1 running', 'Edits: 9', 'Tests: 9, 1 failed'])
69
+ assert.equal(await run(`${visibleInvestigationCount}`), '10', 'initial counts are settled immediately')
70
+ await run(`window.emitMap({nodes:window.mapSnapshot().nodes.map(n=>n.id==='step-27'?{...n,status:'succeeded'}:n)})`)
71
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
72
+ assert.equal(await run(`${investigationCount}.dataset.value`), '10', 'a tool result does not add another event')
73
+ assert.equal(await run(`document.querySelector('.work-map-activity-count.investigating').getAttribute('aria-label')`), 'Investigations: 10')
74
+ assert.equal(await run(`${investigationCount}.getAnimations({subtree:true}).length`), 0, 'status changes do not roll unchanged counts')
75
+ await run(`document.querySelector('.work-map-branch-heading').click()`)
76
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
60
77
  assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 29, 'choices and actions must share one thread')
61
78
  assert.equal(await run(`document.querySelector('.work-map-view')`), null, 'there are no separate decision/activity tabs')
62
79
  assert.match(await run(`document.querySelector('.work-map-reason').textContent`), /Why.*does not interrupt/)
@@ -87,6 +104,13 @@ async function main() {
87
104
  await run(`document.querySelector('.work-map-branch-heading').click()`)
88
105
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
89
106
  assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 0)
107
+ await run(`window.emitMap({nodes:[...window.mapSnapshot().nodes,{...window.mapSnapshot().nodes.at(-1),id:'collapsed-step',status:'running'}]})`)
108
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
109
+ assert.equal(await run(`document.querySelector('.work-map-branch-heading').getAttribute('aria-expanded')`), 'false', 'new activity never reopens a collapsed investigation')
110
+ assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 0)
111
+ assert.equal(await run(`${investigationCount}.dataset.value`), '12', 'collapsed summaries still receive live events')
112
+ assert(await run(`${investigationCount}.getAnimations({subtree:true}).length>0`), 'live counts roll upward')
113
+ await finishRolls()
90
114
  await run(`document.querySelector('.work-map-branch-heading').click()`)
91
115
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
92
116
  await run(`document.querySelector('.work-map-step-button').click()`)
@@ -101,9 +125,44 @@ async function main() {
101
125
  await run(`document.querySelector('.work-map-scroll').scrollTop=0`)
102
126
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
103
127
  assert(await run(`document.documentElement.scrollWidth<=window.innerWidth`), 'no horizontal overflow at panel width')
128
+ await run(`document.querySelector('.work-map-branch-heading').click()`)
129
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
104
130
  const screenshot = process.env.WORK_MAP_SCREENSHOT || path.join(repo, 'experiments/live-map/.cache/work-map-panel.png')
105
131
  await fs.mkdir(path.dirname(screenshot), { recursive: true })
106
132
  await fs.writeFile(screenshot, (await win.webContents.capturePage()).toPNG())
133
+ await run(`
134
+ window.beforeCountNodes=window.mapSnapshot().nodes;
135
+ window.withInvestigationCount=count=>window.emitMap({nodes:[
136
+ ...window.beforeCountNodes.filter(n=>n.kind!=='step'||n.activity!=='investigating'),
137
+ ...Array.from({length:count},(_,i)=>({...window.beforeCountNodes.find(n=>n.kind==='step'&&n.activity==='investigating'),id:'counter-step-'+i}))
138
+ ]});
139
+ window.withInvestigationCount(99);
140
+ `)
141
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
142
+ await finishRolls()
143
+ assert.equal(await run(visibleInvestigationCount), '99')
144
+ await run(`window.withInvestigationCount(100)`)
145
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
146
+ assert(await run(`${investigationCount}.getAnimations({subtree:true}).length>0`), 'digit carry animates from 99 to 100')
147
+ await finishRolls()
148
+ assert.equal(await run(visibleInvestigationCount), '100')
149
+ for (const count of [101, 105]) {
150
+ await run(`window.withInvestigationCount(${count})`)
151
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
152
+ }
153
+ await finishRolls()
154
+ assert.equal(await run(visibleInvestigationCount), '105', 'bursts settle on the latest count')
155
+ await run(`window.withInvestigationCount(9)`)
156
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
157
+ assert.equal(await run(visibleInvestigationCount), '9', 'trimming or reclassification can decrease a count')
158
+ await win.webContents.debugger.sendCommand('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'reduce' }] })
159
+ await run(`window.withInvestigationCount(10)`)
160
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
161
+ assert.equal(await run(visibleInvestigationCount), '10')
162
+ assert.equal(await run(`${investigationCount}.getAnimations({subtree:true}).length`), 0, 'reduced motion updates counts immediately')
163
+ win.setSize(320, 860)
164
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
165
+ assert(await run(`document.documentElement.scrollWidth<=window.innerWidth`), 'activity icons wrap at narrow panel widths')
107
166
  await run(`document.querySelector('.work-map-footer button').click()`)
108
167
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
109
168
  assert.equal(await run(`document.querySelectorAll('.work-map-goal').length`), 0)
@@ -111,6 +170,9 @@ async function main() {
111
170
  await run(`window.emitMap({thinkingStatus:'unavailable',nodes:window.exampleNodes.filter(n=>n.id!=='thought')})`)
112
171
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
113
172
  assert.match(await run(`document.querySelector('.work-map-thinking-status').textContent`), /omits readable thinking/)
173
+ assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 0, 'events collected after clearing also start collapsed')
174
+ await run(`document.querySelector('.work-map-branch-heading').click()`)
175
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
114
176
  assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 28, 'activity remains visible when thinking is omitted')
115
177
  assert.equal(await run(`document.querySelectorAll('.work-map-intent').length`), 0, 'tool activity must not become invented decisions when thinking is omitted')
116
178
  for (const state of ['checking', 'unavailable']) {
@@ -120,7 +182,7 @@ async function main() {
120
182
  assert(await run(`document.querySelector('[role=switch]').disabled`))
121
183
  assert(await run(`document.querySelector('select').disabled`))
122
184
  }
123
- console.log('PASS unified chronological thread, salient excerpts and reasons, source receipts, action links, omitted-thinking notice, controls and scroll')
185
+ console.log('PASS collapsed event groups, live activity summaries, rolling counts and carry, interrupted rolls, reduced motion, unified chronological thread, source receipts, controls and scroll')
124
186
  console.log('Screenshot:', screenshot)
125
187
  } finally {
126
188
  win?.destroy()
@@ -0,0 +1,60 @@
1
+ import { useLayoutEffect, useRef } from 'react'
2
+
3
+ /** Each place rolls independently, including the carry from 9 to 10. */
4
+ export function RollingCount({ value, label }: { value: number; label?: string }) {
5
+ const previous = useRef(value)
6
+ const increasing = value > previous.current
7
+ useLayoutEffect(() => { previous.current = value }, [value])
8
+
9
+ return <span className="work-map-count" role="img" aria-label={label || String(value)} data-value={value}>
10
+ <span className="work-map-counter" aria-hidden="true">
11
+ {String(value).split('').map((_, index, digits) => {
12
+ const place = digits.length - index - 1
13
+ const turns = Math.floor(value / 10 ** place)
14
+ return <RollingDigit key={place} turns={turns} initial={increasing ? 0 : turns} />
15
+ })}
16
+ </span>
17
+ </span>
18
+ }
19
+
20
+ function RollingDigit({ turns, initial }: { turns: number; initial: number }) {
21
+ const reel = useRef<HTMLSpanElement>(null)
22
+ const previous = useRef(initial)
23
+ const animation = useRef<Animation | null>(null)
24
+ const digit = turns % 10
25
+
26
+ useLayoutEffect(() => {
27
+ const el = reel.current!
28
+ const before = previous.current
29
+ previous.current = turns
30
+ let start = before % 10
31
+ // Continue from the visible position when another event arrives mid-roll.
32
+ if (animation.current) {
33
+ const style = getComputedStyle(el)
34
+ start = (-new DOMMatrixReadOnly(style.transform).m42 / parseFloat(style.fontSize)) % 10
35
+ animation.current.cancel()
36
+ animation.current = null
37
+ }
38
+ if (turns <= before || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
39
+
40
+ const distance = Math.min(turns - before, 10 + (turns - before) % 10)
41
+ let end = before % 10 + distance
42
+ while (end <= start) end += 10
43
+ const roll = el.animate([
44
+ { transform: `translateY(${-start}em)` },
45
+ { transform: `translateY(${-end}em)` },
46
+ ], { duration: 550, easing: 'cubic-bezier(.2, .65, .3, 1)' })
47
+ animation.current = roll
48
+ void roll.finished.then(() => {
49
+ if (animation.current === roll) animation.current = null
50
+ }, () => {})
51
+ }, [turns])
52
+
53
+ useLayoutEffect(() => () => { animation.current?.cancel() }, [])
54
+
55
+ return <span className="work-map-counter-digit">
56
+ <span className="work-map-counter-reel" ref={reel} style={{ transform: `translateY(${-digit}em)` }}>
57
+ {Array.from({ length: 30 }, (_, i) => <span key={i}>{i % 10}</span>)}
58
+ </span>
59
+ </span>
60
+ }
@@ -1,8 +1,10 @@
1
1
  import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'
2
2
  import type { MapActivity, MapIntent, MapNode, MapSettings, MapSnapshot } from '../../shared/workMap'
3
3
  import { thinkingHighlights } from '../lib/workMapHighlights'
4
+ import { RollingCount } from './RollingCount'
4
5
 
5
6
  const ICONS: Record<MapActivity, string> = { thinking: '✦', investigating: '⌕', editing: '✎', testing: '✓', planning: '◇', reporting: '≡' }
7
+ const ACTIVITIES: Record<MapActivity, string> = { thinking: 'Thinking', investigating: 'Investigations', editing: 'Edits', testing: 'Tests', planning: 'Plans', reporting: 'Updates' }
6
8
  const STATUS = { running: 'Running', succeeded: 'Succeeded', failed: 'Failed', unknown: 'Recorded', interrupted: 'Stopped' }
7
9
  const INTENTS: Record<MapIntent, string> = { decision: 'Decision', hypothesis: 'Hypothesis', alternative: 'Alternative', next_step: 'Next step', context: 'Thinking' }
8
10
 
@@ -10,7 +12,8 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
10
12
  const [snapshot, setSnapshot] = useState<MapSnapshot | null>(null)
11
13
  const [error, setError] = useState<string | null>(null)
12
14
  const [saving, setSaving] = useState(false)
13
- const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
15
+ const [collapsedGoals, setCollapsedGoals] = useState<Set<string>>(new Set())
16
+ const [expandedThreads, setExpandedThreads] = useState<Set<string>>(new Set())
14
17
  const [selected, setSelected] = useState<string | null>(null)
15
18
  const [following, setFollowing] = useState(true)
16
19
  const [paths, setPaths] = useState({ pythonPath: '', modelPath: '' })
@@ -62,7 +65,14 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
62
65
  finally { if (alive.current) setSaving(false) }
63
66
  }
64
67
 
65
- const toggleBranch = (id: string) => setCollapsed(prev => {
68
+ const toggleGoal = (id: string) => setCollapsedGoals(prev => {
69
+ const next = new Set(prev)
70
+ if (next.has(id)) next.delete(id)
71
+ else next.add(id)
72
+ return next
73
+ })
74
+
75
+ const toggleThread = (id: string) => setExpandedThreads(prev => {
66
76
  const next = new Set(prev)
67
77
  if (next.has(id)) next.delete(id)
68
78
  else next.add(id)
@@ -188,20 +198,23 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
188
198
  : 'Stated decisions, readable thinking, actions, and results appear in one continuous thread. Open any card to inspect its source.'}</p>
189
199
  </div> : <div className="work-map-forest">
190
200
  {goals.map((goal, i) => <article className="work-map-goal" key={goal.id}>
191
- <button className="work-map-goal-heading" onClick={() => toggleBranch(goal.id)} aria-expanded={!collapsed.has(goal.id)} title={goal.evidence[0]?.text}>
201
+ <button className="work-map-goal-heading" onClick={() => toggleGoal(goal.id)} aria-expanded={!collapsedGoals.has(goal.id)} title={goal.evidence[0]?.text}>
192
202
  <span className="work-map-goal-icon" aria-hidden="true">◎</span><span><span className="work-map-kicker">GOAL {String(i + 1).padStart(2, '0')}</span><strong>{goal.title}</strong></span>
193
- <span className="work-map-caret">{collapsed.has(goal.id) ? '▸' : '▾'}</span>
203
+ <span className="work-map-caret">{collapsedGoals.has(goal.id) ? '▸' : '▾'}</span>
194
204
  </button>
195
- {!collapsed.has(goal.id) && <div className="work-map-branches">
205
+ {!collapsedGoals.has(goal.id) && <div className="work-map-branches">
196
206
  {threadRuns(goal.id).map(run => {
197
207
  const failed = run.nodes.some(n => n.status === 'failed')
198
208
  return <div className="work-map-branch" key={run.id}>
199
- <button className="work-map-branch-heading" onClick={() => toggleBranch(run.id)} aria-expanded={!collapsed.has(run.id)}>
209
+ <button className="work-map-branch-heading" onClick={() => toggleThread(run.id)} aria-expanded={expandedThreads.has(run.id)}>
200
210
  <span className={`work-map-branch-point ${failed ? 'failed' : ''}`} />
201
- <span className="work-map-branch-label">{run.label}</span><span className="work-map-count">{run.nodes.length}</span>
202
- <span className="work-map-caret">{collapsed.has(run.id) ? '▸' : '▾'}</span>
211
+ <span className="work-map-branch-copy">
212
+ <span className="work-map-branch-title"><span className="work-map-branch-label">{run.label}</span><RollingCount value={run.nodes.length} label={`${run.nodes.length} events`} /></span>
213
+ <ActivityCounts nodes={run.nodes} />
214
+ </span>
215
+ <span className="work-map-caret">{expandedThreads.has(run.id) ? '▾' : '▸'}</span>
203
216
  </button>
204
- {!collapsed.has(run.id) && <ul className="work-map-steps" aria-label="Activity thread">{run.nodes.map(renderStep)}</ul>}
217
+ {expandedThreads.has(run.id) && <ul className="work-map-steps" aria-label="Activity thread">{run.nodes.map(renderStep)}</ul>}
205
218
  </div>
206
219
  })}
207
220
  </div>}
@@ -213,13 +226,31 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
213
226
  <footer className="work-map-footer"><span>{decisions.length} {decisions.length === 1 ? 'choice' : 'choices'} · {steps.length} events · local</span>
214
227
  <button disabled={!steps.length || saving} onClick={async () => {
215
228
  setError(null)
216
- try { accept(await window.worker.clearWorkMap(tabId)); setCollapsed(new Set()); setSelected(null); setFollowing(true) }
229
+ try { accept(await window.worker.clearWorkMap(tabId)); setCollapsedGoals(new Set()); setExpandedThreads(new Set()); setSelected(null); setFollowing(true) }
217
230
  catch (err) { if (alive.current) setError(String(err)) }
218
231
  }}>Clear map</button>
219
232
  </footer>
220
233
  </section>
221
234
  }
222
235
 
236
+ function ActivityCounts({ nodes }: { nodes: MapNode[] }) {
237
+ return <span className="work-map-activity-counts">
238
+ {(Object.keys(ACTIVITIES) as MapActivity[]).map(activity => {
239
+ const events = nodes.filter(node => node.activity === activity)
240
+ if (!events.length) return null
241
+ const failed = events.filter(node => node.status === 'failed').length
242
+ const running = events.filter(node => node.status === 'running').length
243
+ const label = `${ACTIVITIES[activity]}: ${events.length}${running ? `, ${running} running` : ''}${failed ? `, ${failed} failed` : ''}`
244
+ return <span key={activity} className={`work-map-activity-count ${activity}`} role="img" aria-label={label} title={label}>
245
+ <span className={`work-map-activity ${activity}`} aria-hidden="true">{ICONS[activity]}</span>
246
+ <RollingCount value={events.length} />
247
+ {running > 0 && <span className="work-map-activity-running" aria-hidden="true" />}
248
+ {failed > 0 && <span className="work-map-activity-alert" aria-hidden="true">!</span>}
249
+ </span>
250
+ })}
251
+ </span>
252
+ }
253
+
223
254
  function highlightSource(text: string, focus?: MapNode['focus']): ReactNode {
224
255
  const ranges = thinkingHighlights(text, focus)
225
256
  if (!ranges.length) return text
@@ -61,11 +61,23 @@
61
61
  .work-map-branch { position: relative; padding: 0 0 17px; }
62
62
  .work-map-branch:last-child { padding-bottom: 0; }
63
63
  .work-map-branch::before { content: ''; position: absolute; left: -18px; top: 12px; width: 15px; height: 1px; background: var(--border-dim); }
64
- .work-map-branch-heading { display: flex; align-items: center; gap: 7px; width: 100%; padding: 5px 0; border: 0; background: transparent; text-align: left; color: var(--text-bright); }
65
- .work-map-branch-point { width: 6px; height: 6px; flex-shrink: 0; border-radius: 2px; background: var(--accent); }
64
+ .work-map-branch-heading { display: flex; align-items: flex-start; gap: 7px; width: 100%; padding: 5px 0; border: 0; background: transparent; text-align: left; color: var(--text-bright); }
65
+ .work-map-branch-point { width: 6px; height: 6px; margin-top: 5px; flex-shrink: 0; border-radius: 2px; background: var(--accent); }
66
66
  .work-map-branch-point.failed { background: var(--danger); }
67
+ .work-map-branch-copy { flex: 1; min-width: 0; }
68
+ .work-map-branch-title { display: flex; align-items: center; gap: 7px; line-height: 1.5; }
67
69
  .work-map-branch-label { min-width: 0; overflow-wrap: anywhere; font-size: 11px; font-weight: 600; }
68
- .work-map-count { padding: 1px 5px; border-radius: 4px; background: var(--bg-chip); color: var(--text-soft); font-size: 9px; }
70
+ .work-map-count { display: inline-flex; flex-shrink: 0; padding: 2px 5px; border-radius: 4px; background: var(--bg-chip); color: var(--text-soft); font-size: 9px; }
71
+ .work-map-counter { display: inline-flex; height: 1em; line-height: 1; font-variant-numeric: tabular-nums; }
72
+ .work-map-counter-digit { display: inline-block; width: 1ch; height: 1em; overflow: hidden; }
73
+ .work-map-counter-reel { display: flex; flex-direction: column; }
74
+ .work-map-counter-reel > span { flex: 0 0 1em; height: 1em; text-align: center; }
75
+ .work-map-activity-counts { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin-top: 7px; }
76
+ .work-map-activity-count { display: inline-flex; align-items: center; gap: 4px; padding: 3px 5px; border: 1px solid var(--border-subtle); border-radius: 5px; background: var(--bg-well); }
77
+ .work-map-activity-count .work-map-activity { width: 14px; height: 14px; font-size: 13px; background: transparent; }
78
+ .work-map-activity-count .work-map-count { padding: 0; background: transparent; font-size: 10px; }
79
+ .work-map-activity-running { width: 4px; height: 4px; border-radius: 50%; background: var(--accent); }
80
+ .work-map-activity-alert { color: var(--danger); font-size: 10px; font-weight: 700; line-height: 1; }
69
81
  .work-map-steps { list-style: none; margin: 5px 0 0 2px; padding: 0 0 0 8px; border-left: 1px solid var(--border-subtle); }
70
82
  .work-map-step { margin: 5px 0; border: 1px solid var(--border-well); background: var(--bg-well); border-radius: 7px; overflow: hidden; }
71
83
  .work-map-step.failed { border-color: color-mix(in srgb, var(--danger) 45%, var(--border-well)); }