@kirosnn/mosaic 0.0.7

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 (154) hide show
  1. package/.mosaic/mosaic.local.jsonc +0 -0
  2. package/MOSAIC.md +188 -0
  3. package/README.md +127 -0
  4. package/docs/mosaic.png +0 -0
  5. package/package.json +42 -0
  6. package/src/agent/Agent.ts +131 -0
  7. package/src/agent/context.ts +96 -0
  8. package/src/agent/index.ts +2 -0
  9. package/src/agent/prompts/systemPrompt.ts +138 -0
  10. package/src/agent/prompts/toolsPrompt.ts +139 -0
  11. package/src/agent/provider/anthropic.ts +122 -0
  12. package/src/agent/provider/google.ts +124 -0
  13. package/src/agent/provider/mistral.ts +117 -0
  14. package/src/agent/provider/ollama.ts +531 -0
  15. package/src/agent/provider/openai.ts +220 -0
  16. package/src/agent/provider/xai.ts +122 -0
  17. package/src/agent/tools/bash.ts +20 -0
  18. package/src/agent/tools/definitions.ts +27 -0
  19. package/src/agent/tools/edit.ts +23 -0
  20. package/src/agent/tools/executor.ts +751 -0
  21. package/src/agent/tools/explore.ts +18 -0
  22. package/src/agent/tools/exploreExecutor.ts +320 -0
  23. package/src/agent/tools/glob.ts +16 -0
  24. package/src/agent/tools/grep.ts +19 -0
  25. package/src/agent/tools/index.ts +4 -0
  26. package/src/agent/tools/list.ts +20 -0
  27. package/src/agent/tools/question.ts +20 -0
  28. package/src/agent/tools/read.ts +15 -0
  29. package/src/agent/tools/write.ts +21 -0
  30. package/src/agent/types.ts +155 -0
  31. package/src/components/App.tsx +174 -0
  32. package/src/components/CommandsModal.tsx +77 -0
  33. package/src/components/CustomInput.tsx +328 -0
  34. package/src/components/Main.tsx +1112 -0
  35. package/src/components/Notification.tsx +91 -0
  36. package/src/components/SelectList.tsx +47 -0
  37. package/src/components/Setup.tsx +528 -0
  38. package/src/components/ShortcutsModal.tsx +67 -0
  39. package/src/components/Welcome.tsx +39 -0
  40. package/src/components/main/ApprovalPanel.tsx +134 -0
  41. package/src/components/main/ChatPage.tsx +516 -0
  42. package/src/components/main/HomePage.tsx +111 -0
  43. package/src/components/main/QuestionPanel.tsx +85 -0
  44. package/src/components/main/ThinkingIndicator.tsx +101 -0
  45. package/src/components/main/types.ts +55 -0
  46. package/src/components/main/wrapText.ts +41 -0
  47. package/src/index.tsx +212 -0
  48. package/src/utils/approvalBridge.ts +129 -0
  49. package/src/utils/commands/echo.ts +22 -0
  50. package/src/utils/commands/help.ts +25 -0
  51. package/src/utils/commands/index.ts +68 -0
  52. package/src/utils/commands/init.ts +68 -0
  53. package/src/utils/commands/redo.ts +74 -0
  54. package/src/utils/commands/registry.ts +29 -0
  55. package/src/utils/commands/sessions.ts +129 -0
  56. package/src/utils/commands/types.ts +20 -0
  57. package/src/utils/commands/undo.ts +75 -0
  58. package/src/utils/commands/web.ts +77 -0
  59. package/src/utils/config.ts +357 -0
  60. package/src/utils/diff.ts +201 -0
  61. package/src/utils/diffRendering.tsx +62 -0
  62. package/src/utils/exploreBridge.ts +87 -0
  63. package/src/utils/fileChangeTracker.ts +98 -0
  64. package/src/utils/fileChangesBridge.ts +18 -0
  65. package/src/utils/history.ts +106 -0
  66. package/src/utils/markdown.tsx +232 -0
  67. package/src/utils/models.ts +304 -0
  68. package/src/utils/questionBridge.ts +122 -0
  69. package/src/utils/terminalUtils.ts +25 -0
  70. package/src/utils/toolFormatting.ts +384 -0
  71. package/src/utils/undoRedo.ts +429 -0
  72. package/src/utils/undoRedoBridge.ts +45 -0
  73. package/src/utils/undoRedoDb.ts +338 -0
  74. package/src/utils/uninstall.ts +45 -0
  75. package/src/utils/version.ts +3 -0
  76. package/src/web/app.tsx +606 -0
  77. package/src/web/assets/css/ChatPage.css +212 -0
  78. package/src/web/assets/css/FileExplorer.css +202 -0
  79. package/src/web/assets/css/HomePage.css +119 -0
  80. package/src/web/assets/css/Markdown.css +178 -0
  81. package/src/web/assets/css/MessageItem.css +160 -0
  82. package/src/web/assets/css/Sidebar.css +208 -0
  83. package/src/web/assets/css/SidebarModal.css +137 -0
  84. package/src/web/assets/css/ThinkingIndicator.css +47 -0
  85. package/src/web/assets/css/ToolMessage.css +148 -0
  86. package/src/web/assets/css/global.css +226 -0
  87. package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
  88. package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
  89. package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
  90. package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
  91. package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
  92. package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
  93. package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
  94. package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
  95. package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
  96. package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
  97. package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
  98. package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
  99. package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
  100. package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
  101. package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
  102. package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
  103. package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
  104. package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
  105. package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
  106. package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
  107. package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
  108. package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
  109. package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
  110. package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
  111. package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
  112. package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
  113. package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
  114. package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
  115. package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
  116. package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
  117. package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
  118. package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
  119. package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
  120. package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
  121. package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
  122. package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
  123. package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
  124. package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
  125. package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
  126. package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
  127. package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
  128. package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
  129. package/src/web/assets/images/favicon-v2.svg +6 -0
  130. package/src/web/assets/images/favicon.png +0 -0
  131. package/src/web/assets/images/foruse.svg +5 -0
  132. package/src/web/assets/images/logo_black.svg +5 -0
  133. package/src/web/assets/images/logo_white.svg +5 -0
  134. package/src/web/assets/images/logoblack.png +0 -0
  135. package/src/web/assets/images/logowhite.png +0 -0
  136. package/src/web/build.ts +23 -0
  137. package/src/web/components/ApprovalPanel.tsx +191 -0
  138. package/src/web/components/ChatPage.tsx +273 -0
  139. package/src/web/components/FileExplorer.tsx +162 -0
  140. package/src/web/components/HomePage.tsx +121 -0
  141. package/src/web/components/MessageItem.tsx +178 -0
  142. package/src/web/components/Modal.tsx +30 -0
  143. package/src/web/components/QuestionPanel.tsx +149 -0
  144. package/src/web/components/Setup.tsx +211 -0
  145. package/src/web/components/Sidebar.tsx +292 -0
  146. package/src/web/components/ThinkingIndicator.tsx +85 -0
  147. package/src/web/logo_black.svg +5 -0
  148. package/src/web/logo_white.svg +5 -0
  149. package/src/web/router.ts +46 -0
  150. package/src/web/server.tsx +662 -0
  151. package/src/web/storage.ts +92 -0
  152. package/src/web/types.ts +17 -0
  153. package/src/web/utils.ts +61 -0
  154. package/tsconfig.json +33 -0
@@ -0,0 +1,174 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import type { KeyEvent } from '@opentui/core';
3
+ import { isFirstRun, markFirstRunComplete } from '../utils/config';
4
+ import { useRenderer } from '@opentui/react';
5
+ import { Welcome } from './Welcome';
6
+ import { Setup } from './Setup';
7
+ import { Main } from './Main';
8
+ import { ShortcutsModal } from './ShortcutsModal';
9
+ import { CommandModal } from './CommandsModal';
10
+ import { Notification, type NotificationData } from './Notification';
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ type AppScreen = 'welcome' | 'setup' | 'main';
17
+
18
+ interface AppProps {
19
+ initialMessage?: string;
20
+ }
21
+
22
+ export function App({ initialMessage }: AppProps) {
23
+ const [screen, setScreen] = useState<AppScreen>('main');
24
+ const [isReady, setIsReady] = useState(false);
25
+ const [pasteRequestId, setPasteRequestId] = useState(0);
26
+ const [copyRequestId, setCopyRequestId] = useState(0);
27
+ const [shortcutsOpen, setShortcutsOpen] = useState(false);
28
+ const [shortcutsTab, setShortcutsTab] = useState<0 | 1>(0);
29
+ const [commandsOpen, setCommandsOpen] = useState(false);
30
+ const [notifications, setNotifications] = useState<NotificationData[]>([]);
31
+ const [pendingMessage, setPendingMessage] = useState<string | undefined>(initialMessage);
32
+
33
+ const renderer = useRenderer();
34
+
35
+ const addNotification = useCallback((message: string, type: NotificationData['type'] = 'info', duration?: number) => {
36
+ const id = `${Date.now()}-${Math.random()}`;
37
+ setNotifications(prev => [...prev, { id, message, type, duration }]);
38
+ }, []);
39
+
40
+ const removeNotification = useCallback((id: string) => {
41
+ setNotifications(prev => prev.filter(n => n.id !== id));
42
+ }, []);
43
+
44
+ const copyToClipboard = async (text: string) => {
45
+ try {
46
+ if (process.platform === 'win32') {
47
+ const escaped = text.replace(/'/g, "''");
48
+ await execAsync(`powershell -command "Set-Clipboard -Value '${escaped}'"`);
49
+ } else if (process.platform === 'darwin') {
50
+ await execAsync(`echo ${JSON.stringify(text)} | pbcopy`);
51
+ } else {
52
+ await execAsync(`echo ${JSON.stringify(text)} | xclip -selection clipboard`);
53
+ }
54
+ } catch (error) {
55
+ console.error('Failed to copy to clipboard:', error);
56
+ }
57
+ };
58
+
59
+ useEffect(() => {
60
+ const isDarwin = process.platform === 'darwin';
61
+
62
+ const handleKeyPress = (key: KeyEvent) => {
63
+ const k = key as any;
64
+
65
+ if (k.name === 'escape') {
66
+ setShortcutsOpen(false);
67
+ setCommandsOpen(false);
68
+ return;
69
+ }
70
+
71
+ if (shortcutsOpen && (k.name === 'f1' || k.name === 'f2')) {
72
+ setShortcutsTab(k.name === 'f2' ? 1 : 0);
73
+ return;
74
+ }
75
+
76
+ const seq = k.sequence;
77
+
78
+ if (k.name === 'v' && (k.ctrl || (isDarwin && k.meta && !k.alt) || (!isDarwin && (k.alt || k.meta) && !k.ctrl)) || seq === '\x16') {
79
+ setPasteRequestId(prev => prev + 1);
80
+ return;
81
+ }
82
+
83
+ if (k.name === 'p' && (k.ctrl || (!isDarwin && (k.alt || k.meta) && !k.ctrl)) || seq === '\x10') {
84
+ setShortcutsOpen(prev => !prev);
85
+ return;
86
+ }
87
+
88
+ if (k.name === 'o' && (k.ctrl || (!isDarwin && (k.alt || k.meta) && !k.ctrl)) || seq === '\x0f') {
89
+ setCommandsOpen(prev => !prev);
90
+ return;
91
+ }
92
+
93
+ if (k.name === 'c' && !k.shift && (k.ctrl || (isDarwin && k.meta && !k.alt) || (!isDarwin && (k.alt || k.meta) && !k.ctrl)) || seq === '\x03') {
94
+ setCopyRequestId(prev => prev + 1);
95
+ return;
96
+ }
97
+ };
98
+
99
+ renderer.keyInput.on('keypress', handleKeyPress);
100
+ return () => {
101
+ renderer.keyInput.off('keypress', handleKeyPress);
102
+ };
103
+ }, [renderer.keyInput, shortcutsOpen]);
104
+
105
+ useEffect(() => {
106
+ const checkFirstRun = async () => {
107
+ const firstRun = isFirstRun();
108
+ if (firstRun) {
109
+ setScreen('welcome');
110
+ if (pendingMessage) {
111
+ addNotification('Please complete setup first', 'error', 5000);
112
+ }
113
+ } else {
114
+ setScreen('main');
115
+ }
116
+ setIsReady(true);
117
+ };
118
+
119
+ checkFirstRun();
120
+ }, [pendingMessage, addNotification]);
121
+
122
+ const handleWelcomeComplete = () => {
123
+ setScreen('setup');
124
+ };
125
+
126
+ const handleSetupComplete = (provider: string, model: string, apiKey?: string) => {
127
+ markFirstRunComplete(provider, model, apiKey);
128
+ setScreen('main');
129
+ };
130
+
131
+ if (!isReady) {
132
+ return null;
133
+ }
134
+
135
+ if (screen === 'welcome') {
136
+ return (
137
+ <box width="100%" height="100%">
138
+ <Welcome onComplete={handleWelcomeComplete} isFirstRun={true} shortcutsOpen={shortcutsOpen} commandsOpen={commandsOpen} />
139
+ {shortcutsOpen && <ShortcutsModal activeTab={shortcutsTab} />}
140
+ {commandsOpen && <CommandModal />}
141
+ <Notification notifications={notifications} onRemove={removeNotification} />
142
+ </box>
143
+ );
144
+ }
145
+
146
+ if (screen === 'setup') {
147
+ return (
148
+ <box width="100%" height="100%">
149
+ <Setup onComplete={handleSetupComplete} pasteRequestId={pasteRequestId} shortcutsOpen={shortcutsOpen} commandsOpen={commandsOpen} />
150
+ {shortcutsOpen && <ShortcutsModal activeTab={shortcutsTab} />}
151
+ {commandsOpen && <CommandModal />}
152
+ <Notification notifications={notifications} onRemove={removeNotification} />
153
+ </box>
154
+ );
155
+ }
156
+
157
+ return (
158
+ <box width="100%" height="100%">
159
+ <Main
160
+ pasteRequestId={pasteRequestId}
161
+ copyRequestId={copyRequestId}
162
+ onCopy={copyToClipboard}
163
+ shortcutsOpen={shortcutsOpen}
164
+ commandsOpen={commandsOpen}
165
+ initialMessage={pendingMessage}
166
+ />
167
+ {shortcutsOpen && <ShortcutsModal activeTab={shortcutsTab} />}
168
+ {commandsOpen && <CommandModal />}
169
+ <Notification notifications={notifications} onRemove={removeNotification} />
170
+ </box>
171
+ );
172
+ }
173
+
174
+ export default App;
@@ -0,0 +1,77 @@
1
+ import { TextAttributes } from "@opentui/core";
2
+ import { commandRegistry } from "../utils/commands";
3
+
4
+ interface CommandItem {
5
+ name: string;
6
+ usage?: string;
7
+ description: string;
8
+ aliases?: string[];
9
+ }
10
+
11
+ export function CommandModal() {
12
+ const commands = Array.from(commandRegistry.getAll().entries())
13
+ .filter(([name, cmd]) => name === cmd.name)
14
+ .map(([name, cmd]): CommandItem => ({
15
+ name,
16
+ usage: cmd.usage,
17
+ description: cmd.description,
18
+ aliases: cmd.aliases
19
+ }))
20
+ .sort((a, b) => a.name.localeCompare(b.name));
21
+
22
+ return (
23
+ <box position="absolute" top={0} left={0} right={0} bottom={0} backgroundColor={"#0c0c0c"}>
24
+ <box width="100%" height="100%" justifyContent="center" alignItems="center">
25
+ <box flexDirection="column" width="80%" height="80%" backgroundColor="#1a1a1a" padding={2}>
26
+ <box marginBottom={1} flexDirection="row" justifyContent="space-between" width="100%">
27
+ <text attributes={TextAttributes.BOLD}>Available Commands</text>
28
+ <text attributes={TextAttributes.DIM}>Esc to close</text>
29
+ </box>
30
+
31
+ <box flexDirection="column" width="100%" flexGrow={1} overflow="hidden">
32
+ <box flexDirection="column" width="100%" overflow="scroll">
33
+ {commands.length === 0 ? (
34
+ <box flexDirection="row" width="100%" marginBottom={1}>
35
+ <text attributes={TextAttributes.DIM}>No commands available</text>
36
+ </box>
37
+ ) : (
38
+ commands.map((cmd, idx) => (
39
+ <box key={idx} flexDirection="column" width="100%" marginBottom={1}>
40
+ <box flexDirection="row" width="100%">
41
+ <box width={12}>
42
+ <text fg="#ffca38" attributes={TextAttributes.BOLD}>/{cmd.name}</text>
43
+ </box>
44
+ <box flexGrow={1} minWidth={0}>
45
+ <text>{cmd.description}</text>
46
+ </box>
47
+ </box>
48
+ {cmd.usage && (
49
+ <box flexDirection="row" width="100%" marginTop={0}>
50
+ <box width={12}>
51
+ <text attributes={TextAttributes.DIM}> </text>
52
+ </box>
53
+ <box flexGrow={1} minWidth={0}>
54
+ <text attributes={TextAttributes.DIM}>{cmd.usage}</text>
55
+ </box>
56
+ </box>
57
+ )}
58
+ {cmd.aliases && cmd.aliases.length > 0 && (
59
+ <box flexDirection="row" width="100%" marginTop={0}>
60
+ <box width={12}>
61
+ <text attributes={TextAttributes.DIM}> </text>
62
+ </box>
63
+ <box flexGrow={1} minWidth={0}>
64
+ <text attributes={TextAttributes.DIM}>Aliases: {cmd.aliases.join(", ")}</text>
65
+ </box>
66
+ </box>
67
+ )}
68
+ </box>
69
+ ))
70
+ )}
71
+ </box>
72
+ </box>
73
+ </box>
74
+ </box>
75
+ </box>
76
+ );
77
+ }
@@ -0,0 +1,328 @@
1
+ import { useState, useEffect, useRef } from 'react'
2
+ import { TextAttributes } from "@opentui/core"
3
+ import { useKeyboard } from "@opentui/react"
4
+ import { execSync } from 'child_process'
5
+ import { getInputHistory } from '../utils/history'
6
+
7
+ export interface InputSubmitMeta {
8
+ isPaste?: boolean
9
+ pastedContent?: string
10
+ }
11
+
12
+ interface CustomInputProps {
13
+ onSubmit: (value: string, meta?: InputSubmitMeta) => void
14
+ placeholder?: string
15
+ password?: boolean
16
+ focused?: boolean
17
+ pasteRequestId?: number
18
+ disableHistory?: boolean
19
+ }
20
+
21
+ export function CustomInput({ onSubmit, placeholder = '', password = false, focused = true, pasteRequestId = 0, disableHistory = false }: CustomInputProps) {
22
+ const [value, setValue] = useState('')
23
+ const [cursorPosition, setCursorPosition] = useState(0)
24
+ const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80)
25
+ const [pasteBuffer, setPasteBuffer] = useState('')
26
+ const [inPasteMode, setInPasteMode] = useState(false)
27
+ const [historyIndex, setHistoryIndex] = useState(-1)
28
+ const [currentInput, setCurrentInput] = useState('')
29
+ const [inputHistory, setInputHistory] = useState<string[]>([])
30
+
31
+ const pasteFlagRef = useRef(false)
32
+ const pastedContentRef = useRef('')
33
+ const desiredCursorColRef = useRef<number | null>(null)
34
+
35
+ const normalizePastedText = (text: string) => text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
36
+
37
+ const addPastedBlock = (pastedText: string) => {
38
+ const normalized = normalizePastedText(pastedText)
39
+ if (!normalized) return
40
+
41
+ setValue(prev => prev.slice(0, cursorPosition) + normalized + prev.slice(cursorPosition))
42
+ setCursorPosition(prev => prev + normalized.length)
43
+ pasteFlagRef.current = true
44
+ pastedContentRef.current = normalized
45
+ }
46
+
47
+ useEffect(() => {
48
+ setInputHistory(getInputHistory())
49
+ }, [])
50
+
51
+ useEffect(() => {
52
+ const handleResize = () => {
53
+ setTerminalWidth(process.stdout.columns || 80)
54
+ }
55
+ process.stdout.on('resize', handleResize)
56
+ return () => {
57
+ process.stdout.off('resize', handleResize)
58
+ }
59
+ }, [])
60
+
61
+ useEffect(() => {
62
+ if (!focused) return
63
+
64
+ process.stdout.write('\x1b[?2004h')
65
+
66
+ return () => {
67
+ process.stdout.write('\x1b[?2004l')
68
+ }
69
+ }, [focused])
70
+
71
+ const pasteFromClipboard = () => {
72
+ try {
73
+ let clipboardText = ''
74
+ if (process.platform === 'win32') {
75
+ clipboardText = execSync('powershell.exe -command "Get-Clipboard"', { encoding: 'utf8', timeout: 2000 })
76
+ } else if (process.platform === 'darwin') {
77
+ clipboardText = execSync('pbpaste', { encoding: 'utf8', timeout: 2000 })
78
+ } else {
79
+ clipboardText = execSync('xclip -selection clipboard -o', { encoding: 'utf8', timeout: 2000 })
80
+ }
81
+ if (clipboardText) {
82
+ addPastedBlock(clipboardText)
83
+ }
84
+ } catch (error) {
85
+ }
86
+ }
87
+
88
+ useEffect(() => {
89
+ if (!focused) return
90
+ if (!pasteRequestId) return
91
+ pasteFromClipboard()
92
+ }, [pasteRequestId, focused])
93
+
94
+ useKeyboard((key) => {
95
+ if (!focused) return
96
+
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)
109
+
110
+ if (key.sequence && key.sequence.includes('\x1b[200~')) {
111
+ setInPasteMode(true)
112
+ setPasteBuffer('')
113
+ return
114
+ }
115
+
116
+ if (key.sequence && key.sequence.includes('\x1b[201~')) {
117
+ setInPasteMode(false)
118
+ if (pasteBuffer) {
119
+ addPastedBlock(pasteBuffer)
120
+ setPasteBuffer('')
121
+ }
122
+ return
123
+ }
124
+
125
+ if (inPasteMode) {
126
+ setPasteBuffer(prev => prev + (key.sequence || ''))
127
+ return
128
+ }
129
+
130
+ if (key.name === 'k' && (key.ctrl || key.meta || (key as any).alt)) {
131
+ setValue('')
132
+ setCursorPosition(0)
133
+ setHistoryIndex(-1)
134
+ setCurrentInput('')
135
+ pasteFlagRef.current = false
136
+ pastedContentRef.current = ''
137
+ desiredCursorColRef.current = null
138
+ return
139
+ }
140
+
141
+ if (key.name === 'return') {
142
+ const meta: InputSubmitMeta | undefined = pasteFlagRef.current
143
+ ? { isPaste: true, pastedContent: pastedContentRef.current }
144
+ : undefined
145
+ onSubmit(value, meta)
146
+ setValue('')
147
+ setCursorPosition(0)
148
+ setHistoryIndex(-1)
149
+ setCurrentInput('')
150
+ setInputHistory(getInputHistory())
151
+ pasteFlagRef.current = false
152
+ pastedContentRef.current = ''
153
+ desiredCursorColRef.current = null
154
+ } else if (key.name === 'backspace') {
155
+ desiredCursorColRef.current = null
156
+ if (cursorPosition > 0) {
157
+ setValue(prev => prev.slice(0, cursorPosition - 1) + prev.slice(cursorPosition))
158
+ setCursorPosition(prev => prev - 1)
159
+ }
160
+ } else if (key.name === 'delete') {
161
+ desiredCursorColRef.current = null
162
+ if (key.ctrl || key.meta) {
163
+ setValue('')
164
+ setCursorPosition(0)
165
+ pasteFlagRef.current = false
166
+ pastedContentRef.current = ''
167
+ } else if (cursorPosition < value.length) {
168
+ setValue(prev => prev.slice(0, cursorPosition) + prev.slice(cursorPosition + 1))
169
+ }
170
+ } else if (key.name === 'up') {
171
+ if (currentCursorLine > 0) {
172
+ if (desiredCursorColRef.current === null) {
173
+ desiredCursorColRef.current = currentCursorCol
174
+ }
175
+ const targetLine = currentCursorLine - 1
176
+ const targetCol = desiredCursorColRef.current
177
+ const targetLineLen = displayLines[targetLine]!.length
178
+ const newDisplayPos = (targetLine * lineWidth) + Math.min(targetCol, targetLineLen)
179
+ setCursorPosition(Math.min(value.length, newDisplayPos))
180
+ return
181
+ }
182
+
183
+ if (disableHistory) return
184
+
185
+ desiredCursorColRef.current = null
186
+ if (inputHistory.length === 0) return
187
+
188
+ if (historyIndex === -1) {
189
+ setCurrentInput(value)
190
+ const newIndex = inputHistory.length - 1
191
+ setHistoryIndex(newIndex)
192
+ setValue(inputHistory[newIndex]!)
193
+ setCursorPosition(inputHistory[newIndex]!.length)
194
+ } else if (historyIndex > 0) {
195
+ const newIndex = historyIndex - 1
196
+ setHistoryIndex(newIndex)
197
+ setValue(inputHistory[newIndex]!)
198
+ setCursorPosition(inputHistory[newIndex]!.length)
199
+ }
200
+ } else if (key.name === 'down') {
201
+ if (currentCursorLine < displayLines.length - 1) {
202
+ if (desiredCursorColRef.current === null) {
203
+ desiredCursorColRef.current = currentCursorCol
204
+ }
205
+ const targetLine = currentCursorLine + 1
206
+ const targetCol = desiredCursorColRef.current
207
+ const targetLineLen = displayLines[targetLine]!.length
208
+ const newDisplayPos = (targetLine * lineWidth) + Math.min(targetCol, targetLineLen)
209
+ setCursorPosition(Math.min(value.length, newDisplayPos))
210
+ return
211
+ }
212
+
213
+ if (disableHistory) return
214
+
215
+ desiredCursorColRef.current = null
216
+ if (historyIndex === -1) return
217
+
218
+ if (historyIndex < inputHistory.length - 1) {
219
+ const newIndex = historyIndex + 1
220
+ setHistoryIndex(newIndex)
221
+ setValue(inputHistory[newIndex]!)
222
+ setCursorPosition(inputHistory[newIndex]!.length)
223
+ } else {
224
+ setHistoryIndex(-1)
225
+ setValue(currentInput)
226
+ setCursorPosition(currentInput.length)
227
+ }
228
+ } else if (key.name === 'left') {
229
+ desiredCursorColRef.current = null
230
+ setCursorPosition(prev => Math.max(0, prev - 1))
231
+ } else if (key.name === 'right') {
232
+ desiredCursorColRef.current = null
233
+ setCursorPosition(prev => Math.min(value.length, prev + 1))
234
+ } else if (key.name === 'home') {
235
+ desiredCursorColRef.current = null
236
+ setCursorPosition(0)
237
+ } else if (key.name === 'end') {
238
+ desiredCursorColRef.current = null
239
+ setCursorPosition(value.length)
240
+ } else if (key.sequence && key.sequence.length > 1 && !key.ctrl && !key.meta && !key.name) {
241
+ addPastedBlock(key.sequence)
242
+ setHistoryIndex(-1)
243
+ desiredCursorColRef.current = null
244
+ } else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
245
+ const char = key.sequence
246
+ setValue(prev => prev.slice(0, cursorPosition) + char + prev.slice(cursorPosition))
247
+ setCursorPosition(prev => prev + char.length)
248
+ setHistoryIndex(-1)
249
+ desiredCursorColRef.current = null
250
+ }
251
+ })
252
+
253
+ const typedDisplay = value.replace(/\n/g, ' ')
254
+ const displayValue = password && value ? '•'.repeat(value.length) : typedDisplay
255
+ const cursorChar = '█'
256
+ const isEmpty = value.length === 0
257
+
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
+ }
287
+
288
+ if (isEmpty) {
289
+ if (!placeholder) {
290
+ return (
291
+ <box flexDirection="column" flexGrow={1} width="100%">
292
+ <box flexDirection="row">
293
+ <text>{cursorChar}</text>
294
+ </box>
295
+ </box>
296
+ )
297
+ }
298
+ return (
299
+ <box flexDirection="column" flexGrow={1} width="100%">
300
+ <box flexDirection="row">
301
+ <text>{cursorChar}</text>
302
+ {placeholder.slice(1) && <text attributes={TextAttributes.DIM}>{placeholder.slice(1)}</text>}
303
+ </box>
304
+ </box>
305
+ )
306
+ }
307
+
308
+ const { lines, cursorLine, cursorCol } = wrapTextWithCursor(displayValue, cursorPosition, lineWidth)
309
+
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
+ return (
320
+ <box flexDirection="column" flexGrow={1} width="100%">
321
+ {renderedLines.map((renderedLine, lineIndex) => (
322
+ <box key={lineIndex} flexDirection="row">
323
+ <text>{renderedLine}</text>
324
+ </box>
325
+ ))}
326
+ </box>
327
+ )
328
+ }