@kirosnn/mosaic 0.71.0 → 0.74.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 (79) hide show
  1. package/README.md +1 -5
  2. package/package.json +4 -2
  3. package/src/agent/Agent.ts +353 -131
  4. package/src/agent/context.ts +4 -4
  5. package/src/agent/prompts/systemPrompt.ts +15 -6
  6. package/src/agent/prompts/toolsPrompt.ts +136 -10
  7. package/src/agent/provider/anthropic.ts +100 -100
  8. package/src/agent/provider/google.ts +102 -102
  9. package/src/agent/provider/mistral.ts +95 -95
  10. package/src/agent/provider/ollama.ts +77 -60
  11. package/src/agent/provider/openai.ts +42 -38
  12. package/src/agent/provider/rateLimit.ts +178 -0
  13. package/src/agent/provider/xai.ts +99 -99
  14. package/src/agent/tools/definitions.ts +19 -9
  15. package/src/agent/tools/executor.ts +95 -85
  16. package/src/agent/tools/exploreExecutor.ts +8 -10
  17. package/src/agent/tools/grep.ts +30 -29
  18. package/src/agent/tools/question.ts +7 -1
  19. package/src/agent/types.ts +9 -8
  20. package/src/components/App.tsx +45 -45
  21. package/src/components/CustomInput.tsx +214 -36
  22. package/src/components/Main.tsx +552 -339
  23. package/src/components/Setup.tsx +1 -1
  24. package/src/components/Welcome.tsx +1 -1
  25. package/src/components/main/ApprovalPanel.tsx +4 -3
  26. package/src/components/main/ChatPage.tsx +858 -675
  27. package/src/components/main/HomePage.tsx +53 -38
  28. package/src/components/main/QuestionPanel.tsx +52 -7
  29. package/src/components/main/ThinkingIndicator.tsx +2 -1
  30. package/src/index.tsx +50 -20
  31. package/src/mcp/approvalPolicy.ts +156 -0
  32. package/src/mcp/cli/add.ts +185 -0
  33. package/src/mcp/cli/doctor.ts +74 -0
  34. package/src/mcp/cli/index.ts +85 -0
  35. package/src/mcp/cli/list.ts +50 -0
  36. package/src/mcp/cli/logs.ts +24 -0
  37. package/src/mcp/cli/manage.ts +99 -0
  38. package/src/mcp/cli/show.ts +53 -0
  39. package/src/mcp/cli/tools.ts +77 -0
  40. package/src/mcp/config.ts +234 -0
  41. package/src/mcp/index.ts +80 -0
  42. package/src/mcp/processManager.ts +304 -0
  43. package/src/mcp/rateLimiter.ts +50 -0
  44. package/src/mcp/registry.ts +151 -0
  45. package/src/mcp/schemaConverter.ts +100 -0
  46. package/src/mcp/servers/navigation/browser.ts +151 -0
  47. package/src/mcp/servers/navigation/index.ts +23 -0
  48. package/src/mcp/servers/navigation/tools.ts +263 -0
  49. package/src/mcp/servers/navigation/types.ts +17 -0
  50. package/src/mcp/servers/navigation/utils.ts +20 -0
  51. package/src/mcp/toolCatalog.ts +182 -0
  52. package/src/mcp/types.ts +116 -0
  53. package/src/utils/approvalBridge.ts +17 -5
  54. package/src/utils/commands/compact.ts +30 -0
  55. package/src/utils/commands/echo.ts +1 -1
  56. package/src/utils/commands/index.ts +4 -6
  57. package/src/utils/commands/new.ts +15 -0
  58. package/src/utils/commands/types.ts +3 -0
  59. package/src/utils/config.ts +3 -1
  60. package/src/utils/diffRendering.tsx +1 -3
  61. package/src/utils/exploreBridge.ts +10 -0
  62. package/src/utils/markdown.tsx +220 -122
  63. package/src/utils/models.ts +31 -9
  64. package/src/utils/questionBridge.ts +36 -1
  65. package/src/utils/tokenEstimator.ts +32 -0
  66. package/src/utils/toolFormatting.ts +317 -7
  67. package/src/web/app.tsx +72 -72
  68. package/src/web/components/HomePage.tsx +7 -7
  69. package/src/web/components/MessageItem.tsx +66 -35
  70. package/src/web/components/QuestionPanel.tsx +72 -12
  71. package/src/web/components/Sidebar.tsx +0 -2
  72. package/src/web/components/ThinkingIndicator.tsx +1 -0
  73. package/src/web/server.tsx +767 -683
  74. package/src/utils/commands/redo.ts +0 -74
  75. package/src/utils/commands/sessions.ts +0 -129
  76. package/src/utils/commands/undo.ts +0 -75
  77. package/src/utils/undoRedo.ts +0 -429
  78. package/src/utils/undoRedoBridge.ts +0 -45
  79. package/src/utils/undoRedoDb.ts +0 -338
@@ -23,12 +23,15 @@ interface CustomInputProps {
23
23
  pasteRequestId?: number
24
24
  disableHistory?: boolean
25
25
  submitDisabled?: boolean
26
+ maxWidth?: number
26
27
  }
27
28
 
28
- export function CustomInput({ onSubmit, placeholder = '', password = false, focused = true, pasteRequestId = 0, disableHistory = false, submitDisabled = false }: CustomInputProps) {
29
+ export function CustomInput({ onSubmit, placeholder = '', password = false, focused = true, pasteRequestId = 0, disableHistory = false, submitDisabled = false, maxWidth }: CustomInputProps) {
29
30
  const [value, setValue] = useState('')
30
31
  const [cursorPosition, setCursorPosition] = useState(0)
31
32
  const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80)
33
+ const [selectionStart, setSelectionStart] = useState<number | null>(null)
34
+ const [selectionEnd, setSelectionEnd] = useState<number | null>(null)
32
35
  const [pasteBuffer, setPasteBuffer] = useState('')
33
36
  const [inPasteMode, setInPasteMode] = useState(false)
34
37
  const [historyIndex, setHistoryIndex] = useState(-1)
@@ -45,6 +48,8 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
45
48
  const lastPasteUndoRef = useRef<{ prevValue: string; prevCursor: number; nextValue: string; nextCursor: number } | null>(null)
46
49
  const valueRef = useRef(value)
47
50
  const cursorPositionRef = useRef(cursorPosition)
51
+ const selectionStartRef = useRef<number | null>(selectionStart)
52
+ const selectionEndRef = useRef<number | null>(selectionEnd)
48
53
 
49
54
  const renderer = useRenderer()
50
55
 
@@ -135,9 +140,13 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
135
140
  const normalized = normalizePastedText(pastedText)
136
141
  if (!normalized) return
137
142
 
138
- const insertAt = cursorPositionRef.current
139
143
  const prevValue = valueRef.current
140
- const nextValue = prevValue.slice(0, insertAt) + normalized + prevValue.slice(insertAt)
144
+ const selStart = selectionStartRef.current
145
+ const selEnd = selectionEndRef.current
146
+ const hasSelection = typeof selStart === 'number' && typeof selEnd === 'number' && selEnd > selStart
147
+ const insertAt = hasSelection ? selStart! : cursorPositionRef.current
148
+ const deleteUntil = hasSelection ? selEnd! : insertAt
149
+ const nextValue = prevValue.slice(0, insertAt) + normalized + prevValue.slice(deleteUntil)
141
150
  lastPasteUndoRef.current = {
142
151
  prevValue,
143
152
  prevCursor: insertAt,
@@ -146,6 +155,10 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
146
155
  }
147
156
  setValue(nextValue)
148
157
  setCursorPosition(insertAt + normalized.length)
158
+ if (hasSelection) {
159
+ setSelectionStart(null)
160
+ setSelectionEnd(null)
161
+ }
149
162
  pasteFlagRef.current = true
150
163
  pastedContentRef.current = normalized
151
164
  }
@@ -169,6 +182,8 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
169
182
  setCursorPosition(normalized.length)
170
183
  setHistoryIndex(-1)
171
184
  desiredCursorColRef.current = null
185
+ setSelectionStart(null)
186
+ setSelectionEnd(null)
172
187
  } catch (error) {
173
188
  } finally {
174
189
  try {
@@ -190,6 +205,14 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
190
205
  cursorPositionRef.current = cursorPosition
191
206
  }, [cursorPosition])
192
207
 
208
+ useEffect(() => {
209
+ selectionStartRef.current = selectionStart
210
+ }, [selectionStart])
211
+
212
+ useEffect(() => {
213
+ selectionEndRef.current = selectionEnd
214
+ }, [selectionEnd])
215
+
193
216
  useEffect(() => {
194
217
  const handleResize = () => {
195
218
  setTerminalWidth(process.stdout.columns || 80)
@@ -267,15 +290,23 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
267
290
  }
268
291
  }, [focused, renderer.keyInput])
269
292
 
270
- const wrapTextWithCursor = (text: string, cursorPos: number, maxWidth: number): { lines: string[], cursorLine: number, cursorCol: number } => {
293
+ const wrapTextWithCursor = (text: string, cursorPos: number, maxWidth: number): { lineStarts: number[], lineLengths: number[], cursorLine: number, cursorCol: number } => {
271
294
  const safeCursorPos = Math.max(0, Math.min(text.length, cursorPos))
272
- const lines: string[] = ['']
295
+ const lineStarts: number[] = [0]
296
+ const lineLengths: number[] = [0]
273
297
  let lineIndex = 0
274
298
  let col = 0
275
299
  let cursorLine = 0
276
300
  let cursorCol = 0
277
301
 
278
302
  for (let i = 0; i <= text.length; i += 1) {
303
+ if (col >= maxWidth) {
304
+ lineStarts.push(i)
305
+ lineLengths.push(0)
306
+ lineIndex += 1
307
+ col = 0
308
+ }
309
+
279
310
  if (i === safeCursorPos) {
280
311
  cursorLine = lineIndex
281
312
  cursorCol = col
@@ -285,23 +316,28 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
285
316
 
286
317
  const ch = text[i]!
287
318
  if (ch === '\n') {
288
- lines.push('')
319
+ lineStarts.push(i + 1)
320
+ lineLengths.push(0)
289
321
  lineIndex += 1
290
322
  col = 0
291
323
  continue
292
324
  }
293
325
 
294
- if (col >= maxWidth) {
295
- lines.push('')
296
- lineIndex += 1
297
- col = 0
298
- }
299
-
300
- lines[lineIndex] = (lines[lineIndex] || '') + ch
326
+ lineLengths[lineIndex] = (lineLengths[lineIndex] || 0) + 1
301
327
  col += 1
302
328
  }
303
329
 
304
- return { lines, cursorLine, cursorCol }
330
+ return { lineStarts, lineLengths, cursorLine, cursorCol }
331
+ }
332
+
333
+ const buildDisplayLines = (displayText: string, lineStarts: number[], lineLengths: number[]) => {
334
+ const lines: string[] = []
335
+ for (let i = 0; i < lineStarts.length; i += 1) {
336
+ const start = lineStarts[i] ?? 0
337
+ const len = lineLengths[i] ?? 0
338
+ lines.push(displayText.slice(start, start + len))
339
+ }
340
+ return lines
305
341
  }
306
342
 
307
343
  useKeyboard((key) => {
@@ -342,6 +378,27 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
342
378
  lastPasteUndoRef.current = null
343
379
  pasteFlagRef.current = false
344
380
  pastedContentRef.current = ''
381
+ setSelectionStart(null)
382
+ setSelectionEnd(null)
383
+ }
384
+ return
385
+ }
386
+
387
+ if (key.name === 'a' && (key.ctrl || key.meta)) {
388
+ if (value.length > 0) {
389
+ const selStart = selectionStartRef.current
390
+ const selEnd = selectionEndRef.current
391
+ const hasSelection = typeof selStart === 'number' && typeof selEnd === 'number' && selEnd > selStart
392
+ const fullSelection = hasSelection && selStart === 0 && selEnd === value.length
393
+ if (fullSelection) {
394
+ setSelectionStart(null)
395
+ setSelectionEnd(null)
396
+ setCursorPosition(value.length)
397
+ } else {
398
+ setSelectionStart(0)
399
+ setSelectionEnd(value.length)
400
+ setCursorPosition(value.length)
401
+ }
345
402
  }
346
403
  return
347
404
  }
@@ -361,6 +418,8 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
361
418
  pasteFlagRef.current = false
362
419
  pastedContentRef.current = ''
363
420
  desiredCursorColRef.current = null
421
+ setSelectionStart(null)
422
+ setSelectionEnd(null)
364
423
  return
365
424
  }
366
425
 
@@ -383,34 +442,63 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
383
442
  pasteFlagRef.current = false
384
443
  pastedContentRef.current = ''
385
444
  desiredCursorColRef.current = null
445
+ setSelectionStart(null)
446
+ setSelectionEnd(null)
386
447
  } else if (key.name === 'backspace') {
387
448
  desiredCursorColRef.current = null
449
+ const selStart = selectionStartRef.current
450
+ const selEnd = selectionEndRef.current
451
+ if (typeof selStart === 'number' && typeof selEnd === 'number' && selEnd > selStart) {
452
+ const prev = valueRef.current
453
+ const nextValue = prev.slice(0, selStart) + prev.slice(selEnd)
454
+ setValue(nextValue)
455
+ setCursorPosition(selStart)
456
+ setSelectionStart(null)
457
+ setSelectionEnd(null)
458
+ return
459
+ }
388
460
  if (cursorPosition > 0) {
389
461
  setValue(prev => prev.slice(0, cursorPosition - 1) + prev.slice(cursorPosition))
390
462
  setCursorPosition(prev => prev - 1)
391
463
  }
392
464
  } else if (key.name === 'delete') {
393
465
  desiredCursorColRef.current = null
466
+ const selStart = selectionStartRef.current
467
+ const selEnd = selectionEndRef.current
468
+ if (typeof selStart === 'number' && typeof selEnd === 'number' && selEnd > selStart) {
469
+ const prev = valueRef.current
470
+ const nextValue = prev.slice(0, selStart) + prev.slice(selEnd)
471
+ setValue(nextValue)
472
+ setCursorPosition(selStart)
473
+ setSelectionStart(null)
474
+ setSelectionEnd(null)
475
+ return
476
+ }
394
477
  if (key.ctrl || key.meta) {
395
478
  setValue('')
396
479
  setCursorPosition(0)
397
480
  pasteFlagRef.current = false
398
481
  pastedContentRef.current = ''
482
+ setSelectionStart(null)
483
+ setSelectionEnd(null)
399
484
  } else if (cursorPosition < value.length) {
400
485
  setValue(prev => prev.slice(0, cursorPosition) + prev.slice(cursorPosition + 1))
401
486
  }
402
487
  } else if (key.name === 'up') {
403
488
  const lineWidth = Math.max(10, terminalWidth - 4)
404
- const { lines: displayLines, cursorLine: currentCursorLine, cursorCol: currentCursorCol } = wrapTextWithCursor(value, cursorPosition, lineWidth)
489
+ const { lineLengths, cursorLine: currentCursorLine, cursorCol: currentCursorCol, lineStarts } = wrapTextWithCursor(value, cursorPosition, lineWidth)
405
490
  if (currentCursorLine > 0) {
406
491
  if (desiredCursorColRef.current === null) {
407
492
  desiredCursorColRef.current = currentCursorCol
408
493
  }
409
494
  const targetLine = currentCursorLine - 1
410
495
  const targetCol = desiredCursorColRef.current
411
- const targetLineLen = displayLines[targetLine]!.length
412
- const newDisplayPos = (targetLine * lineWidth) + Math.min(targetCol, targetLineLen)
413
- setCursorPosition(Math.min(value.length, newDisplayPos))
496
+ const targetLineLen = lineLengths[targetLine] ?? 0
497
+ const lineStart = lineStarts[targetLine] ?? 0
498
+ const newCursorPos = lineStart + Math.min(targetCol, targetLineLen)
499
+ setCursorPosition(Math.min(value.length, newCursorPos))
500
+ setSelectionStart(null)
501
+ setSelectionEnd(null)
414
502
  return
415
503
  }
416
504
 
@@ -425,24 +513,31 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
425
513
  setHistoryIndex(newIndex)
426
514
  setValue(inputHistory[newIndex]!)
427
515
  setCursorPosition(inputHistory[newIndex]!.length)
516
+ setSelectionStart(null)
517
+ setSelectionEnd(null)
428
518
  } else if (historyIndex > 0) {
429
519
  const newIndex = historyIndex - 1
430
520
  setHistoryIndex(newIndex)
431
521
  setValue(inputHistory[newIndex]!)
432
522
  setCursorPosition(inputHistory[newIndex]!.length)
523
+ setSelectionStart(null)
524
+ setSelectionEnd(null)
433
525
  }
434
526
  } else if (key.name === 'down') {
435
527
  const lineWidth = Math.max(10, terminalWidth - 4)
436
- const { lines: displayLines, cursorLine: currentCursorLine, cursorCol: currentCursorCol } = wrapTextWithCursor(value, cursorPosition, lineWidth)
437
- if (currentCursorLine < displayLines.length - 1) {
528
+ const { lineLengths, cursorLine: currentCursorLine, cursorCol: currentCursorCol, lineStarts } = wrapTextWithCursor(value, cursorPosition, lineWidth)
529
+ if (currentCursorLine < lineStarts.length - 1) {
438
530
  if (desiredCursorColRef.current === null) {
439
531
  desiredCursorColRef.current = currentCursorCol
440
532
  }
441
533
  const targetLine = currentCursorLine + 1
442
534
  const targetCol = desiredCursorColRef.current
443
- const targetLineLen = displayLines[targetLine]!.length
444
- const newDisplayPos = (targetLine * lineWidth) + Math.min(targetCol, targetLineLen)
445
- setCursorPosition(Math.min(value.length, newDisplayPos))
535
+ const targetLineLen = lineLengths[targetLine] ?? 0
536
+ const lineStart = lineStarts[targetLine] ?? value.length
537
+ const newCursorPos = lineStart + Math.min(targetCol, targetLineLen)
538
+ setCursorPosition(Math.min(value.length, newCursorPos))
539
+ setSelectionStart(null)
540
+ setSelectionEnd(null)
446
541
  return
447
542
  }
448
543
 
@@ -456,31 +551,56 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
456
551
  setHistoryIndex(newIndex)
457
552
  setValue(inputHistory[newIndex]!)
458
553
  setCursorPosition(inputHistory[newIndex]!.length)
554
+ setSelectionStart(null)
555
+ setSelectionEnd(null)
459
556
  } else {
460
557
  setHistoryIndex(-1)
461
558
  setValue(currentInput)
462
559
  setCursorPosition(currentInput.length)
560
+ setSelectionStart(null)
561
+ setSelectionEnd(null)
463
562
  }
464
563
  } else if (key.name === 'left') {
465
564
  desiredCursorColRef.current = null
466
565
  setCursorPosition(prev => Math.max(0, prev - 1))
566
+ setSelectionStart(null)
567
+ setSelectionEnd(null)
467
568
  } else if (key.name === 'right') {
468
569
  desiredCursorColRef.current = null
469
570
  setCursorPosition(prev => Math.min(value.length, prev + 1))
571
+ setSelectionStart(null)
572
+ setSelectionEnd(null)
470
573
  } else if (key.name === 'home') {
471
574
  desiredCursorColRef.current = null
472
575
  setCursorPosition(0)
576
+ setSelectionStart(null)
577
+ setSelectionEnd(null)
473
578
  } else if (key.name === 'end') {
474
579
  desiredCursorColRef.current = null
475
580
  setCursorPosition(value.length)
581
+ setSelectionStart(null)
582
+ setSelectionEnd(null)
476
583
  } else if (key.sequence && key.sequence.length > 1 && !key.ctrl && !key.meta && !key.name) {
477
584
  addPastedBlock(key.sequence)
478
585
  setHistoryIndex(-1)
479
586
  desiredCursorColRef.current = null
587
+ setSelectionStart(null)
588
+ setSelectionEnd(null)
480
589
  } else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
481
590
  const char = key.sequence
482
- setValue(prev => prev.slice(0, cursorPosition) + char + prev.slice(cursorPosition))
483
- setCursorPosition(prev => prev + char.length)
591
+ const selStart = selectionStartRef.current
592
+ const selEnd = selectionEndRef.current
593
+ if (typeof selStart === 'number' && typeof selEnd === 'number' && selEnd > selStart) {
594
+ const prev = valueRef.current
595
+ const nextValue = prev.slice(0, selStart) + char + prev.slice(selEnd)
596
+ setValue(nextValue)
597
+ setCursorPosition(selStart + char.length)
598
+ setSelectionStart(null)
599
+ setSelectionEnd(null)
600
+ } else {
601
+ setValue(prev => prev.slice(0, cursorPosition) + char + prev.slice(cursorPosition))
602
+ setCursorPosition(prev => prev + char.length)
603
+ }
484
604
  setHistoryIndex(-1)
485
605
  desiredCursorColRef.current = null
486
606
  }
@@ -489,7 +609,7 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
489
609
  const displayValue = password && value ? Array.from(value, (char) => (char === '\n' ? '\n' : '•')).join('') : value
490
610
  const isEmpty = value.length === 0
491
611
 
492
- const lineWidth = Math.max(10, terminalWidth - 4)
612
+ const lineWidth = Math.max(10, maxWidth ?? (terminalWidth - 4))
493
613
 
494
614
  if (isEmpty) {
495
615
  if (!placeholder) {
@@ -512,21 +632,79 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
512
632
  )
513
633
  }
514
634
 
515
- const { lines, cursorLine, cursorCol } = wrapTextWithCursor(displayValue, cursorPosition, lineWidth)
635
+ const { lineStarts, lineLengths, cursorLine, cursorCol } = wrapTextWithCursor(value, cursorPosition, lineWidth)
636
+ const lines = buildDisplayLines(displayValue, lineStarts, lineLengths)
637
+ const selectionRange = (() => {
638
+ if (typeof selectionStart === 'number' && typeof selectionEnd === 'number') {
639
+ const start = Math.max(0, Math.min(selectionStart, selectionEnd))
640
+ const end = Math.max(0, Math.max(selectionStart, selectionEnd))
641
+ if (end > start) return { start, end }
642
+ }
643
+ return null
644
+ })()
645
+
646
+ const buildSelectionSegments = (text: string, absStart: number) => {
647
+ if (!selectionRange || text.length === 0) {
648
+ return [{ text, selected: false }]
649
+ }
650
+ const segStart = absStart
651
+ const segEnd = absStart + text.length
652
+ if (selectionRange.end <= segStart || selectionRange.start >= segEnd) {
653
+ return [{ text, selected: false }]
654
+ }
655
+ const parts: Array<{ text: string; selected: boolean }> = []
656
+ if (selectionRange.start > segStart) {
657
+ parts.push({ text: text.slice(0, selectionRange.start - segStart), selected: false })
658
+ }
659
+ const selFrom = Math.max(selectionRange.start, segStart) - segStart
660
+ const selTo = Math.min(selectionRange.end, segEnd) - segStart
661
+ parts.push({ text: text.slice(selFrom, selTo), selected: true })
662
+ if (selectionRange.end < segEnd) {
663
+ parts.push({ text: text.slice(selTo), selected: false })
664
+ }
665
+ return parts.filter(p => p.text.length > 0)
666
+ }
516
667
 
517
668
  return (
518
669
  <box flexDirection="column" flexGrow={1} width="100%">
519
670
  {lines.map((line, lineIndex) => (
520
671
  <box key={lineIndex} flexDirection="row">
521
- {lineIndex === cursorLine ? (
522
- <>
523
- {line.slice(0, cursorCol) && <text>{line.slice(0, cursorCol)}</text>}
524
- <text fg="black" bg="white">{line[cursorCol] || ' '}</text>
525
- {line.slice(cursorCol + 1) && <text>{line.slice(cursorCol + 1)}</text>}
526
- </>
527
- ) : (
528
- <text>{line || ' '}</text>
529
- )}
672
+ {(() => {
673
+ const lineStart = lineStarts[lineIndex] ?? 0
674
+ if (lineIndex === cursorLine) {
675
+ const safeCursorCol = Math.max(0, Math.min(line.length, cursorCol))
676
+ const before = line.slice(0, safeCursorCol)
677
+ const cursorChar = line[safeCursorCol] || ' '
678
+ const after = line.slice(safeCursorCol + 1)
679
+ const beforeSegs = buildSelectionSegments(before, lineStart)
680
+ const afterSegs = buildSelectionSegments(after, lineStart + safeCursorCol + 1)
681
+ return (
682
+ <>
683
+ {beforeSegs.map((seg, i) => (
684
+ <text key={`b-${i}`} fg="white" bg={seg.selected ? "#3a3a3a" : undefined}>
685
+ {seg.text}
686
+ </text>
687
+ ))}
688
+ <text fg="black" bg="white">{cursorChar}</text>
689
+ {afterSegs.map((seg, i) => (
690
+ <text key={`a-${i}`} fg="white" bg={seg.selected ? "#3a3a3a" : undefined}>
691
+ {seg.text}
692
+ </text>
693
+ ))}
694
+ </>
695
+ )
696
+ }
697
+ const segs = buildSelectionSegments(line || ' ', lineStart)
698
+ return (
699
+ <>
700
+ {segs.map((seg, i) => (
701
+ <text key={`l-${i}`} fg="white" bg={seg.selected ? "#3a3a3a" : undefined}>
702
+ {seg.text}
703
+ </text>
704
+ ))}
705
+ </>
706
+ )
707
+ })()}
530
708
  </box>
531
709
  ))}
532
710
  </box>