thatgfsj-code 1.0.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/DEVELOPMENT.md +286 -0
  3. package/dist/app/index.d.ts +40 -3
  4. package/dist/app/index.d.ts.map +1 -1
  5. package/dist/app/index.js +87 -12
  6. package/dist/app/index.js.map +1 -1
  7. package/dist/cache/fingerprint.d.ts +49 -0
  8. package/dist/cache/fingerprint.d.ts.map +1 -0
  9. package/dist/cache/fingerprint.js +100 -0
  10. package/dist/cache/fingerprint.js.map +1 -0
  11. package/dist/cache/index.d.ts +15 -0
  12. package/dist/cache/index.d.ts.map +1 -0
  13. package/dist/cache/index.js +15 -0
  14. package/dist/cache/index.js.map +1 -0
  15. package/dist/cache/smartModel.d.ts +40 -0
  16. package/dist/cache/smartModel.d.ts.map +1 -0
  17. package/dist/cache/smartModel.js +43 -0
  18. package/dist/cache/smartModel.js.map +1 -0
  19. package/dist/cache/stats.d.ts +93 -0
  20. package/dist/cache/stats.d.ts.map +1 -0
  21. package/dist/cache/stats.js +155 -0
  22. package/dist/cache/stats.js.map +1 -0
  23. package/dist/cache/volatile.d.ts +32 -0
  24. package/dist/cache/volatile.d.ts.map +1 -0
  25. package/dist/cache/volatile.js +44 -0
  26. package/dist/cache/volatile.js.map +1 -0
  27. package/dist/cmd/index.d.ts +21 -0
  28. package/dist/cmd/index.d.ts.map +1 -1
  29. package/dist/cmd/index.js +155 -31
  30. package/dist/cmd/index.js.map +1 -1
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/index.js +8 -0
  33. package/dist/config/index.js.map +1 -1
  34. package/dist/config/types.d.ts +14 -0
  35. package/dist/config/types.d.ts.map +1 -1
  36. package/dist/llm/anthropic.d.ts +55 -5
  37. package/dist/llm/anthropic.d.ts.map +1 -1
  38. package/dist/llm/anthropic.js +172 -26
  39. package/dist/llm/anthropic.js.map +1 -1
  40. package/dist/llm/index.d.ts +26 -8
  41. package/dist/llm/index.d.ts.map +1 -1
  42. package/dist/llm/index.js +83 -15
  43. package/dist/llm/index.js.map +1 -1
  44. package/dist/llm/openai.d.ts +28 -8
  45. package/dist/llm/openai.d.ts.map +1 -1
  46. package/dist/llm/openai.js +58 -8
  47. package/dist/llm/openai.js.map +1 -1
  48. package/dist/llm/provider.d.ts +40 -8
  49. package/dist/llm/provider.d.ts.map +1 -1
  50. package/dist/llm/provider.js +8 -1
  51. package/dist/llm/provider.js.map +1 -1
  52. package/dist/prompts/index.d.ts +32 -0
  53. package/dist/prompts/index.d.ts.map +1 -1
  54. package/dist/prompts/index.js +33 -11
  55. package/dist/prompts/index.js.map +1 -1
  56. package/dist/session/index.d.ts +43 -3
  57. package/dist/session/index.d.ts.map +1 -1
  58. package/dist/session/index.js +114 -17
  59. package/dist/session/index.js.map +1 -1
  60. package/dist/skills/brainstorming.js +33 -33
  61. package/dist/skills/code-review.js +35 -35
  62. package/dist/skills/executing-plans.js +18 -18
  63. package/dist/skills/frontend-design.js +25 -25
  64. package/dist/skills/git-workflow.js +26 -26
  65. package/dist/skills/improve-architecture.js +28 -28
  66. package/dist/skills/neuroweave.js +37 -37
  67. package/dist/skills/playwright.js +62 -62
  68. package/dist/skills/prototype.js +20 -20
  69. package/dist/skills/subagent.js +19 -19
  70. package/dist/skills/supabase.js +34 -34
  71. package/dist/skills/systematic-debugging.js +34 -34
  72. package/dist/skills/tdd.js +29 -29
  73. package/dist/skills/triage.js +25 -25
  74. package/dist/skills/verification.js +22 -22
  75. package/dist/skills/writing-plans.js +32 -32
  76. package/dist/tools/nwt.js +16 -16
  77. package/dist/tui/app.d.ts.map +1 -1
  78. package/dist/tui/app.js +9 -2
  79. package/dist/tui/app.js.map +1 -1
  80. package/dist/tui/components/ChatList.d.ts.map +1 -1
  81. package/dist/tui/components/ChatList.js +4 -3
  82. package/dist/tui/components/ChatList.js.map +1 -1
  83. package/dist/tui/components/Header.d.ts +8 -0
  84. package/dist/tui/components/Header.d.ts.map +1 -1
  85. package/dist/tui/components/Header.js +17 -3
  86. package/dist/tui/components/Header.js.map +1 -1
  87. package/dist/tui/components/InitWizard.d.ts +12 -1
  88. package/dist/tui/components/InitWizard.d.ts.map +1 -1
  89. package/dist/tui/components/InitWizard.js +58 -3
  90. package/dist/tui/components/InitWizard.js.map +1 -1
  91. package/dist/tui/components/ToolCall.d.ts +1 -1
  92. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  93. package/dist/tui/components/ToolCall.js +4 -4
  94. package/dist/tui/components/ToolCall.js.map +1 -1
  95. package/dist/tui/hooks/useChat.d.ts +22 -0
  96. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  97. package/dist/tui/hooks/useChat.js +127 -50
  98. package/dist/tui/hooks/useChat.js.map +1 -1
  99. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  100. package/dist/tui/hooks/useCommands.js +78 -0
  101. package/dist/tui/hooks/useCommands.js.map +1 -1
  102. package/dist/types.d.ts +79 -6
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/types.js +7 -0
  105. package/dist/types.js.map +1 -1
  106. package/dist/utils/stableStringify.d.ts +21 -0
  107. package/dist/utils/stableStringify.d.ts.map +1 -0
  108. package/dist/utils/stableStringify.js +53 -0
  109. package/dist/utils/stableStringify.js.map +1 -0
  110. package/dist/utils/thinking.d.ts +59 -0
  111. package/dist/utils/thinking.d.ts.map +1 -0
  112. package/dist/utils/thinking.js +104 -0
  113. package/dist/utils/thinking.js.map +1 -0
  114. package/install.bat +63 -0
  115. package/install.ps1 +238 -0
  116. package/install.sh +113 -0
  117. package/package.json +7 -4
  118. package/src/app/index.ts +180 -108
  119. package/src/cache/fingerprint.ts +101 -0
  120. package/src/cache/index.ts +15 -0
  121. package/src/cache/smartModel.ts +52 -0
  122. package/src/cache/stats.ts +199 -0
  123. package/src/cache/volatile.ts +47 -0
  124. package/src/cmd/index.tsx +288 -160
  125. package/src/config/index.ts +156 -148
  126. package/src/config/providers.ts +234 -234
  127. package/src/config/types.ts +67 -53
  128. package/src/hooks/index.ts +111 -111
  129. package/src/llm/anthropic.ts +388 -243
  130. package/src/llm/gemini.ts +169 -169
  131. package/src/llm/index.ts +265 -200
  132. package/src/llm/openai.ts +243 -196
  133. package/src/llm/provider.ts +66 -34
  134. package/src/prompts/index.ts +260 -220
  135. package/src/session/compactor.ts +103 -103
  136. package/src/session/index.ts +181 -87
  137. package/src/session/message.ts +42 -42
  138. package/src/skills/brainstorming.ts +43 -43
  139. package/src/skills/code-review.ts +45 -45
  140. package/src/skills/executing-plans.ts +27 -27
  141. package/src/skills/frontend-design.ts +35 -35
  142. package/src/skills/git-workflow.ts +36 -36
  143. package/src/skills/improve-architecture.ts +38 -38
  144. package/src/skills/index.ts +136 -136
  145. package/src/skills/neuroweave.ts +47 -47
  146. package/src/skills/playwright.ts +72 -72
  147. package/src/skills/prototype.ts +30 -30
  148. package/src/skills/subagent.ts +28 -28
  149. package/src/skills/supabase.ts +44 -44
  150. package/src/skills/systematic-debugging.ts +44 -44
  151. package/src/skills/tdd.ts +39 -39
  152. package/src/skills/triage.ts +35 -35
  153. package/src/skills/verification.ts +31 -31
  154. package/src/skills/writing-plans.ts +42 -42
  155. package/src/tools/nwt.ts +598 -598
  156. package/src/tools/types.ts +122 -122
  157. package/src/tui/app.tsx +199 -186
  158. package/src/tui/components/ChatList.tsx +41 -32
  159. package/src/tui/components/ChatMessage.tsx +54 -54
  160. package/src/tui/components/Header.tsx +56 -28
  161. package/src/tui/components/InitWizard.tsx +217 -132
  162. package/src/tui/components/Markdown.tsx +35 -35
  163. package/src/tui/components/ModelSelector.tsx +87 -87
  164. package/src/tui/components/StatusBar.tsx +30 -30
  165. package/src/tui/components/Thinking.tsx +17 -17
  166. package/src/tui/components/ToolCall.tsx +102 -71
  167. package/src/tui/components/UserInput.tsx +131 -131
  168. package/src/tui/hooks/useChat.ts +287 -194
  169. package/src/tui/hooks/useCommands.ts +234 -154
  170. package/src/tui/index.ts +6 -6
  171. package/src/tui/welcome.ts +177 -177
  172. package/src/types.ts +104 -42
  173. package/src/utils/diff.ts +71 -71
  174. package/src/utils/project.ts +99 -99
  175. package/src/utils/stableStringify.ts +47 -0
  176. package/src/utils/thinking.ts +119 -0
  177. package/tests/cache/fingerprint.test.ts +75 -0
  178. package/tests/cache/stableStringify.test.ts +43 -0
  179. package/tests/cache/stats.test.ts +146 -0
  180. package/tests/cache/volatile.test.ts +75 -0
  181. package/tests/smoke-pollution.mjs +78 -0
  182. package/tests/smoke-thinking.mjs +110 -0
  183. package/tests/smoke-tool-stream.mjs +69 -0
  184. package/tests/smoke-tool.mjs +117 -0
@@ -1,122 +1,122 @@
1
- /**
2
- * Tool interface definitions
3
- * Extracted from old src/core/types.ts
4
- */
5
-
6
- // ==================== Tool Parameter & Schema ====================
7
-
8
- export interface ToolParameter {
9
- name: string;
10
- type: string;
11
- description: string;
12
- required: boolean;
13
- default?: any;
14
- enum?: string[];
15
- }
16
-
17
- export interface ToolInputSchema {
18
- type: 'object';
19
- properties: Record<string, {
20
- type: string;
21
- description: string;
22
- default?: any;
23
- enum?: string[];
24
- }>;
25
- required?: string[];
26
- }
27
-
28
- export interface ToolMetadata {
29
- permissions?: ('read' | 'write' | 'execute' | 'network')[];
30
- maxDuration?: number;
31
- tags?: string[];
32
- version?: string;
33
- deprecated?: string;
34
- }
35
-
36
- // ==================== Tool Interface ====================
37
-
38
- export interface ToolContext {
39
- sessionId?: string;
40
- workingDirectory?: string;
41
- confirmAction?: (msg: string) => Promise<boolean>;
42
- signal?: AbortSignal;
43
- toolCallId?: string;
44
- }
45
-
46
- export interface ToolResult {
47
- success: boolean;
48
- output?: string;
49
- error?: string;
50
- data?: any;
51
- }
52
-
53
- export interface Tool {
54
- name: string;
55
- description: string;
56
- parameters: ToolParameter[];
57
- inputSchema?: ToolInputSchema;
58
- metadata?: ToolMetadata;
59
- execute(params: Record<string, any>, ctx?: ToolContext): Promise<ToolResult>;
60
- }
61
-
62
- // ==================== Tool Builder ====================
63
-
64
- export class ToolBuilder {
65
- private _name = '';
66
- private _description = '';
67
- private _parameters: ToolParameter[] = [];
68
- private _inputSchema: ToolInputSchema = { type: 'object', properties: {} };
69
- private _metadata: ToolMetadata = {};
70
- private _fn?: (params: Record<string, any>, ctx: ToolContext) => Promise<ToolResult>;
71
-
72
- name(n: string): this { this._name = n; return this; }
73
- description(d: string): this { this._description = d; return this; }
74
-
75
- param(p: ToolParameter): this {
76
- this._parameters.push(p);
77
- this._inputSchema.properties[p.name] = {
78
- type: p.type,
79
- description: p.description,
80
- default: p.default,
81
- enum: p.enum,
82
- };
83
- return this;
84
- }
85
-
86
- stringParam(name: string, description: string, required = true, defaultVal?: string): this {
87
- return this.param({ name, type: 'string', description, required, default: defaultVal });
88
- }
89
-
90
- booleanParam(name: string, description: string, required = false, defaultVal?: boolean): this {
91
- return this.param({ name, type: 'boolean', description, required, default: defaultVal });
92
- }
93
-
94
- permissions(...perms: ('read' | 'write' | 'execute' | 'network')[]): this {
95
- this._metadata.permissions = perms;
96
- return this;
97
- }
98
-
99
- tags(...t: string[]): this {
100
- this._metadata.tags = t;
101
- return this;
102
- }
103
-
104
- handle(fn: (params: Record<string, any>, ctx: ToolContext) => Promise<ToolResult>): this {
105
- this._fn = fn;
106
- return this;
107
- }
108
-
109
- build(): Tool {
110
- if (!this._name || !this._fn) {
111
- throw new Error('Tool name and handler are required');
112
- }
113
- return {
114
- name: this._name,
115
- description: this._description,
116
- parameters: this._parameters,
117
- inputSchema: this._inputSchema,
118
- metadata: this._metadata,
119
- execute: this._fn,
120
- };
121
- }
122
- }
1
+ /**
2
+ * Tool interface definitions
3
+ * Extracted from old src/core/types.ts
4
+ */
5
+
6
+ // ==================== Tool Parameter & Schema ====================
7
+
8
+ export interface ToolParameter {
9
+ name: string;
10
+ type: string;
11
+ description: string;
12
+ required: boolean;
13
+ default?: any;
14
+ enum?: string[];
15
+ }
16
+
17
+ export interface ToolInputSchema {
18
+ type: 'object';
19
+ properties: Record<string, {
20
+ type: string;
21
+ description: string;
22
+ default?: any;
23
+ enum?: string[];
24
+ }>;
25
+ required?: string[];
26
+ }
27
+
28
+ export interface ToolMetadata {
29
+ permissions?: ('read' | 'write' | 'execute' | 'network')[];
30
+ maxDuration?: number;
31
+ tags?: string[];
32
+ version?: string;
33
+ deprecated?: string;
34
+ }
35
+
36
+ // ==================== Tool Interface ====================
37
+
38
+ export interface ToolContext {
39
+ sessionId?: string;
40
+ workingDirectory?: string;
41
+ confirmAction?: (msg: string) => Promise<boolean>;
42
+ signal?: AbortSignal;
43
+ toolCallId?: string;
44
+ }
45
+
46
+ export interface ToolResult {
47
+ success: boolean;
48
+ output?: string;
49
+ error?: string;
50
+ data?: any;
51
+ }
52
+
53
+ export interface Tool {
54
+ name: string;
55
+ description: string;
56
+ parameters: ToolParameter[];
57
+ inputSchema?: ToolInputSchema;
58
+ metadata?: ToolMetadata;
59
+ execute(params: Record<string, any>, ctx?: ToolContext): Promise<ToolResult>;
60
+ }
61
+
62
+ // ==================== Tool Builder ====================
63
+
64
+ export class ToolBuilder {
65
+ private _name = '';
66
+ private _description = '';
67
+ private _parameters: ToolParameter[] = [];
68
+ private _inputSchema: ToolInputSchema = { type: 'object', properties: {} };
69
+ private _metadata: ToolMetadata = {};
70
+ private _fn?: (params: Record<string, any>, ctx: ToolContext) => Promise<ToolResult>;
71
+
72
+ name(n: string): this { this._name = n; return this; }
73
+ description(d: string): this { this._description = d; return this; }
74
+
75
+ param(p: ToolParameter): this {
76
+ this._parameters.push(p);
77
+ this._inputSchema.properties[p.name] = {
78
+ type: p.type,
79
+ description: p.description,
80
+ default: p.default,
81
+ enum: p.enum,
82
+ };
83
+ return this;
84
+ }
85
+
86
+ stringParam(name: string, description: string, required = true, defaultVal?: string): this {
87
+ return this.param({ name, type: 'string', description, required, default: defaultVal });
88
+ }
89
+
90
+ booleanParam(name: string, description: string, required = false, defaultVal?: boolean): this {
91
+ return this.param({ name, type: 'boolean', description, required, default: defaultVal });
92
+ }
93
+
94
+ permissions(...perms: ('read' | 'write' | 'execute' | 'network')[]): this {
95
+ this._metadata.permissions = perms;
96
+ return this;
97
+ }
98
+
99
+ tags(...t: string[]): this {
100
+ this._metadata.tags = t;
101
+ return this;
102
+ }
103
+
104
+ handle(fn: (params: Record<string, any>, ctx: ToolContext) => Promise<ToolResult>): this {
105
+ this._fn = fn;
106
+ return this;
107
+ }
108
+
109
+ build(): Tool {
110
+ if (!this._name || !this._fn) {
111
+ throw new Error('Tool name and handler are required');
112
+ }
113
+ return {
114
+ name: this._name,
115
+ description: this._description,
116
+ parameters: this._parameters,
117
+ inputSchema: this._inputSchema,
118
+ metadata: this._metadata,
119
+ execute: this._fn,
120
+ };
121
+ }
122
+ }
package/src/tui/app.tsx CHANGED
@@ -1,186 +1,199 @@
1
- /** @jsxImportSource react */
2
- import React, { useState, useCallback } from 'react';
3
- import { Box, Text, useStdout } from 'ink';
4
- import { Header } from './components/Header.js';
5
- import { ChatList } from './components/ChatList.js';
6
- import { Thinking } from './components/Thinking.js';
7
- import { UserInput } from './components/UserInput.js';
8
- import { StatusBar } from './components/StatusBar.js';
9
- import { ModelSelector } from './components/ModelSelector.js';
10
- import { InitWizard } from './components/InitWizard.js';
11
- import { useChat } from './hooks/useChat.js';
12
- import { useCommands } from './hooks/useCommands.js';
13
- import type { App } from '../app/index.js';
14
- import type { MessageData } from './components/ChatMessage.js';
15
- import type { ProviderName } from '../config/types.js';
16
- import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
17
- import { join } from 'path';
18
- import { homedir } from 'os';
19
-
20
- interface Props {
21
- app: App;
22
- }
23
-
24
- function saveModelToHistory(model: string) {
25
- const dir = join(homedir(), '.thatgfsj');
26
- const path = join(dir, 'models.json');
27
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
28
- let history: string[] = [];
29
- if (existsSync(path)) {
30
- try { history = JSON.parse(readFileSync(path, 'utf-8')); } catch {}
31
- }
32
- if (!history.includes(model)) {
33
- history.push(model);
34
- writeFileSync(path, JSON.stringify(history, null, 2));
35
- }
36
- }
37
-
38
- type ViewMode = 'chat' | 'model_select' | 'init_wizard' | 'init_key' | 'init_url' | 'init_custom_model';
39
-
40
- export function TuiApp({ app }: Props) {
41
- const { messages, isThinking, streaming, streamingToolCalls, queuedMessage, sendMessage, cancel } = useChat(app);
42
- const { handleCommand } = useCommands(app);
43
- const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
44
- const [viewMode, setViewMode] = useState<ViewMode>('chat');
45
- const [initProvider, setInitProvider] = useState<ProviderName>('siliconflow');
46
- const [initUrl, setInitUrl] = useState('');
47
- const { stdout } = useStdout();
48
- const terminalWidth = stdout?.columns || 80;
49
-
50
- const addMsg = useCallback((content: string) => {
51
- setSystemMessages(prev => [...prev, { role: 'assistant', content }]);
52
- }, []);
53
-
54
- const onSubmit = useCallback(async (input: string) => {
55
- // Init wizard - API Key input
56
- if (viewMode === 'init_key') {
57
- const key = input.trim();
58
- if (!key) { setViewMode('chat'); return; }
59
- // Save and complete
60
- const { App: AppClass } = await import('../app/index.js');
61
- const cfg = app.config.get();
62
- app.config.save({ provider: initProvider, apiKey: key, model: cfg.model || 'default' });
63
- saveModelToHistory(cfg.model || 'default');
64
- setViewMode('chat');
65
- addMsg(`配置已更新: ${initProvider}`);
66
- return;
67
- }
68
-
69
- // Init wizard - Custom URL input
70
- if (viewMode === 'init_url') {
71
- const url = input.trim();
72
- if (!url) { setViewMode('chat'); return; }
73
- setInitUrl(url);
74
- setViewMode('init_key');
75
- return;
76
- }
77
-
78
- // Init wizard - Custom model name
79
- if (viewMode === 'init_custom_model') {
80
- const model = input.trim();
81
- if (model) {
82
- app.config.save({ model });
83
- saveModelToHistory(model);
84
- addMsg(`模型已切换: ${model}`);
85
- }
86
- setViewMode('chat');
87
- return;
88
- }
89
-
90
- // Model selector - ignore text input
91
- if (viewMode === 'model_select') return;
92
-
93
- // Normal command handling
94
- const result = handleCommand(input);
95
-
96
- if (result.handled) {
97
- if (input.trim() === '/模型' || input.trim() === '/model') {
98
- setViewMode('model_select');
99
- return;
100
- }
101
-
102
- if (result.output) {
103
- setSystemMessages(prev => [
104
- ...prev,
105
- { role: 'user', content: input },
106
- { role: 'assistant', content: result.output! },
107
- ]);
108
- }
109
-
110
- if (result.action === 'clear') setSystemMessages([]);
111
-
112
- if (result.action === 'reinit') {
113
- setViewMode('init_wizard');
114
- }
115
-
116
- return;
117
- }
118
-
119
- sendMessage(input);
120
- }, [handleCommand, sendMessage, app, viewMode, initProvider, initUrl, addMsg]);
121
-
122
- const allMessages = [...systemMessages, ...messages];
123
- const activeSkills = app.skills.listActive().map(s => s.id);
124
-
125
- return (
126
- <Box flexDirection="column" paddingX={1}>
127
- <Header provider={app.config.get().provider} model={app.config.get().model} />
128
- <ChatList
129
- messages={allMessages}
130
- streaming={streaming}
131
- streamingToolCalls={streamingToolCalls}
132
- width={terminalWidth - 4}
133
- />
134
- <Thinking active={isThinking} />
135
- {queuedMessage && (
136
- <Box paddingLeft={1}>
137
- <Text color="#F59E0B">📎 已排队: </Text>
138
- <Text color="#94A3B8">{queuedMessage}</Text>
139
- </Box>
140
- )}
141
- {viewMode === 'model_select' ? (
142
- <ModelSelector
143
- currentModel={app.config.get().model}
144
- onSelect={(model) => {
145
- app.config.save({ model });
146
- saveModelToHistory(model);
147
- setViewMode('chat');
148
- addMsg(`模型已切换: ${model}`);
149
- }}
150
- onAddNew={() => setViewMode('init_wizard')}
151
- />
152
- ) : viewMode === 'init_wizard' ? (
153
- <InitWizard
154
- onComplete={(provider, model, apiKey, baseUrl) => {
155
- app.config.save({ provider, model, apiKey, baseUrl });
156
- saveModelToHistory(model);
157
- setViewMode('chat');
158
- addMsg(`配置完成: ${provider} / ${model}`);
159
- }}
160
- onCancel={() => setViewMode('chat')}
161
- />
162
- ) : (
163
- <Box flexDirection="column">
164
- {viewMode === 'init_key' && (
165
- <Box paddingLeft={1} marginBottom={0}>
166
- <Text color="#06B6D4" bold>输入 API Key: </Text>
167
- <Text dimColor>({initProvider})</Text>
168
- </Box>
169
- )}
170
- {viewMode === 'init_url' && (
171
- <Box paddingLeft={1} marginBottom={0}>
172
- <Text color="#06B6D4" bold>输入中转站 URL: </Text>
173
- </Box>
174
- )}
175
- {viewMode === 'init_custom_model' && (
176
- <Box paddingLeft={1} marginBottom={0}>
177
- <Text color="#06B6D4" bold>输入模型名称: </Text>
178
- </Box>
179
- )}
180
- <UserInput onSubmit={onSubmit} onCancel={cancel} disabled={false} />
181
- </Box>
182
- )}
183
- <StatusBar messageCount={allMessages.length} skills={activeSkills} />
184
- </Box>
185
- );
186
- }
1
+ /** @jsxImportSource react */
2
+ import React, { useState, useCallback, useEffect } from 'react';
3
+ import { Box, Text, useStdout } from 'ink';
4
+ import { Header } from './components/Header.js';
5
+ import { ChatList } from './components/ChatList.js';
6
+ import { Thinking } from './components/Thinking.js';
7
+ import { UserInput } from './components/UserInput.js';
8
+ import { StatusBar } from './components/StatusBar.js';
9
+ import { ModelSelector } from './components/ModelSelector.js';
10
+ import { InitWizard } from './components/InitWizard.js';
11
+ import { useChat } from './hooks/useChat.js';
12
+ import { useCommands } from './hooks/useCommands.js';
13
+ import type { App } from '../app/index.js';
14
+ import type { MessageData } from './components/ChatMessage.js';
15
+ import type { ProviderName } from '../config/types.js';
16
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
17
+ import { join } from 'path';
18
+ import { homedir } from 'os';
19
+
20
+ interface Props {
21
+ app: App;
22
+ }
23
+
24
+ function saveModelToHistory(model: string) {
25
+ const dir = join(homedir(), '.thatgfsj');
26
+ const path = join(dir, 'models.json');
27
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
28
+ let history: string[] = [];
29
+ if (existsSync(path)) {
30
+ try { history = JSON.parse(readFileSync(path, 'utf-8')); } catch {}
31
+ }
32
+ if (!history.includes(model)) {
33
+ history.push(model);
34
+ writeFileSync(path, JSON.stringify(history, null, 2));
35
+ }
36
+ }
37
+
38
+ type ViewMode = 'chat' | 'model_select' | 'init_wizard' | 'init_key' | 'init_url' | 'init_custom_model';
39
+
40
+ export function TuiApp({ app }: Props) {
41
+ const { messages, isThinking, streaming, streamingToolCalls, queuedMessage, sendMessage, cancel } = useChat(app);
42
+ const { handleCommand } = useCommands(app);
43
+ const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
44
+ const [viewMode, setViewMode] = useState<ViewMode>('chat');
45
+ const [initProvider, setInitProvider] = useState<ProviderName>('siliconflow');
46
+ const [initUrl, setInitUrl] = useState('');
47
+ const { stdout } = useStdout();
48
+ const terminalWidth = stdout?.columns || 80;
49
+
50
+ // v3.0.0: cache stats snapshot for the Header. Re-read whenever messages
51
+ // change (i.e. a round just completed and recorded new usage). Polling on
52
+ // an interval would be wasteful; React re-render is the trigger.
53
+ const [cacheSnapshot, setCacheSnapshot] = useState(() => app.cacheStats.snapshot());
54
+ useEffect(() => {
55
+ setCacheSnapshot(app.cacheStats.snapshot());
56
+ }, [messages.length]);
57
+
58
+ const addMsg = useCallback((content: string) => {
59
+ setSystemMessages(prev => [...prev, { role: 'assistant', content }]);
60
+ }, []);
61
+
62
+ const onSubmit = useCallback(async (input: string) => {
63
+ // Init wizard - API Key input
64
+ if (viewMode === 'init_key') {
65
+ const key = input.trim();
66
+ if (!key) { setViewMode('chat'); return; }
67
+ // Save and complete
68
+ const { App: AppClass } = await import('../app/index.js');
69
+ const cfg = app.config.get();
70
+ app.config.save({ provider: initProvider, apiKey: key, model: cfg.model || 'default' });
71
+ saveModelToHistory(cfg.model || 'default');
72
+ setViewMode('chat');
73
+ addMsg(`配置已更新: ${initProvider}`);
74
+ return;
75
+ }
76
+
77
+ // Init wizard - Custom URL input
78
+ if (viewMode === 'init_url') {
79
+ const url = input.trim();
80
+ if (!url) { setViewMode('chat'); return; }
81
+ setInitUrl(url);
82
+ setViewMode('init_key');
83
+ return;
84
+ }
85
+
86
+ // Init wizard - Custom model name
87
+ if (viewMode === 'init_custom_model') {
88
+ const model = input.trim();
89
+ if (model) {
90
+ app.config.save({ model });
91
+ saveModelToHistory(model);
92
+ addMsg(`模型已切换: ${model}`);
93
+ }
94
+ setViewMode('chat');
95
+ return;
96
+ }
97
+
98
+ // Model selector - ignore text input
99
+ if (viewMode === 'model_select') return;
100
+
101
+ // Normal command handling
102
+ const result = handleCommand(input);
103
+
104
+ if (result.handled) {
105
+ if (input.trim() === '/模型' || input.trim() === '/model') {
106
+ setViewMode('model_select');
107
+ return;
108
+ }
109
+
110
+ if (result.output) {
111
+ setSystemMessages(prev => [
112
+ ...prev,
113
+ { role: 'user', content: input },
114
+ { role: 'assistant', content: result.output! },
115
+ ]);
116
+ }
117
+
118
+ if (result.action === 'clear') setSystemMessages([]);
119
+
120
+ if (result.action === 'reinit') {
121
+ setViewMode('init_wizard');
122
+ }
123
+
124
+ return;
125
+ }
126
+
127
+ sendMessage(input);
128
+ }, [handleCommand, sendMessage, app, viewMode, initProvider, initUrl, addMsg]);
129
+
130
+ const allMessages = [...systemMessages, ...messages];
131
+ const activeSkills = app.skills.listActive().map(s => s.id);
132
+
133
+ return (
134
+ <Box flexDirection="column" paddingX={1}>
135
+ <Header
136
+ provider={app.config.get().provider}
137
+ model={app.config.get().model}
138
+ cacheHitRate={cacheSnapshot.hitRate > 0 ? cacheSnapshot.hitRate : null}
139
+ cacheSavingsCNY={cacheSnapshot.estimatedSavingsCNY}
140
+ />
141
+ <ChatList
142
+ messages={allMessages}
143
+ streaming={streaming}
144
+ streamingToolCalls={streamingToolCalls}
145
+ width={terminalWidth - 4}
146
+ />
147
+ <Thinking active={isThinking} />
148
+ {queuedMessage && (
149
+ <Box paddingLeft={1}>
150
+ <Text color="#F59E0B">📎 已排队: </Text>
151
+ <Text color="#94A3B8">{queuedMessage}</Text>
152
+ </Box>
153
+ )}
154
+ {viewMode === 'model_select' ? (
155
+ <ModelSelector
156
+ currentModel={app.config.get().model}
157
+ onSelect={(model) => {
158
+ app.config.save({ model });
159
+ saveModelToHistory(model);
160
+ setViewMode('chat');
161
+ addMsg(`模型已切换: ${model}`);
162
+ }}
163
+ onAddNew={() => setViewMode('init_wizard')}
164
+ />
165
+ ) : viewMode === 'init_wizard' ? (
166
+ <InitWizard
167
+ onComplete={(provider, model, apiKey, baseUrl) => {
168
+ app.config.save({ provider, model, apiKey, baseUrl });
169
+ saveModelToHistory(model);
170
+ setViewMode('chat');
171
+ addMsg(`配置完成: ${provider} / ${model}`);
172
+ }}
173
+ onCancel={() => setViewMode('chat')}
174
+ />
175
+ ) : (
176
+ <Box flexDirection="column">
177
+ {viewMode === 'init_key' && (
178
+ <Box paddingLeft={1} marginBottom={0}>
179
+ <Text color="#06B6D4" bold>输入 API Key: </Text>
180
+ <Text dimColor>({initProvider})</Text>
181
+ </Box>
182
+ )}
183
+ {viewMode === 'init_url' && (
184
+ <Box paddingLeft={1} marginBottom={0}>
185
+ <Text color="#06B6D4" bold>输入中转站 URL: </Text>
186
+ </Box>
187
+ )}
188
+ {viewMode === 'init_custom_model' && (
189
+ <Box paddingLeft={1} marginBottom={0}>
190
+ <Text color="#06B6D4" bold>输入模型名称: </Text>
191
+ </Box>
192
+ )}
193
+ <UserInput onSubmit={onSubmit} onCancel={cancel} disabled={false} />
194
+ </Box>
195
+ )}
196
+ <StatusBar messageCount={allMessages.length} skills={activeSkills} />
197
+ </Box>
198
+ );
199
+ }