@shareai-lab/kode 1.0.69 → 1.0.71
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/README.md +205 -72
- package/README.zh-CN.md +246 -0
- package/cli.js +62 -0
- package/package.json +45 -25
- package/scripts/postinstall.js +56 -0
- package/src/ProjectOnboarding.tsx +180 -0
- package/src/Tool.ts +53 -0
- package/src/commands/approvedTools.ts +53 -0
- package/src/commands/bug.tsx +20 -0
- package/src/commands/clear.ts +43 -0
- package/src/commands/compact.ts +120 -0
- package/src/commands/config.tsx +19 -0
- package/src/commands/cost.ts +18 -0
- package/src/commands/ctx_viz.ts +209 -0
- package/src/commands/doctor.ts +24 -0
- package/src/commands/help.tsx +19 -0
- package/src/commands/init.ts +37 -0
- package/src/commands/listen.ts +42 -0
- package/src/commands/login.tsx +51 -0
- package/src/commands/logout.tsx +40 -0
- package/src/commands/mcp.ts +41 -0
- package/src/commands/model.tsx +40 -0
- package/src/commands/modelstatus.tsx +20 -0
- package/src/commands/onboarding.tsx +34 -0
- package/src/commands/pr_comments.ts +59 -0
- package/src/commands/refreshCommands.ts +54 -0
- package/src/commands/release-notes.ts +34 -0
- package/src/commands/resume.tsx +30 -0
- package/src/commands/review.ts +49 -0
- package/src/commands/terminalSetup.ts +221 -0
- package/src/commands.ts +136 -0
- package/src/components/ApproveApiKey.tsx +93 -0
- package/src/components/AsciiLogo.tsx +13 -0
- package/src/components/AutoUpdater.tsx +148 -0
- package/src/components/Bug.tsx +367 -0
- package/src/components/Config.tsx +289 -0
- package/src/components/ConsoleOAuthFlow.tsx +326 -0
- package/src/components/Cost.tsx +23 -0
- package/src/components/CostThresholdDialog.tsx +46 -0
- package/src/components/CustomSelect/option-map.ts +42 -0
- package/src/components/CustomSelect/select-option.tsx +52 -0
- package/src/components/CustomSelect/select.tsx +143 -0
- package/src/components/CustomSelect/use-select-state.ts +414 -0
- package/src/components/CustomSelect/use-select.ts +35 -0
- package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
- package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
- package/src/components/Help.tsx +215 -0
- package/src/components/HighlightedCode.tsx +33 -0
- package/src/components/InvalidConfigDialog.tsx +113 -0
- package/src/components/Link.tsx +32 -0
- package/src/components/LogSelector.tsx +86 -0
- package/src/components/Logo.tsx +145 -0
- package/src/components/MCPServerApprovalDialog.tsx +100 -0
- package/src/components/MCPServerDialogCopy.tsx +25 -0
- package/src/components/MCPServerMultiselectDialog.tsx +109 -0
- package/src/components/Message.tsx +219 -0
- package/src/components/MessageResponse.tsx +15 -0
- package/src/components/MessageSelector.tsx +211 -0
- package/src/components/ModeIndicator.tsx +88 -0
- package/src/components/ModelConfig.tsx +301 -0
- package/src/components/ModelListManager.tsx +223 -0
- package/src/components/ModelSelector.tsx +3208 -0
- package/src/components/ModelStatusDisplay.tsx +228 -0
- package/src/components/Onboarding.tsx +274 -0
- package/src/components/PressEnterToContinue.tsx +11 -0
- package/src/components/PromptInput.tsx +710 -0
- package/src/components/SentryErrorBoundary.ts +33 -0
- package/src/components/Spinner.tsx +129 -0
- package/src/components/StructuredDiff.tsx +184 -0
- package/src/components/TextInput.tsx +246 -0
- package/src/components/TokenWarning.tsx +31 -0
- package/src/components/ToolUseLoader.tsx +40 -0
- package/src/components/TrustDialog.tsx +106 -0
- package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
- package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
- package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
- package/src/components/binary-feedback/utils.ts +220 -0
- package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
- package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
- package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
- package/src/components/messages/AssistantTextMessage.tsx +144 -0
- package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
- package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
- package/src/components/messages/UserBashInputMessage.tsx +28 -0
- package/src/components/messages/UserCommandMessage.tsx +30 -0
- package/src/components/messages/UserKodingInputMessage.tsx +28 -0
- package/src/components/messages/UserPromptMessage.tsx +35 -0
- package/src/components/messages/UserTextMessage.tsx +39 -0
- package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
- package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
- package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
- package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
- package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
- package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
- package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
- package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
- package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
- package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
- package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
- package/src/components/permissions/PermissionRequest.tsx +103 -0
- package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
- package/src/components/permissions/hooks.ts +44 -0
- package/src/components/permissions/toolUseOptions.ts +59 -0
- package/src/components/permissions/utils.ts +23 -0
- package/src/constants/betas.ts +5 -0
- package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
- package/src/constants/figures.ts +4 -0
- package/src/constants/keys.ts +3 -0
- package/src/constants/macros.ts +6 -0
- package/src/constants/models.ts +935 -0
- package/src/constants/oauth.ts +18 -0
- package/src/constants/product.ts +17 -0
- package/src/constants/prompts.ts +177 -0
- package/src/constants/releaseNotes.ts +7 -0
- package/src/context/PermissionContext.tsx +149 -0
- package/src/context.ts +278 -0
- package/src/cost-tracker.ts +84 -0
- package/src/entrypoints/cli.tsx +1498 -0
- package/src/entrypoints/mcp.ts +176 -0
- package/src/history.ts +25 -0
- package/src/hooks/useApiKeyVerification.ts +59 -0
- package/src/hooks/useArrowKeyHistory.ts +55 -0
- package/src/hooks/useCanUseTool.ts +138 -0
- package/src/hooks/useCancelRequest.ts +39 -0
- package/src/hooks/useDoublePress.ts +42 -0
- package/src/hooks/useExitOnCtrlCD.ts +31 -0
- package/src/hooks/useInterval.ts +25 -0
- package/src/hooks/useLogMessages.ts +16 -0
- package/src/hooks/useLogStartupTime.ts +12 -0
- package/src/hooks/useNotifyAfterTimeout.ts +65 -0
- package/src/hooks/usePermissionRequestLogging.ts +44 -0
- package/src/hooks/useSlashCommandTypeahead.ts +137 -0
- package/src/hooks/useTerminalSize.ts +49 -0
- package/src/hooks/useTextInput.ts +315 -0
- package/src/messages.ts +37 -0
- package/src/permissions.ts +268 -0
- package/src/query.ts +704 -0
- package/src/screens/ConfigureNpmPrefix.tsx +197 -0
- package/src/screens/Doctor.tsx +219 -0
- package/src/screens/LogList.tsx +68 -0
- package/src/screens/REPL.tsx +792 -0
- package/src/screens/ResumeConversation.tsx +68 -0
- package/src/services/browserMocks.ts +66 -0
- package/src/services/claude.ts +1947 -0
- package/src/services/customCommands.ts +683 -0
- package/src/services/fileFreshness.ts +377 -0
- package/src/services/mcpClient.ts +564 -0
- package/src/services/mcpServerApproval.tsx +50 -0
- package/src/services/notifier.ts +40 -0
- package/src/services/oauth.ts +357 -0
- package/src/services/openai.ts +796 -0
- package/src/services/sentry.ts +3 -0
- package/src/services/statsig.ts +171 -0
- package/src/services/statsigStorage.ts +86 -0
- package/src/services/systemReminder.ts +406 -0
- package/src/services/vcr.ts +161 -0
- package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
- package/src/tools/ArchitectTool/prompt.ts +15 -0
- package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
- package/src/tools/BashTool/BashTool.tsx +270 -0
- package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
- package/src/tools/BashTool/OutputLine.tsx +48 -0
- package/src/tools/BashTool/prompt.ts +174 -0
- package/src/tools/BashTool/utils.ts +56 -0
- package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
- package/src/tools/FileEditTool/prompt.ts +51 -0
- package/src/tools/FileEditTool/utils.ts +58 -0
- package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
- package/src/tools/FileReadTool/prompt.ts +7 -0
- package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
- package/src/tools/FileWriteTool/prompt.ts +10 -0
- package/src/tools/GlobTool/GlobTool.tsx +119 -0
- package/src/tools/GlobTool/prompt.ts +8 -0
- package/src/tools/GrepTool/GrepTool.tsx +147 -0
- package/src/tools/GrepTool/prompt.ts +11 -0
- package/src/tools/MCPTool/MCPTool.tsx +106 -0
- package/src/tools/MCPTool/prompt.ts +3 -0
- package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
- package/src/tools/MemoryReadTool/prompt.ts +3 -0
- package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
- package/src/tools/MemoryWriteTool/prompt.ts +3 -0
- package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
- package/src/tools/MultiEditTool/prompt.ts +45 -0
- package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
- package/src/tools/NotebookEditTool/prompt.ts +3 -0
- package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
- package/src/tools/NotebookReadTool/prompt.ts +3 -0
- package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
- package/src/tools/StickerRequestTool/prompt.ts +19 -0
- package/src/tools/TaskTool/TaskTool.tsx +382 -0
- package/src/tools/TaskTool/constants.ts +1 -0
- package/src/tools/TaskTool/prompt.ts +56 -0
- package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
- package/src/tools/ThinkTool/prompt.ts +12 -0
- package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
- package/src/tools/TodoWriteTool/prompt.ts +63 -0
- package/src/tools/lsTool/lsTool.tsx +269 -0
- package/src/tools/lsTool/prompt.ts +2 -0
- package/src/tools.ts +63 -0
- package/src/types/PermissionMode.ts +120 -0
- package/src/types/RequestContext.ts +72 -0
- package/src/utils/Cursor.ts +436 -0
- package/src/utils/PersistentShell.ts +373 -0
- package/src/utils/agentStorage.ts +97 -0
- package/src/utils/array.ts +3 -0
- package/src/utils/ask.tsx +98 -0
- package/src/utils/auth.ts +13 -0
- package/src/utils/autoCompactCore.ts +223 -0
- package/src/utils/autoUpdater.ts +318 -0
- package/src/utils/betas.ts +20 -0
- package/src/utils/browser.ts +14 -0
- package/src/utils/cleanup.ts +72 -0
- package/src/utils/commands.ts +261 -0
- package/src/utils/config.ts +771 -0
- package/src/utils/conversationRecovery.ts +54 -0
- package/src/utils/debugLogger.ts +1123 -0
- package/src/utils/diff.ts +42 -0
- package/src/utils/env.ts +57 -0
- package/src/utils/errors.ts +21 -0
- package/src/utils/exampleCommands.ts +108 -0
- package/src/utils/execFileNoThrow.ts +51 -0
- package/src/utils/expertChatStorage.ts +136 -0
- package/src/utils/file.ts +402 -0
- package/src/utils/fileRecoveryCore.ts +71 -0
- package/src/utils/format.tsx +44 -0
- package/src/utils/generators.ts +62 -0
- package/src/utils/git.ts +92 -0
- package/src/utils/globalLogger.ts +77 -0
- package/src/utils/http.ts +10 -0
- package/src/utils/imagePaste.ts +38 -0
- package/src/utils/json.ts +13 -0
- package/src/utils/log.ts +382 -0
- package/src/utils/markdown.ts +213 -0
- package/src/utils/messageContextManager.ts +289 -0
- package/src/utils/messages.tsx +938 -0
- package/src/utils/model.ts +836 -0
- package/src/utils/permissions/filesystem.ts +118 -0
- package/src/utils/ripgrep.ts +167 -0
- package/src/utils/sessionState.ts +49 -0
- package/src/utils/state.ts +25 -0
- package/src/utils/style.ts +29 -0
- package/src/utils/terminal.ts +49 -0
- package/src/utils/theme.ts +122 -0
- package/src/utils/thinking.ts +144 -0
- package/src/utils/todoStorage.ts +431 -0
- package/src/utils/tokens.ts +43 -0
- package/src/utils/toolExecutionController.ts +163 -0
- package/src/utils/unaryLogging.ts +26 -0
- package/src/utils/user.ts +37 -0
- package/src/utils/validate.ts +165 -0
- package/cli.mjs +0 -1803
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { logEvent } from '../services/statsig'
|
|
3
|
+
import { logUnaryEvent, CompletionType } from '../utils/unaryLogging'
|
|
4
|
+
import { ToolUseConfirm } from '../components/permissions/PermissionRequest'
|
|
5
|
+
import { env } from '../utils/env'
|
|
6
|
+
|
|
7
|
+
export type UnaryEvent = {
|
|
8
|
+
completion_type: CompletionType
|
|
9
|
+
language_name: string | Promise<string>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Logs permission request events using Statsig and unary logging.
|
|
14
|
+
* Handles both the Statsig event and the unary event logging.
|
|
15
|
+
* Can handle either a string or Promise<string> for language_name.
|
|
16
|
+
*/
|
|
17
|
+
export function usePermissionRequestLogging(
|
|
18
|
+
toolUseConfirm: ToolUseConfirm,
|
|
19
|
+
unaryEvent: UnaryEvent,
|
|
20
|
+
): void {
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// Log Statsig event
|
|
23
|
+
logEvent('tengu_tool_use_show_permission_request', {
|
|
24
|
+
messageID: toolUseConfirm.assistantMessage.message.id,
|
|
25
|
+
toolName: toolUseConfirm.tool.name,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Handle string or Promise language name
|
|
29
|
+
const languagePromise = Promise.resolve(unaryEvent.language_name)
|
|
30
|
+
|
|
31
|
+
// Log unary event once language is resolved
|
|
32
|
+
languagePromise.then(language => {
|
|
33
|
+
logUnaryEvent({
|
|
34
|
+
completion_type: unaryEvent.completion_type,
|
|
35
|
+
event: 'response',
|
|
36
|
+
metadata: {
|
|
37
|
+
language_name: language,
|
|
38
|
+
message_id: toolUseConfirm.assistantMessage.message.id,
|
|
39
|
+
platform: env.platform,
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}, [toolUseConfirm, unaryEvent])
|
|
44
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useInput } from 'ink'
|
|
2
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
3
|
+
import { Command, getCommand } from '../commands'
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
commands: Command[]
|
|
7
|
+
onInputChange: (value: string) => void
|
|
8
|
+
onSubmit: (value: string, isSubmittingSlashCommand?: boolean) => void
|
|
9
|
+
setCursorOffset: (offset: number) => void
|
|
10
|
+
currentInput?: string // Add current input for monitoring
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useSlashCommandTypeahead({
|
|
14
|
+
commands,
|
|
15
|
+
onInputChange,
|
|
16
|
+
onSubmit,
|
|
17
|
+
setCursorOffset,
|
|
18
|
+
currentInput,
|
|
19
|
+
}: Props): {
|
|
20
|
+
suggestions: string[]
|
|
21
|
+
selectedSuggestion: number
|
|
22
|
+
updateSuggestions: (value: string) => void
|
|
23
|
+
clearSuggestions: () => void
|
|
24
|
+
} {
|
|
25
|
+
const [suggestions, setSuggestions] = useState<string[]>([])
|
|
26
|
+
const [selectedSuggestion, setSelectedSuggestion] = useState(-1)
|
|
27
|
+
|
|
28
|
+
// Force clear suggestions when input doesn't start with /
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (
|
|
31
|
+
currentInput !== undefined &&
|
|
32
|
+
!currentInput.startsWith('/') &&
|
|
33
|
+
suggestions.length > 0
|
|
34
|
+
) {
|
|
35
|
+
setSuggestions([])
|
|
36
|
+
setSelectedSuggestion(-1)
|
|
37
|
+
}
|
|
38
|
+
}, [currentInput, suggestions.length])
|
|
39
|
+
|
|
40
|
+
function updateSuggestions(value: string) {
|
|
41
|
+
if (value.startsWith('/')) {
|
|
42
|
+
const query = value.slice(1).toLowerCase()
|
|
43
|
+
|
|
44
|
+
// Find commands whose name or alias matches the query
|
|
45
|
+
const matchingCommands = commands
|
|
46
|
+
.filter(cmd => !cmd.isHidden)
|
|
47
|
+
.filter(cmd => {
|
|
48
|
+
const names = [cmd.userFacingName()]
|
|
49
|
+
if (cmd.aliases) {
|
|
50
|
+
names.push(...cmd.aliases)
|
|
51
|
+
}
|
|
52
|
+
return names.some(name => name.toLowerCase().startsWith(query))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// For each matching command, include its primary name
|
|
56
|
+
const filtered = matchingCommands.map(cmd => cmd.userFacingName())
|
|
57
|
+
setSuggestions(filtered)
|
|
58
|
+
|
|
59
|
+
// Try to preserve the selected suggestion
|
|
60
|
+
const newIndex =
|
|
61
|
+
selectedSuggestion > -1
|
|
62
|
+
? filtered.indexOf(suggestions[selectedSuggestion]!)
|
|
63
|
+
: 0
|
|
64
|
+
if (newIndex > -1) {
|
|
65
|
+
setSelectedSuggestion(newIndex)
|
|
66
|
+
} else {
|
|
67
|
+
setSelectedSuggestion(0)
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
setSuggestions([])
|
|
71
|
+
setSelectedSuggestion(-1)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
useInput((_, key) => {
|
|
76
|
+
if (suggestions.length > 0) {
|
|
77
|
+
// Handle suggestion navigation (up/down arrows)
|
|
78
|
+
if (key.downArrow) {
|
|
79
|
+
setSelectedSuggestion(prev =>
|
|
80
|
+
prev >= suggestions.length - 1 ? 0 : prev + 1,
|
|
81
|
+
)
|
|
82
|
+
return true
|
|
83
|
+
} else if (key.upArrow) {
|
|
84
|
+
setSelectedSuggestion(prev =>
|
|
85
|
+
prev <= 0 ? suggestions.length - 1 : prev - 1,
|
|
86
|
+
)
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle selection completion via tab or return
|
|
91
|
+
else if (key.tab || (key.return && selectedSuggestion >= 0)) {
|
|
92
|
+
// Ensure a suggestion is selected
|
|
93
|
+
if (selectedSuggestion === -1 && key.tab) {
|
|
94
|
+
setSelectedSuggestion(0)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const suggestionIndex = selectedSuggestion >= 0 ? selectedSuggestion : 0
|
|
98
|
+
const suggestion = suggestions[suggestionIndex]
|
|
99
|
+
if (!suggestion) return true
|
|
100
|
+
|
|
101
|
+
const input = '/' + suggestion + ' '
|
|
102
|
+
onInputChange(input)
|
|
103
|
+
// Manually move cursor to end
|
|
104
|
+
setCursorOffset(input.length)
|
|
105
|
+
setSuggestions([])
|
|
106
|
+
setSelectedSuggestion(-1)
|
|
107
|
+
|
|
108
|
+
// If return was pressed and command doesn't take arguments, just run it
|
|
109
|
+
if (key.return) {
|
|
110
|
+
const command = getCommand(suggestion, commands)
|
|
111
|
+
if (
|
|
112
|
+
command.type !== 'prompt' ||
|
|
113
|
+
(command.argNames ?? []).length === 0
|
|
114
|
+
) {
|
|
115
|
+
onSubmit(input, /* isSubmittingSlashCommand */ true)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Explicitly return false when not handling the input
|
|
123
|
+
return false
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const clearSuggestions = useCallback(() => {
|
|
127
|
+
setSuggestions([])
|
|
128
|
+
setSelectedSuggestion(-1)
|
|
129
|
+
}, [])
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
suggestions,
|
|
133
|
+
selectedSuggestion,
|
|
134
|
+
updateSuggestions,
|
|
135
|
+
clearSuggestions,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
// Global state to share across all hook instances
|
|
4
|
+
let globalSize = {
|
|
5
|
+
columns: process.stdout.columns || 80,
|
|
6
|
+
rows: process.stdout.rows || 24,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const listeners = new Set<() => void>()
|
|
10
|
+
let isListenerAttached = false
|
|
11
|
+
|
|
12
|
+
function updateAllListeners() {
|
|
13
|
+
globalSize = {
|
|
14
|
+
columns: process.stdout.columns || 80,
|
|
15
|
+
rows: process.stdout.rows || 24,
|
|
16
|
+
}
|
|
17
|
+
listeners.forEach(listener => listener())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useTerminalSize() {
|
|
21
|
+
const [size, setSize] = useState(globalSize)
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
// Add this component's listener to the set
|
|
25
|
+
const updateSize = () => setSize({ ...globalSize })
|
|
26
|
+
listeners.add(updateSize)
|
|
27
|
+
|
|
28
|
+
// Only attach the global resize listener once
|
|
29
|
+
if (!isListenerAttached) {
|
|
30
|
+
// Increase max listeners to prevent warnings
|
|
31
|
+
process.stdout.setMaxListeners(20)
|
|
32
|
+
process.stdout.on('resize', updateAllListeners)
|
|
33
|
+
isListenerAttached = true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
// Remove this component's listener
|
|
38
|
+
listeners.delete(updateSize)
|
|
39
|
+
|
|
40
|
+
// If no more listeners, remove the global listener
|
|
41
|
+
if (listeners.size === 0 && isListenerAttached) {
|
|
42
|
+
process.stdout.off('resize', updateAllListeners)
|
|
43
|
+
isListenerAttached = false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
return size
|
|
49
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { type Key } from 'ink'
|
|
3
|
+
import { useDoublePress } from './useDoublePress'
|
|
4
|
+
import { Cursor } from '../utils/Cursor'
|
|
5
|
+
import {
|
|
6
|
+
getImageFromClipboard,
|
|
7
|
+
CLIPBOARD_ERROR_MESSAGE,
|
|
8
|
+
} from '../utils/imagePaste.js'
|
|
9
|
+
|
|
10
|
+
const IMAGE_PLACEHOLDER = '[Image pasted]'
|
|
11
|
+
|
|
12
|
+
type MaybeCursor = void | Cursor
|
|
13
|
+
type InputHandler = (input: string) => MaybeCursor
|
|
14
|
+
type InputMapper = (input: string) => MaybeCursor
|
|
15
|
+
function mapInput(input_map: Array<[string, InputHandler]>): InputMapper {
|
|
16
|
+
return function (input: string): MaybeCursor {
|
|
17
|
+
const handler = new Map(input_map).get(input) ?? (() => {})
|
|
18
|
+
return handler(input)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type UseTextInputProps = {
|
|
23
|
+
value: string
|
|
24
|
+
onChange: (value: string) => void
|
|
25
|
+
onSubmit?: (value: string) => void
|
|
26
|
+
onExit?: () => void
|
|
27
|
+
onExitMessage?: (show: boolean, key?: string) => void
|
|
28
|
+
onMessage?: (show: boolean, message?: string) => void
|
|
29
|
+
onHistoryUp?: () => void
|
|
30
|
+
onHistoryDown?: () => void
|
|
31
|
+
onHistoryReset?: () => void
|
|
32
|
+
focus?: boolean
|
|
33
|
+
mask?: string
|
|
34
|
+
multiline?: boolean
|
|
35
|
+
cursorChar: string
|
|
36
|
+
highlightPastedText?: boolean
|
|
37
|
+
invert: (text: string) => string
|
|
38
|
+
themeText: (text: string) => string
|
|
39
|
+
columns: number
|
|
40
|
+
onImagePaste?: (base64Image: string) => void
|
|
41
|
+
disableCursorMovementForUpDownKeys?: boolean
|
|
42
|
+
externalOffset: number
|
|
43
|
+
onOffsetChange: (offset: number) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type UseTextInputResult = {
|
|
47
|
+
renderedValue: string
|
|
48
|
+
onInput: (input: string, key: Key) => void
|
|
49
|
+
offset: number
|
|
50
|
+
setOffset: (offset: number) => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useTextInput({
|
|
54
|
+
value: originalValue,
|
|
55
|
+
onChange,
|
|
56
|
+
onSubmit,
|
|
57
|
+
onExit,
|
|
58
|
+
onExitMessage,
|
|
59
|
+
onMessage,
|
|
60
|
+
onHistoryUp,
|
|
61
|
+
onHistoryDown,
|
|
62
|
+
onHistoryReset,
|
|
63
|
+
mask = '',
|
|
64
|
+
multiline = false,
|
|
65
|
+
cursorChar,
|
|
66
|
+
invert,
|
|
67
|
+
columns,
|
|
68
|
+
onImagePaste,
|
|
69
|
+
disableCursorMovementForUpDownKeys = false,
|
|
70
|
+
externalOffset,
|
|
71
|
+
onOffsetChange,
|
|
72
|
+
}: UseTextInputProps): UseTextInputResult {
|
|
73
|
+
const offset = externalOffset
|
|
74
|
+
const setOffset = onOffsetChange
|
|
75
|
+
const cursor = Cursor.fromText(originalValue, columns, offset)
|
|
76
|
+
const [imagePasteErrorTimeout, setImagePasteErrorTimeout] =
|
|
77
|
+
useState<NodeJS.Timeout | null>(null)
|
|
78
|
+
|
|
79
|
+
function maybeClearImagePasteErrorTimeout() {
|
|
80
|
+
if (!imagePasteErrorTimeout) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
clearTimeout(imagePasteErrorTimeout)
|
|
84
|
+
setImagePasteErrorTimeout(null)
|
|
85
|
+
onMessage?.(false)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleCtrlC = useDoublePress(
|
|
89
|
+
show => {
|
|
90
|
+
maybeClearImagePasteErrorTimeout()
|
|
91
|
+
onExitMessage?.(show, 'Ctrl-C')
|
|
92
|
+
},
|
|
93
|
+
() => onExit?.(),
|
|
94
|
+
() => {
|
|
95
|
+
if (originalValue) {
|
|
96
|
+
onChange('')
|
|
97
|
+
onHistoryReset?.()
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// Keep Escape for clearing input
|
|
103
|
+
const handleEscape = useDoublePress(
|
|
104
|
+
show => {
|
|
105
|
+
maybeClearImagePasteErrorTimeout()
|
|
106
|
+
onMessage?.(!!originalValue && show, `Press Escape again to clear`)
|
|
107
|
+
},
|
|
108
|
+
() => {
|
|
109
|
+
if (originalValue) {
|
|
110
|
+
onChange('')
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
function clear() {
|
|
115
|
+
return Cursor.fromText('', columns, 0)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handleEmptyCtrlD = useDoublePress(
|
|
119
|
+
show => onExitMessage?.(show, 'Ctrl-D'),
|
|
120
|
+
() => onExit?.(),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
function handleCtrlD(): MaybeCursor {
|
|
124
|
+
maybeClearImagePasteErrorTimeout()
|
|
125
|
+
if (cursor.text === '') {
|
|
126
|
+
// When input is empty, handle double-press
|
|
127
|
+
handleEmptyCtrlD()
|
|
128
|
+
return cursor
|
|
129
|
+
}
|
|
130
|
+
// When input is not empty, delete forward like iPython
|
|
131
|
+
return cursor.del()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function tryImagePaste() {
|
|
135
|
+
const base64Image = getImageFromClipboard()
|
|
136
|
+
if (base64Image === null) {
|
|
137
|
+
if (process.platform !== 'darwin') {
|
|
138
|
+
return cursor
|
|
139
|
+
}
|
|
140
|
+
onMessage?.(true, CLIPBOARD_ERROR_MESSAGE)
|
|
141
|
+
maybeClearImagePasteErrorTimeout()
|
|
142
|
+
setImagePasteErrorTimeout(
|
|
143
|
+
// @ts-expect-error: Bun is overloading types here, but we're using the NodeJS runtime
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
onMessage?.(false)
|
|
146
|
+
}, 4000),
|
|
147
|
+
)
|
|
148
|
+
return cursor
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
onImagePaste?.(base64Image)
|
|
152
|
+
return cursor.insert(IMAGE_PLACEHOLDER)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handleCtrl = mapInput([
|
|
156
|
+
['a', () => cursor.startOfLine()],
|
|
157
|
+
['b', () => cursor.left()],
|
|
158
|
+
['c', handleCtrlC],
|
|
159
|
+
['d', handleCtrlD],
|
|
160
|
+
['e', () => cursor.endOfLine()],
|
|
161
|
+
['f', () => cursor.right()],
|
|
162
|
+
[
|
|
163
|
+
'h',
|
|
164
|
+
() => {
|
|
165
|
+
maybeClearImagePasteErrorTimeout()
|
|
166
|
+
return cursor.backspace()
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
['k', () => cursor.deleteToLineEnd()],
|
|
170
|
+
['l', () => clear()],
|
|
171
|
+
['n', () => downOrHistoryDown()],
|
|
172
|
+
['p', () => upOrHistoryUp()],
|
|
173
|
+
['u', () => cursor.deleteToLineStart()],
|
|
174
|
+
['v', tryImagePaste],
|
|
175
|
+
['w', () => cursor.deleteWordBefore()],
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
const handleMeta = mapInput([
|
|
179
|
+
['b', () => cursor.prevWord()],
|
|
180
|
+
['f', () => cursor.nextWord()],
|
|
181
|
+
['d', () => cursor.deleteWordAfter()],
|
|
182
|
+
])
|
|
183
|
+
|
|
184
|
+
function handleEnter(key: Key) {
|
|
185
|
+
if (
|
|
186
|
+
multiline &&
|
|
187
|
+
cursor.offset > 0 &&
|
|
188
|
+
cursor.text[cursor.offset - 1] === '\\'
|
|
189
|
+
) {
|
|
190
|
+
return cursor.backspace().insert('\n')
|
|
191
|
+
}
|
|
192
|
+
if (key.meta) {
|
|
193
|
+
return cursor.insert('\n')
|
|
194
|
+
}
|
|
195
|
+
onSubmit?.(originalValue)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function upOrHistoryUp() {
|
|
199
|
+
if (disableCursorMovementForUpDownKeys) {
|
|
200
|
+
onHistoryUp?.()
|
|
201
|
+
return cursor
|
|
202
|
+
}
|
|
203
|
+
const cursorUp = cursor.up()
|
|
204
|
+
if (cursorUp.equals(cursor)) {
|
|
205
|
+
// already at beginning
|
|
206
|
+
onHistoryUp?.()
|
|
207
|
+
}
|
|
208
|
+
return cursorUp
|
|
209
|
+
}
|
|
210
|
+
function downOrHistoryDown() {
|
|
211
|
+
if (disableCursorMovementForUpDownKeys) {
|
|
212
|
+
onHistoryDown?.()
|
|
213
|
+
return cursor
|
|
214
|
+
}
|
|
215
|
+
const cursorDown = cursor.down()
|
|
216
|
+
if (cursorDown.equals(cursor)) {
|
|
217
|
+
onHistoryDown?.()
|
|
218
|
+
}
|
|
219
|
+
return cursorDown
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function onInput(input: string, key: Key): void {
|
|
223
|
+
// Direct handling for backspace or delete (which is being detected as delete)
|
|
224
|
+
if (
|
|
225
|
+
key.backspace ||
|
|
226
|
+
key.delete ||
|
|
227
|
+
input === '\b' ||
|
|
228
|
+
input === '\x7f' ||
|
|
229
|
+
input === '\x08'
|
|
230
|
+
) {
|
|
231
|
+
const nextCursor = cursor.backspace()
|
|
232
|
+
if (!cursor.equals(nextCursor)) {
|
|
233
|
+
setOffset(nextCursor.offset)
|
|
234
|
+
if (cursor.text !== nextCursor.text) {
|
|
235
|
+
onChange(nextCursor.text)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const nextCursor = mapKey(key)(input)
|
|
242
|
+
if (nextCursor) {
|
|
243
|
+
if (!cursor.equals(nextCursor)) {
|
|
244
|
+
setOffset(nextCursor.offset)
|
|
245
|
+
if (cursor.text !== nextCursor.text) {
|
|
246
|
+
onChange(nextCursor.text)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function mapKey(key: Key): InputMapper {
|
|
253
|
+
// Direct handling for backspace or delete
|
|
254
|
+
if (key.backspace || key.delete) {
|
|
255
|
+
maybeClearImagePasteErrorTimeout()
|
|
256
|
+
return () => cursor.backspace()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
switch (true) {
|
|
260
|
+
case key.escape:
|
|
261
|
+
return handleEscape
|
|
262
|
+
case key.leftArrow && (key.ctrl || key.meta || key.fn):
|
|
263
|
+
return () => cursor.prevWord()
|
|
264
|
+
case key.rightArrow && (key.ctrl || key.meta || key.fn):
|
|
265
|
+
return () => cursor.nextWord()
|
|
266
|
+
case key.ctrl:
|
|
267
|
+
return handleCtrl
|
|
268
|
+
case key.home:
|
|
269
|
+
return () => cursor.startOfLine()
|
|
270
|
+
case key.end:
|
|
271
|
+
return () => cursor.endOfLine()
|
|
272
|
+
case key.pageDown:
|
|
273
|
+
return () => cursor.endOfLine()
|
|
274
|
+
case key.pageUp:
|
|
275
|
+
return () => cursor.startOfLine()
|
|
276
|
+
case key.meta:
|
|
277
|
+
return handleMeta
|
|
278
|
+
case key.return:
|
|
279
|
+
return () => handleEnter(key)
|
|
280
|
+
case key.tab:
|
|
281
|
+
return () => {}
|
|
282
|
+
case key.upArrow:
|
|
283
|
+
return upOrHistoryUp
|
|
284
|
+
case key.downArrow:
|
|
285
|
+
return downOrHistoryDown
|
|
286
|
+
case key.leftArrow:
|
|
287
|
+
return () => cursor.left()
|
|
288
|
+
case key.rightArrow:
|
|
289
|
+
return () => cursor.right()
|
|
290
|
+
}
|
|
291
|
+
return function (input: string) {
|
|
292
|
+
switch (true) {
|
|
293
|
+
// Home key
|
|
294
|
+
case input == '\x1b[H' || input == '\x1b[1~':
|
|
295
|
+
return cursor.startOfLine()
|
|
296
|
+
// End key
|
|
297
|
+
case input == '\x1b[F' || input == '\x1b[4~':
|
|
298
|
+
return cursor.endOfLine()
|
|
299
|
+
// Handle backspace character explicitly - this is the key fix
|
|
300
|
+
case input === '\b' || input === '\x7f' || input === '\x08':
|
|
301
|
+
maybeClearImagePasteErrorTimeout()
|
|
302
|
+
return cursor.backspace()
|
|
303
|
+
default:
|
|
304
|
+
return cursor.insert(input.replace(/\r/g, '\n'))
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
onInput,
|
|
311
|
+
renderedValue: cursor.render(cursorChar, mask, invert),
|
|
312
|
+
offset,
|
|
313
|
+
setOffset,
|
|
314
|
+
}
|
|
315
|
+
}
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Message } from './query'
|
|
2
|
+
|
|
3
|
+
let getMessages: () => Message[] = () => []
|
|
4
|
+
let setMessages: React.Dispatch<React.SetStateAction<Message[]>> = () => {}
|
|
5
|
+
|
|
6
|
+
export function setMessagesGetter(getter: () => Message[]) {
|
|
7
|
+
getMessages = getter
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getMessagesGetter(): () => Message[] {
|
|
11
|
+
return getMessages
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function setMessagesSetter(
|
|
15
|
+
setter: React.Dispatch<React.SetStateAction<Message[]>>,
|
|
16
|
+
) {
|
|
17
|
+
setMessages = setter
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getMessagesSetter(): React.Dispatch<
|
|
21
|
+
React.SetStateAction<Message[]>
|
|
22
|
+
> {
|
|
23
|
+
return setMessages
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Global UI refresh mechanism for model configuration changes
|
|
27
|
+
let onModelConfigChange: (() => void) | null = null
|
|
28
|
+
|
|
29
|
+
export function setModelConfigChangeHandler(handler: () => void) {
|
|
30
|
+
onModelConfigChange = handler
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function triggerModelConfigChange() {
|
|
34
|
+
if (onModelConfigChange) {
|
|
35
|
+
onModelConfigChange()
|
|
36
|
+
}
|
|
37
|
+
}
|