dsh-ptc-plus 0.3.2 → 0.4.2
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.en.md +63 -80
- package/README.md +63 -80
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.js +30827 -742
- package/compiler-amaro.cjs +5 -0
- package/compiler-amaro.cjs.map +6 -0
- package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
- package/compiler-assets.json +3 -0
- package/compiler-core.cjs +556 -0
- package/compiler-core.cjs.map +6 -0
- package/compiler-platform.cjs +2 -0
- package/compiler-platform.cjs.map +6 -0
- package/compiler-typescript.cjs +388 -0
- package/compiler-typescript.cjs.map +6 -0
- package/docs/README.md +7 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
- package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
- package/docs/adr/0023-global-user-bindings.md +104 -0
- package/docs/adr/0024-repl-console-observation.md +33 -0
- package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
- package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
- package/docs/architecture.md +77 -21
- package/docs/binding-review-placement-design.md +200 -0
- package/docs/capability-projection.md +1 -1
- package/docs/client-ui.md +95 -19
- package/docs/durability-design.md +28 -10
- package/docs/evaluation.md +78 -6
- package/docs/installation.md +41 -6
- package/docs/publishing.md +5 -3
- package/docs/runtime-reference.md +76 -25
- package/docs/semantic-validation.md +29 -0
- package/docs/stateful-binding-updates-proposal.md +759 -0
- package/docs/user-bindings.en.md +66 -0
- package/docs/user-bindings.md +66 -0
- package/docs/value-wire.md +3 -3
- package/docs/verification-optimization-work.md +41 -0
- package/docs/verification.md +71 -0
- package/index.js +369 -92
- package/internal/ast-traversal.js +5 -2
- package/internal/async-local-storage-intrinsics.js +38 -0
- package/internal/binding-declaration-contract.js +8 -0
- package/internal/binding-descriptors.js +1 -0
- package/internal/binding-pattern-traversal.js +25 -0
- package/internal/binding-pattern.js +26 -29
- package/internal/binding-source-analysis.js +708 -0
- package/internal/binding-update-policy.js +72 -0
- package/internal/bounded-worker-round.js +56 -0
- package/internal/callable-definition-environment.js +42 -0
- package/internal/callable-source-catalog.js +129 -0
- package/internal/callable-source-encoding.js +47 -0
- package/internal/callable-source-facts.js +459 -0
- package/internal/callable-source-format.js +2 -0
- package/internal/callable-source-lowering.js +123 -0
- package/internal/cell-analysis-contract.js +25 -0
- package/internal/cell-analysis.js +253 -290
- package/internal/cell-completion.js +15 -0
- package/internal/cell-error.js +7 -0
- package/internal/cell-parser.js +66 -0
- package/internal/cell-rewriter.js +3 -677
- package/internal/commonjs-export-evidence.js +63 -0
- package/internal/commonjs-source-evidence.js +72 -0
- package/internal/compiler-amaro.cjs +2 -0
- package/internal/compiler-compression.js +12 -0
- package/internal/compiler-data.js +58 -0
- package/internal/compiler-descriptors.js +54 -0
- package/internal/compiler-dynamic-cache.js +48 -0
- package/internal/compiler-entry.js +35 -0
- package/internal/compiler-generated-names.js +17 -0
- package/internal/compiler-intrinsics.js +50 -0
- package/internal/compiler-module-links.js +37 -0
- package/internal/compiler-operations.js +61 -0
- package/internal/compiler-platform-factory.js +84 -0
- package/internal/compiler-platform.js +136 -0
- package/internal/compiler-record-roles.js +53 -0
- package/internal/compiler-region-output.js +259 -0
- package/internal/compiler-resource-lowering.js +103 -0
- package/internal/compiler-scope-facts.js +115 -0
- package/internal/compiler-service.js +109 -0
- package/internal/compiler-source-cache.js +32 -0
- package/internal/compiler-source-pieces.js +93 -0
- package/internal/compiler-source-regions.js +132 -0
- package/internal/compiler-storage-source.js +54 -0
- package/internal/compiler-text.js +15 -0
- package/internal/compiler-typescript.cjs +2 -0
- package/internal/config-spec.js +83 -27
- package/internal/cordis-tools-owner.js +130 -21
- package/internal/diagnostic.js +27 -1
- package/internal/direct-surface-owner.js +76 -23
- package/internal/dynamic-binding-evidence.js +16 -0
- package/internal/dynamic-environment-compiler.js +779 -0
- package/internal/dynamic-environment-integration.js +91 -0
- package/internal/dynamic-environment-runtime.js +383 -0
- package/internal/dynamic-native-calls.js +153 -0
- package/internal/dynamic-native-runtime.js +262 -0
- package/internal/dynamic-scope-analysis.js +90 -0
- package/internal/edit-transport-owner.js +27 -2
- package/internal/execution-seam-compat.js +193 -0
- package/internal/failure-reporting.js +136 -28
- package/internal/host-rpc.js +66 -0
- package/internal/isolated-worker.js +555 -0
- package/internal/kernel-child.js +139 -0
- package/internal/kernel-worker.js +1179 -233
- package/internal/language-semantics.js +14 -0
- package/internal/legacy-binding-catalog.js +48 -0
- package/internal/legacy-cell-analysis.js +600 -0
- package/internal/legacy-cell-parser.js +50 -0
- package/internal/legacy-cell-rewriter.js +631 -0
- package/internal/legacy-repl-convenience.js +291 -0
- package/internal/managed-module-operations.js +170 -0
- package/internal/module-compilation.js +322 -0
- package/internal/module-runtime-platform.js +19 -0
- package/internal/module-transform-contract.js +42 -0
- package/internal/native-await-declarations.js +68 -0
- package/internal/native-root-compilation.js +54 -0
- package/internal/native-root-dynamic.js +67 -0
- package/internal/record-utils.js +24 -15
- package/internal/recovery-tips.js +27 -15
- package/internal/rejected-cell-editor.js +13 -8
- package/internal/repl-convenience.js +2 -289
- package/internal/repl-memory-projection.js +81 -12
- package/internal/repl-observation-interest.js +70 -0
- package/internal/repl-rewrite-contract.js +6 -0
- package/internal/repl-scope-normalizer.js +2091 -0
- package/internal/repl-value-observer.js +117 -0
- package/internal/rpc-contract.js +52 -0
- package/internal/runtime-bridge-owner.js +172 -28
- package/internal/runtime-config.js +48 -52
- package/internal/runtime-contexts.js +82 -10
- package/internal/runtime-intrinsics.js +101 -0
- package/internal/runtime-messages.js +111 -0
- package/internal/session-cell-executor.js +402 -87
- package/internal/session-journal-recovery.js +684 -0
- package/internal/session-journal-schema.js +123 -0
- package/internal/session-journal.js +199 -654
- package/internal/session-log-view.js +42 -9
- package/internal/session-runtime.js +257 -95
- package/internal/session-state.js +247 -71
- package/internal/source-position-map.js +230 -71
- package/internal/stateful-module-compiler.js +154 -0
- package/internal/stateful-module-runtime.js +303 -0
- package/internal/stateful-root-compiler.js +600 -0
- package/internal/stateful-root-runtime.js +340 -0
- package/internal/typert-codec-compat.js +28 -0
- package/internal/typescript-transform.js +31 -0
- package/internal/user-binding-authoring.js +70 -0
- package/internal/user-binding-console-worker.js +244 -0
- package/internal/user-binding-console.js +148 -0
- package/internal/user-binding-draft-projection.js +335 -0
- package/internal/user-binding-model-context.js +31 -0
- package/internal/user-binding-runner.js +56 -0
- package/internal/user-bindings-owner.js +1065 -0
- package/internal/user-bindings-store.js +293 -0
- package/internal/user-bindings.js +361 -0
- package/internal/value-wire-schema.js +28 -0
- package/internal/value-wire.js +130 -91
- package/internal/worker-client.js +57 -67
- package/internal/worker-cwd-virtualization.js +80 -53
- package/internal/worker-environment.js +65 -0
- package/internal/worker-output-relay.js +29 -0
- package/internal/worker-repl-realm.js +134 -0
- package/internal/worker-shutdown.js +14 -0
- package/package.json +76 -7
- package/src/client-activity.js +95 -53
- package/src/client-authoring-view.js +580 -0
- package/src/client-binding-review.js +230 -0
- package/src/client-bindings-data.js +29 -0
- package/src/client-catalog.js +127 -0
- package/src/client-code-editor.js +84 -0
- package/src/client-console.js +112 -0
- package/src/client-copy.js +370 -0
- package/src/client-feature-gates.js +62 -0
- package/src/client-host-compat.js +150 -0
- package/src/client-indicator-view.js +267 -0
- package/src/client-repl-view.js +292 -0
- package/src/client-rpc.js +20 -0
- package/src/client-settings-view.js +221 -0
- package/src/client-styles.js +106 -0
- package/src/client-tool-view.js +108 -0
- package/src/client-transport.js +159 -0
- package/src/client-workbench.js +521 -0
- package/src/client.js +233 -615
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { bindingDraftProjection, bindingReviewStatus } from './client-binding-review.js'
|
|
2
|
+
import { bindingModelPreferences } from '../internal/user-binding-model-context.js'
|
|
3
|
+
import { featureEnabled } from './client-feature-gates.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hover dwell before the composer entry opens its menu. The entry sits inside the
|
|
7
|
+
* composer, so a pointer merely crossing it must not drop a menu over the input;
|
|
8
|
+
* click and keyboard paths stay immediate, and the published Menu owns the close
|
|
9
|
+
* grace for a pointer that leaves the trigger and its list.
|
|
10
|
+
*/
|
|
11
|
+
const HOVER_DWELL_MS = 150
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Authoring surfaces: the composer entry menu, the binding review dock and the
|
|
15
|
+
* command card. The review registry, catalog source and workbench controller
|
|
16
|
+
* stay owned by the entry and arrive as dependencies.
|
|
17
|
+
*/
|
|
18
|
+
export function createAuthoringView(React, deps) {
|
|
19
|
+
const {
|
|
20
|
+
ActionButton, IconButton, Menu, Toast, Tooltip, CodeBlock, BindingsDialog, PTCPlusSettingsDialog,
|
|
21
|
+
useWorkbenchController, useBindingReview, catalogOwner, callUserBindings, subscribeReset,
|
|
22
|
+
settingsCardSeat, updateSetting,
|
|
23
|
+
icons: {
|
|
24
|
+
sparkle: IconSparkle16, chevron: IconChevronDownOutline14,
|
|
25
|
+
close: IconCloseOutline16, check: IconCheckOutline14,
|
|
26
|
+
},
|
|
27
|
+
} = deps
|
|
28
|
+
const h = React.createElement
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Where the settings card lives on the installed generation.
|
|
32
|
+
*
|
|
33
|
+
* The compat module reports the seat the host declared rather than the
|
|
34
|
+
* generation, so this copy names the place a user can open now. Before either
|
|
35
|
+
* seat mounts it reports the current one, which is what an up-to-date install
|
|
36
|
+
* offers; the preceding generation's own seat replaces it once it registers.
|
|
37
|
+
*/
|
|
38
|
+
const settingsPath = t => t(settingsCardSeat?.() === 'settings.plugin.item'
|
|
39
|
+
? 'settings.pathSettings' : 'settings.pathPlugins')
|
|
40
|
+
|
|
41
|
+
function BindingAuthorButton({
|
|
42
|
+
sessionId, t, useInput, inputActions, usePtcSettings, useBindingCommand,
|
|
43
|
+
}) {
|
|
44
|
+
const input = useInput?.(snapshot => snapshot)
|
|
45
|
+
const available = useBindingCommand(snapshot => snapshot)
|
|
46
|
+
const settings = usePtcSettings(snapshot => snapshot)
|
|
47
|
+
const [review, view] = useBindingReview(sessionId)
|
|
48
|
+
const anchorRef = React.useRef(null)
|
|
49
|
+
const attachAnchor = React.useCallback(element => {
|
|
50
|
+
anchorRef.current = element
|
|
51
|
+
review.access = element
|
|
52
|
+
}, [review])
|
|
53
|
+
const firstItemRef = React.useRef(null)
|
|
54
|
+
const manageItemRef = React.useRef(null)
|
|
55
|
+
const reloadItemRef = React.useRef(null)
|
|
56
|
+
const settingsItemRef = React.useRef(null)
|
|
57
|
+
const focused = React.useRef(false)
|
|
58
|
+
const dialogReturnFocus = React.useRef(null)
|
|
59
|
+
const hoverTimer = React.useRef(undefined)
|
|
60
|
+
const [menu, setMenu] = React.useState(null)
|
|
61
|
+
const [managing, setManaging] = React.useState(false)
|
|
62
|
+
const [settingsOpen, setSettingsOpen] = React.useState(false)
|
|
63
|
+
// The menu catalog and the workbench catalog are independent sources: a
|
|
64
|
+
// read or write in one never invalidates the other's in-flight request.
|
|
65
|
+
const catalogSource = React.useMemo(() => catalogOwner.claim(), [catalogOwner])
|
|
66
|
+
const catalogState = React.useSyncExternalStore(catalogSource.subscribe, catalogSource.getSnapshot)
|
|
67
|
+
const catalog = catalogState.catalog
|
|
68
|
+
const catalogStatus = catalogState.status
|
|
69
|
+
const catalogError = catalogState.error
|
|
70
|
+
// Rows act on the catalog already on screen. A background read keeps them
|
|
71
|
+
// usable; only a write in flight blocks a second toggle on the same menu.
|
|
72
|
+
const catalogReady = catalog !== null && catalogStatus !== 'writing'
|
|
73
|
+
const catalogFocus = React.useRef(null)
|
|
74
|
+
const toastSequence = React.useRef(0)
|
|
75
|
+
const [toast, setToast] = React.useState(null)
|
|
76
|
+
const hasDraft = view.mounted && view.candidate !== null && view.action === null
|
|
77
|
+
const quickAccess = featureEnabled(settings, 'authorButton')
|
|
78
|
+
const canAuthor = quickAccess && available && typeof useInput === 'function' && typeof inputActions?.setDraft === 'function'
|
|
79
|
+
const menuOpen = (hasDraft || quickAccess) && view.reachable && !managing && !settingsOpen
|
|
80
|
+
&& menu !== null && menu.key === view.candidateKey
|
|
81
|
+
// The dialog owns the management session; losing the input surface only
|
|
82
|
+
// suspends reads, so a draft survives a temporary Host takeover.
|
|
83
|
+
const workbench = useWorkbenchController({
|
|
84
|
+
enabled: quickAccess && managing, active: view.reachable, callUserBindings,
|
|
85
|
+
})
|
|
86
|
+
const refreshCatalog = React.useCallback(async (reload = false) => {
|
|
87
|
+
if (reload) captureCatalogFocus()
|
|
88
|
+
await catalogSource.read({ reload })
|
|
89
|
+
}, [catalogSource])
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
const reset = () => {
|
|
92
|
+
catalogSource.reset()
|
|
93
|
+
setMenu(null)
|
|
94
|
+
setManaging(false)
|
|
95
|
+
setSettingsOpen(false)
|
|
96
|
+
}
|
|
97
|
+
const unsubscribe = subscribeReset(reset)
|
|
98
|
+
return () => {
|
|
99
|
+
unsubscribe()
|
|
100
|
+
catalogSource.release()
|
|
101
|
+
}
|
|
102
|
+
}, [catalogSource])
|
|
103
|
+
const toggleBinding = async entry => {
|
|
104
|
+
if (!catalogReady || !quickAccess) return
|
|
105
|
+
captureCatalogFocus()
|
|
106
|
+
await catalogSource.write(() => callUserBindings(entry.enabled ? 'disable' : 'enable', {
|
|
107
|
+
id: entry.id, expectedRevision: catalog.revision,
|
|
108
|
+
}))
|
|
109
|
+
}
|
|
110
|
+
const cancelHoverOpen = React.useCallback(() => {
|
|
111
|
+
if (hoverTimer.current === undefined) return
|
|
112
|
+
clearTimeout(hoverTimer.current)
|
|
113
|
+
hoverTimer.current = undefined
|
|
114
|
+
}, [])
|
|
115
|
+
React.useEffect(() => cancelHoverOpen, [cancelHoverOpen])
|
|
116
|
+
const showMenu = mode => {
|
|
117
|
+
cancelHoverOpen()
|
|
118
|
+
if (!menuOpen && quickAccess) void refreshCatalog()
|
|
119
|
+
setMenu({ key: view.candidateKey, mode, step: 'root' })
|
|
120
|
+
}
|
|
121
|
+
// Touch has no hover: its tap reaches the same menu through the click path.
|
|
122
|
+
const hoverMenu = event => {
|
|
123
|
+
if (event.pointerType === 'touch' || menuOpen) return
|
|
124
|
+
cancelHoverOpen()
|
|
125
|
+
hoverTimer.current = setTimeout(() => {
|
|
126
|
+
hoverTimer.current = undefined
|
|
127
|
+
showMenu('hover')
|
|
128
|
+
}, HOVER_DWELL_MS)
|
|
129
|
+
}
|
|
130
|
+
const menuElement = () => manageItemRef.current?.closest('[role=menu]')
|
|
131
|
+
?? firstItemRef.current?.closest('[role=menu]')
|
|
132
|
+
const captureCatalogFocus = () => {
|
|
133
|
+
catalogFocus.current = menuElement()?.contains(document.activeElement) ? document.activeElement : null
|
|
134
|
+
}
|
|
135
|
+
// The list must sit next to the entry that opened it. The published Menu reads
|
|
136
|
+
// this rect on open and on every scroll/resize, and re-runs placement whenever
|
|
137
|
+
// the prop identity changes, so measuring the trigger each render also keeps an
|
|
138
|
+
// open list on a composer that grows without emitting a scroll.
|
|
139
|
+
const menuAnchorRect = () => {
|
|
140
|
+
const anchor = anchorRef.current
|
|
141
|
+
if (!anchor) return null
|
|
142
|
+
const rect = anchor.getBoundingClientRect()
|
|
143
|
+
menuElement()?.style.setProperty('--ptc-plus-menu-space', `${Math.max(0, rect.top - 16)}px`)
|
|
144
|
+
return rect
|
|
145
|
+
}
|
|
146
|
+
const hideMenu = () => {
|
|
147
|
+
const itemHasFocus = menuElement()?.contains(document.activeElement)
|
|
148
|
+
|| (catalogFocus.current !== null && document.activeElement === document.body)
|
|
149
|
+
setMenu(null)
|
|
150
|
+
if (itemHasFocus && anchorRef.current?.getClientRects().length) {
|
|
151
|
+
anchorRef.current.querySelector('.ptcPlusAuthorButton')?.focus({ preventScroll: true })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
const observer = typeof IntersectionObserver === 'function'
|
|
156
|
+
? new IntersectionObserver(entries => review.reachable(entries.some(entry => entry.isIntersecting))) : undefined
|
|
157
|
+
if (observer) observer.observe(anchorRef.current)
|
|
158
|
+
else review.reachable(true)
|
|
159
|
+
return () => { observer?.disconnect(); review.reachable(false) }
|
|
160
|
+
}, [review])
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
// Losing the input surface hides the dialog; it does not end the management
|
|
163
|
+
// session. The draft lives in the controller above, so the same text returns
|
|
164
|
+
// with the composer. Explicit close and feature disablement still end it.
|
|
165
|
+
setMenu(null)
|
|
166
|
+
}, [hasDraft, view.reachable, view.candidateKey])
|
|
167
|
+
React.useLayoutEffect(() => {
|
|
168
|
+
if (focused.current && document.activeElement === document.body) {
|
|
169
|
+
focused.current = false
|
|
170
|
+
const anchor = anchorRef.current
|
|
171
|
+
const button = anchor?.querySelector('.ptcPlusAuthorButton')
|
|
172
|
+
if (button?.getClientRects().length) button.focus({ preventScroll: true })
|
|
173
|
+
else focusComposer(anchor)
|
|
174
|
+
}
|
|
175
|
+
}, [hasDraft, canAuthor, view.candidateKey])
|
|
176
|
+
React.useLayoutEffect(() => {
|
|
177
|
+
if (!menuOpen) { catalogFocus.current = null; return }
|
|
178
|
+
if (catalogStatus === 'writing' || catalogStatus === 'loading') return
|
|
179
|
+
const target = catalogFocus.current
|
|
180
|
+
catalogFocus.current = null
|
|
181
|
+
if (!target || document.activeElement !== document.body) return
|
|
182
|
+
const root = menuElement()
|
|
183
|
+
const next = root?.contains(target) && !target.disabled ? target
|
|
184
|
+
: reloadItemRef.current?.closest('button') ?? root?.querySelector('button:not(:disabled)')
|
|
185
|
+
next?.focus({ preventScroll: true })
|
|
186
|
+
}, [catalogStatus, menuOpen])
|
|
187
|
+
React.useLayoutEffect(() => {
|
|
188
|
+
if (!menuOpen || menu.mode === 'hover') return
|
|
189
|
+
// The public Menu commits its measured position before keyboard focus enters it.
|
|
190
|
+
let cancelled = false
|
|
191
|
+
queueMicrotask(() => {
|
|
192
|
+
if (!cancelled && anchorRef.current?.getClientRects().length) menuElement()?.querySelector('button:not(:disabled)')?.focus({ preventScroll: true })
|
|
193
|
+
})
|
|
194
|
+
return () => { cancelled = true }
|
|
195
|
+
}, [menuOpen, menu?.mode, menu?.step])
|
|
196
|
+
React.useEffect(() => {
|
|
197
|
+
if (toast === null || typeof Toast === 'function') return undefined
|
|
198
|
+
const timer = setTimeout(() => setToast(null), 2_500)
|
|
199
|
+
return () => clearTimeout(timer)
|
|
200
|
+
}, [toast])
|
|
201
|
+
const label = hasDraft ? t('bindings.reviewMenuLabel', { count: 1 })
|
|
202
|
+
+ (view.message ? ` · ${t('bindings.reviewAttention')}` : '') : t('bindings.open')
|
|
203
|
+
const hint = t(hasDraft ? 'bindings.draftHint' : 'bindings.openHint')
|
|
204
|
+
// Every authoring action goes through the same busy guard and input owner, so a
|
|
205
|
+
// new or revision request can never overwrite an unsent user draft.
|
|
206
|
+
const prefillAuthoring = value => {
|
|
207
|
+
hideMenu()
|
|
208
|
+
if (!canAuthor) return
|
|
209
|
+
if (typeof input?.draft === 'string' && input.draft.trim() !== '') {
|
|
210
|
+
toastSequence.current += 1
|
|
211
|
+
setToast({ sequence: toastSequence.current, text: t('bindings.composerBusy') })
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
inputActions.setDraft(value)
|
|
215
|
+
}
|
|
216
|
+
const openAuthoring = () => prefillAuthoring('/binding new ')
|
|
217
|
+
const openAuthoringEdit = entry => prefillAuthoring(`/binding edit ${entry.id} `)
|
|
218
|
+
const starButton = h('button', {
|
|
219
|
+
type: 'button', className: 'ptcPlusAuthorButton', 'aria-label': label,
|
|
220
|
+
// Older UI-kit lines ship no Tooltip primitive; the native title carries the hint there.
|
|
221
|
+
title: typeof Tooltip === 'function' ? undefined : hint,
|
|
222
|
+
'aria-haspopup': 'menu', 'aria-expanded': menuOpen,
|
|
223
|
+
onPointerEnter: hoverMenu,
|
|
224
|
+
onPointerLeave: cancelHoverOpen,
|
|
225
|
+
onKeyDown: event => {
|
|
226
|
+
if (['ArrowUp', 'ArrowDown'].includes(event.key)) { event.preventDefault(); showMenu('keyboard') }
|
|
227
|
+
},
|
|
228
|
+
onClick: () => {
|
|
229
|
+
if (menuOpen && menu.mode !== 'hover') hideMenu()
|
|
230
|
+
else showMenu('click')
|
|
231
|
+
},
|
|
232
|
+
}, typeof IconSparkle16 === 'function'
|
|
233
|
+
? h(IconSparkle16, { size: 16, 'aria-hidden': true })
|
|
234
|
+
: h('span', { className: 'ptcPlusAuthorButtonLabel', 'aria-hidden': true }, t('bindings.open')),
|
|
235
|
+
hasDraft ? h('span', { className: 'ptcPlusDraftBadge', 'aria-hidden': true,
|
|
236
|
+
'data-attention': view.message !== null }, '1') : null)
|
|
237
|
+
const catalogItems = !quickAccess ? [] : [
|
|
238
|
+
{ id: 'global-heading', type: 'label', text: t('bindings.quickHeading') },
|
|
239
|
+
...(catalog?.entries ?? []).map(entry => ({ id: `global:${entry.id}`,
|
|
240
|
+
disabled: !catalogReady,
|
|
241
|
+
label: h('span', { className: 'ptcPlusBindingQuickRow', 'data-enabled': entry.enabled },
|
|
242
|
+
h('span', { className: 'ptcPlusBindingQuickName' }, h('strong', { title: entry.name }, entry.name),
|
|
243
|
+
h('span', { className: 'ptcPlusBindingQuickState' }, t(entry.enabled ? 'bindings.enabled' : 'bindings.disabledEntry'))),
|
|
244
|
+
h('span', { className: 'ptcPlusBindingQuickPurpose', title: entry.purpose }, entry.purpose)),
|
|
245
|
+
})),
|
|
246
|
+
...(catalog !== null && catalog.entries.length === 0
|
|
247
|
+
? [{ id: 'empty', type: 'label', text: t('memory.globalEmpty') }] : []),
|
|
248
|
+
...(catalogStatus === 'writing' || (catalog === null && catalogStatus === 'loading')
|
|
249
|
+
? [{ id: 'pending', type: 'label', text: t(catalogStatus === 'writing' ? 'bindings.quickSaving' : 'bindings.quickLoading') }] : []),
|
|
250
|
+
...(catalogError === null ? [] : [{ id: 'error', type: 'label', text: t('bindings.failed', { error: catalogError }) }]),
|
|
251
|
+
]
|
|
252
|
+
// Revision selection is a second step of the same flat Menu. The published Menu
|
|
253
|
+
// disables its scroll viewport as soon as any top-level row has a submenu, so a
|
|
254
|
+
// submenu here would lose the bounded, scrollable catalog this step needs.
|
|
255
|
+
const editItems = [
|
|
256
|
+
{ id: 'edit-heading', type: 'label', text: t('bindings.quickEditHeading') },
|
|
257
|
+
...(catalog?.entries ?? []).map(entry => ({
|
|
258
|
+
id: `edit:${entry.id}`,
|
|
259
|
+
disabled: !catalogReady,
|
|
260
|
+
label: h('span', { className: 'ptcPlusBindingQuickRow' },
|
|
261
|
+
h('span', { className: 'ptcPlusBindingQuickName' }, h('strong', { title: entry.name }, entry.name),
|
|
262
|
+
h('span', { className: 'ptcPlusBindingQuickState' }, t(entry.enabled ? 'bindings.enabled' : 'bindings.disabledEntry'))),
|
|
263
|
+
h('span', { className: 'ptcPlusBindingQuickPurpose', title: entry.purpose }, entry.purpose)),
|
|
264
|
+
})),
|
|
265
|
+
// The back row also anchors menu lookup, so the step still owns its list
|
|
266
|
+
// when the catalog is momentarily empty.
|
|
267
|
+
{ id: 'edit-back', label: h('span', { className: 'ptcPlusBindingMenuAction', ref: firstItemRef }, t('bindings.back')) },
|
|
268
|
+
]
|
|
269
|
+
return h('span', {
|
|
270
|
+
className: 'ptcPlusComposerBindingAnchor', tabIndex: -1,
|
|
271
|
+
onFocusCapture: () => { focused.current = true }, onBlurCapture: () => { focused.current = false },
|
|
272
|
+
'data-text': typeof IconSparkle16 === 'function' ? undefined : true,
|
|
273
|
+
ref: attachAnchor,
|
|
274
|
+
},
|
|
275
|
+
!hasDraft && !quickAccess ? null : h(Menu, {
|
|
276
|
+
className: 'ptcPlusAuthorButtonShell', open: menuOpen,
|
|
277
|
+
anchor: typeof Tooltip === 'function'
|
|
278
|
+
? h(Tooltip, { label: hint, delayMs: 400 }, starButton) : starButton,
|
|
279
|
+
portal: true, side: 'top', dense: true,
|
|
280
|
+
// A hovered menu leaves with the pointer once it leaves the trigger and the
|
|
281
|
+
// list for the published grace, which re-entering either cancels; click and
|
|
282
|
+
// keyboard menus stay until dismissed.
|
|
283
|
+
closeOnPointerLeave: menu !== null && menu.mode === 'hover',
|
|
284
|
+
getAnchorRect: menuAnchorRect,
|
|
285
|
+
selectedIds: (catalog?.entries ?? []).filter(entry => entry.enabled).map(entry => `global:${entry.id}`),
|
|
286
|
+
onClose: hideMenu,
|
|
287
|
+
items: menu?.step === 'edit' ? editItems : [...(hasDraft ? [{ id: 'draft-heading', type: 'label', text: t('bindings.quickDrafts') }, { id: view.candidateKey,
|
|
288
|
+
label: h('span', { className: 'ptcPlusDraftMenuItem', ref: firstItemRef },
|
|
289
|
+
h('strong', null, view.candidate.entry.name),
|
|
290
|
+
h('span', null, t(bindingReviewStatus(view)))) },
|
|
291
|
+
...(quickAccess ? [{ id: 'draft-separator', type: 'separator' }] : [])] : []), ...catalogItems,
|
|
292
|
+
...(quickAccess ? [{ id: 'actions-separator', type: 'separator' }] : []),
|
|
293
|
+
...(canAuthor ? [{ id: 'new', label: h('span', { className: 'ptcPlusBindingMenuAction' }, t('bindings.authorNewDraft')) }] : []),
|
|
294
|
+
...(canAuthor && (catalog?.entries?.length ?? 0) > 0
|
|
295
|
+
? [{ id: 'edit', label: h('span', { className: 'ptcPlusBindingMenuAction' }, t('bindings.authorEdit')) }] : []),
|
|
296
|
+
...(quickAccess ? [
|
|
297
|
+
...(catalogError === null ? [] : [{ id: 'reload', label: h('span', { ref: reloadItemRef }, t('bindings.reload')) }]),
|
|
298
|
+
{ id: 'manage', label: h('span', { ref: manageItemRef, className: 'ptcPlusBindingMenuAction' }, t('bindings.manage')) },
|
|
299
|
+
] : []),
|
|
300
|
+
// The entry is also the shortcut to this plugin's own settings, so the
|
|
301
|
+
// row names the seat the installed generation declared and its hint
|
|
302
|
+
// carries the full path that opens it.
|
|
303
|
+
{ id: 'settings', label: h('span', {
|
|
304
|
+
ref: settingsItemRef, className: 'ptcPlusBindingMenuAction', title: settingsPath(t),
|
|
305
|
+
}, t('settings.menuEntry')) },
|
|
306
|
+
],
|
|
307
|
+
onSelect: id => {
|
|
308
|
+
if (!anchorRef.current?.getClientRects().length) return
|
|
309
|
+
if (id === 'edit') {
|
|
310
|
+
setMenu(current => current === null ? current : { ...current, step: 'edit' })
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
if (id === 'edit-back') {
|
|
314
|
+
setMenu(current => current === null ? current : { ...current, step: 'root' })
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
if (id.startsWith('edit:')) {
|
|
318
|
+
const entry = catalog?.entries.find(entry => `edit:${entry.id}` === id)
|
|
319
|
+
if (entry) openAuthoringEdit(entry)
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
if (id.startsWith('global:')) {
|
|
323
|
+
const entry = catalog?.entries.find(entry => `global:${entry.id}` === id)
|
|
324
|
+
if (entry) void toggleBinding(entry)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
if (id === 'reload') { void refreshCatalog(true); return }
|
|
328
|
+
if (id === 'settings') {
|
|
329
|
+
hideMenu()
|
|
330
|
+
setSettingsOpen(true)
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
hideMenu()
|
|
334
|
+
if (id === 'new') openAuthoring()
|
|
335
|
+
else if (id === 'manage') {
|
|
336
|
+
dialogReturnFocus.current = null
|
|
337
|
+
setManaging(true)
|
|
338
|
+
}
|
|
339
|
+
else if (id === view.candidateKey) openBindingReview(review, view.candidate)
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
// Hiding the composer unmounts the dialog, not the controller: the draft survives.
|
|
343
|
+
managing && quickAccess && view.reachable
|
|
344
|
+
? h(BindingsDialog, {
|
|
345
|
+
controller: workbench, t, onClose: () => setManaging(false), returnFocusRef: dialogReturnFocus,
|
|
346
|
+
getReturnFocus: () => anchorRef.current?.querySelector('.ptcPlusAuthorButton'),
|
|
347
|
+
}) : null,
|
|
348
|
+
settingsOpen && view.reachable
|
|
349
|
+
? h(PTCPlusSettingsDialog, {
|
|
350
|
+
t, usePtcSettings, updateSetting, callUserBindings,
|
|
351
|
+
onClose: () => {
|
|
352
|
+
setSettingsOpen(false)
|
|
353
|
+
queueMicrotask(() => anchorRef.current?.querySelector('.ptcPlusAuthorButton')
|
|
354
|
+
?.focus({ preventScroll: true }))
|
|
355
|
+
},
|
|
356
|
+
}) : null,
|
|
357
|
+
toast === null ? null : typeof Toast === 'function'
|
|
358
|
+
? h(Toast, {
|
|
359
|
+
key: toast.sequence,
|
|
360
|
+
text: toast.text,
|
|
361
|
+
anchor: anchorRef.current,
|
|
362
|
+
onDone: () => setToast(null),
|
|
363
|
+
})
|
|
364
|
+
: h('span', {
|
|
365
|
+
key: toast.sequence, className: 'ptcPlusComposerNotice', role: 'status',
|
|
366
|
+
}, toast.text))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function BindingCandidateContent({ candidate, t, showName = true }) {
|
|
370
|
+
const preferences = bindingModelPreferences(candidate.entry.modelContext)
|
|
371
|
+
const contextTitle = React.useId()
|
|
372
|
+
return h('div', { className: 'ptcPlusAuthoringDraft' },
|
|
373
|
+
showName ? h('strong', null, candidate.entry.name) : null,
|
|
374
|
+
h('span', { className: 'ptcPlusBindingMeta' }, `${candidate.entry.scope} - ${candidate.entry.symbols.join(', ')}`),
|
|
375
|
+
candidate.entry.purpose ? h('p', { className: 'ptcPlusMessage' }, candidate.entry.purpose) : null,
|
|
376
|
+
typeof CodeBlock === 'function'
|
|
377
|
+
? h(CodeBlock, { code: candidate.entry.source, lang: 'typescript', className: 'ptcPlusBindingCommandCode',
|
|
378
|
+
copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
|
|
379
|
+
: h('pre', { className: 'ptcPlusBindingCommandSource' }, candidate.entry.source),
|
|
380
|
+
h('section', { className: 'ptcPlusCandidateContext', 'aria-labelledby': contextTitle },
|
|
381
|
+
h('h4', { id: contextTitle }, t('bindings.modelContext')),
|
|
382
|
+
h('dl', null,
|
|
383
|
+
h('div', { className: 'ptcPlusCandidateDeclaration' },
|
|
384
|
+
h('dt', null, t('bindings.declaration')),
|
|
385
|
+
h('dd', { 'data-included': preferences.includeDeclaration },
|
|
386
|
+
preferences.includeDeclaration ? h(IconCheckOutline14, { size: 14, 'aria-hidden': true }) : null,
|
|
387
|
+
t(preferences.includeDeclaration ? 'bindings.declarationIncluded' : 'bindings.declarationExcluded'))),
|
|
388
|
+
h('div', { className: 'ptcPlusCandidatePrompt' },
|
|
389
|
+
h('dt', null, t('bindings.instructions')),
|
|
390
|
+
h('dd', { 'data-empty': preferences.instructions === '' },
|
|
391
|
+
preferences.instructions || t('bindings.noInstructions'))))))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Native editable semantics are enough to restore focus; no Host class, store or editor internals are used.
|
|
395
|
+
function focusComposer(anchor) {
|
|
396
|
+
if (!anchor || anchor.getClientRects().length === 0) return
|
|
397
|
+
for (let parent = anchor.parentElement; parent; parent = parent.parentElement) {
|
|
398
|
+
const editable = [...parent.querySelectorAll('textarea, [contenteditable="true"]')]
|
|
399
|
+
.find(element => element.getClientRects().length > 0 && !element.disabled)
|
|
400
|
+
if (editable) { editable.focus({ preventScroll: true }); return }
|
|
401
|
+
}
|
|
402
|
+
anchor.focus({ preventScroll: true })
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function openBindingReview(review, candidate = review.getSnapshot().candidate) {
|
|
406
|
+
if (!review.display('expanded', candidate)) return
|
|
407
|
+
requestAnimationFrame(() => {
|
|
408
|
+
if (review.getSnapshot().candidate === candidate && review.panel?.getClientRects().length) {
|
|
409
|
+
review.panel.focus({ preventScroll: true })
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function focusBindingReviewAccess(review) {
|
|
415
|
+
const anchor = review.access
|
|
416
|
+
const button = anchor?.querySelector('button')
|
|
417
|
+
if (button?.getClientRects().length) button.focus({ preventScroll: true })
|
|
418
|
+
else focusComposer(anchor)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function fitBindingReview(panel) {
|
|
422
|
+
const body = panel?.querySelector('.ptcPlusBindingDockBody')
|
|
423
|
+
const anchor = panel?.parentElement
|
|
424
|
+
if (!anchor || typeof ResizeObserver !== 'function') return undefined
|
|
425
|
+
const ancestors = []
|
|
426
|
+
let seat = panel
|
|
427
|
+
let viewport
|
|
428
|
+
for (let parent = panel.parentElement; parent; parent = parent.parentElement) {
|
|
429
|
+
ancestors.push(parent)
|
|
430
|
+
if (/auto|scroll|hidden|clip/.test(getComputedStyle(parent).overflowY)) {
|
|
431
|
+
viewport = parent
|
|
432
|
+
break
|
|
433
|
+
}
|
|
434
|
+
seat = parent
|
|
435
|
+
}
|
|
436
|
+
if (!viewport) return undefined
|
|
437
|
+
let frame
|
|
438
|
+
const update = () => {
|
|
439
|
+
if (!panel.getClientRects().length) return
|
|
440
|
+
const top = Math.max(viewport.getBoundingClientRect().top + viewport.clientTop,
|
|
441
|
+
window.visualViewport?.offsetTop ?? 0)
|
|
442
|
+
// Float above the complete composer stack without changing its height or covering other docks.
|
|
443
|
+
const offset = anchor.getBoundingClientRect().bottom - seat.getBoundingClientRect().top + 8
|
|
444
|
+
panel.style.setProperty('--ptc-plus-review-offset', `${Math.max(8, Math.ceil(offset))}px`)
|
|
445
|
+
const room = Math.max(0, seat.getBoundingClientRect().top - top - 16)
|
|
446
|
+
panel.style.setProperty('--ptc-plus-review-height', `${Math.floor(room)}px`)
|
|
447
|
+
if (body) {
|
|
448
|
+
const chrome = panel.scrollHeight - body.offsetHeight + 2
|
|
449
|
+
const available = Math.max(0, room - chrome)
|
|
450
|
+
// In short viewports one scroller keeps source and actions reachable together.
|
|
451
|
+
panel.dataset.scroll = available < 80 ? 'panel' : 'body'
|
|
452
|
+
body.style.setProperty('--ptc-plus-review-space', `${Math.floor(available)}px`)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const schedule = () => {
|
|
456
|
+
cancelAnimationFrame(frame)
|
|
457
|
+
frame = requestAnimationFrame(update)
|
|
458
|
+
}
|
|
459
|
+
const observer = new ResizeObserver(schedule)
|
|
460
|
+
for (const element of [panel, ...panel.children, ...ancestors]) observer.observe(element)
|
|
461
|
+
window.visualViewport?.addEventListener('resize', schedule)
|
|
462
|
+
window.visualViewport?.addEventListener('scroll', schedule)
|
|
463
|
+
update()
|
|
464
|
+
return () => {
|
|
465
|
+
observer.disconnect()
|
|
466
|
+
cancelAnimationFrame(frame)
|
|
467
|
+
window.visualViewport?.removeEventListener('resize', schedule)
|
|
468
|
+
window.visualViewport?.removeEventListener('scroll', schedule)
|
|
469
|
+
panel.style.removeProperty('--ptc-plus-review-offset')
|
|
470
|
+
panel.style.removeProperty('--ptc-plus-review-height')
|
|
471
|
+
delete panel.dataset.scroll
|
|
472
|
+
body?.style.removeProperty('--ptc-plus-review-space')
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function BindingReviewDock({ sessionId, useProjection, t }) {
|
|
477
|
+
const raw = useProjection('ptcPlusBindingDraft')
|
|
478
|
+
const projection = React.useMemo(() => bindingDraftProjection(raw), [raw])
|
|
479
|
+
const [review, view] = useBindingReview(sessionId)
|
|
480
|
+
const focused = React.useRef(false)
|
|
481
|
+
// The composer entry follows the dock; wait for its refs to attach in the same commit.
|
|
482
|
+
React.useEffect(() => {
|
|
483
|
+
if (view.visibility !== 'hidden') return
|
|
484
|
+
if (view.action !== null && focused.current && document.activeElement === document.body) {
|
|
485
|
+
focusBindingReviewAccess(review)
|
|
486
|
+
}
|
|
487
|
+
focused.current = false
|
|
488
|
+
}, [review, view.visibility, view.action])
|
|
489
|
+
React.useLayoutEffect(() => review.attach(), [review])
|
|
490
|
+
React.useLayoutEffect(() => { review.sync(projection) }, [review, projection])
|
|
491
|
+
const title = React.useId()
|
|
492
|
+
const content = React.useId()
|
|
493
|
+
const expanded = view.visibility === 'expanded'
|
|
494
|
+
const candidateKey = view.candidateKey
|
|
495
|
+
React.useLayoutEffect(() => fitBindingReview(review.panel),
|
|
496
|
+
[review, candidateKey, expanded, view.visibility, view.message])
|
|
497
|
+
const close = () => {
|
|
498
|
+
review.display('hidden')
|
|
499
|
+
requestAnimationFrame(() => focusBindingReviewAccess(review))
|
|
500
|
+
}
|
|
501
|
+
const act = (operation, activate = false) => {
|
|
502
|
+
// Keep focus in the review when pending state disables the clicked action.
|
|
503
|
+
if (review.panel?.contains(document.activeElement)) review.panel.focus({ preventScroll: true })
|
|
504
|
+
void review.act(operation, activate)
|
|
505
|
+
}
|
|
506
|
+
if (view.candidate === null || view.visibility === 'hidden') return null
|
|
507
|
+
return h('div', { className: 'ptcPlusBindingDockAnchor' },
|
|
508
|
+
h('section', { key: candidateKey, className: 'ptcPlusBindingDock', 'aria-labelledby': title, tabIndex: -1,
|
|
509
|
+
ref: element => { review.panel = element },
|
|
510
|
+
onFocusCapture: () => { focused.current = true },
|
|
511
|
+
onBlurCapture: event => {
|
|
512
|
+
// Blink blurs the removed panel before layout effects; completion still owns that lost focus.
|
|
513
|
+
if (event.relatedTarget || review.getSnapshot().action === null) focused.current = false
|
|
514
|
+
},
|
|
515
|
+
'aria-busy': view.busy },
|
|
516
|
+
h('div', { className: 'ptcPlusBindingDockHead' },
|
|
517
|
+
h('button', { type: 'button', className: 'ptcPlusBindingDockToggle',
|
|
518
|
+
'aria-label': t(expanded ? 'bindings.reviewCollapse' : 'bindings.reviewExpand'),
|
|
519
|
+
'aria-expanded': expanded, 'aria-controls': expanded ? content : undefined,
|
|
520
|
+
onClick: () => review.display(expanded ? 'collapsed' : 'expanded') },
|
|
521
|
+
h('span', { className: 'ptcPlusBindingDockSymbol', 'aria-hidden': true }, '</>'),
|
|
522
|
+
h('span', { className: 'ptcPlusBindingDockHeading' },
|
|
523
|
+
h('strong', { id: title, title: view.candidate.entry.name }, view.candidate.entry.name),
|
|
524
|
+
h('span', { role: 'status', title: t(bindingReviewStatus(view)) },
|
|
525
|
+
`${t('bindings.reviewTitle')} · ${t(bindingReviewStatus(view))}`)),
|
|
526
|
+
h('span', { className: 'ptcPlusBindingDockChevron', 'aria-hidden': true },
|
|
527
|
+
h(IconChevronDownOutline14, { size: 16 }))),
|
|
528
|
+
h(IconButton, { icon: IconCloseOutline16, label: t('bindings.reviewClose'), onClick: close })),
|
|
529
|
+
!expanded ? null : h(React.Fragment, null,
|
|
530
|
+
h('div', { className: 'ptcPlusBindingDockBody', id: content, tabIndex: 0,
|
|
531
|
+
role: 'region', 'aria-label': t('bindings.source') },
|
|
532
|
+
h(BindingCandidateContent, { candidate: view.candidate, t, showName: false })),
|
|
533
|
+
view.message === null ? null : h('p', { className: 'ptcPlusMessage ptcPlusDanger', role: 'status' }, t(view.message)),
|
|
534
|
+
h('div', { className: 'ptcPlusBindingDockActions' },
|
|
535
|
+
h(ActionButton, { className: 'ptcPlusBindingDockDiscard', 'data-kind': 'ghost',
|
|
536
|
+
disabled: !view.writable || view.busy,
|
|
537
|
+
onClick: () => act('discard-draft') }, t('bindings.draftDiscard')),
|
|
538
|
+
h(ActionButton, { disabled: !view.writable || view.busy,
|
|
539
|
+
onClick: () => act('save-draft', false) }, t('bindings.draftSave')),
|
|
540
|
+
h(ActionButton, { 'data-kind': 'primary', disabled: !view.writable || view.busy,
|
|
541
|
+
onClick: () => act('save-draft', true) }, t('bindings.draftSaveEnable')),
|
|
542
|
+
view.message === null ? null : h(ActionButton, { disabled: view.busy,
|
|
543
|
+
onClick: () => review.reset() }, t('bindings.reviewRetry'))))))
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function BindingCommandCard({ node, sessionId, t, useProjection }) {
|
|
547
|
+
const projection = bindingDraftProjection(useProjection?.('ptcPlusBindingDraft'))
|
|
548
|
+
const [review, current] = useBindingReview(sessionId)
|
|
549
|
+
const historical = projection?.history?.find(record => record.commandId === node.commandId)
|
|
550
|
+
const matches = projection?.commandId === node.commandId
|
|
551
|
+
const currentMatches = current.candidate?.commandId === node.commandId
|
|
552
|
+
const candidate = historical?.candidate ?? (currentMatches ? current.candidate : null)
|
|
553
|
+
const action = historical?.action ?? (currentMatches ? current.action : null)
|
|
554
|
+
const phase = action?.state ?? (candidate !== null ? 'ready'
|
|
555
|
+
: node.outcome?.kind === 'error' || (matches && projection.phase === 'failed') ? 'failed'
|
|
556
|
+
: matches && projection.phase === 'pending' ? 'pending' : 'idle')
|
|
557
|
+
const status = action != null ? bindingReviewStatus({ action })
|
|
558
|
+
: phase === 'ready' ? 'bindings.commandGenerated'
|
|
559
|
+
: phase === 'pending' ? 'bindings.commandPending'
|
|
560
|
+
: phase === 'failed' ? 'bindings.commandFailed'
|
|
561
|
+
: node.outcome === null ? 'bindings.commandAdmitting' : 'bindings.commandAccepted'
|
|
562
|
+
const canOpen = matches && currentMatches && action == null && current.mounted && current.reachable
|
|
563
|
+
return h('section', { className: 'ptcPlusBindingCommand', 'data-phase': phase,
|
|
564
|
+
'aria-label': t('bindings.commandTitle') },
|
|
565
|
+
h('div', { className: 'ptcPlusBindingCommandHeader' },
|
|
566
|
+
h('strong', { className: 'ptcPlusBindingCommandTitle' }, t('bindings.commandTitle')),
|
|
567
|
+
h('span', { className: 'ptcPlusBindingCommandState' },
|
|
568
|
+
h('span', { className: 'ptcPlusBindingCommandStateDot', 'aria-hidden': true }), t(status))),
|
|
569
|
+
h('pre', { className: 'ptcPlusBindingCommandRequirement' }, `/binding${node.args ?? ''}`),
|
|
570
|
+
node.outcome?.kind !== 'error' ? null : h('p', { className: 'ptcPlusMessage ptcPlusDanger' },
|
|
571
|
+
node.outcome.text ?? t('bindings.commandFailed')),
|
|
572
|
+
canOpen ? h(ActionButton, { onClick: () => openBindingReview(review) }, t('bindings.reviewOpen')) : null,
|
|
573
|
+
!current.mounted && (candidate !== null || matches && projection.phase === 'ready')
|
|
574
|
+
? h('p', { className: 'ptcPlusMessage' }, t('bindings.reviewUnavailable')) : null,
|
|
575
|
+
candidate === null ? null : h('details', { className: 'ptcPlusBindingSourceDetails' },
|
|
576
|
+
h('summary', null, t('tool.source')), h(BindingCandidateContent, { candidate, t })))
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return { BindingAuthorButton, BindingReviewDock, BindingCommandCard }
|
|
580
|
+
}
|