@xp266/dshtui 0.1.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/bin/dshtui.js +65 -0
- package/cordis.patch.yml +81 -0
- package/lib/contract/index.d.mts +2 -0
- package/lib/contract/index.mjs +5 -0
- package/lib/dialog.d.mts +156 -0
- package/lib/dialog.mjs +3 -0
- package/lib/index-DqnaSXqP.d.mts +823 -0
- package/lib/index.d.mts +17 -0
- package/lib/index.mjs +11404 -0
- package/lib/prod-react-Dzof4qJx.mjs +4 -0
- package/lib/surface-DdxzIgIY.mjs +3126 -0
- package/lib/vendor.d.mts +3 -0
- package/lib/vendor.mjs +4 -0
- package/package.json +128 -0
- package/src/apply-theme.ts +8 -0
- package/src/boot-log.ts +35 -0
- package/src/chat/blocks.ts +41 -0
- package/src/chat/bridge.ts +1076 -0
- package/src/chat/builtin-tool-views.ts +53 -0
- package/src/chat/chat-face.ts +16 -0
- package/src/chat/chat-nodes.ts +53 -0
- package/src/chat/efforts.ts +4 -0
- package/src/chat/interactions.ts +329 -0
- package/src/chat/models.ts +263 -0
- package/src/chat/partial-json.ts +125 -0
- package/src/chat/presets.ts +26 -0
- package/src/chat/question-view.ts +106 -0
- package/src/chat/retry-status.ts +33 -0
- package/src/chat/session-groups.ts +34 -0
- package/src/chat/session-list.ts +311 -0
- package/src/chat/store.ts +636 -0
- package/src/chat/todo-view.ts +66 -0
- package/src/chat/tool-view.ts +311 -0
- package/src/chat/tool-views.ts +114 -0
- package/src/contract/index.ts +704 -0
- package/src/contract/upstream.ts +165 -0
- package/src/core/caret-nonce.ts +19 -0
- package/src/core/composer-layout.ts +65 -0
- package/src/core/edit.ts +152 -0
- package/src/core/field-view.ts +52 -0
- package/src/core/fields.ts +185 -0
- package/src/core/metrics.ts +26 -0
- package/src/core/paste.ts +149 -0
- package/src/core/segments.ts +112 -0
- package/src/core/text.ts +290 -0
- package/src/dialog.ts +11 -0
- package/src/env.ts +38 -0
- package/src/harness-home.ts +22 -0
- package/src/hot-theme.ts +56 -0
- package/src/index.tsx +185 -0
- package/src/kernel/registry.ts +102 -0
- package/src/kernel/surface.ts +33 -0
- package/src/log.ts +249 -0
- package/src/model/message.ts +89 -0
- package/src/model/selection.ts +63 -0
- package/src/performance-guard.ts +28 -0
- package/src/runtime/prod-react.ts +10 -0
- package/src/terminal/background.ts +66 -0
- package/src/terminal/capabilities.ts +54 -0
- package/src/terminal/clipboard-backends.ts +69 -0
- package/src/terminal/clipboard.ts +161 -0
- package/src/terminal/cursor-shape.ts +13 -0
- package/src/terminal/glyphs.ts +44 -0
- package/src/terminal/mouse.ts +154 -0
- package/src/terminal/probe.ts +61 -0
- package/src/terminal/screen.ts +349 -0
- package/src/theme-settings.ts +31 -0
- package/src/theme.ts +319 -0
- package/src/ui/app.tsx +454 -0
- package/src/ui/chrome/caps.tsx +9 -0
- package/src/ui/chrome/hint-service.ts +87 -0
- package/src/ui/chrome/input-status.ts +27 -0
- package/src/ui/chrome/key-gate.tsx +29 -0
- package/src/ui/chrome/status-bar.tsx +147 -0
- package/src/ui/contributions.ts +47 -0
- package/src/ui/dialog/defaults-dialog.tsx +152 -0
- package/src/ui/dialog/dialog-item.tsx +38 -0
- package/src/ui/dialog/dialog.tsx +386 -0
- package/src/ui/dialog/effort-dialog.tsx +45 -0
- package/src/ui/dialog/geometry.ts +131 -0
- package/src/ui/dialog/items.ts +124 -0
- package/src/ui/dialog/list-dialog.tsx +63 -0
- package/src/ui/dialog/models-dialog.tsx +203 -0
- package/src/ui/dialog/presets-dialog.tsx +46 -0
- package/src/ui/dialog/providers-dialog.tsx +313 -0
- package/src/ui/dialog/sessions-dialog.tsx +163 -0
- package/src/ui/dialog/sizes.ts +7 -0
- package/src/ui/dialog/status-lines.ts +11 -0
- package/src/ui/dialog/todo-dialog.tsx +29 -0
- package/src/ui/dialog/use-dialog-input.ts +186 -0
- package/src/ui/extension-point.ts +188 -0
- package/src/ui/home-logo.ts +132 -0
- package/src/ui/hooks/use-async-action.ts +25 -0
- package/src/ui/hooks/use-async-list.ts +71 -0
- package/src/ui/hooks/use-caret.ts +24 -0
- package/src/ui/hooks/use-chat-events.ts +153 -0
- package/src/ui/hooks/use-mouse-selection.ts +352 -0
- package/src/ui/hooks/use-scroll.ts +49 -0
- package/src/ui/hooks/use-terminal-size.ts +20 -0
- package/src/ui/input/commands.ts +118 -0
- package/src/ui/input/composer-bus.ts +14 -0
- package/src/ui/input/composer-fields.ts +111 -0
- package/src/ui/input/composer-keys.ts +21 -0
- package/src/ui/input/composer-paste.ts +20 -0
- package/src/ui/input/input-bar.tsx +279 -0
- package/src/ui/input/use-composer.ts +495 -0
- package/src/ui/key-arbiter.ts +37 -0
- package/src/ui/keymap.ts +15 -0
- package/src/ui/layout-service.ts +77 -0
- package/src/ui/message/layout.ts +763 -0
- package/src/ui/message/md/block.ts +247 -0
- package/src/ui/message/md/engine.ts +430 -0
- package/src/ui/message/md/extensions.ts +58 -0
- package/src/ui/message/md/highlight.ts +626 -0
- package/src/ui/message/md/index.ts +14 -0
- package/src/ui/message/md/inline.ts +97 -0
- package/src/ui/message/md/palette.ts +87 -0
- package/src/ui/message/message-list.tsx +69 -0
- package/src/ui/message/message-row.tsx +137 -0
- package/src/ui/message/message-views.ts +16 -0
- package/src/ui/message/renderers.ts +39 -0
- package/src/ui/message/tool-diff.ts +188 -0
- package/src/ui/message/warmup.ts +41 -0
- package/src/ui/message/wave.ts +98 -0
- package/src/ui/overlay.ts +22 -0
- package/src/ui/panels/approval-panel.tsx +193 -0
- package/src/ui/panels/exports.ts +3 -0
- package/src/ui/panels/question-model.ts +424 -0
- package/src/ui/panels/question-panel.tsx +154 -0
- package/src/ui/panels/surface.tsx +106 -0
- package/src/ui/panels-builtin.tsx +86 -0
- package/src/ui/pointer/builtins.ts +281 -0
- package/src/ui/pointer/registry.ts +25 -0
- package/src/ui/region.tsx +31 -0
- package/src/ui/selection/service.ts +92 -0
- package/src/ui/selection-registry.ts +88 -0
- package/src/ui/selection.tsx +148 -0
- package/src/ui/spinner-tick.tsx +24 -0
- package/src/ui/use-available-windows.ts +31 -0
- package/src/ui/widgets/actions.tsx +84 -0
- package/src/ui/widgets/button.tsx +57 -0
- package/src/ui/widgets/checkbox.tsx +58 -0
- package/src/ui/widgets/header.tsx +36 -0
- package/src/ui/widgets/index.ts +8 -0
- package/src/ui/widgets/input.tsx +76 -0
- package/src/ui/widgets/registry.ts +32 -0
- package/src/ui/widgets/search.tsx +59 -0
- package/src/ui/widgets/select.tsx +97 -0
- package/src/ui/widgets/static.tsx +37 -0
- package/src/ui/widgets/types.ts +1 -0
- package/src/ui/window-services-bridge.ts +71 -0
- package/src/ui/window-services.ts +20 -0
- package/src/ui/windows/models-window.tsx +15 -0
- package/src/ui/windows/providers-window.tsx +14 -0
- package/src/ui/windows/service-window.tsx +18 -0
- package/src/ui/windows/sessions-window.tsx +25 -0
- package/src/ui/windows-builtin.tsx +94 -0
- package/src/ui/windows.ts +18 -0
- package/src/vendor.ts +12 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ComponentType } from 'react'
|
|
2
|
+
import { ApprovalPanel } from './panels/approval-panel.tsx'
|
|
3
|
+
import type { ApprovalPanelProps } from './panels/approval-panel.tsx'
|
|
4
|
+
import { QuestionPanel } from './panels/question-panel.tsx'
|
|
5
|
+
import type { QuestionPanelProps } from './panels/question-panel.tsx'
|
|
6
|
+
import type { InteractionPanelComponentProps, InteractionPanelContribution } from '../chat/interactions.ts'
|
|
7
|
+
import type { ChatBridge } from '../chat/bridge.ts'
|
|
8
|
+
|
|
9
|
+
export interface PanelServices {
|
|
10
|
+
bridge: ChatBridge
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function adapt<P extends object>(Component: ComponentType<P>, map: (props: InteractionPanelComponentProps) => Omit<P, 'handleRef'> | null): ComponentType<InteractionPanelComponentProps> {
|
|
14
|
+
const Wrapped = (props: InteractionPanelComponentProps) => {
|
|
15
|
+
const adapted = map(props)
|
|
16
|
+
if (adapted === null) return null
|
|
17
|
+
const Specific = Component as ComponentType<Record<string, unknown>>
|
|
18
|
+
return <Specific {...(adapted as Record<string, unknown>)} handleRef={props.handleRef} />
|
|
19
|
+
}
|
|
20
|
+
return Wrapped
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Builtin panels register as high-order fallback layers so a plugin
|
|
25
|
+
* contribution for the same kind always wins regardless of timing.
|
|
26
|
+
*/
|
|
27
|
+
export const BUILTIN_PANEL_ORDER = 500
|
|
28
|
+
|
|
29
|
+
export function registerBuiltinPanels(services: PanelServices): () => void {
|
|
30
|
+
const { bridge } = services
|
|
31
|
+
const contributions: InteractionPanelContribution[] = [
|
|
32
|
+
{
|
|
33
|
+
kind: 'approval',
|
|
34
|
+
order: BUILTIN_PANEL_ORDER,
|
|
35
|
+
component: adapt<ApprovalPanelProps>(ApprovalPanel, props => {
|
|
36
|
+
const approval = (props.request as { approval?: { reason?: string; callId?: string } }).approval
|
|
37
|
+
if (approval === undefined) return null
|
|
38
|
+
return {
|
|
39
|
+
reason: approval.reason,
|
|
40
|
+
command: approval.callId === undefined ? undefined : commandForCall(bridge, approval.callId),
|
|
41
|
+
background: props.background,
|
|
42
|
+
active: props.active,
|
|
43
|
+
columns: props.columns,
|
|
44
|
+
rows: props.rows,
|
|
45
|
+
innerWidth: props.innerWidth,
|
|
46
|
+
blockWidth: props.blockWidth,
|
|
47
|
+
onDecide: (outcome: 'allowed-once' | 'rejected') => props.resolve(outcome),
|
|
48
|
+
onResize: props.onResize,
|
|
49
|
+
} }),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
kind: 'question',
|
|
53
|
+
order: BUILTIN_PANEL_ORDER + 10,
|
|
54
|
+
component: adapt<QuestionPanelProps>(QuestionPanel, props => {
|
|
55
|
+
const question = (props.request as { question?: QuestionPanelProps['question'] }).question
|
|
56
|
+
if (question === undefined) return null
|
|
57
|
+
return {
|
|
58
|
+
question,
|
|
59
|
+
background: props.background,
|
|
60
|
+
active: props.active,
|
|
61
|
+
columns: props.columns,
|
|
62
|
+
rows: props.rows,
|
|
63
|
+
innerWidth: props.innerWidth,
|
|
64
|
+
blockWidth: props.blockWidth,
|
|
65
|
+
onSubmit: (answer: Parameters<QuestionPanelProps['onSubmit']>[0]) => props.resolve(answer),
|
|
66
|
+
onCancel: () => props.reject(new Error('the user closed the question panel')),
|
|
67
|
+
onResize: props.onResize,
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
const disposers = contributions.map(contribution => services.bridge.interactions.panels.register(contribution))
|
|
73
|
+
return () => {
|
|
74
|
+
for (const dispose of disposers) dispose()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function commandForCall(bridge: ChatBridge, callId: string): string | undefined {
|
|
79
|
+
try {
|
|
80
|
+
const args = JSON.parse(bridge.toolPresenter.argsJson(callId) ?? 'null') as { command?: unknown } | null
|
|
81
|
+
return typeof args?.command === 'string' && args.command !== '' ? args.command : undefined
|
|
82
|
+
} catch {
|
|
83
|
+
// Unparsable or missing args simply carry no command preview.
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type { LineSelection, PointerEventFrame } from '../../contract/index.ts'
|
|
2
|
+
import { panelContains } from '../layout-service.ts'
|
|
3
|
+
import { POINTER_BUILTIN_ORDER } from './registry.ts'
|
|
4
|
+
|
|
5
|
+
const WHEEL_LINES = 3
|
|
6
|
+
|
|
7
|
+
export interface PointerBuiltinDeps {
|
|
8
|
+
inInputContent(y: number): boolean
|
|
9
|
+
inMessageArea(screenRow: number): boolean
|
|
10
|
+
toContentRow(screenRow: number): number
|
|
11
|
+
activeHintRegion(): { top: number; bottom: number } | null
|
|
12
|
+
focusRowFor(current: LineSelection, eventY: number): number
|
|
13
|
+
clampMessageFocus(anchorInMessage: boolean, eventY: number): number
|
|
14
|
+
setPointerArea(inMessageArea: boolean): void
|
|
15
|
+
onScrollbarDown(x: number, y: number): boolean
|
|
16
|
+
onScrollbarDrag(y: number): void
|
|
17
|
+
stopDragScroll(): void
|
|
18
|
+
updateDragScroll(eventY: number): void
|
|
19
|
+
setSelection(next: LineSelection | null | ((current: LineSelection | null) => LineSelection | null)): void
|
|
20
|
+
getSelection(): LineSelection | null
|
|
21
|
+
rowHasText(y: number): boolean
|
|
22
|
+
rowInfoAt(y: number): { messageId: string; clickable: boolean; selectable: boolean } | null | undefined
|
|
23
|
+
inputClickAt(y: number, x: number): void
|
|
24
|
+
panelClickAt(y: number, x: number): void
|
|
25
|
+
dialogWheel(y: number, dir: -1 | 1): boolean
|
|
26
|
+
panelWheel(dir: -1 | 1): boolean
|
|
27
|
+
hintWheel(dir: -1 | 1): void
|
|
28
|
+
inputWheel(dir: -1 | 1): void
|
|
29
|
+
hintPress(x: number, y: number): void
|
|
30
|
+
hintDragStart(x: number, y: number): void
|
|
31
|
+
hintDragMove(x: number, y: number): void
|
|
32
|
+
hintRelease(x: number, y: number, dragged: boolean): void
|
|
33
|
+
onScroll(next: number): void
|
|
34
|
+
toggleMessage(id: string): void
|
|
35
|
+
dialogClick(y: number, x: number): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The six builtin gesture handlers (scrollbar, hint, dialog, panel, input,
|
|
40
|
+
* message area) collapsed into one contribution. Each owns its candidate
|
|
41
|
+
* state; the dispatcher guarantees beginDown() runs on every press so stale
|
|
42
|
+
* candidates never survive into the next gesture.
|
|
43
|
+
*/
|
|
44
|
+
export function createBuiltinPointerHandler(deps: PointerBuiltinDeps) {
|
|
45
|
+
const scrollbarSession: { current: { grabOffset: number } | null } = { current: null }
|
|
46
|
+
const hintGesture: { current: { x: number; y: number; dragged: boolean } | null } = { current: null }
|
|
47
|
+
const clickCandidate: { current: { messageId: string; y: number; x: number; moved: boolean } | null } = { current: null }
|
|
48
|
+
const dialogCandidate: { current: { x: number; y: number } | null } = { current: null }
|
|
49
|
+
const panelCandidate: { current: { x: number; y: number } | null } = { current: null }
|
|
50
|
+
const inputCandidate: { current: { x: number; y: number } | null } = { current: null }
|
|
51
|
+
// Which sub-handler owns the active gesture; '' means none.
|
|
52
|
+
let owner = ''
|
|
53
|
+
|
|
54
|
+
function clearCandidates(): void {
|
|
55
|
+
clickCandidate.current = null
|
|
56
|
+
dialogCandidate.current = null
|
|
57
|
+
panelCandidate.current = null
|
|
58
|
+
inputCandidate.current = null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extendOrDrag(candidate: { current: { x: number; y: number } | null }, event: PointerEventFrame): void {
|
|
62
|
+
const anchor = candidate.current
|
|
63
|
+
if (anchor !== null) {
|
|
64
|
+
candidate.current = null
|
|
65
|
+
deps.setSelection({
|
|
66
|
+
anchorRow: anchor.y,
|
|
67
|
+
anchorCol: anchor.x,
|
|
68
|
+
focusRow: event.event.y,
|
|
69
|
+
focusCol: event.event.x,
|
|
70
|
+
inMessage: false,
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
deps.setSelection(current => current === null || current.inMessage ? current : {
|
|
75
|
+
...current,
|
|
76
|
+
focusRow: event.event.y,
|
|
77
|
+
focusCol: event.event.x,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
order: POINTER_BUILTIN_ORDER,
|
|
83
|
+
id: 'builtin.pointer',
|
|
84
|
+
beginDown(): void {
|
|
85
|
+
clearCandidates()
|
|
86
|
+
scrollbarSession.current = null
|
|
87
|
+
hintGesture.current = null
|
|
88
|
+
owner = ''
|
|
89
|
+
},
|
|
90
|
+
onDown(event: PointerEventFrame): boolean {
|
|
91
|
+
const { x, y } = event.event
|
|
92
|
+
if (deps.onScrollbarDown(x, y)) {
|
|
93
|
+
owner = 'scrollbar'
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
const hintRegionActive = (() => {
|
|
97
|
+
if (event.ui.dialogOpen || event.ui.panelActive) return false
|
|
98
|
+
const region = deps.activeHintRegion()
|
|
99
|
+
return region !== null && y >= region.top && y <= region.bottom
|
|
100
|
+
})()
|
|
101
|
+
if (hintRegionActive) {
|
|
102
|
+
hintGesture.current = { x, y, dragged: false }
|
|
103
|
+
deps.hintPress(x, y)
|
|
104
|
+
clearCandidates()
|
|
105
|
+
deps.stopDragScroll()
|
|
106
|
+
deps.setPointerArea(false)
|
|
107
|
+
deps.setSelection(null)
|
|
108
|
+
owner = 'hint'
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
if (event.ui.dialogOpen) {
|
|
112
|
+
const anchorable = deps.rowHasText(y)
|
|
113
|
+
dialogCandidate.current = { x, y }
|
|
114
|
+
if (anchorable) deps.setSelection({ anchorRow: y, anchorCol: x, focusRow: y, focusCol: x, inMessage: false })
|
|
115
|
+
owner = 'dialog'
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
if (event.ui.panelActive && panelContains(y, { columns: event.ui.columns, rows: event.ui.rows, inputHeight: event.ui.inputHeight, messageHeight: event.ui.messageHeight, panelHeight: null })) {
|
|
119
|
+
panelCandidate.current = { x, y }
|
|
120
|
+
if (deps.rowHasText(y)) {
|
|
121
|
+
deps.setSelection({ anchorRow: y, anchorCol: x, focusRow: y, focusCol: x, inMessage: false })
|
|
122
|
+
}
|
|
123
|
+
owner = 'panel'
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
if (!event.ui.dialogOpen && !event.ui.panelActive && deps.inInputContent(y)) {
|
|
127
|
+
inputCandidate.current = { x, y }
|
|
128
|
+
deps.setSelection(null)
|
|
129
|
+
owner = 'input'
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
const contentRow = deps.toContentRow(y)
|
|
133
|
+
const hit = deps.rowInfoAt(y)
|
|
134
|
+
if (hit?.clickable) {
|
|
135
|
+
clickCandidate.current = { messageId: hit.messageId, y, x, moved: false }
|
|
136
|
+
owner = 'message'
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
// Message rows anchor on the row model: artwork made of block glyphs
|
|
140
|
+
// never passes the screen-level text check. Chrome rows keep it.
|
|
141
|
+
const anchorInMessage = deps.inMessageArea(y)
|
|
142
|
+
if (anchorInMessage ? hit?.selectable === true : deps.rowHasText(y)) {
|
|
143
|
+
deps.setPointerArea(anchorInMessage)
|
|
144
|
+
deps.setSelection({ anchorRow: contentRow, anchorCol: x, focusRow: contentRow, focusCol: x, inMessage: anchorInMessage })
|
|
145
|
+
owner = 'message'
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
// Nothing claimed; the dispatcher keeps default drag behavior alive.
|
|
149
|
+
return false
|
|
150
|
+
},
|
|
151
|
+
onDrag(event: PointerEventFrame): void {
|
|
152
|
+
const { x, y } = event.event
|
|
153
|
+
switch (owner) {
|
|
154
|
+
case 'scrollbar':
|
|
155
|
+
deps.onScrollbarDrag(y)
|
|
156
|
+
return
|
|
157
|
+
case 'hint': {
|
|
158
|
+
const gesture = hintGesture.current
|
|
159
|
+
if (gesture === null) return
|
|
160
|
+
if (!gesture.dragged) {
|
|
161
|
+
gesture.dragged = true
|
|
162
|
+
deps.hintDragStart(x, y)
|
|
163
|
+
} else {
|
|
164
|
+
deps.hintDragMove(x, y)
|
|
165
|
+
}
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
case 'input':
|
|
169
|
+
extendOrDrag(inputCandidate, event)
|
|
170
|
+
return
|
|
171
|
+
case 'panel':
|
|
172
|
+
extendOrDrag(panelCandidate, event)
|
|
173
|
+
return
|
|
174
|
+
case 'dialog':
|
|
175
|
+
extendOrDrag(dialogCandidate, event)
|
|
176
|
+
return
|
|
177
|
+
case 'message': {
|
|
178
|
+
const candidate = clickCandidate.current
|
|
179
|
+
if (candidate !== null) {
|
|
180
|
+
candidate.moved = true
|
|
181
|
+
clickCandidate.current = null
|
|
182
|
+
const anchorInMessage = deps.inMessageArea(candidate.y)
|
|
183
|
+
deps.setPointerArea(anchorInMessage)
|
|
184
|
+
deps.setSelection({
|
|
185
|
+
anchorRow: deps.toContentRow(candidate.y),
|
|
186
|
+
anchorCol: candidate.x,
|
|
187
|
+
focusRow: deps.clampMessageFocus(anchorInMessage, y),
|
|
188
|
+
focusCol: x,
|
|
189
|
+
inMessage: anchorInMessage,
|
|
190
|
+
})
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
deps.setSelection(current => current === null ? current : {
|
|
194
|
+
...current,
|
|
195
|
+
focusRow: deps.focusRowFor(current, y),
|
|
196
|
+
focusCol: x,
|
|
197
|
+
})
|
|
198
|
+
deps.updateDragScroll(y)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
default:
|
|
202
|
+
deps.setSelection(current => current === null ? current : {
|
|
203
|
+
...current,
|
|
204
|
+
focusRow: deps.focusRowFor(current, y),
|
|
205
|
+
focusCol: x,
|
|
206
|
+
})
|
|
207
|
+
deps.updateDragScroll(y)
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
onUp(event: PointerEventFrame): void {
|
|
211
|
+
const { x, y } = event.event
|
|
212
|
+
switch (owner) {
|
|
213
|
+
case 'scrollbar':
|
|
214
|
+
scrollbarSession.current = null
|
|
215
|
+
return
|
|
216
|
+
case 'hint': {
|
|
217
|
+
const gesture = hintGesture.current
|
|
218
|
+
hintGesture.current = null
|
|
219
|
+
if (gesture !== null) deps.hintRelease(x, y, gesture.dragged)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
case 'input': {
|
|
223
|
+
const candidate = inputCandidate.current
|
|
224
|
+
inputCandidate.current = null
|
|
225
|
+
if (candidate !== null) deps.inputClickAt(candidate.y, candidate.x)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
case 'panel': {
|
|
229
|
+
const candidate = panelCandidate.current
|
|
230
|
+
panelCandidate.current = null
|
|
231
|
+
if (candidate !== null) {
|
|
232
|
+
deps.panelClickAt(candidate.y, candidate.x)
|
|
233
|
+
deps.setSelection(null)
|
|
234
|
+
}
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
case 'dialog': {
|
|
238
|
+
const candidate = dialogCandidate.current
|
|
239
|
+
dialogCandidate.current = null
|
|
240
|
+
if (candidate !== null) {
|
|
241
|
+
deps.dialogClick(candidate.y, candidate.x)
|
|
242
|
+
deps.setSelection(null)
|
|
243
|
+
}
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
case 'message': {
|
|
247
|
+
const candidate = clickCandidate.current
|
|
248
|
+
clickCandidate.current = null
|
|
249
|
+
if (candidate !== null && !candidate.moved) deps.toggleMessage(candidate.messageId)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
default:
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
onWheel(event: PointerEventFrame): boolean {
|
|
257
|
+
const { y } = event.event
|
|
258
|
+
const dir: -1 | 1 = event.event.scrollDirection === 'up' ? -1 : 1
|
|
259
|
+
if (event.ui.dialogOpen) return deps.dialogWheel(y, dir)
|
|
260
|
+
if (event.ui.panelActive && panelContains(y, { columns: event.ui.columns, rows: event.ui.rows, inputHeight: event.ui.inputHeight, messageHeight: event.ui.messageHeight, panelHeight: null })) return deps.panelWheel(dir)
|
|
261
|
+
if (!event.ui.dialogOpen && !event.ui.panelActive) {
|
|
262
|
+
const region = deps.activeHintRegion()
|
|
263
|
+
if (region !== null && y >= region.top && y <= region.bottom) {
|
|
264
|
+
deps.hintWheel(dir)
|
|
265
|
+
return true
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!event.ui.dialogOpen && !event.ui.panelActive && deps.inInputContent(y)) {
|
|
269
|
+
deps.inputWheel(dir)
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
const delta = dir === -1 ? -WHEEL_LINES : WHEEL_LINES
|
|
273
|
+
const live = event.ui.getScroll()
|
|
274
|
+
const next = Math.max(0, Math.min(live.maxScroll, live.top + delta))
|
|
275
|
+
if (next !== live.top) deps.onScroll(next)
|
|
276
|
+
return true
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export type BuiltinPointerHandler = ReturnType<typeof createBuiltinPointerHandler>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { keyedRegistry } from '../../kernel/registry.ts'
|
|
2
|
+
import type { PointerHandlerContribution } from '../../contract/index.ts'
|
|
3
|
+
|
|
4
|
+
export type { PointerUiContext, PointerSession, PointerEventFrame, PointerHandlerContribution } from '../../contract/index.ts'
|
|
5
|
+
|
|
6
|
+
const pointerHandlers = keyedRegistry<PointerHandlerContribution>()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plugins observe pointer events the builtin gesture handler does not claim,
|
|
10
|
+
* or preempt the builtins entirely. Default order 300 places a plugin behind
|
|
11
|
+
* every builtin handler (which live at 100..190); order < 100 runs before the
|
|
12
|
+
* builtins and may claim events first.
|
|
13
|
+
*/
|
|
14
|
+
export const POINTER_PLUGIN_ORDER = 300
|
|
15
|
+
|
|
16
|
+
export function registerPointerHandler(contribution: PointerHandlerContribution): () => void {
|
|
17
|
+
return pointerHandlers.register(contribution.id, contribution, { order: contribution.order ?? POINTER_PLUGIN_ORDER })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function pointerHandlerEntries(): Array<{ key: string; order: number; value: PointerHandlerContribution }> {
|
|
21
|
+
return pointerHandlers.entries()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Builtin layer order constant; see pointer/builtins.ts. */
|
|
25
|
+
export const POINTER_BUILTIN_ORDER = 100
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface Origin {
|
|
5
|
+
x: number
|
|
6
|
+
y: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ZERO_ORIGIN: Origin = { x: 0, y: 0 }
|
|
10
|
+
|
|
11
|
+
export const RegionContext = createContext<Origin>(ZERO_ORIGIN)
|
|
12
|
+
|
|
13
|
+
export function useOrigin(): Origin {
|
|
14
|
+
return useContext(RegionContext)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function translate(parent: Origin, x: number, y: number): Origin {
|
|
18
|
+
return { x: parent.x + x, y: parent.y + y }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RegionProps {
|
|
22
|
+
x?: number
|
|
23
|
+
y?: number
|
|
24
|
+
children: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Region({ x = 0, y = 0, children }: RegionProps) {
|
|
28
|
+
const parent = useOrigin()
|
|
29
|
+
const value = useMemo(() => translate(parent, x, y), [parent.x, parent.y, x, y])
|
|
30
|
+
return <RegionContext.Provider value={value}>{children}</RegionContext.Provider>
|
|
31
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { keyedRegistry } from '../../kernel/registry.ts'
|
|
2
|
+
import { writeClipboardText } from '../../terminal/clipboard.ts'
|
|
3
|
+
import type {
|
|
4
|
+
LineSelection,
|
|
5
|
+
SelectionClipboardContribution,
|
|
6
|
+
SelectionDomainContribution,
|
|
7
|
+
SelectionExtractContext,
|
|
8
|
+
SelectionTransformerContribution,
|
|
9
|
+
} from '../../contract/index.ts'
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
SelectionClipboardContribution,
|
|
13
|
+
SelectionDomainContribution,
|
|
14
|
+
SelectionExtractContext,
|
|
15
|
+
SelectionTransformerContribution,
|
|
16
|
+
} from '../../contract/index.ts'
|
|
17
|
+
|
|
18
|
+
const domains = keyedRegistry<SelectionDomainContribution>()
|
|
19
|
+
const transformers = keyedRegistry<SelectionTransformerContribution>()
|
|
20
|
+
const clipboardProviders = keyedRegistry<SelectionClipboardContribution>()
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builtin domains sit behind plugin contributions (default order 100): a
|
|
24
|
+
* plugin domain may claim selections first, and a plugin that unmounts
|
|
25
|
+
* hands extraction back to the shell.
|
|
26
|
+
*/
|
|
27
|
+
const BUILTIN_DOMAIN_ORDER = 500
|
|
28
|
+
|
|
29
|
+
let installed = false
|
|
30
|
+
|
|
31
|
+
function installBuiltins(): void {
|
|
32
|
+
if (installed) return
|
|
33
|
+
installed = true
|
|
34
|
+
const messageDomain: SelectionDomainContribution = {
|
|
35
|
+
id: 'builtin.message',
|
|
36
|
+
hit: sel => sel.inMessage,
|
|
37
|
+
extract: (sel, ctx) => ctx.messageText(sel),
|
|
38
|
+
}
|
|
39
|
+
const chromeDomain: SelectionDomainContribution = {
|
|
40
|
+
id: 'builtin.chrome',
|
|
41
|
+
hit: () => true,
|
|
42
|
+
extract: (sel, ctx) => ctx.chromeText(sel),
|
|
43
|
+
}
|
|
44
|
+
const clipboardProvider: SelectionClipboardContribution = {
|
|
45
|
+
id: 'builtin.clipboard',
|
|
46
|
+
copy: text => {
|
|
47
|
+
writeClipboardText(text)
|
|
48
|
+
return true
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
domains.register(messageDomain.id, messageDomain, { order: BUILTIN_DOMAIN_ORDER })
|
|
52
|
+
domains.register(chromeDomain.id, chromeDomain, { order: BUILTIN_DOMAIN_ORDER + 1 })
|
|
53
|
+
clipboardProviders.register(clipboardProvider.id, clipboardProvider, { order: BUILTIN_DOMAIN_ORDER })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registerSelectionDomain(contribution: SelectionDomainContribution): () => void {
|
|
57
|
+
installBuiltins()
|
|
58
|
+
return domains.register(contribution.id, contribution, { order: contribution.order ?? 100 })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function registerSelectionTransformer(contribution: SelectionTransformerContribution): () => void {
|
|
62
|
+
installBuiltins()
|
|
63
|
+
return transformers.register(contribution.id, contribution, { order: contribution.order ?? 100 })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerSelectionClipboard(contribution: SelectionClipboardContribution): () => void {
|
|
67
|
+
installBuiltins()
|
|
68
|
+
return clipboardProviders.register(contribution.id, contribution, { order: contribution.order ?? 100 })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Copy pipeline: domain extract -> transformer chain -> clipboard provider
|
|
73
|
+
* chain. Returns the text that was extracted, or null when no domain
|
|
74
|
+
* claimed the selection.
|
|
75
|
+
*/
|
|
76
|
+
export function copySelection(sel: LineSelection, ctx: SelectionExtractContext): string | null {
|
|
77
|
+
installBuiltins()
|
|
78
|
+
let text: string | null = null
|
|
79
|
+
for (const domain of domains.values()) {
|
|
80
|
+
if (!domain.hit(sel)) continue
|
|
81
|
+
text = domain.extract(sel, ctx)
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
if (text === null) return null
|
|
85
|
+
for (const transformer of transformers.values()) {
|
|
86
|
+
text = transformer.transform(text)
|
|
87
|
+
}
|
|
88
|
+
for (const provider of clipboardProviders.values()) {
|
|
89
|
+
if (provider.copy(text)) break
|
|
90
|
+
}
|
|
91
|
+
return text
|
|
92
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { selectedRange } from '../model/selection.ts'
|
|
2
|
+
import type { LineSelection } from '../model/selection.ts'
|
|
3
|
+
import { colToCharIndex, textWidth } from '../core/text.ts'
|
|
4
|
+
|
|
5
|
+
export interface RowPiece {
|
|
6
|
+
col: number
|
|
7
|
+
text: string
|
|
8
|
+
layer?: 'chrome' | 'message'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const rows = new Map<number, Map<object, RowPiece>>()
|
|
12
|
+
|
|
13
|
+
export function registerRowPiece(id: object, y: number, piece: RowPiece | null): void {
|
|
14
|
+
let pieces = rows.get(y)
|
|
15
|
+
if (piece === null) {
|
|
16
|
+
if (pieces !== undefined) {
|
|
17
|
+
pieces.delete(id)
|
|
18
|
+
if (pieces.size === 0) rows.delete(y)
|
|
19
|
+
}
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
if (pieces === undefined) {
|
|
23
|
+
pieces = new Map()
|
|
24
|
+
rows.set(y, pieces)
|
|
25
|
+
}
|
|
26
|
+
pieces.set(id, piece)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function rowPieces(y: number): RowPiece[] {
|
|
30
|
+
const pieces = rows.get(y)
|
|
31
|
+
if (pieces === undefined) return []
|
|
32
|
+
return [...pieces.values()].sort((a, b) => a.col - b.col)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function visiblePieces(y: number): RowPiece[] {
|
|
36
|
+
const pieces = rowPieces(y)
|
|
37
|
+
const chrome = pieces.filter(piece => piece.layer !== 'message')
|
|
38
|
+
return chrome.length > 0 ? chrome : pieces
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clearRowPieces(): void {
|
|
42
|
+
rows.clear()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function envelopeOf(selection: LineSelection): { start: number; end: number } {
|
|
46
|
+
return {
|
|
47
|
+
start: Math.min(selection.anchorCol, selection.focusCol),
|
|
48
|
+
end: Math.max(selection.anchorCol, selection.focusCol),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function envelopeOverlaps(selection: LineSelection, col: number, width: number): boolean {
|
|
53
|
+
const env = envelopeOf(selection)
|
|
54
|
+
return env.end > col && env.start < col + width
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sliceByColumns(text: string, startCol: number, endCol: number): string {
|
|
58
|
+
const startIndex = colToCharIndex(text, Math.max(0, startCol))
|
|
59
|
+
const endIndex = colToCharIndex(text, Math.max(0, endCol))
|
|
60
|
+
return text.slice(startIndex, endIndex)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function chromeSelectionText(selection: LineSelection): string {
|
|
64
|
+
const top = Math.min(selection.anchorRow, selection.focusRow)
|
|
65
|
+
const bottom = Math.max(selection.anchorRow, selection.focusRow)
|
|
66
|
+
const env = envelopeOf(selection)
|
|
67
|
+
const lines: string[] = []
|
|
68
|
+
for (let row = top; row <= bottom; row++) {
|
|
69
|
+
const range = selectedRange(selection, row)
|
|
70
|
+
if (range === null) continue
|
|
71
|
+
let line = ''
|
|
72
|
+
let prevEnd = -1
|
|
73
|
+
for (const piece of visiblePieces(row)) {
|
|
74
|
+
const width = textWidth(piece.text)
|
|
75
|
+
if (!(env.end > piece.col && env.start < piece.col + width)) continue
|
|
76
|
+
const start = Math.max(range.start, piece.col)
|
|
77
|
+
const end = Math.min(range.end, piece.col + width)
|
|
78
|
+
if (start >= end) continue
|
|
79
|
+
const text = sliceByColumns(piece.text, start - piece.col, end - piece.col)
|
|
80
|
+
if (text === '') continue
|
|
81
|
+
if (line !== '') line += prevEnd === piece.col ? '' : ' '
|
|
82
|
+
line += text
|
|
83
|
+
prevEnd = piece.col + width
|
|
84
|
+
}
|
|
85
|
+
lines.push(line)
|
|
86
|
+
}
|
|
87
|
+
return lines.join('\n').replace(/[ \t]+$/gm, '').replace(/\n+$/, '')
|
|
88
|
+
}
|