thatgfsj-code 3.0.4 → 3.0.6
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 +64 -21
- package/dist/app/index.d.ts +93 -22
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +232 -40
- package/dist/app/index.js.map +1 -1
- package/dist/cmd/index.d.ts +12 -20
- package/dist/cmd/index.d.ts.map +1 -1
- package/dist/cmd/index.js +192 -146
- package/dist/cmd/index.js.map +1 -1
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/llm/anthropic.d.ts +1 -1
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +116 -84
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/gemini.d.ts +27 -3
- package/dist/llm/gemini.d.ts.map +1 -1
- package/dist/llm/gemini.js +196 -73
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/index.d.ts +20 -1
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +66 -6
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/openai.d.ts +5 -1
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +96 -56
- package/dist/llm/openai.js.map +1 -1
- package/dist/mcp/client.d.ts +84 -34
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +291 -126
- package/dist/mcp/client.js.map +1 -1
- package/dist/session/compactor.d.ts +27 -7
- package/dist/session/compactor.d.ts.map +1 -1
- package/dist/session/compactor.js +78 -23
- package/dist/session/compactor.js.map +1 -1
- package/dist/session/index.d.ts +96 -33
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +278 -60
- package/dist/session/index.js.map +1 -1
- package/dist/tools/file.d.ts +10 -2
- package/dist/tools/file.d.ts.map +1 -1
- package/dist/tools/file.js +54 -5
- package/dist/tools/file.js.map +1 -1
- package/dist/tools/git.d.ts +16 -2
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +61 -18
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/nwt.d.ts.map +1 -1
- package/dist/tools/nwt.js +8 -1
- package/dist/tools/nwt.js.map +1 -1
- package/dist/tools/search.d.ts +8 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +105 -34
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/shell.d.ts +0 -4
- package/dist/tools/shell.d.ts.map +1 -1
- package/dist/tools/shell.js +11 -26
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/types.d.ts +7 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +90 -18
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/ChatMessage.d.ts.map +1 -1
- package/dist/tui/components/ChatMessage.js +9 -2
- package/dist/tui/components/ChatMessage.js.map +1 -1
- package/dist/tui/components/ConfirmPrompt.d.ts +21 -0
- package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
- package/dist/tui/components/ConfirmPrompt.js +45 -0
- package/dist/tui/components/ConfirmPrompt.js.map +1 -0
- package/dist/tui/components/Header.d.ts +10 -5
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +13 -5
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/StatusBar.d.ts +7 -0
- package/dist/tui/components/StatusBar.d.ts.map +1 -1
- package/dist/tui/components/StatusBar.js +8 -3
- package/dist/tui/components/StatusBar.js.map +1 -1
- package/dist/tui/components/Thinking.d.ts +5 -0
- package/dist/tui/components/Thinking.d.ts.map +1 -1
- package/dist/tui/components/Thinking.js +24 -2
- package/dist/tui/components/Thinking.js.map +1 -1
- package/dist/tui/components/ToolCall.d.ts +6 -1
- package/dist/tui/components/ToolCall.d.ts.map +1 -1
- package/dist/tui/components/ToolCall.js +50 -22
- package/dist/tui/components/ToolCall.js.map +1 -1
- package/dist/tui/components/UserInput.d.ts +5 -0
- package/dist/tui/components/UserInput.d.ts.map +1 -1
- package/dist/tui/components/UserInput.js +9 -13
- package/dist/tui/components/UserInput.js.map +1 -1
- package/dist/tui/hooks/useChat.d.ts +1 -0
- package/dist/tui/hooks/useChat.d.ts.map +1 -1
- package/dist/tui/hooks/useChat.js +36 -16
- package/dist/tui/hooks/useChat.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts +7 -1
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +77 -41
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/dist/tui/theme.d.ts +34 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +34 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +9 -7
- package/dist/tui/welcome.js.map +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/diff.d.ts +5 -0
- package/dist/utils/diff.d.ts.map +1 -1
- package/dist/utils/diff.js +15 -0
- package/dist/utils/diff.js.map +1 -1
- package/dist/version.d.ts +12 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +29 -0
- package/dist/version.js.map +1 -0
- package/package.json +19 -7
- package/CHANGELOG.md +0 -192
- package/DEVELOPMENT.md +0 -286
- package/ROADMAP.md +0 -91
- package/docs/API_KEY_GUIDE.md +0 -236
- package/docs/FAQ.md +0 -182
- package/install.bat +0 -63
- package/install.ps1 +0 -238
- package/install.sh +0 -113
- package/src/app/index.ts +0 -197
- package/src/cache/fingerprint.ts +0 -101
- package/src/cache/index.ts +0 -21
- package/src/cache/smartModel.ts +0 -133
- package/src/cache/stats.ts +0 -199
- package/src/cache/volatile.ts +0 -47
- package/src/cmd/index.tsx +0 -288
- package/src/config/index.ts +0 -156
- package/src/config/providers.ts +0 -234
- package/src/config/types.ts +0 -71
- package/src/hooks/index.ts +0 -111
- package/src/llm/anthropic.ts +0 -413
- package/src/llm/gemini.ts +0 -169
- package/src/llm/index.ts +0 -303
- package/src/llm/openai.ts +0 -243
- package/src/llm/provider.ts +0 -71
- package/src/mcp/client.ts +0 -330
- package/src/prompts/index.ts +0 -260
- package/src/session/compactor.ts +0 -103
- package/src/session/index.ts +0 -181
- package/src/session/message.ts +0 -42
- package/src/skills/brainstorming.ts +0 -43
- package/src/skills/code-review.ts +0 -45
- package/src/skills/executing-plans.ts +0 -27
- package/src/skills/frontend-design.ts +0 -35
- package/src/skills/git-workflow.ts +0 -36
- package/src/skills/improve-architecture.ts +0 -38
- package/src/skills/index.ts +0 -136
- package/src/skills/neuroweave.ts +0 -47
- package/src/skills/playwright.ts +0 -72
- package/src/skills/prototype.ts +0 -30
- package/src/skills/subagent.ts +0 -28
- package/src/skills/supabase.ts +0 -44
- package/src/skills/systematic-debugging.ts +0 -44
- package/src/skills/tdd.ts +0 -39
- package/src/skills/triage.ts +0 -35
- package/src/skills/verification.ts +0 -31
- package/src/skills/writing-plans.ts +0 -42
- package/src/tools/file.ts +0 -143
- package/src/tools/git.ts +0 -132
- package/src/tools/index.ts +0 -111
- package/src/tools/nwt.ts +0 -598
- package/src/tools/search.ts +0 -263
- package/src/tools/shell.ts +0 -136
- package/src/tools/types.ts +0 -122
- package/src/tui/app.tsx +0 -159
- package/src/tui/components/ChatList.tsx +0 -41
- package/src/tui/components/ChatMessage.tsx +0 -54
- package/src/tui/components/Header.tsx +0 -66
- package/src/tui/components/InitWizard.tsx +0 -247
- package/src/tui/components/Markdown.tsx +0 -35
- package/src/tui/components/ModelSelector.tsx +0 -107
- package/src/tui/components/StatusBar.tsx +0 -30
- package/src/tui/components/Thinking.tsx +0 -17
- package/src/tui/components/ToolCall.tsx +0 -102
- package/src/tui/components/UserInput.tsx +0 -151
- package/src/tui/hooks/useChat.ts +0 -287
- package/src/tui/hooks/useCommands.ts +0 -271
- package/src/tui/index.ts +0 -6
- package/src/tui/welcome.ts +0 -177
- package/src/types.ts +0 -104
- package/src/utils/diff.ts +0 -71
- package/src/utils/project.ts +0 -99
- package/src/utils/stableStringify.ts +0 -47
- package/src/utils/thinking.ts +0 -119
- package/tests/cache/fingerprint.test.ts +0 -75
- package/tests/cache/providerCatalog.test.ts +0 -31
- package/tests/cache/smartModel.test.ts +0 -100
- package/tests/cache/stableStringify.test.ts +0 -43
- package/tests/cache/stats.test.ts +0 -146
- package/tests/cache/volatile.test.ts +0 -75
- package/tests/smoke-pollution.mjs +0 -78
- package/tests/smoke-thinking.mjs +0 -110
- package/tests/smoke-tool-stream.mjs +0 -69
- package/tests/smoke-tool.mjs +0 -117
- package/tsconfig.json +0 -21
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React, { memo } from 'react';
|
|
3
|
-
import { Box, Static } from 'ink';
|
|
4
|
-
import { ChatMessage, type MessageData } from './ChatMessage.js';
|
|
5
|
-
import type { ToolCallData } from './ToolCall.js';
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
messages: MessageData[];
|
|
9
|
-
streaming?: string;
|
|
10
|
-
streamingToolCalls?: ToolCallData[];
|
|
11
|
-
width?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const ChatList = memo(function ChatList({ messages, streaming, streamingToolCalls, width }: Props) {
|
|
15
|
-
const hasStreaming = !!(streaming || (streamingToolCalls && streamingToolCalls.length > 0));
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<Box flexDirection="column">
|
|
19
|
-
{/* Completed messages - Static prevents re-rendering */}
|
|
20
|
-
{messages.length > 0 && (
|
|
21
|
-
<Static items={messages}>
|
|
22
|
-
{(msg: MessageData, index: number) => (
|
|
23
|
-
<ChatMessage key={`msg-${index}`} message={msg} width={width} />
|
|
24
|
-
)}
|
|
25
|
-
</Static>
|
|
26
|
-
)}
|
|
27
|
-
|
|
28
|
-
{/* Streaming content - only this part re-renders */}
|
|
29
|
-
{hasStreaming && (
|
|
30
|
-
<ChatMessage
|
|
31
|
-
message={{
|
|
32
|
-
role: 'assistant',
|
|
33
|
-
content: streaming || '',
|
|
34
|
-
toolCalls: streamingToolCalls && streamingToolCalls.length > 0 ? streamingToolCalls : undefined,
|
|
35
|
-
}}
|
|
36
|
-
width={width}
|
|
37
|
-
/>
|
|
38
|
-
)}
|
|
39
|
-
</Box>
|
|
40
|
-
);
|
|
41
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { Markdown } from './Markdown.js';
|
|
5
|
-
import { ToolCall, type ToolCallData } from './ToolCall.js';
|
|
6
|
-
|
|
7
|
-
interface MessageData {
|
|
8
|
-
role: 'user' | 'assistant' | 'tool';
|
|
9
|
-
content: string;
|
|
10
|
-
toolCalls?: ToolCallData[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface Props {
|
|
14
|
-
message: MessageData;
|
|
15
|
-
width?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function UserMessage({ content }: { content: string }) {
|
|
19
|
-
return (
|
|
20
|
-
<Box flexDirection="column" marginBottom={1} paddingLeft={1}>
|
|
21
|
-
<Text bold color="#06B6D4">You</Text>
|
|
22
|
-
<Box paddingLeft={2}>
|
|
23
|
-
<Text>{content}</Text>
|
|
24
|
-
</Box>
|
|
25
|
-
</Box>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function AssistantMessage({ content, toolCalls }: { content: string; toolCalls?: ToolCallData[] }) {
|
|
30
|
-
return (
|
|
31
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
32
|
-
{toolCalls && toolCalls.map((tc, i) => (
|
|
33
|
-
<ToolCall key={i} tool={tc} />
|
|
34
|
-
))}
|
|
35
|
-
{content && (
|
|
36
|
-
<Box flexDirection="column" paddingLeft={1}>
|
|
37
|
-
<Text bold color="#22D3EE">AI</Text>
|
|
38
|
-
<Box paddingLeft={2}>
|
|
39
|
-
<Markdown content={content} />
|
|
40
|
-
</Box>
|
|
41
|
-
</Box>
|
|
42
|
-
)}
|
|
43
|
-
</Box>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const ChatMessage = React.memo(function ChatMessage({ message }: Props) {
|
|
48
|
-
if (message.role === 'user') {
|
|
49
|
-
return <UserMessage content={message.content} />;
|
|
50
|
-
}
|
|
51
|
-
return <AssistantMessage content={message.content} toolCalls={message.toolCalls} />;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
export type { MessageData };
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
provider: string;
|
|
7
|
-
model: string;
|
|
8
|
-
/**
|
|
9
|
-
* v3.0.0: optional cache hit-rate + estimated savings. When provided,
|
|
10
|
-
* the header shows `⚡ 命中率 87% · 节省 ¥0.42` on the right side. When
|
|
11
|
-
* null (no rounds yet, or provider does not surface usage), the right
|
|
12
|
-
* side just shows provider/model.
|
|
13
|
-
*/
|
|
14
|
-
cacheHitRate?: number | null;
|
|
15
|
-
cacheSavingsCNY?: number;
|
|
16
|
-
/**
|
|
17
|
-
* v3.0.3: cache TTL marker. '5m' (default for short sessions) or '1h'
|
|
18
|
-
* (auto-decided for long sessions, or pinned by the user). 'auto' is
|
|
19
|
-
* the user-pinned setting and shows up as ⏱ auto. null = not yet
|
|
20
|
-
* decided (waiting for first round).
|
|
21
|
-
*/
|
|
22
|
-
cacheTtl?: '5m' | '1h' | 'auto' | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Choose a color for the hit-rate chip based on Reasonix-style thresholds:
|
|
27
|
-
* ≥ 80% green (excellent — most of the prefix is being reused)
|
|
28
|
-
* ≥ 50% yellow (partial hit — system prompt is cached but messages tail is not)
|
|
29
|
-
* < 50% gray (cold — every round re-creates the cache; investigate)
|
|
30
|
-
*/
|
|
31
|
-
function hitRateColor(rate: number): string {
|
|
32
|
-
if (rate >= 0.8) return '#10B981';
|
|
33
|
-
if (rate >= 0.5) return '#F59E0B';
|
|
34
|
-
return '#6B7280';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const Header = React.memo(function Header({ provider, model, cacheHitRate, cacheSavingsCNY, cacheTtl }: Props) {
|
|
38
|
-
const showCache = typeof cacheHitRate === 'number' && cacheHitRate > 0;
|
|
39
|
-
return (
|
|
40
|
-
<Box flexDirection="column" marginBottom={0}>
|
|
41
|
-
<Box justifyContent="space-between" width="100%">
|
|
42
|
-
<Box>
|
|
43
|
-
<Text color="#06B6D4" bold> ⚡ </Text>
|
|
44
|
-
<Text color="#22D3EE" bold>THATGFSJ CODE</Text>
|
|
45
|
-
<Text dimColor> v3.0.4</Text>
|
|
46
|
-
</Box>
|
|
47
|
-
<Box>
|
|
48
|
-
{showCache && (
|
|
49
|
-
<>
|
|
50
|
-
<Text color={hitRateColor(cacheHitRate!)} bold> ⚡ {(cacheHitRate! * 100).toFixed(1)}% </Text>
|
|
51
|
-
<Text dimColor> · 节省 ¥{(cacheSavingsCNY ?? 0).toFixed(2)} </Text>
|
|
52
|
-
<Text dimColor> · </Text>
|
|
53
|
-
</>
|
|
54
|
-
)}
|
|
55
|
-
{cacheTtl && (
|
|
56
|
-
<Text color="#A78BFA" bold> ⏱ {cacheTtl} </Text>
|
|
57
|
-
)}
|
|
58
|
-
<Text color="#06B6D4" bold> {provider} </Text>
|
|
59
|
-
<Text dimColor>/</Text>
|
|
60
|
-
<Text color="#22D3EE"> {model} </Text>
|
|
61
|
-
</Box>
|
|
62
|
-
</Box>
|
|
63
|
-
<Text color="#374151">{'─'.repeat(80)}</Text>
|
|
64
|
-
</Box>
|
|
65
|
-
);
|
|
66
|
-
});
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React, { useState } from 'react';
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import SelectInput from 'ink-select-input';
|
|
5
|
-
import TextInput from 'ink-text-input';
|
|
6
|
-
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
import { homedir } from 'os';
|
|
9
|
-
import { listProviders, getModelsForProvider, isCustomProvider } from '../../config/providers.js';
|
|
10
|
-
import type { ProviderName } from '../../config/types.js';
|
|
11
|
-
|
|
12
|
-
interface Props {
|
|
13
|
-
onComplete: (provider: ProviderName, model: string, apiKey: string, baseUrl?: string) => void;
|
|
14
|
-
onCancel: () => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type InitStep = 'provider' | 'custom_url' | 'api_key' | 'model' | 'custom_model' | 'cache_strategy';
|
|
18
|
-
|
|
19
|
-
interface CacheChoice {
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
ttl: '5m' | '1h';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* v3.0.1: InitWizard is now fully self-contained — it manages its own
|
|
26
|
-
* step state AND its own keyboard input for text-entry steps (api_key,
|
|
27
|
-
* custom_url, custom_model). The previous version had a fatal bug:
|
|
28
|
-
*
|
|
29
|
-
* The text-entry steps (`api_key`, `custom_url`, `custom_model`) only
|
|
30
|
-
* rendered a `<Text>` prompt with no actual input box. The host
|
|
31
|
-
* component (app.tsx) had its own UserInput BUT only rendered it when
|
|
32
|
-
* viewMode was NOT 'init_wizard' (see app.tsx ternary). So when the
|
|
33
|
-
* wizard's internal step advanced to 'api_key', the user could SEE the
|
|
34
|
-
* "输入 API Key: 服务商: deepseek" prompt but had NO way to type into
|
|
35
|
-
* it. Ink silently swallowed keystrokes (no useInput handler was
|
|
36
|
-
* active), and the user was stuck on a frozen screen — exactly the
|
|
37
|
-
* bug that prompted the user-reported "选择 deepseek 然后退出":
|
|
38
|
-
* pressing Ctrl+C felt like the only escape.
|
|
39
|
-
*
|
|
40
|
-
* Fix: this file now uses `useInput` and `ink-text-input` for the text
|
|
41
|
-
* steps. The SetViewMode-based input delegation in app.tsx is no longer
|
|
42
|
-
* required for the InitWizard flow.
|
|
43
|
-
*/
|
|
44
|
-
export function InitWizard({ onComplete, onCancel }: Props) {
|
|
45
|
-
const [step, setStep] = useState<InitStep>('provider');
|
|
46
|
-
const [selectedProvider, setSelectedProvider] = useState<ProviderName>('siliconflow');
|
|
47
|
-
const [apiKey, setApiKey] = useState('');
|
|
48
|
-
const [selectedModel, setSelectedModel] = useState('');
|
|
49
|
-
const [customUrl, setCustomUrl] = useState('');
|
|
50
|
-
const [cacheChoice, setCacheChoice] = useState<CacheChoice>({ enabled: true, ttl: '5m' });
|
|
51
|
-
|
|
52
|
-
const providers = listProviders();
|
|
53
|
-
const models = getModelsForProvider(selectedProvider);
|
|
54
|
-
|
|
55
|
-
const saveConfig = (provider: ProviderName, model: string, key: string, url?: string) => {
|
|
56
|
-
const dir = join(homedir(), '.thatgfsj');
|
|
57
|
-
const configPath = join(dir, 'config.json');
|
|
58
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
59
|
-
|
|
60
|
-
const config: Record<string, any> = {
|
|
61
|
-
provider,
|
|
62
|
-
model,
|
|
63
|
-
apiKey: key,
|
|
64
|
-
temperature: 0.7,
|
|
65
|
-
maxTokens: 4096,
|
|
66
|
-
contextLength: 50,
|
|
67
|
-
cache: {
|
|
68
|
-
enabled: cacheChoice.enabled,
|
|
69
|
-
ttl: cacheChoice.ttl,
|
|
70
|
-
strategy: 'auto',
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
if (url) config.baseUrl = url;
|
|
74
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// v3.0.1: ESC cancels the wizard from any text-entry step. This is the
|
|
78
|
-
// hook that the previous version was missing — when the user pressed
|
|
79
|
-
// ESC on the frozen "输入 API Key" screen, nothing happened because no
|
|
80
|
-
// useInput was active. Now it bails out cleanly.
|
|
81
|
-
useInput((_input, key) => {
|
|
82
|
-
if (key.escape && (step === 'api_key' || step === 'custom_url' || step === 'custom_model')) {
|
|
83
|
-
onCancel();
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Step 1: Provider selection
|
|
88
|
-
if (step === 'provider') {
|
|
89
|
-
const items = providers.map(p => ({ label: p.name, value: p.key }));
|
|
90
|
-
return (
|
|
91
|
-
<Box flexDirection="column" paddingLeft={1}>
|
|
92
|
-
<Text color="#06B6D4" bold>选择服务商 (↑↓ 回车):</Text>
|
|
93
|
-
<SelectInput
|
|
94
|
-
items={items}
|
|
95
|
-
onSelect={(item) => {
|
|
96
|
-
const provider = item.value as ProviderName;
|
|
97
|
-
setSelectedProvider(provider);
|
|
98
|
-
if (isCustomProvider(provider)) {
|
|
99
|
-
setStep('custom_url');
|
|
100
|
-
} else {
|
|
101
|
-
setStep('api_key');
|
|
102
|
-
}
|
|
103
|
-
}}
|
|
104
|
-
/>
|
|
105
|
-
</Box>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Custom URL input — v3.0.1 NOW has a real TextInput
|
|
110
|
-
if (step === 'custom_url') {
|
|
111
|
-
return (
|
|
112
|
-
<Box flexDirection="column" paddingLeft={1}>
|
|
113
|
-
<Text color="#06B6D4" bold>输入中转站 URL:</Text>
|
|
114
|
-
<Text dimColor>例如: https://api.example.com/v1</Text>
|
|
115
|
-
<Text color="#F59E0B">(请直接输入 URL 并回车,ESC 取消)</Text>
|
|
116
|
-
<Box marginTop={1}>
|
|
117
|
-
<Text color="#06B6D4">❯ </Text>
|
|
118
|
-
<TextInput
|
|
119
|
-
value={customUrl}
|
|
120
|
-
onChange={setCustomUrl}
|
|
121
|
-
onSubmit={(value) => {
|
|
122
|
-
if (value.trim()) {
|
|
123
|
-
setCustomUrl(value.trim());
|
|
124
|
-
setStep('api_key');
|
|
125
|
-
}
|
|
126
|
-
}}
|
|
127
|
-
/>
|
|
128
|
-
</Box>
|
|
129
|
-
</Box>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Step 2: API Key — v3.0.1 NOW has a real TextInput
|
|
134
|
-
if (step === 'api_key') {
|
|
135
|
-
const providerLabel = providers.find(p => p.key === selectedProvider)?.name || selectedProvider;
|
|
136
|
-
return (
|
|
137
|
-
<Box flexDirection="column" paddingLeft={1}>
|
|
138
|
-
<Text color="#06B6D4" bold>输入 API Key:</Text>
|
|
139
|
-
<Text dimColor>服务商: {providerLabel}</Text>
|
|
140
|
-
<Text color="#F59E0B">(请直接输入 Key 并回车,ESC 取消)</Text>
|
|
141
|
-
<Box marginTop={1}>
|
|
142
|
-
<Text color="#06B6D4">❯ </Text>
|
|
143
|
-
<TextInput
|
|
144
|
-
value={apiKey}
|
|
145
|
-
onChange={setApiKey}
|
|
146
|
-
onSubmit={(value) => {
|
|
147
|
-
if (value.trim()) {
|
|
148
|
-
setApiKey(value.trim());
|
|
149
|
-
if (isCustomProvider(selectedProvider)) {
|
|
150
|
-
// Custom OpenAI/Anthropic: after key, go to model
|
|
151
|
-
setStep('model');
|
|
152
|
-
} else {
|
|
153
|
-
// Standard providers: skip to model selection
|
|
154
|
-
setStep('model');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}}
|
|
158
|
-
/>
|
|
159
|
-
</Box>
|
|
160
|
-
</Box>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Step 3: Model selection
|
|
165
|
-
if (step === 'model') {
|
|
166
|
-
const items = [
|
|
167
|
-
...models.map(m => ({ label: `${m.name} - ${m.desc}`, value: m.id })),
|
|
168
|
-
{ label: '+ 输入自定义模型名', value: '__custom__' },
|
|
169
|
-
];
|
|
170
|
-
return (
|
|
171
|
-
<Box flexDirection="column" paddingLeft={1}>
|
|
172
|
-
<Text color="#06B6D4" bold>选择模型 (↑↓ 回车):</Text>
|
|
173
|
-
<SelectInput
|
|
174
|
-
items={items}
|
|
175
|
-
onSelect={(item) => {
|
|
176
|
-
if (item.value === '__custom__') {
|
|
177
|
-
setStep('custom_model');
|
|
178
|
-
} else {
|
|
179
|
-
setSelectedModel(item.value);
|
|
180
|
-
setStep('cache_strategy');
|
|
181
|
-
}
|
|
182
|
-
}}
|
|
183
|
-
/>
|
|
184
|
-
</Box>
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Custom model name input — v3.0.1 NOW has a real TextInput
|
|
189
|
-
if (step === 'custom_model') {
|
|
190
|
-
return (
|
|
191
|
-
<Box flexDirection="column" paddingLeft={1}>
|
|
192
|
-
<Text color="#06B6D4" bold>输入模型名称:</Text>
|
|
193
|
-
<Text color="#F59E0B">(请直接输入模型名并回车,ESC 取消)</Text>
|
|
194
|
-
<Box marginTop={1}>
|
|
195
|
-
<Text color="#06B6D4">❯ </Text>
|
|
196
|
-
<TextInput
|
|
197
|
-
value={selectedModel}
|
|
198
|
-
onChange={setSelectedModel}
|
|
199
|
-
onSubmit={(value) => {
|
|
200
|
-
if (value.trim()) {
|
|
201
|
-
setSelectedModel(value.trim());
|
|
202
|
-
setStep('cache_strategy');
|
|
203
|
-
}
|
|
204
|
-
}}
|
|
205
|
-
/>
|
|
206
|
-
</Box>
|
|
207
|
-
</Box>
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// v3.0.4: prompt-cache strategy step.
|
|
212
|
-
// Default is 1h (long-task TTL): we cannot predict how long a task
|
|
213
|
-
// will run, so we pick the TTL that cannot expire mid-task. 5m is
|
|
214
|
-
// for users who are sure the session is short.
|
|
215
|
-
if (step === 'cache_strategy') {
|
|
216
|
-
const items = [
|
|
217
|
-
{ label: '🕐 1 小时(推荐:默认长任务,缓存不中途失效)', value: '1h' },
|
|
218
|
-
{ label: '⏱ 5 分钟(只适合你确认是短会话)', value: '5m' },
|
|
219
|
-
{ label: '✗ 关闭(每次请求都重新计算)', value: 'off' },
|
|
220
|
-
];
|
|
221
|
-
return (
|
|
222
|
-
<Box flexDirection="column" paddingLeft={1}>
|
|
223
|
-
<Text color="#06B6D4" bold>Prompt Caching 策略:</Text>
|
|
224
|
-
<Text dimColor>对 Anthropic / DeepSeek / Gemini 有效。</Text>
|
|
225
|
-
<Text dimColor>推荐 1 小时:任务开始前无法预知长度,5 分钟 TTL 会在长任务中途过期。</Text>
|
|
226
|
-
<SelectInput
|
|
227
|
-
items={items}
|
|
228
|
-
onSelect={(item) => {
|
|
229
|
-
if (item.value === 'off') {
|
|
230
|
-
setCacheChoice({ enabled: false, ttl: '1h' });
|
|
231
|
-
} else {
|
|
232
|
-
setCacheChoice({ enabled: true, ttl: item.value as '5m' | '1h' });
|
|
233
|
-
}
|
|
234
|
-
saveConfig(selectedProvider, selectedModel, apiKey, customUrl || undefined);
|
|
235
|
-
onComplete(selectedProvider, selectedModel, apiKey, customUrl || undefined);
|
|
236
|
-
}}
|
|
237
|
-
/>
|
|
238
|
-
</Box>
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Export steps for external handling
|
|
246
|
-
export type { InitStep };
|
|
247
|
-
export { isCustomProvider };
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React, { useMemo } from 'react';
|
|
3
|
-
import { Text } from 'ink';
|
|
4
|
-
import { marked } from 'marked';
|
|
5
|
-
import TerminalRenderer from 'marked-terminal';
|
|
6
|
-
|
|
7
|
-
// Configure marked to use terminal renderer
|
|
8
|
-
marked.setOptions({
|
|
9
|
-
// @ts-ignore - marked-terminal types are incomplete
|
|
10
|
-
renderer: new TerminalRenderer({
|
|
11
|
-
showSectionPrefix: false,
|
|
12
|
-
tab: 2,
|
|
13
|
-
}),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
interface Props {
|
|
17
|
-
content: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function Markdown({ content }: Props) {
|
|
21
|
-
const rendered = useMemo(() => {
|
|
22
|
-
try {
|
|
23
|
-
const result = marked.parse(content);
|
|
24
|
-
// marked.parse returns string | Promise<string> depending on async option
|
|
25
|
-
if (typeof result === 'string') {
|
|
26
|
-
return result.trimEnd();
|
|
27
|
-
}
|
|
28
|
-
return content;
|
|
29
|
-
} catch {
|
|
30
|
-
return content;
|
|
31
|
-
}
|
|
32
|
-
}, [content]);
|
|
33
|
-
|
|
34
|
-
return <Text>{rendered}</Text>;
|
|
35
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React, { useState, useEffect } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import SelectInput from 'ink-select-input';
|
|
5
|
-
import { readFileSync, existsSync } from 'fs';
|
|
6
|
-
import { join } from 'path';
|
|
7
|
-
import { homedir } from 'os';
|
|
8
|
-
import { PROVIDERS, getModelsForProvider } from '../../config/providers.js';
|
|
9
|
-
import type { ProviderName } from '../../config/types.js';
|
|
10
|
-
|
|
11
|
-
interface Props {
|
|
12
|
-
currentModel: string;
|
|
13
|
-
currentProvider?: ProviderName;
|
|
14
|
-
onSelect: (model: string) => void;
|
|
15
|
-
onAddNew: () => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface SavedModel {
|
|
19
|
-
label: string;
|
|
20
|
-
value: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function loadSavedModels(currentProvider?: ProviderName): SavedModel[] {
|
|
24
|
-
const configPath = join(homedir(), '.thatgfsj', 'config.json');
|
|
25
|
-
const models: SavedModel[] = [];
|
|
26
|
-
const seen = new Set<string>();
|
|
27
|
-
|
|
28
|
-
// 1. Load from history if exists
|
|
29
|
-
const historyPath = join(homedir(), '.thatgfsj', 'models.json');
|
|
30
|
-
if (existsSync(historyPath)) {
|
|
31
|
-
try {
|
|
32
|
-
const history = JSON.parse(readFileSync(historyPath, 'utf-8'));
|
|
33
|
-
for (const m of history) {
|
|
34
|
-
if (!seen.has(m)) {
|
|
35
|
-
seen.add(m);
|
|
36
|
-
models.push({ label: m, value: m });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
} catch {}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 2. Always include current model
|
|
43
|
-
if (existsSync(configPath)) {
|
|
44
|
-
try {
|
|
45
|
-
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
46
|
-
if (config.model && !seen.has(config.model)) {
|
|
47
|
-
seen.add(config.model);
|
|
48
|
-
models.push({ label: `${config.model} (当前)`, value: config.model });
|
|
49
|
-
}
|
|
50
|
-
} catch {}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 3. v3.0.0 bug fix: ALWAYS include the current provider's catalog of
|
|
54
|
-
// models, otherwise users on a custom relay (mimo / one-api / etc.) who
|
|
55
|
-
// only have the current model in history see a list of 1 item and can't
|
|
56
|
-
// switch to deepseek / gpt-4o / etc. without going through init wizard.
|
|
57
|
-
// This is the model_selector "选 deepseek 然后退出" bug: the list only
|
|
58
|
-
// contained the current model, so the user pressed Enter on the wrong
|
|
59
|
-
// item (or thought the list was broken), then Ctrl+C'd out.
|
|
60
|
-
if (currentProvider && PROVIDERS[currentProvider]) {
|
|
61
|
-
const catalog = getModelsForProvider(currentProvider);
|
|
62
|
-
for (const m of catalog) {
|
|
63
|
-
if (!seen.has(m.id)) {
|
|
64
|
-
seen.add(m.id);
|
|
65
|
-
models.push({ label: m.id, value: m.id });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 4. Universal fallbacks if user is on no history and no catalog
|
|
71
|
-
if (models.length === 0) {
|
|
72
|
-
const defaults = ['deepseek-chat', 'gpt-4o', 'mimo-v2.5-pro'];
|
|
73
|
-
for (const m of defaults) {
|
|
74
|
-
models.push({ label: m, value: m });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return models;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function ModelSelector({ currentModel, currentProvider, onSelect, onAddNew }: Props) {
|
|
82
|
-
const [items, setItems] = useState<SavedModel[]>([]);
|
|
83
|
-
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
const saved = loadSavedModels(currentProvider);
|
|
86
|
-
// Add "add new" option at the end
|
|
87
|
-
saved.push({ label: '+ 添加新模型', value: '__add_new__' });
|
|
88
|
-
setItems(saved);
|
|
89
|
-
}, [currentProvider]);
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<Box flexDirection="column" paddingLeft={1}>
|
|
93
|
-
<Text color="#06B6D4" bold>当前模型: {currentModel}</Text>
|
|
94
|
-
<Text dimColor>选择模型 (↑↓ 回车):</Text>
|
|
95
|
-
<SelectInput
|
|
96
|
-
items={items}
|
|
97
|
-
onSelect={(item) => {
|
|
98
|
-
if (item.value === '__add_new__') {
|
|
99
|
-
onAddNew();
|
|
100
|
-
} else {
|
|
101
|
-
onSelect(item.value);
|
|
102
|
-
}
|
|
103
|
-
}}
|
|
104
|
-
/>
|
|
105
|
-
</Box>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
messageCount: number;
|
|
7
|
-
skills: string[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const StatusBar = React.memo(function StatusBar({ messageCount, skills }: Props) {
|
|
11
|
-
const activeSkills = skills.slice(0, 3).join(', ');
|
|
12
|
-
const moreSkills = skills.length > 3 ? ` +${skills.length - 3}` : '';
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<Box flexDirection="column" marginTop={0}>
|
|
16
|
-
<Text color="#374151">{'─'.repeat(80)}</Text>
|
|
17
|
-
<Box justifyContent="space-between" width="100%">
|
|
18
|
-
<Box>
|
|
19
|
-
<Text backgroundColor="#06B6D4" color="#0F172A" bold> ⚡ THATGFSJ CODE </Text>
|
|
20
|
-
<Text dimColor> │ {messageCount} 条消息</Text>
|
|
21
|
-
</Box>
|
|
22
|
-
<Box>
|
|
23
|
-
{skills.length > 0 && (
|
|
24
|
-
<Text dimColor> 技能: {activeSkills}{moreSkills} </Text>
|
|
25
|
-
)}
|
|
26
|
-
</Box>
|
|
27
|
-
</Box>
|
|
28
|
-
</Box>
|
|
29
|
-
);
|
|
30
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
active?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function Thinking({ active = false }: Props) {
|
|
10
|
-
if (!active) return null;
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<Box paddingLeft={1}>
|
|
14
|
-
<Text color="#06B6D4">⟳ Thinking...</Text>
|
|
15
|
-
</Box>
|
|
16
|
-
);
|
|
17
|
-
}
|