@shareai-lab/kode 1.0.71 ā 1.0.73
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 +142 -1
- package/README.zh-CN.md +47 -1
- package/package.json +5 -1
- package/src/ProjectOnboarding.tsx +47 -29
- package/src/Tool.ts +33 -4
- package/src/commands/agents.tsx +3401 -0
- package/src/commands/help.tsx +2 -2
- package/src/commands/resume.tsx +2 -1
- package/src/commands/terminalSetup.ts +4 -4
- package/src/commands.ts +3 -0
- package/src/components/ApproveApiKey.tsx +1 -1
- package/src/components/Config.tsx +10 -6
- package/src/components/ConsoleOAuthFlow.tsx +5 -4
- package/src/components/CustomSelect/select-option.tsx +28 -2
- package/src/components/CustomSelect/select.tsx +14 -5
- package/src/components/CustomSelect/theme.ts +45 -0
- package/src/components/Help.tsx +4 -4
- package/src/components/InvalidConfigDialog.tsx +1 -1
- package/src/components/LogSelector.tsx +1 -1
- package/src/components/MCPServerApprovalDialog.tsx +1 -1
- package/src/components/Message.tsx +2 -0
- package/src/components/ModelListManager.tsx +10 -6
- package/src/components/ModelSelector.tsx +201 -23
- package/src/components/ModelStatusDisplay.tsx +7 -5
- package/src/components/PromptInput.tsx +117 -87
- package/src/components/SentryErrorBoundary.ts +3 -3
- package/src/components/StickerRequestForm.tsx +16 -0
- package/src/components/StructuredDiff.tsx +36 -29
- package/src/components/TextInput.tsx +13 -0
- package/src/components/TodoItem.tsx +11 -0
- package/src/components/TrustDialog.tsx +1 -1
- package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +5 -1
- package/src/components/messages/AssistantToolUseMessage.tsx +14 -4
- package/src/components/messages/TaskProgressMessage.tsx +32 -0
- package/src/components/messages/TaskToolMessage.tsx +58 -0
- package/src/components/permissions/FallbackPermissionRequest.tsx +2 -4
- package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +1 -1
- package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +5 -3
- package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +1 -1
- package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +5 -3
- package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +2 -4
- package/src/components/permissions/PermissionRequest.tsx +3 -5
- package/src/constants/macros.ts +2 -0
- package/src/constants/modelCapabilities.ts +179 -0
- package/src/constants/models.ts +90 -0
- package/src/constants/product.ts +1 -1
- package/src/context.ts +7 -7
- package/src/entrypoints/cli.tsx +23 -3
- package/src/entrypoints/mcp.ts +10 -10
- package/src/hooks/useCanUseTool.ts +1 -1
- package/src/hooks/useTextInput.ts +5 -2
- package/src/hooks/useUnifiedCompletion.ts +1404 -0
- package/src/messages.ts +1 -0
- package/src/query.ts +3 -0
- package/src/screens/ConfigureNpmPrefix.tsx +1 -1
- package/src/screens/Doctor.tsx +1 -1
- package/src/screens/REPL.tsx +15 -9
- package/src/services/adapters/base.ts +38 -0
- package/src/services/adapters/chatCompletions.ts +90 -0
- package/src/services/adapters/responsesAPI.ts +170 -0
- package/src/services/claude.ts +198 -62
- package/src/services/customCommands.ts +43 -22
- package/src/services/gpt5ConnectionTest.ts +340 -0
- package/src/services/mcpClient.ts +1 -1
- package/src/services/mentionProcessor.ts +273 -0
- package/src/services/modelAdapterFactory.ts +69 -0
- package/src/services/openai.ts +521 -12
- package/src/services/responseStateManager.ts +90 -0
- package/src/services/systemReminder.ts +113 -12
- package/src/test/testAdapters.ts +96 -0
- package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +120 -56
- package/src/tools/BashTool/BashTool.tsx +4 -31
- package/src/tools/BashTool/BashToolResultMessage.tsx +1 -1
- package/src/tools/BashTool/OutputLine.tsx +1 -0
- package/src/tools/FileEditTool/FileEditTool.tsx +4 -5
- package/src/tools/FileReadTool/FileReadTool.tsx +43 -10
- package/src/tools/MCPTool/MCPTool.tsx +2 -1
- package/src/tools/MultiEditTool/MultiEditTool.tsx +2 -2
- package/src/tools/NotebookReadTool/NotebookReadTool.tsx +15 -23
- package/src/tools/StickerRequestTool/StickerRequestTool.tsx +1 -1
- package/src/tools/TaskTool/TaskTool.tsx +170 -86
- package/src/tools/TaskTool/prompt.ts +61 -25
- package/src/tools/ThinkTool/ThinkTool.tsx +1 -3
- package/src/tools/TodoWriteTool/TodoWriteTool.tsx +11 -10
- package/src/tools/lsTool/lsTool.tsx +5 -2
- package/src/tools.ts +16 -16
- package/src/types/conversation.ts +51 -0
- package/src/types/logs.ts +58 -0
- package/src/types/modelCapabilities.ts +64 -0
- package/src/types/notebook.ts +87 -0
- package/src/utils/advancedFuzzyMatcher.ts +290 -0
- package/src/utils/agentLoader.ts +284 -0
- package/src/utils/ask.tsx +1 -0
- package/src/utils/commands.ts +1 -1
- package/src/utils/commonUnixCommands.ts +161 -0
- package/src/utils/config.ts +173 -2
- package/src/utils/conversationRecovery.ts +1 -0
- package/src/utils/debugLogger.ts +13 -13
- package/src/utils/exampleCommands.ts +1 -0
- package/src/utils/fuzzyMatcher.ts +328 -0
- package/src/utils/messages.tsx +6 -5
- package/src/utils/responseState.ts +23 -0
- package/src/utils/secureFile.ts +559 -0
- package/src/utils/terminal.ts +1 -0
- package/src/utils/theme.ts +11 -0
- package/src/hooks/useSlashCommandTypeahead.ts +0 -137
package/src/commands/help.tsx
CHANGED
|
@@ -8,8 +8,8 @@ const help = {
|
|
|
8
8
|
description: 'Show help and available commands',
|
|
9
9
|
isEnabled: true,
|
|
10
10
|
isHidden: false,
|
|
11
|
-
async call(onDone,
|
|
12
|
-
return <Help commands={commands} onClose={onDone} />
|
|
11
|
+
async call(onDone, context) {
|
|
12
|
+
return <Help commands={context.options?.commands || []} onClose={onDone} />
|
|
13
13
|
},
|
|
14
14
|
userFacingName() {
|
|
15
15
|
return 'help'
|
package/src/commands/resume.tsx
CHANGED
|
@@ -13,7 +13,8 @@ export default {
|
|
|
13
13
|
userFacingName() {
|
|
14
14
|
return 'resume'
|
|
15
15
|
},
|
|
16
|
-
async call(onDone,
|
|
16
|
+
async call(onDone, context) {
|
|
17
|
+
const { commands = [], tools = [], verbose = false } = context.options || {}
|
|
17
18
|
const logs = await loadLogList(CACHE_PATHS.messages())
|
|
18
19
|
render(
|
|
19
20
|
<ResumeConversation
|
|
@@ -52,17 +52,17 @@ export function isShiftEnterKeyBindingInstalled(): boolean {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function handleHashCommand(interpreted: string): void {
|
|
55
|
-
// Appends the AI-interpreted content to both
|
|
55
|
+
// Appends the AI-interpreted content to both AGENTS.md and CLAUDE.md (if exists)
|
|
56
56
|
try {
|
|
57
57
|
const cwd = process.cwd()
|
|
58
|
-
const codeContextPath = join(cwd, '
|
|
58
|
+
const codeContextPath = join(cwd, 'AGENTS.md')
|
|
59
59
|
const claudePath = join(cwd, 'CLAUDE.md')
|
|
60
60
|
|
|
61
61
|
// Check which files exist and update them
|
|
62
62
|
const filesToUpdate = []
|
|
63
63
|
|
|
64
|
-
// Always try to update
|
|
65
|
-
filesToUpdate.push({ path: codeContextPath, name: '
|
|
64
|
+
// Always try to update AGENTS.md (create if not exists)
|
|
65
|
+
filesToUpdate.push({ path: codeContextPath, name: 'AGENTS.md' })
|
|
66
66
|
|
|
67
67
|
// Update CLAUDE.md only if it exists
|
|
68
68
|
try {
|
package/src/commands.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react'
|
|
1
2
|
import bug from './commands/bug'
|
|
2
3
|
import clear from './commands/clear'
|
|
3
4
|
import compact from './commands/compact'
|
|
@@ -21,6 +22,7 @@ import review from './commands/review'
|
|
|
21
22
|
import terminalSetup from './commands/terminalSetup'
|
|
22
23
|
import { Tool, ToolUseContext } from './Tool'
|
|
23
24
|
import resume from './commands/resume'
|
|
25
|
+
import agents from './commands/agents'
|
|
24
26
|
import { getMCPCommands } from './services/mcpClient'
|
|
25
27
|
import { loadCustomCommands } from './services/customCommands'
|
|
26
28
|
import type { MessageParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
|
@@ -79,6 +81,7 @@ const INTERNAL_ONLY_COMMANDS = [ctx_viz, resume, listen]
|
|
|
79
81
|
// Declared as a function so that we don't run this until getCommands is called,
|
|
80
82
|
// since underlying functions read from config, which can't be read at module initialization time
|
|
81
83
|
const COMMANDS = memoize((): Command[] => [
|
|
84
|
+
agents,
|
|
82
85
|
clear,
|
|
83
86
|
compact,
|
|
84
87
|
config,
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
3
|
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
|
|
4
4
|
import { getTheme } from '../utils/theme'
|
|
5
|
-
import { Select } from '
|
|
5
|
+
import { Select } from './CustomSelect/select'
|
|
6
6
|
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
|
|
7
7
|
import chalk from 'chalk'
|
|
8
8
|
|
|
@@ -214,13 +214,17 @@ export function Config({ onClose }: Props): React.ReactNode {
|
|
|
214
214
|
) : (
|
|
215
215
|
<Box flexDirection="column" marginLeft={2}>
|
|
216
216
|
{activeProfiles.map(profile => (
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
217
|
+
<React.Fragment key={profile.modelName}>
|
|
218
|
+
<Text color={theme.secondaryText}>
|
|
219
|
+
⢠{profile.name} ({profile.provider})
|
|
220
|
+
</Text>
|
|
221
|
+
</React.Fragment>
|
|
220
222
|
))}
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
<Box marginTop={1}>
|
|
224
|
+
<Text color={theme.suggestion}>
|
|
225
|
+
Use /model to manage model configurations
|
|
226
|
+
</Text>
|
|
227
|
+
</Box>
|
|
224
228
|
</Box>
|
|
225
229
|
)}
|
|
226
230
|
</Box>
|
|
@@ -288,7 +288,7 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
|
|
|
288
288
|
// We need to render the copy-able URL statically to prevent Ink <Text> from inserting
|
|
289
289
|
// newlines in the middle of the URL (this breaks Safari). Because <Static> components are
|
|
290
290
|
// only rendered once top-to-bottom, we also need to make everything above the URL static.
|
|
291
|
-
const staticItems: Record<string, JSX.Element> = {}
|
|
291
|
+
const staticItems: Record<string, React.JSX.Element> = {}
|
|
292
292
|
if (!isClearing) {
|
|
293
293
|
staticItems.header = (
|
|
294
294
|
<Box key="header" flexDirection="column" gap={1}>
|
|
@@ -315,9 +315,10 @@ export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
|
|
|
315
315
|
}
|
|
316
316
|
return (
|
|
317
317
|
<Box flexDirection="column" gap={1}>
|
|
318
|
-
<Static
|
|
319
|
-
{
|
|
320
|
-
|
|
318
|
+
<Static
|
|
319
|
+
items={Object.keys(staticItems)}
|
|
320
|
+
children={(item: string) => staticItems[item]}
|
|
321
|
+
/>
|
|
321
322
|
<Box paddingLeft={1} flexDirection="column" gap={1}>
|
|
322
323
|
{renderStatusMessage()}
|
|
323
324
|
</Box>
|
|
@@ -2,7 +2,7 @@ import figures from 'figures'
|
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
3
|
import React, { type ReactNode } from 'react'
|
|
4
4
|
import { type Theme } from './theme'
|
|
5
|
-
import {
|
|
5
|
+
import { getTheme } from '../../utils/theme'
|
|
6
6
|
|
|
7
7
|
export type SelectOptionProps = {
|
|
8
8
|
/**
|
|
@@ -24,6 +24,11 @@ export type SelectOptionProps = {
|
|
|
24
24
|
* Option label.
|
|
25
25
|
*/
|
|
26
26
|
readonly children: ReactNode
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* React key prop (handled internally by React)
|
|
30
|
+
*/
|
|
31
|
+
readonly key?: React.Key
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export function SelectOption({
|
|
@@ -31,8 +36,29 @@ export function SelectOption({
|
|
|
31
36
|
isSelected,
|
|
32
37
|
smallPointer,
|
|
33
38
|
children,
|
|
39
|
+
...props
|
|
34
40
|
}: SelectOptionProps) {
|
|
35
|
-
const
|
|
41
|
+
const appTheme = getTheme()
|
|
42
|
+
const styles = {
|
|
43
|
+
option: ({ isFocused }: { isFocused: boolean }) => ({
|
|
44
|
+
paddingLeft: 2,
|
|
45
|
+
paddingRight: 1,
|
|
46
|
+
}),
|
|
47
|
+
focusIndicator: () => ({
|
|
48
|
+
color: appTheme.claude,
|
|
49
|
+
}),
|
|
50
|
+
label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
|
|
51
|
+
color: isSelected
|
|
52
|
+
? appTheme.success
|
|
53
|
+
: isFocused
|
|
54
|
+
? appTheme.claude
|
|
55
|
+
: appTheme.text,
|
|
56
|
+
bold: isSelected,
|
|
57
|
+
}),
|
|
58
|
+
selectedIndicator: () => ({
|
|
59
|
+
color: appTheme.success,
|
|
60
|
+
}),
|
|
61
|
+
}
|
|
36
62
|
|
|
37
63
|
return (
|
|
38
64
|
<Box {...styles.option({ isFocused })}>
|
|
@@ -4,7 +4,8 @@ import { SelectOption } from './select-option'
|
|
|
4
4
|
import { type Theme } from './theme'
|
|
5
5
|
import { useSelectState } from './use-select-state'
|
|
6
6
|
import { useSelect } from './use-select'
|
|
7
|
-
import { Option
|
|
7
|
+
import { Option } from '@inkjs/ui'
|
|
8
|
+
import { getTheme } from '../../utils/theme'
|
|
8
9
|
|
|
9
10
|
export type OptionSubtree = {
|
|
10
11
|
/**
|
|
@@ -94,7 +95,16 @@ export function Select({
|
|
|
94
95
|
|
|
95
96
|
useSelect({ isDisabled, state })
|
|
96
97
|
|
|
97
|
-
const
|
|
98
|
+
const appTheme = getTheme()
|
|
99
|
+
const styles = {
|
|
100
|
+
container: () => ({
|
|
101
|
+
flexDirection: 'column' as const,
|
|
102
|
+
}),
|
|
103
|
+
highlightedText: () => ({
|
|
104
|
+
color: appTheme.text,
|
|
105
|
+
backgroundColor: appTheme.warning,
|
|
106
|
+
}),
|
|
107
|
+
}
|
|
98
108
|
|
|
99
109
|
return (
|
|
100
110
|
<Box {...styles.container()}>
|
|
@@ -133,9 +143,8 @@ export function Select({
|
|
|
133
143
|
isFocused={isFocused}
|
|
134
144
|
isSelected={isSelected}
|
|
135
145
|
smallPointer={smallPointer}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
</SelectOption>
|
|
146
|
+
children={label}
|
|
147
|
+
/>
|
|
139
148
|
)
|
|
140
149
|
})}
|
|
141
150
|
</Box>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Theme type definitions for CustomSelect components
|
|
2
|
+
// Used by select.tsx and select-option.tsx
|
|
3
|
+
|
|
4
|
+
import type { BoxProps, TextProps } from 'ink'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Theme interface for CustomSelect components
|
|
8
|
+
* Defines the style functions used by the select components
|
|
9
|
+
*/
|
|
10
|
+
export interface Theme {
|
|
11
|
+
/**
|
|
12
|
+
* Collection of style functions
|
|
13
|
+
*/
|
|
14
|
+
styles: {
|
|
15
|
+
/**
|
|
16
|
+
* Container styles for the select box
|
|
17
|
+
*/
|
|
18
|
+
container(): BoxProps
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Styles for individual option containers
|
|
22
|
+
*/
|
|
23
|
+
option(props: { isFocused: boolean }): BoxProps
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Styles for the focus indicator (arrow/pointer)
|
|
27
|
+
*/
|
|
28
|
+
focusIndicator(): TextProps
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Styles for option labels
|
|
32
|
+
*/
|
|
33
|
+
label(props: { isFocused: boolean; isSelected: boolean }): TextProps
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Styles for the selected indicator (checkmark)
|
|
37
|
+
*/
|
|
38
|
+
selectedIndicator(): TextProps
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Styles for highlighted text in option labels
|
|
42
|
+
*/
|
|
43
|
+
highlightedText(): TextProps
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/components/Help.tsx
CHANGED
|
@@ -175,10 +175,10 @@ export function Help({
|
|
|
175
175
|
Custom commands loaded from:
|
|
176
176
|
</Text>
|
|
177
177
|
<Text color={theme.secondaryText}>
|
|
178
|
-
⢠{getCustomCommandDirectories().
|
|
178
|
+
⢠{getCustomCommandDirectories().userClaude} (user: prefix)
|
|
179
179
|
</Text>
|
|
180
180
|
<Text color={theme.secondaryText}>
|
|
181
|
-
⢠{getCustomCommandDirectories().
|
|
181
|
+
⢠{getCustomCommandDirectories().projectClaude} (project: prefix)
|
|
182
182
|
</Text>
|
|
183
183
|
<Text color={theme.secondaryText}>
|
|
184
184
|
Use /refresh-commands to reload after changes
|
|
@@ -190,10 +190,10 @@ export function Help({
|
|
|
190
190
|
Create custom commands by adding .md files to:
|
|
191
191
|
</Text>
|
|
192
192
|
<Text color={theme.secondaryText}>
|
|
193
|
-
⢠{getCustomCommandDirectories().
|
|
193
|
+
⢠{getCustomCommandDirectories().userClaude} (user: prefix)
|
|
194
194
|
</Text>
|
|
195
195
|
<Text color={theme.secondaryText}>
|
|
196
|
-
⢠{getCustomCommandDirectories().
|
|
196
|
+
⢠{getCustomCommandDirectories().projectClaude} (project: prefix)
|
|
197
197
|
</Text>
|
|
198
198
|
<Text color={theme.secondaryText}>
|
|
199
199
|
Use /refresh-commands to reload after creation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Box, Newline, Text, useInput } from 'ink'
|
|
3
3
|
import { getTheme } from '../utils/theme'
|
|
4
|
-
import { Select } from '
|
|
4
|
+
import { Select } from './CustomSelect/select'
|
|
5
5
|
import { render } from 'ink'
|
|
6
6
|
import { writeFileSync } from 'fs'
|
|
7
7
|
import { ConfigParseError } from '../utils/errors'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
|
-
import { Select } from '
|
|
3
|
+
import { Select } from './CustomSelect/select'
|
|
4
4
|
import type { LogOption } from '../types/logs'
|
|
5
5
|
import { getTheme } from '../utils/theme'
|
|
6
6
|
import { useTerminalSize } from '../hooks/useTerminalSize'
|
|
@@ -121,6 +121,7 @@ function UserMessage({
|
|
|
121
121
|
options: {
|
|
122
122
|
verbose: boolean
|
|
123
123
|
}
|
|
124
|
+
key?: React.Key
|
|
124
125
|
}): React.ReactNode {
|
|
125
126
|
const { columns } = useTerminalSize()
|
|
126
127
|
switch (param.type) {
|
|
@@ -176,6 +177,7 @@ function AssistantMessage({
|
|
|
176
177
|
shouldAnimate: boolean
|
|
177
178
|
shouldShowDot: boolean
|
|
178
179
|
width?: number | string
|
|
180
|
+
key?: React.Key
|
|
179
181
|
}): React.ReactNode {
|
|
180
182
|
switch (param.type) {
|
|
181
183
|
case 'tool_use':
|
|
@@ -173,14 +173,18 @@ export function ModelListManager({ onClose }: Props): React.ReactNode {
|
|
|
173
173
|
<>
|
|
174
174
|
<Text color={theme.secondaryText}>({item.provider})</Text>
|
|
175
175
|
{item.usedBy.length > 0 && (
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
<Box marginLeft={1}>
|
|
177
|
+
<Text color={theme.success}>
|
|
178
|
+
[Active: {item.usedBy.join(', ')}]
|
|
179
|
+
</Text>
|
|
180
|
+
</Box>
|
|
179
181
|
)}
|
|
180
182
|
{item.usedBy.length === 0 && (
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
<Box marginLeft={1}>
|
|
184
|
+
<Text color={theme.secondaryText}>
|
|
185
|
+
[Available]
|
|
186
|
+
</Text>
|
|
187
|
+
</Box>
|
|
184
188
|
)}
|
|
185
189
|
</>
|
|
186
190
|
)}
|
|
@@ -48,13 +48,15 @@ import TextInput from './TextInput'
|
|
|
48
48
|
import OpenAI from 'openai'
|
|
49
49
|
import chalk from 'chalk'
|
|
50
50
|
import { fetchAnthropicModels, verifyApiKey } from '../services/claude'
|
|
51
|
-
import { fetchCustomModels } from '../services/openai'
|
|
51
|
+
import { fetchCustomModels, getModelFeatures } from '../services/openai'
|
|
52
|
+
import { testGPT5Connection, validateGPT5Config } from '../services/gpt5ConnectionTest'
|
|
52
53
|
type Props = {
|
|
53
54
|
onDone: () => void
|
|
54
55
|
abortController?: AbortController
|
|
55
56
|
targetPointer?: ModelPointerType // NEW: Target pointer for configuration
|
|
56
57
|
isOnboarding?: boolean // NEW: Whether this is first-time setup
|
|
57
58
|
onCancel?: () => void // NEW: Cancel callback (different from onDone)
|
|
59
|
+
skipModelType?: boolean // NEW: Skip model type selection
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
type ModelInfo = {
|
|
@@ -154,6 +156,7 @@ export function ModelSelector({
|
|
|
154
156
|
targetPointer,
|
|
155
157
|
isOnboarding = false,
|
|
156
158
|
onCancel,
|
|
159
|
+
skipModelType = false,
|
|
157
160
|
}: Props): React.ReactNode {
|
|
158
161
|
const config = getGlobalConfig()
|
|
159
162
|
const theme = getTheme()
|
|
@@ -1252,7 +1255,7 @@ export function ModelSelector({
|
|
|
1252
1255
|
// Transform the response into our ModelInfo format
|
|
1253
1256
|
const fetchedModels = []
|
|
1254
1257
|
for (const model of response.data) {
|
|
1255
|
-
const modelName = model.modelName || model.id || model.name || model.model || 'unknown'
|
|
1258
|
+
const modelName = (model as any).modelName || (model as any).id || (model as any).name || (model as any).model || 'unknown'
|
|
1256
1259
|
const modelInfo = models[selectedProvider as keyof typeof models]?.find(
|
|
1257
1260
|
m => m.model === modelName,
|
|
1258
1261
|
)
|
|
@@ -1477,7 +1480,42 @@ export function ModelSelector({
|
|
|
1477
1480
|
].includes(selectedProvider)
|
|
1478
1481
|
|
|
1479
1482
|
if (isOpenAICompatible) {
|
|
1480
|
-
//
|
|
1483
|
+
// š„ Use specialized GPT-5 connection test for GPT-5 models
|
|
1484
|
+
const isGPT5 = selectedModel?.toLowerCase().includes('gpt-5')
|
|
1485
|
+
|
|
1486
|
+
if (isGPT5) {
|
|
1487
|
+
console.log(`š Using specialized GPT-5 connection test for model: ${selectedModel}`)
|
|
1488
|
+
|
|
1489
|
+
// Validate configuration first
|
|
1490
|
+
const configValidation = validateGPT5Config({
|
|
1491
|
+
model: selectedModel,
|
|
1492
|
+
apiKey: apiKey,
|
|
1493
|
+
baseURL: testBaseURL,
|
|
1494
|
+
maxTokens: parseInt(maxTokens) || 8192,
|
|
1495
|
+
provider: selectedProvider,
|
|
1496
|
+
})
|
|
1497
|
+
|
|
1498
|
+
if (!configValidation.valid) {
|
|
1499
|
+
return {
|
|
1500
|
+
success: false,
|
|
1501
|
+
message: 'ā GPT-5 configuration validation failed',
|
|
1502
|
+
details: configValidation.errors.join('\n'),
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Use specialized GPT-5 test service
|
|
1507
|
+
const gpt5Result = await testGPT5Connection({
|
|
1508
|
+
model: selectedModel,
|
|
1509
|
+
apiKey: apiKey,
|
|
1510
|
+
baseURL: testBaseURL,
|
|
1511
|
+
maxTokens: parseInt(maxTokens) || 8192,
|
|
1512
|
+
provider: selectedProvider,
|
|
1513
|
+
})
|
|
1514
|
+
|
|
1515
|
+
return gpt5Result
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// For non-GPT-5 OpenAI-compatible models, use existing logic
|
|
1481
1519
|
const endpointsToTry = []
|
|
1482
1520
|
|
|
1483
1521
|
if (selectedProvider === 'minimax') {
|
|
@@ -1503,6 +1541,7 @@ export function ModelSelector({
|
|
|
1503
1541
|
endpoint.path,
|
|
1504
1542
|
endpoint.name,
|
|
1505
1543
|
)
|
|
1544
|
+
|
|
1506
1545
|
if (testResult.success) {
|
|
1507
1546
|
return testResult
|
|
1508
1547
|
}
|
|
@@ -1552,7 +1591,7 @@ export function ModelSelector({
|
|
|
1552
1591
|
const testURL = `${baseURL.replace(/\/+$/, '')}${endpointPath}`
|
|
1553
1592
|
|
|
1554
1593
|
// Create a test message that expects a specific response
|
|
1555
|
-
const testPayload = {
|
|
1594
|
+
const testPayload: any = {
|
|
1556
1595
|
model: selectedModel,
|
|
1557
1596
|
messages: [
|
|
1558
1597
|
{
|
|
@@ -1566,6 +1605,24 @@ export function ModelSelector({
|
|
|
1566
1605
|
stream: false,
|
|
1567
1606
|
}
|
|
1568
1607
|
|
|
1608
|
+
// GPT-5 parameter compatibility fix
|
|
1609
|
+
if (selectedModel && selectedModel.toLowerCase().includes('gpt-5')) {
|
|
1610
|
+
console.log(`Applying GPT-5 parameter fix for model: ${selectedModel}`)
|
|
1611
|
+
|
|
1612
|
+
// GPT-5 requires max_completion_tokens instead of max_tokens
|
|
1613
|
+
if (testPayload.max_tokens) {
|
|
1614
|
+
testPayload.max_completion_tokens = testPayload.max_tokens
|
|
1615
|
+
delete testPayload.max_tokens
|
|
1616
|
+
console.log(`Transformed max_tokens ā max_completion_tokens: ${testPayload.max_completion_tokens}`)
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// GPT-5 temperature handling - ensure it's 1 or undefined
|
|
1620
|
+
if (testPayload.temperature !== undefined && testPayload.temperature !== 1) {
|
|
1621
|
+
console.log(`Adjusting temperature from ${testPayload.temperature} to 1 for GPT-5`)
|
|
1622
|
+
testPayload.temperature = 1
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1569
1626
|
const headers: Record<string, string> = {
|
|
1570
1627
|
'Content-Type': 'application/json',
|
|
1571
1628
|
}
|
|
@@ -1646,6 +1703,123 @@ export function ModelSelector({
|
|
|
1646
1703
|
}
|
|
1647
1704
|
}
|
|
1648
1705
|
|
|
1706
|
+
async function testResponsesEndpoint(
|
|
1707
|
+
baseURL: string,
|
|
1708
|
+
endpointPath: string,
|
|
1709
|
+
endpointName: string,
|
|
1710
|
+
): Promise<{
|
|
1711
|
+
success: boolean
|
|
1712
|
+
message: string
|
|
1713
|
+
endpoint?: string
|
|
1714
|
+
details?: string
|
|
1715
|
+
}> {
|
|
1716
|
+
const testURL = `${baseURL.replace(/\/+$/, '')}${endpointPath}`
|
|
1717
|
+
|
|
1718
|
+
// š§ Enhanced GPT-5 Responses API test payload
|
|
1719
|
+
const testPayload: any = {
|
|
1720
|
+
model: selectedModel,
|
|
1721
|
+
input: [
|
|
1722
|
+
{
|
|
1723
|
+
role: 'user',
|
|
1724
|
+
content:
|
|
1725
|
+
'Please respond with exactly "YES" (in capital letters) to confirm this connection is working.',
|
|
1726
|
+
},
|
|
1727
|
+
],
|
|
1728
|
+
max_completion_tokens: Math.max(parseInt(maxTokens) || 8192, 8192),
|
|
1729
|
+
temperature: 1, // GPT-5 only supports temperature=1
|
|
1730
|
+
// š Add reasoning configuration for better GPT-5 performance
|
|
1731
|
+
reasoning: {
|
|
1732
|
+
effort: 'low', // Fast response for connection test
|
|
1733
|
+
},
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
console.log(`š§ Testing GPT-5 Responses API for model: ${selectedModel}`)
|
|
1737
|
+
console.log(`š§ Test URL: ${testURL}`)
|
|
1738
|
+
console.log(`š§ Test payload:`, JSON.stringify(testPayload, null, 2))
|
|
1739
|
+
|
|
1740
|
+
const headers: Record<string, string> = {
|
|
1741
|
+
'Content-Type': 'application/json',
|
|
1742
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
try {
|
|
1746
|
+
const response = await fetch(testURL, {
|
|
1747
|
+
method: 'POST',
|
|
1748
|
+
headers,
|
|
1749
|
+
body: JSON.stringify(testPayload),
|
|
1750
|
+
})
|
|
1751
|
+
|
|
1752
|
+
if (response.ok) {
|
|
1753
|
+
const data = await response.json()
|
|
1754
|
+
console.log(
|
|
1755
|
+
'[DEBUG] Responses API connection test response:',
|
|
1756
|
+
JSON.stringify(data, null, 2),
|
|
1757
|
+
)
|
|
1758
|
+
|
|
1759
|
+
// Extract content from Responses API format
|
|
1760
|
+
let responseContent = ''
|
|
1761
|
+
|
|
1762
|
+
if (data.output_text) {
|
|
1763
|
+
responseContent = data.output_text
|
|
1764
|
+
} else if (data.output) {
|
|
1765
|
+
responseContent = typeof data.output === 'string' ? data.output : data.output.text || ''
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
console.log('[DEBUG] Extracted response content:', responseContent)
|
|
1769
|
+
|
|
1770
|
+
// Check if response contains "YES" (case insensitive)
|
|
1771
|
+
const containsYes = responseContent.toLowerCase().includes('yes')
|
|
1772
|
+
|
|
1773
|
+
if (containsYes) {
|
|
1774
|
+
return {
|
|
1775
|
+
success: true,
|
|
1776
|
+
message: `ā
Connection test passed with ${endpointName}`,
|
|
1777
|
+
endpoint: endpointPath,
|
|
1778
|
+
details: `GPT-5 responded correctly via Responses API: "${responseContent.trim()}"`,
|
|
1779
|
+
}
|
|
1780
|
+
} else {
|
|
1781
|
+
return {
|
|
1782
|
+
success: false,
|
|
1783
|
+
message: `ā ļø ${endpointName} connected but model response unexpected`,
|
|
1784
|
+
endpoint: endpointPath,
|
|
1785
|
+
details: `Expected "YES" but got: "${responseContent.trim() || '(empty response)'}"`,
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
} else {
|
|
1789
|
+
// š§ Enhanced error handling with detailed debugging
|
|
1790
|
+
const errorData = await response.json().catch(() => null)
|
|
1791
|
+
const errorMessage =
|
|
1792
|
+
errorData?.error?.message || errorData?.message || response.statusText
|
|
1793
|
+
|
|
1794
|
+
console.log(`šØ GPT-5 Responses API Error (${response.status}):`, errorData)
|
|
1795
|
+
|
|
1796
|
+
// š§ Provide specific guidance for common GPT-5 errors
|
|
1797
|
+
let details = `Responses API Error: ${errorMessage}`
|
|
1798
|
+
if (response.status === 400 && errorMessage.includes('max_tokens')) {
|
|
1799
|
+
details += '\nš§ Note: This appears to be a parameter compatibility issue. The fallback to Chat Completions should handle this.'
|
|
1800
|
+
} else if (response.status === 404) {
|
|
1801
|
+
details += '\nš§ Note: Responses API endpoint may not be available for this model or provider.'
|
|
1802
|
+
} else if (response.status === 401) {
|
|
1803
|
+
details += '\nš§ Note: API key authentication failed.'
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
return {
|
|
1807
|
+
success: false,
|
|
1808
|
+
message: `ā ${endpointName} failed (${response.status})`,
|
|
1809
|
+
endpoint: endpointPath,
|
|
1810
|
+
details: details,
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
return {
|
|
1815
|
+
success: false,
|
|
1816
|
+
message: `ā ${endpointName} connection failed`,
|
|
1817
|
+
endpoint: endpointPath,
|
|
1818
|
+
details: error instanceof Error ? error.message : String(error),
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1649
1823
|
async function testProviderSpecificEndpoint(baseURL: string): Promise<{
|
|
1650
1824
|
success: boolean
|
|
1651
1825
|
message: string
|
|
@@ -3181,28 +3355,32 @@ export function ModelSelector({
|
|
|
3181
3355
|
|
|
3182
3356
|
// Render Provider Selection Screen
|
|
3183
3357
|
return (
|
|
3184
|
-
<ScreenContainer
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
Choose the provider you want to use for this model profile.
|
|
3192
|
-
<Newline />
|
|
3193
|
-
This will determine which models are available to you.
|
|
3358
|
+
<ScreenContainer
|
|
3359
|
+
title="Provider Selection"
|
|
3360
|
+
exitState={exitState}
|
|
3361
|
+
children={
|
|
3362
|
+
<Box flexDirection="column" gap={1}>
|
|
3363
|
+
<Text bold>
|
|
3364
|
+
Select your preferred AI provider for this model profile:
|
|
3194
3365
|
</Text>
|
|
3195
|
-
|
|
3366
|
+
<Box flexDirection="column" width={70}>
|
|
3367
|
+
<Text color={theme.secondaryText}>
|
|
3368
|
+
Choose the provider you want to use for this model profile.
|
|
3369
|
+
<Newline />
|
|
3370
|
+
This will determine which models are available to you.
|
|
3371
|
+
</Text>
|
|
3372
|
+
</Box>
|
|
3196
3373
|
|
|
3197
|
-
|
|
3374
|
+
<Select options={providerOptions} onChange={handleProviderSelection} />
|
|
3198
3375
|
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3376
|
+
<Box marginTop={1}>
|
|
3377
|
+
<Text dimColor>
|
|
3378
|
+
You can change this later by running{' '}
|
|
3379
|
+
<Text color={theme.suggestion}>/model</Text> again
|
|
3380
|
+
</Text>
|
|
3381
|
+
</Box>
|
|
3204
3382
|
</Box>
|
|
3205
|
-
|
|
3206
|
-
|
|
3383
|
+
}
|
|
3384
|
+
/>
|
|
3207
3385
|
)
|
|
3208
3386
|
}
|