@kirosnn/mosaic 0.0.91 → 0.73.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 -6
- package/package.json +55 -48
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +209 -70
- package/src/agent/prompts/toolsPrompt.ts +285 -138
- package/src/agent/provider/anthropic.ts +109 -105
- package/src/agent/provider/google.ts +111 -107
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +73 -17
- package/src/agent/provider/openai.ts +146 -102
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +108 -104
- package/src/agent/tools/definitions.ts +15 -1
- package/src/agent/tools/executor.ts +717 -98
- package/src/agent/tools/exploreExecutor.ts +20 -22
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +64 -9
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +15 -14
- package/src/components/App.tsx +50 -8
- package/src/components/CustomInput.tsx +461 -77
- package/src/components/Main.tsx +1459 -1112
- package/src/components/Setup.tsx +1 -1
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -516
- package/src/components/main/HomePage.tsx +58 -39
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +13 -2
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +53 -25
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +45 -12
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +9 -7
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +13 -16
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -16
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +428 -48
- package/src/web/app.tsx +65 -5
- 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 +3 -3
- package/src/web/components/MessageItem.tsx +80 -81
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -3
- package/src/web/components/ThinkingIndicator.tsx +41 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +894 -662
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
|
@@ -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,12 +22,16 @@ interface CustomInputProps {
|
|
|
16
22
|
focused?: boolean
|
|
17
23
|
pasteRequestId?: number
|
|
18
24
|
disableHistory?: boolean
|
|
25
|
+
submitDisabled?: boolean
|
|
26
|
+
maxWidth?: number
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
export function CustomInput({ onSubmit, placeholder = '', password = false, focused = true, pasteRequestId = 0, disableHistory = false }: CustomInputProps) {
|
|
29
|
+
export function CustomInput({ onSubmit, placeholder = '', password = false, focused = true, pasteRequestId = 0, disableHistory = false, submitDisabled = false, maxWidth }: CustomInputProps) {
|
|
22
30
|
const [value, setValue] = useState('')
|
|
23
31
|
const [cursorPosition, setCursorPosition] = useState(0)
|
|
24
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)
|
|
25
35
|
const [pasteBuffer, setPasteBuffer] = useState('')
|
|
26
36
|
const [inPasteMode, setInPasteMode] = useState(false)
|
|
27
37
|
const [historyIndex, setHistoryIndex] = useState(-1)
|
|
@@ -31,23 +41,178 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
31
41
|
const pasteFlagRef = useRef(false)
|
|
32
42
|
const pastedContentRef = useRef('')
|
|
33
43
|
const desiredCursorColRef = useRef<number | null>(null)
|
|
44
|
+
const lastBracketedPasteAtRef = useRef<number | null>(null)
|
|
45
|
+
const lastClipboardPasteRef = useRef<{ at: number; text: string } | null>(null)
|
|
46
|
+
const lastClipboardImageRef = useRef<{ at: number; signature: string } | null>(null)
|
|
47
|
+
const lastPasteRequestIdRef = useRef(0)
|
|
48
|
+
const lastPasteUndoRef = useRef<{ prevValue: string; prevCursor: number; nextValue: string; nextCursor: number } | null>(null)
|
|
49
|
+
const valueRef = useRef(value)
|
|
50
|
+
const cursorPositionRef = useRef(cursorPosition)
|
|
51
|
+
const selectionStartRef = useRef<number | null>(selectionStart)
|
|
52
|
+
const selectionEndRef = useRef<number | null>(selectionEnd)
|
|
53
|
+
|
|
54
|
+
const renderer = useRenderer()
|
|
34
55
|
|
|
35
56
|
const normalizePastedText = (text: string) => text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
36
57
|
|
|
58
|
+
const createId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
59
|
+
|
|
60
|
+
const buildClipboardImage = (data: string, mimeType: string, size: number): ImageAttachment => {
|
|
61
|
+
const ext = mimeType === 'image/jpeg' ? 'jpg' : (mimeType === 'image/png' ? 'png' : 'bin')
|
|
62
|
+
return {
|
|
63
|
+
id: createId(),
|
|
64
|
+
name: `clipboard-${Date.now()}.${ext}`,
|
|
65
|
+
mimeType,
|
|
66
|
+
data,
|
|
67
|
+
size
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isPng = (buffer: Buffer) => buffer.length > 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47
|
|
72
|
+
const isJpeg = (buffer: Buffer) => buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff
|
|
73
|
+
|
|
74
|
+
const readClipboardImage = (): { data: string; mimeType: string; size: number } | null => {
|
|
75
|
+
try {
|
|
76
|
+
if (process.platform === 'win32') {
|
|
77
|
+
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()) }"'
|
|
78
|
+
const base64 = execSync(script, { encoding: 'utf8', timeout: 2000 }).trim()
|
|
79
|
+
if (!base64) return null
|
|
80
|
+
const size = Buffer.from(base64, 'base64').length
|
|
81
|
+
return { data: base64, mimeType: 'image/png', size }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (process.platform === 'darwin') {
|
|
85
|
+
try {
|
|
86
|
+
const buffer = execSync('pbpaste -Prefer png', { timeout: 2000 }) as Buffer
|
|
87
|
+
if (buffer.length > 0 && isPng(buffer)) {
|
|
88
|
+
return { data: buffer.toString('base64'), mimeType: 'image/png', size: buffer.length }
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const buffer = execSync('pbpaste -Prefer jpeg', { timeout: 2000 }) as Buffer
|
|
94
|
+
if (buffer.length > 0 && isJpeg(buffer)) {
|
|
95
|
+
return { data: buffer.toString('base64'), mimeType: 'image/jpeg', size: buffer.length }
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const buffer = execSync('xclip -selection clipboard -t image/png -o', { timeout: 2000 }) as Buffer
|
|
104
|
+
if (buffer.length > 0 && isPng(buffer)) {
|
|
105
|
+
return { data: buffer.toString('base64'), mimeType: 'image/png', size: buffer.length }
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const buffer = execSync('xclip -selection clipboard -t image/jpeg -o', { timeout: 2000 }) as Buffer
|
|
111
|
+
if (buffer.length > 0 && isJpeg(buffer)) {
|
|
112
|
+
return { data: buffer.toString('base64'), mimeType: 'image/jpeg', size: buffer.length }
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tryPasteImage = (): boolean => {
|
|
122
|
+
const image = readClipboardImage()
|
|
123
|
+
if (!image) return false
|
|
124
|
+
const signature = `${image.mimeType}:${image.data.slice(0, 64)}`
|
|
125
|
+
const now = Date.now()
|
|
126
|
+
const last = lastClipboardImageRef.current
|
|
127
|
+
if (last && last.signature === signature && now - last.at < 400) return true
|
|
128
|
+
lastClipboardImageRef.current = { at: now, signature }
|
|
129
|
+
|
|
130
|
+
if (!canUseImages()) {
|
|
131
|
+
notifyNotification('Current model does not support images.', 'warning', 3000)
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
emitImageCommand({ type: 'add', image: buildClipboardImage(image.data, image.mimeType, image.size) })
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
|
|
37
139
|
const addPastedBlock = (pastedText: string) => {
|
|
38
140
|
const normalized = normalizePastedText(pastedText)
|
|
39
141
|
if (!normalized) return
|
|
40
142
|
|
|
41
|
-
|
|
42
|
-
|
|
143
|
+
const prevValue = valueRef.current
|
|
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)
|
|
150
|
+
lastPasteUndoRef.current = {
|
|
151
|
+
prevValue,
|
|
152
|
+
prevCursor: insertAt,
|
|
153
|
+
nextValue,
|
|
154
|
+
nextCursor: insertAt + normalized.length
|
|
155
|
+
}
|
|
156
|
+
setValue(nextValue)
|
|
157
|
+
setCursorPosition(insertAt + normalized.length)
|
|
158
|
+
if (hasSelection) {
|
|
159
|
+
setSelectionStart(null)
|
|
160
|
+
setSelectionEnd(null)
|
|
161
|
+
}
|
|
43
162
|
pasteFlagRef.current = true
|
|
44
163
|
pastedContentRef.current = normalized
|
|
45
164
|
}
|
|
46
165
|
|
|
166
|
+
const openExternalEditor = () => {
|
|
167
|
+
const filePath = join(tmpdir(), `mosaic-input-${Date.now()}-${process.pid}.txt`)
|
|
168
|
+
try {
|
|
169
|
+
writeFileSync(filePath, value, 'utf-8')
|
|
170
|
+
if (process.platform === 'win32') {
|
|
171
|
+
const escaped = filePath.replace(/'/g, "''")
|
|
172
|
+
execSync(`powershell.exe -NoProfile -Command "Start-Process notepad.exe -ArgumentList '${escaped}' -Wait"`, { stdio: 'ignore' })
|
|
173
|
+
} else if (process.platform === 'darwin') {
|
|
174
|
+
execSync(`open -W -a TextEdit "${filePath}"`, { stdio: 'ignore' })
|
|
175
|
+
} else {
|
|
176
|
+
const editor = process.env.EDITOR || 'nano'
|
|
177
|
+
execSync(`${editor} "${filePath}"`, { stdio: 'inherit' })
|
|
178
|
+
}
|
|
179
|
+
const updated = readFileSync(filePath, 'utf-8')
|
|
180
|
+
const normalized = normalizePastedText(updated)
|
|
181
|
+
setValue(normalized)
|
|
182
|
+
setCursorPosition(normalized.length)
|
|
183
|
+
setHistoryIndex(-1)
|
|
184
|
+
desiredCursorColRef.current = null
|
|
185
|
+
setSelectionStart(null)
|
|
186
|
+
setSelectionEnd(null)
|
|
187
|
+
} catch (error) {
|
|
188
|
+
} finally {
|
|
189
|
+
try {
|
|
190
|
+
rmSync(filePath)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
47
196
|
useEffect(() => {
|
|
48
197
|
setInputHistory(getInputHistory())
|
|
49
198
|
}, [])
|
|
50
199
|
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
valueRef.current = value
|
|
202
|
+
}, [value])
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
cursorPositionRef.current = cursorPosition
|
|
206
|
+
}, [cursorPosition])
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
selectionStartRef.current = selectionStart
|
|
210
|
+
}, [selectionStart])
|
|
211
|
+
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
selectionEndRef.current = selectionEnd
|
|
214
|
+
}, [selectionEnd])
|
|
215
|
+
|
|
51
216
|
useEffect(() => {
|
|
52
217
|
const handleResize = () => {
|
|
53
218
|
setTerminalWidth(process.stdout.columns || 80)
|
|
@@ -70,6 +235,7 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
70
235
|
|
|
71
236
|
const pasteFromClipboard = () => {
|
|
72
237
|
try {
|
|
238
|
+
if (tryPasteImage()) return
|
|
73
239
|
let clipboardText = ''
|
|
74
240
|
if (process.platform === 'win32') {
|
|
75
241
|
clipboardText = execSync('powershell.exe -command "Get-Clipboard"', { encoding: 'utf8', timeout: 2000 })
|
|
@@ -78,9 +244,13 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
78
244
|
} else {
|
|
79
245
|
clipboardText = execSync('xclip -selection clipboard -o', { encoding: 'utf8', timeout: 2000 })
|
|
80
246
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
247
|
+
const normalized = normalizePastedText(clipboardText || '')
|
|
248
|
+
if (!normalized) return
|
|
249
|
+
const now = Date.now()
|
|
250
|
+
const last = lastClipboardPasteRef.current
|
|
251
|
+
if (last && last.text === normalized && now - last.at < 400) return
|
|
252
|
+
lastClipboardPasteRef.current = { at: now, text: normalized }
|
|
253
|
+
addPastedBlock(normalized)
|
|
84
254
|
} catch (error) {
|
|
85
255
|
}
|
|
86
256
|
}
|
|
@@ -88,26 +258,93 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
88
258
|
useEffect(() => {
|
|
89
259
|
if (!focused) return
|
|
90
260
|
if (!pasteRequestId) return
|
|
261
|
+
if (pasteRequestId === lastPasteRequestIdRef.current) return
|
|
262
|
+
lastPasteRequestIdRef.current = pasteRequestId
|
|
263
|
+
if (inPasteMode) return
|
|
264
|
+
const now = Date.now()
|
|
265
|
+
const lastBracketed = lastBracketedPasteAtRef.current
|
|
266
|
+
if (lastBracketed && now - lastBracketed < 250) return
|
|
91
267
|
pasteFromClipboard()
|
|
92
|
-
}, [pasteRequestId, focused])
|
|
268
|
+
}, [pasteRequestId, focused, inPasteMode])
|
|
93
269
|
|
|
94
|
-
|
|
270
|
+
useEffect(() => {
|
|
95
271
|
if (!focused) return
|
|
96
272
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
273
|
+
const handlePaste = (event: { text?: string } | string) => {
|
|
274
|
+
const text = typeof event === 'string' ? event : (event?.text || '')
|
|
275
|
+
const normalized = normalizePastedText(text)
|
|
276
|
+
if (!normalized) return
|
|
277
|
+
const now = Date.now()
|
|
278
|
+
const last = lastClipboardPasteRef.current
|
|
279
|
+
if (last && last.text === normalized && now - last.at < 400) return
|
|
280
|
+
lastClipboardPasteRef.current = { at: now, text: normalized }
|
|
281
|
+
addPastedBlock(normalized)
|
|
282
|
+
setHistoryIndex(-1)
|
|
283
|
+
desiredCursorColRef.current = null
|
|
284
|
+
lastBracketedPasteAtRef.current = now
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
renderer.keyInput.on('paste', handlePaste as any)
|
|
288
|
+
return () => {
|
|
289
|
+
renderer.keyInput.off('paste', handlePaste as any)
|
|
290
|
+
}
|
|
291
|
+
}, [focused, renderer.keyInput])
|
|
292
|
+
|
|
293
|
+
const wrapTextWithCursor = (text: string, cursorPos: number, maxWidth: number): { lineStarts: number[], lineLengths: number[], cursorLine: number, cursorCol: number } => {
|
|
294
|
+
const safeCursorPos = Math.max(0, Math.min(text.length, cursorPos))
|
|
295
|
+
const lineStarts: number[] = [0]
|
|
296
|
+
const lineLengths: number[] = [0]
|
|
297
|
+
let lineIndex = 0
|
|
298
|
+
let col = 0
|
|
299
|
+
let cursorLine = 0
|
|
300
|
+
let cursorCol = 0
|
|
301
|
+
|
|
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
|
+
|
|
310
|
+
if (i === safeCursorPos) {
|
|
311
|
+
cursorLine = lineIndex
|
|
312
|
+
cursorCol = col
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (i === text.length) break
|
|
316
|
+
|
|
317
|
+
const ch = text[i]!
|
|
318
|
+
if (ch === '\n') {
|
|
319
|
+
lineStarts.push(i + 1)
|
|
320
|
+
lineLengths.push(0)
|
|
321
|
+
lineIndex += 1
|
|
322
|
+
col = 0
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
lineLengths[lineIndex] = (lineLengths[lineIndex] || 0) + 1
|
|
327
|
+
col += 1
|
|
328
|
+
}
|
|
329
|
+
|
|
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
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
useKeyboard((key) => {
|
|
344
|
+
if (!focused) return
|
|
109
345
|
|
|
110
346
|
if (key.sequence && key.sequence.includes('\x1b[200~')) {
|
|
347
|
+
lastBracketedPasteAtRef.current = Date.now()
|
|
111
348
|
setInPasteMode(true)
|
|
112
349
|
setPasteBuffer('')
|
|
113
350
|
return
|
|
@@ -116,8 +353,14 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
116
353
|
if (key.sequence && key.sequence.includes('\x1b[201~')) {
|
|
117
354
|
setInPasteMode(false)
|
|
118
355
|
if (pasteBuffer) {
|
|
119
|
-
|
|
356
|
+
const now = Date.now()
|
|
357
|
+
const normalized = normalizePastedText(pasteBuffer)
|
|
358
|
+
const last = lastClipboardPasteRef.current
|
|
359
|
+
if (!last || last.text !== normalized || now - last.at >= 400) {
|
|
360
|
+
addPastedBlock(pasteBuffer)
|
|
361
|
+
}
|
|
120
362
|
setPasteBuffer('')
|
|
363
|
+
lastBracketedPasteAtRef.current = now
|
|
121
364
|
}
|
|
122
365
|
return
|
|
123
366
|
}
|
|
@@ -127,6 +370,46 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
127
370
|
return
|
|
128
371
|
}
|
|
129
372
|
|
|
373
|
+
if ((key.name === 'z' && key.ctrl) || key.sequence === '\x1a') {
|
|
374
|
+
const lastPaste = lastPasteUndoRef.current
|
|
375
|
+
if (lastPaste && valueRef.current === lastPaste.nextValue) {
|
|
376
|
+
setValue(lastPaste.prevValue)
|
|
377
|
+
setCursorPosition(lastPaste.prevCursor)
|
|
378
|
+
lastPasteUndoRef.current = null
|
|
379
|
+
pasteFlagRef.current = false
|
|
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
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if ((key.name === 'v' && key.ctrl) || key.sequence === '\x16') {
|
|
407
|
+
pasteFromClipboard()
|
|
408
|
+
setHistoryIndex(-1)
|
|
409
|
+
desiredCursorColRef.current = null
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
130
413
|
if (key.name === 'k' && (key.ctrl || key.meta || (key as any).alt)) {
|
|
131
414
|
setValue('')
|
|
132
415
|
setCursorPosition(0)
|
|
@@ -135,10 +418,18 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
135
418
|
pasteFlagRef.current = false
|
|
136
419
|
pastedContentRef.current = ''
|
|
137
420
|
desiredCursorColRef.current = null
|
|
421
|
+
setSelectionStart(null)
|
|
422
|
+
setSelectionEnd(null)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (key.name === 'g' && (key.ctrl || key.meta)) {
|
|
427
|
+
openExternalEditor()
|
|
138
428
|
return
|
|
139
429
|
}
|
|
140
430
|
|
|
141
431
|
if (key.name === 'return') {
|
|
432
|
+
if (submitDisabled) return
|
|
142
433
|
const meta: InputSubmitMeta | undefined = pasteFlagRef.current
|
|
143
434
|
? { isPaste: true, pastedContent: pastedContentRef.current }
|
|
144
435
|
: undefined
|
|
@@ -151,32 +442,63 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
151
442
|
pasteFlagRef.current = false
|
|
152
443
|
pastedContentRef.current = ''
|
|
153
444
|
desiredCursorColRef.current = null
|
|
445
|
+
setSelectionStart(null)
|
|
446
|
+
setSelectionEnd(null)
|
|
154
447
|
} else if (key.name === 'backspace') {
|
|
155
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
|
+
}
|
|
156
460
|
if (cursorPosition > 0) {
|
|
157
461
|
setValue(prev => prev.slice(0, cursorPosition - 1) + prev.slice(cursorPosition))
|
|
158
462
|
setCursorPosition(prev => prev - 1)
|
|
159
463
|
}
|
|
160
464
|
} else if (key.name === 'delete') {
|
|
161
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
|
+
}
|
|
162
477
|
if (key.ctrl || key.meta) {
|
|
163
478
|
setValue('')
|
|
164
479
|
setCursorPosition(0)
|
|
165
480
|
pasteFlagRef.current = false
|
|
166
481
|
pastedContentRef.current = ''
|
|
482
|
+
setSelectionStart(null)
|
|
483
|
+
setSelectionEnd(null)
|
|
167
484
|
} else if (cursorPosition < value.length) {
|
|
168
485
|
setValue(prev => prev.slice(0, cursorPosition) + prev.slice(cursorPosition + 1))
|
|
169
486
|
}
|
|
170
487
|
} else if (key.name === 'up') {
|
|
488
|
+
const lineWidth = Math.max(10, terminalWidth - 4)
|
|
489
|
+
const { lineLengths, cursorLine: currentCursorLine, cursorCol: currentCursorCol, lineStarts } = wrapTextWithCursor(value, cursorPosition, lineWidth)
|
|
171
490
|
if (currentCursorLine > 0) {
|
|
172
491
|
if (desiredCursorColRef.current === null) {
|
|
173
492
|
desiredCursorColRef.current = currentCursorCol
|
|
174
493
|
}
|
|
175
494
|
const targetLine = currentCursorLine - 1
|
|
176
495
|
const targetCol = desiredCursorColRef.current
|
|
177
|
-
const targetLineLen =
|
|
178
|
-
const
|
|
179
|
-
|
|
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)
|
|
180
502
|
return
|
|
181
503
|
}
|
|
182
504
|
|
|
@@ -191,22 +513,31 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
191
513
|
setHistoryIndex(newIndex)
|
|
192
514
|
setValue(inputHistory[newIndex]!)
|
|
193
515
|
setCursorPosition(inputHistory[newIndex]!.length)
|
|
516
|
+
setSelectionStart(null)
|
|
517
|
+
setSelectionEnd(null)
|
|
194
518
|
} else if (historyIndex > 0) {
|
|
195
519
|
const newIndex = historyIndex - 1
|
|
196
520
|
setHistoryIndex(newIndex)
|
|
197
521
|
setValue(inputHistory[newIndex]!)
|
|
198
522
|
setCursorPosition(inputHistory[newIndex]!.length)
|
|
523
|
+
setSelectionStart(null)
|
|
524
|
+
setSelectionEnd(null)
|
|
199
525
|
}
|
|
200
526
|
} else if (key.name === 'down') {
|
|
201
|
-
|
|
527
|
+
const lineWidth = Math.max(10, terminalWidth - 4)
|
|
528
|
+
const { lineLengths, cursorLine: currentCursorLine, cursorCol: currentCursorCol, lineStarts } = wrapTextWithCursor(value, cursorPosition, lineWidth)
|
|
529
|
+
if (currentCursorLine < lineStarts.length - 1) {
|
|
202
530
|
if (desiredCursorColRef.current === null) {
|
|
203
531
|
desiredCursorColRef.current = currentCursorCol
|
|
204
532
|
}
|
|
205
533
|
const targetLine = currentCursorLine + 1
|
|
206
534
|
const targetCol = desiredCursorColRef.current
|
|
207
|
-
const targetLineLen =
|
|
208
|
-
const
|
|
209
|
-
|
|
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)
|
|
210
541
|
return
|
|
211
542
|
}
|
|
212
543
|
|
|
@@ -220,109 +551,162 @@ export function CustomInput({ onSubmit, placeholder = '', password = false, focu
|
|
|
220
551
|
setHistoryIndex(newIndex)
|
|
221
552
|
setValue(inputHistory[newIndex]!)
|
|
222
553
|
setCursorPosition(inputHistory[newIndex]!.length)
|
|
554
|
+
setSelectionStart(null)
|
|
555
|
+
setSelectionEnd(null)
|
|
223
556
|
} else {
|
|
224
557
|
setHistoryIndex(-1)
|
|
225
558
|
setValue(currentInput)
|
|
226
559
|
setCursorPosition(currentInput.length)
|
|
560
|
+
setSelectionStart(null)
|
|
561
|
+
setSelectionEnd(null)
|
|
227
562
|
}
|
|
228
563
|
} else if (key.name === 'left') {
|
|
229
564
|
desiredCursorColRef.current = null
|
|
230
565
|
setCursorPosition(prev => Math.max(0, prev - 1))
|
|
566
|
+
setSelectionStart(null)
|
|
567
|
+
setSelectionEnd(null)
|
|
231
568
|
} else if (key.name === 'right') {
|
|
232
569
|
desiredCursorColRef.current = null
|
|
233
570
|
setCursorPosition(prev => Math.min(value.length, prev + 1))
|
|
571
|
+
setSelectionStart(null)
|
|
572
|
+
setSelectionEnd(null)
|
|
234
573
|
} else if (key.name === 'home') {
|
|
235
574
|
desiredCursorColRef.current = null
|
|
236
575
|
setCursorPosition(0)
|
|
576
|
+
setSelectionStart(null)
|
|
577
|
+
setSelectionEnd(null)
|
|
237
578
|
} else if (key.name === 'end') {
|
|
238
579
|
desiredCursorColRef.current = null
|
|
239
580
|
setCursorPosition(value.length)
|
|
581
|
+
setSelectionStart(null)
|
|
582
|
+
setSelectionEnd(null)
|
|
240
583
|
} else if (key.sequence && key.sequence.length > 1 && !key.ctrl && !key.meta && !key.name) {
|
|
241
584
|
addPastedBlock(key.sequence)
|
|
242
585
|
setHistoryIndex(-1)
|
|
243
586
|
desiredCursorColRef.current = null
|
|
587
|
+
setSelectionStart(null)
|
|
588
|
+
setSelectionEnd(null)
|
|
244
589
|
} else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
245
590
|
const char = key.sequence
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
}
|
|
248
604
|
setHistoryIndex(-1)
|
|
249
605
|
desiredCursorColRef.current = null
|
|
250
606
|
}
|
|
251
607
|
})
|
|
252
608
|
|
|
253
|
-
const
|
|
254
|
-
const displayValue = password && value ? '•'.repeat(value.length) : typedDisplay
|
|
255
|
-
const cursorChar = '█'
|
|
609
|
+
const displayValue = password && value ? Array.from(value, (char) => (char === '\n' ? '\n' : '•')).join('') : value
|
|
256
610
|
const isEmpty = value.length === 0
|
|
257
611
|
|
|
258
|
-
const lineWidth = Math.max(10, terminalWidth - 4)
|
|
259
|
-
|
|
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
|
-
}
|
|
612
|
+
const lineWidth = Math.max(10, maxWidth ?? (terminalWidth - 4))
|
|
287
613
|
|
|
288
614
|
if (isEmpty) {
|
|
289
615
|
if (!placeholder) {
|
|
290
616
|
return (
|
|
291
617
|
<box flexDirection="column" flexGrow={1} width="100%">
|
|
292
618
|
<box flexDirection="row">
|
|
293
|
-
<text>
|
|
619
|
+
<text fg="black" bg="white"> </text>
|
|
294
620
|
</box>
|
|
295
621
|
</box>
|
|
296
622
|
)
|
|
297
623
|
}
|
|
624
|
+
const firstChar = placeholder[0] || ' '
|
|
298
625
|
return (
|
|
299
626
|
<box flexDirection="column" flexGrow={1} width="100%">
|
|
300
627
|
<box flexDirection="row">
|
|
301
|
-
<text>{
|
|
302
|
-
{placeholder.slice(1) && <text attributes={TextAttributes.DIM}>{placeholder.slice(1)}</text>}
|
|
628
|
+
<text fg="gray" bg="white" attributes={TextAttributes.DIM}>{firstChar}</text>
|
|
629
|
+
{placeholder.slice(1) && <text fg="gray" attributes={TextAttributes.DIM}>{placeholder.slice(1)}</text>}
|
|
303
630
|
</box>
|
|
304
631
|
</box>
|
|
305
632
|
)
|
|
306
633
|
}
|
|
307
634
|
|
|
308
|
-
const {
|
|
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
|
+
})()
|
|
309
645
|
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
const afterCursor = line.slice(cursorCol)
|
|
314
|
-
return beforeCursor + cursorChar + afterCursor
|
|
646
|
+
const buildSelectionSegments = (text: string, absStart: number) => {
|
|
647
|
+
if (!selectionRange || text.length === 0) {
|
|
648
|
+
return [{ text, selected: false }]
|
|
315
649
|
}
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
}
|
|
318
667
|
|
|
319
668
|
return (
|
|
320
669
|
<box flexDirection="column" flexGrow={1} width="100%">
|
|
321
|
-
{
|
|
670
|
+
{lines.map((line, lineIndex) => (
|
|
322
671
|
<box key={lineIndex} flexDirection="row">
|
|
323
|
-
|
|
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
|
+
})()}
|
|
324
708
|
</box>
|
|
325
709
|
))}
|
|
326
710
|
</box>
|
|
327
711
|
)
|
|
328
|
-
}
|
|
712
|
+
}
|