@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.
- package/README.md +3 -1
- package/out/headless/index.mjs +1 -1
- package/out/headless/index.mjs.map +1 -1
- package/out/renderer/assets/{cssMode-LC2bCRa_.js → cssMode-DOrZXO36.js} +3 -3
- package/out/renderer/assets/{freemarker2-Di7O120K.js → freemarker2-5u4104Xl.js} +1 -1
- package/out/renderer/assets/{handlebars-Dt8Fn4oY.js → handlebars-C_laShLt.js} +1 -1
- package/out/renderer/assets/{html-wsYLMz8Z.js → html-DO3EoflS.js} +1 -1
- package/out/renderer/assets/{htmlMode-DAHN3Rfc.js → htmlMode-Cwu8rsx-.js} +3 -3
- package/out/renderer/assets/{index-BSPboVCX.js → index-CHdMn4av.js} +71 -53
- package/out/renderer/assets/{javascript-Ch481vNL.js → javascript-BPewzTWL.js} +2 -2
- package/out/renderer/assets/{jsonMode-BmUlJiUR.js → jsonMode-BjgG963U.js} +3 -3
- package/out/renderer/assets/{liquid-DAvcjBM1.js → liquid-K0EVwJ4n.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-fZZkw0qY.js → lspLanguageFeatures-m4j5zxQl.js} +1 -1
- package/out/renderer/assets/{mdx-su1pUNh_.js → mdx-DG0llsx7.js} +1 -1
- package/out/renderer/assets/{python-U5jDm-Cc.js → python-CwfkGHje.js} +1 -1
- package/out/renderer/assets/{razor-CE13v8rw.js → razor-DxTakA2q.js} +1 -1
- package/out/renderer/assets/{tsMode-Dtr-mLSh.js → tsMode-BANGjk9B.js} +1 -1
- package/out/renderer/assets/{typescript-MRx8M-TQ.js → typescript-DiF0ejYV.js} +1 -1
- package/out/renderer/assets/{xml-DYFD_EMc.js → xml-BLsIDJrq.js} +1 -1
- package/out/renderer/assets/{yaml-B_Zhj_tf.js → yaml-BWqzZSkF.js} +1 -1
- package/out/renderer/index.html +1 -1
- package/package.json +1 -1
- package/scripts/test-work-map-ui.cjs +34 -12
- package/src/renderer/App.tsx +10 -1
- package/src/renderer/components/TicketPanel.tsx +24 -7
- 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')
|
|
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`),
|
|
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`),
|
|
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`),
|
|
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
|
|
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()
|
package/src/renderer/App.tsx
CHANGED
|
@@ -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
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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));
|
|
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>
|