yzcode-cli 1.0.1 → 1.0.3

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 (117) hide show
  1. package/assistant/sessionHistory.ts +87 -0
  2. package/bootstrap/state.ts +1769 -0
  3. package/bridge/bridgeApi.ts +539 -0
  4. package/bridge/bridgeConfig.ts +48 -0
  5. package/bridge/bridgeDebug.ts +135 -0
  6. package/bridge/bridgeEnabled.ts +202 -0
  7. package/bridge/bridgeMain.ts +2999 -0
  8. package/bridge/bridgeMessaging.ts +461 -0
  9. package/bridge/bridgePermissionCallbacks.ts +43 -0
  10. package/bridge/bridgePointer.ts +210 -0
  11. package/bridge/bridgeStatusUtil.ts +163 -0
  12. package/bridge/bridgeUI.ts +530 -0
  13. package/bridge/capacityWake.ts +56 -0
  14. package/bridge/codeSessionApi.ts +168 -0
  15. package/bridge/createSession.ts +384 -0
  16. package/bridge/debugUtils.ts +141 -0
  17. package/bridge/envLessBridgeConfig.ts +165 -0
  18. package/bridge/flushGate.ts +71 -0
  19. package/bridge/inboundAttachments.ts +175 -0
  20. package/bridge/inboundMessages.ts +80 -0
  21. package/bridge/initReplBridge.ts +569 -0
  22. package/bridge/jwtUtils.ts +256 -0
  23. package/bridge/pollConfig.ts +110 -0
  24. package/bridge/pollConfigDefaults.ts +82 -0
  25. package/bridge/remoteBridgeCore.ts +1008 -0
  26. package/bridge/replBridge.ts +2406 -0
  27. package/bridge/replBridgeHandle.ts +36 -0
  28. package/bridge/replBridgeTransport.ts +370 -0
  29. package/bridge/sessionIdCompat.ts +57 -0
  30. package/bridge/sessionRunner.ts +550 -0
  31. package/bridge/trustedDevice.ts +210 -0
  32. package/bridge/types.ts +262 -0
  33. package/bridge/workSecret.ts +127 -0
  34. package/buddy/CompanionSprite.tsx +371 -0
  35. package/buddy/companion.ts +133 -0
  36. package/buddy/prompt.ts +36 -0
  37. package/buddy/sprites.ts +514 -0
  38. package/buddy/types.ts +148 -0
  39. package/buddy/useBuddyNotification.tsx +98 -0
  40. package/coordinator/coordinatorMode.ts +369 -0
  41. package/memdir/findRelevantMemories.ts +141 -0
  42. package/memdir/memdir.ts +507 -0
  43. package/memdir/memoryAge.ts +53 -0
  44. package/memdir/memoryScan.ts +94 -0
  45. package/memdir/memoryTypes.ts +271 -0
  46. package/memdir/paths.ts +278 -0
  47. package/memdir/teamMemPaths.ts +292 -0
  48. package/memdir/teamMemPrompts.ts +100 -0
  49. package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
  50. package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
  51. package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
  52. package/migrations/migrateFennecToOpus.ts +45 -0
  53. package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
  54. package/migrations/migrateOpusToOpus1m.ts +43 -0
  55. package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
  56. package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
  57. package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
  58. package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
  59. package/migrations/resetProToOpusDefault.ts +51 -0
  60. package/native-ts/color-diff/index.ts +999 -0
  61. package/native-ts/file-index/index.ts +370 -0
  62. package/native-ts/yoga-layout/enums.ts +134 -0
  63. package/native-ts/yoga-layout/index.ts +2578 -0
  64. package/outputStyles/loadOutputStylesDir.ts +98 -0
  65. package/package.json +22 -5
  66. package/plugins/builtinPlugins.ts +159 -0
  67. package/plugins/bundled/index.ts +23 -0
  68. package/schemas/hooks.ts +222 -0
  69. package/screens/Doctor.tsx +575 -0
  70. package/screens/REPL.tsx +5006 -0
  71. package/screens/ResumeConversation.tsx +399 -0
  72. package/server/createDirectConnectSession.ts +88 -0
  73. package/server/directConnectManager.ts +213 -0
  74. package/server/types.ts +57 -0
  75. package/skills/bundled/batch.ts +124 -0
  76. package/skills/bundled/claudeApi.ts +196 -0
  77. package/skills/bundled/claudeApiContent.ts +75 -0
  78. package/skills/bundled/claudeInChrome.ts +34 -0
  79. package/skills/bundled/debug.ts +103 -0
  80. package/skills/bundled/index.ts +79 -0
  81. package/skills/bundled/keybindings.ts +339 -0
  82. package/skills/bundled/loop.ts +92 -0
  83. package/skills/bundled/loremIpsum.ts +282 -0
  84. package/skills/bundled/remember.ts +82 -0
  85. package/skills/bundled/scheduleRemoteAgents.ts +447 -0
  86. package/skills/bundled/simplify.ts +69 -0
  87. package/skills/bundled/skillify.ts +197 -0
  88. package/skills/bundled/stuck.ts +79 -0
  89. package/skills/bundled/updateConfig.ts +475 -0
  90. package/skills/bundled/verify/SKILL.md +3 -0
  91. package/skills/bundled/verify/examples/cli.md +3 -0
  92. package/skills/bundled/verify/examples/server.md +3 -0
  93. package/skills/bundled/verify.ts +30 -0
  94. package/skills/bundled/verifyContent.ts +13 -0
  95. package/skills/bundledSkills.ts +220 -0
  96. package/skills/loadSkillsDir.ts +1086 -0
  97. package/skills/mcpSkillBuilders.ts +44 -0
  98. package/tasks/DreamTask/DreamTask.ts +157 -0
  99. package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
  100. package/tasks/InProcessTeammateTask/types.ts +121 -0
  101. package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
  102. package/tasks/LocalMainSessionTask.ts +479 -0
  103. package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
  104. package/tasks/LocalShellTask/guards.ts +41 -0
  105. package/tasks/LocalShellTask/killShellTasks.ts +76 -0
  106. package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
  107. package/tasks/pillLabel.ts +82 -0
  108. package/tasks/stopTask.ts +100 -0
  109. package/tasks/types.ts +46 -0
  110. package/upstreamproxy/relay.ts +455 -0
  111. package/upstreamproxy/upstreamproxy.ts +285 -0
  112. package/vim/motions.ts +82 -0
  113. package/vim/operators.ts +556 -0
  114. package/vim/textObjects.ts +186 -0
  115. package/vim/transitions.ts +490 -0
  116. package/vim/types.ts +199 -0
  117. package/voice/voiceModeEnabled.ts +54 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Vim Text Object Finding
3
+ *
4
+ * Functions for finding text object boundaries (iw, aw, i", a(, etc.)
5
+ */
6
+
7
+ import {
8
+ isVimPunctuation,
9
+ isVimWhitespace,
10
+ isVimWordChar,
11
+ } from '../utils/Cursor.js'
12
+ import { getGraphemeSegmenter } from '../utils/intl.js'
13
+
14
+ export type TextObjectRange = { start: number; end: number } | null
15
+
16
+ /**
17
+ * Delimiter pairs for text objects.
18
+ */
19
+ const PAIRS: Record<string, [string, string]> = {
20
+ '(': ['(', ')'],
21
+ ')': ['(', ')'],
22
+ b: ['(', ')'],
23
+ '[': ['[', ']'],
24
+ ']': ['[', ']'],
25
+ '{': ['{', '}'],
26
+ '}': ['{', '}'],
27
+ B: ['{', '}'],
28
+ '<': ['<', '>'],
29
+ '>': ['<', '>'],
30
+ '"': ['"', '"'],
31
+ "'": ["'", "'"],
32
+ '`': ['`', '`'],
33
+ }
34
+
35
+ /**
36
+ * Find a text object at the given position.
37
+ */
38
+ export function findTextObject(
39
+ text: string,
40
+ offset: number,
41
+ objectType: string,
42
+ isInner: boolean,
43
+ ): TextObjectRange {
44
+ if (objectType === 'w')
45
+ return findWordObject(text, offset, isInner, isVimWordChar)
46
+ if (objectType === 'W')
47
+ return findWordObject(text, offset, isInner, ch => !isVimWhitespace(ch))
48
+
49
+ const pair = PAIRS[objectType]
50
+ if (pair) {
51
+ const [open, close] = pair
52
+ return open === close
53
+ ? findQuoteObject(text, offset, open, isInner)
54
+ : findBracketObject(text, offset, open, close, isInner)
55
+ }
56
+
57
+ return null
58
+ }
59
+
60
+ function findWordObject(
61
+ text: string,
62
+ offset: number,
63
+ isInner: boolean,
64
+ isWordChar: (ch: string) => boolean,
65
+ ): TextObjectRange {
66
+ // Pre-segment into graphemes for grapheme-safe iteration
67
+ const graphemes: Array<{ segment: string; index: number }> = []
68
+ for (const { segment, index } of getGraphemeSegmenter().segment(text)) {
69
+ graphemes.push({ segment, index })
70
+ }
71
+
72
+ // Find which grapheme index the offset falls in
73
+ let graphemeIdx = graphemes.length - 1
74
+ for (let i = 0; i < graphemes.length; i++) {
75
+ const g = graphemes[i]!
76
+ const nextStart =
77
+ i + 1 < graphemes.length ? graphemes[i + 1]!.index : text.length
78
+ if (offset >= g.index && offset < nextStart) {
79
+ graphemeIdx = i
80
+ break
81
+ }
82
+ }
83
+
84
+ const graphemeAt = (idx: number): string => graphemes[idx]?.segment ?? ''
85
+ const offsetAt = (idx: number): number =>
86
+ idx < graphemes.length ? graphemes[idx]!.index : text.length
87
+ const isWs = (idx: number): boolean => isVimWhitespace(graphemeAt(idx))
88
+ const isWord = (idx: number): boolean => isWordChar(graphemeAt(idx))
89
+ const isPunct = (idx: number): boolean => isVimPunctuation(graphemeAt(idx))
90
+
91
+ let startIdx = graphemeIdx
92
+ let endIdx = graphemeIdx
93
+
94
+ if (isWord(graphemeIdx)) {
95
+ while (startIdx > 0 && isWord(startIdx - 1)) startIdx--
96
+ while (endIdx < graphemes.length && isWord(endIdx)) endIdx++
97
+ } else if (isWs(graphemeIdx)) {
98
+ while (startIdx > 0 && isWs(startIdx - 1)) startIdx--
99
+ while (endIdx < graphemes.length && isWs(endIdx)) endIdx++
100
+ return { start: offsetAt(startIdx), end: offsetAt(endIdx) }
101
+ } else if (isPunct(graphemeIdx)) {
102
+ while (startIdx > 0 && isPunct(startIdx - 1)) startIdx--
103
+ while (endIdx < graphemes.length && isPunct(endIdx)) endIdx++
104
+ }
105
+
106
+ if (!isInner) {
107
+ // Include surrounding whitespace
108
+ if (endIdx < graphemes.length && isWs(endIdx)) {
109
+ while (endIdx < graphemes.length && isWs(endIdx)) endIdx++
110
+ } else if (startIdx > 0 && isWs(startIdx - 1)) {
111
+ while (startIdx > 0 && isWs(startIdx - 1)) startIdx--
112
+ }
113
+ }
114
+
115
+ return { start: offsetAt(startIdx), end: offsetAt(endIdx) }
116
+ }
117
+
118
+ function findQuoteObject(
119
+ text: string,
120
+ offset: number,
121
+ quote: string,
122
+ isInner: boolean,
123
+ ): TextObjectRange {
124
+ const lineStart = text.lastIndexOf('\n', offset - 1) + 1
125
+ const lineEnd = text.indexOf('\n', offset)
126
+ const effectiveEnd = lineEnd === -1 ? text.length : lineEnd
127
+ const line = text.slice(lineStart, effectiveEnd)
128
+ const posInLine = offset - lineStart
129
+
130
+ const positions: number[] = []
131
+ for (let i = 0; i < line.length; i++) {
132
+ if (line[i] === quote) positions.push(i)
133
+ }
134
+
135
+ // Pair quotes correctly: 0-1, 2-3, 4-5, etc.
136
+ for (let i = 0; i < positions.length - 1; i += 2) {
137
+ const qs = positions[i]!
138
+ const qe = positions[i + 1]!
139
+ if (qs <= posInLine && posInLine <= qe) {
140
+ return isInner
141
+ ? { start: lineStart + qs + 1, end: lineStart + qe }
142
+ : { start: lineStart + qs, end: lineStart + qe + 1 }
143
+ }
144
+ }
145
+
146
+ return null
147
+ }
148
+
149
+ function findBracketObject(
150
+ text: string,
151
+ offset: number,
152
+ open: string,
153
+ close: string,
154
+ isInner: boolean,
155
+ ): TextObjectRange {
156
+ let depth = 0
157
+ let start = -1
158
+
159
+ for (let i = offset; i >= 0; i--) {
160
+ if (text[i] === close && i !== offset) depth++
161
+ else if (text[i] === open) {
162
+ if (depth === 0) {
163
+ start = i
164
+ break
165
+ }
166
+ depth--
167
+ }
168
+ }
169
+ if (start === -1) return null
170
+
171
+ depth = 0
172
+ let end = -1
173
+ for (let i = start + 1; i < text.length; i++) {
174
+ if (text[i] === open) depth++
175
+ else if (text[i] === close) {
176
+ if (depth === 0) {
177
+ end = i
178
+ break
179
+ }
180
+ depth--
181
+ }
182
+ }
183
+ if (end === -1) return null
184
+
185
+ return isInner ? { start: start + 1, end } : { start, end: end + 1 }
186
+ }
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Vim State Transition Table
3
+ *
4
+ * This is the scannable source of truth for state transitions.
5
+ * To understand what happens in any state, look up that state's transition function.
6
+ */
7
+
8
+ import { resolveMotion } from './motions.js'
9
+ import {
10
+ executeIndent,
11
+ executeJoin,
12
+ executeLineOp,
13
+ executeOpenLine,
14
+ executeOperatorFind,
15
+ executeOperatorG,
16
+ executeOperatorGg,
17
+ executeOperatorMotion,
18
+ executeOperatorTextObj,
19
+ executePaste,
20
+ executeReplace,
21
+ executeToggleCase,
22
+ executeX,
23
+ type OperatorContext,
24
+ } from './operators.js'
25
+ import {
26
+ type CommandState,
27
+ FIND_KEYS,
28
+ type FindType,
29
+ isOperatorKey,
30
+ isTextObjScopeKey,
31
+ MAX_VIM_COUNT,
32
+ OPERATORS,
33
+ type Operator,
34
+ SIMPLE_MOTIONS,
35
+ TEXT_OBJ_SCOPES,
36
+ TEXT_OBJ_TYPES,
37
+ type TextObjScope,
38
+ } from './types.js'
39
+
40
+ /**
41
+ * Context passed to transition functions.
42
+ */
43
+ export type TransitionContext = OperatorContext & {
44
+ onUndo?: () => void
45
+ onDotRepeat?: () => void
46
+ }
47
+
48
+ /**
49
+ * Result of a transition.
50
+ */
51
+ export type TransitionResult = {
52
+ next?: CommandState
53
+ execute?: () => void
54
+ }
55
+
56
+ /**
57
+ * Main transition function. Dispatches based on current state type.
58
+ */
59
+ export function transition(
60
+ state: CommandState,
61
+ input: string,
62
+ ctx: TransitionContext,
63
+ ): TransitionResult {
64
+ switch (state.type) {
65
+ case 'idle':
66
+ return fromIdle(input, ctx)
67
+ case 'count':
68
+ return fromCount(state, input, ctx)
69
+ case 'operator':
70
+ return fromOperator(state, input, ctx)
71
+ case 'operatorCount':
72
+ return fromOperatorCount(state, input, ctx)
73
+ case 'operatorFind':
74
+ return fromOperatorFind(state, input, ctx)
75
+ case 'operatorTextObj':
76
+ return fromOperatorTextObj(state, input, ctx)
77
+ case 'find':
78
+ return fromFind(state, input, ctx)
79
+ case 'g':
80
+ return fromG(state, input, ctx)
81
+ case 'operatorG':
82
+ return fromOperatorG(state, input, ctx)
83
+ case 'replace':
84
+ return fromReplace(state, input, ctx)
85
+ case 'indent':
86
+ return fromIndent(state, input, ctx)
87
+ }
88
+ }
89
+
90
+ // ============================================================================
91
+ // Shared Input Handling
92
+ // ============================================================================
93
+
94
+ /**
95
+ * Handle input that's valid in both idle and count states.
96
+ * Returns null if input is not recognized.
97
+ */
98
+ function handleNormalInput(
99
+ input: string,
100
+ count: number,
101
+ ctx: TransitionContext,
102
+ ): TransitionResult | null {
103
+ if (isOperatorKey(input)) {
104
+ return { next: { type: 'operator', op: OPERATORS[input], count } }
105
+ }
106
+
107
+ if (SIMPLE_MOTIONS.has(input)) {
108
+ return {
109
+ execute: () => {
110
+ const target = resolveMotion(input, ctx.cursor, count)
111
+ ctx.setOffset(target.offset)
112
+ },
113
+ }
114
+ }
115
+
116
+ if (FIND_KEYS.has(input)) {
117
+ return { next: { type: 'find', find: input as FindType, count } }
118
+ }
119
+
120
+ if (input === 'g') return { next: { type: 'g', count } }
121
+ if (input === 'r') return { next: { type: 'replace', count } }
122
+ if (input === '>' || input === '<') {
123
+ return { next: { type: 'indent', dir: input, count } }
124
+ }
125
+ if (input === '~') {
126
+ return { execute: () => executeToggleCase(count, ctx) }
127
+ }
128
+ if (input === 'x') {
129
+ return { execute: () => executeX(count, ctx) }
130
+ }
131
+ if (input === 'J') {
132
+ return { execute: () => executeJoin(count, ctx) }
133
+ }
134
+ if (input === 'p' || input === 'P') {
135
+ return { execute: () => executePaste(input === 'p', count, ctx) }
136
+ }
137
+ if (input === 'D') {
138
+ return { execute: () => executeOperatorMotion('delete', '$', 1, ctx) }
139
+ }
140
+ if (input === 'C') {
141
+ return { execute: () => executeOperatorMotion('change', '$', 1, ctx) }
142
+ }
143
+ if (input === 'Y') {
144
+ return { execute: () => executeLineOp('yank', count, ctx) }
145
+ }
146
+ if (input === 'G') {
147
+ return {
148
+ execute: () => {
149
+ // count=1 means no count given, go to last line
150
+ // otherwise go to line N
151
+ if (count === 1) {
152
+ ctx.setOffset(ctx.cursor.startOfLastLine().offset)
153
+ } else {
154
+ ctx.setOffset(ctx.cursor.goToLine(count).offset)
155
+ }
156
+ },
157
+ }
158
+ }
159
+ if (input === '.') {
160
+ return { execute: () => ctx.onDotRepeat?.() }
161
+ }
162
+ if (input === ';' || input === ',') {
163
+ return { execute: () => executeRepeatFind(input === ',', count, ctx) }
164
+ }
165
+ if (input === 'u') {
166
+ return { execute: () => ctx.onUndo?.() }
167
+ }
168
+ if (input === 'i') {
169
+ return { execute: () => ctx.enterInsert(ctx.cursor.offset) }
170
+ }
171
+ if (input === 'I') {
172
+ return {
173
+ execute: () =>
174
+ ctx.enterInsert(ctx.cursor.firstNonBlankInLogicalLine().offset),
175
+ }
176
+ }
177
+ if (input === 'a') {
178
+ return {
179
+ execute: () => {
180
+ const newOffset = ctx.cursor.isAtEnd()
181
+ ? ctx.cursor.offset
182
+ : ctx.cursor.right().offset
183
+ ctx.enterInsert(newOffset)
184
+ },
185
+ }
186
+ }
187
+ if (input === 'A') {
188
+ return {
189
+ execute: () => ctx.enterInsert(ctx.cursor.endOfLogicalLine().offset),
190
+ }
191
+ }
192
+ if (input === 'o') {
193
+ return { execute: () => executeOpenLine('below', ctx) }
194
+ }
195
+ if (input === 'O') {
196
+ return { execute: () => executeOpenLine('above', ctx) }
197
+ }
198
+
199
+ return null
200
+ }
201
+
202
+ /**
203
+ * Handle operator input (motion, find, text object scope).
204
+ * Returns null if input is not recognized.
205
+ */
206
+ function handleOperatorInput(
207
+ op: Operator,
208
+ count: number,
209
+ input: string,
210
+ ctx: TransitionContext,
211
+ ): TransitionResult | null {
212
+ if (isTextObjScopeKey(input)) {
213
+ return {
214
+ next: {
215
+ type: 'operatorTextObj',
216
+ op,
217
+ count,
218
+ scope: TEXT_OBJ_SCOPES[input],
219
+ },
220
+ }
221
+ }
222
+
223
+ if (FIND_KEYS.has(input)) {
224
+ return {
225
+ next: { type: 'operatorFind', op, count, find: input as FindType },
226
+ }
227
+ }
228
+
229
+ if (SIMPLE_MOTIONS.has(input)) {
230
+ return { execute: () => executeOperatorMotion(op, input, count, ctx) }
231
+ }
232
+
233
+ if (input === 'G') {
234
+ return { execute: () => executeOperatorG(op, count, ctx) }
235
+ }
236
+
237
+ if (input === 'g') {
238
+ return { next: { type: 'operatorG', op, count } }
239
+ }
240
+
241
+ return null
242
+ }
243
+
244
+ // ============================================================================
245
+ // Transition Functions - One per state type
246
+ // ============================================================================
247
+
248
+ function fromIdle(input: string, ctx: TransitionContext): TransitionResult {
249
+ // 0 is line-start motion, not a count prefix
250
+ if (/[1-9]/.test(input)) {
251
+ return { next: { type: 'count', digits: input } }
252
+ }
253
+ if (input === '0') {
254
+ return {
255
+ execute: () => ctx.setOffset(ctx.cursor.startOfLogicalLine().offset),
256
+ }
257
+ }
258
+
259
+ const result = handleNormalInput(input, 1, ctx)
260
+ if (result) return result
261
+
262
+ return {}
263
+ }
264
+
265
+ function fromCount(
266
+ state: { type: 'count'; digits: string },
267
+ input: string,
268
+ ctx: TransitionContext,
269
+ ): TransitionResult {
270
+ if (/[0-9]/.test(input)) {
271
+ const newDigits = state.digits + input
272
+ const count = Math.min(parseInt(newDigits, 10), MAX_VIM_COUNT)
273
+ return { next: { type: 'count', digits: String(count) } }
274
+ }
275
+
276
+ const count = parseInt(state.digits, 10)
277
+ const result = handleNormalInput(input, count, ctx)
278
+ if (result) return result
279
+
280
+ return { next: { type: 'idle' } }
281
+ }
282
+
283
+ function fromOperator(
284
+ state: { type: 'operator'; op: Operator; count: number },
285
+ input: string,
286
+ ctx: TransitionContext,
287
+ ): TransitionResult {
288
+ // dd, cc, yy = line operation
289
+ if (input === state.op[0]) {
290
+ return { execute: () => executeLineOp(state.op, state.count, ctx) }
291
+ }
292
+
293
+ if (/[0-9]/.test(input)) {
294
+ return {
295
+ next: {
296
+ type: 'operatorCount',
297
+ op: state.op,
298
+ count: state.count,
299
+ digits: input,
300
+ },
301
+ }
302
+ }
303
+
304
+ const result = handleOperatorInput(state.op, state.count, input, ctx)
305
+ if (result) return result
306
+
307
+ return { next: { type: 'idle' } }
308
+ }
309
+
310
+ function fromOperatorCount(
311
+ state: {
312
+ type: 'operatorCount'
313
+ op: Operator
314
+ count: number
315
+ digits: string
316
+ },
317
+ input: string,
318
+ ctx: TransitionContext,
319
+ ): TransitionResult {
320
+ if (/[0-9]/.test(input)) {
321
+ const newDigits = state.digits + input
322
+ const parsedDigits = Math.min(parseInt(newDigits, 10), MAX_VIM_COUNT)
323
+ return { next: { ...state, digits: String(parsedDigits) } }
324
+ }
325
+
326
+ const motionCount = parseInt(state.digits, 10)
327
+ const effectiveCount = state.count * motionCount
328
+ const result = handleOperatorInput(state.op, effectiveCount, input, ctx)
329
+ if (result) return result
330
+
331
+ return { next: { type: 'idle' } }
332
+ }
333
+
334
+ function fromOperatorFind(
335
+ state: {
336
+ type: 'operatorFind'
337
+ op: Operator
338
+ count: number
339
+ find: FindType
340
+ },
341
+ input: string,
342
+ ctx: TransitionContext,
343
+ ): TransitionResult {
344
+ return {
345
+ execute: () =>
346
+ executeOperatorFind(state.op, state.find, input, state.count, ctx),
347
+ }
348
+ }
349
+
350
+ function fromOperatorTextObj(
351
+ state: {
352
+ type: 'operatorTextObj'
353
+ op: Operator
354
+ count: number
355
+ scope: TextObjScope
356
+ },
357
+ input: string,
358
+ ctx: TransitionContext,
359
+ ): TransitionResult {
360
+ if (TEXT_OBJ_TYPES.has(input)) {
361
+ return {
362
+ execute: () =>
363
+ executeOperatorTextObj(state.op, state.scope, input, state.count, ctx),
364
+ }
365
+ }
366
+ return { next: { type: 'idle' } }
367
+ }
368
+
369
+ function fromFind(
370
+ state: { type: 'find'; find: FindType; count: number },
371
+ input: string,
372
+ ctx: TransitionContext,
373
+ ): TransitionResult {
374
+ return {
375
+ execute: () => {
376
+ const result = ctx.cursor.findCharacter(input, state.find, state.count)
377
+ if (result !== null) {
378
+ ctx.setOffset(result)
379
+ ctx.setLastFind(state.find, input)
380
+ }
381
+ },
382
+ }
383
+ }
384
+
385
+ function fromG(
386
+ state: { type: 'g'; count: number },
387
+ input: string,
388
+ ctx: TransitionContext,
389
+ ): TransitionResult {
390
+ if (input === 'j' || input === 'k') {
391
+ return {
392
+ execute: () => {
393
+ const target = resolveMotion(`g${input}`, ctx.cursor, state.count)
394
+ ctx.setOffset(target.offset)
395
+ },
396
+ }
397
+ }
398
+ if (input === 'g') {
399
+ // If count provided (e.g., 5gg), go to that line. Otherwise go to first line.
400
+ if (state.count > 1) {
401
+ return {
402
+ execute: () => {
403
+ const lines = ctx.text.split('\n')
404
+ const targetLine = Math.min(state.count - 1, lines.length - 1)
405
+ let offset = 0
406
+ for (let i = 0; i < targetLine; i++) {
407
+ offset += (lines[i]?.length ?? 0) + 1 // +1 for newline
408
+ }
409
+ ctx.setOffset(offset)
410
+ },
411
+ }
412
+ }
413
+ return {
414
+ execute: () => ctx.setOffset(ctx.cursor.startOfFirstLine().offset),
415
+ }
416
+ }
417
+ return { next: { type: 'idle' } }
418
+ }
419
+
420
+ function fromOperatorG(
421
+ state: { type: 'operatorG'; op: Operator; count: number },
422
+ input: string,
423
+ ctx: TransitionContext,
424
+ ): TransitionResult {
425
+ if (input === 'j' || input === 'k') {
426
+ return {
427
+ execute: () =>
428
+ executeOperatorMotion(state.op, `g${input}`, state.count, ctx),
429
+ }
430
+ }
431
+ if (input === 'g') {
432
+ return { execute: () => executeOperatorGg(state.op, state.count, ctx) }
433
+ }
434
+ // Any other input cancels the operator
435
+ return { next: { type: 'idle' } }
436
+ }
437
+
438
+ function fromReplace(
439
+ state: { type: 'replace'; count: number },
440
+ input: string,
441
+ ctx: TransitionContext,
442
+ ): TransitionResult {
443
+ // Backspace/Delete arrive as empty input in literal-char states. In vim,
444
+ // r<BS> cancels the replace; without this guard, executeReplace("") would
445
+ // delete the character under the cursor instead.
446
+ if (input === '') return { next: { type: 'idle' } }
447
+ return { execute: () => executeReplace(input, state.count, ctx) }
448
+ }
449
+
450
+ function fromIndent(
451
+ state: { type: 'indent'; dir: '>' | '<'; count: number },
452
+ input: string,
453
+ ctx: TransitionContext,
454
+ ): TransitionResult {
455
+ if (input === state.dir) {
456
+ return { execute: () => executeIndent(state.dir, state.count, ctx) }
457
+ }
458
+ return { next: { type: 'idle' } }
459
+ }
460
+
461
+ // ============================================================================
462
+ // Helper functions for special commands
463
+ // ============================================================================
464
+
465
+ function executeRepeatFind(
466
+ reverse: boolean,
467
+ count: number,
468
+ ctx: TransitionContext,
469
+ ): void {
470
+ const lastFind = ctx.getLastFind()
471
+ if (!lastFind) return
472
+
473
+ // Determine the effective find type based on reverse
474
+ let findType = lastFind.type
475
+ if (reverse) {
476
+ // Flip the direction
477
+ const flipMap: Record<FindType, FindType> = {
478
+ f: 'F',
479
+ F: 'f',
480
+ t: 'T',
481
+ T: 't',
482
+ }
483
+ findType = flipMap[findType]
484
+ }
485
+
486
+ const result = ctx.cursor.findCharacter(lastFind.char, findType, count)
487
+ if (result !== null) {
488
+ ctx.setOffset(result)
489
+ }
490
+ }