@vsuryav/agent-sim 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 (154) hide show
  1. package/README.md +25 -0
  2. package/bin/agent-sim.js +25 -0
  3. package/package.json +72 -0
  4. package/src/app-paths.ts +29 -0
  5. package/src/app-sync.test.ts +75 -0
  6. package/src/app-sync.ts +110 -0
  7. package/src/cli.ts +129 -0
  8. package/src/collector/claude-code.test.ts +102 -0
  9. package/src/collector/claude-code.ts +133 -0
  10. package/src/collector/codex-cli.test.ts +116 -0
  11. package/src/collector/codex-cli.ts +149 -0
  12. package/src/collector/db.test.ts +59 -0
  13. package/src/collector/db.ts +125 -0
  14. package/src/collector/names.test.ts +21 -0
  15. package/src/collector/names.ts +28 -0
  16. package/src/collector/personality.test.ts +40 -0
  17. package/src/collector/personality.ts +46 -0
  18. package/src/collector/remote-sync.test.ts +31 -0
  19. package/src/collector/remote-sync.ts +171 -0
  20. package/src/collector/sync.test.ts +67 -0
  21. package/src/collector/sync.ts +148 -0
  22. package/src/collector/types.ts +1 -0
  23. package/src/engine/bootstrap/state.ts +3 -0
  24. package/src/engine/buddy/CompanionSprite.tsx +371 -0
  25. package/src/engine/buddy/companion.ts +133 -0
  26. package/src/engine/buddy/prompt.ts +36 -0
  27. package/src/engine/buddy/sprites.ts +514 -0
  28. package/src/engine/buddy/types.ts +148 -0
  29. package/src/engine/buddy/useBuddyNotification.tsx +98 -0
  30. package/src/engine/ink/Ansi.tsx +292 -0
  31. package/src/engine/ink/bidi.ts +139 -0
  32. package/src/engine/ink/clearTerminal.ts +74 -0
  33. package/src/engine/ink/colorize.ts +231 -0
  34. package/src/engine/ink/components/AlternateScreen.tsx +80 -0
  35. package/src/engine/ink/components/App.tsx +658 -0
  36. package/src/engine/ink/components/AppContext.ts +21 -0
  37. package/src/engine/ink/components/Box.tsx +214 -0
  38. package/src/engine/ink/components/Button.tsx +192 -0
  39. package/src/engine/ink/components/ClockContext.tsx +112 -0
  40. package/src/engine/ink/components/CursorDeclarationContext.ts +32 -0
  41. package/src/engine/ink/components/ErrorOverview.tsx +109 -0
  42. package/src/engine/ink/components/Link.tsx +42 -0
  43. package/src/engine/ink/components/Newline.tsx +39 -0
  44. package/src/engine/ink/components/NoSelect.tsx +68 -0
  45. package/src/engine/ink/components/RawAnsi.tsx +57 -0
  46. package/src/engine/ink/components/ScrollBox.tsx +237 -0
  47. package/src/engine/ink/components/Spacer.tsx +20 -0
  48. package/src/engine/ink/components/StdinContext.ts +49 -0
  49. package/src/engine/ink/components/TerminalFocusContext.tsx +52 -0
  50. package/src/engine/ink/components/TerminalSizeContext.tsx +7 -0
  51. package/src/engine/ink/components/Text.tsx +254 -0
  52. package/src/engine/ink/constants.ts +2 -0
  53. package/src/engine/ink/dom.ts +484 -0
  54. package/src/engine/ink/events/click-event.ts +38 -0
  55. package/src/engine/ink/events/dispatcher.ts +233 -0
  56. package/src/engine/ink/events/emitter.ts +39 -0
  57. package/src/engine/ink/events/event-handlers.ts +73 -0
  58. package/src/engine/ink/events/event.ts +11 -0
  59. package/src/engine/ink/events/focus-event.ts +21 -0
  60. package/src/engine/ink/events/input-event.ts +205 -0
  61. package/src/engine/ink/events/keyboard-event.ts +51 -0
  62. package/src/engine/ink/events/terminal-event.ts +107 -0
  63. package/src/engine/ink/events/terminal-focus-event.ts +19 -0
  64. package/src/engine/ink/focus.ts +181 -0
  65. package/src/engine/ink/frame.ts +124 -0
  66. package/src/engine/ink/get-max-width.ts +27 -0
  67. package/src/engine/ink/global.d.ts +18 -0
  68. package/src/engine/ink/hit-test.ts +130 -0
  69. package/src/engine/ink/hooks/use-animation-frame.ts +57 -0
  70. package/src/engine/ink/hooks/use-app.ts +8 -0
  71. package/src/engine/ink/hooks/use-declared-cursor.ts +73 -0
  72. package/src/engine/ink/hooks/use-input.ts +92 -0
  73. package/src/engine/ink/hooks/use-interval.ts +67 -0
  74. package/src/engine/ink/hooks/use-search-highlight.ts +53 -0
  75. package/src/engine/ink/hooks/use-selection.ts +104 -0
  76. package/src/engine/ink/hooks/use-stdin.ts +8 -0
  77. package/src/engine/ink/hooks/use-tab-status.ts +72 -0
  78. package/src/engine/ink/hooks/use-terminal-focus.ts +16 -0
  79. package/src/engine/ink/hooks/use-terminal-title.ts +31 -0
  80. package/src/engine/ink/hooks/use-terminal-viewport.ts +96 -0
  81. package/src/engine/ink/ink.tsx +1723 -0
  82. package/src/engine/ink/instances.ts +10 -0
  83. package/src/engine/ink/layout/engine.ts +6 -0
  84. package/src/engine/ink/layout/geometry.ts +97 -0
  85. package/src/engine/ink/layout/node.ts +152 -0
  86. package/src/engine/ink/layout/yoga.ts +308 -0
  87. package/src/engine/ink/line-width-cache.ts +24 -0
  88. package/src/engine/ink/log-update.ts +773 -0
  89. package/src/engine/ink/measure-element.ts +23 -0
  90. package/src/engine/ink/measure-text.ts +47 -0
  91. package/src/engine/ink/node-cache.ts +54 -0
  92. package/src/engine/ink/optimizer.ts +93 -0
  93. package/src/engine/ink/output.ts +797 -0
  94. package/src/engine/ink/parse-keypress.ts +801 -0
  95. package/src/engine/ink/reconciler.ts +512 -0
  96. package/src/engine/ink/render-border.ts +231 -0
  97. package/src/engine/ink/render-node-to-output.ts +1462 -0
  98. package/src/engine/ink/render-to-screen.ts +231 -0
  99. package/src/engine/ink/renderer.ts +178 -0
  100. package/src/engine/ink/root.ts +184 -0
  101. package/src/engine/ink/screen.ts +1486 -0
  102. package/src/engine/ink/searchHighlight.ts +93 -0
  103. package/src/engine/ink/selection.ts +917 -0
  104. package/src/engine/ink/squash-text-nodes.ts +92 -0
  105. package/src/engine/ink/stringWidth.ts +222 -0
  106. package/src/engine/ink/styles.ts +771 -0
  107. package/src/engine/ink/supports-hyperlinks.ts +57 -0
  108. package/src/engine/ink/tabstops.ts +46 -0
  109. package/src/engine/ink/terminal-focus-state.ts +47 -0
  110. package/src/engine/ink/terminal-querier.ts +212 -0
  111. package/src/engine/ink/terminal.ts +248 -0
  112. package/src/engine/ink/termio/ansi.ts +75 -0
  113. package/src/engine/ink/termio/csi.ts +319 -0
  114. package/src/engine/ink/termio/dec.ts +60 -0
  115. package/src/engine/ink/termio/esc.ts +67 -0
  116. package/src/engine/ink/termio/osc.ts +493 -0
  117. package/src/engine/ink/termio/parser.ts +394 -0
  118. package/src/engine/ink/termio/sgr.ts +308 -0
  119. package/src/engine/ink/termio/tokenize.ts +319 -0
  120. package/src/engine/ink/termio/types.ts +236 -0
  121. package/src/engine/ink/useTerminalNotification.ts +126 -0
  122. package/src/engine/ink/warn.ts +9 -0
  123. package/src/engine/ink/widest-line.ts +19 -0
  124. package/src/engine/ink/wrap-text.ts +74 -0
  125. package/src/engine/ink/wrapAnsi.ts +20 -0
  126. package/src/engine/native-ts/yoga-layout/enums.ts +134 -0
  127. package/src/engine/native-ts/yoga-layout/index.ts +2578 -0
  128. package/src/engine/stubs/bootstrap-state.ts +4 -0
  129. package/src/engine/stubs/debug.ts +6 -0
  130. package/src/engine/stubs/log.ts +4 -0
  131. package/src/engine/utils/debug.ts +5 -0
  132. package/src/engine/utils/earlyInput.ts +4 -0
  133. package/src/engine/utils/env.ts +15 -0
  134. package/src/engine/utils/envUtils.ts +4 -0
  135. package/src/engine/utils/execFileNoThrow.ts +24 -0
  136. package/src/engine/utils/fullscreen.ts +4 -0
  137. package/src/engine/utils/intl.ts +9 -0
  138. package/src/engine/utils/log.ts +3 -0
  139. package/src/engine/utils/semver.ts +13 -0
  140. package/src/engine/utils/sliceAnsi.ts +10 -0
  141. package/src/engine/utils/theme.ts +17 -0
  142. package/src/game/App.tsx +141 -0
  143. package/src/game/agents/behavior.ts +249 -0
  144. package/src/game/agents/speech.ts +57 -0
  145. package/src/game/canvas.ts +98 -0
  146. package/src/game/launch.ts +36 -0
  147. package/src/game/ship/ShipView.tsx +145 -0
  148. package/src/game/ship/ship-map.ts +172 -0
  149. package/src/game/ui/AgentBio.tsx +72 -0
  150. package/src/game/ui/HUD.tsx +63 -0
  151. package/src/game/ui/StatusBar.tsx +49 -0
  152. package/src/game/useKeyboard.ts +62 -0
  153. package/src/main.tsx +22 -0
  154. package/src/run-interactive.ts +74 -0
@@ -0,0 +1,233 @@
1
+ import {
2
+ ContinuousEventPriority,
3
+ DefaultEventPriority,
4
+ DiscreteEventPriority,
5
+ NoEventPriority,
6
+ } from 'react-reconciler/constants.js'
7
+ import { logError } from '../../utils/log.js'
8
+ import { HANDLER_FOR_EVENT } from './event-handlers.js'
9
+ import type { EventTarget, TerminalEvent } from './terminal-event.js'
10
+
11
+ // --
12
+
13
+ type DispatchListener = {
14
+ node: EventTarget
15
+ handler: (event: TerminalEvent) => void
16
+ phase: 'capturing' | 'at_target' | 'bubbling'
17
+ }
18
+
19
+ function getHandler(
20
+ node: EventTarget,
21
+ eventType: string,
22
+ capture: boolean,
23
+ ): ((event: TerminalEvent) => void) | undefined {
24
+ const handlers = node._eventHandlers
25
+ if (!handlers) return undefined
26
+
27
+ const mapping = HANDLER_FOR_EVENT[eventType]
28
+ if (!mapping) return undefined
29
+
30
+ const propName = capture ? mapping.capture : mapping.bubble
31
+ if (!propName) return undefined
32
+
33
+ return handlers[propName] as ((event: TerminalEvent) => void) | undefined
34
+ }
35
+
36
+ /**
37
+ * Collect all listeners for an event in dispatch order.
38
+ *
39
+ * Uses react-dom's two-phase accumulation pattern:
40
+ * - Walk from target to root
41
+ * - Capture handlers are prepended (unshift) → root-first
42
+ * - Bubble handlers are appended (push) → target-first
43
+ *
44
+ * Result: [root-cap, ..., parent-cap, target-cap, target-bub, parent-bub, ..., root-bub]
45
+ */
46
+ function collectListeners(
47
+ target: EventTarget,
48
+ event: TerminalEvent,
49
+ ): DispatchListener[] {
50
+ const listeners: DispatchListener[] = []
51
+
52
+ let node: EventTarget | undefined = target
53
+ while (node) {
54
+ const isTarget = node === target
55
+
56
+ const captureHandler = getHandler(node, event.type, true)
57
+ const bubbleHandler = getHandler(node, event.type, false)
58
+
59
+ if (captureHandler) {
60
+ listeners.unshift({
61
+ node,
62
+ handler: captureHandler,
63
+ phase: isTarget ? 'at_target' : 'capturing',
64
+ })
65
+ }
66
+
67
+ if (bubbleHandler && (event.bubbles || isTarget)) {
68
+ listeners.push({
69
+ node,
70
+ handler: bubbleHandler,
71
+ phase: isTarget ? 'at_target' : 'bubbling',
72
+ })
73
+ }
74
+
75
+ node = node.parentNode
76
+ }
77
+
78
+ return listeners
79
+ }
80
+
81
+ /**
82
+ * Execute collected listeners with propagation control.
83
+ *
84
+ * Before each handler, calls event._prepareForTarget(node) so event
85
+ * subclasses can do per-node setup.
86
+ */
87
+ function processDispatchQueue(
88
+ listeners: DispatchListener[],
89
+ event: TerminalEvent,
90
+ ): void {
91
+ let previousNode: EventTarget | undefined
92
+
93
+ for (const { node, handler, phase } of listeners) {
94
+ if (event._isImmediatePropagationStopped()) {
95
+ break
96
+ }
97
+
98
+ if (event._isPropagationStopped() && node !== previousNode) {
99
+ break
100
+ }
101
+
102
+ event._setEventPhase(phase)
103
+ event._setCurrentTarget(node)
104
+ event._prepareForTarget(node)
105
+
106
+ try {
107
+ handler(event)
108
+ } catch (error) {
109
+ logError(error)
110
+ }
111
+
112
+ previousNode = node
113
+ }
114
+ }
115
+
116
+ // --
117
+
118
+ /**
119
+ * Map terminal event types to React scheduling priorities.
120
+ * Mirrors react-dom's getEventPriority() switch.
121
+ */
122
+ function getEventPriority(eventType: string): number {
123
+ switch (eventType) {
124
+ case 'keydown':
125
+ case 'keyup':
126
+ case 'click':
127
+ case 'focus':
128
+ case 'blur':
129
+ case 'paste':
130
+ return DiscreteEventPriority as number
131
+ case 'resize':
132
+ case 'scroll':
133
+ case 'mousemove':
134
+ return ContinuousEventPriority as number
135
+ default:
136
+ return DefaultEventPriority as number
137
+ }
138
+ }
139
+
140
+ // --
141
+
142
+ type DiscreteUpdates = <A, B>(
143
+ fn: (a: A, b: B) => boolean,
144
+ a: A,
145
+ b: B,
146
+ c: undefined,
147
+ d: undefined,
148
+ ) => boolean
149
+
150
+ /**
151
+ * Owns event dispatch state and the capture/bubble dispatch loop.
152
+ *
153
+ * The reconciler host config reads currentEvent and currentUpdatePriority
154
+ * to implement resolveUpdatePriority, resolveEventType, and
155
+ * resolveEventTimeStamp — mirroring how react-dom's host config reads
156
+ * ReactDOMSharedInternals and window.event.
157
+ *
158
+ * discreteUpdates is injected after construction (by InkReconciler)
159
+ * to break the import cycle.
160
+ */
161
+ export class Dispatcher {
162
+ currentEvent: TerminalEvent | null = null
163
+ currentUpdatePriority: number = DefaultEventPriority as number
164
+ discreteUpdates: DiscreteUpdates | null = null
165
+
166
+ /**
167
+ * Infer event priority from the currently-dispatching event.
168
+ * Called by the reconciler host config's resolveUpdatePriority
169
+ * when no explicit priority has been set.
170
+ */
171
+ resolveEventPriority(): number {
172
+ if (this.currentUpdatePriority !== (NoEventPriority as number)) {
173
+ return this.currentUpdatePriority
174
+ }
175
+ if (this.currentEvent) {
176
+ return getEventPriority(this.currentEvent.type)
177
+ }
178
+ return DefaultEventPriority as number
179
+ }
180
+
181
+ /**
182
+ * Dispatch an event through capture and bubble phases.
183
+ * Returns true if preventDefault() was NOT called.
184
+ */
185
+ dispatch(target: EventTarget, event: TerminalEvent): boolean {
186
+ const previousEvent = this.currentEvent
187
+ this.currentEvent = event
188
+ try {
189
+ event._setTarget(target)
190
+
191
+ const listeners = collectListeners(target, event)
192
+ processDispatchQueue(listeners, event)
193
+
194
+ event._setEventPhase('none')
195
+ event._setCurrentTarget(null)
196
+
197
+ return !event.defaultPrevented
198
+ } finally {
199
+ this.currentEvent = previousEvent
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Dispatch with discrete (sync) priority.
205
+ * For user-initiated events: keyboard, click, focus, paste.
206
+ */
207
+ dispatchDiscrete(target: EventTarget, event: TerminalEvent): boolean {
208
+ if (!this.discreteUpdates) {
209
+ return this.dispatch(target, event)
210
+ }
211
+ return this.discreteUpdates(
212
+ (t, e) => this.dispatch(t, e),
213
+ target,
214
+ event,
215
+ undefined,
216
+ undefined,
217
+ )
218
+ }
219
+
220
+ /**
221
+ * Dispatch with continuous priority.
222
+ * For high-frequency events: resize, scroll, mouse move.
223
+ */
224
+ dispatchContinuous(target: EventTarget, event: TerminalEvent): boolean {
225
+ const previousPriority = this.currentUpdatePriority
226
+ try {
227
+ this.currentUpdatePriority = ContinuousEventPriority as number
228
+ return this.dispatch(target, event)
229
+ } finally {
230
+ this.currentUpdatePriority = previousPriority
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,39 @@
1
+ import { EventEmitter as NodeEventEmitter } from 'events'
2
+ import { Event } from './event.js'
3
+
4
+ // Similar to node's builtin EventEmitter, but is also aware of our `Event`
5
+ // class, and so `emit` respects `stopImmediatePropagation()`.
6
+ export class EventEmitter extends NodeEventEmitter {
7
+ constructor() {
8
+ super()
9
+ // Disable the default maxListeners warning. In React, many components
10
+ // can legitimately listen to the same event (e.g., useInput hooks).
11
+ // The default limit of 10 causes spurious warnings.
12
+ this.setMaxListeners(0)
13
+ }
14
+
15
+ override emit(type: string | symbol, ...args: unknown[]): boolean {
16
+ // Delegate to node for `error`, since it's not treated like a normal event
17
+ if (type === 'error') {
18
+ return super.emit(type, ...args)
19
+ }
20
+
21
+ const listeners = this.rawListeners(type)
22
+
23
+ if (listeners.length === 0) {
24
+ return false
25
+ }
26
+
27
+ const ccEvent = args[0] instanceof Event ? args[0] : null
28
+
29
+ for (const listener of listeners) {
30
+ listener.apply(this, args)
31
+
32
+ if (ccEvent?.didStopImmediatePropagation()) {
33
+ break
34
+ }
35
+ }
36
+
37
+ return true
38
+ }
39
+ }
@@ -0,0 +1,73 @@
1
+ import type { ClickEvent } from './click-event.js'
2
+ import type { FocusEvent } from './focus-event.js'
3
+ import type { KeyboardEvent } from './keyboard-event.js'
4
+ import type { PasteEvent } from './paste-event.js'
5
+ import type { ResizeEvent } from './resize-event.js'
6
+
7
+ type KeyboardEventHandler = (event: KeyboardEvent) => void
8
+ type FocusEventHandler = (event: FocusEvent) => void
9
+ type PasteEventHandler = (event: PasteEvent) => void
10
+ type ResizeEventHandler = (event: ResizeEvent) => void
11
+ type ClickEventHandler = (event: ClickEvent) => void
12
+ type HoverEventHandler = () => void
13
+
14
+ /**
15
+ * Props for event handlers on Box and other host components.
16
+ *
17
+ * Follows the React/DOM naming convention:
18
+ * - onEventName: handler for bubble phase
19
+ * - onEventNameCapture: handler for capture phase
20
+ */
21
+ export type EventHandlerProps = {
22
+ onKeyDown?: KeyboardEventHandler
23
+ onKeyDownCapture?: KeyboardEventHandler
24
+
25
+ onFocus?: FocusEventHandler
26
+ onFocusCapture?: FocusEventHandler
27
+ onBlur?: FocusEventHandler
28
+ onBlurCapture?: FocusEventHandler
29
+
30
+ onPaste?: PasteEventHandler
31
+ onPasteCapture?: PasteEventHandler
32
+
33
+ onResize?: ResizeEventHandler
34
+
35
+ onClick?: ClickEventHandler
36
+ onMouseEnter?: HoverEventHandler
37
+ onMouseLeave?: HoverEventHandler
38
+ }
39
+
40
+ /**
41
+ * Reverse lookup: event type string → handler prop names.
42
+ * Used by the dispatcher for O(1) handler lookup per node.
43
+ */
44
+ export const HANDLER_FOR_EVENT: Record<
45
+ string,
46
+ { bubble?: keyof EventHandlerProps; capture?: keyof EventHandlerProps }
47
+ > = {
48
+ keydown: { bubble: 'onKeyDown', capture: 'onKeyDownCapture' },
49
+ focus: { bubble: 'onFocus', capture: 'onFocusCapture' },
50
+ blur: { bubble: 'onBlur', capture: 'onBlurCapture' },
51
+ paste: { bubble: 'onPaste', capture: 'onPasteCapture' },
52
+ resize: { bubble: 'onResize' },
53
+ click: { bubble: 'onClick' },
54
+ }
55
+
56
+ /**
57
+ * Set of all event handler prop names, for the reconciler to detect
58
+ * event props and store them in _eventHandlers instead of attributes.
59
+ */
60
+ export const EVENT_HANDLER_PROPS = new Set<string>([
61
+ 'onKeyDown',
62
+ 'onKeyDownCapture',
63
+ 'onFocus',
64
+ 'onFocusCapture',
65
+ 'onBlur',
66
+ 'onBlurCapture',
67
+ 'onPaste',
68
+ 'onPasteCapture',
69
+ 'onResize',
70
+ 'onClick',
71
+ 'onMouseEnter',
72
+ 'onMouseLeave',
73
+ ])
@@ -0,0 +1,11 @@
1
+ export class Event {
2
+ private _didStopImmediatePropagation = false
3
+
4
+ didStopImmediatePropagation(): boolean {
5
+ return this._didStopImmediatePropagation
6
+ }
7
+
8
+ stopImmediatePropagation(): void {
9
+ this._didStopImmediatePropagation = true
10
+ }
11
+ }
@@ -0,0 +1,21 @@
1
+ import { type EventTarget, TerminalEvent } from './terminal-event.js'
2
+
3
+ /**
4
+ * Focus event for component focus changes.
5
+ *
6
+ * Dispatched when focus moves between elements. 'focus' fires on the
7
+ * newly focused element, 'blur' fires on the previously focused one.
8
+ * Both bubble, matching react-dom's use of focusin/focusout semantics
9
+ * so parent components can observe descendant focus changes.
10
+ */
11
+ export class FocusEvent extends TerminalEvent {
12
+ readonly relatedTarget: EventTarget | null
13
+
14
+ constructor(
15
+ type: 'focus' | 'blur',
16
+ relatedTarget: EventTarget | null = null,
17
+ ) {
18
+ super(type, { bubbles: true, cancelable: false })
19
+ this.relatedTarget = relatedTarget
20
+ }
21
+ }
@@ -0,0 +1,205 @@
1
+ import { nonAlphanumericKeys, type ParsedKey } from '../parse-keypress.js'
2
+ import { Event } from './event.js'
3
+
4
+ export type Key = {
5
+ upArrow: boolean
6
+ downArrow: boolean
7
+ leftArrow: boolean
8
+ rightArrow: boolean
9
+ pageDown: boolean
10
+ pageUp: boolean
11
+ wheelUp: boolean
12
+ wheelDown: boolean
13
+ home: boolean
14
+ end: boolean
15
+ return: boolean
16
+ escape: boolean
17
+ ctrl: boolean
18
+ shift: boolean
19
+ fn: boolean
20
+ tab: boolean
21
+ backspace: boolean
22
+ delete: boolean
23
+ meta: boolean
24
+ super: boolean
25
+ }
26
+
27
+ function parseKey(keypress: ParsedKey): [Key, string] {
28
+ const key: Key = {
29
+ upArrow: keypress.name === 'up',
30
+ downArrow: keypress.name === 'down',
31
+ leftArrow: keypress.name === 'left',
32
+ rightArrow: keypress.name === 'right',
33
+ pageDown: keypress.name === 'pagedown',
34
+ pageUp: keypress.name === 'pageup',
35
+ wheelUp: keypress.name === 'wheelup',
36
+ wheelDown: keypress.name === 'wheeldown',
37
+ home: keypress.name === 'home',
38
+ end: keypress.name === 'end',
39
+ return: keypress.name === 'return',
40
+ escape: keypress.name === 'escape',
41
+ fn: keypress.fn,
42
+ ctrl: keypress.ctrl,
43
+ shift: keypress.shift,
44
+ tab: keypress.name === 'tab',
45
+ backspace: keypress.name === 'backspace',
46
+ delete: keypress.name === 'delete',
47
+ // `parseKeypress` parses \u001B\u001B[A (meta + up arrow) as meta = false
48
+ // but with option = true, so we need to take this into account here
49
+ // to avoid breaking changes in Ink.
50
+ // TODO(vadimdemedes): consider removing this in the next major version.
51
+ meta: keypress.meta || keypress.name === 'escape' || keypress.option,
52
+ // Super (Cmd on macOS / Win key) — only arrives via kitty keyboard
53
+ // protocol CSI u sequences. Distinct from meta (Alt/Option) so
54
+ // bindings like cmd+c can be expressed separately from opt+c.
55
+ super: keypress.super,
56
+ }
57
+
58
+ let input = keypress.ctrl ? keypress.name : keypress.sequence
59
+
60
+ // Handle undefined input case
61
+ if (input === undefined) {
62
+ input = ''
63
+ }
64
+
65
+ // When ctrl is set, keypress.name for space is the literal word "space".
66
+ // Convert to actual space character for consistency with the CSI u branch
67
+ // (which maps 'space' → ' '). Without this, ctrl+space leaks the literal
68
+ // word "space" into text input.
69
+ if (keypress.ctrl && input === 'space') {
70
+ input = ' '
71
+ }
72
+
73
+ // Suppress unrecognized escape sequences that were parsed as function keys
74
+ // (matched by FN_KEY_RE) but have no name in the keyName map.
75
+ // Examples: ESC[25~ (F13/Right Alt on Windows), ESC[26~ (F14), etc.
76
+ // Without this, the ESC prefix is stripped below and the remainder (e.g.,
77
+ // "[25~") leaks into the input as literal text.
78
+ if (keypress.code && !keypress.name) {
79
+ input = ''
80
+ }
81
+
82
+ // Suppress ESC-less SGR mouse fragments. When a heavy React commit blocks
83
+ // the event loop past App's 50ms NORMAL_TIMEOUT flush, a CSI split across
84
+ // stdin chunks gets its buffered ESC flushed as a lone Escape key, and the
85
+ // continuation arrives as a text token with name='' — which falls through
86
+ // all of parseKeypress's ESC-anchored regexes and the nonAlphanumericKeys
87
+ // clear below (name is falsy). The fragment then leaks into the prompt as
88
+ // literal `[<64;74;16M`. This is the same defensive sink as the F13 guard
89
+ // above; the underlying tokenizer-flush race is upstream of this layer.
90
+ if (!keypress.name && /^\[<\d+;\d+;\d+[Mm]/.test(input)) {
91
+ input = ''
92
+ }
93
+
94
+ // Strip meta if it's still remaining after `parseKeypress`
95
+ // TODO(vadimdemedes): remove this in the next major version.
96
+ if (input.startsWith('\u001B')) {
97
+ input = input.slice(1)
98
+ }
99
+
100
+ // Track whether we've already processed this as a special sequence
101
+ // that converted input to the key name (CSI u or application keypad mode).
102
+ // For these, we don't want to clear input with nonAlphanumericKeys check.
103
+ let processedAsSpecialSequence = false
104
+
105
+ // Handle CSI u sequences (Kitty keyboard protocol): after stripping ESC,
106
+ // we're left with "[codepoint;modifieru" (e.g., "[98;3u" for Alt+b).
107
+ // Use the parsed key name instead for input handling. Require a digit
108
+ // after [ — real CSI u is always [<digits>…u, and a bare startsWith('[')
109
+ // false-matches X10 mouse at row 85 (Cy = 85+32 = 'u'), leaking the
110
+ // literal text "mouse" into the prompt via processedAsSpecialSequence.
111
+ if (/^\[\d/.test(input) && input.endsWith('u')) {
112
+ if (!keypress.name) {
113
+ // Unmapped Kitty functional key (Caps Lock 57358, F13–F35, KP nav,
114
+ // bare modifiers, etc.) — keycodeToName() returned undefined. Swallow
115
+ // so the raw "[57358u" doesn't leak into the prompt. See #38781.
116
+ input = ''
117
+ } else {
118
+ // 'space' → ' '; 'escape' → '' (key.escape carries it;
119
+ // processedAsSpecialSequence bypasses the nonAlphanumericKeys
120
+ // clear below, so we must handle it explicitly here);
121
+ // otherwise use key name.
122
+ input =
123
+ keypress.name === 'space'
124
+ ? ' '
125
+ : keypress.name === 'escape'
126
+ ? ''
127
+ : keypress.name
128
+ }
129
+ processedAsSpecialSequence = true
130
+ }
131
+
132
+ // Handle xterm modifyOtherKeys sequences: after stripping ESC, we're left
133
+ // with "[27;modifier;keycode~" (e.g., "[27;3;98~" for Alt+b). Same
134
+ // extraction as CSI u — without this, printable-char keycodes (single-letter
135
+ // names) skip the nonAlphanumericKeys clear and leak "[27;..." as input.
136
+ if (input.startsWith('[27;') && input.endsWith('~')) {
137
+ if (!keypress.name) {
138
+ // Unmapped modifyOtherKeys keycode — swallow for consistency with
139
+ // the CSI u handler above. Practically untriggerable today (xterm
140
+ // modifyOtherKeys only sends ASCII keycodes, all mapped), but
141
+ // guards against future terminal behavior.
142
+ input = ''
143
+ } else {
144
+ input =
145
+ keypress.name === 'space'
146
+ ? ' '
147
+ : keypress.name === 'escape'
148
+ ? ''
149
+ : keypress.name
150
+ }
151
+ processedAsSpecialSequence = true
152
+ }
153
+
154
+ // Handle application keypad mode sequences: after stripping ESC,
155
+ // we're left with "O<letter>" (e.g., "Op" for numpad 0, "Oy" for numpad 9).
156
+ // Use the parsed key name (the digit character) for input handling.
157
+ if (
158
+ input.startsWith('O') &&
159
+ input.length === 2 &&
160
+ keypress.name &&
161
+ keypress.name.length === 1
162
+ ) {
163
+ input = keypress.name
164
+ processedAsSpecialSequence = true
165
+ }
166
+
167
+ // Clear input for non-alphanumeric keys (arrows, function keys, etc.)
168
+ // Skip this for CSI u and application keypad mode sequences since
169
+ // those were already converted to their proper input characters.
170
+ if (
171
+ !processedAsSpecialSequence &&
172
+ keypress.name &&
173
+ nonAlphanumericKeys.includes(keypress.name)
174
+ ) {
175
+ input = ''
176
+ }
177
+
178
+ // Set shift=true for uppercase letters (A-Z)
179
+ // Must check it's actually a letter, not just any char unchanged by toUpperCase
180
+ if (
181
+ input.length === 1 &&
182
+ typeof input[0] === 'string' &&
183
+ input[0] >= 'A' &&
184
+ input[0] <= 'Z'
185
+ ) {
186
+ key.shift = true
187
+ }
188
+
189
+ return [key, input]
190
+ }
191
+
192
+ export class InputEvent extends Event {
193
+ readonly keypress: ParsedKey
194
+ readonly key: Key
195
+ readonly input: string
196
+
197
+ constructor(keypress: ParsedKey) {
198
+ super()
199
+ const [key, input] = parseKey(keypress)
200
+
201
+ this.keypress = keypress
202
+ this.key = key
203
+ this.input = input
204
+ }
205
+ }
@@ -0,0 +1,51 @@
1
+ import type { ParsedKey } from '../parse-keypress.js'
2
+ import { TerminalEvent } from './terminal-event.js'
3
+
4
+ /**
5
+ * Keyboard event dispatched through the DOM tree via capture/bubble.
6
+ *
7
+ * Follows browser KeyboardEvent semantics: `key` is the literal character
8
+ * for printable keys ('a', '3', ' ', '/') and a multi-char name for
9
+ * special keys ('down', 'return', 'escape', 'f1'). The idiomatic
10
+ * printable-char check is `e.key.length === 1`.
11
+ */
12
+ export class KeyboardEvent extends TerminalEvent {
13
+ readonly key: string
14
+ readonly ctrl: boolean
15
+ readonly shift: boolean
16
+ readonly meta: boolean
17
+ readonly superKey: boolean
18
+ readonly fn: boolean
19
+
20
+ constructor(parsedKey: ParsedKey) {
21
+ super('keydown', { bubbles: true, cancelable: true })
22
+
23
+ this.key = keyFromParsed(parsedKey)
24
+ this.ctrl = parsedKey.ctrl
25
+ this.shift = parsedKey.shift
26
+ this.meta = parsedKey.meta || parsedKey.option
27
+ this.superKey = parsedKey.super
28
+ this.fn = parsedKey.fn
29
+ }
30
+ }
31
+
32
+ function keyFromParsed(parsed: ParsedKey): string {
33
+ const seq = parsed.sequence ?? ''
34
+ const name = parsed.name ?? ''
35
+
36
+ // Ctrl combos: sequence is a control byte (\x03 for ctrl+c), name is the
37
+ // letter. Browsers report e.key === 'c' with e.ctrlKey === true.
38
+ if (parsed.ctrl) return name
39
+
40
+ // Single printable char (space through ~, plus anything above ASCII):
41
+ // use the literal char. Browsers report e.key === '3', not 'Digit3'.
42
+ if (seq.length === 1) {
43
+ const code = seq.charCodeAt(0)
44
+ if (code >= 0x20 && code !== 0x7f) return seq
45
+ }
46
+
47
+ // Special keys (arrows, F-keys, return, tab, escape, etc.): sequence is
48
+ // either an escape sequence (\x1b[B) or a control byte (\r, \t), so use
49
+ // the parsed name. Browsers report e.key === 'ArrowDown'.
50
+ return name || seq
51
+ }