@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.
Files changed (159) hide show
  1. package/bin/dshtui.js +65 -0
  2. package/cordis.patch.yml +81 -0
  3. package/lib/contract/index.d.mts +2 -0
  4. package/lib/contract/index.mjs +5 -0
  5. package/lib/dialog.d.mts +156 -0
  6. package/lib/dialog.mjs +3 -0
  7. package/lib/index-DqnaSXqP.d.mts +823 -0
  8. package/lib/index.d.mts +17 -0
  9. package/lib/index.mjs +11404 -0
  10. package/lib/prod-react-Dzof4qJx.mjs +4 -0
  11. package/lib/surface-DdxzIgIY.mjs +3126 -0
  12. package/lib/vendor.d.mts +3 -0
  13. package/lib/vendor.mjs +4 -0
  14. package/package.json +128 -0
  15. package/src/apply-theme.ts +8 -0
  16. package/src/boot-log.ts +35 -0
  17. package/src/chat/blocks.ts +41 -0
  18. package/src/chat/bridge.ts +1076 -0
  19. package/src/chat/builtin-tool-views.ts +53 -0
  20. package/src/chat/chat-face.ts +16 -0
  21. package/src/chat/chat-nodes.ts +53 -0
  22. package/src/chat/efforts.ts +4 -0
  23. package/src/chat/interactions.ts +329 -0
  24. package/src/chat/models.ts +263 -0
  25. package/src/chat/partial-json.ts +125 -0
  26. package/src/chat/presets.ts +26 -0
  27. package/src/chat/question-view.ts +106 -0
  28. package/src/chat/retry-status.ts +33 -0
  29. package/src/chat/session-groups.ts +34 -0
  30. package/src/chat/session-list.ts +311 -0
  31. package/src/chat/store.ts +636 -0
  32. package/src/chat/todo-view.ts +66 -0
  33. package/src/chat/tool-view.ts +311 -0
  34. package/src/chat/tool-views.ts +114 -0
  35. package/src/contract/index.ts +704 -0
  36. package/src/contract/upstream.ts +165 -0
  37. package/src/core/caret-nonce.ts +19 -0
  38. package/src/core/composer-layout.ts +65 -0
  39. package/src/core/edit.ts +152 -0
  40. package/src/core/field-view.ts +52 -0
  41. package/src/core/fields.ts +185 -0
  42. package/src/core/metrics.ts +26 -0
  43. package/src/core/paste.ts +149 -0
  44. package/src/core/segments.ts +112 -0
  45. package/src/core/text.ts +290 -0
  46. package/src/dialog.ts +11 -0
  47. package/src/env.ts +38 -0
  48. package/src/harness-home.ts +22 -0
  49. package/src/hot-theme.ts +56 -0
  50. package/src/index.tsx +185 -0
  51. package/src/kernel/registry.ts +102 -0
  52. package/src/kernel/surface.ts +33 -0
  53. package/src/log.ts +249 -0
  54. package/src/model/message.ts +89 -0
  55. package/src/model/selection.ts +63 -0
  56. package/src/performance-guard.ts +28 -0
  57. package/src/runtime/prod-react.ts +10 -0
  58. package/src/terminal/background.ts +66 -0
  59. package/src/terminal/capabilities.ts +54 -0
  60. package/src/terminal/clipboard-backends.ts +69 -0
  61. package/src/terminal/clipboard.ts +161 -0
  62. package/src/terminal/cursor-shape.ts +13 -0
  63. package/src/terminal/glyphs.ts +44 -0
  64. package/src/terminal/mouse.ts +154 -0
  65. package/src/terminal/probe.ts +61 -0
  66. package/src/terminal/screen.ts +349 -0
  67. package/src/theme-settings.ts +31 -0
  68. package/src/theme.ts +319 -0
  69. package/src/ui/app.tsx +454 -0
  70. package/src/ui/chrome/caps.tsx +9 -0
  71. package/src/ui/chrome/hint-service.ts +87 -0
  72. package/src/ui/chrome/input-status.ts +27 -0
  73. package/src/ui/chrome/key-gate.tsx +29 -0
  74. package/src/ui/chrome/status-bar.tsx +147 -0
  75. package/src/ui/contributions.ts +47 -0
  76. package/src/ui/dialog/defaults-dialog.tsx +152 -0
  77. package/src/ui/dialog/dialog-item.tsx +38 -0
  78. package/src/ui/dialog/dialog.tsx +386 -0
  79. package/src/ui/dialog/effort-dialog.tsx +45 -0
  80. package/src/ui/dialog/geometry.ts +131 -0
  81. package/src/ui/dialog/items.ts +124 -0
  82. package/src/ui/dialog/list-dialog.tsx +63 -0
  83. package/src/ui/dialog/models-dialog.tsx +203 -0
  84. package/src/ui/dialog/presets-dialog.tsx +46 -0
  85. package/src/ui/dialog/providers-dialog.tsx +313 -0
  86. package/src/ui/dialog/sessions-dialog.tsx +163 -0
  87. package/src/ui/dialog/sizes.ts +7 -0
  88. package/src/ui/dialog/status-lines.ts +11 -0
  89. package/src/ui/dialog/todo-dialog.tsx +29 -0
  90. package/src/ui/dialog/use-dialog-input.ts +186 -0
  91. package/src/ui/extension-point.ts +188 -0
  92. package/src/ui/home-logo.ts +132 -0
  93. package/src/ui/hooks/use-async-action.ts +25 -0
  94. package/src/ui/hooks/use-async-list.ts +71 -0
  95. package/src/ui/hooks/use-caret.ts +24 -0
  96. package/src/ui/hooks/use-chat-events.ts +153 -0
  97. package/src/ui/hooks/use-mouse-selection.ts +352 -0
  98. package/src/ui/hooks/use-scroll.ts +49 -0
  99. package/src/ui/hooks/use-terminal-size.ts +20 -0
  100. package/src/ui/input/commands.ts +118 -0
  101. package/src/ui/input/composer-bus.ts +14 -0
  102. package/src/ui/input/composer-fields.ts +111 -0
  103. package/src/ui/input/composer-keys.ts +21 -0
  104. package/src/ui/input/composer-paste.ts +20 -0
  105. package/src/ui/input/input-bar.tsx +279 -0
  106. package/src/ui/input/use-composer.ts +495 -0
  107. package/src/ui/key-arbiter.ts +37 -0
  108. package/src/ui/keymap.ts +15 -0
  109. package/src/ui/layout-service.ts +77 -0
  110. package/src/ui/message/layout.ts +763 -0
  111. package/src/ui/message/md/block.ts +247 -0
  112. package/src/ui/message/md/engine.ts +430 -0
  113. package/src/ui/message/md/extensions.ts +58 -0
  114. package/src/ui/message/md/highlight.ts +626 -0
  115. package/src/ui/message/md/index.ts +14 -0
  116. package/src/ui/message/md/inline.ts +97 -0
  117. package/src/ui/message/md/palette.ts +87 -0
  118. package/src/ui/message/message-list.tsx +69 -0
  119. package/src/ui/message/message-row.tsx +137 -0
  120. package/src/ui/message/message-views.ts +16 -0
  121. package/src/ui/message/renderers.ts +39 -0
  122. package/src/ui/message/tool-diff.ts +188 -0
  123. package/src/ui/message/warmup.ts +41 -0
  124. package/src/ui/message/wave.ts +98 -0
  125. package/src/ui/overlay.ts +22 -0
  126. package/src/ui/panels/approval-panel.tsx +193 -0
  127. package/src/ui/panels/exports.ts +3 -0
  128. package/src/ui/panels/question-model.ts +424 -0
  129. package/src/ui/panels/question-panel.tsx +154 -0
  130. package/src/ui/panels/surface.tsx +106 -0
  131. package/src/ui/panels-builtin.tsx +86 -0
  132. package/src/ui/pointer/builtins.ts +281 -0
  133. package/src/ui/pointer/registry.ts +25 -0
  134. package/src/ui/region.tsx +31 -0
  135. package/src/ui/selection/service.ts +92 -0
  136. package/src/ui/selection-registry.ts +88 -0
  137. package/src/ui/selection.tsx +148 -0
  138. package/src/ui/spinner-tick.tsx +24 -0
  139. package/src/ui/use-available-windows.ts +31 -0
  140. package/src/ui/widgets/actions.tsx +84 -0
  141. package/src/ui/widgets/button.tsx +57 -0
  142. package/src/ui/widgets/checkbox.tsx +58 -0
  143. package/src/ui/widgets/header.tsx +36 -0
  144. package/src/ui/widgets/index.ts +8 -0
  145. package/src/ui/widgets/input.tsx +76 -0
  146. package/src/ui/widgets/registry.ts +32 -0
  147. package/src/ui/widgets/search.tsx +59 -0
  148. package/src/ui/widgets/select.tsx +97 -0
  149. package/src/ui/widgets/static.tsx +37 -0
  150. package/src/ui/widgets/types.ts +1 -0
  151. package/src/ui/window-services-bridge.ts +71 -0
  152. package/src/ui/window-services.ts +20 -0
  153. package/src/ui/windows/models-window.tsx +15 -0
  154. package/src/ui/windows/providers-window.tsx +14 -0
  155. package/src/ui/windows/service-window.tsx +18 -0
  156. package/src/ui/windows/sessions-window.tsx +25 -0
  157. package/src/ui/windows-builtin.tsx +94 -0
  158. package/src/ui/windows.ts +18 -0
  159. 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
+ }