dsh-ptc-plus 0.4.8 → 0.5.0
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 +61 -123
- package/README.md +61 -123
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +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.cjs +42336 -0
- package/cordis.patch.yml +13 -2
- package/docs/README.md +7 -2
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -179
- package/docs/adr/0008-define-agent-operating-contract.md +5 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
- package/docs/adr/0023-global-user-bindings.md +3 -1
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +101 -35
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
- package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
- package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
- package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
- package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
- package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
- package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
- package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
- package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
- package/docs/adr/0041-record-cell-execution-directories.md +65 -0
- package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
- package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
- package/docs/adr/README.md +6 -0
- package/docs/architecture.md +25 -27
- package/docs/binding-review-placement-design.md +29 -21
- package/docs/capability-planning-components.md +93 -0
- package/docs/capability-projection.md +7 -0
- package/docs/client-ui.md +88 -29
- package/docs/client-visual-contract.md +65 -0
- package/docs/context-management.md +22 -0
- package/docs/context-optimization-plan.md +353 -0
- package/docs/context-public-interface-coverage.md +25 -0
- package/docs/durability-design.md +31 -14
- package/docs/evaluation.md +41 -6
- package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
- package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
- package/docs/installation.md +50 -8
- package/docs/program-data-plane.md +5 -3
- package/docs/publishing.md +5 -5
- package/docs/release-coverage.md +73 -0
- package/docs/release-notes.md +1 -1
- package/docs/review-procedure.md +56 -0
- package/docs/runtime-reference.md +48 -16
- package/docs/semantic-validation.md +14 -2
- package/docs/stateful-binding-updates-proposal.md +2 -2
- package/docs/user-bindings.en.md +2 -2
- package/docs/user-bindings.md +3 -3
- package/docs/value-wire.md +11 -1
- package/docs/verification-optimization-work.md +52 -15
- package/docs/verification.md +30 -86
- package/index.js +147 -23
- package/internal/binding-toggle-receipt.js +24 -0
- package/internal/capability-command-receipt.js +29 -0
- package/internal/capability-environment.js +1247 -0
- package/internal/capability-plan.js +285 -0
- package/internal/capability-policy.js +96 -0
- package/internal/capability-statistics-store.js +180 -0
- package/internal/capability-statistics.js +210 -0
- package/internal/compiler-platform.js +5 -3
- package/internal/compiler-service.js +2 -9
- package/internal/config-spec.js +79 -9
- package/internal/cordis-tools-owner.js +22 -19
- package/internal/direct-surface-owner.js +29 -29
- package/internal/dynamic-environment-runtime.js +9 -9
- package/internal/edit-transport-owner.js +183 -139
- package/internal/execution-provider-owner.js +160 -0
- package/internal/execution-seam-compat.js +76 -120
- package/internal/isolated-worker.js +12 -8
- package/internal/kernel-worker.js +104 -32
- package/internal/native-root-dynamic.js +12 -13
- package/internal/program-bindings.js +190 -0
- package/internal/recovery-tips.js +53 -23
- package/internal/rejected-cell-editor.js +8 -8
- package/internal/repl-observation-interest.js +11 -3
- package/internal/rpc-contract.js +1 -0
- package/internal/runtime-bridge-owner.js +351 -82
- package/internal/runtime-config.js +58 -0
- package/internal/runtime-contexts.js +85 -16
- package/internal/runtime-intrinsics.js +1 -0
- package/internal/runtime-messages.js +41 -0
- package/internal/sdk-projection.js +10 -82
- package/internal/session-cell-executor.js +42 -19
- package/internal/session-events.js +74 -4
- package/internal/session-journal-recovery.js +904 -184
- package/internal/session-journal-schema.js +6 -3
- package/internal/session-journal.js +31 -2
- package/internal/session-log-view.js +189 -43
- package/internal/session-runtime.js +175 -23
- package/internal/settings-compat.js +23 -6
- package/internal/stateful-module-compiler.js +3 -2
- package/internal/stateful-module-runtime.js +7 -3
- package/internal/stateful-root-runtime.js +9 -4
- package/internal/user-binding-authoring.js +2 -7
- package/internal/user-bindings-owner.js +47 -7
- package/internal/validated-parse-repair.js +1 -1
- package/internal/value-wire.js +68 -21
- package/internal/worker-cwd-virtualization.js +20 -18
- package/internal/worker-realm-surfaces.js +3 -3
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +55 -38
- package/src/client-authoring-view.js +286 -168
- package/src/client-binding-review.js +4 -1
- package/src/client-binding-toggle.js +18 -0
- package/src/client-capabilities-view.js +1341 -0
- package/src/client-capability-sort.js +29 -0
- package/src/client-code-editor.js +6 -0
- package/src/client-copy.js +881 -8
- package/src/client-feature-gates.js +15 -5
- package/src/client-host-compat.js +23 -2
- package/src/client-indicator-view.js +4 -6
- package/src/client-primitives.js +280 -0
- package/src/client-repl-view.js +23 -16
- package/src/client-settings-view.js +412 -69
- package/src/client-styles.js +193 -41
- package/src/client-tool-hover.js +185 -0
- package/src/client-tool-view.js +45 -11
- package/src/client-transport.js +44 -9
- package/src/client-workbench.js +9 -10
- package/src/client.js +148 -68
- 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/client.js +0 -31857
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const HOVER_DWELL_MS = 400
|
|
2
|
+
const SCROLL_IDLE_MS = 350
|
|
3
|
+
|
|
4
|
+
/** Presentation excerpt only; the expanded row retains the complete record. */
|
|
5
|
+
export function toolHoverExcerpt(text, maxLines) {
|
|
6
|
+
const prefix = text.slice(0, 1800).replace(/[\uD800-\uDBFF]$/, '')
|
|
7
|
+
const lines = prefix.split(/\r\n|\r|\n/)
|
|
8
|
+
const selected = lines.slice(0, maxLines)
|
|
9
|
+
let truncated = prefix.length < text.length || lines.length > maxLines
|
|
10
|
+
const bounded = selected.map(line => {
|
|
11
|
+
if (line.length <= 100) return line
|
|
12
|
+
truncated = true
|
|
13
|
+
// Do not leave a dangling UTF-16 high surrogate at the cut.
|
|
14
|
+
return line.slice(0, 99).replace(/[\uD800-\uDBFF]$/, '') + '…'
|
|
15
|
+
})
|
|
16
|
+
return { text: bounded.join('\n'), truncated }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Only the owned anchor and preview participate in placement. */
|
|
20
|
+
function placeToolHover(anchor, preview, pointer) {
|
|
21
|
+
const margin = 12
|
|
22
|
+
const gap = 10
|
|
23
|
+
const viewport = window.visualViewport
|
|
24
|
+
const leftEdge = viewport?.offsetLeft ?? 0
|
|
25
|
+
const topEdge = viewport?.offsetTop ?? 0
|
|
26
|
+
const width = viewport?.width ?? window.innerWidth
|
|
27
|
+
const height = viewport?.height ?? window.innerHeight
|
|
28
|
+
const rect = anchor.getBoundingClientRect()
|
|
29
|
+
const previewWidth = Math.min(420, width - margin * 2)
|
|
30
|
+
const right = pointer === null ? rect.right + gap : pointer[0] + 16
|
|
31
|
+
const onRight = right + previewWidth <= leftEdge + width - margin
|
|
32
|
+
const below = Math.max(0, topEdge + height - rect.bottom - gap - margin)
|
|
33
|
+
const above = Math.max(0, rect.top - topEdge - gap - margin)
|
|
34
|
+
const onTop = below < 340 && above > below
|
|
35
|
+
const available = Math.min(360, onRight ? height - margin * 2 : onTop ? above : below)
|
|
36
|
+
if (available < 100 || width < 100) return false
|
|
37
|
+
preview.style.width = `${previewWidth}px`
|
|
38
|
+
preview.style.maxHeight = `${available}px`
|
|
39
|
+
preview.style.left = onRight ? `${right}px` : `${Math.max(leftEdge + margin,
|
|
40
|
+
Math.min(rect.left + 16, leftEdge + width - previewWidth - margin))}px`
|
|
41
|
+
preview.style.top = onRight
|
|
42
|
+
? `${Math.max(topEdge + margin, Math.min(pointer?.[1] ?? rect.top,
|
|
43
|
+
topEdge + height - margin - Math.min(preview.offsetHeight, available)))}px`
|
|
44
|
+
: onTop ? `${rect.top - gap - Math.min(preview.offsetHeight, available)}px`
|
|
45
|
+
: `${rect.bottom + gap}px`
|
|
46
|
+
preview.dataset.placement = onRight ? 'right' : onTop ? 'top' : 'bottom'
|
|
47
|
+
preview.style.visibility = 'visible'
|
|
48
|
+
preview.dataset.clipped = String([...preview.querySelectorAll(
|
|
49
|
+
'.ptcPlusToolHoverContent,.ptcPlusToolHoverSection,.ptcPlusToolHoverDescription,.ptcPlusFeatures,pre',
|
|
50
|
+
)].some(element => element.scrollHeight > element.clientHeight + 1
|
|
51
|
+
|| element.scrollWidth > element.clientWidth + 1))
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Passive quick peeks never enter hit testing. Shared capture observers cancel
|
|
57
|
+
* every pending/visible tool preview on wheel or scroll without cancelling or
|
|
58
|
+
* redispatching the event. A fresh pointer gesture after idle owns reopening.
|
|
59
|
+
*/
|
|
60
|
+
export function createToolHover(React, createPortal) {
|
|
61
|
+
const h = React.createElement
|
|
62
|
+
const subscribers = new Set()
|
|
63
|
+
let lastScroll = -Infinity
|
|
64
|
+
const scrolling = () => {
|
|
65
|
+
lastScroll = Date.now()
|
|
66
|
+
for (const cancel of subscribers) cancel()
|
|
67
|
+
}
|
|
68
|
+
const escaping = event => {
|
|
69
|
+
if (event.key === 'Escape') for (const cancel of subscribers) cancel()
|
|
70
|
+
}
|
|
71
|
+
const subscribe = cancel => {
|
|
72
|
+
if (subscribers.size === 0) {
|
|
73
|
+
window.addEventListener('wheel', scrolling, { capture: true, passive: true })
|
|
74
|
+
window.addEventListener('scroll', scrolling, { capture: true, passive: true })
|
|
75
|
+
window.addEventListener('keydown', escaping)
|
|
76
|
+
}
|
|
77
|
+
subscribers.add(cancel)
|
|
78
|
+
return () => {
|
|
79
|
+
subscribers.delete(cancel)
|
|
80
|
+
if (subscribers.size === 0) {
|
|
81
|
+
window.removeEventListener('wheel', scrolling, true)
|
|
82
|
+
window.removeEventListener('scroll', scrolling, true)
|
|
83
|
+
window.removeEventListener('keydown', escaping)
|
|
84
|
+
lastScroll = -Infinity
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ToolHoverAnchor({ disabled, content, children }) {
|
|
90
|
+
const [visible, setVisible] = React.useState(false)
|
|
91
|
+
const anchorRef = React.useRef(null)
|
|
92
|
+
const previewRef = React.useRef(null)
|
|
93
|
+
const pending = React.useRef(null)
|
|
94
|
+
const visibleRef = React.useRef(false)
|
|
95
|
+
const lastPointer = React.useRef(null)
|
|
96
|
+
const cancelled = React.useRef(false)
|
|
97
|
+
const clearPending = React.useCallback(() => {
|
|
98
|
+
if (pending.current !== null) clearTimeout(pending.current)
|
|
99
|
+
pending.current = null
|
|
100
|
+
}, [])
|
|
101
|
+
const dismiss = React.useCallback(() => {
|
|
102
|
+
clearPending()
|
|
103
|
+
visibleRef.current = false
|
|
104
|
+
// Hide the owned surface immediately, even before React's next render.
|
|
105
|
+
if (previewRef.current !== null) previewRef.current.hidden = true
|
|
106
|
+
setVisible(false)
|
|
107
|
+
}, [clearPending])
|
|
108
|
+
const cancel = React.useCallback(() => {
|
|
109
|
+
cancelled.current = true
|
|
110
|
+
dismiss()
|
|
111
|
+
}, [dismiss])
|
|
112
|
+
React.useEffect(() => disabled ? undefined : subscribe(cancel), [disabled, cancel])
|
|
113
|
+
React.useEffect(() => () => clearPending(), [clearPending])
|
|
114
|
+
React.useLayoutEffect(() => {
|
|
115
|
+
if (disabled) cancel()
|
|
116
|
+
}, [disabled, cancel])
|
|
117
|
+
const show = () => {
|
|
118
|
+
if (disabled) return
|
|
119
|
+
cancelled.current = false
|
|
120
|
+
visibleRef.current = true
|
|
121
|
+
setVisible(true)
|
|
122
|
+
}
|
|
123
|
+
const arm = event => {
|
|
124
|
+
if (disabled || event.pointerType === 'touch' || visibleRef.current
|
|
125
|
+
|| pending.current !== null || Date.now() - lastScroll < SCROLL_IDLE_MS) return
|
|
126
|
+
cancelled.current = false
|
|
127
|
+
pending.current = setTimeout(() => {
|
|
128
|
+
pending.current = null
|
|
129
|
+
if (Date.now() - lastScroll >= SCROLL_IDLE_MS && !cancelled.current) show()
|
|
130
|
+
}, HOVER_DWELL_MS)
|
|
131
|
+
}
|
|
132
|
+
React.useLayoutEffect(() => {
|
|
133
|
+
if (!visible || disabled) return
|
|
134
|
+
const preview = previewRef.current
|
|
135
|
+
const anchor = anchorRef.current
|
|
136
|
+
if (preview === null || anchor === null) return
|
|
137
|
+
preview.hidden = false
|
|
138
|
+
if (typeof preview.showPopover === 'function') preview.showPopover()
|
|
139
|
+
const place = () => {
|
|
140
|
+
if (!placeToolHover(anchor, preview, lastPointer.current)) dismiss()
|
|
141
|
+
}
|
|
142
|
+
place()
|
|
143
|
+
const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(place) : null
|
|
144
|
+
observer?.observe(anchor)
|
|
145
|
+
observer?.observe(preview)
|
|
146
|
+
window.addEventListener('resize', place)
|
|
147
|
+
window.visualViewport?.addEventListener('resize', place)
|
|
148
|
+
return () => {
|
|
149
|
+
observer?.disconnect()
|
|
150
|
+
window.removeEventListener('resize', place)
|
|
151
|
+
window.visualViewport?.removeEventListener('resize', place)
|
|
152
|
+
}
|
|
153
|
+
}, [visible, disabled, dismiss, cancel])
|
|
154
|
+
const preview = !visible || disabled ? null : h('div', {
|
|
155
|
+
ref: previewRef, className: 'ptcPlusToolHover',
|
|
156
|
+
popover: typeof HTMLElement.prototype.showPopover === 'function' ? 'manual' : undefined,
|
|
157
|
+
style: { visibility: 'hidden' },
|
|
158
|
+
role: 'tooltip',
|
|
159
|
+
}, content)
|
|
160
|
+
return h('div', {
|
|
161
|
+
className: 'ptcPlusTool', ref: anchorRef,
|
|
162
|
+
onPointerEnter: event => {
|
|
163
|
+
lastPointer.current = [event.clientX, event.clientY]
|
|
164
|
+
arm(event)
|
|
165
|
+
},
|
|
166
|
+
onPointerMove: event => {
|
|
167
|
+
const point = lastPointer.current
|
|
168
|
+
lastPointer.current = [event.clientX, event.clientY]
|
|
169
|
+
if (point === null || point[0] !== event.clientX || point[1] !== event.clientY) arm(event)
|
|
170
|
+
},
|
|
171
|
+
onPointerLeave: () => { lastPointer.current = null; dismiss() },
|
|
172
|
+
onPointerDownCapture: cancel,
|
|
173
|
+
onKeyDownCapture: event => { if (event.key === 'Escape') cancel() },
|
|
174
|
+
onFocusCapture: event => {
|
|
175
|
+
if (event.target.matches(':focus-visible')) { lastPointer.current = null; clearPending(); show() }
|
|
176
|
+
},
|
|
177
|
+
onBlurCapture: event => {
|
|
178
|
+
if (!event.currentTarget.contains(event.relatedTarget)) dismiss()
|
|
179
|
+
},
|
|
180
|
+
}, children, preview === null ? null : typeof createPortal === 'function'
|
|
181
|
+
? createPortal(preview, document.body) : preview)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { ToolHoverAnchor }
|
|
185
|
+
}
|
package/src/client-tool-view.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { derivePtcToolView } from './client-activity.js'
|
|
2
|
+
import { isHostComponent } from './client-host-compat.js'
|
|
3
|
+
import { createToolHover, toolHoverExcerpt } from './client-tool-hover.js'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* run_code / edit_run_code tool rows. The row is a pure projection of the
|
|
@@ -6,15 +8,16 @@ import { derivePtcToolView } from './client-activity.js'
|
|
|
6
8
|
* DisclosureRow when the host provides it and an accessible fallback otherwise.
|
|
7
9
|
*/
|
|
8
10
|
export function createPtcToolView(React, deps) {
|
|
9
|
-
const { CodeBlock, DisclosureRow, icons } = deps
|
|
11
|
+
const { CodeBlock, DisclosureRow, createPortal, icons } = deps
|
|
10
12
|
const h = React.createElement
|
|
11
|
-
const hasDisclosureRow =
|
|
12
|
-
|
|
13
|
+
const hasDisclosureRow = isHostComponent(DisclosureRow)
|
|
14
|
+
const { ToolHoverAnchor } = createToolHover(React, createPortal)
|
|
13
15
|
|
|
14
|
-
function PTCPlusToolRow({ toolName, block, inspect, t }) {
|
|
16
|
+
function PTCPlusToolRow({ toolName, block, inspect, t, hoverEnabled = true }) {
|
|
15
17
|
const [open, setOpen] = React.useState(false)
|
|
16
18
|
const view = derivePtcToolView(block, toolName)
|
|
17
|
-
const
|
|
19
|
+
const previewable = view.code !== '' || view.output !== ''
|
|
20
|
+
const expandable = previewable || typeof inspect === 'function'
|
|
18
21
|
const stateKey = {
|
|
19
22
|
running: 'tool.running',
|
|
20
23
|
ok: 'tool.completed',
|
|
@@ -35,10 +38,10 @@ export function createPtcToolView(React, deps) {
|
|
|
35
38
|
stateText === null ? null : h('span', { className: 'ptcPlusToolState' }, stateText),
|
|
36
39
|
summaryText === null ? null : h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
|
|
37
40
|
summaryText === null ? null : h('span', { className: 'ptcPlusToolDescription' }, summaryText))
|
|
38
|
-
const
|
|
41
|
+
const renderBody = (withInspect) => h('div', { className: 'ptcPlusToolBody' },
|
|
39
42
|
view.code === '' ? null : h('div', { className: 'ptcPlusToolSection' },
|
|
40
43
|
h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.source')),
|
|
41
|
-
|
|
44
|
+
isHostComponent(CodeBlock)
|
|
42
45
|
? h(CodeBlock, {
|
|
43
46
|
code: view.code, lang: 'typescript', className: 'ptcPlusToolCode',
|
|
44
47
|
copyLabel: t('tool.copy'), copiedLabel: t('tool.copied'),
|
|
@@ -51,7 +54,7 @@ export function createPtcToolView(React, deps) {
|
|
|
51
54
|
className: 'ptcPlusIoText',
|
|
52
55
|
'data-error': view.state === 'error' || undefined,
|
|
53
56
|
}, view.output))),
|
|
54
|
-
typeof inspect !== 'function' ? null : h('button', {
|
|
57
|
+
!withInspect || typeof inspect !== 'function' ? null : h('button', {
|
|
55
58
|
type: 'button', className: 'ptcPlusInspect', onClick: inspect,
|
|
56
59
|
}, h(icons.inspect, { 'aria-hidden': true }), t('tool.inspect')))
|
|
57
60
|
const features = view.features.length === 0 ? null : h('div', { className: 'ptcPlusFeatures' },
|
|
@@ -62,8 +65,34 @@ export function createPtcToolView(React, deps) {
|
|
|
62
65
|
feature.detail === '' ? null
|
|
63
66
|
: h('span', { className: 'ptcPlusFeatureDetail', title: feature.detail }, feature.detail))))
|
|
64
67
|
const collapsedContent = h('div', { className: 'ptcPlusToolPreview' }, summaryLine, features)
|
|
68
|
+
const body = open ? renderBody(true) : null
|
|
69
|
+
const sourceExcerpt = toolHoverExcerpt(view.code, 6)
|
|
70
|
+
const outputExcerpt = toolHoverExcerpt(view.output, 3)
|
|
71
|
+
const preview = h(React.Fragment, null,
|
|
72
|
+
h('div', { className: 'ptcPlusToolHoverHeader', 'data-state': view.state },
|
|
73
|
+
h('span', { className: 'ptcPlusToolHoverHeading' },
|
|
74
|
+
t(toolName === 'edit_run_code' ? 'tool.editPreview' : 'tool.preview')),
|
|
75
|
+
h('span', { className: 'ptcPlusToolHoverState' }, t(stateKey))),
|
|
76
|
+
h('div', { className: 'ptcPlusToolHoverContent' },
|
|
77
|
+
view.description === '' ? null : h('div', { className: 'ptcPlusToolHoverDescription' }, view.description),
|
|
78
|
+
view.code === '' ? null : h('div', { className: 'ptcPlusToolHoverSection' },
|
|
79
|
+
h('span', { className: 'ptcPlusToolSectionLabel' }, `${t('tool.source')} · TypeScript`),
|
|
80
|
+
isHostComponent(CodeBlock) ? h(CodeBlock, {
|
|
81
|
+
code: sourceExcerpt.text, lang: 'typescript', showHeader: false, lineNumbers: true,
|
|
82
|
+
className: 'ptcPlusToolCode ptcPlusToolHoverCode',
|
|
83
|
+
}) : h('pre', { className: 'ptcPlusToolCode ptcPlusToolHoverCode' }, sourceExcerpt.text)),
|
|
84
|
+
view.output === '' ? null : h('div', { className: 'ptcPlusToolHoverSection' },
|
|
85
|
+
h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.result')),
|
|
86
|
+
h('pre', { className: 'ptcPlusIoText', 'data-error': view.state === 'error' || undefined }, outputExcerpt.text)),
|
|
87
|
+
features),
|
|
88
|
+
h('div', { className: 'ptcPlusToolHoverFooter' },
|
|
89
|
+
h('span', null, t('tool.previewHint')),
|
|
90
|
+
h('span', { className: 'ptcPlusToolHoverBounded',
|
|
91
|
+
'data-truncated': sourceExcerpt.truncated || outputExcerpt.truncated || undefined,
|
|
92
|
+
}, t('tool.previewBounded'))))
|
|
93
|
+
const wrapRow = anchor => h(ToolHoverAnchor, { disabled: !hoverEnabled || open || !previewable, content: preview }, anchor)
|
|
65
94
|
if (hasDisclosureRow) {
|
|
66
|
-
return
|
|
95
|
+
return wrapRow(
|
|
67
96
|
h(DisclosureRow, {
|
|
68
97
|
icon: expandable
|
|
69
98
|
? h(icons.chevron, { size: 14 })
|
|
@@ -75,11 +104,16 @@ export function createPtcToolView(React, deps) {
|
|
|
75
104
|
expandOnRowClick: true,
|
|
76
105
|
previewChevron: false,
|
|
77
106
|
keepContentWhenOpen: true,
|
|
107
|
+
className: 'ptcPlusToolDisclosure',
|
|
108
|
+
rowClassName: 'ptcPlusToolRow',
|
|
109
|
+
contentClassName: 'ptcPlusToolDisclosureContent',
|
|
110
|
+
contentLayoutClassName: 'ptcPlusToolDisclosureLayout',
|
|
111
|
+
titleClassName: 'ptcPlusToolTitle',
|
|
78
112
|
collapsedContent,
|
|
79
113
|
children: body,
|
|
80
114
|
}))
|
|
81
115
|
}
|
|
82
|
-
return h(
|
|
116
|
+
return wrapRow(h(React.Fragment, null,
|
|
83
117
|
h('div', {
|
|
84
118
|
className: 'ptcPlusToolSummary', 'data-state': view.state,
|
|
85
119
|
'data-expandable': expandable || undefined,
|
|
@@ -103,7 +137,7 @@ export function createPtcToolView(React, deps) {
|
|
|
103
137
|
h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
|
|
104
138
|
h('span', { className: 'ptcPlusToolDescription' }, summary)),
|
|
105
139
|
features,
|
|
106
|
-
body)
|
|
140
|
+
body))
|
|
107
141
|
}
|
|
108
142
|
|
|
109
143
|
return { PTCPlusToolRow }
|
package/src/client-transport.js
CHANGED
|
@@ -7,9 +7,9 @@ import { normalizeReplMemorySnapshot } from '../internal/repl-memory-projection.
|
|
|
7
7
|
* - observeRepl watches one visible REPL region, retries the watch with bounded
|
|
8
8
|
* backoff and only reports an observation that still matches the requested
|
|
9
9
|
* inventory. Everything it starts is aborted by its disposer.
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - createCommandAccess publishes command availability per session, keyed by
|
|
11
|
+
* epoch so late directory replies cannot revive an ended subscription.
|
|
12
|
+
* Execution receipts must also match the connection and preset generations.
|
|
13
13
|
*/
|
|
14
14
|
export function createReplObserver({ rpc, ctx }) {
|
|
15
15
|
function observeRepl(sessionId, element, memory, onObservation) {
|
|
@@ -28,25 +28,33 @@ export function createReplObserver({ rpc, ctx }) {
|
|
|
28
28
|
previous?.abort()
|
|
29
29
|
}
|
|
30
30
|
const watch = async current => {
|
|
31
|
+
let receipt
|
|
31
32
|
try {
|
|
32
|
-
await rpc.call(RPC_CONTRACTS.repl, 'watch', { sessionId }, current.signal)
|
|
33
|
+
receipt = await rpc.call(RPC_CONTRACTS.repl, 'watch', { sessionId }, current.signal)
|
|
33
34
|
} catch {}
|
|
34
35
|
if (disposed || controller !== current) return
|
|
35
36
|
controller = undefined
|
|
36
37
|
current.abort()
|
|
37
38
|
if (!visible || document.visibilityState === 'hidden') { stop(); return }
|
|
39
|
+
if (receipt?.ok === true && receipt.value?.changed === true) {
|
|
40
|
+
retries = 0
|
|
41
|
+
sync()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
38
44
|
const delay = retryDelays[retries++]
|
|
39
45
|
if (delay === undefined) return
|
|
40
46
|
retryTimer = setTimeout(() => { retryTimer = undefined; sync() }, delay)
|
|
41
47
|
}
|
|
42
48
|
const read = async current => {
|
|
43
|
-
if (!memory.available || memory.entries.length === 0
|
|
49
|
+
if (!memory.available || memory.entries.length === 0) return
|
|
44
50
|
try {
|
|
45
51
|
const result = await rpc.call(RPC_CONTRACTS.repl, 'observe', { sessionId, memory }, current.signal)
|
|
46
52
|
if (disposed || controller !== current || current.signal.aborted || result?.ok !== true || result.value === null) return
|
|
47
53
|
const observed = normalizeReplMemorySnapshot(result.value)
|
|
48
54
|
const { observation, ...inventory } = observed
|
|
49
|
-
|
|
55
|
+
const { observation: _previousObservation, ...expected } = memory
|
|
56
|
+
if (!observed.available || (observation !== undefined
|
|
57
|
+
&& JSON.stringify(inventory) === JSON.stringify(expected))) onObservation(observed)
|
|
50
58
|
} catch {}
|
|
51
59
|
}
|
|
52
60
|
const sync = () => {
|
|
@@ -78,14 +86,15 @@ export function createReplObserver({ rpc, ctx }) {
|
|
|
78
86
|
return { observeRepl }
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
export function
|
|
89
|
+
export function createCommandAccess(scope, name) {
|
|
82
90
|
const entries = new Map()
|
|
83
91
|
let commandRemote
|
|
92
|
+
let connectionGeneration = 0
|
|
84
93
|
const entryFor = (sessionId) => {
|
|
85
94
|
const id = String(sessionId)
|
|
86
95
|
let entry = entries.get(id)
|
|
87
96
|
if (entry === undefined) {
|
|
88
|
-
entry = { available: false, epoch: 0, listeners: new Set() }
|
|
97
|
+
entry = { available: false, epoch: 0, commandGeneration: 0, listeners: new Set() }
|
|
89
98
|
entry.source = {
|
|
90
99
|
getSnapshot: () => entry.available,
|
|
91
100
|
subscribe(listener) {
|
|
@@ -119,7 +128,7 @@ export function createBindingCommandAvailability(scope) {
|
|
|
119
128
|
const result = await commandRemote?.list(String(sessionId))
|
|
120
129
|
available = result?.ok === true
|
|
121
130
|
&& Array.isArray(result.value)
|
|
122
|
-
&& result.value.some(command => command?.name ===
|
|
131
|
+
&& result.value.some(command => command?.name === name)
|
|
123
132
|
} catch {}
|
|
124
133
|
if (entry.epoch === epoch) publish(entry, available)
|
|
125
134
|
}
|
|
@@ -128,10 +137,12 @@ export function createBindingCommandAvailability(scope) {
|
|
|
128
137
|
const entry = entries.get(String(sessionId))
|
|
129
138
|
if (entry === undefined || entry.listeners.size === 0) return
|
|
130
139
|
entry.epoch += 1
|
|
140
|
+
entry.commandGeneration += 1
|
|
131
141
|
publish(entry, false)
|
|
132
142
|
void refresh(sessionId)
|
|
133
143
|
}
|
|
134
144
|
scope.inject(['remote', 'remote.commands'], commandScope => {
|
|
145
|
+
connectionGeneration++
|
|
135
146
|
commandRemote = commandScope.get('remote.commands')
|
|
136
147
|
commandScope.effect(() => commandScope.remote.$on('commands/change', () => {
|
|
137
148
|
for (const sessionId of entries.keys()) void refresh(sessionId)
|
|
@@ -139,6 +150,7 @@ export function createBindingCommandAvailability(scope) {
|
|
|
139
150
|
commandScope.effect(() => commandScope.remote.$on('agent-preset/selected', reset))
|
|
140
151
|
for (const sessionId of entries.keys()) void refresh(sessionId)
|
|
141
152
|
commandScope.effect(() => () => {
|
|
153
|
+
connectionGeneration++
|
|
142
154
|
commandRemote = undefined
|
|
143
155
|
for (const entry of entries.values()) {
|
|
144
156
|
entry.epoch++
|
|
@@ -147,13 +159,36 @@ export function createBindingCommandAvailability(scope) {
|
|
|
147
159
|
})
|
|
148
160
|
})
|
|
149
161
|
scope.on('connection/reset', () => {
|
|
162
|
+
connectionGeneration++
|
|
150
163
|
for (const sessionId of entries.keys()) reset(sessionId)
|
|
151
164
|
})
|
|
152
165
|
scope.effect(() => () => {
|
|
166
|
+
connectionGeneration++
|
|
153
167
|
for (const entry of entries.values()) entry.epoch++
|
|
154
168
|
entries.clear()
|
|
155
169
|
})
|
|
156
170
|
return Object.freeze({
|
|
157
171
|
source: sessionId => entryFor(sessionId).source,
|
|
172
|
+
async run(sessionId, input, signal) {
|
|
173
|
+
signal?.throwIfAborted()
|
|
174
|
+
const remote = commandRemote
|
|
175
|
+
const generation = connectionGeneration
|
|
176
|
+
const entry = entries.get(String(sessionId))
|
|
177
|
+
const commandGeneration = entry?.commandGeneration
|
|
178
|
+
if (!entries.get(String(sessionId))?.available || typeof remote?.execute !== 'function') {
|
|
179
|
+
throw new Error(`/${name} is unavailable in this session`)
|
|
180
|
+
}
|
|
181
|
+
const result = await remote.execute(String(sessionId), `/${name}${input ? ` ${input}` : ''}`, [], signal)
|
|
182
|
+
signal?.throwIfAborted()
|
|
183
|
+
if (remote !== commandRemote || generation !== connectionGeneration
|
|
184
|
+
|| commandGeneration !== entry?.commandGeneration) throw new Error('Command connection changed')
|
|
185
|
+
if (result?.ok !== true || !result.value) throw new Error(result?.error?.message ?? 'Command was not accepted')
|
|
186
|
+
if (result.value.result?.kind !== 'success') throw new Error(result.value.result?.text ?? 'Command failed')
|
|
187
|
+
return result.value.result
|
|
188
|
+
},
|
|
158
189
|
})
|
|
159
190
|
}
|
|
191
|
+
|
|
192
|
+
export function createBindingCommandAvailability(scope) {
|
|
193
|
+
return createCommandAccess(scope, 'binding')
|
|
194
|
+
}
|
package/src/client-workbench.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { blankBinding, editableBinding, bindingPayload } from './client-bindings-data.js'
|
|
2
|
-
import { isHostIconComponent } from './client-host-compat.js'
|
|
2
|
+
import { isHostComponent, isHostIconComponent } from './client-host-compat.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* User Bindings workbench. The controller owns every piece of edit state in one
|
|
@@ -12,7 +12,7 @@ import { isHostIconComponent } from './client-host-compat.js'
|
|
|
12
12
|
* discard an unsaved draft.
|
|
13
13
|
*/
|
|
14
14
|
export function createUserBindingsWorkbench(React, deps) {
|
|
15
|
-
const { TypeScriptEditor, BindingConsole, IconButton, ActionButton, CodeBlock, Modal, icons, catalogOwner } = deps
|
|
15
|
+
const { TypeScriptEditor, BindingConsole, IconButton, ActionButton, CodeBlock, Modal, Select, icons, catalogOwner } = deps
|
|
16
16
|
const h = React.createElement
|
|
17
17
|
|
|
18
18
|
const initialState = Object.freeze({
|
|
@@ -120,7 +120,7 @@ export function createUserBindingsWorkbench(React, deps) {
|
|
|
120
120
|
}
|
|
121
121
|
if (action.imported === true) next.importPath = ''
|
|
122
122
|
next.message = action.removed === true ? { key: 'bindings.removed' }
|
|
123
|
-
: action.imported === true ? { key: 'bindings.imported' } : state.message
|
|
123
|
+
: action.imported === true ? { key: 'bindings.imported' } : stored ? { key: stored.enabled ? 'bindings.toggleEnabled' : 'bindings.toggleDisabled', params: { name: stored.name } } : state.message
|
|
124
124
|
return next
|
|
125
125
|
}
|
|
126
126
|
case 'reloadResult': {
|
|
@@ -391,7 +391,7 @@ export function createUserBindingsWorkbench(React, deps) {
|
|
|
391
391
|
h('details', { className: 'ptcPlusBindingSection ptcPlusBindingSourcePreview', open: true },
|
|
392
392
|
h('summary', { className: 'ptcPlusBindingFieldLabel' }, t('bindings.declaration')),
|
|
393
393
|
state.declaration === '' ? h('p', { className: 'ptcPlusMessage' }, t('bindings.unvalidated'))
|
|
394
|
-
:
|
|
394
|
+
: isHostComponent(CodeBlock)
|
|
395
395
|
? h(CodeBlock, { code: state.declaration, lang: 'typescript', className: 'ptcPlusCodeBlock',
|
|
396
396
|
copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
|
|
397
397
|
: h('pre', { className: 'ptcPlusDeclaration' }, state.declaration)),
|
|
@@ -420,7 +420,7 @@ export function createUserBindingsWorkbench(React, deps) {
|
|
|
420
420
|
state.editing
|
|
421
421
|
? h(TypeScriptEditor, { documentId: state.documentId, value: state.draft.source, disabled: state.busy,
|
|
422
422
|
label: t('bindings.source'), onChange: value => edit('source', value) })
|
|
423
|
-
: state.sourceOpen &&
|
|
423
|
+
: state.sourceOpen && isHostComponent(CodeBlock)
|
|
424
424
|
? h(CodeBlock, { code: state.draft.source, lang: 'typescript', className: 'ptcPlusSourceCode',
|
|
425
425
|
copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
|
|
426
426
|
: state.sourceOpen ? h('pre', { className: 'ptcPlusSourceCode' }, state.draft.source) : null)),
|
|
@@ -450,10 +450,9 @@ export function createUserBindingsWorkbench(React, deps) {
|
|
|
450
450
|
})),
|
|
451
451
|
h('label', { className: 'ptcPlusBindingField' },
|
|
452
452
|
h('span', { className: 'ptcPlusBindingFieldLabel' }, t('bindings.scope')),
|
|
453
|
-
h(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}, h('option', { value: 'namespace' }, 'namespace'), h('option', { value: 'top-level' }, 'top-level'))),
|
|
453
|
+
h(Select, { value: state.draft.scope, disabled: state.busy || !state.editing,
|
|
454
|
+
label: t('bindings.scope'), onChange: next => edit('scope', next),
|
|
455
|
+
options: [{ value: 'namespace', label: 'namespace' }, { value: 'top-level', label: 'top-level' }] })),
|
|
457
456
|
h('label', { className: 'ptcPlusBindingField' },
|
|
458
457
|
h('span', { className: 'ptcPlusBindingFieldLabel' }, t('bindings.symbols')),
|
|
459
458
|
h('input', {
|
|
@@ -502,7 +501,7 @@ export function createUserBindingsWorkbench(React, deps) {
|
|
|
502
501
|
const trapFocus = event => {
|
|
503
502
|
if (event.key !== 'Tab') return
|
|
504
503
|
const controls = [...content.current.querySelectorAll('button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select:not(:disabled),[contenteditable=true],summary')]
|
|
505
|
-
.filter(element => element.getClientRects().length > 0)
|
|
504
|
+
.filter(element => element.getClientRects().length > 0 && !element.closest('[aria-hidden=true],[hidden]'))
|
|
506
505
|
const first = controls[0]
|
|
507
506
|
const last = controls.at(-1)
|
|
508
507
|
if ((event.shiftKey && document.activeElement === first) || (!event.shiftKey && document.activeElement === last)) {
|