dsh-bettertaskgraphs 0.0.0-stage → 0.2.3

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.
@@ -0,0 +1,318 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ // Loaded in the existing DSH browser seat, without a separate React root.
3
+ export function createClientPlugin(React, z, createRemoteContribution, graphPresentation, branchLayout) {
4
+ const { createElement: h, useEffect, useId, useMemo, useRef, useState } = React
5
+ const locale = {
6
+ zh: {
7
+ title: '事务图', progress: '任务成功 {done}/{total}', active: '当前事务', ready: '可开始', noReady: '暂无可开始事务',
8
+ retry: '刷新', unavailable: '暂时无法读取事务图', stale: '显示上次读取的状态', recovering: '当前事务等待恢复对账',
9
+ pending: '状态更新待对账', waiting: '暂时没有可执行步骤,等待当前状态核对',
10
+ tree: '事务层级', details: '事务关系', incoming: '进入条件:', conditionSeparator: ':',
11
+ noEdges: '无进入条件',
12
+ complete: '完成', inProgress: '执行中', pendingNode: '待执行', inactive: '分支尚未激活', incomplete: '未完成',
13
+ branchWaiting: '等待分支选择', branchExcluded: '未选分支', branchChosen: '已选路径', graphComplete: '选中路径已完成',
14
+ skipped: '跳过,未执行', failed: '失败', blocked: '受阻', unsatisfied: '未满足', grouped: '组内尚有未完成',
15
+ fullscreen: '全屏', exitFullscreen: '退出全屏', branchGraph: '事务分支图',
16
+ prerequisiteMet: '前置已满足', edgeWaiting: '等待条件',
17
+ additionalPrerequisites: '另有 {count} 项前置',
18
+ requires: '前置完成后', onComplete: '完成时', onIncomplete: '未完成时', onCase: '分支 {value}',
19
+ totalIncomplete: '{count} 项未完成', totalSkipped: '{count} 项跳过', totalInactive: '{count} 项分支未激活',
20
+ totalExcluded: '{count} 项属于未选分支',
21
+ revision: '版本 {revision}',
22
+ },
23
+ en: {
24
+ title: 'Transaction graph', progress: '{done}/{total} tasks successful', active: 'Current transaction', ready: 'Ready to start',
25
+ noReady: 'No transaction ready to start', retry: 'Refresh', unavailable: 'Transaction graph is unavailable', stale: 'Showing last known state',
26
+ recovering: 'Current transaction awaits reconciliation', pending: 'Status update awaits reconciliation',
27
+ waiting: 'Execution is on hold until the current state is reconciled',
28
+ tree: 'Transaction hierarchy', details: 'Transaction relationships', incoming: 'Entry conditions:', conditionSeparator: ': ',
29
+ noEdges: 'No entry conditions',
30
+ complete: 'Complete', inProgress: 'In progress', pendingNode: 'Pending', inactive: 'Branch not activated',
31
+ incomplete: 'Incomplete', skipped: 'Skipped', failed: 'Failed', blocked: 'Blocked', unsatisfied: 'Unsatisfied', grouped: 'Group incomplete',
32
+ fullscreen: 'Full screen', exitFullscreen: 'Exit full screen', branchGraph: 'Transaction branch graph',
33
+ prerequisiteMet: 'Prerequisite met', edgeWaiting: 'Awaiting condition',
34
+ additionalPrerequisites: '+{count} prerequisites',
35
+ branchWaiting: 'Awaiting branch selection', branchExcluded: 'Branch not selected',
36
+ branchChosen: 'Selected path', graphComplete: 'Selected paths complete',
37
+ requires: 'After prerequisite', onComplete: 'On completion', onIncomplete: 'On incomplete', onCase: 'Case {value}',
38
+ totalIncomplete: '{count} incomplete', totalSkipped: '{count} skipped', totalInactive: '{count} inactive branches',
39
+ totalExcluded: '{count} on unselected paths',
40
+ revision: 'Revision {revision}',
41
+ },
42
+ }
43
+ const statusLabels = {
44
+ ACTIVE: 'inProgress', COMPLETE: 'complete', CONTAINER_INCOMPLETE: 'grouped', INACTIVE: 'inactive',
45
+ INCOMPLETE: 'incomplete', PENDING: 'pendingNode', SKIPPED: 'skipped',
46
+ }
47
+ const dispositions = { FAILED: 'failed', UNSATISFIED: 'unsatisfied', SKIPPED_NOT_RUN: 'skipped' }
48
+ const styles = `
49
+ .tg-dock{--tg-border:var(--dsw-alias-border-l1,#8885);--tg-panel:var(--dsw-specific-menu,Canvas);--tg-muted:var(--dsw-alias-label-secondary,inherit);container-type:inline-size;box-sizing:border-box;width:calc(100% - 24px);max-width:var(--dsh-composer-card-max-width,860px);margin:0 auto;border:1px solid var(--tg-border);border-radius:var(--dsw-radius-lg,12px);background:var(--tg-panel);color:var(--dsw-alias-label-primary,CanvasText);font:inherit}
50
+ .tg-dock button{font:inherit;color:inherit;cursor:pointer}
51
+ .tg-dock button:focus-visible,.tg-canvas-scroll:focus-visible{outline:2px solid #4f91ed;outline-offset:-2px}
52
+ .tg-top{display:flex;align-items:center;gap:3px;padding-inline-end:8px}
53
+ .tg-header{display:flex;align-items:center;gap:10px;flex:1 1 auto;padding:7px 10px;border:0;border-radius:8px;background:transparent;text-align:left;font-size:13px!important;min-width:0}
54
+ .tg-heading{font-weight:600;flex:none}.tg-summary{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--tg-muted)}.tg-count{flex:none;font-size:12px;color:var(--tg-muted)}
55
+ .tg-chevron{display:inline-block;flex:none;transition:transform 180ms ease}.tg-header[aria-expanded=true] .tg-chevron{transform:rotate(180deg)}
56
+ .tg-fold{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows 180ms ease,opacity 150ms ease}
57
+ .tg-fold[data-expanded=true]{grid-template-rows:1fr;opacity:1}.tg-fold-clip{min-height:0;overflow:hidden}
58
+ .tg-body{padding:0 12px 10px;max-height:min(46vh,440px);overflow:auto;overscroll-behavior:contain}.tg-overview{display:flex;align-items:center;gap:8px;justify-content:space-between;margin:4px 0 7px;overflow-wrap:anywhere}
59
+ .tg-overview h3,.tg-area h4,.tg-condition-heading{font-size:13px;margin:0;font-weight:600}
60
+ .tg-actions{display:flex;align-items:center;gap:7px;flex:none;font-size:11px}
61
+ .tg-dock .tg-control{border:1px solid var(--tg-border);padding:3px 7px;border-radius:6px;background:transparent;font-size:12px;flex:none}
62
+ .tg-control:disabled{cursor:default;opacity:.6}
63
+ .tg-notice{padding:6px 8px;background:#d78c0c20;border-radius:6px;margin:4px 0;font-size:12px;overflow-wrap:anywhere}
64
+ .tg-progress,.tg-ready{display:flex;flex-wrap:wrap;gap:5px 12px;margin:5px 0;font-size:12px;align-items:baseline}.tg-progress{color:var(--tg-muted)}
65
+ .tg-ready button,.tg-link-button{border:0;background:transparent;padding:0;text-align:left;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px;overflow-wrap:anywhere;max-width:100%}
66
+ .tg-grid{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:8px;margin-top:8px}
67
+ .tg-area{min-width:0;padding:8px;border:1px solid var(--tg-border);border-radius:8px;overflow-wrap:anywhere}
68
+ .tg-area h4{margin-bottom:6px}
69
+ .tg-tree,.tg-tree ol{list-style:none;margin:0;padding:0}.tg-tree ol{margin-inline-start:8px;padding-inline-start:8px;border-inline-start:1px solid var(--tg-border)}
70
+ .tg-node{display:flex;align-items:baseline;gap:5px;flex-wrap:wrap;width:100%;text-align:left;border:0;border-radius:5px;background:transparent;padding:4px 3px;font-size:12px!important;overflow-wrap:anywhere;transition:background-color 140ms ease}
71
+ .tg-node:hover,.tg-node[aria-pressed=true]{background:#4f91ed21}.tg-node[aria-current=step]{outline:1px solid #4f91ed;outline-offset:-1px}.tg-node-name{min-width:0;max-width:100%;overflow-wrap:anywhere}.tg-node[data-kind=container] .tg-node-name{font-weight:650}
72
+ .tg-badge{padding:0 4px;border-radius:4px;background:#8882;font-size:10px;line-height:16px}
73
+ .tg-badge[data-state=complete]{background:#3399682c}.tg-badge[data-state=inProgress]{background:#4f91ed35}
74
+ .tg-badge:is([data-state=blocked],[data-state=incomplete],[data-state=unsatisfied]){background:#d78c0c2c}
75
+ .tg-badge[data-state=failed]{background:#d452522f}
76
+ .tg-current,.tg-conditions{font-size:12px;line-height:1.4;overflow-wrap:anywhere}.tg-current{margin-bottom:7px}.tg-current .tg-badge{margin-inline-start:6px}
77
+ .tg-conditions{list-style:decimal;margin:4px 0 0;padding-inline-start:1.7em}.tg-conditions li+li{margin-top:4px}.tg-no-conditions{margin:4px 0 0;color:var(--tg-muted);font-size:12px}
78
+
79
+ .tg-viewer{
80
+ --tg-panel:var(--dsw-alias-bg-base,Canvas);
81
+ --tg-card-fill:color-mix(in srgb,var(--tg-panel),currentColor 7%);
82
+ --tg-card-border:color-mix(in srgb,var(--tg-panel),currentColor 32%);
83
+ --tg-complete:light-dark(#287552,#6acda0);
84
+ --tg-active:light-dark(#296ac4,#80b2ff);
85
+ --tg-blocked:light-dark(#a36119,#edab5b);
86
+ --tg-failed:light-dark(#aa4242,#fa8585);
87
+ box-sizing:border-box;position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:14px 18px;border:0;overflow:hidden;background:linear-gradient(var(--tg-panel),var(--tg-panel)),Canvas;color:inherit;display:grid;grid-template-rows:auto auto minmax(0,1fr) auto;gap:10px;opacity:0;transform:translateY(8px) scale(.995);transition:opacity 180ms ease,transform 180ms ease,display 180ms allow-discrete,overlay 180ms allow-discrete
88
+ }
89
+ .tg-viewer:not([open]){display:none}.tg-viewer[open]{opacity:1;transform:none}
90
+ @starting-style{.tg-viewer[open]{opacity:0;transform:translateY(8px) scale(.995)}}
91
+ .tg-viewer-header{display:flex;align-items:center;justify-content:space-between;gap:10px 20px;flex-wrap:wrap}
92
+ .tg-viewer-header h2{margin:2px 0;font-size:20px;overflow-wrap:anywhere}.tg-viewer-eyebrow{color:var(--tg-muted);font-size:12px}
93
+ .tg-viewer-header .tg-actions{flex:0 1 auto;flex-wrap:wrap}.tg-viewer .tg-area{max-height:min(28vh,210px);overflow:auto}
94
+ .tg-legend{display:flex;flex-wrap:wrap;gap:5px 16px;margin:0;padding:0;list-style:none;color:var(--tg-muted);font-size:12px}
95
+ .tg-legend li{display:flex;align-items:center;gap:5px}
96
+ .tg-wire,.tg-legend-mark{--tg-line:light-dark(#676d76,#b0b8c6);--tg-line-opacity:.78}
97
+ :is(.tg-wire,.tg-legend-mark)[data-state=branchChosen]{--tg-line:var(--tg-active);--tg-line-opacity:1}
98
+ :is(.tg-wire,.tg-legend-mark)[data-state=prerequisiteMet]{--tg-line:var(--tg-complete);--tg-line-opacity:1}
99
+ :is(.tg-wire,.tg-legend-mark)[data-state=blocked]{--tg-line:var(--tg-blocked);--tg-line-opacity:1}
100
+ :is(.tg-wire,.tg-legend-mark)[data-state=branchExcluded]{--tg-line-opacity:.62}
101
+ .tg-legend-mark{width:16px;border-top:2px solid var(--tg-line);opacity:var(--tg-line-opacity)}
102
+ .tg-legend-mark[data-state=branchExcluded]{border-top-style:dashed}
103
+ .tg-canvas-scroll{display:flex;min-height:0;overflow:auto;overscroll-behavior:contain;border:1px solid var(--tg-border);border-radius:10px;background-color:color-mix(in srgb,var(--tg-panel),currentColor 2%);background-image:radial-gradient(var(--tg-border) .65px,transparent .65px);background-size:15px 15px}
104
+ .tg-canvas{flex:none;position:relative;margin:auto}.tg-wires{position:absolute;inset:0;pointer-events:none}
105
+ .tg-wire{fill:none;stroke:var(--tg-line);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;opacity:var(--tg-line-opacity);pointer-events:stroke}
106
+ .tg-wire[data-state=branchExcluded]{stroke-dasharray:5 5}.tg-wire[data-state=branchChosen]{stroke-width:2.6}
107
+ .tg-wire-label{fill:currentColor;stroke:var(--tg-panel);stroke-width:4;stroke-linejoin:round;paint-order:stroke;font-size:11px}
108
+ .tg-dock .tg-card{--tg-card-accent:var(--tg-card-border);box-sizing:border-box;position:absolute;display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:3px;padding:5px 9px;overflow:hidden;text-align:left;border:1px solid var(--tg-card-border);border-inline-start:4px solid var(--tg-card-accent);border-radius:9px;background:var(--tg-card-fill);box-shadow:0 2px 5px #0002;transition:background-color 140ms ease,border-color 140ms ease}
109
+ .tg-dock .tg-card[data-state=complete]{--tg-card-accent:var(--tg-complete)}
110
+ .tg-dock .tg-card[data-state=failed]{--tg-card-accent:var(--tg-failed)}
111
+ .tg-dock .tg-card[data-state=blocked]{--tg-card-accent:var(--tg-blocked)}
112
+ .tg-dock .tg-card[data-state=inProgress]{--tg-card-accent:var(--tg-active)}
113
+ .tg-dock .tg-card[data-state=branchExcluded]{border-style:dashed;box-shadow:none}
114
+ .tg-dock .tg-card:hover{background:color-mix(in srgb,var(--tg-card-fill),var(--tg-active) 8%)}
115
+ .tg-dock .tg-card[aria-pressed=true]{background:color-mix(in srgb,var(--tg-card-fill),var(--tg-active) 15%);border-color:var(--tg-active);border-inline-start-color:var(--tg-card-accent);box-shadow:0 0 0 1px var(--tg-active)}
116
+ .tg-card-title{font-size:14px;line-height:1.2;font-weight:650;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
117
+ .tg-card-group{font-size:11px;line-height:1.2;color:var(--tg-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
118
+ .tg-card-states{display:flex;gap:5px;max-width:100%;white-space:nowrap}.tg-card-states .tg-badge{min-width:0;overflow:hidden;text-overflow:ellipsis}
119
+ .tg-card-states .tg-badge[data-state]{font-size:11px;line-height:18px;background:color-mix(in srgb,var(--tg-card-fill),var(--tg-card-accent) 20%)}
120
+ @container (max-width:580px){.tg-grid{grid-template-columns:minmax(0,1fr)}.tg-tree ol{margin-inline-start:3px;padding-inline-start:4px}}
121
+ @container (max-width:250px){.tg-top,.tg-header,.tg-overview,.tg-actions{flex-wrap:wrap}.tg-top{padding:4px;gap:3px}.tg-header{padding:3px;gap:4px}.tg-summary{flex-basis:100%;order:1}.tg-count{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}.tg-body{padding-inline:4px}.tg-area{padding:4px}.tg-tree ol{margin-inline-start:1px;padding-inline-start:2px}.tg-actions{flex:0 1 auto;min-width:0}}
122
+ @media(max-width:580px){.tg-viewer{padding:8px;gap:7px}.tg-viewer-header h2{font-size:16px}}
123
+ @media(prefers-reduced-motion:reduce){.tg-viewer,.tg-fold,.tg-chevron,.tg-node,.tg-dock .tg-card{transition:none}}
124
+ `
125
+
126
+ const contribution = createRemoteContribution(z)
127
+ const statusResult = contribution.descriptors[0].result.create()
128
+ function nodeState(node) {
129
+ if (node.branchState === 'NOT_SELECTED') return 'branchExcluded'
130
+ if (node.branchState === 'WAITING' && !node.disposition) return 'branchWaiting'
131
+ return dispositions[node.disposition] ?? (node.blocked ? 'blocked' : statusLabels[node.status])
132
+ }
133
+ function badge(node, t) {
134
+ const state = nodeState(node)
135
+ return h('span', { className: 'tg-badge', 'data-state': state }, t(state))
136
+ }
137
+ function edgeText(edge, t) {
138
+ if (edge.kind === 'prerequisite') return t('requires')
139
+ if (edge.kind === 'case') return t('onCase', { value: edge.case })
140
+ return t(edge.outcome === 'complete' ? 'onComplete' : 'onIncomplete')
141
+ }
142
+ function Conditions({ selected, conditions, t, onSelect }) {
143
+ if (!selected) return null
144
+ return h('section', { className: 'tg-area', 'aria-label': t('details') },
145
+ h('h4', null, t('details')),
146
+ h('div', { className: 'tg-current' }, h('strong', null, selected.label), badge(selected, t)),
147
+ h('h5', { className: 'tg-condition-heading' }, t('incoming')),
148
+ conditions.length ? h('ol', { className: 'tg-conditions' }, conditions.map(node =>
149
+ h('li', { key: node.id },
150
+ h('button', { type: 'button', className: 'tg-link-button', onClick: () => onSelect(node.id) }, node.label),
151
+ t('conditionSeparator'), t(nodeState(node))))) :
152
+ h('p', { className: 'tg-no-conditions' }, t('noEdges')))
153
+ }
154
+ function BranchCanvas({ graph, selected, active, t, onSelect }) {
155
+ const layout = useMemo(() => branchLayout(graph), [graph])
156
+ const arrowId = useId()
157
+ const wires = layout.wires.map(wire => h('g', { key: wire.key },
158
+ h('path', { className: 'tg-wire', 'data-state': wire.state, d: wire.path, markerEnd: `url(#${arrowId})` },
159
+ h('title', null, `${wire.from.label} → ${wire.to.label} · ${edgeText(wire.edge, t)} · ${t(wire.state)}`)),
160
+ wire.showLabel && h('text', { className: 'tg-wire-label', x: wire.labelX, y: wire.labelY, textAnchor: 'middle' },
161
+ edgeText(wire.edge, t))))
162
+ const cards = layout.cards.map(({ node, group, x, y, additionalPrerequisites }) => h('button', {
163
+ key: node.id, type: 'button', className: 'tg-card', 'data-state': nodeState(node),
164
+ 'aria-pressed': selected?.id === node.id, 'aria-current': active?.id === node.id ? 'step' : undefined,
165
+ style: { left: x, top: y, width: layout.cardWidth, height: layout.cardHeight },
166
+ onClick: () => onSelect(node.id), title: node.label,
167
+ }, group && h('span', { className: 'tg-card-group' }, group),
168
+ h('strong', { className: 'tg-card-title' }, node.label),
169
+ h('span', { className: 'tg-card-states' }, badge(node, t),
170
+ additionalPrerequisites > 0 && h('span', { className: 'tg-badge' },
171
+ t('additionalPrerequisites', { count: additionalPrerequisites })))))
172
+ return h('div', { className: 'tg-canvas-scroll', tabIndex: 0, role: 'region', 'aria-label': t('branchGraph') },
173
+ h('div', { className: 'tg-canvas', style: { width: layout.width, height: layout.height } },
174
+ h('svg', { className: 'tg-wires', 'aria-hidden': true, width: layout.width, height: layout.height },
175
+ h('defs', null, h('marker', { id: arrowId, viewBox: '0 0 8 8', refX: 7, refY: 4, markerWidth: 5, markerHeight: 5, orient: 'auto' },
176
+ h('path', { d: 'M 0 0 L 8 4 L 0 8 Z', fill: 'context-stroke' }))),
177
+ wires),
178
+ cards))
179
+ }
180
+ function GraphPanel({ graph, t, busy, failed, refresh }) {
181
+ const [expanded, setExpanded] = useState(false)
182
+ const [selectedId, setSelected] = useState(null)
183
+ const [viewerVisited, setViewerVisited] = useState(false)
184
+ const viewer = useRef(null)
185
+ const bodyId = useId()
186
+ const viewerId = useId()
187
+ const viewerTitleId = useId()
188
+ const model = graphPresentation(graph, selectedId)
189
+ const { active, selected, progress } = model
190
+ let current = t('noReady')
191
+ if (active) current = `${t(graph.active.recoveryRequired ? 'recovering' : 'active')} · ${active.label}`
192
+ else if (graph.pending) current = t('pending')
193
+ else if (model.children.get(null)?.[0]?.status === 'COMPLETE') current = t('graphComplete')
194
+ else if (progress.ready) current = `${t('ready')} · ${progress.ready}`
195
+ const title = failed ? `${t('stale')} · ${current}` : current
196
+ const progressText = t('progress', { done: progress.complete, total: progress.total })
197
+ const conditionProps = { selected, conditions: model.conditions, t, onSelect: setSelected }
198
+ const refreshButton = () => h('button', { className: 'tg-control', type: 'button', disabled: busy, onClick: refresh }, t('retry'))
199
+ const notices = () => h(React.Fragment, null,
200
+ failed && h('p', { className: 'tg-notice', role: 'status' }, t('unavailable')),
201
+ graph.pending && h('p', { className: 'tg-notice', role: 'status' }, t('waiting')))
202
+ function openViewer() {
203
+ const dialog = viewer.current
204
+ if (dialog.open) return
205
+ setViewerVisited(true)
206
+ dialog.showModal()
207
+ dialog.querySelector('[data-tg-close]').focus()
208
+ }
209
+ function branch(parent, depth = 0) {
210
+ const rows = model.children.get(parent)
211
+ if (!rows || depth >= 16) return null
212
+ return h('ol', { className: parent === null ? 'tg-tree' : undefined }, rows.map(node =>
213
+ h('li', { key: node.id },
214
+ h('button', { className: 'tg-node', type: 'button', 'data-kind': node.kind,
215
+ 'aria-pressed': selected?.id === node.id,
216
+ 'aria-current': active?.id === node.id ? 'step' : undefined,
217
+ onClick: () => setSelected(node.id) },
218
+ h('span', { className: 'tg-node-name' }, node.label), badge(node, t),
219
+ model.legal.has(node.id) && h('span', { className: 'tg-badge' }, t('ready'))),
220
+ branch(node.id, depth + 1))))
221
+ }
222
+ return h('section', { className: 'tg-dock', 'data-testid': 'transaction-graph-dock', 'aria-label': t('title') },
223
+ h('style', null, styles),
224
+ h('div', { className: 'tg-top' },
225
+ h('button', { type: 'button', className: 'tg-header', 'aria-expanded': expanded, 'aria-controls': bodyId,
226
+ onClick: () => setExpanded(value => !value) },
227
+ h('span', { className: 'tg-heading' }, t('title')),
228
+ h('span', { className: 'tg-summary', title, role: 'status' }, title),
229
+ h('span', { className: 'tg-count' }, progressText),
230
+ h('span', { className: 'tg-chevron', 'aria-hidden': true }, '⌄')),
231
+ h('button', { type: 'button', className: 'tg-control', 'aria-haspopup': 'dialog', 'aria-controls': viewerId,
232
+ onClick: openViewer }, t('fullscreen'))),
233
+ h('div', { id: bodyId, className: 'tg-fold', 'data-expanded': expanded, 'aria-hidden': !expanded,
234
+ ref: element => { if (element) element.inert = !expanded } },
235
+ h('div', { className: 'tg-fold-clip' }, h('div', { className: 'tg-body' },
236
+ h('div', { className: 'tg-overview' }, h('h3', null, graph.title),
237
+ h('div', { className: 'tg-actions' }, h('span', null, t('revision', { revision: graph.revision })), refreshButton())),
238
+ notices(),
239
+ (progress.incomplete > 0 || progress.skipped > 0 || progress.inactive > 0 || progress.excluded > 0) &&
240
+ h('p', { className: 'tg-progress' },
241
+ progress.incomplete > 0 && h('span', null, t('totalIncomplete', { count: progress.incomplete })),
242
+ progress.skipped > 0 && h('span', null, t('totalSkipped', { count: progress.skipped })),
243
+ progress.inactive > 0 && h('span', null, t('totalInactive', { count: progress.inactive })),
244
+ progress.excluded > 0 && h('span', null, t('totalExcluded', { count: progress.excluded }))),
245
+ graph.legalNext.length > 0 && h('div', { className: 'tg-ready' }, h('strong', null, t('ready')),
246
+ graph.legalNext.map(choice => h('button', { type: 'button', className: 'tg-link-button', key: choice.id,
247
+ onClick: () => setSelected(choice.id) }, model.nodes.get(choice.id)?.label))),
248
+ h('div', { className: 'tg-grid' },
249
+ h('section', { className: 'tg-area', 'aria-label': t('tree') }, h('h4', null, t('tree')), branch(null)),
250
+ h(Conditions, conditionProps))))),
251
+ h('dialog', { id: viewerId, ref: viewer, className: 'tg-viewer', 'aria-labelledby': viewerTitleId },
252
+ h('div', { className: 'tg-viewer-header' },
253
+ h('div', null, h('div', { className: 'tg-viewer-eyebrow' }, t('branchGraph')), h('h2', { id: viewerTitleId }, graph.title)),
254
+ h('div', { className: 'tg-actions' },
255
+ h('span', null, progressText), h('span', null, t('revision', { revision: graph.revision })), refreshButton(),
256
+ h('button', { className: 'tg-control', type: 'button', 'data-tg-close': true,
257
+ onClick: () => viewer.current.close() }, t('exitFullscreen')))),
258
+ h('div', null, notices(),
259
+ h('ul', { className: 'tg-legend' }, ['branchChosen', 'prerequisiteMet', 'blocked', 'edgeWaiting', 'branchExcluded'].map(state =>
260
+ h('li', { key: state }, h('span', { className: 'tg-legend-mark', 'data-state': state, 'aria-hidden': true }), t(state))))),
261
+ viewerVisited && h(BranchCanvas, { graph, selected, active, t, onSelect: setSelected }),
262
+ h(Conditions, conditionProps)))
263
+ }
264
+
265
+ function makeDock(api) {
266
+ return function TransactionGraphDock({ sessionId, useSession, useAccess, t }) {
267
+ const canRead = useAccess('transactionGraphView/status')
268
+ const opened = useSession(session => session.openState === 'open' && !session.removed)
269
+ const running = useSession(session => session.running)
270
+ const [result, setResult] = useState(null)
271
+ const [refreshCount, requestRefresh] = useState(0)
272
+ const refresh = () => requestRefresh(value => value + 1)
273
+ useEffect(() => {
274
+ if (!canRead || !opened) return
275
+ const controller = new AbortController()
276
+ let timer
277
+ async function refreshStatus() {
278
+ const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(8000)])
279
+ setResult(value => ({
280
+ sessionId, graph: value?.sessionId === sessionId ? value.graph : null,
281
+ busy: true, failed: value?.sessionId === sessionId && value.failed,
282
+ }))
283
+ try {
284
+ const response = await api.status({ sessionId }, signal)
285
+ if (controller.signal.aborted) return
286
+ const parsed = response?.ok ? statusResult.safeParse(response.value) : null
287
+ if (!parsed?.success || parsed.data.sessionId !== sessionId || parsed.data.status === 'unavailable') throw new Error('Unavailable')
288
+ setResult({ sessionId, graph: parsed.data.graph, busy: false, failed: false })
289
+ } catch {
290
+ if (controller.signal.aborted) return
291
+ setResult(value => ({ sessionId, graph: value?.sessionId === sessionId ? value.graph : null, busy: false, failed: true }))
292
+ } finally {
293
+ if (running && !controller.signal.aborted) timer = setTimeout(refreshStatus, 2500)
294
+ }
295
+ }
296
+ void refreshStatus()
297
+ return () => { controller.abort(); clearTimeout(timer) }
298
+ }, [canRead, opened, running, sessionId, refreshCount])
299
+ if (!canRead || !opened || result?.sessionId !== sessionId || !result.graph) return null
300
+ return h(GraphPanel, { key: sessionId, graph: result.graph, t, busy: result.busy, failed: result.failed, refresh })
301
+ }
302
+ }
303
+
304
+ return {
305
+ inject: ['slots', 'locale', 'remote'],
306
+ async apply(ctx) {
307
+ ctx.effect(() => ctx.locale.register('projectTransactionGraph', locale))
308
+ const unmount = await ctx.remote.$mount(contribution)
309
+ ctx.effect(() => unmount)
310
+ ctx.inject(['remote.transactionGraphView'], mounted => {
311
+ const Dock = makeDock(mounted.remote.transactionGraphView)
312
+ mounted.slots.inject('conversation.input.dock', () => mounted.slots.register({
313
+ name: 'conversation.input.dock', id: 'project-transaction-graph', locale: 'projectTransactionGraph', order: 10,
314
+ }, Dock))
315
+ })
316
+ },
317
+ }
318
+ }
@@ -0,0 +1,5 @@
1
+ - insert:
2
+ - id: dsh-bettertaskgraphs
3
+ name: dsh-bettertaskgraphs
4
+ config:
5
+ enableSwitch: false