thatgfsj-code 2.2.9 → 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 (182) hide show
  1. package/CHANGELOG.md +15 -67
  2. package/dist/app/index.d.ts +28 -5
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +69 -10
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cache/fingerprint.d.ts +49 -0
  7. package/dist/cache/fingerprint.d.ts.map +1 -0
  8. package/dist/cache/fingerprint.js +100 -0
  9. package/dist/cache/fingerprint.js.map +1 -0
  10. package/dist/cache/index.d.ts +15 -0
  11. package/dist/cache/index.d.ts.map +1 -0
  12. package/dist/cache/index.js +15 -0
  13. package/dist/cache/index.js.map +1 -0
  14. package/dist/cache/smartModel.d.ts +40 -0
  15. package/dist/cache/smartModel.d.ts.map +1 -0
  16. package/dist/cache/smartModel.js +43 -0
  17. package/dist/cache/smartModel.js.map +1 -0
  18. package/dist/cache/stats.d.ts +93 -0
  19. package/dist/cache/stats.d.ts.map +1 -0
  20. package/dist/cache/stats.js +155 -0
  21. package/dist/cache/stats.js.map +1 -0
  22. package/dist/cache/volatile.d.ts +32 -0
  23. package/dist/cache/volatile.d.ts.map +1 -0
  24. package/dist/cache/volatile.js +44 -0
  25. package/dist/cache/volatile.js.map +1 -0
  26. package/dist/cmd/index.js +49 -57
  27. package/dist/cmd/index.js.map +1 -1
  28. package/dist/config/index.d.ts.map +1 -1
  29. package/dist/config/index.js +8 -0
  30. package/dist/config/index.js.map +1 -1
  31. package/dist/config/types.d.ts +14 -0
  32. package/dist/config/types.d.ts.map +1 -1
  33. package/dist/llm/anthropic.d.ts +55 -5
  34. package/dist/llm/anthropic.d.ts.map +1 -1
  35. package/dist/llm/anthropic.js +172 -26
  36. package/dist/llm/anthropic.js.map +1 -1
  37. package/dist/llm/index.d.ts +26 -8
  38. package/dist/llm/index.d.ts.map +1 -1
  39. package/dist/llm/index.js +83 -15
  40. package/dist/llm/index.js.map +1 -1
  41. package/dist/llm/openai.d.ts +28 -8
  42. package/dist/llm/openai.d.ts.map +1 -1
  43. package/dist/llm/openai.js +58 -8
  44. package/dist/llm/openai.js.map +1 -1
  45. package/dist/llm/provider.d.ts +40 -8
  46. package/dist/llm/provider.d.ts.map +1 -1
  47. package/dist/llm/provider.js +8 -1
  48. package/dist/llm/provider.js.map +1 -1
  49. package/dist/prompts/index.d.ts +32 -0
  50. package/dist/prompts/index.d.ts.map +1 -1
  51. package/dist/prompts/index.js +33 -11
  52. package/dist/prompts/index.js.map +1 -1
  53. package/dist/session/index.d.ts +32 -3
  54. package/dist/session/index.d.ts.map +1 -1
  55. package/dist/session/index.js +51 -36
  56. package/dist/session/index.js.map +1 -1
  57. package/dist/skills/brainstorming.js +33 -33
  58. package/dist/skills/code-review.js +35 -35
  59. package/dist/skills/executing-plans.js +18 -18
  60. package/dist/skills/frontend-design.js +25 -25
  61. package/dist/skills/git-workflow.js +26 -26
  62. package/dist/skills/improve-architecture.js +28 -28
  63. package/dist/skills/neuroweave.js +37 -37
  64. package/dist/skills/playwright.js +62 -62
  65. package/dist/skills/prototype.js +20 -20
  66. package/dist/skills/subagent.js +19 -19
  67. package/dist/skills/supabase.js +34 -34
  68. package/dist/skills/systematic-debugging.js +34 -34
  69. package/dist/skills/tdd.js +29 -29
  70. package/dist/skills/triage.js +25 -25
  71. package/dist/skills/verification.js +22 -22
  72. package/dist/skills/writing-plans.js +32 -32
  73. package/dist/tools/nwt.js +16 -16
  74. package/dist/tui/app.d.ts.map +1 -1
  75. package/dist/tui/app.js +9 -2
  76. package/dist/tui/app.js.map +1 -1
  77. package/dist/tui/components/Header.d.ts +8 -0
  78. package/dist/tui/components/Header.d.ts.map +1 -1
  79. package/dist/tui/components/Header.js +17 -4
  80. package/dist/tui/components/Header.js.map +1 -1
  81. package/dist/tui/components/InitWizard.d.ts +12 -1
  82. package/dist/tui/components/InitWizard.d.ts.map +1 -1
  83. package/dist/tui/components/InitWizard.js +58 -3
  84. package/dist/tui/components/InitWizard.js.map +1 -1
  85. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  86. package/dist/tui/components/ToolCall.js +1 -6
  87. package/dist/tui/components/ToolCall.js.map +1 -1
  88. package/dist/tui/hooks/useChat.d.ts +22 -0
  89. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  90. package/dist/tui/hooks/useChat.js +82 -46
  91. package/dist/tui/hooks/useChat.js.map +1 -1
  92. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  93. package/dist/tui/hooks/useCommands.js +57 -0
  94. package/dist/tui/hooks/useCommands.js.map +1 -1
  95. package/dist/tui/welcome.d.ts.map +1 -1
  96. package/dist/tui/welcome.js +2 -3
  97. package/dist/tui/welcome.js.map +1 -1
  98. package/dist/types.d.ts +79 -6
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +7 -0
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/stableStringify.d.ts +21 -0
  103. package/dist/utils/stableStringify.d.ts.map +1 -0
  104. package/dist/utils/stableStringify.js +53 -0
  105. package/dist/utils/stableStringify.js.map +1 -0
  106. package/dist/utils/thinking.d.ts.map +1 -1
  107. package/dist/utils/thinking.js +0 -3
  108. package/dist/utils/thinking.js.map +1 -1
  109. package/package.json +6 -6
  110. package/src/app/index.ts +180 -124
  111. package/src/cache/fingerprint.ts +101 -0
  112. package/src/cache/index.ts +15 -0
  113. package/src/cache/smartModel.ts +52 -0
  114. package/src/cache/stats.ts +199 -0
  115. package/src/cache/volatile.ts +47 -0
  116. package/src/cmd/index.tsx +288 -292
  117. package/src/config/index.ts +156 -148
  118. package/src/config/providers.ts +234 -234
  119. package/src/config/types.ts +67 -53
  120. package/src/hooks/index.ts +111 -111
  121. package/src/llm/anthropic.ts +388 -243
  122. package/src/llm/gemini.ts +169 -169
  123. package/src/llm/index.ts +265 -200
  124. package/src/llm/openai.ts +243 -196
  125. package/src/llm/provider.ts +66 -34
  126. package/src/prompts/index.ts +260 -220
  127. package/src/session/compactor.ts +103 -103
  128. package/src/session/index.ts +181 -168
  129. package/src/session/message.ts +42 -42
  130. package/src/skills/brainstorming.ts +43 -43
  131. package/src/skills/code-review.ts +45 -45
  132. package/src/skills/executing-plans.ts +27 -27
  133. package/src/skills/frontend-design.ts +35 -35
  134. package/src/skills/git-workflow.ts +36 -36
  135. package/src/skills/improve-architecture.ts +38 -38
  136. package/src/skills/index.ts +136 -136
  137. package/src/skills/neuroweave.ts +47 -47
  138. package/src/skills/playwright.ts +72 -72
  139. package/src/skills/prototype.ts +30 -30
  140. package/src/skills/subagent.ts +28 -28
  141. package/src/skills/supabase.ts +44 -44
  142. package/src/skills/systematic-debugging.ts +44 -44
  143. package/src/skills/tdd.ts +39 -39
  144. package/src/skills/triage.ts +35 -35
  145. package/src/skills/verification.ts +31 -31
  146. package/src/skills/writing-plans.ts +42 -42
  147. package/src/tools/nwt.ts +598 -598
  148. package/src/tools/types.ts +122 -122
  149. package/src/tui/app.tsx +199 -186
  150. package/src/tui/components/ChatList.tsx +41 -41
  151. package/src/tui/components/ChatMessage.tsx +54 -54
  152. package/src/tui/components/Header.tsx +56 -29
  153. package/src/tui/components/InitWizard.tsx +217 -132
  154. package/src/tui/components/Markdown.tsx +35 -35
  155. package/src/tui/components/ModelSelector.tsx +87 -87
  156. package/src/tui/components/StatusBar.tsx +30 -30
  157. package/src/tui/components/Thinking.tsx +17 -17
  158. package/src/tui/components/ToolCall.tsx +102 -107
  159. package/src/tui/components/UserInput.tsx +131 -131
  160. package/src/tui/hooks/useChat.ts +287 -238
  161. package/src/tui/hooks/useCommands.ts +234 -176
  162. package/src/tui/index.ts +6 -6
  163. package/src/tui/welcome.ts +177 -178
  164. package/src/types.ts +104 -42
  165. package/src/utils/diff.ts +71 -71
  166. package/src/utils/project.ts +99 -99
  167. package/src/utils/stableStringify.ts +47 -0
  168. package/src/utils/thinking.ts +118 -121
  169. package/tests/cache/fingerprint.test.ts +75 -0
  170. package/tests/cache/stableStringify.test.ts +43 -0
  171. package/tests/cache/stats.test.ts +146 -0
  172. package/tests/cache/volatile.test.ts +75 -0
  173. package/tests/smoke-pollution.mjs +78 -0
  174. package/tests/smoke-thinking.mjs +110 -0
  175. package/tests/smoke-tool-stream.mjs +69 -0
  176. package/tests/smoke-tool.mjs +117 -0
  177. package/.nwt/meta.json +0 -5
  178. package/dist/version.d.ts +0 -16
  179. package/dist/version.d.ts.map +0 -1
  180. package/dist/version.js +0 -16
  181. package/dist/version.js.map +0 -1
  182. package/src/version.ts +0 -16
@@ -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
+ }