@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.
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/package.json +52 -47
- package/src/agent/prompts/systemPrompt.ts +198 -68
- package/src/agent/prompts/toolsPrompt.ts +217 -135
- package/src/agent/provider/anthropic.ts +19 -15
- package/src/agent/provider/google.ts +21 -17
- package/src/agent/provider/ollama.ts +80 -41
- package/src/agent/provider/openai.ts +107 -67
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +19 -15
- package/src/agent/tools/definitions.ts +9 -5
- package/src/agent/tools/executor.ts +655 -46
- package/src/agent/tools/exploreExecutor.ts +12 -12
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +62 -8
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +6 -6
- package/src/components/App.tsx +67 -25
- package/src/components/CustomInput.tsx +274 -68
- package/src/components/Main.tsx +323 -168
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/main/ChatPage.tsx +217 -58
- package/src/components/main/HomePage.tsx +5 -1
- package/src/components/main/ThinkingIndicator.tsx +11 -1
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +3 -5
- package/src/utils/approvalBridge.ts +29 -8
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +5 -1
- package/src/utils/diffRendering.tsx +13 -14
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/models.ts +0 -7
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/toolFormatting.ts +162 -43
- package/src/web/app.tsx +94 -34
- package/src/web/assets/css/ChatPage.css +102 -30
- package/src/web/assets/css/MessageItem.css +26 -29
- package/src/web/assets/css/ThinkingIndicator.css +44 -6
- package/src/web/assets/css/ToolMessage.css +36 -14
- package/src/web/components/ChatPage.tsx +228 -105
- package/src/web/components/HomePage.tsx +6 -6
- package/src/web/components/MessageItem.tsx +88 -89
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -1
- package/src/web/components/ThinkingIndicator.tsx +40 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +187 -39
- package/src/web/storage.ts +23 -1
- 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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
247
|
+
useEffect(() => {
|
|
95
248
|
if (!focused) return
|
|
96
249
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
|
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>
|
|
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>{
|
|
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
|
-
{
|
|
519
|
+
{lines.map((line, lineIndex) => (
|
|
322
520
|
<box key={lineIndex} flexDirection="row">
|
|
323
|
-
|
|
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
|
+
}
|