@phenx-inc/ctlsurf 0.17.2 → 0.17.4
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 +25 -2
- package/bin/ctlsurf-worker.js +243 -44
- package/out/headless/index.mjs +1033 -128
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +933 -17
- package/out/preload/index.js +15 -0
- package/out/renderer/assets/{cssMode-BtQxI2Eb.js → cssMode-BJfoToLX.js} +3 -3
- package/out/renderer/assets/{freemarker2-DRW5dmY5.js → freemarker2-T0eMtP1Z.js} +1 -1
- package/out/renderer/assets/{handlebars-CFLF3cP7.js → handlebars-Dbbib8fi.js} +1 -1
- package/out/renderer/assets/{html-DtnBOkwp.js → html-BRHT_oxc.js} +1 -1
- package/out/renderer/assets/{htmlMode-DMVysDCg.js → htmlMode-BRlZTxME.js} +3 -3
- package/out/renderer/assets/{index-CEjwKjtd.css → index-CBWAeUYW.css} +91 -0
- package/out/renderer/assets/{index-Dm70hk5s.js → index-RCAeuJTr.js} +476 -411
- package/out/renderer/assets/{javascript-ChlyA1sf.js → javascript-BbOZOxq9.js} +2 -2
- package/out/renderer/assets/{jsonMode-D1_oHzeN.js → jsonMode-CUvAeYcT.js} +3 -3
- package/out/renderer/assets/{liquid-D4ftdy1G.js → liquid-DRIACo_s.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Ckqu5ubG.js → lspLanguageFeatures-Ds-3nH9p.js} +1 -1
- package/out/renderer/assets/{mdx-B9wsVYSC.js → mdx-D76jlpLO.js} +1 -1
- package/out/renderer/assets/{python-e23tD6IS.js → python-CMuRPKJi.js} +1 -1
- package/out/renderer/assets/{razor-CrDyXj3d.js → razor-DRoH5mal.js} +1 -1
- package/out/renderer/assets/{tsMode-BwGC5o4O.js → tsMode-dPJzgFD7.js} +1 -1
- package/out/renderer/assets/{typescript-Do-98d_O.js → typescript-D5USKnLj.js} +1 -1
- package/out/renderer/assets/{xml-Dpdu5R3m.js → xml-qYg1wt2P.js} +1 -1
- package/out/renderer/assets/{yaml-B0Zt7LCJ.js → yaml-Uc-CvnvL.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +4 -2
- package/scripts/check-work-map-gpu.cjs +62 -0
- package/scripts/check-work-map-gpu.d.cts +13 -0
- package/scripts/postinstall.cjs +44 -0
- package/scripts/prepare-work-map.py +49 -0
- package/scripts/setup-ui.cjs +161 -0
- package/scripts/test-work-map-capability.cjs +45 -0
- package/scripts/test-work-map-ui.cjs +130 -0
- package/scripts/test-work-map.cjs +352 -0
- package/scripts/work-map-clef.py +81 -0
- package/scripts/work-map-setup.cjs +127 -0
- package/src/main/index.ts +43 -1
- package/src/main/observer/observer.ts +2 -0
- package/src/main/orchestrator.ts +39 -4
- package/src/main/transcripts.ts +35 -16
- package/src/main/workMap/clef.ts +156 -0
- package/src/main/workMap/normalize.ts +172 -0
- package/src/main/workMap/source.ts +28 -0
- package/src/main/workMap/tree.ts +199 -0
- package/src/main/workMap/workMap.ts +244 -0
- package/src/preload/index.ts +17 -0
- package/src/renderer/App.tsx +15 -16
- package/src/renderer/components/ObserverPanel.tsx +3 -0
- package/src/renderer/components/SettingsDialog.tsx +0 -104
- package/src/renderer/components/WorkMapPanel.tsx +235 -0
- package/src/renderer/components/WorkMapToggle.tsx +50 -0
- package/src/renderer/lib/workMapHighlights.ts +41 -0
- package/src/renderer/styles.css +91 -0
- package/src/shared/workMap.d.ts +70 -0
|
@@ -69,60 +69,21 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
|
69
69
|
const [saved, setSaved] = useState(false)
|
|
70
70
|
const [version, setVersion] = useState('')
|
|
71
71
|
|
|
72
|
-
// Observer (global) — the key is write-only over IPC; we only learn
|
|
73
|
-
// whether one is configured (hasApiKey) and from where (keySource).
|
|
74
|
-
const [obsEnabled, setObsEnabled] = useState(false)
|
|
75
|
-
const [obsApiKey, setObsApiKey] = useState('')
|
|
76
|
-
const [obsHasKey, setObsHasKey] = useState(false)
|
|
77
|
-
const [obsKeySource, setObsKeySource] = useState<'settings' | 'env' | null>(null)
|
|
78
|
-
const [obsCapUsd, setObsCapUsd] = useState(1)
|
|
79
|
-
const [obsSaved, setObsSaved] = useState(false)
|
|
80
|
-
|
|
81
72
|
const loadProfiles = async () => {
|
|
82
73
|
const data = await window.worker.listProfiles() as ProfileListResponse
|
|
83
74
|
setProfileList(data)
|
|
84
75
|
setActiveProfileId(data.activeProfile)
|
|
85
76
|
}
|
|
86
77
|
|
|
87
|
-
const loadObserver = async () => {
|
|
88
|
-
try {
|
|
89
|
-
const o = await window.worker.getObserverSettings()
|
|
90
|
-
setObsEnabled(o.enabled)
|
|
91
|
-
setObsHasKey(o.hasApiKey)
|
|
92
|
-
setObsKeySource(o.keySource)
|
|
93
|
-
setObsCapUsd(o.dailyCapUsd)
|
|
94
|
-
setObsApiKey('')
|
|
95
|
-
} catch { /* older main process without observer IPC */ }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
78
|
useEffect(() => {
|
|
99
79
|
if (open) {
|
|
100
80
|
loadProfiles()
|
|
101
|
-
loadObserver()
|
|
102
81
|
setEditingId(null)
|
|
103
82
|
setSaved(false)
|
|
104
|
-
setObsSaved(false)
|
|
105
83
|
window.worker.getVersion().then(setVersion).catch(() => { /* ignore */ })
|
|
106
84
|
}
|
|
107
85
|
}, [open])
|
|
108
86
|
|
|
109
|
-
const handleObserverSave = async () => {
|
|
110
|
-
const data: { enabled: boolean; anthropicApiKey?: string; dailyCapUsd?: number } = {
|
|
111
|
-
enabled: obsEnabled,
|
|
112
|
-
dailyCapUsd: Math.max(0.05, obsCapUsd) || 1,
|
|
113
|
-
}
|
|
114
|
-
// Only send the key if the user typed a new one (blank = keep stored).
|
|
115
|
-
if (obsApiKey.trim()) {
|
|
116
|
-
data.anthropicApiKey = obsApiKey.trim()
|
|
117
|
-
}
|
|
118
|
-
const o = await window.worker.setObserverSettings(data)
|
|
119
|
-
setObsHasKey(o.hasApiKey)
|
|
120
|
-
setObsKeySource(o.keySource)
|
|
121
|
-
setObsApiKey('')
|
|
122
|
-
setObsSaved(true)
|
|
123
|
-
setTimeout(() => setObsSaved(false), 900)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
87
|
const startEditing = (profile: ProfileSummary) => {
|
|
127
88
|
setEditingId(profile.id)
|
|
128
89
|
setName(profile.name)
|
|
@@ -359,71 +320,6 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
|
359
320
|
</div>
|
|
360
321
|
)}
|
|
361
322
|
|
|
362
|
-
{!editingId && (
|
|
363
|
-
// ─── Observer (global) ─────────────────
|
|
364
|
-
<div className="settings-section">
|
|
365
|
-
<h4>Observer</h4>
|
|
366
|
-
<p className="settings-hint">
|
|
367
|
-
Reviews agent sessions for assumptions, unverified claims, and
|
|
368
|
-
dependency/license risk, tracks time per work item, and posts a
|
|
369
|
-
session digest to the project's Agent Datastore page.
|
|
370
|
-
</p>
|
|
371
|
-
|
|
372
|
-
<label className="settings-checkbox">
|
|
373
|
-
<input
|
|
374
|
-
type="checkbox"
|
|
375
|
-
checked={obsEnabled}
|
|
376
|
-
onChange={e => setObsEnabled(e.target.checked)}
|
|
377
|
-
/>
|
|
378
|
-
<span>Enable Observer</span>
|
|
379
|
-
<span className="settings-hint">
|
|
380
|
-
Applies to newly started agent tabs. Advisory only — never
|
|
381
|
-
blocks or slows the agent.
|
|
382
|
-
</span>
|
|
383
|
-
</label>
|
|
384
|
-
|
|
385
|
-
<label>
|
|
386
|
-
Anthropic API Key
|
|
387
|
-
<input
|
|
388
|
-
type="password"
|
|
389
|
-
value={obsApiKey}
|
|
390
|
-
onChange={e => setObsApiKey(e.target.value)}
|
|
391
|
-
placeholder={
|
|
392
|
-
obsKeySource === 'settings' ? '***configured*** (leave blank to keep)'
|
|
393
|
-
: obsKeySource === 'env' ? 'using ANTHROPIC_API_KEY from environment'
|
|
394
|
-
: 'sk-ant-…'
|
|
395
|
-
}
|
|
396
|
-
/>
|
|
397
|
-
<span className="settings-hint">
|
|
398
|
-
Your own Anthropic key — separate from the ctlsurf profile key.
|
|
399
|
-
Without one, the Observer still records the session ledger,
|
|
400
|
-
Tier-0 tags, and spans, but skips model review and digests.
|
|
401
|
-
</span>
|
|
402
|
-
</label>
|
|
403
|
-
|
|
404
|
-
<label>
|
|
405
|
-
Daily budget cap (USD)
|
|
406
|
-
<input
|
|
407
|
-
type="number"
|
|
408
|
-
min={0.05}
|
|
409
|
-
step={0.25}
|
|
410
|
-
value={obsCapUsd}
|
|
411
|
-
onChange={e => setObsCapUsd(parseFloat(e.target.value) || 1)}
|
|
412
|
-
/>
|
|
413
|
-
<span className="settings-hint">
|
|
414
|
-
Model review pauses for the day once spend reaches the cap;
|
|
415
|
-
Tier-0 tagging and spans continue.
|
|
416
|
-
</span>
|
|
417
|
-
</label>
|
|
418
|
-
|
|
419
|
-
<div className="settings-actions">
|
|
420
|
-
<button className="btn-primary" onClick={handleObserverSave}>
|
|
421
|
-
{obsSaved ? 'Saved!' : 'Save Observer Settings'}
|
|
422
|
-
</button>
|
|
423
|
-
</div>
|
|
424
|
-
</div>
|
|
425
|
-
)}
|
|
426
|
-
|
|
427
323
|
<div className="settings-section settings-about">
|
|
428
324
|
<h4>About</h4>
|
|
429
325
|
<div className="about-name">ctlsurf{version ? ` · v${version}` : ''}</div>
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'
|
|
2
|
+
import type { MapActivity, MapIntent, MapNode, MapSettings, MapSnapshot } from '../../shared/workMap'
|
|
3
|
+
import { thinkingHighlights } from '../lib/workMapHighlights'
|
|
4
|
+
|
|
5
|
+
const ICONS: Record<MapActivity, string> = { thinking: '✦', investigating: '⌕', editing: '✎', testing: '✓', planning: '◇', reporting: '≡' }
|
|
6
|
+
const STATUS = { running: 'Running', succeeded: 'Succeeded', failed: 'Failed', unknown: 'Recorded', interrupted: 'Stopped' }
|
|
7
|
+
const INTENTS: Record<MapIntent, string> = { decision: 'Decision', hypothesis: 'Hypothesis', alternative: 'Alternative', next_step: 'Next step', context: 'Thinking' }
|
|
8
|
+
|
|
9
|
+
export function WorkMapPanel({ tabId }: { tabId: string }) {
|
|
10
|
+
const [snapshot, setSnapshot] = useState<MapSnapshot | null>(null)
|
|
11
|
+
const [error, setError] = useState<string | null>(null)
|
|
12
|
+
const [saving, setSaving] = useState(false)
|
|
13
|
+
const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
|
|
14
|
+
const [selected, setSelected] = useState<string | null>(null)
|
|
15
|
+
const [following, setFollowing] = useState(true)
|
|
16
|
+
const [paths, setPaths] = useState({ pythonPath: '', modelPath: '' })
|
|
17
|
+
const [fullSource, setFullSource] = useState<{ eventId: string; text?: string; error?: string; loading?: boolean } | null>(null)
|
|
18
|
+
const scroll = useRef<HTMLDivElement>(null)
|
|
19
|
+
const alive = useRef(true)
|
|
20
|
+
const revision = useRef(-1)
|
|
21
|
+
|
|
22
|
+
const accept = (next: MapSnapshot) => {
|
|
23
|
+
if (!alive.current || next.tabId !== tabId || next.revision < revision.current) return
|
|
24
|
+
revision.current = next.revision
|
|
25
|
+
setSnapshot(next)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
alive.current = true
|
|
30
|
+
const unsubscribe = window.worker.onWorkMapUpdate(accept)
|
|
31
|
+
window.worker.getWorkMap(tabId).then(next => {
|
|
32
|
+
accept(next)
|
|
33
|
+
if (alive.current) setPaths({ pythonPath: next.settings.pythonPath, modelPath: next.settings.modelPath })
|
|
34
|
+
}).catch(err => { if (alive.current) setError(String(err)) })
|
|
35
|
+
return () => { alive.current = false; unsubscribe() }
|
|
36
|
+
}, [tabId])
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const event = snapshot?.nodes.find(n => n.id === selected)?.evidence.find(e => e.kind === 'thinking' || e.kind === 'message')
|
|
40
|
+
if (!event) { setFullSource(null); return }
|
|
41
|
+
let cancelled = false
|
|
42
|
+
setFullSource({ eventId: event.id, loading: true })
|
|
43
|
+
if (typeof window.worker.getWorkMapSource !== 'function') {
|
|
44
|
+
setFullSource({ eventId: event.id, error: 'Restart the app to load full source text. Showing the recorded preview.' })
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
window.worker.getWorkMapSource(tabId, event.id).then(source => {
|
|
48
|
+
if (!cancelled) setFullSource(source ? { eventId: event.id, text: source.text } : { eventId: event.id, error: 'The original source record is no longer available.' })
|
|
49
|
+
}).catch(err => { if (!cancelled) setFullSource({ eventId: event.id, error: String(err) }) })
|
|
50
|
+
return () => { cancelled = true }
|
|
51
|
+
}, [selected, tabId])
|
|
52
|
+
|
|
53
|
+
useLayoutEffect(() => {
|
|
54
|
+
if (following && scroll.current) scroll.current.scrollTop = scroll.current.scrollHeight
|
|
55
|
+
}, [snapshot?.revision, following])
|
|
56
|
+
|
|
57
|
+
async function configure(settings: Partial<MapSettings>) {
|
|
58
|
+
setSaving(true)
|
|
59
|
+
setError(null)
|
|
60
|
+
try { accept(await window.worker.setWorkMap(tabId, settings)) }
|
|
61
|
+
catch (err) { if (alive.current) setError(String(err)) }
|
|
62
|
+
finally { if (alive.current) setSaving(false) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const toggleBranch = (id: string) => setCollapsed(prev => {
|
|
66
|
+
const next = new Set(prev)
|
|
67
|
+
if (next.has(id)) next.delete(id)
|
|
68
|
+
else next.add(id)
|
|
69
|
+
return next
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const nodes = snapshot?.nodes || []
|
|
73
|
+
const steps = nodes.filter(n => n.kind === 'step')
|
|
74
|
+
const decisions = steps.filter(n => n.intent && n.intent !== 'context')
|
|
75
|
+
const nodeById = new Map(nodes.map(n => [n.id, n]))
|
|
76
|
+
const goals = nodes.filter(n => n.kind === 'goal')
|
|
77
|
+
|
|
78
|
+
// Render consecutive runs in event order. Reusing a topic must not move new
|
|
79
|
+
// actions back up the page or separate them from the preceding decision.
|
|
80
|
+
function threadRuns(goalId: string) {
|
|
81
|
+
const runs: Array<{ id: string; key: string | null; label: string; nodes: MapNode[] }> = []
|
|
82
|
+
for (const node of steps) {
|
|
83
|
+
if (nodeById.get(node.parentId || '')?.parentId !== goalId) continue
|
|
84
|
+
const textSource = node.evidence[0]?.kind === 'thinking' || node.evidence[0]?.kind === 'message'
|
|
85
|
+
const anchor = textSource ? node : nodeById.get(node.contextId || '')
|
|
86
|
+
const key = anchor?.id || node.parentId
|
|
87
|
+
const previous = runs.at(-1)
|
|
88
|
+
if (previous?.key === key) previous.nodes.push(node)
|
|
89
|
+
else runs.push({ id: `thread-${node.id}`, key, label: nodeById.get(anchor?.parentId || node.parentId || '')?.title || 'Activity', nodes: [node] })
|
|
90
|
+
}
|
|
91
|
+
return runs
|
|
92
|
+
}
|
|
93
|
+
const enabled = snapshot?.settings.enabled || false
|
|
94
|
+
const available = snapshot?.capability?.state === 'available'
|
|
95
|
+
const runtime = snapshot?.runtime
|
|
96
|
+
const activity = snapshot?.ended ? 'Session ended'
|
|
97
|
+
: !enabled ? 'Map is off'
|
|
98
|
+
: !snapshot?.supported ? 'Waiting for a local agent'
|
|
99
|
+
: snapshot.state === 'awaiting_decision' ? 'Waiting for approval'
|
|
100
|
+
: snapshot.state === 'awaiting_input' ? 'Waiting for you'
|
|
101
|
+
: runtime === 'loading' ? 'Loading Clef-Flash…'
|
|
102
|
+
: runtime === 'thinking' ? 'Connecting the dots…'
|
|
103
|
+
: runtime === 'error' ? 'Model paused'
|
|
104
|
+
: steps.length ? 'Following this session' : 'Waiting for activity'
|
|
105
|
+
|
|
106
|
+
function renderStep(node: MapNode) {
|
|
107
|
+
const open = selected === node.id
|
|
108
|
+
const thinking = node.evidence[0]?.kind === 'thinking'
|
|
109
|
+
const actions = nodes.filter(n => n.contextId === node.id)
|
|
110
|
+
const linked = !!node.contextId && nodeById.has(node.contextId)
|
|
111
|
+
return <li key={node.id} className={`work-map-step ${node.status}${linked ? ' follows-intent' : ''}`}>
|
|
112
|
+
<button className="work-map-step-button" onClick={() => setSelected(open ? null : node.id)} aria-expanded={open}>
|
|
113
|
+
<span className={`work-map-activity ${node.activity}`} title={node.activity} aria-label={node.activity}>{ICONS[node.activity]}</span>
|
|
114
|
+
<span className="work-map-step-copy"><span className="work-map-step-title">{node.focus?.text || node.title}</span>
|
|
115
|
+
{node.focus?.reason && <span className="work-map-reason"><b>Why</b> {node.focus.reason}</span>}
|
|
116
|
+
<span className="work-map-step-meta">
|
|
117
|
+
{node.intent && node.intent !== 'context' ? <span className={`work-map-intent ${node.intent}`}>{INTENTS[node.intent]}</span> : <span className={`work-map-status ${node.status}`}>{STATUS[node.status]}</span>}
|
|
118
|
+
{(thinking || node.evidence[0]?.kind === 'message') && <span>{thinking ? 'Thinking summary' : 'Agent update'}</span>}
|
|
119
|
+
{linked && <span className="work-map-link" title={`Follows: ${nodeById.get(node.contextId!)?.title}`}>↳ {nodeById.get(node.contextId!)?.evidence[0]?.kind === 'thinking' ? 'Follows thinking' : 'Follows update'}</span>}
|
|
120
|
+
<time dateTime={node.ts}>{new Date(node.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</time>
|
|
121
|
+
{actions.length > 0 && <span>{actions.length} following {actions.length === 1 ? 'action' : 'actions'}</span>}
|
|
122
|
+
{node.placement === 'clef-flash' && <span title="Topic placement suggested by Clef-Flash">Clef ↗</span>}
|
|
123
|
+
</span>
|
|
124
|
+
</span>
|
|
125
|
+
<span className="work-map-receipt-caret" aria-hidden="true">{open ? '−' : '+'}</span>
|
|
126
|
+
</button>
|
|
127
|
+
{open && <div className="work-map-evidence">
|
|
128
|
+
<div className="work-map-evidence-heading">Source events</div>
|
|
129
|
+
{node.evidence.map(e => <div key={e.id} className="work-map-receipt">
|
|
130
|
+
<span>{e.kind === 'thinking' ? fullSource?.eventId === e.id && fullSource.text !== undefined ? 'Full thinking' : 'Thinking summary' : e.kind === 'tool' ? 'Tool call' : e.kind === 'result' ? 'Tool result' : fullSource?.eventId === e.id && fullSource.text !== undefined ? 'Full agent update' : 'Agent message'}</span>
|
|
131
|
+
{fullSource?.eventId === e.id && fullSource.loading && <small>Loading full source…</small>}
|
|
132
|
+
{fullSource?.eventId === e.id && fullSource.error && <small>{fullSource.error}</small>}
|
|
133
|
+
<pre className="work-map-source-text">{highlightSource(fullSource?.eventId === e.id && fullSource.text !== undefined ? fullSource.text : e.text, node.focus)}</pre>
|
|
134
|
+
{node.focus && <small className="work-map-highlight-key"><span>Highlighted: important choice</span>{node.focus.reason && <span>supporting reason</span>}</small>}
|
|
135
|
+
<code title={`${e.source} · ${e.toolId || e.id}`}>{e.source}<br />{e.toolId || e.id}</code>
|
|
136
|
+
</div>)}
|
|
137
|
+
{node.placement === 'clef-flash' && <small>Clef suggested the topic{thinking ? ' and interpretation' : ''}. {thinking ? 'A stated plan or hypothesis is not a verified result.' : 'Execution status comes from the tool result.'}</small>}
|
|
138
|
+
</div>}
|
|
139
|
+
</li>
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return <section className="work-map-panel" aria-label="Live work map">
|
|
143
|
+
<div className="work-map-controls">
|
|
144
|
+
<div className="work-map-heading"><span className="work-map-kicker">THE THREAD OF WORK</span>
|
|
145
|
+
<div className="work-map-heading-row"><h2>Follow the thread.</h2>
|
|
146
|
+
<button className={`work-map-switch ${enabled ? 'on' : ''}`} role="switch" aria-label="Enable work map" aria-checked={enabled}
|
|
147
|
+
disabled={!snapshot || saving || !available} onClick={() => void configure({ enabled: !enabled })}>
|
|
148
|
+
<span /><b>{enabled ? 'On' : 'Off'}</b>
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="work-map-engine-row"><label htmlFor={`map-engine-${tabId}`}>Engine</label>
|
|
153
|
+
<select id={`map-engine-${tabId}`} value={snapshot?.settings.engine || 'clef-flash'} disabled={!snapshot || saving || !available}
|
|
154
|
+
onChange={e => void configure({ engine: e.target.value as MapSettings['engine'] })}>
|
|
155
|
+
<option value="clef-flash">Clef-Flash · local</option><option value="rules">Rules · lightweight</option>
|
|
156
|
+
</select>
|
|
157
|
+
</div>
|
|
158
|
+
{!available && snapshot && <p className="work-map-thinking-status" role="status">{snapshot.capability?.reason || 'Restart the app to check Apple GPU availability.'}</p>}
|
|
159
|
+
{enabled && snapshot?.supported && <p className="work-map-thinking-status" role="status">
|
|
160
|
+
{snapshot.thinkingStatus === 'available' ? 'Readable thinking included'
|
|
161
|
+
: snapshot.thinkingStatus === 'unavailable' ? 'This transcript omits readable thinking summaries.'
|
|
162
|
+
: 'Waiting for a readable thinking summary.'}
|
|
163
|
+
</p>}
|
|
164
|
+
<div className="work-map-live" role="status"><span className={`work-map-live-dot ${enabled && snapshot?.supported && !snapshot?.ended ? 'on' : ''}`} />
|
|
165
|
+
{activity}<span className="work-map-agent">{snapshot?.agentName}</span>
|
|
166
|
+
</div>
|
|
167
|
+
{(error || snapshot?.error) && <div className="work-map-error" role="alert">{error || snapshot?.error}
|
|
168
|
+
<button disabled={saving || !available} onClick={() => void configure({ enabled: true })}>Retry</button>
|
|
169
|
+
</div>}
|
|
170
|
+
{snapshot?.settings.engine === 'clef-flash' && <details className="work-map-setup">
|
|
171
|
+
<summary>Local model setup</summary>
|
|
172
|
+
<p>Use your downloaded Clef-Flash 8-bit model and its Python environment. Inference stays on this Mac.</p>
|
|
173
|
+
<label>Python executable<input value={paths.pythonPath} onChange={e => setPaths(p => ({ ...p, pythonPath: e.target.value }))} spellCheck={false} /></label>
|
|
174
|
+
<label>Model folder<input value={paths.modelPath} onChange={e => setPaths(p => ({ ...p, modelPath: e.target.value }))} spellCheck={false} /></label>
|
|
175
|
+
<button disabled={saving || !available || !paths.pythonPath.trim() || !paths.modelPath.trim()} onClick={() => void configure(paths)}>Save paths</button>
|
|
176
|
+
</details>}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div className="work-map-scroll" ref={scroll} onScroll={() => {
|
|
180
|
+
const el = scroll.current
|
|
181
|
+
if (el) setFollowing(el.scrollHeight - el.scrollTop - el.clientHeight < 55)
|
|
182
|
+
}}>
|
|
183
|
+
{goals.length === 0 ? <div className="work-map-empty">
|
|
184
|
+
<div className="work-map-empty-tree" aria-hidden="true">◇<span>├─ ⌕</span><span>└─ ✓</span></div>
|
|
185
|
+
<h3>{enabled ? 'The choices and the work, together.' : 'A map of what happens next.'}</h3>
|
|
186
|
+
<p>{!enabled ? 'Turn on the map to follow goals, investigations, edits, and checks as your agent works.'
|
|
187
|
+
: !snapshot?.supported ? 'Start a local Claude Code or Codex session to see its work here. Remote sessions are not supported yet.'
|
|
188
|
+
: 'Stated decisions, readable thinking, actions, and results appear in one continuous thread. Open any card to inspect its source.'}</p>
|
|
189
|
+
</div> : <div className="work-map-forest">
|
|
190
|
+
{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}>
|
|
192
|
+
<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>
|
|
194
|
+
</button>
|
|
195
|
+
{!collapsed.has(goal.id) && <div className="work-map-branches">
|
|
196
|
+
{threadRuns(goal.id).map(run => {
|
|
197
|
+
const failed = run.nodes.some(n => n.status === 'failed')
|
|
198
|
+
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)}>
|
|
200
|
+
<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>
|
|
203
|
+
</button>
|
|
204
|
+
{!collapsed.has(run.id) && <ul className="work-map-steps" aria-label="Activity thread">{run.nodes.map(renderStep)}</ul>}
|
|
205
|
+
</div>
|
|
206
|
+
})}
|
|
207
|
+
</div>}
|
|
208
|
+
</article>)}
|
|
209
|
+
{snapshot?.dropped ? <p className="work-map-retained">Earlier activity trimmed · keeping the latest 240 nodes</p> : null}
|
|
210
|
+
</div>}
|
|
211
|
+
</div>
|
|
212
|
+
{!following && steps.length > 0 && <button className="work-map-jump" onClick={() => setFollowing(true)}>↓ Follow latest</button>}
|
|
213
|
+
<footer className="work-map-footer"><span>{decisions.length} {decisions.length === 1 ? 'choice' : 'choices'} · {steps.length} events · local</span>
|
|
214
|
+
<button disabled={!steps.length || saving} onClick={async () => {
|
|
215
|
+
setError(null)
|
|
216
|
+
try { accept(await window.worker.clearWorkMap(tabId)); setCollapsed(new Set()); setSelected(null); setFollowing(true) }
|
|
217
|
+
catch (err) { if (alive.current) setError(String(err)) }
|
|
218
|
+
}}>Clear map</button>
|
|
219
|
+
</footer>
|
|
220
|
+
</section>
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function highlightSource(text: string, focus?: MapNode['focus']): ReactNode {
|
|
224
|
+
const ranges = thinkingHighlights(text, focus)
|
|
225
|
+
if (!ranges.length) return text
|
|
226
|
+
const parts: ReactNode[] = []
|
|
227
|
+
let offset = 0
|
|
228
|
+
for (const range of ranges) {
|
|
229
|
+
parts.push(text.slice(offset, range.start))
|
|
230
|
+
parts.push(<mark key={range.start} className={`work-map-highlight ${range.kind}`}>{text.slice(range.start, range.end)}</mark>)
|
|
231
|
+
offset = range.end
|
|
232
|
+
}
|
|
233
|
+
parts.push(text.slice(offset))
|
|
234
|
+
return parts
|
|
235
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import type { MapCapability } from '../../shared/workMap'
|
|
3
|
+
|
|
4
|
+
let capability: MapCapability = { state: 'checking', reason: 'Checking Apple GPU availability…' }
|
|
5
|
+
|
|
6
|
+
export function canOpenWorkMap(): boolean {
|
|
7
|
+
return capability.state === 'available'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function WorkMapToggle({ visible, onToggle, onUnavailable }: {
|
|
11
|
+
visible: boolean
|
|
12
|
+
onToggle: () => void
|
|
13
|
+
onUnavailable: () => void
|
|
14
|
+
}) {
|
|
15
|
+
const [current, setCurrent] = useState(capability)
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let cancelled = false
|
|
18
|
+
let pushed = false
|
|
19
|
+
const accept = (next: MapCapability) => {
|
|
20
|
+
if (cancelled) return
|
|
21
|
+
capability = next
|
|
22
|
+
setCurrent(next)
|
|
23
|
+
if (next.state === 'unavailable') onUnavailable()
|
|
24
|
+
}
|
|
25
|
+
if (!window.worker.getWorkMapCapability) {
|
|
26
|
+
accept({ state: 'unavailable', reason: 'Restart the app to check Apple GPU availability.' })
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const unsubscribe = window.worker.onWorkMapCapability(next => { pushed = true; accept(next) })
|
|
30
|
+
window.worker.getWorkMapCapability().then(next => { if (!pushed) accept(next) })
|
|
31
|
+
.catch(() => accept({ state: 'unavailable', reason: 'GPU availability could not be verified.' }))
|
|
32
|
+
return () => { cancelled = true; unsubscribe() }
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
const available = current.state === 'available'
|
|
36
|
+
return <button
|
|
37
|
+
className={`titlebar-btn titlebar-icon-btn ${visible ? 'active' : ''}`}
|
|
38
|
+
onClick={onToggle}
|
|
39
|
+
disabled={!available}
|
|
40
|
+
title={available ? `Show or hide the live work map · ${current.gpuName || 'Apple GPU'} (Ctrl+Shift+M)` : current.reason || 'Work map is unavailable'}
|
|
41
|
+
aria-label="Work map"
|
|
42
|
+
aria-pressed={visible}
|
|
43
|
+
>
|
|
44
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.7" aria-hidden="true">
|
|
45
|
+
<rect x="2" y="9" width="6" height="6" rx="2" /><rect x="16" y="2" width="6" height="6" rx="2" /><rect x="16" y="16" width="6" height="6" rx="2" />
|
|
46
|
+
<path d="M8 12h4V5h4M12 12v7h4" />
|
|
47
|
+
</svg>
|
|
48
|
+
<span>Work map</span>
|
|
49
|
+
</button>
|
|
50
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ThinkingHighlight { start: number; end: number; kind: 'choice' | 'reason' }
|
|
2
|
+
|
|
3
|
+
function canonical(text: string) {
|
|
4
|
+
let value = ''
|
|
5
|
+
const offsets: number[] = []
|
|
6
|
+
for (let i = 0; i < text.length; i++) {
|
|
7
|
+
if (text[i] === '*' || text[i] === '`') continue
|
|
8
|
+
const ch = /\s/.test(text[i]) ? ' ' : text[i]
|
|
9
|
+
if (ch === ' ' && value.endsWith(' ')) continue
|
|
10
|
+
value += ch
|
|
11
|
+
offsets.push(i)
|
|
12
|
+
}
|
|
13
|
+
return { value, offsets }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Locate selected excerpts in the original text, preserving whitespace and Markdown. */
|
|
17
|
+
export function thinkingHighlights(text: string, focus?: { text: string; reason?: string }): ThinkingHighlight[] {
|
|
18
|
+
if (!focus) return []
|
|
19
|
+
const source = canonical(text)
|
|
20
|
+
const ranges: ThinkingHighlight[] = []
|
|
21
|
+
for (const kind of ['choice', 'reason'] as const) {
|
|
22
|
+
const excerpt = kind === 'choice' ? focus.text : focus.reason
|
|
23
|
+
if (!excerpt) continue
|
|
24
|
+
const needle = canonical(excerpt).value.trim()
|
|
25
|
+
if (!needle) continue
|
|
26
|
+
const index = source.value.indexOf(needle)
|
|
27
|
+
if (index < 0) continue
|
|
28
|
+
let start = source.offsets[index]
|
|
29
|
+
let end = source.offsets[index + needle.length - 1] + 1
|
|
30
|
+
while (start > 0 && /[*`]/.test(text[start - 1])) start--
|
|
31
|
+
while (end < text.length && /[*`]/.test(text[end])) end++
|
|
32
|
+
ranges.push({ start, end, kind })
|
|
33
|
+
}
|
|
34
|
+
const merged: ThinkingHighlight[] = []
|
|
35
|
+
for (const range of ranges.sort((a, b) => a.start - b.start)) {
|
|
36
|
+
const last = merged.at(-1)
|
|
37
|
+
if (last && range.start < last.end) { last.end = Math.max(last.end, range.end); if (range.kind === 'choice') last.kind = 'choice' }
|
|
38
|
+
else merged.push({ ...range })
|
|
39
|
+
}
|
|
40
|
+
return merged
|
|
41
|
+
}
|
package/src/renderer/styles.css
CHANGED
|
@@ -17,6 +17,97 @@
|
|
|
17
17
|
flipped mechanically.
|
|
18
18
|
───────────────────────────────────────────────────────────── */
|
|
19
19
|
|
|
20
|
+
/* Live work map */
|
|
21
|
+
.work-map-panel { position: relative; display: flex; flex-direction: column; height: 100%; min-width: 0; overflow: hidden; background: var(--bg-app); color: var(--text); font-size: 12px; }
|
|
22
|
+
.work-map-controls { padding: 20px 18px 12px; flex-shrink: 0; border-bottom: 1px solid var(--border-subtle); background: var(--bg-chrome); max-height: 55%; overflow-y: auto; }
|
|
23
|
+
.work-map-kicker { display: block; color: var(--text-muted); font-size: 9px; font-weight: 700; letter-spacing: 1.6px; line-height: 1.8; }
|
|
24
|
+
.work-map-heading-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: 4px 0 10px; }
|
|
25
|
+
.work-map-heading h2 { margin: 0; color: var(--text-bright); font-size: 21px; font-weight: 550; letter-spacing: -0.7px; }
|
|
26
|
+
.work-map-panel button, .work-map-panel select, .work-map-panel input { font: inherit; }
|
|
27
|
+
.work-map-panel button { cursor: pointer; }
|
|
28
|
+
.work-map-panel button:disabled { opacity: .45; cursor: default; }
|
|
29
|
+
.work-map-panel button:focus-visible, .work-map-panel summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
30
|
+
.work-map-switch { display: flex; align-items: center; gap: 6px; flex-shrink: 0; padding: 4px 7px; border: 1px solid var(--border); border-radius: 20px; background: var(--bg-surface); color: var(--text-muted); }
|
|
31
|
+
.work-map-switch span { width: 11px; height: 11px; border-radius: 50%; background: var(--text-muted); }
|
|
32
|
+
.work-map-switch b { font-size: 10px; font-weight: 600; }
|
|
33
|
+
.work-map-switch.on { color: var(--success); border-color: var(--success); background: var(--bg-chip-soft); }
|
|
34
|
+
.work-map-switch.on span { background: var(--success); }
|
|
35
|
+
.work-map-engine-row { display: flex; gap: 12px; align-items: center; }
|
|
36
|
+
.work-map-engine-row label { color: var(--text-soft); font-size: 11px; }
|
|
37
|
+
.work-map-engine-row select { flex: 1; min-width: 0; padding: 6px 8px; color: var(--text-bright); background: var(--bg-well); border: 1px solid var(--border-subtle); border-radius: 6px; }
|
|
38
|
+
.work-map-live { display: flex; align-items: center; gap: 6px; padding-top: 12px; font-size: 10px; color: var(--text-soft); }
|
|
39
|
+
.work-map-live-dot { width: 5px; height: 5px; flex-shrink: 0; background: var(--text-muted); border-radius: 50%; }
|
|
40
|
+
.work-map-live-dot.on { background: var(--success); box-shadow: 0 0 7px color-mix(in srgb, var(--success) 30%, transparent); }
|
|
41
|
+
.work-map-agent { margin-left: auto; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
42
|
+
.work-map-error { padding: 10px; margin-top: 12px; border: 1px solid var(--danger); border-radius: 6px; color: var(--danger); font-size: 11px; line-height: 1.5; overflow-wrap: anywhere; }
|
|
43
|
+
.work-map-error button, .work-map-setup button { display: block; background: var(--bg-surface); border: 1px solid var(--border); color: var(--text-bright); border-radius: 4px; padding: 4px 9px; margin-top: 8px; }
|
|
44
|
+
.work-map-setup { margin-top: 10px; font-size: 10px; color: var(--text-muted); }
|
|
45
|
+
.work-map-setup summary { cursor: pointer; width: fit-content; }
|
|
46
|
+
.work-map-setup p { color: var(--text-soft); line-height: 1.6; }
|
|
47
|
+
.work-map-setup label { display: block; margin-top: 8px; color: var(--text-soft); }
|
|
48
|
+
.work-map-setup input { display: block; width: 100%; box-sizing: border-box; margin-top: 4px; padding: 6px; background: var(--bg-well); color: var(--text); border: 1px solid var(--border); border-radius: 4px; font-size: 10px; }
|
|
49
|
+
.work-map-scroll { flex: 1; min-height: 0; overflow: auto; padding: 20px 14px 40px; scrollbar-gutter: stable; overflow-anchor: none; }
|
|
50
|
+
.work-map-empty { padding: 24px 14px; max-width: 280px; margin: auto; }
|
|
51
|
+
.work-map-empty-tree { display: flex; flex-direction: column; color: var(--accent); font: 22px/1.7 ui-monospace, monospace; margin: 0 0 20px 10px; }
|
|
52
|
+
.work-map-empty-tree span { color: var(--text-muted); padding-left: 5px; }
|
|
53
|
+
.work-map-empty h3 { font-size: 16px; font-weight: 500; line-height: 1.5; color: var(--text-bright); margin: 0 0 10px; }
|
|
54
|
+
.work-map-empty p { color: var(--text-soft); font-size: 12px; line-height: 1.8; margin: 0; }
|
|
55
|
+
.work-map-goal + .work-map-goal { margin-top: 28px; }
|
|
56
|
+
.work-map-goal-heading { display: flex; align-items: flex-start; gap: 9px; width: 100%; border: 0; padding: 0; background: transparent; text-align: left; color: var(--text-bright); }
|
|
57
|
+
.work-map-goal-icon { font-size: 22px; line-height: 1.5; color: var(--accent); flex-shrink: 0; }
|
|
58
|
+
.work-map-goal-heading strong { display: block; line-height: 1.5; font-size: 13px; font-weight: 550; overflow-wrap: anywhere; }
|
|
59
|
+
.work-map-caret { margin-left: auto; color: var(--text-muted); flex-shrink: 0; font-size: 12px; }
|
|
60
|
+
.work-map-branches { margin: 12px 0 0 10px; padding-left: 17px; border-left: 1px solid var(--border-dim); }
|
|
61
|
+
.work-map-branch { position: relative; padding: 0 0 17px; }
|
|
62
|
+
.work-map-branch:last-child { padding-bottom: 0; }
|
|
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); }
|
|
66
|
+
.work-map-branch-point.failed { background: var(--danger); }
|
|
67
|
+
.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; }
|
|
69
|
+
.work-map-steps { list-style: none; margin: 5px 0 0 2px; padding: 0 0 0 8px; border-left: 1px solid var(--border-subtle); }
|
|
70
|
+
.work-map-step { margin: 5px 0; border: 1px solid var(--border-well); background: var(--bg-well); border-radius: 7px; overflow: hidden; }
|
|
71
|
+
.work-map-step.failed { border-color: color-mix(in srgb, var(--danger) 45%, var(--border-well)); }
|
|
72
|
+
.work-map-step-button { display: flex; align-items: flex-start; gap: 8px; padding: 10px 9px; width: 100%; border: 0; background: transparent; color: var(--text); text-align: left; }
|
|
73
|
+
.work-map-step-button:hover { background: var(--bg-inset-hi); }
|
|
74
|
+
.work-map-activity { display: grid; place-items: center; width: 22px; height: 22px; flex-shrink: 0; border-radius: 5px; background: var(--bg-chip); color: var(--accent); font-size: 17px; }
|
|
75
|
+
.work-map-activity.editing { color: var(--warning); }
|
|
76
|
+
.work-map-activity.testing { color: var(--success); }
|
|
77
|
+
.work-map-activity.planning { color: var(--sev-digest); }
|
|
78
|
+
.work-map-activity.thinking { color: var(--sev-digest); }
|
|
79
|
+
.work-map-step.follows-intent { margin-left: 12px; border-left: 2px solid var(--border-dim); }
|
|
80
|
+
.work-map-link { color: var(--text-soft); }
|
|
81
|
+
.work-map-reason { display: block; border-left: 2px solid var(--border-dim); padding-left: 7px; margin-top: 7px; color: var(--text-soft); font-size: 10px; line-height: 1.7; overflow-wrap: anywhere; }
|
|
82
|
+
.work-map-reason b { color: var(--text-bright); font-size: 9px; margin-right: 3px; }
|
|
83
|
+
.work-map-receipt .work-map-source-text { max-height: 310px; line-height: 1.8; }
|
|
84
|
+
.work-map-highlight { color: var(--text-bright); background: color-mix(in srgb, var(--warning) 25%, transparent); border-radius: 2px; padding: 1px 0; }
|
|
85
|
+
.work-map-highlight.reason { background: color-mix(in srgb, var(--accent) 25%, transparent); }
|
|
86
|
+
.work-map-highlight-key { display: flex !important; flex-wrap: wrap; gap: 10px; }
|
|
87
|
+
.work-map-highlight-key span:first-child { color: var(--warning); }
|
|
88
|
+
.work-map-highlight-key span + span { color: var(--accent); }
|
|
89
|
+
.work-map-thinking-status { color: var(--text-muted); font-size: 10px; line-height: 1.5; margin: 8px 0 0; }
|
|
90
|
+
.work-map-intent { color: var(--sev-digest); }
|
|
91
|
+
.work-map-intent.hypothesis { color: var(--warning); }
|
|
92
|
+
.work-map-intent.decision { color: var(--accent); }
|
|
93
|
+
.work-map-step-copy { min-width: 0; flex: 1; }
|
|
94
|
+
.work-map-step-title { display: block; overflow-wrap: anywhere; line-height: 1.6; font-size: 11px; }
|
|
95
|
+
.work-map-step-meta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 5px; font-size: 9px; color: var(--text-muted); }
|
|
96
|
+
.work-map-status.running { color: var(--accent); }
|
|
97
|
+
.work-map-status.succeeded { color: var(--success); }
|
|
98
|
+
.work-map-status.failed { color: var(--danger); }
|
|
99
|
+
.work-map-receipt-caret { color: var(--text-muted); line-height: 1.6; font-size: 12px; }
|
|
100
|
+
.work-map-evidence { padding: 0 10px 10px; font-size: 10px; line-height: 1.6; }
|
|
101
|
+
.work-map-evidence-heading { border-top: 1px solid var(--border-subtle); padding-top: 10px; color: var(--text-bright); font-weight: 600; }
|
|
102
|
+
.work-map-receipt { margin-top: 10px; color: var(--text-soft); }
|
|
103
|
+
.work-map-receipt pre { font: 10px/1.6 ui-monospace, monospace; color: var(--text); background: var(--bg-app); white-space: pre-wrap; overflow-wrap: anywhere; padding: 8px; border-radius: 4px; max-height: 170px; overflow: auto; }
|
|
104
|
+
.work-map-receipt code { display: block; color: var(--text-muted); font-size: 9px; overflow-wrap: anywhere; }
|
|
105
|
+
.work-map-evidence small { display: block; margin-top: 10px; color: var(--text-muted); font-size: 9px; }
|
|
106
|
+
.work-map-retained { color: var(--text-muted); font-size: 10px; padding: 12px 0; text-align: center; }
|
|
107
|
+
.work-map-jump { position: absolute; bottom: 45px; left: 50%; transform: translateX(-50%); white-space: nowrap; border: 1px solid var(--border-strong); border-radius: 20px; padding: 7px 13px; background: var(--bg-raised); color: var(--accent); box-shadow: 0 3px 12px var(--shadow-color); }
|
|
108
|
+
.work-map-footer { display: flex; align-items: center; justify-content: space-between; padding: 9px 14px; gap: 8px; border-top: 1px solid var(--border-subtle); background: var(--bg-chrome); color: var(--text-muted); font-size: 9px; }
|
|
109
|
+
.work-map-footer button { border: 0; background: transparent; color: var(--text-soft); padding: 2px; font-size: 10px; }
|
|
110
|
+
|
|
20
111
|
:root,
|
|
21
112
|
:root[data-theme='tokyo-night'] {
|
|
22
113
|
color-scheme: dark;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type MapEngine = 'rules' | 'clef-flash'
|
|
2
|
+
export type MapActivity = 'thinking' | 'investigating' | 'editing' | 'testing' | 'planning' | 'reporting'
|
|
3
|
+
export type MapIntent = 'decision' | 'hypothesis' | 'alternative' | 'next_step' | 'context'
|
|
4
|
+
export type MapStatus = 'running' | 'succeeded' | 'failed' | 'unknown' | 'interrupted'
|
|
5
|
+
|
|
6
|
+
export interface MapCapability {
|
|
7
|
+
state: 'checking' | 'available' | 'unavailable'
|
|
8
|
+
reason: string | null
|
|
9
|
+
gpuName?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MapSettings {
|
|
13
|
+
enabled: boolean
|
|
14
|
+
engine: MapEngine
|
|
15
|
+
pythonPath: string
|
|
16
|
+
modelPath: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MapEvidence {
|
|
20
|
+
id: string
|
|
21
|
+
ts: string
|
|
22
|
+
kind: 'prompt' | 'thinking' | 'message' | 'tool' | 'result'
|
|
23
|
+
text: string
|
|
24
|
+
source: string
|
|
25
|
+
tool?: string
|
|
26
|
+
toolId?: string
|
|
27
|
+
status?: MapStatus
|
|
28
|
+
/** Salient candidates from the complete readable block, before display truncation. */
|
|
29
|
+
segments?: string[]
|
|
30
|
+
/** Populated only by an on-demand source read, never retained in map snapshots. */
|
|
31
|
+
fullText?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MapNode {
|
|
35
|
+
id: string
|
|
36
|
+
parentId: string | null
|
|
37
|
+
kind: 'goal' | 'branch' | 'step'
|
|
38
|
+
title: string
|
|
39
|
+
activity: MapActivity
|
|
40
|
+
status: MapStatus
|
|
41
|
+
ts: string
|
|
42
|
+
evidence: MapEvidence[]
|
|
43
|
+
placement?: MapEngine
|
|
44
|
+
confidence?: number
|
|
45
|
+
intent?: MapIntent
|
|
46
|
+
/** Most recent stated reasoning/intent before this action, within the same goal. */
|
|
47
|
+
contextId?: string
|
|
48
|
+
focus?: { text: string; reason?: string }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MapSnapshot {
|
|
52
|
+
revision: number
|
|
53
|
+
tabId: string
|
|
54
|
+
agentName: string | null
|
|
55
|
+
supported: boolean
|
|
56
|
+
ended: boolean
|
|
57
|
+
state: string
|
|
58
|
+
settings: MapSettings
|
|
59
|
+
runtime: 'off' | 'idle' | 'loading' | 'ready' | 'thinking' | 'error'
|
|
60
|
+
error: string | null
|
|
61
|
+
nodes: MapNode[]
|
|
62
|
+
dropped: number
|
|
63
|
+
thinkingStatus: 'waiting' | 'available' | 'unavailable'
|
|
64
|
+
capability: MapCapability
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface MapSource {
|
|
68
|
+
text: string
|
|
69
|
+
source: string
|
|
70
|
+
}
|