@phenx-inc/ctlsurf 0.17.5 → 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-CRJW4vYa.js → cssMode-LC2bCRa_.js} +3 -3
  5. package/out/renderer/assets/{freemarker2-IgsORiC6.js → freemarker2-Di7O120K.js} +1 -1
  6. package/out/renderer/assets/{handlebars-pDu2AR4b.js → handlebars-Dt8Fn4oY.js} +1 -1
  7. package/out/renderer/assets/{html-DNF7NUE5.js → html-wsYLMz8Z.js} +1 -1
  8. package/out/renderer/assets/{htmlMode-gbfJICtW.js → htmlMode-DAHN3Rfc.js} +3 -3
  9. package/out/renderer/assets/{index-CbuISbRh.js → index-BSPboVCX.js} +97 -29
  10. package/out/renderer/assets/{index-CBWAeUYW.css → index-CmvYnexl.css} +15 -3
  11. package/out/renderer/assets/{javascript-CzPWOwBB.js → javascript-Ch481vNL.js} +2 -2
  12. package/out/renderer/assets/{jsonMode-CWOEvILb.js → jsonMode-BmUlJiUR.js} +3 -3
  13. package/out/renderer/assets/{liquid-CP-HUzpK.js → liquid-DAvcjBM1.js} +1 -1
  14. package/out/renderer/assets/{lspLanguageFeatures-DsiaqQq_.js → lspLanguageFeatures-fZZkw0qY.js} +1 -1
  15. package/out/renderer/assets/{mdx-DerBk5VK.js → mdx-su1pUNh_.js} +1 -1
  16. package/out/renderer/assets/{python-DkUMRAVL.js → python-U5jDm-Cc.js} +1 -1
  17. package/out/renderer/assets/{razor-C6ynuc0X.js → razor-CE13v8rw.js} +1 -1
  18. package/out/renderer/assets/{tsMode-DXrOkNVu.js → tsMode-Dtr-mLSh.js} +1 -1
  19. package/out/renderer/assets/{typescript-BmhhpI43.js → typescript-MRx8M-TQ.js} +1 -1
  20. package/out/renderer/assets/{xml-CTMRAgAB.js → xml-DYFD_EMc.js} +1 -1
  21. package/out/renderer/assets/{yaml-DYNNafNI.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 +55 -1
  25. package/src/renderer/components/RollingCount.tsx +60 -0
  26. package/src/renderer/components/WorkMapPanel.tsx +24 -1
  27. package/src/renderer/styles.css +15 -3
@@ -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
 
@@ -206,7 +208,10 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
206
208
  return <div className="work-map-branch" key={run.id}>
207
209
  <button className="work-map-branch-heading" onClick={() => toggleThread(run.id)} aria-expanded={expandedThreads.has(run.id)}>
208
210
  <span className={`work-map-branch-point ${failed ? 'failed' : ''}`} />
209
- <span className="work-map-branch-label">{run.label}</span><span className="work-map-count">{run.nodes.length}</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>
210
215
  <span className="work-map-caret">{expandedThreads.has(run.id) ? '▾' : '▸'}</span>
211
216
  </button>
212
217
  {expandedThreads.has(run.id) && <ul className="work-map-steps" aria-label="Activity thread">{run.nodes.map(renderStep)}</ul>}
@@ -228,6 +233,24 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
228
233
  </section>
229
234
  }
230
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
+
231
254
  function highlightSource(text: string, focus?: MapNode['focus']): ReactNode {
232
255
  const ranges = thinkingHighlights(text, focus)
233
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)); }