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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CONFIG_DEFAULTS } from '../internal/config-spec.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Settings snapshot to feature eligibility. Each feature names the settings that
|
|
3
5
|
* gate exactly one contribution, so a new gated surface reads one table instead
|
|
@@ -11,22 +13,30 @@ const FEATURE_SETTINGS = Object.freeze({
|
|
|
11
13
|
bindings: ['userBindingsEnabled'],
|
|
12
14
|
// Enhanced run_code tool rows.
|
|
13
15
|
toolView: ['enhancedToolView'],
|
|
16
|
+
toolHover: ['enhancedToolView', 'toolHoverPreviewEnabled'],
|
|
14
17
|
// The REPL view tab; the session preset condition is added by its registration.
|
|
15
18
|
replView: ['replViewEnabled'],
|
|
16
19
|
// The composer authoring menu.
|
|
17
20
|
authorButton: ['userBindingsEnabled', 'bindingAuthorButtonVisible'],
|
|
21
|
+
// Settings and capability refresh do not require Global User Bindings.
|
|
22
|
+
ptcMenu: ['bindingAuthorButtonVisible'],
|
|
18
23
|
})
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
|
-
* Pure settings projection: a feature is eligible
|
|
22
|
-
*
|
|
23
|
-
*
|
|
26
|
+
* Pure settings projection: a feature is eligible unless the settings say
|
|
27
|
+
* otherwise. Only a readable snapshot can turn a feature off — an unreadable or
|
|
28
|
+
* not-yet-arrived settings transport falls back to the defaults the plugin ships
|
|
29
|
+
* in `CONFIG_FIELDS`, so a transport outage degrades the settings surface
|
|
30
|
+
* instead of removing every contribution the plugin owns. Writability gates
|
|
31
|
+
* setting writes, not contribution presence.
|
|
24
32
|
*/
|
|
25
33
|
export function featureEnabled(snapshot, feature) {
|
|
26
34
|
const rule = FEATURE_SETTINGS[feature]
|
|
27
35
|
if (rule === undefined) throw new Error(`Unknown client feature: ${feature}`)
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
const value = snapshot?.status === 'ready' && snapshot.value !== undefined
|
|
37
|
+
? snapshot.value
|
|
38
|
+
: CONFIG_DEFAULTS
|
|
39
|
+
if (value.enabled === false) return false
|
|
30
40
|
return rule.every(key => value[key] !== false)
|
|
31
41
|
}
|
|
32
42
|
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { SETTINGS_NAMESPACE } from '../internal/config-spec.js'
|
|
2
|
+
import { ForwardRef, Lazy, Memo, isValidElementType } from 'react-is'
|
|
3
|
+
|
|
4
|
+
/** Host exports are components, including React's wrapped object types. */
|
|
5
|
+
export function isHostComponent(component) {
|
|
6
|
+
if (typeof component === 'function') return true
|
|
7
|
+
if (typeof component !== 'object' || component === null) return false
|
|
8
|
+
const seen = new Set()
|
|
9
|
+
while (isValidElementType(component)) {
|
|
10
|
+
if (typeof component !== 'object' || component === null) return true
|
|
11
|
+
if (seen.has(component)) return false
|
|
12
|
+
seen.add(component)
|
|
13
|
+
// react-is also admits server references and wrappers with missing payloads.
|
|
14
|
+
if (component.$$typeof === ForwardRef) return typeof component.render === 'function'
|
|
15
|
+
if (component.$$typeof === Lazy) return typeof component._init === 'function'
|
|
16
|
+
&& typeof component._payload === 'object' && component._payload !== null
|
|
17
|
+
if (component.$$typeof !== Memo) return false
|
|
18
|
+
if (component.compare != null && typeof component.compare !== 'function') return false
|
|
19
|
+
component = component.type
|
|
20
|
+
}
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
2
23
|
|
|
3
24
|
const HOST_ICON_EXPORTS = Object.freeze({
|
|
4
25
|
check: ['IconCheckOutlineRegular', 'IconCheckOutline14'],
|
|
@@ -28,7 +49,7 @@ ABSENT_HOST_ICON.absentHostIcon = true
|
|
|
28
49
|
|
|
29
50
|
/** Whether a normalized icon entry is a glyph the host actually published. */
|
|
30
51
|
export function isHostIconComponent(icon) {
|
|
31
|
-
return
|
|
52
|
+
return isHostComponent(icon) && icon.absentHostIcon !== true
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
/**
|
|
@@ -42,7 +63,7 @@ export function hostIconComponents(primitives) {
|
|
|
42
63
|
return Object.fromEntries(Object.entries(HOST_ICON_EXPORTS).map(([name, candidates]) => [
|
|
43
64
|
name,
|
|
44
65
|
candidates.map(candidate => primitives?.[candidate])
|
|
45
|
-
.find(
|
|
66
|
+
.find(isHostComponent) ?? ABSENT_HOST_ICON,
|
|
46
67
|
]))
|
|
47
68
|
}
|
|
48
69
|
|
|
@@ -22,6 +22,7 @@ export function createIndicatorView(React, deps) {
|
|
|
22
22
|
normalizeReplMemorySnapshot, unavailableReplMemorySnapshot,
|
|
23
23
|
ReplMemoryCard, replPopoverIsOpen, placeReplPopover,
|
|
24
24
|
catalogOwner, subscribeReset,
|
|
25
|
+
useObservedReplMemory, observeRepl,
|
|
25
26
|
} = deps
|
|
26
27
|
const h = React.createElement
|
|
27
28
|
|
|
@@ -225,13 +226,10 @@ export function createIndicatorView(React, deps) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
}, [cancelClose, cancelOpen, schedulePlacement])
|
|
229
|
+
const memory = useObservedReplMemory({ sessionId, projected: projectionMemory,
|
|
230
|
+
active: expanded && sessionUsesPtcPreset(preset) && featureEnabled(settings, 'plugin'),
|
|
231
|
+
elementRef: popoverRef, observeRepl })
|
|
228
232
|
if (!sessionUsesPtcPreset(preset) || !featureEnabled(settings, 'plugin')) return null
|
|
229
|
-
let memory
|
|
230
|
-
try {
|
|
231
|
-
memory = normalizeReplMemorySnapshot(projectionMemory)
|
|
232
|
-
} catch {
|
|
233
|
-
memory = unavailableReplMemorySnapshot()
|
|
234
|
-
}
|
|
235
233
|
const popoverId = `ptc-plus-repl-${String(resolvedSessionId).replace(/[^A-Za-z0-9_-]/g, '-')}`
|
|
236
234
|
const titleId = `${popoverId}-title`
|
|
237
235
|
return h('span', { className: 'ptcPlusActiveShell', ref: shellRef },
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { isHostComponent } from './client-host-compat.js'
|
|
2
|
+
import * as RadixSelect from '@radix-ui/react-select'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the Harness Client components this plugin renders against the
|
|
6
|
+
* installed generation.
|
|
7
|
+
*
|
|
8
|
+
* The plugin may load a generation that renamed, removed, or reshaped a
|
|
9
|
+
* primitive; a missing component would otherwise throw while rendering and take
|
|
10
|
+
* the whole slot entry down with it. Each component therefore resolves to the
|
|
11
|
+
* shipped primitive when that generation provides it and to a plugin-local
|
|
12
|
+
* fallback otherwise, so a primitive change degrades one control instead of
|
|
13
|
+
* blanking the entry that renders it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** One menu row: separators and labels paint; every other row selects by id. */
|
|
17
|
+
function menuEntry(React, item, onSelect) {
|
|
18
|
+
const h = React.createElement
|
|
19
|
+
const content = item.label ?? item.text ?? null
|
|
20
|
+
if (item.type === 'separator') {
|
|
21
|
+
return h('div', { key: item.id, className: 'ptcPlusFallbackMenuSeparator', role: 'separator' })
|
|
22
|
+
}
|
|
23
|
+
if (item.type === 'label') {
|
|
24
|
+
return h('div', { key: item.id, className: 'ptcPlusFallbackMenuLabel' }, content)
|
|
25
|
+
}
|
|
26
|
+
return h('button', {
|
|
27
|
+
key: item.id, type: 'button', role: 'menuitem', className: 'ptcPlusFallbackMenuItem',
|
|
28
|
+
disabled: item.disabled === true,
|
|
29
|
+
onClick: () => onSelect?.(item.id),
|
|
30
|
+
}, content)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function useCompositionGuard(React, open) {
|
|
34
|
+
const composing = React.useRef(false)
|
|
35
|
+
const ended = React.useRef(false)
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
composing.current = false
|
|
38
|
+
ended.current = false
|
|
39
|
+
if (!open) return undefined
|
|
40
|
+
const start = () => { composing.current = true }
|
|
41
|
+
const end = () => { composing.current = false; ended.current = true }
|
|
42
|
+
const release = () => { ended.current = false }
|
|
43
|
+
const blur = () => { composing.current = false; ended.current = false }
|
|
44
|
+
document.addEventListener('compositionstart', start, true)
|
|
45
|
+
document.addEventListener('compositionend', end, true)
|
|
46
|
+
document.addEventListener('keyup', release, true)
|
|
47
|
+
window.addEventListener('blur', blur)
|
|
48
|
+
return () => {
|
|
49
|
+
document.removeEventListener('compositionstart', start, true)
|
|
50
|
+
document.removeEventListener('compositionend', end, true)
|
|
51
|
+
document.removeEventListener('keyup', release, true)
|
|
52
|
+
window.removeEventListener('blur', blur)
|
|
53
|
+
composing.current = false
|
|
54
|
+
ended.current = false
|
|
55
|
+
}
|
|
56
|
+
}, [open])
|
|
57
|
+
return event => {
|
|
58
|
+
const guarded = composing.current || ended.current || event.isComposing === true || event.keyCode === 229
|
|
59
|
+
ended.current = false
|
|
60
|
+
return guarded
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Menu fallback: the anchor, the item list, the pinned footer and the children region. */
|
|
65
|
+
function createFallbackMenu(React, createPortal = content => content) {
|
|
66
|
+
const h = React.createElement
|
|
67
|
+
return function FallbackMenu({ open, items = [], children, footer, className, listClassName, autoFocus, anchor,
|
|
68
|
+
onClose, onSelect, portal = false, side = 'bottom', align = 'start', getAnchorRect, closeOnPointerLeave = false }) {
|
|
69
|
+
const root = React.useRef(null)
|
|
70
|
+
const list = React.useRef(null)
|
|
71
|
+
const closeTimer = React.useRef(null)
|
|
72
|
+
const close = React.useRef(onClose)
|
|
73
|
+
close.current = onClose
|
|
74
|
+
const isComposing = useCompositionGuard(React, open)
|
|
75
|
+
const cancelClose = () => { clearTimeout(closeTimer.current); closeTimer.current = null }
|
|
76
|
+
const scheduleClose = () => {
|
|
77
|
+
if (!closeOnPointerLeave) return
|
|
78
|
+
cancelClose()
|
|
79
|
+
closeTimer.current = setTimeout(() => close.current?.(), 180)
|
|
80
|
+
}
|
|
81
|
+
React.useEffect(() => cancelClose, [open])
|
|
82
|
+
React.useLayoutEffect(() => {
|
|
83
|
+
if (!open) return undefined
|
|
84
|
+
const place = () => {
|
|
85
|
+
const rect = getAnchorRect?.() ?? root.current?.getBoundingClientRect()
|
|
86
|
+
const surface = list.current
|
|
87
|
+
if (!rect || !surface) return
|
|
88
|
+
const viewport = window.visualViewport
|
|
89
|
+
const viewportTop = viewport?.offsetTop ?? 0
|
|
90
|
+
const viewportLeft = viewport?.offsetLeft ?? 0
|
|
91
|
+
const viewportHeight = viewport?.height ?? window.innerHeight
|
|
92
|
+
const viewportWidth = viewport?.width ?? window.innerWidth
|
|
93
|
+
const available = Math.max(0, side === 'top' ? rect.top - viewportTop - 12
|
|
94
|
+
: viewportTop + viewportHeight - rect.bottom - 12)
|
|
95
|
+
surface.style.maxHeight = `${available}px`
|
|
96
|
+
const measured = surface.getBoundingClientRect()
|
|
97
|
+
const left = align === 'end' ? rect.right - measured.width : rect.left
|
|
98
|
+
surface.style.left = `${Math.max(viewportLeft + 8, Math.min(left, viewportLeft + viewportWidth - measured.width - 8))}px`
|
|
99
|
+
surface.style.top = `${side === 'top' ? Math.max(viewportTop + 8, rect.top - measured.height - 4) : rect.bottom + 4}px`
|
|
100
|
+
}
|
|
101
|
+
place()
|
|
102
|
+
window.addEventListener('resize', place)
|
|
103
|
+
window.addEventListener('scroll', place, true)
|
|
104
|
+
window.visualViewport?.addEventListener('resize', place)
|
|
105
|
+
window.visualViewport?.addEventListener('scroll', place)
|
|
106
|
+
return () => {
|
|
107
|
+
window.removeEventListener('resize', place)
|
|
108
|
+
window.removeEventListener('scroll', place, true)
|
|
109
|
+
window.visualViewport?.removeEventListener('resize', place)
|
|
110
|
+
window.visualViewport?.removeEventListener('scroll', place)
|
|
111
|
+
}
|
|
112
|
+
}, [open, side, align, getAnchorRect, children, items, footer])
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
if (!open) return undefined
|
|
115
|
+
if (autoFocus) list.current?.querySelector('button:not(:disabled)')?.focus()
|
|
116
|
+
const onKeyDown = event => {
|
|
117
|
+
if (event.defaultPrevented || isComposing(event)) return
|
|
118
|
+
if (event.key === 'Escape') {
|
|
119
|
+
event.preventDefault()
|
|
120
|
+
event.stopPropagation()
|
|
121
|
+
if (!event.repeat) close.current?.()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (!root.current?.contains(event.target) && !list.current?.contains(event.target)) return
|
|
125
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return
|
|
126
|
+
const controls = [...list.current.querySelectorAll('button:not(:disabled)')]
|
|
127
|
+
if (controls.length === 0) return
|
|
128
|
+
const current = controls.indexOf(document.activeElement)
|
|
129
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? controls.length - 1
|
|
130
|
+
: (current + (event.key === 'ArrowDown' ? 1 : -1) + controls.length) % controls.length
|
|
131
|
+
event.preventDefault()
|
|
132
|
+
controls[next].focus()
|
|
133
|
+
}
|
|
134
|
+
const onPointerDown = event => {
|
|
135
|
+
if (root.current?.contains(event.target) || list.current?.contains(event.target)) return
|
|
136
|
+
close.current?.()
|
|
137
|
+
}
|
|
138
|
+
document.addEventListener('keydown', onKeyDown)
|
|
139
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
140
|
+
return () => {
|
|
141
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
142
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
143
|
+
}
|
|
144
|
+
}, [open, autoFocus])
|
|
145
|
+
const rows = list => list.map(item => menuEntry(React, item, onSelect))
|
|
146
|
+
const surface = open ? h('div', { ref: list, className: ['ptcPlusFallbackMenuList', listClassName].filter(Boolean).join(' '),
|
|
147
|
+
role: 'menu', onPointerEnter: cancelClose, onPointerLeave: scheduleClose }, ...rows(items),
|
|
148
|
+
...(Array.isArray(footer) ? rows(footer) : []),
|
|
149
|
+
children ?? null) : null
|
|
150
|
+
return h('span', { ref: root, className: ['ptcPlusFallbackMenu', className].filter(Boolean).join(' '),
|
|
151
|
+
onPointerEnter: cancelClose, onPointerLeave: scheduleClose }, anchor ?? null,
|
|
152
|
+
portal && surface !== null ? createPortal(surface, document.body) : surface)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Modal fallback: headless content or a titled dialog that closes on Escape and backdrop. */
|
|
157
|
+
function createFallbackModal(React, createPortal = content => content) {
|
|
158
|
+
const h = React.createElement
|
|
159
|
+
const modalStack = []
|
|
160
|
+
return function FallbackModal({ open, onClose, title, closeLabel, description, className, contentClassName, headless, children }) {
|
|
161
|
+
const root = React.useRef(null)
|
|
162
|
+
const close = React.useRef(onClose)
|
|
163
|
+
close.current = onClose
|
|
164
|
+
const isComposing = useCompositionGuard(React, open)
|
|
165
|
+
const focusable = 'button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select:not(:disabled),[href],[tabindex]:not([tabindex="-1"]),[contenteditable=true],summary'
|
|
166
|
+
React.useLayoutEffect(() => {
|
|
167
|
+
if (!open) return undefined
|
|
168
|
+
const previous = document.activeElement
|
|
169
|
+
const surface = root.current
|
|
170
|
+
const target = surface?.querySelector('[data-modal-autofocus]:not(:disabled)') ?? surface?.querySelector(focusable) ?? surface
|
|
171
|
+
target?.focus({ preventScroll: true })
|
|
172
|
+
modalStack.push(surface)
|
|
173
|
+
const onDocumentKeyDown = event => {
|
|
174
|
+
if (modalStack.at(-1) === surface) onKeyDown(event)
|
|
175
|
+
}
|
|
176
|
+
document.addEventListener('keydown', onDocumentKeyDown)
|
|
177
|
+
return () => {
|
|
178
|
+
document.removeEventListener('keydown', onDocumentKeyDown)
|
|
179
|
+
modalStack.splice(modalStack.indexOf(surface), 1)
|
|
180
|
+
if (document.activeElement === document.body || surface?.contains(document.activeElement)) previous?.focus?.({ preventScroll: true })
|
|
181
|
+
}
|
|
182
|
+
}, [open])
|
|
183
|
+
const onKeyDown = event => {
|
|
184
|
+
if (event.defaultPrevented || isComposing(event)) return
|
|
185
|
+
if (event.key === 'Escape') {
|
|
186
|
+
event.preventDefault()
|
|
187
|
+
event.stopPropagation()
|
|
188
|
+
if (!event.repeat) close.current?.()
|
|
189
|
+
} else if (event.key === 'Tab') {
|
|
190
|
+
const controls = [...root.current.querySelectorAll(focusable)]
|
|
191
|
+
.filter(control => control.getClientRects().length > 0 && !control.closest('[aria-hidden=true],[hidden]'))
|
|
192
|
+
const target = event.shiftKey ? controls.at(-1) : controls[0]
|
|
193
|
+
const boundary = event.shiftKey ? controls[0] : controls.at(-1)
|
|
194
|
+
if (controls.length === 0 || document.activeElement === boundary || !root.current.contains(document.activeElement)) {
|
|
195
|
+
event.preventDefault()
|
|
196
|
+
;(target ?? root.current).focus({ preventScroll: true })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!open) return null
|
|
201
|
+
const body = h('div', { className: contentClassName ?? 'ptcPlusFallbackModalBody' }, children)
|
|
202
|
+
const surface = ['ptcPlusFallbackModal', className].filter(Boolean).join(' ')
|
|
203
|
+
return createPortal(h('div', {
|
|
204
|
+
className: 'ptcPlusFallbackModalBackdrop',
|
|
205
|
+
onPointerDown: event => { if (event.target === event.currentTarget) onClose?.() },
|
|
206
|
+
}, h('div', { ref: root, tabIndex: -1, className: surface, role: 'dialog', 'aria-modal': 'true', 'aria-label': title },
|
|
207
|
+
headless === true ? null : h('div', { className: 'ptcPlusFallbackModalHead' },
|
|
208
|
+
h('h2', null, title),
|
|
209
|
+
h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': closeLabel ?? title, onClick: () => onClose?.() },
|
|
210
|
+
closeLabel ?? '\u00d7')),
|
|
211
|
+
headless === true || description === undefined ? null : h('p', { className: 'ptcPlusDetail' }, description),
|
|
212
|
+
headless === true ? children : body)), document.body)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Resolve every component the plugin renders.
|
|
218
|
+
* @param primitives - the installed generation's Client primitives.
|
|
219
|
+
* @param React - the browser React instance.
|
|
220
|
+
* @returns the component set the views consume.
|
|
221
|
+
*/
|
|
222
|
+
export function resolvePrimitives(primitives, React, createPortal) {
|
|
223
|
+
return Object.freeze({
|
|
224
|
+
Button: primitives.Button,
|
|
225
|
+
CodeBlock: primitives.CodeBlock,
|
|
226
|
+
DisclosureRow: primitives.DisclosureRow,
|
|
227
|
+
Menu: isHostComponent(primitives.Menu) ? primitives.Menu : createFallbackMenu(React, createPortal),
|
|
228
|
+
MenuFallback: createFallbackMenu(React, createPortal),
|
|
229
|
+
Select: createSelect(React),
|
|
230
|
+
Modal: isHostComponent(primitives.Modal) ? primitives.Modal : createFallbackModal(React, createPortal),
|
|
231
|
+
Toast: primitives.Toast,
|
|
232
|
+
Tooltip: primitives.Tooltip,
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Shared themed choice control; Radix owns listbox focus, typeahead, and dismissal. */
|
|
237
|
+
export function createSelect(React) {
|
|
238
|
+
const h = React.createElement
|
|
239
|
+
return function Select({ value, options, onChange, label, disabled = false, className = '',
|
|
240
|
+
placeholder, dataStrategy, autoFocus = false }) {
|
|
241
|
+
const [open, setOpen] = React.useState(false)
|
|
242
|
+
React.useEffect(() => { if (disabled) setOpen(false) }, [disabled])
|
|
243
|
+
const chosen = options.find(option => option.value === value)
|
|
244
|
+
let emptyToken = '__ptc_plus_empty_choice__'
|
|
245
|
+
while (options.some(option => option.value === emptyToken)) emptyToken += '_'
|
|
246
|
+
return h(RadixSelect.Root, { value: value === '' ? emptyToken : value, open, disabled,
|
|
247
|
+
onOpenChange: setOpen, onValueChange: next => onChange(next === emptyToken ? '' : next) },
|
|
248
|
+
h(RadixSelect.Trigger, { className: ['ptcPlusSelectTrigger', className].filter(Boolean).join(' '),
|
|
249
|
+
'aria-label': label, 'data-strategy': dataStrategy, 'data-modal-autofocus': autoFocus || undefined },
|
|
250
|
+
h(RadixSelect.Value, { placeholder: placeholder ?? chosen?.label ?? '' }),
|
|
251
|
+
h(RadixSelect.Icon, { className: 'ptcPlusSelectChevron', 'aria-hidden': true },
|
|
252
|
+
h('svg', { viewBox: '0 0 16 16', width: 16, height: 16, fill: 'none' },
|
|
253
|
+
h('path', { d: 'm4 6 4 4 4-4', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' })))),
|
|
254
|
+
h(RadixSelect.Portal, null,
|
|
255
|
+
h(RadixSelect.Content, { className: 'ptcPlusSelectMenu', position: 'popper', sideOffset: 4,
|
|
256
|
+
onEscapeKeyDown: event => event.stopPropagation() },
|
|
257
|
+
h(RadixSelect.Viewport, { className: 'ptcPlusSelectViewport' },
|
|
258
|
+
...options.map(option => h(RadixSelect.Item, { key: option.value, value: option.value === '' ? emptyToken : option.value,
|
|
259
|
+
disabled: option.disabled === true, className: 'ptcPlusSelectOption' },
|
|
260
|
+
h(RadixSelect.ItemText, null, option.label),
|
|
261
|
+
h(RadixSelect.ItemIndicator, { className: 'ptcPlusSelectCheck' }, '✓')))))))
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Adapt host action styling, or own it completely when the primitive is absent. */
|
|
266
|
+
export function createActionButton(React, Button) {
|
|
267
|
+
const h = React.createElement
|
|
268
|
+
return function ActionButton({ className = '', 'data-kind': kind, ...props }) {
|
|
269
|
+
const classes = className.split(/\s+/).filter(name => name !== '' && name !== 'ptcPlusButton')
|
|
270
|
+
return isHostComponent(Button)
|
|
271
|
+
? h(Button, {
|
|
272
|
+
type: 'button', ...props, size: 'sm',
|
|
273
|
+
variant: kind === 'primary' ? 'primary' : kind === 'ghost' ? 'ghost' : 'outline',
|
|
274
|
+
className: classes.join(' '),
|
|
275
|
+
})
|
|
276
|
+
: h('button', { type: 'button', ...props, className: ['ptcPlusButton', ...classes].join(' '), 'data-kind': kind })
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export { createFallbackMenu, createFallbackModal }
|
package/src/client-repl-view.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isHostIconComponent } from './client-host-compat.js'
|
|
1
|
+
import { isHostComponent, isHostIconComponent } from './client-host-compat.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* REPL surface views: the conversation tab (session bindings + global workbench)
|
|
@@ -7,7 +7,7 @@ import { isHostIconComponent } from './client-host-compat.js'
|
|
|
7
7
|
*/
|
|
8
8
|
export function createReplView(React, deps) {
|
|
9
9
|
const {
|
|
10
|
-
CodeBlock, featureEnabled, normalizeReplMemorySnapshot,
|
|
10
|
+
CodeBlock, Select, featureEnabled, normalizeReplMemorySnapshot,
|
|
11
11
|
unavailableReplMemorySnapshot, useSessionPreset, sessionUsesPtcPreset,
|
|
12
12
|
useWorkbenchController, UserBindingsWorkbench,
|
|
13
13
|
icons: { search: IconSearchOutline16, chevron: IconChevronDownOutline14 },
|
|
@@ -16,6 +16,18 @@ export function createReplView(React, deps) {
|
|
|
16
16
|
|
|
17
17
|
function ReplComposer() { return null }
|
|
18
18
|
|
|
19
|
+
function useObservedReplMemory({ sessionId, projected, active, elementRef, observeRepl }) {
|
|
20
|
+
const memory = React.useMemo(() => {
|
|
21
|
+
try { return normalizeReplMemorySnapshot(projected) } catch { return unavailableReplMemorySnapshot() }
|
|
22
|
+
}, [projected])
|
|
23
|
+
const [observed, setObserved] = React.useState(null)
|
|
24
|
+
React.useEffect(() => active && typeof observeRepl === 'function'
|
|
25
|
+
? observeRepl(sessionId, elementRef.current, memory,
|
|
26
|
+
value => setObserved({ sessionId, source: memory, value }))
|
|
27
|
+
: undefined, [active, observeRepl, sessionId, memory, elementRef])
|
|
28
|
+
return observed?.sessionId === sessionId && observed.source === memory ? observed.value : memory
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
function ReplBindingInspector({ entry, preview, t }) {
|
|
20
32
|
const readable = preview?.status === 'readable'
|
|
21
33
|
return h('aside', { className: 'ptcPlusBindingInspector', 'aria-label': entry.name },
|
|
@@ -23,7 +35,7 @@ export function createReplView(React, deps) {
|
|
|
23
35
|
h('strong', null, entry.name),
|
|
24
36
|
h('span', null, t('memory.location', entry.definition))),
|
|
25
37
|
h('div', { className: 'ptcPlusObservationLabel' }, t('console.definition')),
|
|
26
|
-
|
|
38
|
+
isHostComponent(CodeBlock)
|
|
27
39
|
? h(CodeBlock, { code: entry.definition.source, lang: 'typescript', className: 'ptcPlusObservationCode',
|
|
28
40
|
copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
|
|
29
41
|
: h('pre', { className: 'ptcPlusObservationCode' }, entry.definition.source),
|
|
@@ -61,10 +73,9 @@ export function createReplView(React, deps) {
|
|
|
61
73
|
isHostIconComponent(IconSearchOutline16) ? h(IconSearchOutline16, { size: 16 }) : null,
|
|
62
74
|
h('input', { value: query, onChange: event => setQuery(event.target.value),
|
|
63
75
|
placeholder: t('console.search'), 'aria-label': t('console.search') })),
|
|
64
|
-
h(
|
|
65
|
-
'
|
|
66
|
-
|
|
67
|
-
t(value === 'all' ? 'console.allKinds' : `memory.kind.${value}`))))),
|
|
76
|
+
h(Select, { value: kind, onChange: setKind, label: t('console.kind'),
|
|
77
|
+
options: ['all', 'variable', 'function', 'class', 'import'].map(value => ({ value,
|
|
78
|
+
label: t(value === 'all' ? 'console.allKinds' : `memory.kind.${value}`) })) })),
|
|
68
79
|
h('div', { className: 'ptcPlusObservations' },
|
|
69
80
|
h('table', { className: 'ptcPlusObservationTable', 'aria-label': t('console.session') },
|
|
70
81
|
h('thead', null, h('tr', null,
|
|
@@ -98,18 +109,14 @@ export function createReplView(React, deps) {
|
|
|
98
109
|
const globalEnabled = featureEnabled(settings, 'bindings')
|
|
99
110
|
const workbench = useWorkbenchController({ enabled: globalEnabled, active: true, callUserBindings })
|
|
100
111
|
const eligible = featureEnabled(settings, 'replView') && sessionUsesPtcPreset(preset)
|
|
101
|
-
const memory = React.useMemo(() => {
|
|
102
|
-
try { return normalizeReplMemorySnapshot(projected) } catch { return unavailableReplMemorySnapshot() }
|
|
103
|
-
}, [projected])
|
|
104
|
-
const [observed, setObserved] = React.useState(null)
|
|
105
112
|
const observationRegion = React.useRef(null)
|
|
113
|
+
const memory = useObservedReplMemory({ sessionId, projected, active: eligible,
|
|
114
|
+
elementRef: observationRegion, observeRepl })
|
|
106
115
|
React.useEffect(() => eligible ? hideComposer(sessionId) : undefined, [eligible, hideComposer, sessionId])
|
|
107
|
-
React.useEffect(() => eligible ? observeRepl(sessionId, observationRegion.current, memory,
|
|
108
|
-
value => setObserved({ sessionId, source: memory, value })) : undefined, [eligible, observeRepl, sessionId, memory])
|
|
109
116
|
if (!eligible) return null
|
|
110
117
|
return h('div', { className: 'ptcPlusConsole', 'data-conversation-composer-overlay': '' },
|
|
111
118
|
h('div', { className: 'ptcPlusConsoleSection', ref: observationRegion }, h(ReplSessionBindings, {
|
|
112
|
-
key: sessionId, memory
|
|
119
|
+
key: sessionId, memory, t })),
|
|
113
120
|
globalEnabled ? h('div', { className: 'ptcPlusConsoleSection ptcPlusBindingsSurface' },
|
|
114
121
|
h(UserBindingsWorkbench, { controller: workbench, t, headingLabel: t('console.global') })) : null)
|
|
115
122
|
}
|
|
@@ -275,7 +282,7 @@ export function createReplView(React, deps) {
|
|
|
275
282
|
line: binding.definition.line,
|
|
276
283
|
column: binding.definition.column,
|
|
277
284
|
})),
|
|
278
|
-
|
|
285
|
+
isHostComponent(CodeBlock)
|
|
279
286
|
? h(CodeBlock, {
|
|
280
287
|
code: binding.definition.source,
|
|
281
288
|
lang: 'typescript',
|
|
@@ -290,5 +297,5 @@ export function createReplView(React, deps) {
|
|
|
290
297
|
: h('span', { className: 'ptcPlusReplMore' }, t('memory.more', { count: memory.omitted }))))
|
|
291
298
|
}
|
|
292
299
|
|
|
293
|
-
return { ReplComposer, ReplConsole, ReplMemoryCard, replPopoverIsOpen, placeReplPopover }
|
|
300
|
+
return { ReplComposer, ReplConsole, ReplMemoryCard, replPopoverIsOpen, placeReplPopover, useObservedReplMemory }
|
|
294
301
|
}
|