@phenx-inc/ctlsurf 0.17.6 → 0.17.7

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 (26) hide show
  1. package/README.md +3 -1
  2. package/out/headless/index.mjs +1 -1
  3. package/out/headless/index.mjs.map +1 -1
  4. package/out/renderer/assets/{cssMode-LC2bCRa_.js → cssMode-DOrZXO36.js} +3 -3
  5. package/out/renderer/assets/{freemarker2-Di7O120K.js → freemarker2-5u4104Xl.js} +1 -1
  6. package/out/renderer/assets/{handlebars-Dt8Fn4oY.js → handlebars-C_laShLt.js} +1 -1
  7. package/out/renderer/assets/{html-wsYLMz8Z.js → html-DO3EoflS.js} +1 -1
  8. package/out/renderer/assets/{htmlMode-DAHN3Rfc.js → htmlMode-Cwu8rsx-.js} +3 -3
  9. package/out/renderer/assets/{index-BSPboVCX.js → index-CHdMn4av.js} +71 -53
  10. package/out/renderer/assets/{javascript-Ch481vNL.js → javascript-BPewzTWL.js} +2 -2
  11. package/out/renderer/assets/{jsonMode-BmUlJiUR.js → jsonMode-BjgG963U.js} +3 -3
  12. package/out/renderer/assets/{liquid-DAvcjBM1.js → liquid-K0EVwJ4n.js} +1 -1
  13. package/out/renderer/assets/{lspLanguageFeatures-fZZkw0qY.js → lspLanguageFeatures-m4j5zxQl.js} +1 -1
  14. package/out/renderer/assets/{mdx-su1pUNh_.js → mdx-DG0llsx7.js} +1 -1
  15. package/out/renderer/assets/{python-U5jDm-Cc.js → python-CwfkGHje.js} +1 -1
  16. package/out/renderer/assets/{razor-CE13v8rw.js → razor-DxTakA2q.js} +1 -1
  17. package/out/renderer/assets/{tsMode-Dtr-mLSh.js → tsMode-BANGjk9B.js} +1 -1
  18. package/out/renderer/assets/{typescript-MRx8M-TQ.js → typescript-DiF0ejYV.js} +1 -1
  19. package/out/renderer/assets/{xml-DYFD_EMc.js → xml-BLsIDJrq.js} +1 -1
  20. package/out/renderer/assets/{yaml-B_Zhj_tf.js → yaml-BWqzZSkF.js} +1 -1
  21. package/out/renderer/index.html +1 -1
  22. package/package.json +1 -1
  23. package/scripts/test-work-map-ui.cjs +34 -12
  24. package/src/renderer/App.tsx +10 -1
  25. package/src/renderer/components/TicketPanel.tsx +24 -7
  26. package/src/renderer/components/WorkMapPanel.tsx +23 -25
@@ -62,9 +62,10 @@ async function main() {
62
62
  const investigationCount = `document.querySelector('.work-map-activity-count.investigating .work-map-count')`
63
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('')`
64
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')
65
+ assert.equal(await run(`document.querySelector('button.work-map-goal-heading')`), null, 'goals cannot hide reasoning and updates')
66
+ assert.equal(await run(`document.querySelector('button.work-map-branch-heading').getAttribute('aria-expanded')`), 'false')
67
+ assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 1, 'thinking and decisions stay visible while technical activity starts collapsed')
68
+ assert.match(await run(`document.querySelector('.work-map-step').textContent`), /Preserve the reader/)
68
69
  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
70
  assert.equal(await run(`${visibleInvestigationCount}`), '10', 'initial counts are settled immediately')
70
71
  await run(`window.emitMap({nodes:window.mapSnapshot().nodes.map(n=>n.id==='step-27'?{...n,status:'succeeded'}:n)})`)
@@ -72,7 +73,7 @@ async function main() {
72
73
  assert.equal(await run(`${investigationCount}.dataset.value`), '10', 'a tool result does not add another event')
73
74
  assert.equal(await run(`document.querySelector('.work-map-activity-count.investigating').getAttribute('aria-label')`), 'Investigations: 10')
74
75
  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(`document.querySelector('button.work-map-branch-heading').click()`)
76
77
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
77
78
  assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 29, 'choices and actions must share one thread')
78
79
  assert.equal(await run(`document.querySelector('.work-map-view')`), null, 'there are no separate decision/activity tabs')
@@ -101,17 +102,17 @@ async function main() {
101
102
  await run(`new Promise(resolve=>setTimeout(resolve,80))`)
102
103
  assert.equal(await run(`document.querySelector('.work-map-scroll').scrollTop`), 0, 'updates preserve scroll position while reading history')
103
104
  assert(await run(`!!document.querySelector('.work-map-jump')`))
104
- await run(`document.querySelector('.work-map-branch-heading').click()`)
105
+ await run(`document.querySelector('button.work-map-branch-heading').click()`)
105
106
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
106
- assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 0)
107
+ assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 1, 'collapsing technical activity keeps its preceding choice visible')
107
108
  await run(`window.emitMap({nodes:[...window.mapSnapshot().nodes,{...window.mapSnapshot().nodes.at(-1),id:'collapsed-step',status:'running'}]})`)
108
109
  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)
110
+ assert.equal(await run(`document.querySelector('button.work-map-branch-heading').getAttribute('aria-expanded')`), 'false', 'new activity never reopens a collapsed investigation')
111
+ assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 1)
111
112
  assert.equal(await run(`${investigationCount}.dataset.value`), '12', 'collapsed summaries still receive live events')
112
113
  assert(await run(`${investigationCount}.getAnimations({subtree:true}).length>0`), 'live counts roll upward')
113
114
  await finishRolls()
114
- await run(`document.querySelector('.work-map-branch-heading').click()`)
115
+ await run(`document.querySelector('button.work-map-branch-heading').click()`)
115
116
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
116
117
  await run(`document.querySelector('.work-map-step-button').click()`)
117
118
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
@@ -125,7 +126,8 @@ async function main() {
125
126
  await run(`document.querySelector('.work-map-scroll').scrollTop=0`)
126
127
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
127
128
  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(`document.querySelector('button.work-map-branch-heading').click()`)
130
+ await run(`document.querySelector('.work-map-step-button[aria-expanded="true"]')?.click(); document.querySelector('.work-map-scroll').scrollTop=0`)
129
131
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
130
132
  const screenshot = process.env.WORK_MAP_SCREENSHOT || path.join(repo, 'experiments/live-map/.cache/work-map-panel.png')
131
133
  await fs.mkdir(path.dirname(screenshot), { recursive: true })
@@ -163,6 +165,26 @@ async function main() {
163
165
  win.setSize(320, 860)
164
166
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
165
167
  assert(await run(`document.documentElement.scrollWidth<=window.innerWidth`), 'activity icons wrap at narrow panel widths')
168
+ await run(`window.emitMap({nodes:[...window.exampleNodes,
169
+ ...['planning','reporting','thinking','investigating','editing','testing'].map((activity,i)=>({
170
+ ...window.exampleNodes[2],id:'update-'+i,activity,intent:i===0?'next_step':i===1?'context':undefined,
171
+ title:'Visible agent update '+i,focus:undefined,
172
+ evidence:[{...window.exampleNodes[2].evidence[0],id:'update-event-'+i,kind:i===2?'thinking':'message'}]
173
+ }))
174
+ ]})`)
175
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
176
+ assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 7, 'plans, reporting and readable text stay visible regardless of model activity labels')
177
+ assert.equal(await run(`document.querySelectorAll('button.work-map-branch-heading').length`), 1, 'narrative-only groups have no collapse control')
178
+ await run(`document.querySelector('button.work-map-branch-heading').click()`)
179
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
180
+ assert.deepEqual(await run(`[...document.querySelectorAll('.work-map-step-title')].map(e=>e.textContent)`), [
181
+ 'Preserve the reader’s position instead of forcing the viewport down',
182
+ ...Array.from({length:28},(_,i)=>['Inspect the viewport and scroll listeners','Preserve the reader’s position during new output','Check terminal scroll regression'][i%3]),
183
+ ...Array.from({length:6},(_,i)=>'Visible agent update '+i)
184
+ ], 'expanding technical details preserves the chronology of surrounding updates')
185
+ await run(`document.querySelector('button.work-map-branch-heading').click()`)
186
+ await run(`new Promise(resolve=>setTimeout(resolve,30))`)
187
+ assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 7, 'collapsing technical activity cannot hide later reporting')
166
188
  await run(`document.querySelector('.work-map-footer button').click()`)
167
189
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
168
190
  assert.equal(await run(`document.querySelectorAll('.work-map-goal').length`), 0)
@@ -171,7 +193,7 @@ async function main() {
171
193
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
172
194
  assert.match(await run(`document.querySelector('.work-map-thinking-status').textContent`), /omits readable thinking/)
173
195
  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()`)
196
+ await run(`document.querySelector('button.work-map-branch-heading').click()`)
175
197
  await run(`new Promise(resolve=>setTimeout(resolve,30))`)
176
198
  assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 28, 'activity remains visible when thinking is omitted')
177
199
  assert.equal(await run(`document.querySelectorAll('.work-map-intent').length`), 0, 'tool activity must not become invented decisions when thinking is omitted')
@@ -182,7 +204,7 @@ async function main() {
182
204
  assert(await run(`document.querySelector('[role=switch]').disabled`))
183
205
  assert(await run(`document.querySelector('select').disabled`))
184
206
  }
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')
207
+ console.log('PASS visible thinking, decisions, plans and reporting, collapsed technical activity, live summaries, rolling counts, chronology, source receipts, controls and scroll')
186
208
  console.log('Screenshot:', screenshot)
187
209
  } finally {
188
210
  win?.destroy()
@@ -201,6 +201,14 @@ export default function App() {
201
201
  const [sessionStart, setSessionStart] = useState<Date | null>(null)
202
202
  const [layout, setLayout] = useState<LayoutNode>(DEFAULT_LAYOUT)
203
203
  const [hiddenPanes, setHiddenPanes] = useState<Set<string>>(new Set())
204
+ const ticketStartupPending = useRef(true)
205
+ const handleInitialTicketsLoaded = useCallback((empty: boolean) => {
206
+ if (!ticketStartupPending.current) return
207
+ ticketStartupPending.current = false
208
+ if (!empty) return
209
+ setLayout(previous => removePane(previous, 'tickets') || previous)
210
+ setHiddenPanes(previous => new Set([...previous, 'tickets']))
211
+ }, [])
204
212
  const [showSettings, setShowSettings] = useState(false)
205
213
  const [wsStatus, setWsStatus] = useState('disconnected')
206
214
  const [cwd, setCwd] = useState<string | null>(null)
@@ -529,6 +537,7 @@ export default function App() {
529
537
 
530
538
  // Toggle pane: hide removes from layout tree, show re-inserts
531
539
  const togglePane = useCallback((id: string) => {
540
+ if (id === 'tickets') ticketStartupPending.current = false
532
541
  if (id === 'workmap' && !visiblePaneIds.includes(id) && !canOpenWorkMap()) return
533
542
  if (visiblePaneIds.includes(id)) {
534
543
  // Hide: remove from tree
@@ -680,7 +689,7 @@ export default function App() {
680
689
  ),
681
690
  },
682
691
  { id: 'ctlsurf', label: 'ctlsurf', content: <CtlsurfPanel navigate={ctlsurfNav} /> },
683
- { id: 'tickets', label: 'Tickets', content: <TicketPane onSendToTerminal={handleSendToTerminal} /> },
692
+ { id: 'tickets', label: 'Tickets', content: <TicketPane onSendToTerminal={handleSendToTerminal} onInitialLoad={handleInitialTicketsLoaded} /> },
684
693
  { id: 'workmap', label: 'Work map', content: <WorkMapPanel key={activeTabId} tabId={activeTabId} /> },
685
694
  ]
686
695
 
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback } from 'react'
1
+ import { useState, useEffect, useCallback, useRef } from 'react'
2
2
 
3
3
  interface Option { value: string; color: string }
4
4
 
@@ -167,7 +167,9 @@ function TicketCard({ ticket, onClick, onSend }: { ticket: Ticket; onClick: () =
167
167
  // Docked side-pane variant of the ticket UI: header + list/form body + footer.
168
168
  // It only mounts when its pane is visible, so it loads on mount and refreshes
169
169
  // when the connected project (cwd) changes. Closed via the pane's own header.
170
- export function TicketPane({ onSendToTerminal }: { onSendToTerminal: (text: string) => void }) {
170
+ export function TicketPane({ onSendToTerminal, onInitialLoad }: { onSendToTerminal: (text: string) => void; onInitialLoad?: (empty: boolean) => void }) {
171
+ const initialLoadHandled = useRef(false)
172
+ const listRevision = useRef(0)
171
173
  const [project, setProject] = useState<string | null>(null)
172
174
  const [view, setView] = useState<View>('list')
173
175
 
@@ -193,20 +195,25 @@ export function TicketPane({ onSendToTerminal }: { onSendToTerminal: (text: stri
193
195
  const [save, setSave] = useState<SaveState>({ kind: 'idle' })
194
196
 
195
197
  const refreshList = useCallback(async () => {
198
+ const revision = ++listRevision.current
196
199
  setListLoading(true)
197
200
  setListError(null)
198
201
  try {
199
202
  const r = await window.worker.listTickets()
203
+ if (revision !== listRevision.current) return null
200
204
  if (r?.ok) {
201
205
  setTickets(r.tickets || [])
202
206
  setRefs(r.blockId && r.pageId ? { blockId: r.blockId, pageId: r.pageId } : null)
203
207
  } else { setTickets([]); setRefs(null); setListError(r?.error || 'Failed to load tickets') }
208
+ return r?.ok ? (r.tickets || []).length === 0 : null
204
209
  } catch (err: any) {
210
+ if (revision !== listRevision.current) return null
205
211
  setTickets([])
206
212
  setRefs(null)
207
213
  setListError(err?.message || 'Failed to load tickets')
214
+ return null
208
215
  } finally {
209
- setListLoading(false)
216
+ if (revision === listRevision.current) setListLoading(false)
210
217
  }
211
218
  }, [])
212
219
 
@@ -218,12 +225,22 @@ export function TicketPane({ onSendToTerminal }: { onSendToTerminal: (text: stri
218
225
  .then(r => { if (!cancelled) setProject(r?.name ?? null) })
219
226
  .catch(() => { if (!cancelled) setProject(null) })
220
227
  loadProject()
221
- void refreshList()
222
- const unsub = window.worker.onCwdChanged(() => { loadProject(); void refreshList() })
223
- return () => { cancelled = true; unsub() }
224
- }, [refreshList])
228
+ // The first successful load may follow choosing the startup session.
229
+ const loadTickets = () => void refreshList().then(empty => {
230
+ if (cancelled || empty === null || initialLoadHandled.current) return
231
+ initialLoadHandled.current = true
232
+ onInitialLoad?.(empty)
233
+ })
234
+ loadTickets()
235
+ const unsub = window.worker.onCwdChanged(() => {
236
+ loadProject()
237
+ loadTickets()
238
+ })
239
+ return () => { cancelled = true; listRevision.current++; unsub() }
240
+ }, [refreshList, onInitialLoad])
225
241
 
226
242
  const openNewForm = useCallback(() => {
243
+ initialLoadHandled.current = true
227
244
  setEditingId(null)
228
245
  setEditingBlockId(null)
229
246
  setTitle('')
@@ -12,7 +12,6 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
12
12
  const [snapshot, setSnapshot] = useState<MapSnapshot | null>(null)
13
13
  const [error, setError] = useState<string | null>(null)
14
14
  const [saving, setSaving] = useState(false)
15
- const [collapsedGoals, setCollapsedGoals] = useState<Set<string>>(new Set())
16
15
  const [expandedThreads, setExpandedThreads] = useState<Set<string>>(new Set())
17
16
  const [selected, setSelected] = useState<string | null>(null)
18
17
  const [following, setFollowing] = useState(true)
@@ -65,13 +64,6 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
65
64
  finally { if (alive.current) setSaving(false) }
66
65
  }
67
66
 
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
67
  const toggleThread = (id: string) => setExpandedThreads(prev => {
76
68
  const next = new Set(prev)
77
69
  if (next.has(id)) next.delete(id)
@@ -88,15 +80,19 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
88
80
  // Render consecutive runs in event order. Reusing a topic must not move new
89
81
  // actions back up the page or separate them from the preceding decision.
90
82
  function threadRuns(goalId: string) {
91
- const runs: Array<{ id: string; key: string | null; label: string; nodes: MapNode[] }> = []
83
+ const runs: Array<{ id: string; key: string | null; label: string; technical: boolean; nodes: MapNode[] }> = []
92
84
  for (const node of steps) {
93
85
  if (nodeById.get(node.parentId || '')?.parentId !== goalId) continue
94
86
  const textSource = node.evidence[0]?.kind === 'thinking' || node.evidence[0]?.kind === 'message'
95
87
  const anchor = textSource ? node : nodeById.get(node.contextId || '')
96
88
  const key = anchor?.id || node.parentId
89
+ // Only tool activity can be tucked away. Keep stated reasoning and
90
+ // updates visible even when their topic or activity is reclassified.
91
+ const technical = !node.intent && ['investigating', 'editing', 'testing'].includes(node.activity)
92
+ && node.evidence.length > 0 && node.evidence.every(e => e.kind === 'tool' || e.kind === 'result')
97
93
  const previous = runs.at(-1)
98
- if (previous?.key === key) previous.nodes.push(node)
99
- else runs.push({ id: `thread-${node.id}`, key, label: nodeById.get(anchor?.parentId || node.parentId || '')?.title || 'Activity', nodes: [node] })
94
+ if (previous?.key === key && previous.technical === technical) previous.nodes.push(node)
95
+ else runs.push({ id: `thread-${node.id}`, key, technical, label: nodeById.get(anchor?.parentId || node.parentId || '')?.title || 'Activity', nodes: [node] })
100
96
  }
101
97
  return runs
102
98
  }
@@ -198,26 +194,28 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
198
194
  : 'Stated decisions, readable thinking, actions, and results appear in one continuous thread. Open any card to inspect its source.'}</p>
199
195
  </div> : <div className="work-map-forest">
200
196
  {goals.map((goal, i) => <article className="work-map-goal" key={goal.id}>
201
- <button className="work-map-goal-heading" onClick={() => toggleGoal(goal.id)} aria-expanded={!collapsedGoals.has(goal.id)} title={goal.evidence[0]?.text}>
197
+ <div className="work-map-goal-heading" title={goal.evidence[0]?.text}>
202
198
  <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>
203
- <span className="work-map-caret">{collapsedGoals.has(goal.id) ? '▸' : '▾'}</span>
204
- </button>
205
- {!collapsedGoals.has(goal.id) && <div className="work-map-branches">
199
+ </div>
200
+ <div className="work-map-branches">
206
201
  {threadRuns(goal.id).map(run => {
207
202
  const failed = run.nodes.some(n => n.status === 'failed')
203
+ const heading = <>
204
+ <span className={`work-map-branch-point ${failed ? 'failed' : ''}`} />
205
+ <span className="work-map-branch-copy">
206
+ <span className="work-map-branch-title"><span className="work-map-branch-label">{run.technical ? `Technical activity · ${run.label}` : run.label}</span><RollingCount value={run.nodes.length} label={`${run.nodes.length} events`} /></span>
207
+ <ActivityCounts nodes={run.nodes} />
208
+ </span>
209
+ </>
208
210
  return <div className="work-map-branch" key={run.id}>
209
- <button className="work-map-branch-heading" onClick={() => toggleThread(run.id)} aria-expanded={expandedThreads.has(run.id)}>
210
- <span className={`work-map-branch-point ${failed ? 'failed' : ''}`} />
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>
211
+ {run.technical ? <button className="work-map-branch-heading" onClick={() => toggleThread(run.id)} aria-expanded={expandedThreads.has(run.id)}>
212
+ {heading}
215
213
  <span className="work-map-caret">{expandedThreads.has(run.id) ? '▾' : '▸'}</span>
216
- </button>
217
- {expandedThreads.has(run.id) && <ul className="work-map-steps" aria-label="Activity thread">{run.nodes.map(renderStep)}</ul>}
214
+ </button> : <div className="work-map-branch-heading">{heading}</div>}
215
+ {(!run.technical || expandedThreads.has(run.id)) && <ul className="work-map-steps" aria-label="Activity thread">{run.nodes.map(renderStep)}</ul>}
218
216
  </div>
219
217
  })}
220
- </div>}
218
+ </div>
221
219
  </article>)}
222
220
  {snapshot?.dropped ? <p className="work-map-retained">Earlier activity trimmed · keeping the latest 240 nodes</p> : null}
223
221
  </div>}
@@ -226,7 +224,7 @@ export function WorkMapPanel({ tabId }: { tabId: string }) {
226
224
  <footer className="work-map-footer"><span>{decisions.length} {decisions.length === 1 ? 'choice' : 'choices'} · {steps.length} events · local</span>
227
225
  <button disabled={!steps.length || saving} onClick={async () => {
228
226
  setError(null)
229
- try { accept(await window.worker.clearWorkMap(tabId)); setCollapsedGoals(new Set()); setExpandedThreads(new Set()); setSelected(null); setFollowing(true) }
227
+ try { accept(await window.worker.clearWorkMap(tabId)); setExpandedThreads(new Set()); setSelected(null); setFollowing(true) }
230
228
  catch (err) { if (alive.current) setError(String(err)) }
231
229
  }}>Clear map</button>
232
230
  </footer>