@kirosnn/mosaic 0.0.91 → 0.71.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 (56) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -2
  3. package/package.json +52 -47
  4. package/src/agent/prompts/systemPrompt.ts +198 -68
  5. package/src/agent/prompts/toolsPrompt.ts +217 -135
  6. package/src/agent/provider/anthropic.ts +19 -15
  7. package/src/agent/provider/google.ts +21 -17
  8. package/src/agent/provider/ollama.ts +80 -41
  9. package/src/agent/provider/openai.ts +107 -67
  10. package/src/agent/provider/reasoning.ts +29 -0
  11. package/src/agent/provider/xai.ts +19 -15
  12. package/src/agent/tools/definitions.ts +9 -5
  13. package/src/agent/tools/executor.ts +655 -46
  14. package/src/agent/tools/exploreExecutor.ts +12 -12
  15. package/src/agent/tools/fetch.ts +58 -0
  16. package/src/agent/tools/glob.ts +20 -4
  17. package/src/agent/tools/grep.ts +62 -8
  18. package/src/agent/tools/plan.ts +27 -0
  19. package/src/agent/tools/read.ts +2 -0
  20. package/src/agent/types.ts +6 -6
  21. package/src/components/App.tsx +67 -25
  22. package/src/components/CustomInput.tsx +274 -68
  23. package/src/components/Main.tsx +323 -168
  24. package/src/components/ShortcutsModal.tsx +11 -8
  25. package/src/components/main/ChatPage.tsx +217 -58
  26. package/src/components/main/HomePage.tsx +5 -1
  27. package/src/components/main/ThinkingIndicator.tsx +11 -1
  28. package/src/components/main/types.ts +11 -10
  29. package/src/index.tsx +3 -5
  30. package/src/utils/approvalBridge.ts +29 -8
  31. package/src/utils/approvalModeBridge.ts +17 -0
  32. package/src/utils/commands/approvals.ts +48 -0
  33. package/src/utils/commands/image.ts +109 -0
  34. package/src/utils/commands/index.ts +5 -1
  35. package/src/utils/diffRendering.tsx +13 -14
  36. package/src/utils/history.ts +82 -40
  37. package/src/utils/imageBridge.ts +28 -0
  38. package/src/utils/images.ts +31 -0
  39. package/src/utils/models.ts +0 -7
  40. package/src/utils/notificationBridge.ts +23 -0
  41. package/src/utils/toolFormatting.ts +162 -43
  42. package/src/web/app.tsx +94 -34
  43. package/src/web/assets/css/ChatPage.css +102 -30
  44. package/src/web/assets/css/MessageItem.css +26 -29
  45. package/src/web/assets/css/ThinkingIndicator.css +44 -6
  46. package/src/web/assets/css/ToolMessage.css +36 -14
  47. package/src/web/components/ChatPage.tsx +228 -105
  48. package/src/web/components/HomePage.tsx +6 -6
  49. package/src/web/components/MessageItem.tsx +88 -89
  50. package/src/web/components/Setup.tsx +1 -1
  51. package/src/web/components/Sidebar.tsx +1 -1
  52. package/src/web/components/ThinkingIndicator.tsx +40 -21
  53. package/src/web/router.ts +1 -1
  54. package/src/web/server.tsx +187 -39
  55. package/src/web/storage.ts +23 -1
  56. package/src/web/types.ts +7 -6
@@ -1,8 +1,14 @@
1
1
  import { useState, useEffect, useRef } from 'react'
2
2
  import { TextAttributes } from "@opentui/core"
3
- import { useKeyboard } from "@opentui/react"
3
+ import { useKeyboard, useRenderer } from "@opentui/react"
4
4
  import { execSync } from 'child_process'
5
+ import { writeFileSync, readFileSync, rmSync } from 'fs'
6
+ import { join } from 'path'
7
+ import { tmpdir } from 'os'
5
8
  import { getInputHistory } from '../utils/history'
9
+ import { emitImageCommand, canUseImages } from '../utils/imageBridge'
10
+ import { notifyNotification } from '../utils/notificationBridge'
11
+ import type { ImageAttachment } from '../utils/images'
6
12
 
7
13
  export interface InputSubmitMeta {
8
14
  isPaste?: boolean
@@ -16,9 +22,10 @@ interface CustomInputProps {
16
22
  focused?: boolean
17
23
  pasteRequestId?: number
18
24
  disableHistory?: boolean
25
+ submitDisabled?: boolean
19
26
  }
20
27
 
21
- export function CustomInput({ onSubmit, placeholder = '', password = false, focused = true, pasteRequestId = 0, disableHistory = false }: CustomInputProps) {
28
+ export function CustomInput({ onSubmit, placeholder = '', password = false, focused = true, pasteRequestId = 0, disableHistory = false, submitDisabled = false }: CustomInputProps) {
22
29
  const [value, setValue] = useState('')
23
30
  const [cursorPosition, setCursorPosition] = useState(0)
24
31
  const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80)
@@ -31,23 +38,158 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
31
38
  const pasteFlagRef = useRef(false)
32
39
  const pastedContentRef = useRef('')
33
40
  const desiredCursorColRef = useRef<number | null>(null)
41
+ const lastBracketedPasteAtRef = useRef<number | null>(null)
42
+ const lastClipboardPasteRef = useRef<{ at: number; text: string } | null>(null)
43
+ const lastClipboardImageRef = useRef<{ at: number; signature: string } | null>(null)
44
+ const lastPasteRequestIdRef = useRef(0)
45
+ const lastPasteUndoRef = useRef<{ prevValue: string; prevCursor: number; nextValue: string; nextCursor: number } | null>(null)
46
+ const valueRef = useRef(value)
47
+ const cursorPositionRef = useRef(cursorPosition)
48
+
49
+ const renderer = useRenderer()
34
50
 
35
51
  const normalizePastedText = (text: string) => text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
36
52
 
53
+ const createId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
54
+
55
+ const buildClipboardImage = (data: string, mimeType: string, size: number): ImageAttachment => {
56
+ const ext = mimeType === 'image/jpeg' ? 'jpg' : (mimeType === 'image/png' ? 'png' : 'bin')
57
+ return {
58
+ id: createId(),
59
+ name: `clipboard-${Date.now()}.${ext}`,
60
+ mimeType,
61
+ data,
62
+ size
63
+ }
64
+ }
65
+
66
+ const isPng = (buffer: Buffer) => buffer.length > 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47
67
+ const isJpeg = (buffer: Buffer) => buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff
68
+
69
+ const readClipboardImage = (): { data: string; mimeType: string; size: number } | null => {
70
+ try {
71
+ if (process.platform === 'win32') {
72
+ const script = 'powershell.exe -NoProfile -Command "$img=Get-Clipboard -Format Image -ErrorAction SilentlyContinue; if ($img) { $ms=New-Object System.IO.MemoryStream; $img.Save($ms,[System.Drawing.Imaging.ImageFormat]::Png); [Convert]::ToBase64String($ms.ToArray()) }"'
73
+ const base64 = execSync(script, { encoding: 'utf8', timeout: 2000 }).trim()
74
+ if (!base64) return null
75
+ const size = Buffer.from(base64, 'base64').length
76
+ return { data: base64, mimeType: 'image/png', size }
77
+ }
78
+
79
+ if (process.platform === 'darwin') {
80
+ try {
81
+ const buffer = execSync('pbpaste -Prefer png', { timeout: 2000 }) as Buffer
82
+ if (buffer.length > 0 && isPng(buffer)) {
83
+ return { data: buffer.toString('base64'), mimeType: 'image/png', size: buffer.length }
84
+ }
85
+ } catch {
86
+ }
87
+ try {
88
+ const buffer = execSync('pbpaste -Prefer jpeg', { timeout: 2000 }) as Buffer
89
+ if (buffer.length > 0 && isJpeg(buffer)) {
90
+ return { data: buffer.toString('base64'), mimeType: 'image/jpeg', size: buffer.length }
91
+ }
92
+ } catch {
93
+ }
94
+ return null
95
+ }
96
+
97
+ try {
98
+ const buffer = execSync('xclip -selection clipboard -t image/png -o', { timeout: 2000 }) as Buffer
99
+ if (buffer.length > 0 && isPng(buffer)) {
100
+ return { data: buffer.toString('base64'), mimeType: 'image/png', size: buffer.length }
101
+ }
102
+ } catch {
103
+ }
104
+ try {
105
+ const buffer = execSync('xclip -selection clipboard -t image/jpeg -o', { timeout: 2000 }) as Buffer
106
+ if (buffer.length > 0 && isJpeg(buffer)) {
107
+ return { data: buffer.toString('base64'), mimeType: 'image/jpeg', size: buffer.length }
108
+ }
109
+ } catch {
110
+ }
111
+ } catch {
112
+ }
113
+ return null
114
+ }
115
+
116
+ const tryPasteImage = (): boolean => {
117
+ const image = readClipboardImage()
118
+ if (!image) return false
119
+ const signature = `${image.mimeType}:${image.data.slice(0, 64)}`
120
+ const now = Date.now()
121
+ const last = lastClipboardImageRef.current
122
+ if (last && last.signature === signature && now - last.at < 400) return true
123
+ lastClipboardImageRef.current = { at: now, signature }
124
+
125
+ if (!canUseImages()) {
126
+ notifyNotification('Current model does not support images.', 'warning', 3000)
127
+ return true
128
+ }
129
+
130
+ emitImageCommand({ type: 'add', image: buildClipboardImage(image.data, image.mimeType, image.size) })
131
+ return true
132
+ }
133
+
37
134
  const addPastedBlock = (pastedText: string) => {
38
135
  const normalized = normalizePastedText(pastedText)
39
136
  if (!normalized) return
40
137
 
41
- setValue(prev => prev.slice(0, cursorPosition) + normalized + prev.slice(cursorPosition))
42
- setCursorPosition(prev => prev + normalized.length)
138
+ const insertAt = cursorPositionRef.current
139
+ const prevValue = valueRef.current
140
+ const nextValue = prevValue.slice(0, insertAt) + normalized + prevValue.slice(insertAt)
141
+ lastPasteUndoRef.current = {
142
+ prevValue,
143
+ prevCursor: insertAt,
144
+ nextValue,
145
+ nextCursor: insertAt + normalized.length
146
+ }
147
+ setValue(nextValue)
148
+ setCursorPosition(insertAt + normalized.length)
43
149
  pasteFlagRef.current = true
44
150
  pastedContentRef.current = normalized
45
151
  }
46
152
 
153
+ const openExternalEditor = () => {
154
+ const filePath = join(tmpdir(), `mosaic-input-${Date.now()}-${process.pid}.txt`)
155
+ try {
156
+ writeFileSync(filePath, value, 'utf-8')
157
+ if (process.platform === 'win32') {
158
+ const escaped = filePath.replace(/'/g, "''")
159
+ execSync(`powershell.exe -NoProfile -Command "Start-Process notepad.exe -ArgumentList '${escaped}' -Wait"`, { stdio: 'ignore' })
160
+ } else if (process.platform === 'darwin') {
161
+ execSync(`open -W -a TextEdit "${filePath}"`, { stdio: 'ignore' })
162
+ } else {
163
+ const editor = process.env.EDITOR || 'nano'
164
+ execSync(`${editor} "${filePath}"`, { stdio: 'inherit' })
165
+ }
166
+ const updated = readFileSync(filePath, 'utf-8')
167
+ const normalized = normalizePastedText(updated)
168
+ setValue(normalized)
169
+ setCursorPosition(normalized.length)
170
+ setHistoryIndex(-1)
171
+ desiredCursorColRef.current = null
172
+ } catch (error) {
173
+ } finally {
174
+ try {
175
+ rmSync(filePath)
176
+ } catch (error) {
177
+ }
178
+ }
179
+ }
180
+
47
181
  useEffect(() => {
48
182
  setInputHistory(getInputHistory())
49
183
  }, [])
50
184
 
185
+ useEffect(() => {
186
+ valueRef.current = value
187
+ }, [value])
188
+
189
+ useEffect(() => {
190
+ cursorPositionRef.current = cursorPosition
191
+ }, [cursorPosition])
192
+
51
193
  useEffect(() => {
52
194
  const handleResize = () => {
53
195
  setTerminalWidth(process.stdout.columns || 80)
@@ -70,6 +212,7 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
70
212
 
71
213
  const pasteFromClipboard = () => {
72
214
  try {
215
+ if (tryPasteImage()) return
73
216
  let clipboardText = ''
74
217
  if (process.platform === 'win32') {
75
218
  clipboardText = execSync('powershell.exe -command "Get-Clipboard"', { encoding: 'utf8', timeout: 2000 })
@@ -78,9 +221,13 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
78
221
  } else {
79
222
  clipboardText = execSync('xclip -selection clipboard -o', { encoding: 'utf8', timeout: 2000 })
80
223
  }
81
- if (clipboardText) {
82
- addPastedBlock(clipboardText)
83
- }
224
+ const normalized = normalizePastedText(clipboardText || '')
225
+ if (!normalized) return
226
+ const now = Date.now()
227
+ const last = lastClipboardPasteRef.current
228
+ if (last && last.text === normalized && now - last.at < 400) return
229
+ lastClipboardPasteRef.current = { at: now, text: normalized }
230
+ addPastedBlock(normalized)
84
231
  } catch (error) {
85
232
  }
86
233
  }
@@ -88,26 +235,80 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
88
235
  useEffect(() => {
89
236
  if (!focused) return
90
237
  if (!pasteRequestId) return
238
+ if (pasteRequestId === lastPasteRequestIdRef.current) return
239
+ lastPasteRequestIdRef.current = pasteRequestId
240
+ if (inPasteMode) return
241
+ const now = Date.now()
242
+ const lastBracketed = lastBracketedPasteAtRef.current
243
+ if (lastBracketed && now - lastBracketed < 250) return
91
244
  pasteFromClipboard()
92
- }, [pasteRequestId, focused])
245
+ }, [pasteRequestId, focused, inPasteMode])
93
246
 
94
- useKeyboard((key) => {
247
+ useEffect(() => {
95
248
  if (!focused) return
96
249
 
97
- const typedDisplay = value.replace(/\n/g, ' ')
98
- const displayValueRaw = typedDisplay
99
- const displayCursorPos = cursorPosition
100
- const lineWidth = Math.max(10, terminalWidth - 4)
101
- const displayLines = displayValueRaw.length > 0
102
- ? Array.from({ length: Math.ceil(displayValueRaw.length / lineWidth) }, (_, i) => displayValueRaw.slice(i * lineWidth, (i + 1) * lineWidth))
103
- : ['']
104
- const boundedDisplayCursorPos = Math.max(0, Math.min(displayValueRaw.length, displayCursorPos))
105
- const currentCursorLine = displayValueRaw.length === 0 ? 0 : Math.min(displayLines.length - 1, Math.floor(boundedDisplayCursorPos / lineWidth))
106
- const currentCursorCol = boundedDisplayCursorPos >= displayValueRaw.length
107
- ? displayLines[Math.max(0, displayLines.length - 1)]!.length
108
- : (boundedDisplayCursorPos % lineWidth)
250
+ const handlePaste = (event: { text?: string } | string) => {
251
+ const text = typeof event === 'string' ? event : (event?.text || '')
252
+ const normalized = normalizePastedText(text)
253
+ if (!normalized) return
254
+ const now = Date.now()
255
+ const last = lastClipboardPasteRef.current
256
+ if (last && last.text === normalized && now - last.at < 400) return
257
+ lastClipboardPasteRef.current = { at: now, text: normalized }
258
+ addPastedBlock(normalized)
259
+ setHistoryIndex(-1)
260
+ desiredCursorColRef.current = null
261
+ lastBracketedPasteAtRef.current = now
262
+ }
263
+
264
+ renderer.keyInput.on('paste', handlePaste as any)
265
+ return () => {
266
+ renderer.keyInput.off('paste', handlePaste as any)
267
+ }
268
+ }, [focused, renderer.keyInput])
269
+
270
+ const wrapTextWithCursor = (text: string, cursorPos: number, maxWidth: number): { lines: string[], cursorLine: number, cursorCol: number } => {
271
+ const safeCursorPos = Math.max(0, Math.min(text.length, cursorPos))
272
+ const lines: string[] = ['']
273
+ let lineIndex = 0
274
+ let col = 0
275
+ let cursorLine = 0
276
+ let cursorCol = 0
277
+
278
+ for (let i = 0; i <= text.length; i += 1) {
279
+ if (i === safeCursorPos) {
280
+ cursorLine = lineIndex
281
+ cursorCol = col
282
+ }
283
+
284
+ if (i === text.length) break
285
+
286
+ const ch = text[i]!
287
+ if (ch === '\n') {
288
+ lines.push('')
289
+ lineIndex += 1
290
+ col = 0
291
+ continue
292
+ }
293
+
294
+ if (col >= maxWidth) {
295
+ lines.push('')
296
+ lineIndex += 1
297
+ col = 0
298
+ }
299
+
300
+ lines[lineIndex] = (lines[lineIndex] || '') + ch
301
+ col += 1
302
+ }
303
+
304
+ return { lines, cursorLine, cursorCol }
305
+ }
306
+
307
+ useKeyboard((key) => {
308
+ if (!focused) return
109
309
 
110
310
  if (key.sequence && key.sequence.includes('\x1b[200~')) {
311
+ lastBracketedPasteAtRef.current = Date.now()
111
312
  setInPasteMode(true)
112
313
  setPasteBuffer('')
113
314
  return
@@ -116,8 +317,14 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
116
317
  if (key.sequence && key.sequence.includes('\x1b[201~')) {
117
318
  setInPasteMode(false)
118
319
  if (pasteBuffer) {
119
- addPastedBlock(pasteBuffer)
320
+ const now = Date.now()
321
+ const normalized = normalizePastedText(pasteBuffer)
322
+ const last = lastClipboardPasteRef.current
323
+ if (!last || last.text !== normalized || now - last.at >= 400) {
324
+ addPastedBlock(pasteBuffer)
325
+ }
120
326
  setPasteBuffer('')
327
+ lastBracketedPasteAtRef.current = now
121
328
  }
122
329
  return
123
330
  }
@@ -127,6 +334,25 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
127
334
  return
128
335
  }
129
336
 
337
+ if ((key.name === 'z' && key.ctrl) || key.sequence === '\x1a') {
338
+ const lastPaste = lastPasteUndoRef.current
339
+ if (lastPaste && valueRef.current === lastPaste.nextValue) {
340
+ setValue(lastPaste.prevValue)
341
+ setCursorPosition(lastPaste.prevCursor)
342
+ lastPasteUndoRef.current = null
343
+ pasteFlagRef.current = false
344
+ pastedContentRef.current = ''
345
+ }
346
+ return
347
+ }
348
+
349
+ if ((key.name === 'v' && key.ctrl) || key.sequence === '\x16') {
350
+ pasteFromClipboard()
351
+ setHistoryIndex(-1)
352
+ desiredCursorColRef.current = null
353
+ return
354
+ }
355
+
130
356
  if (key.name === 'k' && (key.ctrl || key.meta || (key as any).alt)) {
131
357
  setValue('')
132
358
  setCursorPosition(0)
@@ -138,7 +364,13 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
138
364
  return
139
365
  }
140
366
 
367
+ if (key.name === 'g' && (key.ctrl || key.meta)) {
368
+ openExternalEditor()
369
+ return
370
+ }
371
+
141
372
  if (key.name === 'return') {
373
+ if (submitDisabled) return
142
374
  const meta: InputSubmitMeta | undefined = pasteFlagRef.current
143
375
  ? { isPaste: true, pastedContent: pastedContentRef.current }
144
376
  : undefined
@@ -168,6 +400,8 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
168
400
  setValue(prev => prev.slice(0, cursorPosition) + prev.slice(cursorPosition + 1))
169
401
  }
170
402
  } else if (key.name === 'up') {
403
+ const lineWidth = Math.max(10, terminalWidth - 4)
404
+ const { lines: displayLines, cursorLine: currentCursorLine, cursorCol: currentCursorCol } = wrapTextWithCursor(value, cursorPosition, lineWidth)
171
405
  if (currentCursorLine > 0) {
172
406
  if (desiredCursorColRef.current === null) {
173
407
  desiredCursorColRef.current = currentCursorCol
@@ -198,6 +432,8 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
198
432
  setCursorPosition(inputHistory[newIndex]!.length)
199
433
  }
200
434
  } else if (key.name === 'down') {
435
+ const lineWidth = Math.max(10, terminalWidth - 4)
436
+ const { lines: displayLines, cursorLine: currentCursorLine, cursorCol: currentCursorCol } = wrapTextWithCursor(value, cursorPosition, lineWidth)
201
437
  if (currentCursorLine < displayLines.length - 1) {
202
438
  if (desiredCursorColRef.current === null) {
203
439
  desiredCursorColRef.current = currentCursorCol
@@ -250,56 +486,27 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
250
486
  }
251
487
  })
252
488
 
253
- const typedDisplay = value.replace(/\n/g, ' ')
254
- const displayValue = password && value ? '•'.repeat(value.length) : typedDisplay
255
- const cursorChar = '█'
489
+ const displayValue = password && value ? Array.from(value, (char) => (char === '\n' ? '\n' : '•')).join('') : value
256
490
  const isEmpty = value.length === 0
257
491
 
258
492
  const lineWidth = Math.max(10, terminalWidth - 4)
259
493
 
260
- const wrapTextWithCursor = (text: string, cursorPos: number, maxWidth: number): { lines: string[], cursorLine: number, cursorCol: number } => {
261
- if (text.length === 0) {
262
- return { lines: [''], cursorLine: 0, cursorCol: 0 }
263
- }
264
-
265
- const safeCursorPos = Math.max(0, Math.min(text.length, cursorPos))
266
- const lines: string[] = []
267
- for (let i = 0; i < text.length; i += maxWidth) {
268
- lines.push(text.slice(i, i + maxWidth))
269
- }
270
-
271
- let cursorLine: number
272
- let cursorCol: number
273
-
274
- if (safeCursorPos >= text.length) {
275
- cursorLine = lines.length - 1
276
- cursorCol = lines[cursorLine]!.length
277
- } else {
278
- cursorLine = Math.floor(safeCursorPos / maxWidth)
279
- cursorCol = safeCursorPos % maxWidth
280
- }
281
-
282
- cursorLine = Math.max(0, Math.min(lines.length - 1, cursorLine))
283
- cursorCol = Math.max(0, Math.min(lines[cursorLine]!.length, cursorCol))
284
-
285
- return { lines, cursorLine, cursorCol }
286
- }
287
-
288
494
  if (isEmpty) {
289
495
  if (!placeholder) {
290
496
  return (
291
497
  <box flexDirection="column" flexGrow={1} width="100%">
292
498
  <box flexDirection="row">
293
- <text>{cursorChar}</text>
499
+ <text fg="black" bg="white"> </text>
294
500
  </box>
295
501
  </box>
296
502
  )
297
503
  }
504
+ const firstChar = placeholder[0] || ' '
298
505
  return (
299
506
  <box flexDirection="column" flexGrow={1} width="100%">
300
507
  <box flexDirection="row">
301
- <text>{cursorChar}</text>
302
- {placeholder.slice(1) && <text attributes={TextAttributes.DIM}>{placeholder.slice(1)}</text>}
508
+ <text fg="gray" bg="white" attributes={TextAttributes.DIM}>{firstChar}</text>
509
+ {placeholder.slice(1) && <text fg="gray" attributes={TextAttributes.DIM}>{placeholder.slice(1)}</text>}
303
510
  </box>
304
511
  </box>
305
512
  )
@@ -307,22 +514,21 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
307
514
 
308
515
  const { lines, cursorLine, cursorCol } = wrapTextWithCursor(displayValue, cursorPosition, lineWidth)
309
516
 
310
- const renderedLines = lines.map((line, lineIndex) => {
311
- if (lineIndex === cursorLine) {
312
- const beforeCursor = line.slice(0, cursorCol)
313
- const afterCursor = line.slice(cursorCol)
314
- return beforeCursor + cursorChar + afterCursor
315
- }
316
- return line || ' '
317
- })
318
-
319
517
  return (
320
518
  <box flexDirection="column" flexGrow={1} width="100%">
321
- {renderedLines.map((renderedLine, lineIndex) => (
519
+ {lines.map((line, lineIndex) => (
322
520
  <box key={lineIndex} flexDirection="row">
323
- <text>{renderedLine}</text>
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
+ )}
324
530
  </box>
325
531
  ))}
326
532
  </box>
327
533
  )
328
- }
534
+ }